Skip to main content
Spec: CL-29-EN-66 — FUA & FUI Follow-Up Measures · Task: T4.2 (Phase 4) · Status: spec draft, human-merge Boundary owner: CL-63 (DS4P Data Segmentation & Privacy) — specs/cl/specs/CL-63-ds4p-data-segmentation-privacy.md Regulatory: 42 CFR Part 2 (SUD), HL7 DS4P IG This is the focused trace doc for DS4P label propagation on the FUA/FUI out-of-network ED-SUD ADT path. The broader FUA/FUI integration doc (PF-108 consumer contract + CL-51 registration) is a separate task (T4.4).

1. Propagation chain: ADT source → cl_fua_adt_events

Out-of-network ED-SUD ADT events arrive (from the PF-108 / Contexture HIE feed) and land in cl_fua_adt_events. Two DS4P facets are fail-closed at the DB chokepoint — two BEFORE INSERT/UPDATE triggers that fire on every write path (direct PostgREST, the service-role feed, and the sanctioned cl_ingest_fua_adt_event RPC), so no writer can persist an unlabeled SUD row: T4.2 closes the substrate invariant declared in 20260701000016_cl29en66_fua_fui_substrate.sql:16-17 (“SUD ADT data … requires ds4p_confidentiality_code / ds4p_security_label when sud_indicated=true”) that P3b had left to Phase 4 (P3b fail-closed only the code; the label was a passthrough). A feed-supplied non-blank label is preserved (passthrough); non-SUD rows are never forced.

SSOT and the DB↔TS drift trap

The label is not a hand-typed string. It is the canonical serialization of the SSOT deriveSecurityLabels({ sensitivityClass: 'part2_sud' }):
  • TS SSOT: src/cores/cl/lib/ds4p/security-labels.tsPART2_ADT_SECURITY_LABEL = serializeDs4pSecurityLabel(deriveSecurityLabels({ sensitivityClass: 'part2_sud' })).
  • serializeDs4pSecurityLabel sorts each HL7 ActCode group ascending + comma-joins, then pipe-delimits the two groups: "<sensitivity>|<obligation>". The Part 2 floor (sensitivity ETH + 42CFRPart2, obligation NORDSLCD) therefore serializes deterministically and order-independently to 42CFRPart2,ETH|NORDSLCD ('4' < 'E' by codepoint).
  • The DB trigger writes that exact literal. The integration test (tests/integration/cl/cl-29-en-66-ds4p-label.integration.test.ts, D1/D4) imports the TS constant and asserts the persisted DB value is byte-equal to it — so any future drift between the derivation matrix and the DB literal fails the suite loudly.

Trigger fire-order (load-bearing)

Both triggers are BEFORE-ROW on the same table/events, so PostgreSQL fires them in trigger-name ascending order. trg_cl_fua_adt_events_derive < trg_cl_fua_adt_events_ds4p_label ('derive' < 'ds4p_label'), so the P3b derive trigger sets NEW.sud_indicated before the T4.2 trigger reads it. The label fail-close is therefore evaluated against the authoritatively-derived SUD flag, never a client-supplied one. This ordering is documented in both migrations; a rename that reorders the names would silently break the fail-close. sud_indicated itself is server-derived fail-closed (D-7): indeterminate/unparseable ⇒ true (presumed SUD, maximum Part 2 protection).

2. cl_fua_adt_events → export: DS4P propagation is BLOCKING before payer export

The label carries the redisclosureProhibited = true / NORDSLCD obligation. That obligation is operationalized at runtime by the T4.1 payer-export consent gate (cl_fua_payer_export_gate() BEFORE-INSERT trigger on cl_fua_payer_export_requests, 20260701000020): a payer-facing export of any SUD chart’s FUA/FUI denominator membership is fail-closed at the DB unless (a) active 42 CFR Part 2 consent exists (via cl_check_sud_consent()), and (b) the org has explicitly enabled payer export — and every permitted payer redisclosure writes a cl_redisclosure_log row. Plainly: DS4P propagation is BLOCKING before payer export — a SUD row cannot reach a payer destination without consent. The label marks the data; the consent gate enforces the prohibition the label asserts. (This task does not modify the T4.1 gate; it is referenced here only to show the end-to-end blocking relationship.)

3. CL-63 boundary

DS4P segmentation is owned by CL-63 (specs/cl/specs/CL-63-ds4p-data-segmentation-privacy.md); its implementation surface is the shared src/cores/cl/lib/ds4p/ module (security-labels.ts, confidentiality.ts) plus the platform DS4P types (@/platform/clinicalsrc/platform/clinical/ds4p/types.ts). T4.2 consumes that module — it does not fork it. The same deriveSecurityLabels SSOT already feeds CL-69’s at-rest outcome-response labeling (useOutcomeEpisodeMutation.ts); this task mirrors that derive-in-TS / persist-at-rest shape at the ADT DB chokepoint.

4. Human-verify-open

End-to-end DS4P propagation against a live PF-108 / Contexture HIE feed is human-verify-open — PF-108 is a planning stub, not a live feed. This task proves: (1) the DB label fail-close, (2) the TS↔DB SSOT agreement (drift trap), and (3) the BLOCKING-before-payer-export relationship. It does not prove a live feed end to end.

5. Documented follow-up (not this PR)

Surfacing the § 2.32 notice / DS4P label codes inside the cl_redisclosure_log.records_disclosed JSONB would require re-opening the audited T4.1 export gate; it is deferred as a follow-up. The federal redisclosure prohibition is already enforced at runtime by the consent gate and implied by the log’s basis_type = 'consent' — so the deferral does not leave the prohibition unenforced.