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 SSOTderiveSecurityLabels({ sensitivityClass: 'part2_sud' }):
- TS SSOT:
src/cores/cl/lib/ds4p/security-labels.ts→PART2_ADT_SECURITY_LABEL = serializeDs4pSecurityLabel(deriveSecurityLabels({ sensitivityClass: 'part2_sud' })). serializeDs4pSecurityLabelsorts each HL7 ActCode group ascending + comma-joins, then pipe-delimits the two groups:"<sensitivity>|<obligation>". The Part 2 floor (sensitivityETH+42CFRPart2, obligationNORDSLCD) therefore serializes deterministically and order-independently to42CFRPart2,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/clinical →
src/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 thecl_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.