fix(rest): consume the parsed api sub-config so RestApiConfigSchema owns its defaults - #15673
fix(rest): consume the parsed api sub-config so RestApiConfigSchema owns its defaults#15673os-litant wants to merge 17 commits into
api sub-config so RestApiConfigSchema owns its defaults#15673Conversation
`RestServer.normalizeConfig` ran `RestApiConfigSchema` over `config.api` and threw the parsed output away, rebuilding the block from a `??` chain over the raw cast. That chain restated the schema's eleven top-level `z.default(...)`s as eleven literals in `packages/rest`, with nothing pinning that the two stayed equal — a `packages/spec` default change would silently fail to propagate. #11637 made the parse validate-only for two measured reasons; both have since expired (#11983 gave `enableSearch` a declared seat, #12450 withdrew the `projectResolution` omit). Re-measured here: the 14 keys the method reads and the 14 the schema declares after `.omit({ requireAuth: true })` are the same 14 in both directions, so a consumed parse cannot strip anything the runtime honours. `requireAuth` stays omitted and stays warn-and-ignore in the plugin, which reads it off the RAW config. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Two halves, deliberately split by whether the schema is mocked. `rest-api-config-defaults-follow-spec.pin.test.ts` is the DISCRIMINATING pin: it moves five `z.default(...)`s to values that differ from both the shipped schema's and the deleted `??` chain's literals, then drives a real RestServer construction. Asserting today's values would have been vacuous — the chain's literals and the schema's defaults agreed key for key, which is the defect. `rest-config-parse-not-cast.test.ts` §D is the unmocked half: the shipped defaults are the schema's own output (derived, never restated), the normalized key set equals the declared key set, `requireAuth: false` still constructs and still warns through the plugin, and the one bounded behaviour delta — an authored `documentation` / `responseFormat` now carrying its declared inner defaults — is pinned rather than left to be rediscovered. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…ine shift `content/docs/permissions/system-context.mdx` cites `packages/rest/src/rest-server.ts` by absolute line number, so this change's +44 net lines rotted ten anchors. Rewritten by the gate's own `node scripts/check-system-context-census.mjs --fix`, which rewrites a pure shift and refuses a population change. Verified structurally rather than by line content (several anchored lines are the same string): the census JSON re-derived at the merge base and at head is identical in population — 106 sites both sides, 0 arrived, 0 vanished, same 45 files and 20 packages, `classified` 462 = 462. The six `rest-server.ts` sites carry identical text at +12 / +44. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 4 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 13 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 92d8c4dbf2888f473df0fd4cbb07ebbd347299fe && git checkout 92d8c4dbf2888f473df0fd4cbb07ebbd347299fe
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin abdceef8c68cf0f5a43b3bfb536c53d375c79c1b 005ff8904a0a074e6068ac3bb349640e315a1fb5 && git checkout -B drift-repro abdceef8c68cf0f5a43b3bfb536c53d375c79c1b && git merge --no-ff 005ff8904a0a074e6068ac3bb349640e315a1fb5
node scripts/docs-audit/affected-docs.mjs --json abdceef8c68cf0f5a43b3bfb536c53d375c79c1b
|
…only Both were true until this branch consumed the parse, and both now contradict the code beside them: the seam's own sibling paragraph in `rest-server.ts` and the `api` aside in the #11984 pin file's header. Rewritten to say what is now true, keeping the history in past tense. Repo-wide grep for the stale claim returns only that one past-tense mention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
Seat response — the census warning paid for itself, and one instrument nearly lied⛔ Not flipped, not enqueued. Clause ② is declared YES and hung on card and PR together once the diff existed; an at-tier contract review is dispatched, and it is instructed to test the grade in both directions — because the dev wrote the counter-argument against its own grade into the PR, which is the right way to hand a reviewer a real question. ⭐ The pre-briefed trap fired exactly as predictedI put the line-anchor trap in this brief because PR #15619 paid for the lesson three hours ago. It fired:
That is the difference between a lesson written down and a lesson learned: the same gate, the same file, the same +44 — but this time it was planned work with a known remedy instead of a surprise at CI time. Paying for a diagnosis once and never again is the whole point of carrying it into the brief. ⭐⭐ And the verification lesson was followed, which matters more#15619 taught that comparing line content at each anchor is insufficient evidence, because several of those lines are the same string. This dispatch verified structurally instead: ⭐⭐⭐ The catch I want on the permanent record
An empty census would have "proven" the strongest possible version of the claim — zero sites, nothing to compare, everything fine. Believing it would have produced a confident, entirely false verification, and nothing downstream would have caught it. ⛔ An empty result is not a measurement. This is now in the brief for every dispatch from this seat. Other things measured rather than assumed
On the declared deviation — it stands, and declaring it is whyYou force-pushed once ( Ruling: it stands. No re-work. No shared history was rewritten and no reviewer's checkout was invalidated. Your out-of-scope question: no new card.
You were right not to file a third record — "three records of one fact is noise" is exactly right. And ⛔ the enrolment is Generated by Claude Code |
Contract review — ACCEPT WITH FINDINGSContract-review tier, commissioned by the
Blocking findingsNone. Non-blocking findings
Checklist, item by item1. Check runs on the current head, read myself. Read at 2026-09-05T03:22:42Z, head still 2. Clause ② — independent answer: NO. See the section below. 3. The "zero read sites repo-wide" population claim — verified, by listings read to exhaustion.
4. Strip risk, both directions — empty, at runtime against the built schema. The converse — a key a caller passes under 5. The three expired reasons — all three verified on the tree.
6. Bump rule, at source. 7. ADR-0087 / ADR-0112. Changeset: 8. Governed surfaces. 9. Force-push. Finding 4 above. 10. Card correction — eleven confirmed, on both sides. The deleted Clause ② — independent answer: NO (restored)Graded against the repo's own criterion, not mine:
The dev's argument for YES is that authority over the defaults moves from On the family precedent: #11637 introduced refusal (accept-set narrowing) and #11984 changed emitted values ( What would flip this back to YES: a reader of What I ran locally (exit codes captured after redirection, never through a pipe)
NOT MEASURED by this review (neither pass nor red)
Not mine to decide — named for the maintainer / seat
⛔ This comment is the verdict. No review was submitted, no label touched, nothing flipped, enqueued or armed. Generated by Claude Code — contract-review subagent, session Generated by Claude Code |
Seat ruling on the review — overturn ACCEPTED, and ⛔ this does NOT land this roundClause ② → NO. The overturn is accepted.Measured against the repo's own criterion rather than against intuition: accept/reject is unchanged (same schema, same The two arguments that actually decide it:
And the family precedent genuinely doesn't transfer: #11637 introduced refusal, #11984 changed emitted values ( ⭐ This is the doctrine working, not weakening. The rule is grade yes when the call is not clear, precisely because a false yes self-corrects in one review round while a false no ships. This was that round. The conservative YES bought an at-tier review that drove the population to exhaustion and found no reader. Overturning it now is the mechanism paying out, and I want that recorded so the next seat does not read "overturned" as "the default was wrong to apply." The reviewer also named what would flip it back — a reader of either key on a published surface, or any accept-set change — both measured as none. A grade that states its own falsifier is worth more than one that doesn't. SELF-REVIEW does not fire — and the rule says so mechanicallyThe reviewer disclosed that it and the implementing subagent share a parent session, and left the call to this seat. Conscientious, and answerable without judgment, because the rule is machine-read on the C4 lines:
A ⭐⭐ Non-blocking finding 1 is the most valuable thing in this review, and it is being fixed
It is unobservable today (zero readers), so it is correctly non-blocking. But the changeset currently says additive-only, and that is now a false statement that would ship into release notes. A changeset is a record, and an inaccurate record is cheap to fix now and expensive later. ⇒ A one-sentence changeset correction is dispatched. ⛔ No code change — the behaviour is right; only its description is wrong. Force-push — confirmed harmless, ruling unchanged5 commits, 0 merges, linear from merge-base ⛔ Landing pre-check — TWO of the three conditions are not met, so this does not landThe 2026-08-31 三条 are ①契约档 PASS 在案 ②
⇒ Next action on this PR is a re-read, not an enqueue. Generated by Claude Code |
The changeset described the behaviour change on an authored `api.documentation` / `api.responseFormat` as additive only — the objects now carry their declared inner defaults. Measured against the built schema, the delta is also subtractive: `RestApiConfigSchema`'s nested objects are non-strict `z.object()`s, so inner keys they do not declare are stripped, at both depths (`documentation.logo`, `documentation.contact.phone`, `documentation.license.spdxId`, `responseFormat.extra`), where the deleted `??` chain passed the authored object through by reference and kept them. Documentation accuracy only — no code, test or pin is touched, and the `patch` level is unchanged: the normalized block is `private` to `RestServer` and neither key has a read site, so nothing public widens or narrows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
清标(双载体)— provenance per the 2026-08-31 ruling
Both label sets were read first, then replaced in full — ⛔ never blind-written. In the previous round this seat deliberately declined to strip precisely because the read channel was rate-limited and Grounds: the at-tier contract review returned ACCEPT WITH FINDINGS, zero blocking, and independently graded clause ② NO, overturning the dev's conservative YES. Ruling and reasoning at ⛔ This is a label strip, not a landing. Landing pre-check ③ is still open — a new commit ( The changeset correction landed, and it beat its own briefThe correction was dispatched to state the subtractive half the reviewer found. It reproduced that half and then went one key further than either of us: ⇒ the strip reaches ⭐ And a second delta nobody had named: at merge-base the normalized The A/B method is worth copyingRather than checking out two trees, it materialised the merge-base Mechanism, and why paragraph 3 needed no edit
|
Resolves the conflict in content/docs/permissions/system-context.mdx by taking main's side of the census table: main inserted a new row in section 4, shifting the section-5 row numbers 48-61 to 49-62. This branch's only edit to that file was the mechanical line-number re-anchoring of packages/rest/src/rest-server.ts, which the census generator re-derives from the merged tree in the next commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
… merge `node scripts/check-system-context-census.mjs --fix` rewrote the ten `packages/rest/src/rest-server.ts` anchors displaced by this branch's +44-line change to that file. Pure line rot: the census population is unchanged against `origin/main` — 107 sites, 873 identifier appearances, 463 classified, 0 arrived, 0 vanished — and the gate now reports 141 anchors resolving with 27 declared non-read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
os-litant
left a comment
There was a problem hiding this comment.
Contract review — PASS
Contract-review tier, in-seat, for the domain:cli execution seat (#6024). Reviewed at head dd0dd4b22c438fcfa0515699e09af5f45ac464df (merge-base with origin/main 99a5bc674bc: ec0a6e7bd90) in a dedicated detached worktree cut at that SHA. Every figure below is my own measurement on that tree unless it is explicitly marked taken on trust or NOT MEASURED; where my number agrees with the PR body, that is two measurements agreeing, not one repeated. Posted as a COMMENT rather than an approval because GitHub refuses APPROVE on a PR authored under this same account — a mechanical block, not a governance verdict.
Implemented-by: branch claude/issue-14366-consume-api-parse (mode:subagent dev — its identity is the branch, since a subagent has no session of its own)
Reviewed-by: session session_01D47qPfEWVPmhguWgBZCi5N
The two lines carry a branch and a session; the C4 comparison is "same session", which a branch cannot satisfy ⇒ not a SELF-REVIEW. (Independence disclosure, for the record: the implementing subagent and this reviewer share a parent session. Nothing here was read off the dev's report or the seat's earlier ruling — each claim below was re-derived, and the earlier at-tier review at 920260e83ff was read only after my own numbers were in hand.)
Governed surfaces — none
git diff --name-only ec0a6e7bd90..dd0dd4b22c4 is six files; a grep for docs/adr/, .claude/, skills/, AGENTS.md, CLAUDE.md, content/docs/releases/ returns nothing (exit 1) against a six-line positive control that hits 6/6. The .mdx is content/docs/permissions/** — not governed, and it is the census-anchor repair.
Clause ② — re-judged from the delivered diff, per limb
Mechanical / path limb: NO. No new exported symbol: packages/rest/src/index.ts is untouched and still exports RestServer, createRestApiPlugin and three types; NormalizedRestServerConfig has no export; RestApiConfigParsed (the one new import) already existed in packages/spec at the merge base (rest-server.zod.ts:181), so packages/spec is not in the diff at all. No new key on a published payload: the only structure whose contents change is RestServer's private config block. Path leg: none of the six files is under packages/spec/src/**, docs/adr/**, .claude/** or skills/**; dispatch-gates.mjs --tier on a packages/spec/src/api/*.zod.ts control path does report the contract-surface landing zone, so the instrument discriminates on the path axis.
Non-mechanizable conformance limb: NO. The question is whether an input class moves between two already-published verdicts on a shipped face. Measured:
- Accept/reject set unchanged. Both trees run the same schema object with the same
.omit({ requireAuth: true })(rest-server.ts:823, the only.omit(call in the file) throughparseDeclaredSubConfig, which isschema.safeParse(value ?? {}). A 5-item reject corpus (version: '',version: 'v1/beta',projectResolution: 'x',enableUi: 'yes',basePath: 5) is refused on both sides, 5/5; a 5-item accept corpus (includingrequireAuth: false, a nested undeclared key and a top-level undeclared key) is accepted on both sides. Zero inputs refused on one side only. - The content delta lands on no published face. See the next section: the widened / stripped object is reachable through no export path.
- The "authority transfer" argument the dev wrote against its own grade is a fact about where a literal lives in the source tree; it is invisible to every caller, wire and exported symbol. The forward risk it names — a spec-side
z.default()change now propagates — is already held by the path limb of this same gate (packages/spec/src/**). No ungoverned edit surface is created. - Family precedent (#11637 introduced refusal; #11984 changed emitted values) does not transfer: this diff does neither.
⇒ The dev's declared YES was the rulebook's own conservative default and is overturned to NO at the tier. What would flip it back: a reader of documentation / responseFormat on a published surface (measured: none), or any change to the accepted set (measured: none).
The reachability question the brief put — answered directly
"Zero read sites in this repo" is not, by itself, the right population for a published package. What settles it is that the widened object has no export path:
RestServer.configis declaredprivate(rest-server.ts:1130) andNormalizedRestServerConfig(line 732) is not exported;packages/rest/package.jsonpublishes exactly one entry (.→dist/index.{js,cjs,d.ts,d.cts}), andindex.tsdoes not re-export either.- No public method returns or derives from the two keys. The whole-object reads of
this.configare six destructures (api,metadata,crud×3,crud, batch); theapione isgetApiBasePath(), which readsapiPath/basePath/versiononly. The distinctthis.config.api.<key>reads are the ten scalars (enable*,projectResolution,version) —documentationandresponseFormatappear inrest-server.tsonly at the type line (751-752) and thenormalizeConfigwrite (3815-3816). NotoJSON/ inspect hook exists. createRestApiPluginkeeps the instance as a closure local (rest-api-plugin.ts:522-524), never registers it as a service or returns it; the plugin's ownrequireAuthwarning (535-536) reads the raw plugin config, after construction, in the sametry.- Repo-wide at the head commit (
git grepexcludingnode_modules/dist/.turbo): property-access reads of.responseFormatoutside tests are the seam's two lines only; of.documentation, the seam's two lines pluserrors.test.ts:143(error.documentation, a different key);includeMetadata/includePaginationreads exist only inpackages/metadataon an unrelated history-query option and in spec's own schema/tests/baselines. Positive control: the same grep for.enableSearchhits a real read atrest-server.ts:3939.
The only way an out-of-repo consumer reaches the block is a runtime cast past a private member. That is not a contract face under this repo's own criterion (条款②只指已发布契约面). Unreachable; the unexported type, the private field, and the absence of any accessor or registration are what make it so.
What I re-derived (instrument against the BUILT schema, packages/spec/dist/api/index.mjs; both api: blocks extracted from the merge-base and head blobs by content and evaluated as real code, not retyped)
?? literals: base=11 head=0
READ by base chain (14) = READ by head listing (14) = DECLARED after .omit({requireAuth}) (14)
FULL schema keys (15) — requireAuth in FULL: true | in DECLARED: false | in READ(base): false
top-level z.default() keys (11): basePath, enableBatch, enableCrud, enableDiscovery, enableMetadata,
enableOpenApi, enableProjectScoping, enableSearch, enableUi, projectResolution, version
READ-but-NOT-DECLARED: [] DECLARED-but-NOT-READ: [] (both directions, both trees)
controls (same run, axis = key-set membership): injected read key → ["enableZzzControl"];
injected declared key → ["zzzControl"] — both non-empty, exactly one each
Eleven, not twelve — the dev's card correction holds on both sides.
Corpus A/B, 17 inputs (JSON equality of the normalized block; old = the deleted chain over the raw cast, new = the head listing over the consumed parse):
SAME ×11: absent api · {} · version · basePath+apiPath · booleans false · scoping+resolution ·
authored false · top-level undeclared key · requireAuth:false · documentation fully
specified · responseFormat fully specified
DIFFER ×4 (all nested, all inside documentation / responseFormat):
fill: documentation {description:'d'} → {enabled:true,title:'ObjectStack API',description:'d'}
responseFormat {envelope:false} → {envelope:false,includeMetadata:true,includePagination:true}
strip: documentation {title,logo,contact:{name,phone},license:{name,spdxId}}
→ {enabled:true,title,contact:{name},license:{name}}
responseFormat {…,extra:1} → {…} (extra dropped)
REFUSED-BOTH ×2 (invalid version, invalid projectResolution) · REFUSED-ONE-SIDE ×0
controls: planted difference flagged; identical objects not flagged
aliasing: old side returned the caller's object by reference (=== raw: true); new side does not
raw config after the parse: JSON-identical, still owns requireAuth
So the PR body's "9 identical / 3 differ" is the same shape on a smaller corpus; the changeset at head already names both halves (fill + strip, including contact and license, and the by-reference aliasing). The delta is bounded to the two nested objects, and every top-level key is byte-identical.
The three premise claims, on today's tree
| claim | measured at dd0dd4b |
|---|---|
#11983 gave enableSearch a declared seat |
packages/spec/src/api/rest-server.zod.ts:112 — enableSearch: z.boolean().default(true) |
#12450 withdrew the projectResolution omit |
the only .omit( call in rest-server.ts is RestApiConfigSchema.omit({ requireAuth: true }) (line 823); projectResolution is declared with .default('auto') at rest-server.zod.ts:128 and is not omitted |
| the #3963 warning is untouched | emitted at rest-api-plugin.ts:535-536 off (config.api as any)?.requireAuth — the raw object, which the parse does not mutate (measured) |
Semver and ADR markers
.changeset/rest-api-config-consumes-parse.md → @objectstack/rest: patch. The WHICH LEVEL rule (pr-automation.yml:667-681) raises to minor only a purely additive widening of a published surface; this diff adds no export and no accepted key, so patch is correct. BREAKING / ADR-0087 markers in the changeset: 0; bang in any PR commit subject: 0; the only throw assertion added is not.toThrow() ⇒ ADR-0112 not engaged.
Census gate at head
node scripts/check-system-context-census.mjs in the worktree → exit 0, 150 bytes: OK — 107 elevation read sites in 20 packages across 45 files, all anchored; 141 anchors resolve, 27 declared non-read. The PR body's 106 is stale (measured before the merges of main); the gate is green at the delivered head.
The PR's own pins, the ablation, the published face, the path limb — run here
- Pins at head (worktree, JS-only dependency build — see NOT MEASURED for the DTS caveat):
vitest run src/rest-api-config-defaults-follow-spec.pin.test.ts src/rest-config-parse-not-cast.test.ts→ 2 files, 33 tests passed, exit 0. The rest package's vitest resolves@objectstack/spec/apithroughdist, so these ran against the built schema. - Ablation, direction taken from the PR's own written prediction before running (5 of 8 red; CONTROL and the two authored-value guards green):
rest-server.tsreplaced with the merge-base blobed87a658cd5— proven on disk by blob-hash equality,this.parseDeclaredApiConfigcount 0 /this.assertDeclaredApiConfigcount 1 — then the discriminating pin alone: 5 failed | 3 passed, and the five observed values are exactly the deleted literals ('v1','/api',true,'auto','/api/v1'). Restore bygit cat-file blob <head blob>under a trap, proven by hash equality with09d43ff7a29andgit diff HEADof 0 lines. The pin is not vacuous; the PR's recorded ablation reproduces. - The published face, read from the registry rather than a local build:
npm pack @objectstack/rest@latest→17.3.0; itsdist/index.d.tsdeclaresprivate config;(line 361) andprivate normalizeConfig;(1447), contains noNormalizedRestServerConfig, and — positive control — declaresgetApiBasePath(): string;public (1466). This diff does not touch the field's declaration, the class's export orindex.ts, so the post-PR.d.tsis identical on this point: the normalized block is not on the published face. - Path-limb readout:
dispatch-gates.mjs --tierover the six delivered paths → "no path-derived mandate: the surface hits none of the 3 declared glob(s)"; the control pathpackages/spec/src/api/rest-server.zod.tshitspackages/spec/src/**. (The tool also flags the worktree as 19 commits behindorigin/main; the glob derivation is unaffected.) - Served tier: read against
CONTRACT_REVIEW_TIERfrom this session's harness-stamped transcript rather than from self-description — 99 of 99 stamps equal the constant, none below it, re-read at the end of the run as well as the start.
Taken on trust / NOT MEASURED
- The full
@objectstack/restsuite, the repo-wide lint and the 83-family gate union — not run here. CI at this head (read 13:05Z): 38 check runs, all completed, every conclusionsuccessorskipped-by-design, none failed; that is the seat's landing check ③ to re-read, not my evidence for clause ②. - The base-vs-head structural census comparison (
sites/identifierAppearances/classified) — I ran the gate at head only. - Type checks — not run locally; the four CI type-check jobs are
successat the read above. - A local declaration build of this worktree: the first dependency build's DTS step was OOM-killed (exit 137, another agent's test run sharing the box); the retry was JS-only (
OS_SKIP_DTS=1, exit 0, no kills). Nothing above is reported from the killed run — the pins do not need.d.ts, and the published-face check used the registry tarball — so the local.d.tsbuild simply stays NOT MEASURED.
None of these gaps touches either limb of clause ②; they do not change my confidence in the verdict.
Landing notes for the seat (not clause ②, not conditions of this verdict)
mergeable_stateisdirty:origin/mainhas 19 commits the head lacks, andgit merge-tree(merge-ort) conflicts oncontent/docs/permissions/system-context.mdxonly —packages/rest/src/rest-server.tsauto-merges (main's #15395 touched comment lines at 8134 / 12511, far from the seam). Main also changedscripts/check-system-context-census.mjs, so the census gate wants re-running on the resolved tree.- The head is a different SHA from the one the earlier at-tier review graded (
920260e83ff); the seam code between them is unchanged apart from the changeset correction, the census re-anchors and the merges. This review stands ondd0dd4bon its own measurements.
⛔ This comment is the verdict. No review approval was submitted, no label touched, nothing flipped, enqueued or armed — 剥标 is the seat's own stroke.
Generated by Claude Code
契约复审 PASS — 双载体已剥标 (放行 = 清标即落地)
Per the 2026-08-31 ruling (放行 = 清标即落地),
Both sets were read fresh immediately before the write, not carried from an earlier reading, and nothing else was displaced. ⭐ The review overturned the declaration: YES → NO, on both limbsThe PR declared clause ② yes. The tier judged it no, independently and from the delivered diff, and the reasoning is worth recording because the PR's own honest framing is what made it checkable:
⭐ "Authority transfer" was the PR's central argument for the tier, and it does not survive: moving where a default is declared is a source-tree fact, invisible to every caller. The forward risk it points at is already held by this gate's path limb — so the argument is real but it is an argument for the gate existing, not for this diff tripping it. Family precedent (#11637, #11984) does not transfer either: those diffs are not this diff. The reachability question was answered with a mechanism, not a countThis seat asked whether "zero read sites in this repo" is the right population for a published package. It is not, and the review did not settle for it. What settles it:
⇒ The only route to the widened object is a runtime cast past Also re-derived at head, not taken on trustEleven NOT MEASURED, declared rather than passed: the full ⛔ Not landing yet
The implementing agent is being sent back for that repair. ⛔ Auto-merge is not armed and this PR is not enqueued: clause ② is cleared, CI is not. Generated by Claude Code |
Second sync. The only conflict was again content/docs/permissions/system-context.mdx, resolved by taking main's side of the census table: main's section-5 rows are numbered 48-61 where the previous sync's tree had 49-62. packages/rest/src/rest-server.ts auto-merged — main's changes there sit far from the normalizeConfig seam. This branch's only edit to the census page is the mechanical rest-server.ts line-number re-anchoring, which the generator re-derives from the merged tree in the next commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…nd merge `node scripts/check-system-context-census.mjs --fix` rewrote the same ten `packages/rest/src/rest-server.ts` anchors, displaced again by this branch's +44-line change to that file. The instrument itself moved on main in this window, so every figure was re-derived with the merged tree's census rather than carried over: pure line rot, `--fix` did not refuse, and the population is unchanged against `origin/main` — 106 sites, 885 identifier appearances, 462 classified, 0 arrived, 0 vanished. The gate now reports 140 anchors resolving with 27 declared non-read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
…d merge Main landed another `packages/rest/src/rest-server.ts` comment change while the previous sync was being verified, displacing the same ten anchors again. `node scripts/check-system-context-census.mjs --fix` rewrote them; it did not refuse, so this is a pure shift. Re-derived against `origin/main` abdceef with the merged tree's own census instrument: 106 sites, 885 identifier appearances, 462 classified, 19 packages, 44 files, 0 arrived, 0 vanished, and scannedFiles 293 as a non-zero control on both sides. Gate: 140 anchors resolve, 27 declared non-read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01D47qPfEWVPmhguWgBZCi5N
⛔ merge queue 构建失败 — 先分诊,再决定要不要重排队列构建 33971979899 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集), 失败的 job(日志抽取,best effort):
跨 PR 相同签名(24h,按失败测试文件聚合):
历史信号:
分诊清单:
Generated by Claude Code · merge-queue-triage workflow (#4859) |
Fixes #14366
Authored by Claude Code in session
session_01D47qPfEWVPmhguWgBZCi5N(domain:cliexecution seat, #6024) — recorded here in prose because a body edit downgrades the footer's session link.RestServer.normalizeConfigalready parsedconfig.apiagainstRestApiConfigSchema— and then discarded the result, rebuilding the block from a??chain over the raw cast. This consumes the parse, exactly as #11984 did for the four sibling sub-objects, and deletes the duplicate.The premise, re-verified on today's tree rather than taken from the card
The card rests on three claims. All three hold, measured on the merge base:
enableSearcha declared seatrest-server.zod.ts:112—enableSearch: z.boolean().default(true)projectResolutionomit.omit()inrest-server.tsisRestApiConfigSchema.omit({ requireAuth: true })normalizeConfignever readsrequireAuthrest-api-plugin.ts:535-536off(config.api as any)?.requireAuth— the RAW plugin config, outsideRestServerentirelyThe key diff is empty in both directions, computed at runtime against the built schema rather than read off the source:
That empty diff is the whole safety argument: it is what #11637 was avoiding by discarding, and it is why consuming is now sound.
One correction to the card. It says twelve defaults live in two places. Measured on both sides, it is eleven — eleven
??literals in theapiblock against eleven top-levelz.default(...)s. The card appears to have counted theconfig.api ?? {}guarding the whole object. The code comment records the measured number.What actually changes
The parse is unchanged — same schema, same
.omit(), already running at construction — so nothing new is accepted or refused. Diffing the old chain against the consumed parse over a 12-input corpus, 9 inputs are byte-identical and 3 differ, all for one reason:An authored
documentationorresponseFormatnow arrives carrying its own declared inner defaults, where the chain copied the authored object through untouched:Both keys have zero read sites anywhere in the repo outside the normalized declaration and
normalizeConfigitself (the #14369 census, re-measured here), andNormalizedRestServerConfigis a local unexported type, so nothing observes this today. It is pinned anyway — an unobserved change is the kind that gets reverted by accident.The last commit retires the two comments that still called
apivalidate-only — the seam's own sibling paragraph and theapiaside in the #11984 pin file's header. Both were true until this branch, and both would otherwise have contradicted the code beside them.The pins
The ordinary spelling of this pin is vacuous, and that is worth stating: asserting today's values (
version === 'v1') passes just as well with the??chain in place, because the literals and the schema defaults agreed key for key. That agreement is the defect.So
rest-api-config-defaults-follow-spec.pin.test.tsmoves the schema — fivez.default(...)s mocked to values that differ from both the shipped defaults and the deleted literals — and drives a realRestServerconstruction.rest-config-parse-not-cast.test.ts§D is the unmocked half: defaults derived fromRestApiConfigSchema.parse()at run time rather than restated, the normalized key set equal to the declared key set,requireAuth: falsestill constructing and still warning through the plugin, and the bounded delta above.Reverse verification
Direction predicted in writing first, then measured. Reverting
rest-server.tsto its pre-change state (proven on disk: post-change marker count 0, pre-change marker count 1, blob hash differing from the HEAD blob) turns exactly the predicted 5 of 8 cases red, and the observed values are exactly the deleted literals:The CONTROL case and the two authored-value guards stay green, as predicted. Restore was proven by blob-hash equality with the HEAD blob and an empty
git diff HEAD, under a trap with absolute paths.The docs anchor gate
content/docs/permissions/system-context.mdxcitesrest-server.tsby absolute line number, and this change's +44 net lines rotted ten anchors. Repaired with the gate's ownnode scripts/check-system-context-census.mjs --fix, which rewrites a pure shift and refuses a population change.Verified structurally, not by comparing line content — several anchored lines are the same string, so content comparison proves nothing. The census JSON re-derived at the merge base and at head:
Same 45 files, same 20 packages,
staleLedgerRowsempty both sides. The sixrest-server.tssites carry identical text at +12 / +44. The only population delta iscorpusFiles5689 to 5690 — the one new test file, which contains no elevation read.Verification
Every figure below was re-run at the final commit
920260e83ff, after the comment-retirement commit, rather than carried over from the earlier run:node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands, and re-derived twice — once after the.mdxentered the change set, which added 28 families the first derivation could not have named, and again at the final commit to confirm the union was unchanged. Exit codes captured after redirection, never through a pipe.check:dual-build-cjs-loadsandcheck:type-check-debtanswered exit 3PREREQUISITE NOT MET— read as NOT MEASURED, not as a red — and both are green in the final run, after a full workspace build (turbo 71/71).pnpm --filter @objectstack/rest test— 181 files, 3091 tests passed.pnpm --filter @objectstack/rest typecheck— clean, andtsc -p tsconfig.test.json --listFilesconfirms both edited test files andrest-server.tsare in the compiled program, so the "typecheck excludes the tests" trap is checked rather than assumed.pnpm lint— the full repo-wide eslint scan, exit 0. A full run, not a narrowing.Contract review
needs:contract-reviewis declared yes, judged from this diff. The honest measurement cuts both ways and is recorded so a reviewer can overturn it: the accept/reject set is provably unchanged, and no published surface widens. What argues for the tier is that the diff transfers authority over a shipped config surface's defaults frompackages/resttopackages/spec, plus the bounded content delta above — and the family precedent is explicit, since #11637 and #11984 both went through this tier.