fix(store): ADR 0114 sub-lever A — the claim-proc startup gate has never passed - #86
Merged
Merged
Conversation
wshallwshall
force-pushed
the
claude/adr0114-claim-proc-gate
branch
from
July 30, 2026 22:49
e49d720 to
787f21b
Compare
…re, and pin the wiring
Sixteen test modules gate themselves at import on MEFOR_TEST_SQLSERVER /
MEFOR_TEST_POSTGRES and are named by no workflow step. A module-level pytestmark skipif
yields zero collected tests when the gate is unset, so these did not pass, did not fail,
and did not appear as skips on any trigger — not a PR, not push-to-main, not the nightly
cron. 84 tests, dark. Among them: engine-shard crash recovery on both server backends,
the PostgreSQL failover suite (its SQL Server twin has run for months), the ADR 0048/0049
DR seed gate that must refuse to activate a standby onto a non-empty store, and the
.mfbak backup runner against a real server DB.
Wires all sixteen into four new steps — two on sqlserver-store (engine-shard and
statement-dispatch; DR seed-gate and backup) and two on postgres-store (failover and
shard recovery; DR seed-gate and backup) — reusing each leg's existing env block and, on
SQL Server, the pyodbc#1459 native-crash retry wrapper.
Extends the `serverdb` change-detection alternation to admit them, plus four files the
legs already ran that it never matched (connscale_postgres, load_failover_{postgres,
sqlserver}, load_runner). Editing any of them now pulls the leg that proves it; before,
they were reachable only by the nightly cron. The regex's own comment already required
this ("Keep this in sync with those steps") — nothing enforced it.
tests/test_serverdb_ci_coverage.py is that enforcement. It asserts (a) every module-gated
suite is named by a workflow step and (b) every file a gated step runs is matched by the
alternation. Detection is AST-based and deliberately narrow: only a module-level gate
counts, because that is the shape that yields zero executed tests. A per-test decorator
leaves the SQLite cases running, which is a weaker and separately-tracked gap — keeping
the assertion sharp means a failure always means "this file runs nowhere" and the
allow-list can stay empty.
Verified: both assertions go red under mutation (unwire one file; drop one alternation
entry) and green on restore. All 16 suites collect — 84 tests, no import rot.
CI is the remaining verifier: these suites have not executed in a long time, so some may
legitimately fail against the real backends. That is the point of running them.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
(cherry picked from commit 8e24dbf)
…ver passed OBJECT_DEFINITION() does not return a CREATE OR ALTER module verbatim: SQL Server deletes the OR and ALTER keyword TOKENS and keeps their separators, so a head submitted as `CREATE OR ALTER PROCEDURE dbo.x` comes back as `CREATE` + three spaces + `PROCEDURE dbo.x` (delta exactly 7; everything after the head is byte-identical). The gate hashed the SUBMITTED text, so it could never match. Sub-lever A has therefore been INERT in every deployment since it shipped — the gate degraded to the ad-hoc batch on every open, and no test noticed because the offline fixture fed the submitted body back as the "deployed" body, modelling the server as an identity function. Both sides of the comparison were the same function of the same argument. Measured on SQL Server 2022 16.0.4255.1 and 2025 17.0.4055.5, compat 130/160/170, across five deploy paths. Live legs go 5-failed/1-passed -> 6 passed. THE FIX IS SHIPPED-SIDE ONLY. The gate compares the deployed hash against a small set of code-controlled constants — the head forms a server may store for a module THIS build deployed (_CLAIM_PROC_STORED_HEADS: the measured `rewritten` form, plus `verbatim` for a hypothetical non-rewriting engine). _claim_proc_body() renders byte-identically, so _claim_proc_ddl, _SCHEMA, _schema_hash() and both golden body pins are untouched: no re-pin, no forced DDL re-apply on any live database. The expected map is keyed PER PROC. A single flat accepted-set would take the cid body served under the dst name — reachable with no tampering intent via sp_rename, which does not rewrite sys.sql_modules.definition — and silently swap the lane predicate, claiming zero rows forever. Head spellings this deploy path cannot emit (CREATE PROC, differing case) keep failing the gate: each is affirmative evidence of an out-of-band hand deploy, which is the AC-7 event. A two-sided canonicalization would launder exactly that signal, and its correctness rests on regex minutiae no finite assertion can pin (mutation-scored 0/7 against this suite; the shipped-side form scores 3/3). ALSO IN THIS COMMIT, because the gate fix makes them reachable: * The proc CALL's 9 parameter pins are PERSISTENT cursor state and were never cleared. At OUTBOUND the same pooled cursor runs the H2 delivery probe, binding an NVARCHAR id against the stale SQL_DOUBLE descriptor for @now FLOAT -> client 22018 -> rollback -> outbound delivery collapse. Fixing the gate alone would arm this for the first operator who sets fifo_claim_proc=true. Cleared the moment the CALL's result is drained. Verified on pyodbc 5.3.0 / ODBC Driver 18 that a ZERO-parameter execute tolerates surplus descriptors, so the shielded `SET LOCK_TIMEOUT -1;` reset in the finally-guard is unaffected and the exposure is exactly the H2 bind chain. * AC-11 no-match parity was honoured only on the two flagged branches. The ad-hoc batch bound the raw lane list into (VALUES (?),...) feeding a `DECLARE @heads TABLE (lane NVARCHAR(256) NOT NULL` — SQL Server evaluates that narrowing conversion on the outer constant scan before the CROSS APPLY filters it, so with ANSI_WARNINGS ON an oversized lane raises 2628 even when zero rows would match. 2628 is not 1222, so it is not translated to EMPTY-all: it rolls back and re-raises. The skip is hoisted ahead of the dispatch-path split so all three branches agree. Clamp still runs before the skip (the tested contract). TESTS — the fix's only evidence. The offline suite could not previously tell a working gate from a broken one; three contradictory implementations all scored 51/51. Both fixture doors are closed (the `_gate_rows` default AND the whitespace test that passed cid_body= explicitly), the "deployed" body now comes from an INDEPENDENT model of the server rewrite with a liveness receipt, and new tests pin the accepted set by set-equality, reject cross-proc substitution, reject un-emittable head spellings, and require the anchor break to degrade diagnosably. The live tamper leg gains the in-test positive control it lacked — under the old defect the gate rejected everything, so its degrade assertions were vacuous. Mutation check: bug 10 failed, flattened-key 3 failed, over-wide 8 failed, pin-clear no-op 1 failed; correct implementation 64 passed. DOCS — three docstrings asserted the false premise ("OBJECT_DEFINITION() preserves the definition text as executed"). The operator-facing degraded reason prescribed `DELETE FROM schema_meta`, which could not work: the re-apply submits the same text and mismatches again, forever. ADR 0114 amended; AC-7b added, because AC-7 is one-directional and was LITERALLY SATISFIED by the defect — nothing anywhere required a correctly deployed proc to pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wshallwshall
force-pushed
the
claude/adr0114-claim-proc-gate
branch
from
July 30, 2026 23:17
787f21b to
9541d4b
Compare
wshallwshall
added a commit
that referenced
this pull request
Jul 31, 2026
… 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>
wshallwshall
added a commit
that referenced
this pull request
Jul 31, 2026
… 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>
wshallwshall
added a commit
that referenced
this pull request
Jul 31, 2026
… 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>
wshallwshall
added a commit
that referenced
this pull request
Jul 31, 2026
…diagnosed as a missing proc (#97) * fix(store): a missing VIEW DEFINITION grant read as a missing claim proc 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> * feat(ops): publish ADR 0114's degraded gauge on /status, /metrics and 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> * docs(adr-0114): record the degraded gauge and the missing-vs-unreadable 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> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
OBJECT_DEFINITION()does not return aCREATE OR ALTERmodule verbatim. SQL Server deletes theORandALTERkeyword tokens and keeps their separators, so a head submitted asCREATE OR ALTER PROCEDURE dbo.xcomes back asCREATE+ three spaces +PROCEDURE dbo.x— character delta exactly 7, everything after the head byte-identical.The ADR 0114 startup gate hashed the submitted text. It therefore could never match, and sub-lever A has been inert in every deployment since it shipped —
fifo_claim_procdegraded to the ad-hoc batch on every open, silently, for the whole life of the feature.Measured on SQL Server 2022 16.0.4255.1 and 2025 17.0.4055.5, compat 130/160/170, across five deploy paths (fresh
CREATE, theOR ALTERre-apply, a plain batch, inside the shipped guardedEXEC(N'…'), and an out-of-bandALTER PROCEDURE— which the engine also rewrites, to a single-spacedCREATE PROCEDURE).Live legs: 5 failed / 1 passed → 6 passed.
Why the tests didn't catch it
_gate_rowsresolved its"SHIPPED"sentinel by callingss._claim_proc_body(...)and handing that back as the fakeOBJECT_DEFINITIONanswer — modelling the server as an identity function. Both sides of the gate's comparison were the same function of the same argument. Three mutually contradictory implementations (the bug, a correct fix, and an over-wide fix that blinds the gate to the whole signature) all scored 51/51 against it.There was a second door:
test_ac7_gate_tolerates_whitespace_differencesbypassed the fixture default by passingcid_body=explicitly, with a string that still carriedCREATE OR ALTER. Fixing only the default would have left the suite blind in exactly the same way.Both are closed here.
The fix — shipped-side only
The gate now compares the deployed hash against a small set of code-controlled constants: the head forms a server may store for a module this build deployed (
_CLAIM_PROC_STORED_HEADS— the measuredrewrittenform, plusverbatimfor a hypothetical non-rewriting engine, which is simply the text we submitted)._claim_proc_body()renders byte-identically, so_claim_proc_ddl,_SCHEMA,_schema_hash()and both golden body pins are untouched — no re-pin, and no forced DDL re-apply on any live database. Verified: the body hashes are still9c685181…/0eaf2de2….Three design points worth review:
cidbody served under thedstname — reachable with no tampering intent viasp_rename, which does not rewritesys.sql_modules.definition— and silently swap the lane predicate, claiming zero rows forever.CREATE PROC, differing case). Each is affirmative evidence of an out-of-band hand deploy, which is the AC-7 event. A two-sided canonicalization would launder exactly that signal.^, addingMULTILINE, droppingcount=1, widening to\w+, eating the object name, droppingIGNORECASEall survive every test, and one of them makes the gate accept a body deployed under the other lane family's name. The shipped-side form is pinned by one set-equality assertion.Two more fixes, in the same PR because the gate fix makes them reachable
The pin leak — this is why D1 must not land alone
The proc CALL pins 9 parameter descriptors on the pooled cursor (
descriptor[0] = SQL_DOUBLEfor@now FLOAT).setinputsizesis persistent cursor state, and at OUTBOUND the same cursor then runs the H2 delivery probe, binding an NVARCHAR id against the staleSQL_DOUBLEdescriptor → client22018→ rollback → outbound delivery collapse (measured previously at 1122 errors == 1122 failed deliveries). Pins are now cleared the moment the CALL's result is drained.maintoday ships the lever but not the fixes. A prior cherry-pick landed the ADR 0114 flag architecture while reverting this and the gate fix. Flags default OFF so it is inert — but anyone settingfifo_claim_proc=trueon currentmaingets the dead gate and the collapse. Fixing the gate alone would arm the collapse for the first operator who flips the flag.CI could never have caught it. Every test in
test_adr0114_claim_proc_live.pyclaims atStage.INGRESS; the only OUTBOUND leg that exists anywhere is the one this PR adds. So landing the gate fix and wiring the file — the obvious plan — would have turned that file green in CI while the collapse stayed live in production, with nothing downstream able to see it.Measured on pyodbc 5.3.0 / ODBC Driver 18: a zero-parameter execute tolerates surplus descriptors, so the shielded
SET LOCK_TIMEOUT -1;reset in thefinallyguard is unaffected and the exposure is exactly the H2 bind chain. That measurement carries its own positive control — the same run reproduced[22018] Invalid character value for cast specification (0)on the parameterized bind, where(0)is theSQL_DOUBLE@nowdescriptor. A recorded negative with a demonstrated positive control, so nobody has to re-open the question.AC-11 no-match parity on the ad-hoc batch
Only the two flagged branches honoured it. The batch bound the raw lane list into
(VALUES (?),…)feeding aDECLARE @heads TABLE (lane NVARCHAR(256) NOT NULL— SQL Server evaluates that narrowing conversion on the outer constant scan before theCROSS APPLYfilters it, so withANSI_WARNINGSON an oversized lane raises 2628 even when zero rows would match. 2628 is not 1222, so it is not translated to EMPTY-all: it rolls back and re-raises to the dispatcher.This was not deferrable — fixing the gate made the parity test reach its batch arm and it failed live. The skip is hoisted ahead of the dispatch-path split so all three branches agree. The clamp still runs before the skip, preserving the tested contract (
test_prepared_lane_encoding_shares_the_proc_rulespins 499, not 500).Verification
The mutation row is the point: the previous suite could not distinguish a working gate from a broken one. Every wrong implementation is now caught by a named test.
Docs
Three docstrings asserted the false premise ("OBJECT_DEFINITION() preserves the definition text as executed"). The operator-facing degraded reason prescribed
DELETE FROM schema_meta, which could not work — the re-apply submits the same text, the engine rewrites it identically, and the hash mismatches again, forever. Both corrected, plus an ADR 0114 amendment.AC-7b added. AC-7 is one-directional: it required the gate to degrade on a mismatch, and nothing anywhere required a correctly deployed proc to pass. The defect therefore satisfied AC-7 literally while leaving the lever inert — AC review could not have caught this. Any future gate-shaped AC needs both directions.
CI
Adopts the dormant
ci-suite-wiringcommit (cherry-picked with-x), which wires 16 module-gated suites that ran in no workflow step — 84 tests, dark — into four new steps, and addsadr0114to the path-filter alternation so a test-only follow-up still fires the leg. Includestest_serverdb_ci_coverage.py, which fails if a module-gated suite is added without wiring.Performance
Latency, not capacity. Previously measured at ~65%
claim_meanreduction (~9 ms/claim). Sustained throughput is unchanged — the characterisation of record is "latency-proven, throughput-null on N=4", sourced to the at-knee soak (336.3 vs 337.1, arm-identical) andTO-ENGINE-072, not to the @150/180 re-gate, whose rungs were sub-knee and explicitly declined to project. No throughput claim is made and no published capacity figure changes.Not in this PR — recommended follow-ups, not yet filed
Deliberately out of scope here. Both need a BACKLOG number, and I have not allocated one: the allocator mutates a shared ledger, several sessions are live, and number reuse has corrupted that ledger before. Flagging them for the owner to file (or to tell me to) rather than allocating speculatively mid-review.
The AC-7 degraded gauge does not exist.
claim_proc_effectiveandclaim_proc_degraded_reasonreach no API,/stats,/metricsor console surface — grep returns onlystore/sqlserver.pyand tests. ADR 0114:401 describes the gate as setting a degraded gauge; it does not. The sole operator signal is one WARNING line at open. That is a real part of why this defect ran unnoticed for the life of the feature, and it is not fixed here.A missing
VIEW DEFINITIONgrant is misdiagnosed as a missing proc. A principal holding onlyEXECUTEseesOBJECT_IDnon-NULL butOBJECT_DEFINITIONNULL. The gate treats a NULL body as missing and reports "guarded DDL skipped — CREATE PROCEDURE / ALTER-on-schema denied", sending the operator to entirely the wrong control; the cure isGRANT VIEW DEFINITION.WITH ENCRYPTIONproduces the same NULL and the same misdiagnosis. This is not hypothetical for this codebase — the sub-lever B comment atsqlserver.py:904-905explicitly designs for a fleet whose principal can never holdCREATE PROCEDURE, which is exactly the posture that hits it. Fix: distinguishOBJECT_ID IS NULLfromOBJECT_ID NOT NULL AND OBJECT_DEFINITION IS NULLand name the right grant. I left the probe SQL unchanged here so the gate's stubbed-_fetchonecontract stays pinned.Also deferred: the AC-11 lane-skip ordering question (the clamp currently runs before the skip, so an unmatchable lane still consumes a chunk slot — changing it would serve more lanes per call but alters a tested contract), and the orphaned
vaultall/vaultref namespace, which holds ADR 0116, ADR 0117, two features and 22 BACKLOG items invisible to any normal git query. A separate session is surveying that.🤖 Generated with Claude Code