Spaces:
Sleeping
Sleeping
| """ | |
| FDA compliance rule engine. | |
| Provides TRANSITION_TABLE, ComplianceResult, and check_fda_compliance. | |
| """ | |
| from __future__ import annotations | |
| from pydantic import BaseModel | |
| from models import ActionType, TrialAction, TrialLatentState | |
| from server.rules.prerequisite_rules import check_prerequisites | |
| # Maps each episode phase to the set of permitted ActionTypes. | |
| TRANSITION_TABLE: dict[str, set[ActionType]] = { | |
| "literature_review": { | |
| ActionType.SET_PRIMARY_ENDPOINT, | |
| ActionType.OBSERVE_SAFETY_SIGNAL, | |
| ActionType.ESTIMATE_EFFECT_SIZE, | |
| }, | |
| "hypothesis": { | |
| ActionType.SET_PRIMARY_ENDPOINT, | |
| ActionType.SET_SAMPLE_SIZE, | |
| ActionType.SET_INCLUSION_CRITERIA, | |
| ActionType.SET_EXCLUSION_CRITERIA, | |
| ActionType.ESTIMATE_EFFECT_SIZE, | |
| }, | |
| "design": { | |
| ActionType.SET_SAMPLE_SIZE, | |
| ActionType.SET_INCLUSION_CRITERIA, | |
| ActionType.SET_EXCLUSION_CRITERIA, | |
| ActionType.SET_DOSING_SCHEDULE, | |
| ActionType.SET_CONTROL_ARM, | |
| ActionType.SET_RANDOMIZATION_RATIO, | |
| ActionType.SET_BLINDING, | |
| ActionType.ADD_BIOMARKER_STRATIFICATION, | |
| ActionType.REQUEST_PROTOCOL_AMENDMENT, | |
| ActionType.ENROLL_PATIENTS, | |
| }, | |
| "enrollment": { | |
| ActionType.ENROLL_PATIENTS, | |
| ActionType.RUN_DOSE_ESCALATION, | |
| ActionType.OBSERVE_SAFETY_SIGNAL, | |
| ActionType.MODIFY_SAMPLE_SIZE, | |
| ActionType.ADD_BIOMARKER_STRATIFICATION, | |
| ActionType.REQUEST_PROTOCOL_AMENDMENT, | |
| ActionType.ESTIMATE_EFFECT_SIZE, | |
| ActionType.RUN_INTERIM_ANALYSIS, | |
| }, | |
| "monitoring": { | |
| ActionType.RUN_INTERIM_ANALYSIS, | |
| ActionType.OBSERVE_SAFETY_SIGNAL, | |
| ActionType.MODIFY_SAMPLE_SIZE, | |
| ActionType.REQUEST_PROTOCOL_AMENDMENT, | |
| ActionType.ESTIMATE_EFFECT_SIZE, | |
| ActionType.RUN_PRIMARY_ANALYSIS, | |
| }, | |
| "analysis": { | |
| ActionType.RUN_PRIMARY_ANALYSIS, | |
| ActionType.ESTIMATE_EFFECT_SIZE, | |
| ActionType.SYNTHESIZE_CONCLUSION, | |
| ActionType.SUBMIT_TO_FDA_REVIEW, | |
| }, | |
| "submission": { | |
| ActionType.SUBMIT_TO_FDA_REVIEW, | |
| ActionType.REQUEST_PROTOCOL_AMENDMENT, | |
| ActionType.SYNTHESIZE_CONCLUSION, | |
| }, | |
| } | |
| class ComplianceResult(BaseModel): | |
| """Result of an FDA compliance check.""" | |
| valid: bool | |
| violations: list[str] | |
| def check_fda_compliance( | |
| action: TrialAction, latent: TrialLatentState | |
| ) -> ComplianceResult: | |
| """Check whether *action* is compliant given the current *latent* state. | |
| Does NOT mutate *latent*. | |
| Returns a ComplianceResult with valid=True and empty violations when all | |
| checks pass, or valid=False with descriptive violation messages otherwise. | |
| """ | |
| violations: list[str] = [] | |
| # 1. Transition table check — episode_phase lives in latent state | |
| permitted = TRANSITION_TABLE.get(latent.episode_phase, set()) | |
| if action.action_type not in permitted: | |
| violations.append( | |
| f"Action '{action.action_type.value}' is not permitted in episode " | |
| f"phase '{latent.episode_phase}'. Permitted actions: " | |
| f"{sorted(a.value for a in permitted) if permitted else '[]'}." | |
| ) | |
| # 2. FDA hard rules | |
| if action.action_type == ActionType.SET_SAMPLE_SIZE: | |
| sample_size = action.parameters.get("sample_size") | |
| if sample_size is not None and sample_size < 30: | |
| violations.append( | |
| f"Sample size {sample_size} is below the regulatory minimum of 30." | |
| ) | |
| if action.action_type == ActionType.SUBMIT_TO_FDA_REVIEW: | |
| # Note: protocol_submitted is set BY this action in TransitionEngine, | |
| # so we only require phase_i_complete as a prerequisite. | |
| if not latent.phase_i_complete: | |
| violations.append( | |
| "Cannot submit to FDA review: Phase I has not been completed " | |
| "(phase_i_complete=False)." | |
| ) | |
| if action.action_type == ActionType.RUN_PRIMARY_ANALYSIS: | |
| if not latent.interim_complete: | |
| violations.append( | |
| "Cannot run primary analysis: interim analysis has not been completed " | |
| "(interim_complete=False)." | |
| ) | |
| if action.action_type == ActionType.RUN_INTERIM_ANALYSIS: | |
| if latent.patients_enrolled <= 0: | |
| violations.append( | |
| "Cannot run interim analysis: no patients are enrolled " | |
| "(patients_enrolled=0)." | |
| ) | |
| if action.action_type == ActionType.MODIFY_SAMPLE_SIZE: | |
| if ActionType.SET_SAMPLE_SIZE.value not in latent.action_history: | |
| violations.append( | |
| "Cannot modify sample size: SET_SAMPLE_SIZE has not been performed " | |
| "in this episode." | |
| ) | |
| if action.action_type == ActionType.SYNTHESIZE_CONCLUSION: | |
| if not latent.primary_analysis_complete: | |
| violations.append( | |
| "Cannot synthesize conclusion: primary analysis has not been run " | |
| "(primary_analysis_complete=False)." | |
| ) | |
| # 3. Prerequisite checks | |
| prerequisite_violations = check_prerequisites(action, latent) | |
| violations.extend(prerequisite_violations) | |
| return ComplianceResult(valid=len(violations) == 0, violations=violations) | |