Skip to content

ADR 0114 follow-ups: the AC-7 degraded gauge, and VIEW DEFINITION misdiagnosed as a missing proc - #97

Merged
wshallwshall merged 3 commits into
mainfrom
adr0114-followups
Jul 31, 2026
Merged

ADR 0114 follow-ups: the AC-7 degraded gauge, and VIEW DEFINITION misdiagnosed as a missing proc#97
wshallwshall merged 3 commits into
mainfrom
adr0114-followups

Conversation

@wshallwshall

Copy link
Copy Markdown
Collaborator

The two follow-ups PR #86 deliberately held out of the bug fix. Both are ADR 0114 §4 startup-gate items; neither belonged in a bug-fix PR.

1. A missing VIEW DEFINITION grant was reported as a missing proc

ADR 0114 §4 has always specified the gate's probe (a) as "OBJECT_ID of both procs", but the implementation folded (a) into (b) and inferred absence from a NULL OBJECT_DEFINITION.

MEASURED: a principal holding only EXECUTE on the proc gets a non-NULL OBJECT_ID and a NULL OBJECT_DEFINITION, and the compat probe still passes. So a deployed, working, correct procedure was reported as missing, and the operator was sent to grant CREATE PROCEDURE — neither the cause nor the cure. WITH ENCRYPTION produces the identical NULL.

This is not hypothetical: the sub-lever B design note in the same module explicitly serves "a fleet whose DB principal can never hold CREATE PROCEDURE" — DBA-provisioned procs plus a least-privilege app principal, which is exactly the posture that hits it.

condition reason
OBJECT_ID NULL genuinely absent — guarded DDL skipped, CREATE PROCEDURE/ALTER-on-schema denied, or a pre-2016-SP1 engine
OBJECT_ID non-NULL, OBJECT_DEFINITION NULL deployed but unreadable — GRANT VIEW DEFINITION, or the module is WITH ENCRYPTION

Both still degrade — the gate hashes the body and cannot pass on one it cannot read — so no accept/reject behaviour changed, only the diagnosis.

