From 51227fed3a10fd6681fdd90191bb5b0da3ace08d Mon Sep 17 00:00:00 2001 From: wshallwshall Date: Thu, 30 Jul 2026 20:13:10 -0500 Subject: [PATCH] docs(adr): decline ADR 0089 Phase D by owner ruling Owner ruled 2026-07-30 that helper descent is too risky. ADR 0076 Amendment B goes PROPOSED -> DECLINED, and Phase D is not to be built. Recorded in four places, because a decline that only exists where the decline was argued is a decline nobody finds. The load-bearing one is ADR 0089 itself: a session planning the next lens phase reads 0089's phase list, not an amendment to a different ADR, and 0089's own text sells Phase D as "the largest structural lever" against "265 delegating call-sites". That paragraph is left intact with the ruling above it, plus the correction that 265 is a statement-scan count and not a yield estimate -- descent makes nothing editable by itself. Decline-by-design, not deferral. The specification stays so the reasoning is auditable and so the next reader who thinks helper delegation is the biggest remaining win finds the work already done. Reopening takes a new amendment, and must first answer the aliasing question (two call sites of one helper produce identical child spans; expect_src matches both, so an edit to one silently rewrites the other) and produce a real per-helper measurement rather than the 218/522 heuristic superset. Promotes the alternative from precondition to recommendation: teaching Phase A the ast.Assign-to-ast.Subscript form (msg["X"] = v) widens what is editable without touching the row shape, and is on no phase list. Also fixes a stale cross-reference this session introduced: #239's banner cited "#240 (comment-only rows)" from before that item was renumbered to #248. #240 is now a permanent hole, so the citation pointed at nothing -- the exact failure the #88 erratum describes. The same banner's "~70% of the opaque mass" argument is corrected: with Phase D declined it rests on comment-only rows alone. Co-Authored-By: Claude Opus 4.8 --- docs/BACKLOG.md | 2 +- ...yped-action-vocabulary-action-list-lens.md | 28 +++++++++++++++---- ...89-recognition-first-lens-native-idioms.md | 20 +++++++++++++ docs/adr/README.md | 2 +- 4 files changed, 44 insertions(+), 8 deletions(-) diff --git a/docs/BACKLOG.md b/docs/BACKLOG.md index a4c2e535..082302eb 100644 --- a/docs/BACKLOG.md +++ b/docs/BACKLOG.md @@ -7073,7 +7073,7 @@ The webview cannot import from `src/` (it is loaded as a plain script into a `de > ✅ **SHIPPED (2026-07-30, PR #81).** `scripts/quality/lens_coverage.py` drives the shipped `lens parse --json` — not a second `ast` walk — so the number cannot drift from what the Steps view actually renders. Measured against the de-identified estate: 388 files · 145 handlers · 1,423 rows · **0 parse refusals**; editable share **42.0%**, fully-typed handlers **14.5%** (21/145), median opaque rows/handler **3**. Full result and the pre-registered decision rule are recorded on PR #81. > -> ⚠️ **The pre-registered rule fired 🔴 RED, and the RED prescription was *not* adopted** — both triggers landed exactly on their boundaries (B = 14.5% missed the 15% floor by 0.5pp; median opaque = 3 hit `≥ 3` exactly), while A = 42.0% sat mid-AMBER. The AMBER prescription (breadth before depth) was taken instead, on the argument that the opacity is *mechanical* — comment-only rows (28%) plus helper delegation (41.8%) are ~70% of the opaque mass and both are addressable within the projection model. **This override was a delegated judgment call, never explicitly ratified by the owner**; treat it as open if the next measurement does not move. See #240 (comment-only rows) and ADR 0089 Phase D (helper descent). +> ⚠️ **The pre-registered rule fired 🔴 RED, and the RED prescription was *not* adopted** — both triggers landed exactly on their boundaries (B = 14.5% missed the 15% floor by 0.5pp; median opaque = 3 hit `≥ 3` exactly), while A = 42.0% sat mid-AMBER. The AMBER prescription (breadth before depth) was taken instead, on the argument that the opacity is *mechanical* — comment-only rows (28%) plus helper delegation (41.8%) are ~70% of the opaque mass and both are addressable within the projection model. **This override was a delegated judgment call, never explicitly ratified by the owner**; treat it as open if the next measurement does not move. See **#248** (comment-only rows — [ADR 0076](adr/0076-typed-action-vocabulary-action-list-lens.md) Amendment A, ratified 2026-07-30). ⚠️ **The other half of that "~70% of the opaque mass" argument no longer stands:** helper delegation was to be addressed by ADR 0089 Phase D, which the owner **declined 2026-07-30 as too risky** (ADR 0076 Amendment B) — and its 41.8% was a heuristic superset whose real yield may be negative. So the breadth-before-depth case now rests on comment-only rows alone; re-measure before assuming the number moves. **Cluster:** IDE & Authoring. **Priority:** P1 — this number decides how much further Steps-view investment is justified. **Verdict:** build (cheap, reproducible). **Severity:** none (measurement). diff --git a/docs/adr/0076-typed-action-vocabulary-action-list-lens.md b/docs/adr/0076-typed-action-vocabulary-action-list-lens.md index 18986efe..fbfdd3d4 100644 --- a/docs/adr/0076-typed-action-vocabulary-action-list-lens.md +++ b/docs/adr/0076-typed-action-vocabulary-action-list-lens.md @@ -1,6 +1,6 @@ # ADR 0076 — Typed action vocabulary + structured action-list lens over Python Handlers -**Status:** Accepted (2026-07-10) — ratified by the owner 2026-07-10; the PLAN-8 lanes may build. Gating rule: **phase 1 (the vocabulary) requires only the #26-amendment merge; phases 2–3 require this ADR Accepted.** In practice phase 1 builds after Acceptance anyway — its v1 roster is fixed by §2 and MULTISESSION-PLAN-8 bundles it with phase 2a in one lane. **Amendment A — ACCEPTED, ratified by the owner 2026-07-30 and IN FORCE:** a `note` row kind so comment-only rows stop projecting as opaque `code`, superseding ADR 0106 §5 (L); §3's enum and §4's ladder read as amended, and BACKLOG #248 is the build. **Amendment B — still PROPOSED and NOT ratified:** ADR 0089 Phase D "helper descent" is specified and priced but **not buildable**; its yield is unmeasured and may be negative, and its three §B.4 preconditions are unmet. Do not read A's ratification as covering B. +**Status:** Accepted (2026-07-10) — ratified by the owner 2026-07-10; the PLAN-8 lanes may build. Gating rule: **phase 1 (the vocabulary) requires only the #26-amendment merge; phases 2–3 require this ADR Accepted.** In practice phase 1 builds after Acceptance anyway — its v1 roster is fixed by §2 and MULTISESSION-PLAN-8 bundles it with phase 2a in one lane. **Amendment A — ACCEPTED, ratified by the owner 2026-07-30 and IN FORCE:** a `note` row kind so comment-only rows stop projecting as opaque `code`, superseding ADR 0106 §5 (L); §3's enum and §4's ladder read as amended, and BACKLOG #248 is the build. **Amendment B — ⛔ DECLINED by owner ruling 2026-07-30 (too risky):** ADR 0089 Phase D "helper descent" is **not adopted and not to be built** — aliasing across duplicate call sites has no solution in any ADR, and the yield is unmeasured and may be negative. The specification is retained so the decline is auditable, not as a plan; reopening needs a **new** amendment. The better lever, explicitly not declined, is teaching Phase A the `msg["X"] = v` subscript form — it widens what is editable without touching the row shape. **Deciders:** owner + IDE/DX working group **Related:** BACKLOG **#222** (this build), **#26 amendment** (the narrow carve-out this ADR operates under), **#221** (sibling IDE-polish lane), the deep-research findings ([`docs/research/ide-low-code-options.md`](../research/ide-low-code-options.md) — verified precedents: InterSystems low-code custom editors, Kaoto/Karavan/AWS Workflow Studio, Iguana annotations, Corepoint action-lists), ADR 0007/0033/0014 (the sanctioned config-as-data GUIs), ADR 0072 (traced dry-run — the live values rendered beside action rows), ADR 0010/0043 (`db_lookup`/`fhir_lookup` — the sanctioned read-only lookups the lens renders as DBSelect-style rows), ADR 0035 (IDE workspace-trust — `lens` CLI calls are exec-gated like every CLI call), CLAUDE.md §9 (PHI), §12 (the amended bright line). Plan: [`docs/releases/MULTISESSION-PLAN-8.md`](../releases/MULTISESSION-PLAN-8.md) (L2 builds phases 1+2a; L3 builds phase 2b; L4 = phase 3, owner-gated). @@ -363,12 +363,28 @@ New residual: pragma notes are visible but read-only, an intentional and documen - **AC-N6** — WHEN a construct is unrecognized, THE SYSTEM SHALL emit a `code` row; a whole-file refusal SHALL occur only on `ast.parse` failure → unchanged ladder assertion, re-run over the note corpus. -## Amendment B (PROPOSED, 2026-07-30) — ADR 0089 Phase D "helper descent" +## Amendment B (DECLINED, 2026-07-30) — ADR 0089 Phase D "helper descent" -> **Status of this amendment: PROPOSED — owner-gated, not ratified, not buildable.** Amendment A above -> is independent of this one and does not depend on it. This section exists so the grammar change Phase -> D implies is specified and priced *before* anyone builds it, per §2's amendment rule; it is not a -> licence to start. +> **Status of this amendment: ⛔ DECLINED by owner ruling 2026-07-30 — too risky.** The grammar +> widening described below is **not adopted**, and **ADR 0089 Phase D is not to be built**. Amendment A +> above is independent of this one, was ratified separately, and is unaffected. +> +> **This is a decline-by-design, not a deferral.** The specification is kept — not as a plan, but so the +> decline is auditable and so the next session that reads ADR 0089 §Phase D and thinks "265 delegating +> call-sites is the biggest remaining win" finds the reasoning already done. The case against it is +> §B.1 (aliasing has no solution in any ADR: two call sites of one helper produce identical child spans, +> `expect_src` matches both, and an edit to one silently rewrites the other) and §B.2 (the yield is +> unmeasured and may be **negative** — the shipped sample helper writes exclusively in the unrecognized +> `msg["X"] = v` form, so descending it returns six opaque rows and zero editable ones). +> +> **What is NOT declined, and is the better lever:** teaching Phase A the `ast.Assign`-to-`ast.Subscript` +> form (`msg["X"] = v`). It converts writes into editable rows **without touching the row shape at all**, +> is on no phase list, and may move the coverage number further than descent for a fraction of the risk. +> §B.4 precondition 2 already argued this; the decline promotes it from precondition to recommendation. +> +> **Reopening requires a new amendment**, not a reading of this one. Any future proposal must first +> answer §B.1's aliasing question and produce the §B.4(1) measurement — a real per-helper editable-share +> number, not the `218/522` heuristic superset. ADR 0089 §2 names Phase D — "descend into same-module helper functions … projecting each `_fn(msg, …)` call as an expandable group whose rows are the helper body's recognized actions, edited in place diff --git a/docs/adr/0089-recognition-first-lens-native-idioms.md b/docs/adr/0089-recognition-first-lens-native-idioms.md index 1aa12817..d44c579c 100644 --- a/docs/adr/0089-recognition-first-lens-native-idioms.md +++ b/docs/adr/0089-recognition-first-lens-native-idioms.md @@ -46,6 +46,26 @@ When a `msg.set` value (or a computed local) is a recognizable transform of a fi `for i in range(1, msg.count_segments("SEG")+1)` → **For each SEG segment** (segment id editable) (111); `for x in msg.groups()/segments()` → native iteration (24, already recognized — keep); `if current_environment() in (...)` → **Environment gate** (44); `if ` → **When field …** condition (206); `if .search(...)` → **Filter/guard** (36); `return None` → **Filter (drop)** row (150). Control rows may stay structure-only (read-only header) where the body is what's edited, but the *header operands* (segment id, environment list, field path) should be editable where unambiguous. ### Phase D — helper descent (265 delegating call-sites + the writes inside them) + +> ⛔ **DECLINED by owner ruling 2026-07-30 — too risky. Do not build this phase.** Phase D is a +> *grammar* widening under [ADR 0076](0076-typed-action-vocabulary-action-list-lens.md) §2, so it was +> specified and priced as **ADR 0076 Amendment B** and declined there; read that amendment before +> re-proposing anything in this section. Two findings killed it: **aliasing** — two call sites of the +> same helper produce identical child row spans, `expect_src` matches both, and an edit to one silently +> rewrites the other, which no ADR addresses — and **yield**, which is unmeasured and may be *negative*: +> `msg["X"] = v` is not recognized (only `msg.set(...)` is), and the shipped sample helper +> `samples/config/_demo_oru_transforms.py` writes exclusively in that form, so descending it returns six +> opaque rows and zero editable ones. +> +> **The "265 delegating call-sites" figure below is a statement-scan count and is not a yield estimate.** +> Descent makes nothing editable by itself — it replaces opaque delegating rows with helper-body rows +> whose editability is whatever Phases A–C already achieve *inside* helpers. +> +> **Better lever, not declined:** teach Phase A the `ast.Assign`-to-`ast.Subscript` form (`msg["X"] = v`). +> It converts writes into editable rows without touching the row shape, and is on no phase list. +> +> The paragraph below is the original Phase D text, retained unchanged for the record. + Handlers delegate to `_`-prefixed helper functions (`_msh(msg)`, `_pid(msg)`); the bulk of the 1,283 native writes live **inside** those helpers, invisible when you open the calling Handler. To satisfy "see *everything* editable," the lens must **descend into same-module helper functions** — projecting each `_fn(msg, …)` call as an expandable group whose rows are the helper body's recognized actions, edited in place (rewrites target the helper's own line span). This is the largest structural lever and the highest-risk (cross-function byte-stable rewrite); it ships after A–C prove the recognition rules. ### Phase E — compute chains (688 computed locals; partial by design) diff --git a/docs/adr/README.md b/docs/adr/README.md index c5245a22..b7e57a02 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -108,7 +108,7 @@ what is withheld and what you can request. | [0073](0073-ownership-scoped-recovery-single-consumer-lanes.md) | Ownership-scoped recovery + single delivery consumer per outbound lane — the N-active-shards-on-one-unified-store reliability runtime (builds ADR 0063's deferred primitive): `reset_stale_inflight(owned=OwnedLanes)` scopes startup/DR crash recovery to a shard's config-graph lanes (channel_id for ingress/routed/response, destination_name for outbound; empty set matches nothing; residual-`IN`/`ANY` predicate keeps the WS-B ready-index seek); deterministic rendezvous (sha256 HRW) outbound-lane ownership over the pinned shard universe, gated at the wake boundary + pooled lane provider + per-lane spawn (predicate, not set — a reload-dropped lane keeps exactly its owner); `--shard`+`[cluster]` refused fail-closed; shard-set-changing reloads refused (fleet restart required); owner-only outbound controls/purge (409 names the owner; `/connections` rows carry `owner_shard`); sharded-only non-owned-lane buildup/stall watchdog (hung-owner paging). N-active stays gated on the clean 4-engine no-loss bench before SYSTEM-REQUIREMENTS calls it supported | Accepted (2026-07-06) — built | | [0074](0074-adopter-capacity-estimator.md) | Adopter-run capacity estimator (BACKLOG #96) — productize the **built** `harness/load/` rate-walk + zero-loss reconcile as a supported `messagefoundry capacity` command an adopter points at *their* box/store/config to answer "does this carry my ~36 msg/s hospital with headroom?"; reports the **per-interface** no-loss ceiling + engine-wide aggregate + a **backend-aware limiting-factor** label + provision-at-≤50%-of-ceiling guidance. Hard requirements: **isolated throwaway store** (refuse to run against a non-isolated/production store — count-and-log intact), **synthetic PHI-free** payloads only (ADR 0030), backend-aware labels (SQLite knob rankings do **not** transfer to server backends — the B12 lesson), explicit harness-ceiling caveats (~450/s ACK per driver, ~135–144/s delivered per sink, poller-zero ⇒ sub-ceiling knee). v1 = rate-walk + limiting-factor labels; deeper per-stage diagnostics deferred. A productization of throughput-campaign evidence (PR #768), not a new measurement effort | Accepted (2026-07-07) — ratified. ⛔ **BUILD GATED (2026-07-14)**: a validity re-check vs STEP-4 Arm 0 found **14 confirmed blockers** in the *measurement* method (the named "only success gate" over-reports by **3–5.5×**; the poller-zero failure mode *satisfies* it; the estimand is intake not delivery; the aggregate-is-the-sum rule is measured-false; the ceiling is instant-partner). **Premise + hard requirements + the fail-closed guard layer still hold and remain buildable.** See the ADR's 2026-07-14 Amendment | | [0075](0075-per-hop-sql-statement-batching.md) | Per-hop SQL statement batching (`[pipeline].batch_handoff_statements`, default-ON (emergency off-switch), fail-closed, SQL-Server-only) — the last [ADR 0069](0069-durable-write-throughput-lever.md)-named feed lever ("batching SQL statements per executor hop"): fold a multi-statement handoff **body** (guard-DELETE + inserts + finalize applock + `messages.status` UPDATE + event, from the SAME shared `(sql,params)` builders that keep the async/sync twins in lockstep) into 1–2 `pyodbc.execute()` batches, cutting **network round-trips + aioodbc executor crossings — NOT transactions** (`commits/msg` stays 2.000; the ADR 0069 cross-lane/commit fence is not hit). Attacks the serial-RT co-bottleneck the [ADR 0071](0071-cut-executor-round-trips-b5.md) B5 fusion NO-GO could not (the ~11 ms inter-box store RTT × ~4–5 RT/msg) and works on the **default async path**. Microbench (adversarially re-reviewed, counts VERIFIED honest): per-hop drop 27–50%, but the **≥40% figure is CONDITIONAL on the applock-rc-fold** — under the strict interpretation 27–33% clears nothing, so the microbench JUSTIFIES a live-rig e2e A/B, it does not substitute for it. Content-vs-infra error attribution + a golden-SQL/living RT-count CI gate are load-bearing. **Promoted default-ON 2026-07-08** (Bench B distance-insurance A/B: harmless-near + helps-far, green SS correctness precondition) — flag retained only as an emergency off-switch | Accepted (2026-07-07) — promoted default-ON 2026-07-08 | -| [0076](0076-typed-action-vocabulary-action-list-lens.md) | Typed action vocabulary + structured action-list lens over Python Handlers (BACKLOG #222, under the #26 amendment) — phase 1: `messagefoundry/actions.py`, pure typed helpers mirroring Corepoint's action classes over the existing `Message` API (control flow stays native Python; no flow wrappers); phase 2: static-only `lens parse --json` (stdlib `ast`, never imports/executes config) + a VS Code `CustomTextEditorProvider` rendering any *parseable* Handler as a Corepoint-style action-list view — typed rows for the bounded structural grammar, in-place read-only `code` rows for anything else (coverage invariant: rows exactly partition the def body — never drop/reorder/synthesize) and whole-file refusal only on parse failure; phase 3 (bake + owner-go gated): row-scoped line-splice rewrites (byte-stable outside the edited row). The `.py` stays the **only artifact and execution path** — no interpreter, no declarative artifact, no canvas; InterSystems guardrails adopted (sync-on-save, one-editor-at-a-time, degrade-to-text-editor); live values reuse the ADR 0072 stream + `--show-phi` gate unchanged; stdlib-only, no new runtime dep (libcst deferred) | Accepted (2026-07-10) — **Amendment A ACCEPTED 2026-07-30 (owner-ratified, in force):** a `note` row kind so comment-only rows stop projecting as opaque `code`, **superseding ADR 0106 §5 (L)**, and reconciling the §3 enum to the kinds the parser already emits; build = BACKLOG #248. **Amendment B still PROPOSED, not ratified:** ADR 0089 Phase D "helper descent", specified and priced but **not buildable** — yield unmeasured and possibly negative, §B.4 preconditions unmet | +| [0076](0076-typed-action-vocabulary-action-list-lens.md) | Typed action vocabulary + structured action-list lens over Python Handlers (BACKLOG #222, under the #26 amendment) — phase 1: `messagefoundry/actions.py`, pure typed helpers mirroring Corepoint's action classes over the existing `Message` API (control flow stays native Python; no flow wrappers); phase 2: static-only `lens parse --json` (stdlib `ast`, never imports/executes config) + a VS Code `CustomTextEditorProvider` rendering any *parseable* Handler as a Corepoint-style action-list view — typed rows for the bounded structural grammar, in-place read-only `code` rows for anything else (coverage invariant: rows exactly partition the def body — never drop/reorder/synthesize) and whole-file refusal only on parse failure; phase 3 (bake + owner-go gated): row-scoped line-splice rewrites (byte-stable outside the edited row). The `.py` stays the **only artifact and execution path** — no interpreter, no declarative artifact, no canvas; InterSystems guardrails adopted (sync-on-save, one-editor-at-a-time, degrade-to-text-editor); live values reuse the ADR 0072 stream + `--show-phi` gate unchanged; stdlib-only, no new runtime dep (libcst deferred) | Accepted (2026-07-10) — **Amendment A ACCEPTED 2026-07-30 (owner-ratified, in force):** a `note` row kind so comment-only rows stop projecting as opaque `code`, **superseding ADR 0106 §5 (L)**, and reconciling the §3 enum to the kinds the parser already emits; build = BACKLOG #248. **Amendment B ⛔ DECLINED 2026-07-30 (owner ruling — too risky):** ADR 0089 Phase D "helper descent" is **not adopted and not to be built** — duplicate-call-site aliasing is unsolved in any ADR, and the yield is unmeasured and possibly negative; spec retained for auditability, reopening needs a new amendment. Better lever, not declined: recognize `msg["X"] = v` | | [0077](0077-action-bound-step-up.md) | Action-bound step-up re-verification for durable-takeover operations (ASVS 2.2.4 / BACKLOG #187) — a fresh re-authentication bound to the *specific* privileged action, not merely to a recent login, so a hijacked live session cannot silently perform a durable takeover | Accepted | | [0078](0078-certificate-revocation-posture.md) | Certificate revocation posture (OCSP/CRL, ASVS 12.1.4, BACKLOG #201) — **enforced start-time refusal + delegated proxy**, NOT in-engine OCSP (stdlib `ssl` has no OCSP/CRL fetch; a hand-rolled responder fetch fights on-prem offline-by-default). Refines [ADR 0002](0002-phase2-transport-security-and-strong-auth.md)'s *documented* revocation residual into an **enforced** control: `serve` REFUSES to start an in-process, off-loopback `[api]` TLS bind (`tls_cert_file` set + non-loopback `host`) UNLESS revocation is *proven in front* — a declared TLS-terminating proxy (`tls_terminated_upstream` + `trusted_proxies`, which does its own OCSP-must-staple/CRL) OR the operator opt-out `MEFOR_TLS_REVOCATION_ATTESTED=1`. Secure default = refuse; the loopback default + proxy-terminated paths start **byte-identically** (the pure `config/tls_policy.py:in_process_tls_revocation_refused` predicate short-circuits). Compensating controls: the SQL-Server SChannel path already does OS-managed revocation, and `pipeline/cert_expiry.py` alerts on expiring certs (steering short-lived certs). **Amendment 2026-07-12 (BACKLOG #201 residual):** extends the SAME posture-keyed refusal to the OUTBOUND verifying-TLS connectors — pure `revocation_hop_disposition(*, is_phi, production, is_loopback_hop, proxy_proven, attested)` + `RevocationHopGuard` in `config/tls_policy.py`, wired into MLLP-over-TLS egress, the REST/SOAP/FHIR https paths (`refuse_unrevoked_verified_hop`), and the Postgres asyncpg store hop (`_refuse_store_revocation`); per-connection `tls_revocation_attested` + the blanket `MEFOR_TLS_REVOCATION_ATTESTED` env are the opt-outs. Composes with #200 (fires only on a VERIFYING hop — no double-refusal). Still out of scope: SQL-Server/SChannel (already OS-managed), DICOM-SCU/FTPS, the FhirLookup read path. Flips the ASVS 12.1.4 row from documented-residual to enforced-delegation | Accepted (2026-07-10; amended 2026-07-12) — built | | [0079](0079-kerberos-idp-session-coordination.md) | Kerberos/AD engine-session lifetime coordinated with the directory (IdP) — terminate engine sessions when the directory revokes or disables the account, rather than letting a local session outlive its AD principal. **Amendment 2026-07-21:** mechanism 1's preferred input — the Kerberos ticket `endtime` — is **unobtainable** via pyspnego 0.12.1 (no expiry on the public `ContextProxy`; `SSPIProxy.step()` discards sspilib's `AcceptContextResult.expiry`), so on the Kerberos/LDAPS path it would degrade to a second local constant dressed as directory data. ASVS 7.1.3 therefore **closed by ACCEPTANCE** (signed register row, theme 3) and this ADR's Proposed→Accepted trigger is **NOT fired**; mechanism 1 ships only where the datum genuinely exists — the federated `id_token.exp` session cap. Mechanism 2 (background re-validation loop) stays deferred. **Amendment 2026-07-22: mechanism 2 is BUILT.** The deferral's stated cost was void — the candidate set derives from the existing `list_users()` + `list_sessions()`, so **no `sessions` schema change on any backend** (provenance columns were only ever mechanism 1's need). Recorded narrowing: `require_step_up` performs **no** directory bind (it compares the stored `reauth_at`), so the step-up surface is protected by inability to REFRESH — leaving a ≤`step_up_max_age_seconds` residual — while **bulk/raw PHI reads** (`require_phi_read`, 120/min) and **connection start/stop** (`require_paced`) survived to the 12 h cap. Adds three properties the design did not name: **two-strike** before revoking (the lookup returns one `None` for disabled ∪ deleted ∪ wrong-search-base), **all-or-nothing passes** (planned in `auth/reconcile.py` before any write), and a **mass-revoke circuit breaker** — a bad search base answers "not found" for everyone, so a pass exceeding **both** `ad_session_revoke_max` (5) **and** `ad_session_revoke_max_fraction` (0.34) aborts + alerts. AND, not OR: the floor alone signs out a 5-person site, the proportion alone fires on a 3-of-3 offboarding. Group re-diff rides the pass free (demotions no longer wait for a login); channel scope deliberately excluded. Default OFF (`ad_session_recheck_seconds = 0`) | Accepted (mechanism 2 built 2026-07-22; mech 1 Kerberos path closed by acceptance, federated path shipped in ADR 0142) |