feat(client, rest): bind both getHistory exits to HistoryMetaItemResponse; ledger row names the schema - #16694
Conversation
…onse; ledger row names the schema Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
The `note` field ships to authors and operators through generated surfaces, where an issue id resolves to nothing. The anchors move to an adjacent source comment, which only a reader who can resolve them sees. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YFY46JydE1gMxQG1TqBcMZ
…story-door-schema-rebind
📓 Docs Drift CheckThis PR changes 2 package(s): 9 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 22 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 3ac8bd89f96104a667be73cc7c084bcfb60c8b90 && git checkout 3ac8bd89f96104a667be73cc7c084bcfb60c8b90
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 001a83b0486391847d45f6866896c53ad8714569 cd9f93413eb577817fdfa9d0ad8cb7867dcd079b && git checkout -B drift-repro 001a83b0486391847d45f6866896c53ad8714569 && git merge --no-ff cd9f93413eb577817fdfa9d0ad8cb7867dcd079b
node scripts/docs-audit/affected-docs.mjs --json 001a83b0486391847d45f6866896c53ad8714569
|
|
契约复核 PASS —
|
Docs-drift owed item: all 10 rows answered — nothing falsified, nothing pushedVerdict: Case A. No page in the corpus is falsified by this diff. No commit, no push, no docs edit. The branch head stays Tree auditedThe bot computed on Auditing in the Why the bot could not answer itTwo distinct reasons, one per half of its list:
SearchedCorpus: Positive control (live, run before the negatives): One hit, on the exact page the bot flagged. The instrument reaches the corpus and would have reported the term had it been elsewhere — so the zeroes below are the corpus's zeroes, not the instrument's. Second control: Terms swept (each run corpus-wide,
The emitter-side hand read the bot said it structurally cannot doThe bot's own limitation — "a page that states a rule by its inputs shares no identifier with the emitter that implements the rule" — is real, and the sweep did surface a hand-written page outside its 9:
Not falsified — it is corroborated. Three reasons: (a) the verb union it spells out matches No edit made: nothing here is this diff's own consequence, and touching it would be widening. The 9 rows1. Sentence read (line 78, inside the
This is not the method this diff touches. It is the kernel service contract, and the page says so in its own Callout: Source: and Decisively, the two
Different names, different members, different files, different layers. The page names no field of the history door's body — a corpus-wide scan of it for 2. Sentence read (line 253, under A cross-link naming the route's path only. No body, no field, no 3–8. The six
9. Sentence read (lines 170–172, under
"Both routes" are Release-owned page audit (read-only — not edited, and not proposed for edit)
The anchor that put it on the list is line 190:
Two checks, both pass. First, the sentence's pointer to One negative worth recordingThe generated (not hand-written) That is the widened GatesNone run, and none owed: Case A means no file changed, so there is no tree to gate and no empty commit was made. Head is unchanged at Generated by Claude Code |
Fixes #13523
Clause-②: no
Clause-② — ruled
yesby the dispatching PM (#6024), on the condition its ownClaim:setThe claim (
5574952927) declared Clause-② conditionally:yesif the hand-written inline shape andz.infer<typeof HistoryMetaItemResponseSchema>differ,noif they are byte-equivalent. The delivered changeset measures them as differing, so the condition resolvesyes:HistoryMetaItemResponseactorstringnullfor every system-initiated write (boot sync, migration, scheduled job), and the schema declares it "never a sentinel string"opstringcreate/update/delete/rename/publish/revert)version,previousName,ref.versionref.orgorg?: string(optional)_unwrapwith no type argument ⇒Promise<unknown>⇒ the published return type narrows. Same shape as #14313, and the maintainer's #12104 ruling (
5472614711) puts this class atClause-②: yesexplicitly. The changeset is graded@objectstack/client: minor/@objectstack/rest: patchand carriesBREAKING (types)in its own body.The two consumers bound
packages/rest/src/rest-route-ledger.ts, theGET /api/v1/meta/:type/:name/historyentry now carriesresponseSchema: 'HistoryMetaItemResponseSchema', joining its already-boundaudit/rollback/diffsiblings. Data only, in a package-internal module: no route, handler or emitted byte moves. This row was the one member of the metadata family left unfilled, because the declaration (MetadataProtocoldeclares nohistoryMetaItemmember, so the REST history door's request literal is compiled against nothing — the last undeclared read door of the audit/history pair #12005, PR feat(spec): declare MetadataProtocol.historyMetaItem and de-cast the REST history door #13521) landed after the row was written.getHistoryexits inpackages/client/src/index.ts— the unscopedObjectStackClient.meta.getHistoryand the scopedScopedEnvironmentClient.meta.getHistory, each now annotatedPromise<HistoryMetaItemResponse>with the unwrap call given the matching type argument.The two
getHistoryoccurrences: measured, not assumedTriage flagged binding only one as "this card's most likely error" and asked for the relationship to be measured. They are two exits of ONE door, not two doors.
Located by symbol (⛔ never by the stale line numbers — the file moved under #15445, #14526 and #16676):
packages/client/src/index.tsholds exactly twogetHistorydefinitions, one onObjectStackClient.metaand one onScopedEnvironmentClient.meta.The producer side proves they are one mount. In
packages/rest/src/rest-server.ts,registerMetadataEndpoints(bp)is called from inside theregisterForBase(bp)closure, andregisterForBaseis invoked twice — once withbasePathand once withscopedBase(${basePath}/environments/:environmentId). Inside it,isScopedis derived from that same string (basePath.includes('/environments/:environmentId')) andregisterPerItemRouteregisters one handler body for the/:type/:name/historypath under both bases; the only difference is whetherenvironmentIdis read fromreq.params. ⇒ one handler, replayed against two prefixes, answering a byte-identical body.Both exits are bound. Their declared states before this change were nevertheless different — the unscoped carried a hand-written inline object type, the scoped carried nothing (
Promise<unknown>) — so binding one and leaving the other would have relocated the divergence rather than removed it.return-type-precision.test.tspinsUnscopedHistory ≡ HistoryMetaItemResponse,ScopedHistory ≡ HistoryMetaItemResponseandUnscopedHistory ≡ ScopedHistory; that third assertion is the one that refuses a half-fix, and the ablation below shows it is red for either half alone.Gates — measured at head
cd9f93413eb577817fdfa9d0ad8cb7867dcd079bEvery reading below is against that commit, working tree clean (
git status --porcelainempty,git diff HEADempty). Each exit code was captured before any pipe; no verdict is read from a;-sequenced batch.origin/mainmoved to001a83b048during this run. It was not re-merged: its one new commit (#16650) touchespackages/drivers/*,packages/services/*,packages/types/*and adds two new test files underpackages/rest/src/— it overlaps none of this PR's five files, so no merge was needed and the head above is the head that was measured.Measured green
dispatch-gates --commandsderived family — 58 commands, all run individuallydispatch-gates --ran ran.listreconciliation✓ 58 derived famil(ies) accounted for — 58 run, 0 NOT-MEASURED, 0 UNRUNpnpm --filter @objectstack/client typechecktsc --noEmitclean;check:test-typecheckOK, 0 files / 0 errors intest-typecheck-debt.jsonpnpm --filter @objectstack/rest typecheck@objectstack/clientunit tier (pnpm --filter @objectstack/client test)pnpm exec eslint . --no-inline-config(whole repo)The
ran.listwas built from the byte-for-byte command strings--commandsprinted (verifieddiff-identical to the harvested list), never from log file names.NOT MEASURED — with each gate's own stated prerequisite
dispatch-gates --ran's0 NOT-MEASUREDis the runner's own accounting of the derived set, not verification of everything CI runs. The honest split:check-changeset-no-majorLEVEL AXIS — exit 0 overall, but the gate itself prints: "LEVEL AXIS: NOT MEASURED — no clause-② declaration was readable for this PR… nopull_requestpayload was available to read a declaration from." Prerequisite: a livepull_requestpayload, which exists only once this PR does. CI measures it on this PR.check-shard-attestation.mjs(×3,${{ matrix.shard }}) andcheck-test-completeness.mjs(×2,$RUNNER_TEMP/*.log). Prerequisite: a CI run; there is no local invocation. Named by the tool as⊘ NOT MEASUREDand deliberately outside the 58.check:init-service-contract,check:live-db-isolation,check:meta-type-normalized,check:optional-error-sink,check:resume-authority-declared,check:runner-env-posture,check:settings-bind-window,check:startup-registry-verdict,check:verify-stand-in,check:wildcard-fallthrough.lint.ymlcarries no paths filter, so CI runs them on every PR; no path derivation can say whether they bite. Their absence from the derived total is not a clearance.silent— a fact about a tracked-file baseline, not about these paths (4 of them keep that roster in a directory one of these paths is in). Not clearance in either direction.pr-labels.mjs, the publish smoke script) — outside--commandsby construction, run by CI.Zero gates hit
exit 3, zero were killed by the OS, zero fell to UNRUN.Ablation — the new pin is proved able to fail
return-type-precision.test.tsis the guard for this whole card, so it was ablated rather than trusted. Nothing was committed: the committed tree was mutated in place by exact-anchor replacement that refuses on anchor count ≠ 1, the mutation was proved on disk by occurrence counts andgit hash-object, and restoration was proved by blob hash and an emptygit diff HEAD— never by an exit code. Atrap … EXIT INT TERMwith absolute paths held the restore.Pristine blob of
packages/client/src/index.ts:7fa9e129ca1aa66c8bb099a243351e9b73389606.tsc --noEmitcheck:test-typecheckviteston the pin fileunwrapResponse(res)9e7640b5ee…Promise<HistoryMetaItemResponse>2→1; oldactor: string;0→1;unwrapResponse<…>1→0src/return-type-precision.test.ts_unwrap(res)12e1014d8f…Promise<HistoryMetaItemResponse>2→1;_unwrap<…>1→0src/return-type-precision.test.ts615a6ac70b…Promise<HistoryMetaItemResponse>2→0; both unwrap type args goneTS6133 'HistoryMetaItemResponse' is declared but its value is never read(a side effect of the full revert, not the pin)src/return-type-precision.test.ts7fa9e129ca…Restoration after every row: blob hash matched pristine and
git diff HEADwas empty. Final state:git status --porcelain0 lines, HEAD stillcd9f93413e,git hash-object packages/client/src/index.ts=7fa9e129ca….Two readings worth stating plainly:
vitestis green in every row, and that is correct, not a hole. The pin is type-level on purpose:expectTypeOfand@ts-expect-errorare erased at runtime, so the value the stubbed transport returns is identical either way. The teeth are incheck:test-typecheck(which compiles the test layer underpackages/client/tsconfig.test.json), and the pin file's own docblock says so. A runtime-only reading of this pin would have reported a false green three times.Serial scan —
packages/client/src/index.tsis clearRe-scanned at claim time as the card demands, ⛔ not taken from any comment. All 17 open PRs enumerated and paged to the last page (
list_pull_requests state=open perPage=100; page 1 returned 17, page 2 returned empty — the paging failure that cost this card two days is exactly what that second call exists to rule out), then each scanned per ref:git fetch --depth=80 origin refs/pull/N/head,git merge-base FETCH_HEAD origin/main,git diff --name-only <merge-base> FETCH_HEAD.Result: every one of the 17 refs fetched and resolved a real merge base within depth; none holds
packages/client/src/index.ts. The only open PR touchingpackages/client/at all is the changeset-release bot (#15334), and onlyCHANGELOG.md/package.json— same-package, which is exempt in this lane; same-file is the hard-serial rule and nothing holds it. The four cards historically queued on this file (#14313, #14314, #15451, #14879) have no open PR: #14879's carrier PR #16676 merged at 19:32Z, and none of the 17 open heads is any of the others.Method validated rather than assumed: the git per-ref file list for the largest open PR (#16628, 37 files) is byte-identical to GitHub's own
get_fileslist for it.Scope
⛔ Out of scope, preserved from the card: the dead
environmentIdwire-spread on the same door belongs to #12005, not this card. Untouched here.No wire byte moves.
HistoryMetaItemResponseSchemais a describe-only transcription of whathistoryMetaItemalready returned, and the SDK's runtime path is unchanged — only what the compiler knows about it.Generated by Claude Code
Generated by Claude Code