The offline stub pins the probe SQL by exact match (a typo'd probe must fail loudly rather than silently match). That pin moved with the SQL and stayed exact; it now also asserts both placeholders bind the same object, so a probe reporting one proc's id against another's body fails too. Verified RED against the unfixed store, green with the fix.

2. The AC-7 degraded gauge did not exist

AC-7 requires "a WARNING naming the reason + degraded gauge", and §4's compensating-control story assumes an operator can SEE the degraded state. Nobody could: claim_proc_effective / claim_proc_degraded_reason were read by the store's own tests and nothing else — no /stats, no /status, no /metrics, no console. The whole signal was one WARNING at open(), in a log nobody was watching, that until #86 named the wrong cause. That is a load-bearing part of why the lever sat inert in every deployment for its entire life.

A claim_proc_status() store accessor now feeds three surfaces:

surface carries
GET /statusclaim_proc effective, the human-readable degraded_reason, the matched head_forms
GET /metrics messagefoundry_store_claim_proc_effective, messagefoundry_store_claim_proc_head_verbatim
console store panel (/ui/status) active-vs-degraded, plus the reason when degraded / the head forms when green

Three shape decisions, recorded in the ADR so they are not re-litigated:

  • Absent, not 0/false, when the lever was never requested. A constant 0 on every SQLite fleet is unalertable noise, and absence keeps "not requested" distinct from "requested and degraded".
  • No reason label in the exposition. The reason is free text embedding a proc name and, on the probe-failure arm, an exception string — a label would be unbounded cardinality and a breach of the exporter's strict {connection, destination, status, version, le} allowlist. The string goes to /status and the console.
  • It does not feed the console's engine-health heart. Claims keep flowing on the batch, so a degrade is a performance lever not paying off, not an unwell store; making the nav cry wolf would devalue the signal that means it is.

head_forms is surfaced for the same reason it is logged: a fleet reporting verbatim is a live counterexample to _CLAIM_PROC_STORED_HEADS's compatibility assumption, and was previously visible only at INFO.

Observability only — the gate's accept/degrade logic, just adjudicated in #86, is untouched.

Seam bump

Rendering a new SystemStatus field on the console is a seam change: ENGINE_UI_SEAM and SUPPORTED_ENGINE_SEAMS both move to 16, golden snapshot refreshed. A separate seam rather than a correction to the unreleased v15 — v15 is a SecurityPosture change, and folding an unrelated DTO into it would make that note describe a field set it does not cover.

ADR

§4 now names the surfaces that publish the gauge and records the three shape decisions. AC-7 gains the unreadable-definition condition; new AC-7c requires the gauge to be a surface an operator can READ, not merely an attribute — AC-7 as written required a gauge and nothing required anyone to be able to see it, the same one-directional gap the 2026-07-30 amendment found in the gate itself.

Verification

  • Full offline suite: 9529 passed, 820 skipped (12m30s, clean env).
  • Targeted -k "adr0114 or sqlserver or claim or fifo": 468 passed (was 457).
  • Live (MEFOR_TEST_SQLSERVER=1): 7 passed — the required 6 plus a new leg pinning on a real server the premise no offline stub can show, that OBJECT_ID and OBJECT_DEFINITION genuinely disagree. It uses WITH ENCRYPTION because that needs no security principal (self-cleaning; confirmed no stray proc). The EXECUTE-without-VIEW DEFINITION half stays deferred with AC-10's other permission scenarios.
  • ruff check / ruff format --check clean; mypy --strict clean over the changed modules.

Not touched

Per the handoff's scope discipline: the gate's accepted-set design and per-proc keying, _normalize_tsql, the AC-8 body lint, and _claim_proc_body()'s byte-pinned output (no re-pin, no forced DDL re-apply on any live database).

No BACKLOG number was allocated — both items were implemented rather than filed, so no ADR file or ## heading was created. The ledger gate passed on every commit.

🤖 Generated with Claude Code

@wshallwshall
wshallwshall enabled auto-merge (squash) July 31, 2026 13:09
@wshallwshall
wshallwshall force-pushed the adr0114-followups branch 2 times, most recently from 20217f6 to bf2a4b1 Compare July 31, 2026 13:24
wshallwshall and others added 3 commits July 31, 2026 08:48
ADR 0114 §4 has always specified the startup gate's probe (a) as "OBJECT_ID of
both procs", but the implementation folded (a) into (b) and inferred absence from
a NULL OBJECT_DEFINITION.

MEASURED: a principal holding only EXECUTE on the proc gets a non-NULL OBJECT_ID
and a NULL OBJECT_DEFINITION, and the compat probe still passes. So a deployed,
working, correct procedure was reported as *missing* and the operator was sent to
grant CREATE PROCEDURE — neither the cause nor the cure. WITH ENCRYPTION produces
the identical NULL. This is not hypothetical: the sub-lever B design note in the
same module explicitly serves a fleet whose principal can never hold CREATE
PROCEDURE (DBA-provisioned procs + a least-privilege app principal), which is
exactly the posture that hits it.

The probe now returns OBJECT_ID beside the definition and the two conditions get
separate reasons; the actual cure, GRANT VIEW DEFINITION, is named. Both still
DEGRADE — the gate hashes the body and cannot pass on one it cannot read — so no
accept/reject behaviour changed, only the diagnosis.

The offline stub pins the probe SQL by exact match (a typo'd probe must fail
loudly rather than silently match); that pin moved with the SQL and stayed exact,
and now also asserts both placeholders bind the same object. New offline legs
cover both arms and fail without the fix. A live leg pins the premise no stub can
show — that OBJECT_ID and OBJECT_DEFINITION genuinely disagree on a real server —
using WITH ENCRYPTION, which needs no security principal; the permission half
stays deferred with AC-10's other permission scenarios.

Also adds the store-side half of AC-7's degraded gauge: a claim_proc_status()
accessor on the store protocol (None on every backend without the lever and when
the flag is off, so "not requested" stays distinguishable from "requested and
degraded"). The surfaces that read it land next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… the console

AC-7 requires "a WARNING naming the reason + degraded gauge", and §4's
compensating-control story assumes an operator can SEE the degraded state. Nobody
could: claim_proc_effective / claim_proc_degraded_reason were read by the store's
own tests and nothing else — no /stats, no /status, no /metrics, no console. The
whole operator signal was one WARNING at open(), in a log nobody was watching,
that until PR #86 named the wrong cause. That is a load-bearing part of why the
lever could sit inert in every deployment for its entire life.

Surfaces the store's claim_proc_status() on:

  * GET /status  -> claim_proc: effective, the human-readable degraded_reason,
                    and the matched head_forms
  * GET /metrics -> messagefoundry_store_claim_proc_effective and
                    messagefoundry_store_claim_proc_head_verbatim
  * /ui/status   -> store-panel rows

Three shape decisions. The field/series are ABSENT, not 0/false, when the lever
was never requested: a constant 0 on every SQLite fleet is unalertable noise, and
absence keeps "not requested" distinct from "requested and degraded". No reason
LABEL in the exposition — the reason is free text embedding a proc name and, on
the probe-failure arm, an exception string, so a label would be unbounded
cardinality and a breach of the exporter's strict {connection, destination,
status, version, le} allowlist; the string goes to /status and the console. And it
deliberately does NOT feed the console's engine-health heart: claims keep flowing
on the batch, so a degrade is a lever not paying off, not an unwell store, and
making the nav cry wolf would devalue the signal that means it is.

head_forms is surfaced for the same reason it is logged: a fleet reporting
"verbatim" is a live counterexample to _CLAIM_PROC_STORED_HEADS's compatibility
assumption, and was previously visible only at INFO.

Observability only — the gate's accept/degrade logic is untouched. The new tests
assert the rendered output of each surface, not the existence of a property.

Rendering a new SystemStatus field on the console is a seam change, so
ENGINE_UI_SEAM and the console's SUPPORTED_ENGINE_SEAMS both move to 16 and the
golden snapshot is refreshed. A separate seam rather than a correction to the
unreleased v15: v15 is a SecurityPosture change, and folding an unrelated DTO into
it would make that note describe a field set it does not cover.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…le split

Amends §4 and the acceptance criteria for the two follow-ups the 2026-07-30
amendment deliberately held out of the bug fix.

"Sets a degraded gauge" stops being aspirational: §4 now names the three surfaces
that publish it, and records the three shape decisions (absent rather than 0 when
the lever is not requested; no free-text reason label in the exposition; it does
not feed the console's engine-health heart) so they are not re-litigated.

AC-7 gains the unreadable-definition condition. New AC-7c requires the gauge to be
a surface an operator can READ, not merely an attribute — AC-7 as written required
a gauge and nothing required anyone to be able to see it, which is the same
one-directional gap the 2026-07-30 amendment found in the gate itself.

Also corrects the record on probe (a): the ADR always specified an OBJECT_ID
probe, and the implementation had folded it into the OBJECT_DEFINITION read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wshallwshall
wshallwshall merged commit 2fd1ad8 into main Jul 31, 2026
33 checks passed
@wshallwshall
wshallwshall deleted the adr0114-followups branch July 31, 2026 14:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant