Skip to content

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.py from the predicates, ordering, and signature below. Every row here becomes a fixture in tests/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 uses medical_status.completion instead.
  • LLM-emitted conversation_intent classification — 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. support deliberately 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 (if financial_readiness.completion == 0.0 AND patient asked about cost).
  • Exit conditions: Intent extractor flips procedure_identified=True → next turn resolves to procedure_identification or records_collection per their predicates.

3.3 procedure_identification

  • Purpose: Confirm specific procedure, laterality, anatomy, mechanism. Tighten intent before pushing for records.
  • Predicate: workflow_state.procedure_identified == True AND layer_state.intent_capture.completion < 1.0
  • Allowed addendums: procedure_clinical_facts/{procedure_slug} once case.procedure_code is set.
  • Exit conditions: intent_capture.completion == 1.0 → resolves to records_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 == True AND workflow_state.documents_uploaded == False AND layer_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=True OR medical_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 == True AND workflow_state.provider_selected == False
  • Allowed addendums: financial_options (cost questions are frequent here); insurance_handling if patient mentioned insurance.
  • Exit conditions: provider_selected=Trueconsent_capture. Backward to procedure_identification if patient pivots (parent §2.6 backward-move table).
  • Purpose: Capture HIPAA/GDPR/DPDP consent for record forwarding to selected providers.
  • Predicate: workflow_state.provider_selected == True AND workflow_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=Truemso_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_enabled gates whether the offer fires at all).
  • Predicate: workflow_state.consent_given == True AND workflow_state.mso_offered == False
  • Allowed addendums: mso_second_opinion (migrated from conversation_v4.yaml:mso_offer_addendum per parent §3.5.1 item 9).
  • Exit conditions: mso_offered=Truescheduling (only if also mso_accepted=True per parent §2.6 transient-state branch 2).

3.8 scheduling

  • Purpose: Pick MSO video slot / book consultation.
  • Predicate: workflow_state.mso_offered == True AND workflow_state.mso_accepted == True AND workflow_state.consultation_scheduled == False
  • Allowed addendums: None.
  • Exit conditions: consultation_scheduled=True → either stays here through reschedules or, once consultation_completed=True, advances to pre_travel.

3.9 pre_travel

  • Purpose: Logistics, visas, passport, travel readiness.
  • Predicate: workflow_state.consultation_completed == True AND workflow_state.treatment_started == False AND layer_state.logistics.completion < 1.0
  • Allowed addendums: post_travel_logistics (gates on patient passport status per parent §2.3).
  • Exit conditions: treatment_started=Truein_treatment.

3.10 in_treatment

  • Purpose: During-stay support. Coordinator handles most of this; the agent is light-touch.
  • Predicate: workflow_state.treatment_started == True AND workflow_state.treatment_completed == False
  • Allowed addendums: None.
  • Exit conditions: treatment_completed=Truerecovery_offer.

3.11 recovery_offer

  • Purpose: Post-op recovery facility offer (ADR-0018 §K).
  • Predicate: workflow_state.treatment_completed == True AND workflow_state.recovery_offered == False
  • Allowed addendums: None — recovery offer text is ADR-0018 §K mandatory clinical text in base.
  • Exit conditions: recovery_offered=True AND recovery_accepted=Truerecovery_followup. Decline → terminal (resolver falls through to support with reason recovery_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 input
  • proc_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_accepted
  • IC, 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.

# 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_followuprecovery_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_followup and in_treatment first 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.
  • discovery last among positive predicates. It's the broadest predicate (proc_id=F); placing it last ensures more-specific late-stage states are checked first even if proc_id somehow 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.