Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions docs/BACKLOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28622,3 +28622,87 @@ ADR 0078's stated residuals were meant to land under ADR 0173, and ADR 0173's on
**What did ship.** (b), the optional restore-token vintage cross-check, is built, and (c), the risk acceptance, is recorded. BACKLOG #223 closed on that, verified against `origin/main` 2026-07-28. That closure is correct for what it claims and is not what this row reopens.

**What this row asks for.** One ruling: schedule (a), or accept the residual as permanent and say so in the ADR and the risk-acceptance register. Either answer closes this. What must not persist is a deferral pointed at an owner decision that no artifact ever surfaces.

---

## 1503. ADR 0075's AC-1 asserts a batched `mark_done` that does not exist, and cites a test name that does not exist

> 🔢 **Filed 2026-09-09 -- not started. Scored at filing.** Value **5/10** · Difficulty **3/10** · _fill-in_. Found by an adversarial over-claim hunt across 125 ADR build claims. The record is otherwise honest and the feature is real; the defect is that a counted acceptance criterion names a third hop that was never built, and anchors it to a test that does not exist. Value 5 because a cited-but-absent test reads as MORE verified than no citation at all. Difficulty 3: either build the third hop or correct the criterion, and the ADR's own open items already frame the choice.
> Verdict: owner-ruling
> Research: none -- the measurement is in the body
> Closing-act: code

**Cluster:** ADR record integrity / SQL Server store. **Priority:** P3. **Verdict:** decide batch-or-defer, then make AC-1 match.
**Severity:** no deployment axis (sec. 0). The shipped batching is correct for the two hops it covers; nothing mis-executes. What is wrong is the record.

**What AC-1 asserts.** [ADR 0075](adr/0075-per-hop-sql-statement-batching.md), inside its counted Acceptance Criteria block: *"WHEN `batch_handoff_statements=true` on a SQL Server store, THE SYSTEM SHALL emit, for each of `route_handoff` / `transform_handoff` / `mark_done`, the identical logical `(sql, params)` sequence as the unbatched path"* -- cited to `tests/test_adr0075_batch_golden_sql.py::test_batched_matches_unbatched_sequence`. The ADR is **Accepted** and the flag is **promoted default-ON**.

**What the tree has.** Two of the three hops.

| Hop | Batched method | Reads the flag |
|---|---|---|
| `route_handoff` | `_route_handoff_batched` | yes |
| `transform_handoff` | `_transform_handoff_batched` | yes |
| `mark_done` | **none** | **no** |

`store/sqlserver.py` reads `_batch_handoff_statements` in exactly two places, both handoff dispatchers. `mark_done` never reads it, so the third hop is unreachable by construction rather than merely untested.

**The cited test does not exist.** `tests/test_adr0075_batch_golden_sql.py` defines `test_route_batched_matches_unbatched_sequence` and `test_transform_batched_matches_unbatched_sequence`. Neither that file nor `tests/test_adr0075_rt_count_gate.py` contains the string `mark_done`.

**The ADR already knows.** Its own open items carry an **unchecked** box: *"`mark_done` inclusion ... decide whether to batch it in v1 or defer it."* So a counted criterion asserts as delivered the exact thing the same document lists as undecided.

**Scope, stated so nobody over-corrects.** The README index cell says only *"fold a multi-statement handoff BODY"*, which is accurate. The over-claim is in the ADR body, not the index row. The §Evidence round-trip table also prices `mark_done` at 11 to 7/8 round-trips, which the code does not do either.

---

## 1504. ADR 0133 D3 asserts content-triggered alerts are built, but a Handler cannot reach `content_match`

> 🔢 **Filed 2026-09-09 -- not started. Scored at filing.** Value **5/10** · Difficulty **4/10** · _fill-in_. Found by the same over-claim hunt. Two of the ADR's three capabilities are genuinely wired across all three backends; the third is a method with no reachable caller and no path from the authoring surface. `docs/BACKLOG.md:698` already records the capability as absent, so the ledger and the ADR disagree with each other. Value 5: an operator reading the ADR would plan a Handler they cannot write. Difficulty 4: either export an emitter onto the authoring surface or retract D3.
> Verdict: owner-ruling
> Research: none -- the measurement is in the body
> Closing-act: code

**Cluster:** alerting / ADR record integrity. **Priority:** P3. **Verdict:** build the seam or retract D3.
**Severity:** no deployment axis (sec. 0). No mis-execution; a documented capability simply is not reachable.

**What is genuinely built.** Do not let this row read as though ADR 0133 failed. D1 and D2 are complete:

