AHP-Led Care Pathway
Physician-absent clinics as a swappable kit: capability grants, red-flag detector, lanes A/B/refer/blocked, default-OFF.
Status (2026-06-10): P0 scaffold SHIPPED (default-off on both planes, zero behavior change, sandbox-verified at
?target=AhpStationGate). P1+ are design. Read this before any doctorless-clinic / scope-of-practice / standing-order / remote-cosign / rural-deployment work.
1. What this is
A care-setting profile for clinics that are statutorily doctorless — Thai รพ.สต. (Tambon Health Promoting Hospitals) and Vietnamese trạm y tế xã (commune health stations) — where the operators are Allied Health Professionals (NP/พยาบาลเวชปฏิบัติ, RN, y sĩ, pharmacist, CHW) and the responsible physician is remote.
It is NOT a flag that lets AI (or anyone unlicensed) diagnose autonomously. It encodes the real-world legal model those facilities already run on:
- Lane A — the action falls inside a physician-SIGNED standing-order envelope → permitted
without per-case sign-off, but the write is CAPPED at
treated_per_standing_order(a treatment act under protocol — never diagnostic authorship). The signing physician is the legal author. - Lane B — outside the envelope → remote physician co-sign required before the action
becomes of-record (
diagnosed_with_cosign, …). - refer — the universal safe sink. Always permitted, never gated, works offline.
- blocked — default-deny: no active capability grant ⇒ the action does not exist for that role (refer remains).
A deterministic, client-local red-flag detector runs before every gate and can force
refer regardless of any grant. AI (Smart-Diagnosis / case-finder over the HIE corpus) is a
severable recommender adjunct — when it is down, the AHP still operates inside the
pre-signed envelope; it never authors an of-record state and never gates anything.
2. The three architectural ideas
- One context marker, stamped once. Registration stamps
ahp_context = { care_setting:'ahp-led', operating_ahp_role, supervising_physician_id, … }ontoencounter_journey_cache(migration20260610a); the orchestrator projects it ontodepartment_queuesrows so every station self-configures. An AHP-led encounter cannot exist without a named supervising physician. - One gate, five chokepoints. The same evaluator
(
evaluateAhpGate, wrapping the treatment-series 3-outcome shape) runs at screening-commit, registration-complete, assessment-commit, order-release, and disposition. Order of authority: red flags → refer-shortcut → default-deny → VN invariant → lane. - One capability table = all the scope.
ahp_capability_grant: one row per (role × jurisdiction × facility_tier × station × action) →{verdict auto|cosign|forbid, envelope_ref, drug_tier, acuity_ceiling, red_flag_pack, priority, version, status, author_physician_id}. Scope-of-practice, standing orders, drug tiers, and red-flag pack selection are all slices of this table. New country = market-pack seed rows, zero TypeScript — but see §9: each country’s rows are a regulated clinical algorithm under change control, not free config.
3. Station contracts
| Station | AHP does | Gate outcome | Fail-safe | Licensed author-of-record |
|---|---|---|---|---|
| Screening | CC + vitals + danger-sign checklist per signed triage protocol | Red-flag check runs FIRST, can hard-stop to EMS/refer; else commit per grant | Detector is client-local + synchronous; missing input ⇒ escalate; survives outage | Physician who signed the triage-protocol version |
| Registration | Binds encounter to supervising MD + consent; stamps ahp_context |
Completeness block: no AHP encounter without named MD + active grant + consent | No MD resolvable ⇒ degraded all-cosign mode; refer still works | The named supervising physician |
| Assessment | Working impression; ICD-of-record field locked in Lane A | in-envelope ⇒ treated_per_standing_order / out ⇒ cosign ⇒ diagnosed_with_cosign / red ⇒ refer |
Cosign channel down ⇒ cannot commit of-record, fails to refer | Lane A: envelope signer (capped); Lane B: cosigner |
| Orders | Places orders constrained to role drug tier + envelope whitelist | tier-covered ⇒ Lane A / restricted (abx, steroids, injectables) ⇒ cosign / forbidden ⇒ refer | Unresolved context ⇒ cosign-hold (NOT the platform’s nurse_ack default) |
Envelope signer (A) / cosigner (B) |
| Disposition | Discharge-with-followup, refer-up, teleconsult | Refer always-auto; out-of-envelope discharge ⇒ cosign | Referral works offline (printed/QR packet); source stays open until receiver ACCEPTS | The receiving MD who accepts the TransferRequest |
The non-negotiable thread: the AI/UI only ever advances proposed → recommended; a
transition into any of-record state is only ever written by a licensed-human event (a signed
envelope or a captured co-sign).
4. Data model (shipped in P0)
| Object | Migration | Notes |
|---|---|---|
ahp_capability_grant + ahp_gate_log (append-only, dual-plane) + encounter_journey_cache.ahp_context |
20260610a |
Default-deny; active requires author_physician_id (DB CHECK). Gate log has frontend/backend plane column. |
ahp_scope_envelope |
20260610b |
Renamed to dodge the standing_orders collision (20260517 = patient RRULE recurring orders, a different thing). signed requires signer + valid_to (envelopes expire). |
module_entitlements + module_enabled_server() / ahp_module_enabled() |
20260610c |
GENERIC backend half of module entitlement. No row ⇒ FALSE (regulated default-off, inverted from demo-safe). |
commit_diagnosis + commit_prescription policy_gates rows |
20260610d |
These trigger_actions did not exist anywhere. hard_stop TRUE, no override (the emergency path IS refer, which is ungated). Scoped care_settings:['ahp-led'] ⇒ physician-present flow never sees them. |
| Market-pack seeds | medos-thailand/seed-ahp-capability-grant.sql, new medos-vietnam/ pack |
Bilingual; ALL rows ship status='draft'. VN: cosign_required=TRUE everywhere, no auto outside disposition. |
Migrations are manual-apply per the repo’s deployment-status table; nothing runs until an operator applies them.
5. Kit packaging (the swappable module)
One module, two planes, engine-under-adapters. Manifest:
infrastructure/modules/ahp-care-pathway/module.json
(requires: [read-model-core], the 4 migrations, flags, tier enterprise,
postInstall seeds the module_entitlements row disabled — activation is a compliance
event, not an install side-effect).
web/packages/medical-kit/src/ahp-care-pathway/ ← @medical-kit/ahp-care-pathway
types.ts entitlement.ts index.ts
engine/ redFlagDetector.ts + redFlagPacks.ts (core-v1)
capabilityResolver.ts gateEvaluator.ts outputContracts.ts
__tests__/ (red-flag + gate property tests = the drift tripwire)
components/ StationGateModal.tsx CosignRequestModal.tsx (design-kit wrappers)
adapters/ ahpModule.ts (footer ModuleDefinition) registerOpdActions.ts (payload builders)
services/clinical/src/api/clinical/modules/ahp/ ← backend trust anchor (P0: UNWIRED)
ahpEntitlement.helper.ts ahpEnforcement.helper.ts redFlagDetector.shared.ts README.md
Placed inside medical-kit (the compounding/ / treatment-series-engine/ precedent) so
no tsconfig/vite change was needed; promotion to a Tier-1 @ever-medos/ahp-care-pathway-kit
package is one alias line later, content unchanged.
Entitlement — one logical flag, two planes, regulated default-OFF
| Plane | Predicate | Default | File |
|---|---|---|---|
| Frontend (advisory — decides what mounts) | ahpEnabled() = VITE_AHP_CARE_PATHWAY_ENABLED==='true' && module listed in VITE_ENABLED_MODULES && tier ⊇ enterprise |
OFF (explicit opt-in; inverts demo-safe) | entitlement.ts (self-contained mirror, fpa-dashboard precedent) |
| Backend (authoritative — decides what commits) | isAhpEntitled() (env AHP_CARE_PATHWAY_ENABLED==='true' + tier) && ahp_module_enabled() DB row |
OFF | ahpEntitlement.helper.ts + migration 20260610c |
Off-semantics: frontend off ⇒ the boot block in web/src/store/index.ts no-ops ⇒ no modals, no
payload builders — hosts byte-identical. Backend off ⇒ enforceAhpGate REFUSES of-record
writes on ahp-led encounters (FEATURE_NOT_ENTITLED) — it never silently falls back to
physician-present semantics. Schema persists (platform has no down-migrations) but is inert.
The central tier matrix also carries the key (FEATURE_PLANS['ahp.care-pathway']=['enterprise']).
Six-host integration matrix (verified against the real seams)
| Host | Seam | Kit mechanism | Fork? | P0 state |
|---|---|---|---|---|
| Footer global modal | ✅ moduleRegistry + registerDynamicModals() |
ahpCarePathwayModule.getModals() → 'ahp-care-pathway/StationGate', 'ahp-care-pathway/CosignRequest'; isActive() re-checked every render |
No | Shipped (guarded registration in store/index.ts) |
| OPD worklist | ✅ registerPayloadBuilder() (actions.ts:646) + role_worklists visibility |
builders 'ahp-station-gate', 'ahp-request-cosign'; actions authored as workflow JSON with visibility.path on care_setting |
No | Shipped (builders registered; inert until a workflow row references them) |
| Workflow-store | ✅ workflow_templates IS the registry |
AHP templates via Deployment-Profile template_set |
No | P3 (template authoring) |
| Workflow-editor | ❌ NodeComponentMap hardcoded; removal of a custom nodeType crashes saved canvases |
Fork avoided: gate expressed as policy_gates data on the existing PolicyGateDecision node + edge conditions |
No (avoided) | Gate rows shipped (20260610d) |
| Admin config | ❌ AdminRoutes.tsx hardcoded |
Near-term: grant editor via scheme-extensions slot / kit miniapp tab; right fix: generic registerAdminPage() registry |
One-time generic seam | P2 |
| IPD worklist | ❌ IpdRow.tsx orphaned from workflow-config |
One-time generic migration of /ipd/work-list onto ConfigDrivenTable (already the platform’s own backlog item; INVARIANT #0 fail-open preserved) |
One-time generic seam | P3+ |
6. The red-flag detector (the front-door safety net)
Net-new and deliberately not built on the existing CDS engine, which is structurally
disqualified for this job (verified): evaluate-cds-rules is numeric-vitals-only (skips
non-numeric inputs), non-blocking and failure-swallowed at the call site
(observation-handlers.ts .catch(()=>undefined)), qSOFA is seeded as three independent
amber toasts, and the vitals util getVitalSignValueState returns NORMAL for null input
(fail-open). The detector inverts all four properties:
- Presentation-based: chest pain → ACS, FAST → stroke, severe bleeding, anaphylaxis, active seizure, respiratory distress, eclampsia — these present with normal vitals.
- Aggregated scores: qSOFA ≥2 fires as ONE critical finding.
- Unknown ≠ normal: required vitals (RR/SBP/SpO2/mentation) and the explicit presentation
checklist must be assessed; missing ⇒
incomplete-screening⇒ escalate. IMCI danger-sign checklist for under-5s must be explicitly answered. - Fail-toward-escalation: an internal detector error returns
escalate+degraded:true. - Dual-plane by design: advisory copy in the kit, trust copy in
services/clinical/.../ahp/redFlagDetector.shared.ts. Same algorithm, duplicated on purpose (backend can’t import web/; frontend must work offline). The kit’s Jest vectors are the drift tripwire — change a rule in one copy ⇒ change both + the vectors in one commit. - Packs (
core-v1baked in; unknown pack id falls back, never empty) are selected per grant row, so jurisdictions can extend via data later. Thresholds/rules require clinical validation before P1 go-live — they are seed scaffolding, not validated medicine.
7. Backend enforcement (the trust anchor) — and the verified bug list
enforceAhpGate() re-makes the lane decision server-side. P0 ships it UNWIRED (zero
behavior change); wiring is the P2 hard gate. The three of-record write paths:
commitDiagnosis— net-new action (verified: no such handler exists in services/clinical today;createDiagnosiswrites a full ICD diagnosis with zero scope/authority check).orderRequestsign (services/medication) — wrap the existing sign path. Must also fix theactivateStandingOrders.tslaundering bug (verified): it hardcodesisDoctorAssign:trueand ignoresco_sign_required, writing doctor-attributed orders with no human author event — the most direct bypass of this entire legal model. ThepickOrderReleaseGateModefail-safe default isnurse_ack(policy-gate.service.ts:332), not cosign — AHP mode must override it to cosign-hold, regression-tested against existing order flows.transferRequest.accept— when the TransferRequest entity ships (transfer-request-system.md); todaytransfervalidates the status transition but not who accepts.
Also verified frontend-only today: usePolicyGate evaluates locally and nothing re-checks
gates on the write path — which is exactly why the policy-gate rows in 20260610d +
enforceAhpGate wiring are ship-blockers, not enhancements. Offline-outbox replays must
re-enter the same backend gate (no trusted client writes).
8. Invariants
- No active grant ⇒ blocked (default-deny). Refer is never gated, at any station, ever.
- Lane A is capped: a standing order delegates a treatment act, never diagnostic
authorship.
diagnosedrequires a physician event. - An AHP-led encounter cannot exist without a named supervising physician.
- Red flags beat grants: detector escalation overrides everything; missing input ⇒ escalate; detector error ⇒ escalate.
- Entitlement-off ⇒ refuse, never fall back: with the module off, AHP-scoped of-record writes 403; physician-present flow passes through untouched.
- VN invariant:
jurisdiction='VN'+verdict='auto'outside disposition ⇒ cosign, enforced in the backend regardless of row content, until a cited y sĩ scope determination exists. - Regulated data: grants/envelopes ship as
draft; activation requires a named physician; every gate decision lands in append-onlyahp_gate_logwith its plane. - Frontend is advisory: every lane decision is re-made server-side before commit.
- Per-intended-use device flags: screening-triage, AHP diagnosis-CDSS, AHP prescribing,
and patient self-screening are separately classifiable SaMD intended uses
(
AHP_DEVICE_INTENDED_USE); a refer-only clinic must not carry the diagnostic-CDSS registration burden. - Physician-present mode is byte-identical to a deployment that never had the kit.
9. Regulatory posture (TH / VN)
- The physician-signed standing order + referral pathway is the regulated clinical workflow; the AI is a severable recommender adjunct — that framing survives review; “the standing order is the AI’s legal protection” does not.
- Thai FDA’s SaMD/AI guidance classifies diagnostic CDSS as a registrable device and directs that AI shall not replace clinical judgment; physician-absent + non-physician operator + drives-management is a higher-risk intended use (likely TH Class 3 for the stroke/sepsis-scope screening). There is no US-style Non-Device-CDS safe harbor here.
- “New country = data rows” is true for engineering but each country pack is a device registration + change-control event: version-stamped, physician-approved seeds, not a config flip. Budget it that way commercially.
- Vietnam: keep
cosign_default=TRUE+ no-auto until a Vietnamese legal reviewer confirms y sĩ scope against Decree 96/2023 + Circular 28/2023 (and the 2028 capacity-exam transition). The backend invariant exists so a mis-seeded pack cannot fall back to autonomous practice. - Patient-facing self-triage (
patient-triagecare setting) is a separate SaMD with its own bar — out of scope for the clinician kit.
10. Rollout (safety + enforcement gate real-patient use)
| Phase | Scope | Gate |
|---|---|---|
| P0 — SHIPPED 2026-06-10 | Everything in §4–§5: module + 4 migrations + TH/VN seeds (draft) + kit engine/components/adapters + guarded boot registration + unwired backend helpers + sandbox target + tests | Zero behavior change; default-off both planes |
| P1 | Red-flag net to clinical grade: validated thresholds, per-jurisdiction packs as data, total-outage backstop (hardcoded local EMS number + printed/QR referral packet) | HARD GATE: clinical validation sign-off |
| P2 | Backend enforcement wired: commitDiagnosis (new action), orderRequest sign wrap (+ activateStandingOrders fix + cosign-hold default), DB entitlement join; envelope/grant admin surfaces (registerAdminPage seam or scheme-slot) + activation compliance checklist |
HARD GATE: server-side re-enforcement live; no real patient before this |
| P3 | Station wiring behind the flag: registration stamping, screening form w/ checklist, AHP workflow templates via Deployment Profile, orchestrator ahp_context projection |
|
| P4 | Disposition completion: TransferRequest wiring, cosign SLA + escalation roster via AcknowledgementRequest | |
| P5 | TH market activation: Class-3 SaMD classification opinion, named physician activation flow, pilot | Device registration |
| P6 | VN pack activation (cosign-forced) + named VN legal reviewer | Legal determination |
| P7 | Hardening: offline outbox re-gating, RUDS rules for AHP anomalies, voice/face attestation binding for cosign |
11. Open questions
- VN y sĩ scope determination (owner: VN legal reviewer) — blocks any VN
autoverdict. - TH device classification opinion for the screening intended-use (blocks P5).
- Deployment-Profile authoring UI (workflow-store gap — profiles are setup-time only today).
- Generic seams the platform team should own:
registerAdminPage(),registerNodeType()+ unknown-node fallback renderer, IPD→ConfigDrivenTable migration (all kit-agnostic). - Cosign SLA economics: who staffs the remote physician pool per district, and what the
cosign_sla_minutesdefaults should be per jurisdiction.
12. Origin
Synthesized from three multi-agent design passes (2026-06-10): the diagnosis-gate pass
(rural-ahp-gate-design), the end-to-end pathway pass (ahp-pathway-design), and the kit
packaging pass (ahp-kit-packaging-design) — 39 agents total, including 6 adversarial
verifiers whose corrections (numeric-only CDS, fail-open vitals util, frontend-only gates,
standing_orders collision, activateStandingOrders laundering, missing commit actions,
US-safe-harbor misframing, VN scope assumption) are folded into §6–§9 above.