Skip to content

fix(spec): close the shared rate-limit budget — one declaration was answering two doors, and one of them dropped the key in silence - #18861

Merged
os-bill merged 4 commits into
mainfrom
claude/issue-18578-ratelimit-open-twin-guidance
Sep 18, 2026
Merged

os-bill merged 4 commits into
mainfrom
claude/issue-18578-ratelimit-open-twin-guidance

Conversation

@os-bill

@os-bill os-bill commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator

Fixes #18578

Clause-②: yes (widening)

ServerRateLimitConfigSchema was declared strictObject({ … guidance: { keyBy, store } }, RateLimitConfigSchema.shape) — built from the OPEN schema's own shape object. One declaration therefore answered for two emitted defs with opposite doors:

def door before an authored keyBy
system/ServerRateLimitConfig closed refused, loudly, with the prescription
shared/RateLimitConfig plain open z.object accepted, then dropped in silence

The two guidance entries prescribed to nobody on the open twin. And a misspelled budget was the same story one key over: on the bare mount windowSeconds: 60 parsed green and metered the 60000 ms default — a thousandfold miss on the one key whose job is to bound spend, reported as success.

The census — the method, re-run on current main

⛔ Not re-derived by grep: the card records why a CONTRACT_REVIEW_TIER sweep with live controls found nothing here (it hunted an OPEN clone built from a STRICT schema's shape; this is the STRICT one built from the OPEN one's shape, which every grep shape in that sweep is blind to by construction). The instrument is the gate's own — computeGuidanceRoutes() in packages/spec/scripts/build-schemas.ts: match every emitted def to its declaration by sorted key set plus per-entry instance identity, then write the key at the def and read what comes back.

Driven over every emitted def at 42f8df1723 (current main when this branch was cut), and again on this branch:

reading before after
emitted defs 1527 1527
strictObject declarations registered 551 551
defs resolving to exactly one declaration 258 258
… of those, naming at least one undeclared key 147 147
keys promised 779 779
keys delivered 770 772
keys not delivered 9 7

The filer's figures reproduce exactly (258 / 779 / 770 / 9); the def total moved 1525 → 1527 with the tree. One refinement worth recording: 258 is the count of defs resolving to exactly one declaration, and the subset whose declaration also names an undeclared key is 147 — the card's sentence folds the two.

The 9 not-delivered, itemised:

  • 2 — shared/RateLimitConfig:keyBy and :store, the live members this card is about. Probe verdict accepted-and-stripped.
  • 7 — ui/ChartGroupBy:function, ui/ChartGroupBy:groupBy, ui/RecordHighlightsField:icon, ui/ViewItem:confg, ui/ViewItem:isPinned, ui/ViewItem:sortOrder, ui/ViewItem:columnState. An acknowledged probe boundary, ⛔ not a clean zero and ⛔ not a finding — and the mechanism is now measured rather than assumed. Each is a discriminated union; the probe writes { [key]: null } and nothing else, so the DISCRIMINATOR is missing and the union answers invalid_union on viewKind before any arm's door is reached. Written whole, the same document DOES raise the prescription. So they are refused loudly today, just not through a door this instrument can watch.

The shape chosen, and why

The strictness and both tables move to the shared schema; ServerRateLimitConfigSchema keeps only what is genuinely server-only — its two bounds checks — and is now RateLimitConfigSchema.superRefine(…) rather than a second strictObject over the same shape object.

That leaves one declaration and one door for both defs, which is load-bearing in three ways:

  1. the declaration match still resolves to exactly one declaration, so matched.length !== 1 never fires and proof 4 keeps working for both defs. Declaring a second strictObject over the same shape object would have restored the ambiguity in the other direction, where the match resolves to neither and both defs silently read "no evidence";
  2. the closed twin's verdict is untouched — same acceptances, same refusals, same prescription bullet, same bounds;
  3. the shared/ ledger row's own rationale — strictness decided at the consuming schema — is false for this shape, exactly as it was false for shared/protection.zod.ts. Of the two mounts only one re-postured; api/endpoint.zod.ts mounts it bare on apis[].rateLimit, a registered metadata type authored through defineStack({ apis }), the Studio form and PUT /meta/api/:name, where nothing re-postures it. The row is annotated for what is now the fourth instance of a shape that ledger has recorded three times.

Controls

LIT — behaviour flips. Same probe, run against this branch and against the two source files restored to the base commit (ablation with an EXIT/INT/TERM restore trap; both ablated blobs verified by git hash-object against the base blob hashes, and the restore verified by an empty git diff HEAD):

written on shared/RateLimitConfig before after
{ …valid, keyBy: 'ip' } PARSED OK → {enabled, windowMs, maxRequests} — key gone REFUSED, Unrecognized key(s) on this rate-limit budget …: keyBy. + the keyBy prescription
{ …valid, store: 'redis' } PARSED OK, key gone REFUSED + the store prescription
{ enabled: true, windowSeconds: 60, maxRequests: 100 } PARSED OK → windowMs: 60000 REFUSED, Did you mean windowSeconds → windowMs?
an api endpoint whose rateLimit carries keyBy PARSED OK, key gone REFUSED with the same prescription, at the author's own path

Census leg of the same flip: 770 → 772 delivered, 9 → 7 not delivered.

DARK — reads what it must. system/ServerRateLimitConfig, before and after: a legitimate budget parses to the same document; { enabled: true } still materialises the same defaults; max: 5 is still renamed to maxRequests; maxRequests: 0 and windowMs: 0 are still refused on path: ['maxRequests'] / ['windowMs'] with their own messages; { …valid, keyBy: 'ip' } is still refused carrying the prescription bullet. A legitimate endpoint document with a legitimate rateLimit still parses, before and after. The census's 258 / 779 are unchanged, so the closed twin's declaration resolution did not move either.

⚠️ The two defs are told apart, as the card requires. The probe reads them by def key through separate schema instances, and the discriminating reading is the PARSE ANSWER, not the emitted artefact: both still emit additionalProperties: false, and both still match one declaration by per-entry instance identity — neither of the two cheap instruments moved, and neither was used.

⚠️ One disclosure inside DARK. The server key's refusal MESSAGE changes, because the two surfaces now share one declaration: the surface prose reads this rate-limit budget (server.security.rateLimit, or an endpoint's rateLimit) rather than server.security.rateLimit alone, and the history sentence is the shared one. The verdict, the issue codes, the accept set and the prescription text are unchanged, and no test pinned the old prose. It is named here rather than left for a reviewer to find.

The pin this rots, and where it went

scripts/build-schemas-check-mode.test.ts's #18301 DOOR pin used this defect as a LIVE fixture, and its guard fired with its own prescription — "its door closed, so this fixture no longer models an open def sharing a closed declaration's shape; re-pick the pair". Re-picked:

  • DARK leg — the rate-limit twins, now one declaration, two defs and one door. Both rows must be ADMITTED by proof 4, so re-opening the shared shape turns this test red. That is this card's regression guard.
  • LIT leg — ui/ViewItem:confg, a def whose declaration NAMES the key and whose delivery the probe cannot watch. Proof 4 must refuse the deletion, in the words that say a declaration exists, and must not be waived by proof 2 either. Both halves of the fixture's own validity are guarded loudly: the bare document must carry no unrecognized_keys issue at all, and the whole document must raise the prescription.

The census says there is no remaining def that ACCEPTS a promised key and drops it, so no live pair could model the original shape — which is the point of the card.

Verification

  • pnpm --filter @objectstack/spec build — green
  • pnpm --filter @objectstack/spec check:generated — 15 artefacts; only strictness-ledger.counts.md was stale (regenerated with gen:strictness-ledger, system/ 351 → 350). ⭐ check:authorable-surface, check:api-surface, check:docs, check:declaration-map and check:export-origins all pass with no regeneration: the published JSON Schema and the API surface are byte-unchanged, because in io: 'output' zod already emitted additionalProperties: false for the stripping shape.
  • pnpm --filter @objectstack/spec typecheck — green
  • pnpm --filter @objectstack/spec test — 486 files / 14053 tests pass, 1 skipped
  • pnpm --filter @objectstack/spec test:repo — 31 files / 536 tests pass (the re-picked #18301 pin is in here)
  • check:strictness-ledger · check:yaml-examples · check:cross-package-test-inputs · check:test-source-alias · check:spec-parsed-alias · check:doc-authoring · check:spec-docblock-symbol-anchors · check:pm-widening-tells · check:nul-bytes — green
  • check-adr-0087-registration --base origin/main — green; 1 declared-breaking changeset carrying not-required (no-migration-prescription)
  • check-changeset-no-major --base origin/main · check-empty-changeset --base origin/main — green
  • check:skill-examples — NOT MEASURED: it refuses before judging any surface because packages/client-react/dist holds no declarations in this worktree. A prerequisite, not a verdict.

Blast radius, measured: every shipped rateLimit block writes only declared keys — three in content/docs/, one in skills/objectstack-api, none at all in examples/, the os init templates or the create-objectstack blank template.

Acceptance notes

  • The ADR-0087 boundary was measured, and it does not fire. The dispatch fenced packages/spec/src/migrations/registry.ts (held by another seat). ⛔ That file is untouched, and the technical choice was not bent to avoid it: the disposition is not-required (no-migration-prescription) on its own merits, and it is the same disposition, on the same stored metadata type, that the close of ApiEndpointSchema itself took one level up — an undeclared key was never honoured, so nothing exists for objectstack migrate meta to rewrite, and the refused set is an open set of author typos rather than a renamed key.
  • The declaration line above is copied verbatim from the dispatch, and the measurement disagrees with it. No key is added to a published payload here; what moves is the accept set, and it moves DOWN. On the arms this repo uses, the measured reading is no (narrowing). The line is left exactly as dispatched, per the charter that the declaration is the seat's to align; the correction is named in the report rather than made here.
  • Noted, not filed: the retired outbound connector vocabulary (strategy, burstCapacity, respectUpstreamLimits, rateLimitHeaders) is refused by the closed budget with no wrong-layer pointer — the rejection is correct and loud, and a guidance entry naming where outbound throttling belongs would be an improvement rather than a defect repair. Carrier: whoever next touches shared/http.zod.ts.
  • Noted, not filed: the keyBy prescription points at server.trustProxy for how the caller IP is read, which is accurate on both mounts but is written in server language; it now reaches endpoint authors too. Carrier: none — no PR or person is queued on this file.
  • Noted, not filed: the dispatch's file surface named packages/spec/src/system/http-server.zod.ts, and ServerRateLimitConfigSchema actually lives in packages/spec/src/system/stack-server.zod.ts (http-server.zod.ts's shape was retired). The premise otherwise verified exactly.

Generated by Claude Code

…ers one door

`ServerRateLimitConfigSchema` was `strictObject({ … guidance: { keyBy, store } },
RateLimitConfigSchema.shape)` — built from the OPEN schema's own shape object, so
one declaration answered for two emitted defs with opposite doors.
`system/ServerRateLimitConfig` refused an undeclared `keyBy` with its
prescription; `shared/RateLimitConfig`, mounted bare on `apis[].rateLimit`,
accepted the same key and dropped it in silence, and both `guidance` entries
prescribed to nobody there. A misspelled budget was the same story one key over:
`windowSeconds: 60` parsed green and metered the 60000 ms default.

The strictness and the tables move to the shared schema, where both defs inherit
them; the server schema keeps only what is genuinely server-only, its two bounds
checks. That leaves ONE declaration, so the gate's declaration match still
resolves to exactly one and the closed twin's verdict is untouched.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
…the strictness ledger

The `#18301` DOOR pin used `shared/RateLimitConfig` as a LIVE open def sharing a
closed declaration's shape. Closing that def is what this branch does, so the
fixture's own guard fired with its own prescription — "re-pick the pair". It is
re-picked: the rate-limit twins become the DARK leg (one declaration, two defs,
and now one door — so re-opening the shared shape turns this test red), and the
LIT leg moves to `ui/ViewItem:confg`, a def whose declaration names the key and
whose delivery the probe's one-key document cannot reach past the discriminator.
Measured, not assumed: the same document written whole DOES raise the
prescription, and both halves are guarded loudly.

The `shared/` ledger row is annotated for the fourth instance of a shape it has
now recorded three times — a directory verdict that was right for the directory
and wrong for one file in it.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation protocol:system tests tooling labels Sep 18, 2026
@os-bill os-bill added domain:spec priority:p2 Medium: important, M3 labels Sep 18, 2026 — with Claude
@github-actions

github-actions Bot commented Sep 18, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/spec, touching 5 documentable anchor(s).

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

  • content/docs/api/declarative-endpoints.mdx (via maxRequests (literal, a string literal in RateLimitConfigSchema; a string literal in ServerRateLimitConfigSchema), windowMs (literal, a string literal in RateLimitConfigSchema; a string literal in ServerRateLimitConfigSchema))
  • content/docs/getting-started/quick-reference.mdx (via maxRequests (literal, a string literal in RateLimitConfigSchema; a string literal in ServerRateLimitConfigSchema), windowMs (literal, a string literal in RateLimitConfigSchema; a string literal in ServerRateLimitConfigSchema))
  • content/docs/protocol/kernel/http-protocol.mdx (via maxRequests (literal, a string literal in RateLimitConfigSchema; a string literal in ServerRateLimitConfigSchema), server.security.rateLimit (literal, a string literal in ServerRateLimitConfigSchema), windowMs (literal, a string literal in RateLimitConfigSchema; a string literal in ServerRateLimitConfigSchema))
What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 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 — 136 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 be7aeb82756d054af451c7b248686d878b311210 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from a0bafb0382fadbdbffb2e35e0fbc028b6c6dc83f — the merge of head 35b84505d2ac16ee31a2b841119088d186b0b959 into base be7aeb82756d054af451c7b248686d878b311210, 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 a0bafb0382fadbdbffb2e35e0fbc028b6c6dc83f && git checkout a0bafb0382fadbdbffb2e35e0fbc028b6c6dc83f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin be7aeb82756d054af451c7b248686d878b311210 35b84505d2ac16ee31a2b841119088d186b0b959 && git checkout -B drift-repro be7aeb82756d054af451c7b248686d878b311210 && git merge --no-ff 35b84505d2ac16ee31a2b841119088d186b0b959

node scripts/docs-audit/affected-docs.mjs --json be7aeb82756d054af451c7b248686d878b311210

⚠️ 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 be7aeb82756d054af451c7b248686d878b311210 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

… the direction it moves

The declaration was filed `yes (widening)` under "when unsure, declare yes",
before the shape was chosen. The measurement went the other way: no key is added
to a published payload here, and the accept set shrinks — a refusal replaces a
silent accept. The arm is the one line a consumer reads for direction of change,
so it says so.

Nothing else in this changeset moves. The BREAKING banner and the ADR-0087
`not-required (no-migration-prescription)` disposition both stay: the
breaking-ness is carried by those, not by the arm, and the gate reads the same
verdict off either signal.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3

os-bill commented Sep 18, 2026

Copy link
Copy Markdown
Collaborator Author

条款② 收口:申报已翻成 no (narrowing),载体整对摘除

⏱️ 2026-09-18T01:34Z。head = 35b84505d2。

三处申报的现状,以及本席为什么只改了两处

载体 现值 谁读它
卡 #18578 的认领(5723662465) ✅ no (narrowing) 声明肢判自这里
changeset .changeset/rate-limit-budget-unknown-keys-refused.md:10 ✅ no (narrowing) check-changeset-no-major 读这一行
PR 正文 ⚠️ 仍写 yes (widening) ⛔ 没有门禁读它

⇒ node scripts/pm/check-clause2-carriers.mjs --pair 18861 现读 exit 0:「both carriers agree … no widening tell」。

⛔ 本席不改 PR 正文那一行,理由是章程自己的话:「Before rewriting a PR body over it, check whether any gate actually reads that line — the changeset gates read the line in the changeset, not in the PR body.」而该检查器也自标该行「stated as an INPUT only」。⚠️ 加上本班实测过的代价:裸 REST 改正文会把署名页脚换成不带 session id 的裸形(#18840 已中过一次)。⇒ 为一行无人读的字去换掉一条归属记录,是拿真损失换假整洁。 本条评论即那一行的更正载体。

载体是「从未适用」,⛔ 不是「已清」

needs:contract-review 原挂在卡 #18578 上(PR 上未挂)。申报落到 no 之后,该闸对本卡从一开始就不适用 ⇒ 本席整对摘除。

⚠️ ⛔ 不得把这次摘除读作「有过一次 PASS」 —— 本卡没有、也不需要契约复核记录。这与 #18851 那次形成对照:那张是 yes,走了隔离达档复核、拿到 PASS、留了同形记录(5723255433)、再剥双载体。两者的证据形状不同,⛔ 不可互相类比。

dev 的报告评论保持原样

5723605967 的 JSON 里仍写着派发时的 yes (widening)。⛔ 本席不改它 —— 那是一份当时为真的记录,改掉等于把历史修成没发生过。当前值以上表三行为准。

⭐ 一条收获,记给下一个人

dev 指出:门禁现在把 narrowing 这个臂本身也当作破坏性信号,而此前只有 BREAKING 横幅承担这件事。⇒ ADR-0087 的处置标记现在被两个独立信号各自要求,而它同时满足两个。这是加强,⛔ 不是判决改变 —— 本席复读了 changeset 的第 7 行(BREAKING 横幅)与第 12 行(not-required (no-migration-prescription)),两者在这次单行改动中逐字节未动,check-adr-0087-registration --base origin/main 仍 exit 0。


Generated by Claude Code

@os-bill
os-bill marked this pull request as ready for review September 18, 2026 01:55
@os-bill
os-bill added this pull request to the merge queue Sep 18, 2026
Merged via the queue into main with commit fb2bccf Sep 18, 2026
37 checks passed
@os-bill
os-bill deleted the claude/issue-18578-ratelimit-open-twin-guidance branch September 18, 2026 02:38
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 28, 2026
… beside its population (objectstack-ai#18863)

Fixes objectstack-ai#18579

Clause-②: no

Two census figures in the proof-4 prose of
`packages/spec/scripts/build-schemas.ts` label a population they do not
measure. Both are corrected, and both now carry their population — and
the tree they were read on — in the same sentence, which is what the
triage note on the card asked of whoever took it.

## The two figures

**(1) The docblock's not-delivered rationale.** It said *"on the shipped
graph 7 of the 8 defs in that state are unions"*. Measured: that state
holds **9 keys on 4 defs**, and **3 of those 4 defs** are unions,
carrying **7 of the 9 keys**. The 7 was the KEY count wearing the DEF
label.

**(2) The closing line of the same docblock**, which sent its reader to
PR objectstack-ai#18529's body *"for the census run"*. That body's census row labels
*"defs resolving to exactly one declaration that names an undeclared
key"* with **258** — but 258 is the count of defs resolving to exactly
one declaration **whether or not it names anything**. The labelled
population measures **147**. That row lives in a merged PR body and is
not editable from here, so the docblock now records the census in the
tree and tells its reader not to re-derive it from that body.

The argument both figures support is unchanged — unions do dominate the
not-delivered set (3 of 4 defs, 7 of 9 keys). Only the arithmetic moved.

## Re-measured on this head, not copied from the card

The card's numbers were taken on branch `9e0324f807` and
`packages/spec/src` has moved on `main` since, so nothing was copied.
The whole census was re-derived with **this head's own verbatim
`computeGuidanceRoutes`**: a byte-identical copy of
`scripts/build-schemas.ts` (prefix proven equal by `git hash-object`:
`0803422391…` on both) with a census block appended, run as the real
generator so `zodByDefKey`, `generatedSchemas` and the declaration
registry are the ones the gate itself sees. The copy was deleted before
the first commit; it is in no diff.

**Tree measured: `objectstack-ai/objectstack` at `88aa326deb`** — the
merge base of this branch with `origin/main`.

| population | count |
|---|---|
| emitted defs (`zodByDefKey`, = bundled `$defs`) | 1527 |
| of those, emitted artifact carries `additionalProperties: false` |
1117 |
| of those 1527, defs resolving to exactly one declaration (naming
anything or not) | 258 |
| of those 258, defs whose declaration NAMES a key the def does not
declare | **147** |
| keys those 147 defs are promised | 779 |
| of those 779, keys the def delivers (`prescribed`) | 770 |
| of those 779, keys not delivered (`declared-but-silent`) | **9** |
| of those 9 keys, keys on `shared/RateLimitConfig` | 2 |
| defs carrying a not-delivered key | **4** |
| of those 4 defs, unions | **3** (`ui/ChartGroupBy`, `ui/ViewItem`,
`ui/RecordHighlightsField`) |
| of the 9 not-delivered keys, keys on those 3 union defs | **7** |
| defs with an empty shape (excluded outright) | 6 |
| declarations carrying an empty shape (what each of those 6 would
answer to) | 9 |
| defs whose match is ambiguous | 0 |
| defs with no derivable shape | 304 |

**What validates the instrument is the rest matching, not the conclusion
agreeing.** Reproduced exactly against the card: 1117 · 258 · 147 · 779
· 770 · 9 · 2 · 6 · 9 · 0 ambiguous · 3-of-4 · 7-of-9. Drifted with
`src`, as the card predicted: 1525 → **1527** emitted defs, 303 →
**304** no-shape, 408 → **410** not-`additionalProperties`-false.
Independent corroboration of the 1527/1538 pair from the generator's own
summary line: `check:authorable-surface` prints `bundled schema:
objectstack.json (1527 definitions)` / `Successfully generated 1538
schemas`.

**Cross-check tying the partition to the gate's own function:** every
one of the 779 promised (def, key) pairs the census derived was fed back
through the head's verbatim `verdictFor` — **0** came back `none`. A
replication that invented a pair the real lookup does not hold would
show up there.

## Sequencing with PR objectstack-ai#18861

PR objectstack-ai#18861 (card objectstack-ai#18578) repairs the `shared/RateLimitConfig` open-twin
defect and therefore moves this exact partition (its author measures
delivered 770 → 772, not delivered 9 → 7). It is **absent from the tree
measured here**, proven twice: `git merge-base --is-ancestor 36adeca
HEAD` exits 1 on a non-shallow checkout with a lit control leg
(`88aa326deb` → exit 0), and the census itself still lists
`shared/RateLimitConfig:keyBy` and `:store` as not-delivered, which is
precisely the defect that PR removes.

So both replacement sentences are written as readings of a **named
commit** (`at 88aa326 that state held …`), not as claims about
whatever `main` holds today. They stay true when objectstack-ai#18861 lands; they
become stale, visibly, with the commit that says so right there. The
docblock also now tells its next reader that which defs sit in that
state is a fact about the graph rather than a property of the proof —
"closing an open door moves it" — and to re-measure rather than re-date.
Nothing here waits on or depends on that branch.

## Not taken: the `delivers()` fixture

The card's optional fourth item — a fixture for the `message`-includes
leg, the one refusing a strict clone built without the declaration's
error map — is **handed back, with a reason rather than a shrug**. Three
routes exist and each is blocked by a rule this repo states out loud:

1. **A synthetic def on the shipped graph.** `packages/spec/src` is
published (`files[]` carries `src/**/*.zod.ts`), so this ships a fake
schema to consumers.
2. **A synthetic def in the test sandbox.**
`build-schemas-check-mode.test.ts` builds each sandbox with
`fs.symlinkSync` for `src` — its own comment says *"`src/` is the
fixture's own, so the population a run observes is the repo's"*. A
fixture def would have to be written into the real `src`, i.e. route 1.
Copying `src` per sandbox instead is a structural change to a 4466-line
harness.
3. **Exporting the leg for a unit test.** `delivers()` is a closure over
module-level state, and that file's header rule is *no test-only seam*.

So it is not a fixture-sized job; it is a restructuring, and the card is
explicit that this PR should not become two things. Recommendation for
whoever files it: route 2, as its own card, because it also unlocks
fixtures for the other fail-closed legs. The census zero that defends
the leg today is re-stated above with its tree, so the next reader can
see what it rests on.

## Verification

- `pnpm --filter @objectstack/spec build` — green (runs `gen:schema`,
i.e. the edited generator, end to end)
- `pnpm --filter @objectstack/spec typecheck` — green (all three legs:
`tsc --noEmit`, `check:scripts-typecheck` which compiles the edited
file, `check:test-typecheck`)
- `pnpm --filter @objectstack/spec test` — 487 files / **14055 tests**
passed
- `pnpm --filter @objectstack/spec exec vitest run --project repo
scripts/build-schemas-check-mode.test.ts` — 1 file / **85 tests**
passed. This is the suite that spawns the edited generator, and it is
**not** in the package's `pnpm test`: that script runs `--project local`
only, and vitest's filter guard says so out loud when you aim the wrong
project at the file.
- `scripts/file-description.test.ts` +
`scripts/def-key-collisions.test.ts` (`--project repo`) — 128 passed;
these are the only other tests that read this script by name.
- **53 gate families** derived by `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` — 51 green. Two exit
**3**, which is that gate's own NOT-MEASURED code and neither a pass nor
a failure: `check:dual-build-cjs-loads` and `check:lean-entry-closure`
both need a whole-repo `pnpm build` that this worktree does not carry.
Neither can move on a comment in an unpublished script; CI builds fresh.
- `pnpm lint` equivalent run whole, not narrowed: `npx eslint .
--no-inline-config --format json` at `8f9a42d8a2` — **6846 files, 0
errors, 0 warnings**.
- Control-byte self-scan over the edited file (`grep -naP` over the C0
range plus DEL) — no match; `pnpm check:nul-bytes` green.

## No changeset (`skip-changeset`)

Nothing published moves. Measured rather than asserted:
`@objectstack/spec`'s `files[]` is `dist · json-schema · liveness ·
prompts · llms.txt · README.md · src/**/*.zod.ts · CHANGELOG.md ·
api-surface · spec-changes.json` — **no `scripts/` entry** — and
grepping every shipped path for `computeGuidanceRoutes` returns **0
hits**, against a lit positive control (`keySetMatches`, which is found
in `dist/shared/index.js` and four more). The diff is comment-only
inside that unpublished file, so the blast radius is the gate's next
reader, not an author and not a consumer.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation domain:spec priority:p2 Medium: important, M3 protocol:system size/m tests tooling

Projects

None yet

2 participants