Skip to content

fix(engine): a failed sys_organization read is no longer answered as an emptiness — the two genuinely-wrong read seams - #12864

Merged
os-zhuang merged 5 commits into
mainfrom
claude/issue-12853-narrow-benign-read-catches
Aug 28, 2026
Merged

fix(engine): a failed sys_organization read is no longer answered as an emptiness — the two genuinely-wrong read seams#12864
os-zhuang merged 5 commits into
mainfrom
claude/issue-12853-narrow-benign-read-catches

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#12853
Fixes#12852

Merged dispatch, as triage specified: the only two read seams graded genuinely wrong out of the eleven parameterless-catch sites from one measurement. Same shape, same repair, same lane, two different package files — one PR, one commit per card. The other nine seams (graded correct-but-undeclared) are untouched, and so is checkGovernance, which #8906 (already closed) covers.

The two seams

Both are the same defect: a catch whose comment names ONE benign cause while the catch swallows every cause, so a real outage is answered exactly like the benign case — and the value it answers with is the one the caller acts on.

1. packages/metadata-protocol/src/seed-loader.tsresolveSoleOrganizationId() (#12852)

Consequence: invisibility. undefined is the verdict the method's own JSDoc calls "genuinely ambiguous", so load() stamps no organization_id and every BUSINESS seed row of the run lands org-less — invisible afterwards under strict org-scoping. SeedLoadResult carries an errors field this path never touches, so the operator sees a clean, successful seed.

This is not a design question: the sibling probe across the engine boundary, ObjectQL.probeInstallOrganizations, had the same shape and was repaired by PR #9817. This site was missed by that pass. The repair is that repair, copied:

}catch(error){if(!isMissingTableError(error))throwerror;}

The JSDoc's "or when sys_organization is absent" stays true — an unprovisioned table can hold no organization — while every other cause propagates with its envelope intact.

2. packages/objectql/src/lifecycle/lifecycle-service.tsloadGovernance() (#12853)

Consequence: deletion. snapshot.tenantOverrides is the ADR-0057 section 3.2 per-tenant retention/expiry window set. An empty map is indistinguishable from "this deployment has tuned no tenant", so reap() and archiveObject() fall every tenant back to the global window — wrong in both directions, and the expensive direction is a tenant configured to retain LONGER having its rows expired early. Nothing reported it: GovernanceSnapshot has no field saying the tenant pass did not complete, and the catch logged nothing.

Same narrowing. Operational posture changes, deliberately: a transient sys_organization outage now costs a sweep instead of silently completing on the wrong window. For a deletion action, "do not act on incomplete evidence" is the correct failure direction — a log cannot bring back a reaped row, and the rows a deferred sweep leaves are still there for the next one.

One implementation decision that is NOT in the dispatch fence — please rule on it

The fenced line is implemented verbatim. What the fence did not settle is where the throw lands, and the answer was not free:

So the throw is contained at the call site inside sweep(): the sweep aborts before a single policy is applied, and the failure is reported through report.errors (one entry per declared object — that field means "a lifecycle policy did not get applied", and here none of them did) plus a warn. Triage's posture is delivered exactly — governance loading fails, the sweep aborts, nothing is deleted on unverified evidence — and no declared invariant is broken.

I did not switch to the forbidden "log an error and continue" option: nothing is reaped on the aborted path. I also do not think the posture should be fail-open. If you would rather the rejection escape sweep() and change the contract at all three declaration sites, that is a one-line revert of the containment and I will make it.

Tests

New pins on each seam separately, each paired with positive controls on the same harness so a fixture that had stopped exercising the seam could not pass vacuously:

  • packages/metadata-protocol/src/seed-loader-sole-organization-read-failure.test.ts (new, 9 tests): one-org / no-org / several-org controls, a dropped connection and a permission refusal both surfacing with the injected error object identity intact and no rows written, both benign phrasings (SQLite no such table, Postgres 42P01) still seeding org-less, the Postgres missing-COLUMN superstring staying loud, and a non-effect (a pinned config.organizationId never consults the probe).
  • packages/objectql/src/lifecycle/lifecycle-service.test.ts (7 added, 109 total in file): two controls, the benign unprovisioned case still reaping on the global window, and — the pin this card exists for — a non-benign failure where no candidate row is even read and no delete is issued, with the tenant that would have been found configured to retain three times longer than the global window. Plus the contract pin: the abort is reported, sweep() still resolves, and the next sweep runs.

Two independent ablations

Each prediction was committed empty before the mutation ran; each mutation was proven on disk with anchored greps in both directions plus a git hash-object change; each restore was proven by git hash-object equal to the HEAD blob and an empty git diff HEAD; both ran under trap restore EXIT INT TERM with absolute paths. Both files are loaded from source through their own relative specifiers, so no rebuild sits between the mutation and the run.

AblationMutatedPredictedObserved
Aonly the #12852 narrowing3 of 9 red in the seed-loader file; 109 of 109 green in the lifecycle fileexactly that
Bonly the #12853 narrowing4 of 109 red in the lifecycle file, all in the new describe; 9 of 9 green in the seed-loader fileexactly that

Each ablation reds only its own pins.

Verification

Union re-run on the final commit 9b8095943, working tree clean:

  • pnpm --filter @objectstack/metadata-protocol exec vitest run — 1993 passed, 10 skipped (145 files)
  • pnpm --filter @objectstack/objectql exec vitest run — 4260 passed (246 files)
  • pnpm --filter @objectstack/objectql typecheck — clean. Stated honestly: --listFiles shows lifecycle-service.ts in the program and lifecycle-service.test.ts NOT in it, so that reading says nothing about the new test code. metadata-protocol declares no typecheck script at all; both are answered by the ledger below.
  • pnpm lint (eslint . --no-inline-config, whole repo) — clean, 91s
  • pnpm check:type-check-debt on a fully built workspace closure — "31 ledger entries re-measured, 1570 raw tsc errors total, none above its recorded number. surplus: none"
  • Gate families derived from the actual diff by node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, all green: changeset-gate-self-tests, cross-package-test-inputs, durability-log-level, objectql-double-limit, objectui-changeset, page-declaration-shape, published-files, slot-lookup, test-source-alias, type-source-resolution, nul-bytes, query-options-erasure, type-check-coverage, engine-double-contract, where-matcher, plus check-adr-0087-registration, check-changeset-no-major, check-ci-filter-parity, check-comment-mask-adoption, check-cross-package-test-inputs, check-empty-changeset, check-engine-split-ratio, check-plugin-teardown-shape, docs-audit/check-affected-docs, docs-audit/check-drift-comment, pm/release-rehearsal-clone --self-test.
  • NOT MEASURED, not green and not red: node scripts/pm/check-half-states.mjs refuses with "PREREQUISITE NOT MET — the token in the environment is not a valid GitHub credential" (exit 3). Its CI spelling pnpm check:pm-half-states is the self-test and passes (1515 cases).

Two gates were red on the first run and are fixed in the third commit, both caused by the new test file rather than by either seam: the fixture's WHERE matcher could not be lifted by check:where-matcher (an UNJUDGED verdict, which that gate treats as a failure, not a pass) and is now a module-level helper with no free bindings; and the file's engine double was pinned to the producer's dispatch predicates but absent from the shrink-only PINNED ledger, recorded with --write (3 rows added, 0 lost, no DEBT-baseline row touched).

Scope, stated so it can be checked

git diff --stat against the merge base is seven files: the two source files, two test files, two changesets, and the engine-double pinned ledger.

Changesets

Both patch, argued rather than defaulted. No exported signature, type, option or report field moves in either package, and the declared answer for every case the JSDoc describes is unchanged. The honest tension is that what a deployment observes on a failure path does change — a seed run that used to complete while writing invisible rows now fails loudly, and a sweep that used to complete silently now aborts and says so. That is the correction of a defect rather than a new capability, and the three landed repairs in this family (#8896, #8906, #9817) all shipped as patch.


Generated by Claude Code

…le organization'
`SeedLoaderService.resolveSoleOrganizationId()` sat behind a bare `catch {}`
whose comment named ONE benign cause while the catch swallowed every cause. A
dropped connection, a timeout or a permission refusal all arrived at the caller
as `undefined` — the 'genuinely ambiguous' verdict — so `load()` stamped no
`organization_id` and every business seed row of that run landed org-less,
invisible afterwards under strict org-scoping, with the seed report's `errors`
field never touched.
Discriminate by error TYPE through the shared `isMissingTableError` predicate,
which is the repair PR #9817 already landed on the sibling probe
(`ObjectQL.probeInstallOrganizations`) with this exact shape. Only an
unprovisioned table is truthful emptiness; everything else propagates.
Part of #12852
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…a retention window
`LifecycleService.loadGovernance()` filled `snapshot.tenantOverrides` — the
ADR-0057 3.2 per-tenant retention/expiry window set — behind a bare `catch {}`
whose comment named one benign cause while the catch swallowed every cause. On
any read failure the map came back EMPTY, which `reap()` and `archiveObject()`
read as 'this deployment has tuned no tenant': every tenant fell back to the
global window, so a tenant configured to retain LONGER had its rows expired
early. Nothing reported it — the snapshot has no field for an incomplete tenant
pass and the catch logged nothing.
Discriminate by error TYPE through the shared `isMissingTableError` predicate.
An unprovisioned `sys_organization` really is 'no tenant overrides', so a
single-tenant kernel is unchanged; every other cause aborts the sweep before any
policy is applied. For a deletion action, not acting on incomplete evidence is
the correct failure direction.
The abort is reported, not thrown: one `report.errors` entry per declared object
plus a warn. `sweep()`'s declared contract is that it never throws, and the
scheduler enters it as `void this.sweep()` where a rejection would be unhandled
— the objection #8906 recorded when it declined to rethrow from
`checkGovernance` one method below.
Part of #12853
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
Ablation A reverts the seed-loader narrowing (packages/metadata-protocol/src/
seed-loader.ts, resolveSoleOrganizationId) back to a bare swallow, leaving the
#12853 seam untouched.
PREDICTION, committed before the mutation runs:
- Direction: RED. Not 'fewer diagnostics' and not a reversal — the narrowing is
the only thing that turns a non-benign read failure into a propagated error,
and three pins assert exactly that.
- seed-loader-sole-organization-read-failure.test.ts: 3 of 9 FAIL —
'a dropped connection surfaces that error and writes NO org-less rows',
'a permission refusal surfaces the same way',
'a missing COLUMN on an existing sys_organization stays loud (the superstring
case)'. Each fails inside the rejection() helper: the load RESOLVES instead of
rejecting, and the store carries an org-less widget row.
- The other 6 in that file stay GREEN (two probe controls, the several-orgs
control, both benign phrasings, and the pinned-organizationId non-effect):
none of them reaches a non-benign throw.
- objectql lifecycle-service.test.ts: 109 of 109 stay GREEN. Ablation A must red
ONLY its own pins; a run that reds #12853's pins too proves neither card.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
Ablation B reverts the lifecycle narrowing (packages/objectql/src/lifecycle/
lifecycle-service.ts, loadGovernance's tenant scan) back to a bare swallow,
leaving the #12852 seam untouched. The sweep()-side containment stays in place
deliberately: without the throw it is unreachable, which is exactly what the
pins must detect.
PREDICTION, committed before the mutation runs:
- Direction: RED. The narrowing is the only thing that stops a non-benign read
failure being answered as an empty tenant-override map.
- lifecycle-service.test.ts: 4 of 109 FAIL, all in the #12853 describe —
'a dropped connection aborts the sweep: nothing is read as a candidate and
nothing is deleted' (the reaper reads a candidate page and deletes on the
GLOBAL 30d window instead of the tenant's 90d, and report.errors is empty),
'a permission refusal aborts it the same way',
'a missing COLUMN on an existing sys_organization stays loud (the superstring
case)', and
'the abort is REPORTED, never thrown — sweep() still resolves, and the next
sweep runs' (first.errors is empty and box.deletes is not).
- The other 105 stay GREEN, including this describe's two controls and the
benign unprovisioned-table case: none of them reaches a non-benign throw.
- metadata-protocol seed-loader-sole-organization-read-failure.test.ts: 9 of 9
stay GREEN. Ablation B must red ONLY its own pins.
Ablation A already ran and matched its own prediction: 3 failed / 6 passed in
the #12852 file, 109/109 green in this one.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
…e, and pin its engine double
Two gate families move on a NEW test file, and both were red before this:
- `pnpm check:where-matcher`: the fixture's matcher was an inline closure the
gate could not lift out of the file ('could not lift: ReferenceError: Cannot
access store before initialization'), so it landed as 1 UNJUDGED — and
'could not run' is a failure, not a pass. Lifted to a module-level
`matchesWhere(row, where)` with no free bindings; behaviour is identical
(flat equality, and a `$`-prefixed combinator is REFUSED rather than read as
a column name). Gate now: 310 discovered, 310 conforming, 194 by refusing,
0 unjudged.
- `pnpm check:engine-double-contract`: the file's engine double is pinned to
the producer's own dispatch predicates, but the shrink-only PINNED ledger did
not know the file, so the pins protected nothing. Recorded via
`node scripts/check-engine-double-contract.mjs --write` — 3 rows added
(delete/findOne/update), 0 lost, and no DEBT-baseline row was touched.
Part of #12852
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZbWd2jNV1FErXTPSS4Dry
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-protocol, @objectstack/objectql, touching 4 documentable anchor(s).

7 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/data-modeling/drivers.mdx(via LifecycleService (symbol))
  • content/docs/data-modeling/objects.mdx(via LifecycleService (symbol))
  • content/docs/data-modeling/validation.mdx(via SeedLoaderService (symbol))
  • content/docs/kernel/services.mdx(via LifecycleService (symbol))
  • content/docs/permissions/attachments-access.mdx(via LifecycleService (symbol))
  • content/docs/protocol/knowledge.mdx(via LifecycleService (symbol))
  • content/docs/protocol/objectql/state-machine.mdx(via SeedLoaderService (symbol))

3 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v14.mdx(via LifecycleService (symbol))
  • content/docs/releases/v15.mdx(via LifecycleService (symbol))
  • content/docs/releases/v17.mdx(via SeedLoaderService (symbol))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 20 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 277948f7aea27a6f246bc73d95d8359086cca250packageMentionDocs.

Which tree this was computed on

This run read content/docs from 9409b7f1dfc9c98fc07ce8c33d14c326c2af112e — the merge of head 9b809594315c921e01e8655eef54a90ba9387d59 into base 277948f7aea27a6f246bc73d95d8359086cca250, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 9409b7f1dfc9c98fc07ce8c33d14c326c2af112e && git checkout 9409b7f1dfc9c98fc07ce8c33d14c326c2af112e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 277948f7aea27a6f246bc73d95d8359086cca250 9b809594315c921e01e8655eef54a90ba9387d59 && git checkout -B drift-repro 277948f7aea27a6f246bc73d95d8359086cca250 && git merge --no-ff 9b809594315c921e01e8655eef54a90ba9387d59
node scripts/docs-audit/affected-docs.mjs --json 277948f7aea27a6f246bc73d95d8359086cca250

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 277948f7aea27a6f246bc73d95d8359086cca250 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Reviewer-of-record notes on the drift rows above. The release-owned audit is the interesting part.

1. Release-owned pages — audited, none falsified, and v17 is better than neutral

⛔ Read-only, but the check says they are still audited, so here it is.

content/docs/releases/v17.mdx:3695 opens the section that these two cards belong to, in almost exactly their words:

A recurring class this release closes in several packages at once: a read that FAILED was indistinguishable from a read that legitimately found nothing.

and states the method:

None of the four catches is removed — each is discriminated by error type through the shared isMissingTableError predicate, so an unprovisioned table stays benign and everything else surfaces.

⇒ These two seams are the same declared class, fixed by the same published method. The page is not falsified, and it does not claim exhaustiveness — it enumerates four metadata-protocol seams under #8896 and then explicitly continues "Siblings across the tree: …". #12852 is one more sibling; #12853 is the same shape one package over. So this PR is consistent with shipped guidance rather than introducing a posture.

v14:147 / v14:214 / v15:367 name LifecycleService only as the platform service and its default-on switch — nothing about read-failure behaviour. Untouched by this diff. Nothing to file on any of the three.

2. The seven hand-written rows are coarse-anchor noise

Every row matched the bare class name LifecycleService or SeedLoaderService, not anything this diff changed. I hand-read for the claim that could have been falsified — whether any page documents sweep or seed behaviour on a failed or missing read — with a positive control first (retention/governance prose reaches 8+ pages, so the channel is live).

Found nothing falsified. The nearest page, data-modeling/objects.mdx:208, describes retention_overrides as an ops knob and states no failure behaviour at all.

3. ⚠️ One genuine question for the report — raised, not asserted

content/docs/references/api/protocol.mdx:2165 documents the seed report's errors field as:

Per-record failures reported by the seed loader, plus any seed-body read failures. May be present and empty on a clean load.

A sys_organization probe failure is neither of those, which is consistent with #12852's premise that this path never touches errors and the operator sees a clean seed. After the fix the probe throws rather than answering "empty" — so where does that failure now surface? If it propagates out of the apply it plausibly becomes the singular error at :2164 ("present when the apply failed before the loader ran"), whose wording may or may not fit a probe that runs at a different point.

⇒ Please answer this in the report with a measurement rather than a reading: what an operator actually sees after the fix when that probe fails. If the answer makes either :2164 or :2165 inaccurate, that is a docs finding to file — ⛔ not a reason to widen this PR.


Generated by Claude Code

@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Reviewer-of-record: option A stands. This is mechanism inside a ruled posture, not a re-opening of it — so it does not go to the maintainer.

The seat raised where loadGovernance's new throw should land, having implemented containment at the call site inside sweep(). Raising it was right; the fence covered the throw, not its landing. Here is the call, with the reasoning, so nobody re-litigates it later.

Why this is not the escalation triage armed

Triage's stop-and-report trigger was explicit: if the implementer concludes the ops posture must be fail-open, do not switch — escalate. The seat concluded the opposite and implemented fail-closed. The trigger was correctly not pulled.

Triage's ruled outcome was: governance loading fails ⇒ the sweep aborts ⇒ nothing is deleted on unverified evidence. Option A delivers that exactly — on the aborted path nothing is reaped at all, and the failure is reported through LifecycleSweepReport.errors, a field that already exists for this. Under A and B the behaviour triage specified is identical. What differs is only whether the host process also dies.

Why B is wrong, verified rather than argued

I checked the three facts the choice turns on, on origin/main:

⇒ B does not mean "the sweep aborts." It means a transient database blip kills the process. That is a far heavier operational change than triage chose, it contradicts a declared invariant in at least two places, and it reverses a decision the file already recorded. ⛔ Not taken.

On the fence that was not drawn

The dispatch said "narrow the catch so the sweep aborts" and stopped there. It should have said where the throw lands, because in a service whose scheduler calls it as void this.sweep() those are different changes. That gap is mine, not the seat's — and the seat handling it by implementing the conservative option and putting the choice in front of me is the right behaviour, not a deviation.

Two other things worth recording

The marker-discrimination trap was caught independently. The seat reports the narrowing as 5 → 4 counting the code form only, and states why the naive token count reads 6 → 6: the new comments mention the token they replace, so that marker's count does not move and cannot discriminate. That is the same hazard this lane hit earlier today from the other direction, found here without prompting.

Both cards carry a small factual imprecision, reported not absorbed: they place isMissingTableError in packages/metadata/src/errors.ts; it actually lives in packages/metadata/src/utils/schema-sync-errors.ts and is re-exported through the @objectstack/metadata/errors subpath. The prescribed import spelling is correct, so nothing follows — but stronger than my own pre-dispatch answer: both target files already imported the predicate on origin/main (lifecycle-service.ts:5, seed-loader.ts:28). No import line was added, and no local message-match predicate was written.


Generated by Claude Code

@os-zhuang
os-zhuang marked this pull request as ready for review August 28, 2026 07:19
@os-zhuang
os-zhuang enabled auto-merge August 28, 2026 07:19
@os-zhuang
os-zhuang added this pull request to the merge queueAug 28, 2026
@github-merge-queue
github-merge-queueBot removed this pull request from the merge queue due to failed status checks Aug 28, 2026
@os-zhuangClaude

Copy link
Copy Markdown
ContributorAuthor

Dequeued for CI_FAILURE — established as not this PR's, and standing down rather than re-queuing

Per the drive-to-green rules, standing down on a failure that is not this PR's is never silent. Here is the reading.

The failing check:Lint & Repo Gatespnpm check:adr-anchors on the merge-group build (run 33154419315), not on this PR's own head — which was and remains green (32/32 reconciled after the suite-completed event).

✗ check-adr-anchors --self-test — 1 failure(s) of 106 assertion(s)
• live-decision-letters-are-green-today
ADR-0006 D4 is cited by 1 file(s), but ADR-0006 declares no D4 —
it decides: D1, D1.1, D1.2, D1.3, D2, D3
packages/spec/src/data/object.zod.ts

Why it is not this PR's, three independent ways:

  1. The named file is packages/spec/src/data/object.zod.ts. This PR touches no file under packages/spec — its diff is two source files, two test files, two changesets and one engine-double pin row, all under metadata-protocol / objectql / .changeset / scripts.
  2. The same check fails identically on four other PRs' queue builds in the same window: pr-12878 (33154421802), pr-12895 (33154420991), pr-12897 (33154423240), pr-12901 (33154423941).
  3. origin/main itself carries the offending citation at object.zod.ts:1467. The defect is in the base, not in the merge.

Root cause is a latent defect exposed by a correct, newly-landed gate. Measured with git log -S: the citation dates to 7bba90b06 (#4001/#4522); the assertion that catches it landed today in 8c0b5cf06 (#12785). Nobody broke main — a gate went live that is right, over a violation that was already there.

No fix exists to port, and I cannot write one: the correction belongs in packages/spec, which is the spec seat's surface, and the alternative would be docs/adr/**, which is governed. ⛔ Widening this PR to reach either would be the wrong move.

⇒ Filed as #12913 with the full diagnosis and a proposed patch — including the likely specific mistake (D4 appears to mean the file version v4; ADR-0006 exists as {,.v2,.v4}.md, the gate correctly treats them as one revised decision, and no ADR-0006 file declares a D4).

Not re-queuing, and not spending a re-run. This is a deterministic content defect on main, not a flake — five PRs reproduce it identically and I read the citation on main directly. A re-run would re-fail and re-dequeue; re-queuing before the base is fixed is churn. This PR goes back in the queue once #12913 is resolved, and it needs no change of its own to do so.

⛔ Note for anyone tempted: the gate's own message answers this — "Not a fix: loosening this check. A letter that resolves to nothing is the defect it exists to name."


Generated by Claude Code

@os-zhuang
os-zhuang added this pull request to the merge queueAug 28, 2026
Merged via the queue into main with commit bd0c5ccAug 28, 2026
34 checks passed
@os-zhuang
os-zhuang deleted the claude/issue-12853-narrow-benign-read-catches branch August 28, 2026 09:56
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

2 participants

@os-zhuang@claude