- **D1, escalation tiers** -- `EscalationTier` and `AlertRule.escalate` in `config/settings.py`, tier selection and occurrence counting in `pipeline/alert_sinks.py`, persisted monotonically on all three backends with each dialect's own idiom (`MAX` / `GREATEST` / `CASE`) and the DDL plus migration present in `store.py`, `postgres.py` and `sqlserver.py`.
- **D2, schedule-aware thresholds** -- `AlertRule.schedule` plus the `is_active(now_dt)` gate in `alert_sinks.py`.

**What D3 asserts.** [ADR 0133](adr/0133-alert-escalation-tiers-schedule-aware-thresholds-and-content-triggered-alerts-the-56-remainder.md) status reads `Accepted (2026-07-18, built)`, and AC-3 is *"WHEN a Handler emits a `content_match`, THE SYSTEM SHALL emit a PHI-free event."* The README index row repeats that a Handler *"emits off the routing hot path"*.

**Why a Handler cannot do that.** `content_match` exists only as `NotifierAlertSink.content_match` in `pipeline/alert_sinks.py`. It is:

1. **absent from the `AlertSink` Protocol** in `pipeline/alerts.py` -- that protocol's methods do not include it;
2. **absent from `LoggingAlertSink`**;
3. **not exported** -- `messagefoundry/__init__.py`'s `__all__` carries no alert emitter;
4. **unreachable from a `@handler`**, which receives only `msg` (`config/wiring.py`).

Zero non-test callers exist in the engine.

**The ledger already says so.** `docs/BACKLOG.md:698` records content-triggered alerting as not delivered. This row exists because the ADR does not, and the ADR is the document a reader trusts for build state.

---

## 1505. ADR 0089 claims a Phase A read-atom row that was never built, and names a `set_segment` the Message API lacks

> 🔢 **Filed 2026-09-09 -- not started. Scored at filing.** Value **3/10** · Difficulty **3/10** · _fill-in_. Found by the same over-claim hunt, and one of only two findings in it that were not already written down somewhere in the repo. The bulk of Phase A shipped; the Status line claims the whole of it. Value 3: the affected rows render as opaque code in the Steps view rather than failing, so the cost is a wrong expectation, not a defect. Difficulty 3: either recognise the read atom or narrow the Status line and the Phase A table.
> Verdict: build
> Research: none -- the measurement is in the body, and it was measured by running the lens, not by grep
> Closing-act: code

**Cluster:** Steps view / lens. **Priority:** P3. **Verdict:** narrow the claim, or build the read row.
**Severity:** no deployment axis (sec. 0). IDE authoring surface only, no engine path, no PHI.

**What the Status line claims.** [ADR 0089](adr/0089-recognition-first-lens-native-idioms.md): *"Phase A (native-idiom recognition) is built and adopted."* Phase A is titled *"native write/**read** atoms"*, and its table lists `x = msg.field("Y")` as a **Read Field to var** row (22 occurrences in its own estate scan) and names `set_segment`.

**What the lens recognises.** `_recognize_native_method` in `messagefoundry/lens.py` handles `msg.set` (set_field), the `msg.set(dst, msg.field(src))` copy form, `msg.delete_segments` / `delete_segment`, plus `msg.add_segment` and `msg.add_repetition` (both tagged ADR 0106, so credited to a later ADR).

**Why the read atom can never reach it.** The recogniser is invoked only inside an `isinstance(s, ast.Expr)` branch, and the comment there says so outright -- *"a mutating method call, so always a bare expression statement, never an assignment."* An assignment is structurally excluded. There is no `read_field` action anywhere in the product.

**Measured, not inferred.** Running `messagefoundry.lens.parse_source` over a handler containing all four forms returns `{"kind": "code"}` for `x = msg.field("PID-5.1")`, while `msg.set`, `msg.delete_segments` and `msg.add_segment` in the same body return editable `action` rows with populated `literal_params`. So the 22 statements the ADR's own scan counted still render as opaque grey code rows.

**`set_segment` does not exist.** `parsing/message.py` has `field` and `add_segment`; there is no `def set_segment` anywhere.

