v6 Stage Resolver — Truth Table¶
Status: Draft (2026-05-12) — Phase 0 companion artifact for docs/specs/conversation-v6-feature.md. Blocks Phase 1 implementation of app/services/stage_resolver.py. DRAFT for SD review before Dr. Naidu clinical review (Phase 2a).
Parent spec: docs/specs/conversation-v6-feature.md — §2.5 stage definitions, §2.6 resolver summary. This doc is the full expansion of §2.6.
Tracking issue: #836.
This is a spec, not code. Phase 1 reads it and builds
stage_resolver.pyfrom the predicates, ordering, and signature below. Every row here becomes a fixture intests/test_stage_resolver_determinism.py.
1. Function signature¶
def resolve_stage(
case: Case, # Tenant-scoped Case ORM object
workflow_state: WorkflowState, # Pydantic view over case.workflow_state JSONB
layer_state: PatientLayerState, # Pydantic view over case.layer_state JSONB
recovery: RecoveryWorkflowState | None, # case.workflow_state["recovery"] sub-object
*,
now: datetime, # Injected for determinism — no time.now() inside
strict: bool = True, # Mirrors stage_resolver_strict flag (parent §2.6)
) -> StageResolution:
"""Pure function with respect to return value. No I/O, no LLM, no DB writes. Logging is allowed for malformed-input observability — see `stage_resolver.py` lines 146, 158, 168 for the three warn/debug paths.
Returns the active stage_id plus the reason code that matched.
On malformed input or no-rule-match, returns stage_id="support"
with reason="fallback:<cause>". Never raises user-visible errors.
"""
StageResolution is (stage_id: str, reason: str, matched_predicate_index: int). The reason string is what populates Telegram alert + Langfuse trace tags (parent §2.6 PHI-safe schema).
Lockstep guarantee (parent §2.6 "Determinism guarantee"): resolve_stage is a pure function over its arguments. No DB reads, no Redis reads, no datetime.now(), no Flagsmith reads, no exceptions raised. The strict flag changes what the caller does with the result (alert vs no alert) — it does not change the returned StageResolution. CI gate: tests/test_stage_resolver_determinism.py runs Hypothesis property tests asserting resolve_stage(x) == resolve_stage(x) over the full input domain and that mocking time.time(), random, and the DB session produces identical output.
2. Inputs¶
2.1 Case fields read¶
| Field | Source | Nullability | Valid values | Notes |
|---|---|---|---|---|
case.tenant_id |
app/models/case.py:Case.tenant_id |
NOT NULL | Clerk org id | Asserted; TenantIsolationViolation if missing (parent §2.6 tenant scoping) |
case.status |
app/models/case.py:Case.status |
NOT NULL | intake, matching, consent, quoting, scheduled, in_treatment, recovery, closed, cancelled |
Used only as a sanity cross-check; not a primary predicate input |
case.created_at |
app/models/case.py:Case.created_at |
NOT NULL | UTC datetime | Used only to compute case-age for the "new case" fallback |
case.case_role |
app/models/case.py:Case.case_role |
nullable | self, caregiver_family, caregiver_legal, helper, None |
Influences companion-aware guidance; does NOT change stage choice. Documented so reviewers know it's intentionally not in any predicate. |
case.procedure_code |
app/models/case.py:Case.procedure_code |
nullable | ICD-10 / internal code | Not a predicate input; informs the knowledge addendum selector, not the resolver |
2.2 WorkflowState booleans¶
Read from case.workflow_state JSONB. Each defaults to False if absent (the resolver MUST tolerate missing keys without raising).
| Boolean | Set by | Flips back? |
|---|---|---|
procedure_identified |
Intent extractor on procedure confirmation | Yes — pivot to different procedure |
documents_uploaded |
Document service after first successful FHIR-parsed upload | Rare; GDPR erasure can flip back |
match_results_shown |
Matching engine after shortlist surfaced in chat | No |
provider_selected |
Patient selects one+ providers in match_review card | Yes — patient deselects |
consent_given |
Consent service after HIPAA/GDPR/DPDP signature | Yes — patient revokes |
mso_offered |
Orchestrator after MSO card surfaced | No |
mso_accepted |
Patient accepts MSO card (or declines explicitly → False stays) | Yes |
consultation_scheduled |
MSO booking confirmed | Yes — reschedule keeps True; full cancel flips |
consultation_completed |
MSO video session ended + notes posted | No |
treatment_started |
Coordinator marks treatment start | No |
treatment_completed |
Coordinator marks discharge | No |
recovery_offered |
Recovery offer card surfaced (ADR-0018 §K) | No |
recovery_accepted |
Patient accepts recovery facility offer | Yes |
2.3 PatientLayerState completions¶
Read from case.layer_state JSONB. Each is {completion: float[0..1], confidence: float[0..1]}; missing keys default to {completion: 0.0, confidence: 0.0}.
| Layer | Meaning |
|---|---|
intent_capture |
Why the patient is here, what they want |
medical_status |
Conditions, meds, allergies, demographics |
travel_readiness |
Passport, visa, companion, dates |
logistics |
Accommodation, transit, post-op support |
financial_readiness |
Budget tier, funding source, insurance |
The resolver consumes completion only. confidence is reserved for future extractor-quality gating; explicitly not in any v6 predicate.
2.4 Recovery sub-state¶
Read from case.workflow_state["recovery"] via get_recovery_state(case) → RecoveryWorkflowState | None (app/models/recovery_state.py). When None, the recovery sub-flow has not started. When present, state is one of recovery_offered, recovery_accepted, recovery_in_progress, recovery_completed, recovery_declined.
case.recovery_milestones — list of milestone records (presence + ISO date). Empty [] is valid and triggers the transient-state branch in parent §2.6.
2.5 NOT inputs to the resolver¶
Explicitly excluded (reviewers should not propose adding):
- Conversation history / message content / NLU intent classification of latest user turn. Stage is state-driven, not utterance-driven. Conversation intent influences extractor routing in
triage_agent, not stage selection. prefer_consult_method— recorded on the case but does not branch the stage. Routing between MSO video and async consult is a knowledge-addendum decision, not a stage decision.ehr_completeness— a derived metric for matching-engine gates; the resolver usesmedical_status.completioninstead.- LLM-emitted
conversation_intentclassification — would create an LLM round-trip inside the resolver, violating purity. - Time-of-day / quiet hours — handled by the messaging layer, not stage selection.
- Tenant identity (beyond the assertion that
tenant_id is not None). Per-tenant stage customization is out of scope for v6.
3. Stage catalog¶
Twelve stages. Exactly one is active per turn. Order in this list = predicate evaluation order (first match wins; rationale in §5).
3.1 support (fallback / safety net)¶
- Purpose: Catch-all for malformed state or "I don't know what stage." Keeps voice intact, no proactive question, no advancement pressure.
- Predicate: Activates when no other stage's predicate matches OR any malformed-state condition triggers (see §4 fallback cases). Despite being listed first in §3, it is evaluated last (see §5 precedence) — the order in this catalog is logical, not evaluation order.
- Allowed addendums: None.
supportdeliberately runs base-only to minimize footgun surface during incident response. - Exit conditions: Any other stage's predicate becomes satisfied on the next turn (e.g., state corruption is repaired by a coordinator).
3.2 discovery¶
- Purpose: Open conversation — find out what procedure, why now, where they're starting from. Frictionless records-first pitch.
- Predicate:
workflow_state.procedure_identified == False - Allowed addendums:
financial_options(iffinancial_readiness.completion == 0.0AND patient asked about cost). - Exit conditions: Intent extractor flips
procedure_identified=True→ next turn resolves toprocedure_identificationorrecords_collectionper their predicates.
3.3 procedure_identification¶
- Purpose: Confirm specific procedure, laterality, anatomy, mechanism. Tighten intent before pushing for records.
- Predicate:
workflow_state.procedure_identified == TrueANDlayer_state.intent_capture.completion < 1.0 - Allowed addendums:
procedure_clinical_facts/{procedure_slug}oncecase.procedure_codeis set. - Exit conditions:
intent_capture.completion == 1.0→ resolves torecords_collection(typical) or onward.
3.4 records_collection¶
- Purpose: Get medical records uploaded for clinical context. Re-offer records upload on cadence (parent §4 Rule 2.4).
- Predicate:
workflow_state.procedure_identified == TrueANDworkflow_state.documents_uploaded == FalseANDlayer_state.medical_status.completion < 0.7 - Allowed addendums:
procedure_clinical_facts/{procedure_slug}(so the agent knows which records matter for this procedure). - Exit conditions:
documents_uploaded=TrueORmedical_status.completion >= 0.7→ next stage per remaining predicates.
3.5 match_review¶
- Purpose: Patient reviews matched providers, asks questions, picks a subset.
- Predicate:
workflow_state.match_results_shown == TrueANDworkflow_state.provider_selected == False - Allowed addendums:
financial_options(cost questions are frequent here);insurance_handlingif patient mentioned insurance. - Exit conditions:
provider_selected=True→consent_capture. Backward toprocedure_identificationif patient pivots (parent §2.6 backward-move table).
3.6 consent_capture¶
- Purpose: Capture HIPAA/GDPR/DPDP consent for record forwarding to selected providers.
- Predicate:
workflow_state.provider_selected == TrueANDworkflow_state.consent_given == False - Allowed addendums: None (consent text is base-prompt verbatim per ADR-0018 mandatory clinical text; addendums would dilute compliance-critical wording).
- Exit conditions:
consent_given=True→mso_offer. Patient revoke or decline → re-resolves on next turn.
3.7 mso_offer¶
- Purpose: Offer Medical Second Opinion video consult before booking (Flagsmith
mso_patient_offer_enabledgates whether the offer fires at all). - Predicate:
workflow_state.consent_given == TrueANDworkflow_state.mso_offered == False - Allowed addendums:
mso_second_opinion(migrated fromconversation_v4.yaml:mso_offer_addendumper parent §3.5.1 item 9). - Exit conditions:
mso_offered=True→scheduling(only if alsomso_accepted=Trueper parent §2.6 transient-state branch 2).
3.8 scheduling¶
- Purpose: Pick MSO video slot / book consultation.
- Predicate:
workflow_state.mso_offered == TrueANDworkflow_state.mso_accepted == TrueANDworkflow_state.consultation_scheduled == False - Allowed addendums: None.
- Exit conditions:
consultation_scheduled=True→ either stays here through reschedules or, onceconsultation_completed=True, advances topre_travel.
3.9 pre_travel¶
- Purpose: Logistics, visas, passport, travel readiness.
- Predicate:
workflow_state.consultation_completed == TrueANDworkflow_state.treatment_started == FalseANDlayer_state.logistics.completion < 1.0 - Allowed addendums:
post_travel_logistics(gates on patient passport status per parent §2.3). - Exit conditions:
treatment_started=True→in_treatment.
3.10 in_treatment¶
- Purpose: During-stay support. Coordinator handles most of this; the agent is light-touch.
- Predicate:
workflow_state.treatment_started == TrueANDworkflow_state.treatment_completed == False - Allowed addendums: None.
- Exit conditions:
treatment_completed=True→recovery_offer.
3.11 recovery_offer¶
- Purpose: Post-op recovery facility offer (ADR-0018 §K).
- Predicate:
workflow_state.treatment_completed == TrueANDworkflow_state.recovery_offered == False - Allowed addendums: None — recovery offer text is ADR-0018 §K mandatory clinical text in base.
- Exit conditions:
recovery_offered=TrueANDrecovery_accepted=True→recovery_followup. Decline → terminal (resolver falls through tosupportwith reasonrecovery_declined_no_next_stage, which is acceptable terminal state).
3.12 recovery_followup¶
- Purpose: Post-op milestone check-ins (ADR-0018 §K).
- Predicate:
workflow_state.recovery_accepted == True(milestone presence intentionally NOT required — see parent §2.6 transient-state branch 1) - Allowed addendums: None.
- Exit conditions: Recovery sub-flow ends. No further stage; case status moves to
closed.
4. Truth table¶
Columns:
case.status— informational, not a predicate inputproc_id=procedure_identified,docs=documents_uploaded,match=match_results_shown,sel=provider_selected,consent=consent_given,mso_off=mso_offered,mso_acc=mso_accepted,sched=consultation_scheduled,cons_done=consultation_completed,tx_start=treatment_started,tx_done=treatment_completed,rec_off=recovery_offered,rec_acc=recovery_acceptedIC,MS,LOG=intent_capture.completion,medical_status.completion,logistics.completion→ stage= resolver output
Rows are grouped by happy-path → branching → edge → fallback. Every row is a fixture seed.
4.1 Happy path (intake → records → consult prep → match → consent → routing → recovery)¶
| # | case.status | proc_id | docs | IC | MS | match | sel | consent | mso_off | mso_acc | sched | cons_done | LOG | tx_start | tx_done | rec_off | rec_acc | → stage |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| H1 | intake | F | F | 0.0 | 0.0 | F | F | F | F | F | F | F | 0.0 | F | F | F | F | discovery |
| H2 | intake | T | F | 0.4 | 0.0 | F | F | F | F | F | F | F | 0.0 | F | F | F | F | procedure_identification |
| H3 | intake | T | F | 1.0 | 0.2 | F | F | F | F | F | F | F | 0.0 | F | F | F | F | records_collection |
| H4 | intake | T | T | 1.0 | 0.8 | F | F | F | F | F | F | F | 0.0 | F | F | F | F | support (no rule fires — pre-match handoff window; see §4.5 row F3) |
| H5 | matching | T | T | 1.0 | 0.9 | T | F | F | F | F | F | F | 0.0 | F | F | F | F | match_review |
| H6 | consent | T | T | 1.0 | 0.9 | T | T | F | F | F | F | F | 0.0 | F | F | F | F | consent_capture |
| H7 | consent | T | T | 1.0 | 0.9 | T | T | T | F | F | F | F | 0.0 | F | F | F | F | mso_offer |
| H8 | quoting | T | T | 1.0 | 0.9 | T | T | T | T | T | F | F | 0.0 | F | F | F | F | scheduling |
| H9 | scheduled | T | T | 1.0 | 0.9 | T | T | T | T | T | T | F | 0.0 | F | F | F | F | scheduling (stays until cons_done=T) |
| H10 | scheduled | T | T | 1.0 | 0.9 | T | T | T | T | T | T | T | 0.4 | F | F | F | F | pre_travel |
| H11 | scheduled | T | T | 1.0 | 0.9 | T | T | T | T | T | T | T | 1.0 | F | F | F | F | support (logistics complete but treatment not yet started — handoff window; see §4.5 row F4) |
| H12 | in_treatment | T | T | 1.0 | 0.9 | T | T | T | T | T | T | T | 1.0 | T | F | F | F | in_treatment |
| H13 | in_treatment | T | T | 1.0 | 0.9 | T | T | T | T | T | T | T | 1.0 | T | T | F | F | recovery_offer |
| H14 | recovery | T | T | 1.0 | 0.9 | T | T | T | T | T | T | T | 1.0 | T | T | T | T | recovery_followup |
4.2 Branching — caregiver vs self, fast-track vs full discovery, MSO-declined¶
Per §2.1: case_role is intentionally NOT a predicate input. Caregiver vs self rows produce IDENTICAL resolver output; the difference is in stage guidance (companion-aware language injected from stages.yaml), not stage choice. Listed here so reviewers can verify the identity.
| # | Scenario | case_role | proc_id | docs | IC | MS | (other flags as H-row) | → stage |
|---|---|---|---|---|---|---|---|---|
| B1 | Self, fast-tracker (uploads records first turn) | self | T | T | 1.0 | 0.85 | match=T, sel=F | match_review |
| B2 | Caregiver, fast-tracker (same state as B1) | caregiver_family | T | T | 1.0 | 0.85 | match=T, sel=F | match_review (identical to B1 — see note above) |
| B3 | Helper without consent flag — fast-track blocked | helper | T | T | 1.0 | 0.85 | match=F (matching engine refused to surface results until consent_token present) | records_collection (match_results_shown=F, so falls back to records — see §5 precedence) |
| B4 | MSO declined — skip to pre_travel via consultation_completed | self | T | T | 1.0 | 0.9 | sel=T, consent=T, mso_off=T, mso_acc=F, sched=F, cons_done=F | support (per parent §2.6 transient-state branch 2: scheduling requires mso_acc=T; no other stage matches until coordinator advances state) |
| B5 | MSO declined, coordinator advances treatment manually | self | T | T | 1.0 | 0.9 | sel=T, consent=T, mso_off=T, mso_acc=F, sched=F, cons_done=T, LOG=0.3 | pre_travel |
| B6 | MSO-routed, accepted, slot picked | self | T | T | 1.0 | 0.9 | sel=T, consent=T, mso_off=T, mso_acc=T, sched=T | scheduling (stays until consultation_completed) |
| B7 | Direct (no MSO offered — flag off for tenant) | self | T | T | 1.0 | 0.9 | sel=T, consent=T, mso_off=F | mso_offer (predicate is consent=T AND mso_off=F; the flag governs whether the offer addendum loads inside the stage, not the stage choice. The stage still resolves to mso_offer and the prompt collapses to base+stage without the addendum when mso_patient_offer_enabled=False.) |
4.3 Edge cases¶
| # | Scenario | Inputs | → stage | Reason |
|---|---|---|---|---|
| E1 | No consent yet, but patient asks to schedule | proc_id=T, docs=T, match=T, sel=T, consent=F, mso_off=F (patient utterance ignored) | consent_capture |
Resolver is state-driven, not utterance-driven. Patient pressure does not advance state. |
| E2 | Missing medical_status data — proc_id=T, docs=F, MS=0.0 |
proc_id=T, docs=F, IC=1.0, MS=0.0 | records_collection |
Predicate matches (MS < 0.7). |
| E3 | documents_uploaded=T but medical_status.completion < 0.7 (uploads parsed poorly) |
proc_id=T, docs=T, MS=0.4 | support (no stage matches; records_collection requires docs=F) |
Reviewer flag: this is a real production hole. Q-C1 in §6. |
| E4 | Pivot: patient mid-flow says "actually it's the other knee" — intent extractor drops IC to 0.6 | proc_id=T, docs=T, IC=0.6, MS=0.9, match=F | procedure_identification |
Backward move (parent §2.6 backward-move table). Intentional. |
| E5 | Consent revoked post-MSO-offer | sel=T, consent=F (was T, flipped), mso_off=T | consent_capture |
Backward move; intentional. Forces re-capture. |
| E6 | Post-procedure recovery vs in-procedure — treatment_started=T, treatment_completed=F |
tx_start=T, tx_done=F | in_treatment |
Distinguished from recovery_offer (which requires tx_done=T). |
| E7 | Recovery accepted, milestones empty (cron lag window) | rec_acc=T, recovery_milestones=[] | recovery_followup |
Per parent §2.6 transient-state branch 1. Stage guidance is "acknowledge acceptance, wait for first milestone." |
| E8 | Recovery declined (terminal) | tx_done=T, rec_off=T, rec_acc=F | support with reason recovery_declined_no_next_stage |
Acceptable terminal — no rule should advance the patient further. |
| E9 | Multi-procedure patient on second case (case.case_number different) |
proc_id=F (new case fresh) | discovery |
Per-case resolution; cross-case state is not consulted. |
| E10 | New case created in last 60s, no extractor has run yet (workflow_state={}, layer_state=None) |
All flags F, layer_state None | discovery |
The resolver tolerates layer_state=None by defaulting completions to 0.0. |
| E11 | Coordinator pre-selected provider before matching surfaced (impossible-looking but legitimate) | proc_id=T, docs=T, match=F, sel=T, consent=F | consent_capture |
Per parent §2.6 transient-state branch 3. consent_capture predicate is satisfied by sel=T AND consent=F; the absence of match_results_shown does not block it. |
| E12 | Workflow flag corruption — consent_given=T but procedure_identified=F |
proc_id=F, consent=T | discovery |
discovery matches first (proc_id=F). The consent=T flag is honored as latent state, not unwound. If clinical review wants this to alert, see Q-C2 in §6. |
4.4 Caregiver-axis tertiary check¶
These rows exist so the property test can assert resolve_stage is invariant under case_role swaps.
| # | case_role | Same workflow inputs as | → stage | Equals |
|---|---|---|---|---|
| C1 | self | H1 | discovery |
C2 |
| C2 | caregiver_family | H1 | discovery |
C1 |
| C3 | caregiver_legal | H7 | mso_offer |
C4 |
| C4 | helper | H7 | mso_offer |
C3 |
4.5 Fallback / error rows¶
| # | Cause | → stage | reason string | Telegram alert? |
|---|---|---|---|---|
| F1 | case.tenant_id is None |
(raises TenantIsolationViolation — caller catches) |
n/a | No (caller's exception handler decides) |
| F2 | case.workflow_state is not a dict (e.g., JSON corruption returns None or a list) |
support |
fallback:malformed_workflow_state |
Yes (strict=True) |
| F3 | proc_id=T, docs=T, MS>=0.7, match=F — records done but matching engine has not surfaced results yet |
support |
fallback:awaiting_match_engine |
No — known transient handoff window, gated by Q-O1 |
| F4 | cons_done=T, tx_start=F, LOG=1.0 — pre-travel complete but treatment not yet flipped |
support |
fallback:awaiting_treatment_start |
No — known transient handoff window |
| F5 | proc_id=T, docs=T, MS=0.4 (E3 above) |
support |
fallback:records_uploaded_but_extraction_incomplete |
Yes — possible extractor bug |
| F6 | layer_state dict has unexpected keys / wrong shape |
support |
fallback:malformed_layer_state |
Yes |
| F7 | Recovery sub-state recovery_in_progress but rec_acc=F |
support |
fallback:recovery_substate_inconsistent |
Yes |
| F8 | All predicates evaluate False AND no malformed condition | support |
fallback:no_predicate_matched |
Yes |
| F9 | recovery_followup → recovery_offer backward (rec_acc flipped T→F) |
recovery_offer |
(normal resolution per backward-move table) | Yes — parent §2.6 marks this a bug signal; alert + investigate |
5. Precedence¶
Predicates are evaluated in this exact order. First match wins. support is the catch-all; it does not appear in the ordered list because it has no positive predicate — it returns when nothing else matches OR when a malformed-state condition fires.
1. malformed-state checks (returns support immediately, alerts per F1-F7)
2. recovery_followup (rec_acc=T)
3. recovery_offer (tx_done=T AND rec_off=F)
4. in_treatment (tx_start=T AND tx_done=F)
5. pre_travel (cons_done=T AND tx_start=F AND LOG<1.0)
6. scheduling (mso_off=T AND mso_acc=T AND sched=F)
— note: requires mso_acc=T per parent §2.6 transient-state branch 2
7. mso_offer (consent=T AND mso_off=F)
8. consent_capture (sel=T AND consent=F)
9. match_review (match=T AND sel=F)
10. procedure_identification (proc_id=T AND IC<1.0)
11. records_collection (proc_id=T AND docs=F AND MS<0.7)
12. discovery (proc_id=F)
13. support (no rule matched)
Why this order (clinical justification per parent §2.6 rule-order):
- Late-stage states first. Once a patient is in treatment or recovery, no earlier predicate should reclaim them. Evaluating
recovery_followupandin_treatmentfirst guarantees this. - Procedure_identification before records_collection. When
proc_id=T AND docs=F AND IC<1.0, both #10 and #11 could match. Clinical rationale (parent §2.6): establish why before pushing for documents. Dr. Naidu re-confirms this ordering in Phase 2a. discoverylast among positive predicates. It's the broadest predicate (proc_id=F); placing it last ensures more-specific late-stage states are checked first even ifproc_idsomehow remained False due to a flag-flip race.
Determinism corollary: No two predicates can simultaneously match for the same input with this ordering, because the first-match rule collapses any overlap into a single deterministic outcome. The property test (tests/test_stage_resolver_determinism.py) verifies the implication: for every input in a Hypothesis-generated sample of ~10k cases, resolve_stage(x) == resolve_stage(x) and the matched predicate index is stable across runs.
6. Open questions for Dr. Naidu (Phase 2a clinical review)¶
These shape the predicates but are not decisions this doc makes. Phase 1 implements the spec as written; Phase 2a clinical review may flip any of these, in which case the change happens in one place (this table + the resolver function) with no downstream ripple.
| # | Question | Default in this draft | Where it bites |
|---|---|---|---|
| Q-C1 | What counts as "ready for matching"? The current predicate uses medical_status.completion >= 0.7 as the gate between records_collection and match_review. Is 0.7 the right threshold? Should it vary by procedure complexity (e.g., oncology might require 0.9, ortho 0.6)? |
0.7 flat for all procedures | §3.4 exit condition; E2, E3, F5 |
| Q-C2 | When workflow flags are inconsistent (e.g., E12: consent=T but proc_id=F), should the resolver alert? Currently we tolerate latent state. |
No alert — discovery wins |
E12, parent §2.6 transient-state branch 3 |
| Q-C3 | Should procedure_identification precede records_collection (current order) or the reverse? Parent §2.6 raised this; defaulted to clinical "why before what." Dr. Naidu re-confirms. |
procedure_identification first |
§5 precedence rule #10/#11 |
| Q-C4 | When MSO is declined (mso_off=T, mso_acc=F), where does the patient go? Currently the resolver returns support until coordinator advances cons_done=T (B4 row). Is that acceptable, or should there be an explicit mso_declined_followup stage to keep the conversation alive? |
support fallback (no new stage) |
B4, parent §2.6 transient-state branch 2 |
| Q-C5 | Should recovery_offer decline (tx_done=T, rec_off=T, rec_acc=F) terminate the conversation (current behavior — support) or have a "post-decline check-in" stage? ADR-0018 §K is silent. |
support terminal (E8) |
§3.11 exit; E8 |
| Q-C6 | The medical_status.completion < 0.7 gate may not be tight enough for procedures where document extraction quality varies. Should confidence enter the predicate? Currently §2.3 says confidence is reserved for v7. |
confidence ignored |
§2.3, Q-C1 |
| Q-C7 | Recovery-accepted-with-empty-milestones (E7) — clinical preference: should the stage be silent until cron seeds the first milestone, or proactively reassure the patient? Currently the stage guidance is "acknowledge acceptance, wait." | Acknowledge + wait | E7, parent §2.6 transient-state branch 1 |
| Q-O1 | F3 (fallback:awaiting_match_engine) is a known 0-N-minute transient window between records-complete and match-engine-surfacing. Should the resolver have a dedicated awaiting_match stage instead of falling back to support? Operational, not clinical — flagged for SD review. |
support |
F3 |
| Q-O2 | The case.case_role field is read but does not affect stage choice (parent §2.5 notes companion-aware guidance lives in stages.yaml, not predicates). Reviewer should confirm this remains the boundary — caregiver-vs-self affects voice, not flow. |
Excluded from predicates | §2.1, B1/B2, C1-C4 |
7. CI gates this doc requires¶
| Gate | What it asserts | File |
|---|---|---|
| Determinism property test | resolve_stage(x) == resolve_stage(x) over Hypothesis-generated inputs; pure function |
tests/test_stage_resolver_determinism.py (NEW, Phase 1) |
| Truth-table fixture coverage | Every row H1-H14, B1-B7, C1-C4, E1-E12, F1-F9 is a fixture seed | same |
| Predicate mutual-exclusion | For every fixture, exactly one positive predicate matches OR a fallback fires | same |
| PHI-safe alert payload | Telegram alert constructed for F2/F5/F6/F7/F8/F9 contains zero patient-identifying fields | tests/test_stage_resolver_alert_pii_safe.py (parent §2.6) |
| Tenant assertion | resolve_stage raises TenantIsolationViolation when case.tenant_id is None |
same as determinism test |
case_role invariance |
Swapping case_role across the four valid values produces identical StageResolution for the same workflow inputs |
new test, derived from C1-C4 rows |
8. Relationship to parent spec¶
This doc EXPANDS conversation-v6-feature.md §2.6. Shared definitions:
- Stage list, predicate summary table → parent §2.6
- Tenant-scoping rule → parent §2.6 ("Tenant scoping")
- Telegram alert PHI-safe schema → parent §2.6 ("Telegram alert payload — strict PHI-safe schema")
- Transient-state handling rationale → parent §2.6 ("Transient-state handling")
- Backward stage moves → parent §2.6 ("Backward stage moves")
- Flag dependency matrix governing whether v6 runs at all → parent §3.5.1 item 4
This doc adds: full enumerated truth table, function signature, per-stage exit conditions, allowed-addendum-per-stage map, fallback-row alert wiring, and the open-question list for Phase 2a.