Policy Gate Override Protocol
Break-glass override-with-reason: hard_stop gates + override audit table + extended reason-code enum.
Status: Draft (2026-05-27). Major upgrade — not yet implemented. Replaces: The unbuilt
override_jsoncolumn onpolicy_gatesand the deferred Phase 2 items inpolicy-gates.md§9. Generalizes: The custody-handoff override design inhospital-movement-architecture.md§6 so it applies to every gate type, not just mover handoffs. Companion: Tie-in to the existingOverrideReasonCodeenum used by CDS (seeevent-contract.ts).
0. TL;DR
| Today | Proposed |
|---|---|
policy_gates.action_json.type ∈ {block, warn, require_override}; require_override never implemented |
Override-with-reason is a first-class outcome of every blocking gate; require_override becomes block_with_override with a structured reason capture |
policy_gates.override_json column exists but is unused |
Carries override authorization + reason whitelist + downstream consequences |
| No audit table for overrides | New policy_gate_overrides append-only table; surfaced to compliance review queue |
| Each gate is silently bypassable via direct API call (frontend-only enforcement) | Override is a recorded, audited, optionally-policy-driven event that downstream consumers (billing, alerts, compliance) can react to |
OverrideReasonCode enum exists for CDS only |
Extended + reused across policy gates, CDS rules, drug-interaction conflicts, allergy-vs-order conflicts — one canonical reason taxonomy |
TransferRequest’s transfer_accept gate says “unless override” but has no override data shape |
Override flow is the same component / hook / table everywhere |
| Staff bypass gates off-the-books when clinical reality forces it | Override is the in-band path; the off-book path is closed |
The headline change: a hard stop is the rare exception; structured override-with-reason is the default for any gate touching clinical reality.
1. Why this exists
1.1 The clinical-reality problem
Every gate this codebase ships hits the same wall eventually:
| Gate | Block UX today | What staff actually do when blocked |
|---|---|---|
| Pathology specimen collection (unpaid) | “กรุณาชำระเงิน…” replaces button | Walk the sample to the lab without scanning, ask cashier to backfill |
| Transfer accept (receiving ward full) | (would block) | Phone the charge nurse, agree verbally, type in the bed assignment elsewhere |
| Specimen biohazard transport (no BSL3-certified runner) | (would block) | Hand the sample to whichever runner is nearest, sign for it later |
| Pharmacy max-dose / interaction (CDS rule fires) | Inline warning | Click “Confirm anyway” with no captured reason |
| IPD admission (no beds available) | “Ward full” toast | Boarder in ER, admission flag flipped after-the-fact |
In every case the clinical action is correct; the software model is wrong. A hard stop that staff cannot legitimately bypass forces the bypass to happen out-of-band — and now we have a compliance violation AND a blind spot in tracking.
The right primitive is not “block” — it’s “block-by-default, but allow a recorded override with structured reason, optional authorization escalation, and downstream consequences.”
1.2 What’s already partially there
policy_gates.action_json.typehas therequire_overrideenum value — it was reserved for this. Never implemented.policy_gates.override_jsoncolumn exists. Never populated.- CDS has the
OverrideReasonCodeenum on the alert side (event-contract.ts). Not joined to gates. - The custody-handoff doc (hospital-movement-architecture.md §6) sketches the
policy_gate_overridesaudit table — but only for handoff/mover scope.
This document promotes that sketch into a generic protocol and specifies the implementation across every surface.
2. Design principles
- Hard-stop is per-gate operator policy, not a code decision. Defaults to override-with-reason because over-blocking → off-book moves, and we have no way to audit those.
- Override is structured. Reason is a typed code from a controlled vocabulary, not free-text.
OTHERallowed but requires free-text supplement. - Override is audited. Append-only row in
policy_gate_overrides. Joinable to billing, compliance, and downstream side-effect rails. - Override is sometimes authorized. Per-gate, certain overrides require a higher-privilege user (charge nurse, attending, safety officer). The gate row carries the required role/permission.
- Override has consequences. Downstream consumers (billing rail, alert fan-out, runner bonus calc, compliance digest, capacity-pressure metric) read the override row and adjust behavior. The gate doesn’t decide consequences — it makes them addressable.
- One reason taxonomy. Same
OverrideReasonCodeenum used by CDS, drug-interaction, allergy-vs-order, policy gates, custody handoffs. - Scope is bounded. An override applies to a single triggering action, not a session. Re-triggering the same gate re-prompts.
- The override is reversible until consequences settle. Within a short window (per-gate config, default 5 min), the override can be retracted, which removes the row and re-blocks the action.
3. Schema changes
3.1 Migration: policy_gates column additions
ALTER TABLE policy_gates
-- Hard stop: if true, no override allowed. Default false (override-with-reason permitted).
ADD COLUMN IF NOT EXISTS hard_stop BOOLEAN NOT NULL DEFAULT FALSE,
-- Reason code whitelist: if non-null, override MUST use one of these codes.
-- If null, any code from the OverrideReasonCode enum is permitted.
ADD COLUMN IF NOT EXISTS override_reason_codes TEXT[] DEFAULT NULL,
-- Authorization: who can override this gate?
-- Schema: { "roles": ["charge_nurse", "attending"], "permissions": ["override.transfer_accept"] }
-- If null, any authenticated user can override.
ADD COLUMN IF NOT EXISTS override_authorization JSONB DEFAULT NULL,
-- Reversal window in seconds. Default 300 (5 min). 0 = irreversible.
ADD COLUMN IF NOT EXISTS override_reversal_window_seconds INT NOT NULL DEFAULT 300,
-- Alert recipients on override (notified via acknowledgement_requests).
-- Schema: { "roles": ["safety_officer"], "user_ids": [...], "department_ids": [...] }
ADD COLUMN IF NOT EXISTS override_alert_recipients JSONB DEFAULT NULL,
-- Downstream consequence hints (read by billing rail, runner-bonus calc, etc.)
-- Schema: { "billing_flag": "retro_collection", "suppress_runner_bonus": true,
-- "capacity_pressure_metric": "ward_overcap" }
ADD COLUMN IF NOT EXISTS override_consequences JSONB DEFAULT NULL;
Note: the existing override_json column is retired and replaced by the four scalar/JSONB columns above. Any rows that populated override_json (currently zero in practice) get migrated by a one-shot script in the same migration file.
3.2 Migration: new table policy_gate_overrides
CREATE TABLE IF NOT EXISTS policy_gate_overrides (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-- Which gate fired
gate_id UUID NOT NULL REFERENCES policy_gates(id),
gate_trigger TEXT NOT NULL, -- denormalized for fast query
gate_name TEXT NOT NULL, -- denormalized for compliance UI
-- What was being attempted
subject_resource TEXT NOT NULL, -- 'TransferRequest' | 'Task' | 'LabRequest' | 'MedicationRequest' | ...
subject_id TEXT NOT NULL, -- the entity overridden against
triggering_action TEXT NOT NULL, -- e.g. 'collect_specimen', 'accept_transfer'
-- Patient context (nullable for facility-level gates)
encounter_id UUID,
patient_id UUID,
-- Who overrode
override_by UUID NOT NULL,
override_by_role TEXT NOT NULL, -- snapshot of role at time of override
override_authorized_by UUID, -- if escalated authorization, who approved
override_authorized_role TEXT,
-- Why
override_reason_code TEXT NOT NULL, -- from OverrideReasonCode enum
override_reason_text TEXT, -- mandatory if code == 'OTHER'
-- Context
gate_eval_inputs JSONB NOT NULL DEFAULT '{}',-- the context that was evaluated (for replay/audit)
gate_eval_result JSONB NOT NULL DEFAULT '{}',-- which predicate failed, severity, etc.
override_metadata JSONB NOT NULL DEFAULT '{}',-- gate-specific extra context
-- Lifecycle
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now(),
reversed_at TIMESTAMPTZ, -- non-null if retracted within reversal window
reversed_by UUID,
reversed_reason TEXT,
-- Downstream sync
alert_dispatched_at TIMESTAMPTZ, -- when override_alert_recipients were notified
compliance_reviewed_at TIMESTAMPTZ, -- when compliance closed the case
compliance_reviewed_by UUID,
compliance_disposition TEXT, -- 'acceptable' | 'flagged' | 'training_needed' | 'sanction'
compliance_notes TEXT
);
CREATE INDEX idx_pgo_gate_trigger ON policy_gate_overrides (gate_trigger, occurred_at DESC);
CREATE INDEX idx_pgo_override_by ON policy_gate_overrides (override_by, occurred_at DESC);
CREATE INDEX idx_pgo_patient ON policy_gate_overrides (patient_id) WHERE patient_id IS NOT NULL;
CREATE INDEX idx_pgo_subject ON policy_gate_overrides (subject_resource, subject_id);
CREATE INDEX idx_pgo_pending_review ON policy_gate_overrides (occurred_at DESC)
WHERE compliance_reviewed_at IS NULL;
CREATE INDEX idx_pgo_unreversed ON policy_gate_overrides (gate_id, occurred_at DESC)
WHERE reversed_at IS NULL;
ALTER PUBLICATION supabase_realtime ADD TABLE policy_gate_overrides;
3.3 Action JSON extension
Existing action_json.type values stay the same name-wise but with sharper semantics:
type value |
Meaning | Override behavior |
|---|---|---|
block |
Block this action. If policy_gates.hard_stop = true, no override. Otherwise default override-with-reason. |
Determined by hard_stop column, not action type |
warn |
Show warning; do not block. | N/A — no override needed |
require_override |
Block AND require explicit override even if the predicate passes. Used for “always confirm before doing X” gates (e.g. ordering controlled substance, ESI 1 actions). | Always requires override |
This is a small change: block + hard_stop=true is “true hard stop”; block + hard_stop=false is “override-with-reason.” require_override becomes “always-prompt-even-on-pass” rather than the unbuilt third state.
3.4 Reason code enum extension
Existing in event-contract.ts:
export type OverrideReasonCode =
| 'CLINICALLY_JUSTIFIED'
| 'BENEFITS_OUTWEIGH_RISKS'
| 'MONITORING_IN_PLACE'
| 'SENIOR_APPROVED'
| 'OTHER';
Extended to:
export type OverrideReasonCode =
// Existing (CDS / clinical)
| 'CLINICALLY_JUSTIFIED'
| 'BENEFITS_OUTWEIGH_RISKS'
| 'MONITORING_IN_PLACE'
| 'SENIOR_APPROVED'
// Handoff / logistics
| 'TIME_CRITICAL'
| 'NO_QUALIFIED_MOVER_AVAILABLE'
| 'PATIENT_REFUSED_WAIT'
| 'EMERGENCY_OVERRIDE'
// Capacity / operations
| 'NO_ALTERNATIVE_AVAILABLE' // ward full but patient needs admission
| 'BOARDING_AUTHORIZED' // ER boarder pending bed
| 'CAPACITY_OVERRIDE_APPROVED' // bed-manager OK
| 'PHYSICIAN_DIRECTED' // attending wrote the order despite the warning
// Financial / payor
| 'EMERGENCY_BYPASS_PAYMENT' // ER unpaid sample collection
| 'INSURANCE_AUTHORIZED' // preauth verbally confirmed, paperwork pending
| 'DEPOSIT_WAIVED' // VIP / staff / charity
| 'RETRO_BILLING_ACCEPTED' // proceed now, bill later
// System / data
| 'SYSTEM_ERROR_BYPASS' // gate eval inputs unreliable
| 'DATA_CORRECTION_PENDING' // chart fix in progress
| 'DUPLICATE_ENTRY_AVOIDANCE' // override to prevent double-recording
// Catch-all
| 'OTHER'; // free-text required
Reason codes are grouped by domain in the picker UI so the override modal only shows relevant ones based on policy_gates.override_reason_codes whitelist (or the gate’s category field, if added later).
4. Runtime evaluation extension
4.1 evaluateGates() return shape
Today:
{ blocked: boolean, message: string, severity: string, matchedGate: PolicyGate }
After:
{
blocked: boolean,
message: string,
severity: 'error' | 'warning' | 'info',
matchedGate: PolicyGate,
// NEW
hardStop: boolean, // matched gate's hard_stop flag
overrideAllowed: boolean, // !hardStop && blocked
overrideReasonCodes: OverrideReasonCode[], // whitelist for this gate, or full enum if null
overrideAuthorization: { // null if any user can override
roles?: string[];
permissions?: string[];
} | null,
alertRecipientsOnOverride: { // for downstream consequences preview
roles?: string[];
user_ids?: string[];
department_ids?: string[];
} | null,
}
4.2 recordOverride() action
New service action in web/src/services/policy-gate.service.ts:
async recordOverride(input: {
gateId: string;
gateTrigger: string;
subjectResource: string;
subjectId: string;
triggeringAction: string;
encounterId?: string;
patientId?: string;
reasonCode: OverrideReasonCode;
reasonText?: string;
evalInputs: object;
evalResult: object;
metadata?: object;
}): Promise<PolicyGateOverride> {
// 1. Authorization check (caller must hold the required role/permission)
// 2. Whitelist check (reasonCode must be in override_reason_codes if set)
// 3. If reasonCode === 'OTHER', reasonText is required
// 4. INSERT policy_gate_overrides row
// 5. Fire acknowledgement_requests to alert recipients if configured
// 6. Return the row (UI shows confirmation, attaches row id to the subject)
}
This action is the only path to recording an override. The frontend never bypasses gates silently.
4.3 Reversal
async reverseOverride(overrideId: string, reason: string): Promise<void> {
// Within reversal_window_seconds from occurred_at: mark reversed_at, reversed_by, reversed_reason.
// Past the window: requires admin permission + audit trail.
// Reversing an override should also undo downstream side effects where possible
// (cancel the dispatched alert, retract the billing flag, etc.) — handled by
// a reversal-event consumer per gate's override_consequences config.
}
5. Frontend UX
5.1 The override modal — one component, all gates
New shared component: web/src/common/components/policy-gate/OverrideReasonModal.tsx
<OverrideReasonModal
gate={matchedGate}
evalResult={policyResult}
subjectResource="TransferRequest"
subjectId={req.id}
triggeringAction="accept_transfer"
onConfirm={(override) => { /* row recorded, proceed with action */ }}
onCancel={() => { /* close, action stays blocked */ }}
/>
Internal flow:
- Shows the gate name + matched predicate explanation (“Receiving ward at capacity: ICU-2 has 0 beds free”)
- Reason code dropdown — filtered by
gate.override_reason_codesif set - If
OTHERselected → required free-text field - If
gate.override_authorizationis set → either:- Current user has the role/permission → proceed
- Current user does not → escalation flow (“Request charge nurse approval”)
- Preview block: “This will alert: Safety Officer, ICU charge nurse. The override will appear on the daily compliance digest.”
- Confirm button calls
recordOverride()then closes; UI proceeds with the action
5.2 Hook wrapper
Existing usePolicyGate(ctx) returns the new result shape. New companion:
const { result, gates, recordOverride } = usePolicyGate(ctx);
const handleConfirm = async () => {
if (result.blocked && result.overrideAllowed) {
setShowOverrideModal(true);
return;
}
if (result.blocked) {
// hard stop — UI shows the block message, no override path
return;
}
await proceed();
};
Every existing dialog that consumes usePolicyGate keeps working without changes; the override path is opt-in per dialog.
5.3 Override badge on the subject
After an override fires, the subject (TransferRequest, LabRequest, etc.) carries a visible “Overridden” badge in the worklist + detail view. Click → opens the override row with full audit (who, when, why, downstream alerts dispatched).
5.4 Compliance review surface
New admin page: /admin/policy-gate-overrides
- List of
policy_gate_overrideswithcompliance_reviewed_at IS NULL - Filter by gate, date range, override_by user, reason code
- Row actions: Mark Reviewed →
acceptable | flagged | training_needed | sanction+ notes - Daily/shift digest export for compliance officer
- Realtime updates so multi-officer review doesn’t double-handle
6. Application surfaces — where this gets wired
6.1 TransferRequest
Current gates in transfer-request-system.md §10:
| Gate | Current spec | After upgrade |
|---|---|---|
transfer_request_create |
Blocks low-priv users from STAT transfers | hard_stop=false, codes: CLINICALLY_JUSTIFIED, EMERGENCY_OVERRIDE. Authorization: charge_nurse. |
transfer_accept |
Blocks if receiving ward at capacity (unless override) | hard_stop=false, codes: NO_ALTERNATIVE_AVAILABLE, CAPACITY_OVERRIDE_APPROVED, BOARDING_AUTHORIZED. Alert recipients: bed_manager, receiving charge nurse. Consequences: capacity_pressure_metric: ward_overcap. |
transfer_late_cancel |
Requires admin override + reason | hard_stop=false, codes: DATA_CORRECTION_PENDING, PHYSICIAN_DIRECTED, OTHER. Authorization: admin. |
6.2 Custody handoff (per hospital-movement §6)
Already designed there. This doc absorbs that design into the general protocol — no new spec needed beyond the schema unification.
| Gate (example) | Notes |
|---|---|
| Mover certification mismatch (BSL3, radioactive) | hard_stop=false, codes: TIME_CRITICAL, NO_QUALIFIED_MOVER_AVAILABLE. Alert: safety_officer. Consequence: suppress_runner_bonus: true. |
| Mortuary handoff without dual identity verification | hard_stop=true per operator policy in some jurisdictions |
6.3 CDS rules bridge
CDS rules already use OverrideReasonCode. Currently they fire acknowledgement_requests and accept dismissal with a reason — but the reason is not joined to any audit table that compliance reviews systematically.
The upgrade: when a CDS rule dismissal includes a reason code, also write a policy_gate_overrides row with:
gate_id= synthetic CDS gate id (one per CDS rule, materialized inpolicy_gates)gate_trigger='cds_alert_dismiss'subject_resource= the resource the CDS was evaluating (MedicationRequest,LabRequest, etc.)override_reason_code= the dismissal reason
This unifies CDS dismissals into the same compliance review surface as policy-gate overrides. CDS rule schema doesn’t change — only the dismissal handler gains a write-through.
6.4 Specimen biohazard (future)
When the specimen biohazard gate ships (currently TODO per specimen-transport-bounded-context.md), it slots into this protocol directly. No additional design needed.
6.5 Payment gates (existing pathology)
The five wired dialogs in policy-gates.md §5 get an upgrade: when payment-gate fires and the encounter is ER, the override modal appears with the EMERGENCY_BYPASS_PAYMENT code preselected. Cashier supervisor reviews the daily digest, escalates if abuse.
6.6 Pharmacy max-dose / interaction
Today these go through CDS (no separate policy gate). After the §6.3 bridge ships, dismissals are audited identically.
6.7 IPD admission / bed assignment
Today: no gate. After this upgrade, a ward_capacity gate can be configured with hard_stop=false + BOARDING_AUTHORIZED reason code, giving bed managers visibility into how often capacity is being overridden.
7. Downstream consequences — the powerful part
policy_gates.override_consequences is the lever that lets an override change downstream behavior without each consumer hardcoding gate-specific logic.
Example consequences:
| Consequence field | Consumer | Effect |
|---|---|---|
billing_flag: 'retro_collection' |
Financial service | Encounter flagged in cashier’s “outstanding to backfill” queue |
suppress_runner_bonus: true |
Custody-handoff bonus calc | No bonus paid on this handoff |
capacity_pressure_metric: 'ward_overcap' |
Operations dashboard | Increment ward-overcap counter for shift report |
alert_safety_officer: true |
Already handled by override_alert_recipients, but consequence flag lets billing rail also react (e.g., insurance non-coverage for off-protocol care) |
|
requires_24h_followup_review: true |
Compliance scheduler | Auto-creates a follow-up review item 24h later |
escalate_to_qa: true |
Quality assurance | Override appears in monthly QA case review |
Consequences are declared on the gate, not in the consumer. Adding a new consequence means:
- Add the key to the consequence schema
- Add a consumer that reads it from the override row
- Configure the gate to emit it
No gate-specific code in the consumer.
8. Phase plan
| Phase | Scope | Effort |
|---|---|---|
| P1: Schema + reason enum | Migration: policy_gates column additions + policy_gate_overrides table + realtime + RLS; OverrideReasonCode enum extension in event-contract.ts + platform-api-schema |
~250 LOC SQL/TS |
| P2: Service layer | evaluateGates() return shape extension; recordOverride() + reverseOverride() service actions; React Query mutations |
~300 LOC |
| P3: Shared override modal + hook | OverrideReasonModal.tsx + usePolicyGate() updated return; preview block, authorization escalation UX |
~400 LOC |
| P4: Wire TransferRequest gates | All three transfer gates configured + override flow on accept/cancel dialogs | ~200 LOC |
| P5: Wire pathology payment gates | Update the five wired dialogs to surface override modal in ER/VIP context | ~250 LOC |
| P6: CDS bridge | Synthetic CDS gates materialized into policy_gates; dismissal-handler write-through to policy_gate_overrides |
~300 LOC |
| P7: Compliance review surface | /admin/policy-gate-overrides page with filter/review/disposition workflow + daily digest export |
~500 LOC |
| P8: Downstream consequences wiring | Billing-flag reader, runner-bonus suppressor, capacity-pressure metric, QA escalation hooks | ~400 LOC |
| P9: Custody-handoff gates (depends on hospital-movement P1) | Mover-cert gates configured into this protocol; handoff dispatch UI integrates override modal | ~300 LOC |
| P10: Cross-region seeds | Per-market-pack override reason codes (some codes are region-specific — e.g. KAIGO_AUTHORIZED for Japan); per-region hard_stop defaults |
~150 LOC SQL |
Total estimated: ~3,050 LOC across ~40 files in 12 directories.
Dependency graph:
P1 ──► P2 ──► P3 ──┬──► P4 ──► P5
├──► P6 ──► P7
├──► P8
└──► P9 (also depends on hospital-movement protocol §6 land)
P1 ──────────────────────► P10
P3 can ship before any wiring — just the modal + hook ready for reuse. P4 + P5 are the visible demo surface. P6 is the highest-leverage compliance unification.
9. Invariants
- Hard-stop is the exception, not the default. Gate authors must explicitly opt into
hard_stop=truewith a documented reason. - No silent bypass. Frontend dialogs that consume
usePolicyGateMUST route through the override modal whenblocked && overrideAllowed. Direct API calls that skip the gate are eventually blocked by backend duplicate enforcement (Phase 2 of originalpolicy-gates.md). - Reason code is mandatory and structured. Free-text alone is never accepted.
- Override is auditable forever.
policy_gate_overridesis append-only. Reversal sets a column, doesn’t delete. - Override authorization is checked at record time. Frontend MAY hide the override path from unauthorized users, but the backend MUST re-check.
- Consequences are declared on the gate, consumed by domain services. No consumer hardcodes gate-id checks.
- CDS dismissals and policy-gate overrides share one taxonomy. The reason code enum is single-source.
- Reversal closes the loop. If an override is reversed within window, downstream consequences must be undoable (or the gate must declare them irreversible and disallow reversal).
10. Open questions
- Per-gate vs per-tenant hard-stop policy? Today the spec is per-gate row. A tenant might want to globally promote all
medication.*gates to hard-stop. Add apolicy_gate_groupstable for batch policy, or rely on per-gate config? Recommend per-gate until tenants ask. - Override expiry? If a charge nurse overrides the ward-capacity gate at 8am, does the gate re-fire at 10am for the next admission attempt? Default: yes (each triggering action gets its own evaluation). Some shops may want session-scoped overrides — defer until requested.
- CDS bridge — gate materialization on every rule edit? When admins edit a CDS rule, do we synthetically maintain a matching
policy_gatesrow? Or query CDS rules virtually at override-record time? Materialization is simpler; virtual is more consistent. Recommend materialization with a sync trigger. - Authorization escalation — synchronous or async? Today’s spec assumes the higher-privilege user is physically present (“ask charge nurse to scan their badge”). For escalation that requires reaching someone remotely, we need an async approval flow — that’s a bigger feature, probably out of scope for the first cut.
- Compliance review SLA? What’s the expected turnaround on
compliance_reviewed_at? 24h for most, 1h forEMERGENCY_OVERRIDE-coded entries? Configurable per gate? - Cross-region reason codes. Japan’s kaigo flows may need codes that don’t exist in Thailand. How are region-specific codes added without forking the enum? Recommend a tier 2 enum (
OverrideReasonCodeExtended) that market packs can extend, with the base enum frozen at the codes in §3.4. - Overrides on behalf-of-machine. If an HL7v2 inbound feed creates a transfer that would have tripped a gate, who’s the override actor? A system service account, or do we hold the message in a triage queue for a human to override? Defer until HL7v2 ADT^A02 ingest is built.
- What happens to existing CDS overrides on day 1? Backfill
policy_gate_overridesfrom historical CDS dismissals, or start fresh? Recommend start fresh; historical CDS dismissals stay in their original log.
11. Migration sequencing notes
- P1 ships standalone. Schema additions are backward compatible — no existing code breaks.
- P2 + P3 ship together. The hook + modal + service action form one usable unit.
- P4-P9 can ship in any order depending on which gate domain has highest demand.
- P7 (compliance UI) can ship at any point after P1 — it just queries the override table; doesn’t need any of the other phases.
- P10 (cross-region) is non-blocking — happens per market-pack adoption.
The frontend-only enforcement caveat from policy-gates.md §9 still applies: a direct API call to backend services bypasses the gate UI entirely. Backend enforcement is Phase 2 of original policy-gates.md and is separate from this protocol. When it ships, the override flow will need to be re-implementable backend-side (the recordOverride() action and reason taxonomy already live in shared schema, so this is mostly a matter of adding the Moleculer/NestJS service-layer hook).
12. References
policy-gates.md— Base rule engine (this doc extends with override protocol)hospital-movement-architecture.md§6 — Custody-handoff override design (this doc generalizes it)transfer-request-system.md— TransferRequest gates that consume thisspecimen-transport-bounded-context.md— Specimen biohazard gate (future consumer)cds-vital-signs-rules.md— CDS rule engine (bridge target via §6.3)acknowledgement-system.md— How override alerts reach recipientsevent-contract.ts—OverrideReasonCodeenum (extension target)policy-gates-coverage.md— Visual map of UI buttons already gated (each becomes a candidate for override-with-reason)cross-region-policy-gates-deployment.md— Per-region rollout pattern this upgrade inherits