**Scope, stated so nobody over-corrects.** The README index row is **honest** -- it names only `msg.set` / `msg.field`-copy / `msg.delete_segments` and the actions `set_field` / `copy_field` / `delete_segment`. `tests/test_lens_native.py` covers exactly the shipped forms, consistent with there being no read row. The over-claim is internal to the ADR file.
31 changes: 30 additions & 1 deletion docs/adr/0002-phase2-transport-security-and-strong-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,36 @@
SAML via Entra) and SMART on FHIR are *out of this ADR's scope*** — they get a dedicated
federated-SSO ADR when 0.2 design begins. *(Originally Proposed 2026-06-12 as design-only under the
"design now, build then" rule; this acceptance supersedes that deferral.)*
- **Built:** Nothing in this ADR is built yet — it designs **WP-13a** (API/WebSocket TLS), **WP-13b**
- **Built:** **All four work packages are BUILT.** Corrected 2026-09-09. The stale sentence below is
kept rather than deleted, because a reader planning off it would plan to build code that already
exists. Its WP-14 parenthetical is that sentence's only build marker, so the leading claim reads as
scoping the exception to MFA alone while WP-13a, WP-13b and WP-15 shipped underneath it. Verified by
symbol against the tree on 2026-09-09, because line numbers rot and symbols do not: **WP-13a** is
`build_api_ssl_context` and `ensure_api_tls_material` ([api/tls.py](../../messagefoundry/api/tls.py)),
imported and called on the `serve` path ([__main__.py](../../messagefoundry/__main__.py)) for every
topology that terminates TLS in-process -- a declared `[api].tls_terminated_upstream` proxy is the
deliberate exception, where `ensure_api_tls_material` returns `None` and the serve path builds no
context at all
([ADR 0172](0172-the-engine-always-serves-tls-minting-a-self-signed-certificate-on-first-run.md)
decision 3); **WP-13b** is `_mllp_ssl_context`
([transports/mllp.py](../../messagefoundry/transports/mllp.py)), built once per connection and wired
on both the outbound (`MLLPDestination`) and the inbound (`MLLPSource`) side, each call site
self-labelled WP-13b; **WP-14** is `auth/totp.py` (RFC 6238) with the enroll / verify / step-up flow
in [auth/service.py](../../messagefoundry/auth/service.py); **WP-15** is `[api].trusted_proxies` and
`[api].tls_terminated_upstream` ([config/settings.py](../../messagefoundry/config/settings.py)),
reaching uvicorn as `forwarded_allow_ips` and the app as `app.state.trusted_proxies`
([api/app.py](../../messagefoundry/api/app.py)); `ApiSettings.exposure_protected` is the §0
exposed-gate predicate. **Scope: the four work packages shipped. That is NOT a claim that every
clause of this ADR is live.** Two sub-clauses it already scopes stay unbuilt on purpose -- the mTLS
client-certificate identity resolver is inert under stock uvicorn (§4,
[ADR 0083](0083-mtls-client-certificate-identity.md)), and in-engine OCSP/CRL revocation is delegated
rather than implemented (*Certificate revocation*,
[ADR 0078](0078-certificate-revocation-posture.md)).
**STALE AS WRITTEN, kept for the record. The stale span ENDS at "forwarded-header hardening)" and
the claim was true only of the original design-only ADR. The Phase-0/1 groundwork sentence after
that point is NOT stale and NOT corrected here -- its four anchors (`_security_headers`,
`_ws_origin_allowed`, the `--allow-insecure-bind` bind guard, `insecure_tls_allowed()`) each still
resolve to a live symbol, checked 2026-09-09:** Nothing in this ADR is built yet — it designs **WP-13a** (API/WebSocket TLS), **WP-13b**
(MLLP-over-TLS, v0.1), **WP-14** (MFA, **built 2026-06-17 — see §3**), and **WP-15** (reverse-proxy /
forwarded-header hardening). The
Phase-0/1 groundwork it *builds on* is already shipped and must **not** be redesigned:
Expand Down
27 changes: 21 additions & 6 deletions docs/adr/0003-non-hl7-transports-database-rest-soap.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,27 @@
router/store changes) is a **follow-up ADR** before any non-HL7 source is built, so §3 stays
forward-looking. **First database backend: SQL Server** (reuses the store's existing `aioodbc` path —
no new driver). (Proposed → Accepted same day.)
- **Built:** Nothing yet. The extension *point* it uses is already built and is explicitly meant for
this: the connector **registry** (`register_source` / `register_destination` keyed by
`ConnectorType`, [transports/base.py](../../messagefoundry/transports/base.py)) and the
transport-agnostic `Source`/`Destination` models ([config/models.py](../../messagefoundry/config/models.py),
whose docstring says adding a transport "never requires touching this file"; the enum already carries
the placeholder comment `# Phase 2+: TCP, DATABASE, REST, FHIR`).
- **Built:** BUILT, not pending — the §1 connectors shipped, and the §3 database source with them.
**AMENDED 2026-09-09.** This line previously read *"Nothing yet. The extension point it uses is
already built and is explicitly meant for this"*, which was true when written (2026-06-12, before
§2 was started) and is now wrong by the whole feature. It under-claimed, so a reader planning off
it would plan to build what already ships.
**Verified in the tree 2026-09-09, by symbol:** `ConnectorType.REST` / `.DATABASE` / `.SOAP` are
declared in [config/models.py](../../messagefoundry/config/models.py), each citing this ADR; and
`RestDestination` ([transports/rest.py](../../messagefoundry/transports/rest.py)),
`DatabaseDestination` + `DatabaseSource` ([transports/database.py](../../messagefoundry/transports/database.py))
and `SoapDestination` ([transports/soap.py](../../messagefoundry/transports/soap.py)) each register
themselves into the connector registry. §5's gate cleared: the follow-up landed as
[ADR 0004](0004-payload-agnostic-ingress.md), and `content_type` now reaches `store/` and `pipeline/`.
**Scope of that check:** symbols and registry registration only — not test coverage, and not every
§2/§4 sub-decision. There is no REST or SOAP *source* connector, and none is planned.
**The extension point named above is unchanged and is still the mechanism:** the connector
**registry** (`register_source` / `register_destination` keyed by `ConnectorType`,
[transports/base.py](../../messagefoundry/transports/base.py)) and the transport-agnostic
`Source`/`Destination` models ([config/models.py](../../messagefoundry/config/models.py)), whose
docstring still says adding a transport never requires touching that file. One detail of it went
stale: the enum's placeholder comment `# Phase 2+: TCP, DATABASE, REST, FHIR` is gone, replaced by
the real members.
- **Related:** [ADR 0001](0001-staged-pipeline-architecture.md) (the staged pipeline these feed:
sources → ingress, destinations ← outbound), [ADR 0002](0002-phase2-transport-security-and-strong-auth.md)
(TLS/egress posture these inherit), the Corepoint-migration estate (185 MLLP but also database, REST,
Expand Down
26 changes: 25 additions & 1 deletion docs/adr/0004-payload-agnostic-ingress.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,31 @@
§"To resolve" leans are taken as the answers: **DB-IN first**; `RawMessage` = `.raw`/`.text`/`.json()`
(`.xml()` shipped #31); a **callable** summary hook; a small `content_type` **enum**. (Proposed → Accepted
same day.)
- **Built:** Nothing here yet. It builds on the **already-shipped** non-HL7 *destinations* (REST/DATABASE/
- **Built:** BUILT, in the parts named here — §7.1's ingress contract shipped, and so did §7.2's first
non-HL7 sources. `ContentType` ([config/models.py](../../messagefoundry/config/models.py)) carries
`HL7V2`/`JSON`/`XML`/`TEXT`/`X12`/`FHIR`/`BINARY`/`DICOM`; `content_type` is a field on
`InboundConnection` and a parameter of the `inbound(...)` factory
([config/wiring.py](../../messagefoundry/config/wiring.py), coerced by `_coerce_content_type`, with
§5's fail-loud `WiringError` when `strict` meets a non-HL7 type). §2's ingress branch and §4's generic
store fields are in [pipeline/wiring_runner.py](../../messagefoundry/pipeline/wiring_runner.py), at
both `_handle_inbound` and `_handle_inbound_http`. The `hl7v2` path keeps the HL7 peek and optional
strict-validate; any other type skips both and commits the body with no HL7 parse — decoded text for
the text types, base64-carried through `RawMessage.from_bytes` for the binary ones, per ADR 0028 —
with `message_type` = the `content_type` value and a null `control_id`/`summary`. §3's `RawMessage`
([parsing/message.py](../../messagefoundry/parsing/message.py)) exposes
`.raw`/`.text`/`.json()`/`.xml()`/`.raw_bytes`/`.binary()`/`.encode()`/`.copy()`, and the 2026-07-17
amendment's magic-byte check was hoisted to [parsing/sniff.py](../../messagefoundry/parsing/sniff.py)
(`_content_matches_declared`) out of `transports/file.py`, where that amendment placed it;
`transports/file.py` re-imports and re-exports it, so the name still resolves there. Both first
sources are registered: `DatabaseSource`
([transports/database.py](../../messagefoundry/transports/database.py)) and `HttpSource`
([transports/http_listener.py](../../messagefoundry/transports/http_listener.py), ADR 0023).
**NOT BUILT:** §4's optional per-inbound id/summary extractor hook — `control_id` and `summary` stay
null on a non-HL7 inbound. *This bullet opened "Nothing here yet." until 2026-09-09. That was true at
acceptance (2026-06-12); nobody revised it as the build landed, so the record under-reported what
shipped, and the Status line above already contradicted it by recording `.xml()` as shipped. Only the
symbols named above were re-verified against the tree — read the rest of this bullet as written in
June.* It builds on the **already-shipped** non-HL7 *destinations* (REST/DATABASE/
SOAP, ADR 0003) and on a reliability core that is **already format-agnostic**: the staged queue +
ingress/routed/outbound stages and the disposition finalizer
([store/store.py](../../messagefoundry/store/store.py) `_maybe_finalize_message`) carry no HL7 coupling
Expand Down
Loading
Loading