fix(client): the scoped SDK reads metadata.prefix off the advertised routes instead of restating /meta - #17122
Conversation
…16675) The card's mandated first step: reuse #14879's data-prefix fixture with metadata.prefix in place of crud.dataPrefix, and record the reading before touching the implementation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
…es (#16675) Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015QE8qk46e5CHJxyQEUjbf8
📓 Docs Drift CheckThis PR changes 1 package(s): 14 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 14 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 5b61e138d8bee6b679dab1ad132ecce6878c950d && git checkout 5b61e138d8bee6b679dab1ad132ecce6878c950d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8a70e1bf64e2a63a9a0c597e59524272e0903c8f 772ef3519ccb56b15b5db9b39e0b40d9ce54c958 && git checkout -B drift-repro 8a70e1bf64e2a63a9a0c597e59524272e0903c8f && git merge --no-ff 772ef3519ccb56b15b5db9b39e0b40d9ce54c958
node scripts/docs-audit/affected-docs.mjs --json 8a70e1bf64e2a63a9a0c597e59524272e0903c8f
|
Fixes #16675
Clause-②: no
ScopedEnvironmentClientrestated/metaas a literal in all six of its metadata methods, so on a deployment that movedmetadata.prefixthe scoped half of one SDK called paths the server does not mount while the unscoped half of the same SDK called the right ones. The six now read the advertised value, exactly as their unscoped twins already do.1. The probe first, not the edit — and it came back RED
Triage made this a hard condition: 「第一步是跑那条探针,⛔ 不是直接改代码 … 读数无论红绿都贴进 PR:绿了说明本卡的前提不成立,那也是有价值的结论」.
The probe is #14879's data-key fixture with the single substitution the card names —
crud.dataPrefixbecomesmetadata.prefix— landed aspackages/client/src/client.metadata-prefix.test.tsand committed BEFORE the implementation (692f7fb). Run against the untouched implementation at that commit, on a live server booted atmetadata: { prefix: '/metadata' }:The card's premise HOLDS. The two supporting legs passed in the same run and are what make that reading mean something: the raw-fetch leg proves
/api/v1/environments/proj-alpha/metareally answers 404 on that deployment while the mounted path answers 200, and the discovery leg proves the server advertisesroutes.metadata = /api/v1/metadata. Per triage's stated escalation trigger, this reading is the one that lifts the card frompriority:p3— recorded here, not acted on: priority is the triage seat's to set.After the fix, the same suite is 6/6 green.
2. Six call sites, not five — re-derived at this branch's own head, by symbol
Counted by
this.url(inside theScopedEnvironmentClientregion (located by symbol, not by line number), which is the method triage prescribed after its own first attempt answered 1:this.url(total/metathis.url('/meta/batch,/packages×2,/automation×6)My count agrees with triage's 6 and not with the card's 5, and the naive single-quote query reproduces triage's wrong answer of 1 — five of the six are template literals. The replacement asserted
total == 6in the edit script and refused to write otherwise. The nine survivingthis.url(sites are/batch,/packagesand/automation, which are mounted offbasePathdirectly and are not this card's key.3. The negative control — the acceptance criterion, and proof it can distinguish
Triage: 「默认
metadata.prefix = '/meta'的部署上,这 6 个方法的请求 URL 在改动前后逐字节相同。一个"总是从 discovery 拼"的实现会让第 1 条绿,同时让所有默认部署多依赖一次 discovery 往返。」Two pinned legs, both full-string equality on all six URLs (never
toContain— the realistic non-default value/metadataCONTAINS the conventional/meta, so a substring probe over this pair of values answers noise):/metaURLs byte for byte;That second leg is the instrument for the hazard triage named, and it is the only one that can be: on a connected default deployment an always-from-discovery implementation still derives
/meta, so the extra dependency is invisible there. It is visible only where the document is absent.Both legs were then proven capable of failing, from the committed fix, each mutation proven on disk (anchor occurrence counts before/after plus a
git hash-objectblob that moved off the HEAD blob) and each restored withgit checkout HEAD -- ...proven by an emptygit diff HEAD, under anEXIT INT TERMtrap with absolute paths:692f7fb)/meta''instead of the conventionmetaUrltakes_dataPrefix()Resolution note for the ablations: the suite imports
./indexrelatively, so the mutation reaches the subject from source with nodist/in the path — there is no build state that could have made a mutated run falsely green.4. The fix
_metaPrefix()is the exact sibling of the_dataPrefix()that #14879 landed in this same file, fallback discipline included — 「通告文档不能确定前缀时退回约定值,⛔ 不得抛错」:routes.metadataalready ends with the conventional/meta, that IS the prefix — taken first, which is both what makes the change incapable of regressing a working deployment and what keeps a default deployment free of any new dependency;routes.datasupplies the second equation over the samerealBase, and the common run cut back to its last/lands on the base boundary;/meta. An unconnected client never reaches a rule at all.The six methods build through a new
private metaUrl(), the sibling of the existingdataUrl(): base from_apiBase(), prefix from_metaPrefix(), environment segment between them.The unscoped half is untouched — 「未加作用域的那一半 ⛔ 不要动」. Verified:
getRoute('metadata')is read at the same sites as before and no unscoped method appears in the diff.5. Clause-② re-derived from the DELIVERED diff
Delivered paths:
packages/client/src/index.ts,packages/client/src/client.metadata-prefix.test.ts,.changeset/scoped-sdk-honours-metadata-prefix.md. Nothing underpackages/spec/src/**, no*.zod.ts, no error code, no authorable key, and no accept-set of any route moves — this is client-side URL construction only, and it is a pull-back toMetadataEndpointsConfigSchema.prefix, which already declares.default('/meta'), and toroutes.metadata, which discovery already advertises. SKILL.md:517: 条款②只指已发布契约面,拉回已声明契约不触它.⇒
Clause-②: no, and stated on the visible facts rather than on a summary: the emittedpackages/client/dist/index.d.tsdoes gain one line —_metaPrefix(): string;, immediately beside the_dataPrefix(): string;that already ships — plusprivate metaUrl;, which carries no signature. No new top-level export, no signature moves, no wire payload member.6. Verification
Run at
772ef35unless noted.client.metadata-prefix(the new pin),client.data-prefix([finding] The client SDK hard-codes/data/${object}…whilecrud.dataPrefixis live and discovery advertisesroutes.data = base + dataPrefix— a non-default prefix makes the SDK disagree with the mounts #14879's, to prove that key did not regress),client.environment-scoping,client-url-conformance: 4 files / 21 tests passed.packages/clientfull suite —pnpm --filter @objectstack/client test: 37 files / 468 tests passed.packages/clienttypecheck —tsc --noEmitpluscheck:test-typecheck: green, and the latter reports the test layer compiles undertsconfig.test.jsonat 0 errors, so the new fixture is genuinely type-checked rather than silently excluded.pnpm --filter '@objectstack/client^...' build: exit 0.scripts/pm/dispatch-gates.mjs(never a hand-written list), then reconciled with--ran: 58 derived, 58 run, 0 NOT-MEASURED, 0 UNRUN. Three needed a second look and none was a finding:check:skill-examplesfirst returned a PREREQUISITE refusal on an unbuiltclient-react/distand is green after building it;check:type-check-debtfirst OOM'd at exit 3 ("nothing was measured") and is green re-run at a larger heap, reporting every ledger entry at or under its recorded number.eslint . --no-inline-config, not a narrowing: 6419 files, 0 errors, 0 warnings, counted from--format json.content/docscompared by tree object:1a7f20dat the branch point and1a7f20dat head, identical, so nothing under it moved andcontent/docs/releases/was never touched.scripts/docs-audit/affected-docs.mjsnames 18 pages from 7 anchors — not a structural zero — and none is falsified: every one documents the DEFAULT deployment's literals, which this change keeps byte-identical, and none of them claims the prefix is fixed.One gate is NOT MEASURED and declared rather than counted as a pass:
check:dual-build-cjs-loadsreturns its own exit 3 PREREQUISITE refusal here because 33 packages have nodist/in this worktree, and it needs a repo-wide build. Its inputs areexportsmaps and emitted CJS bytes, neither of which this diff touches. CI runs it on a fresh full build.7. Holder re-measurement — done at claim time, not inherited
packages/client/src/index.tsis the #12104 family's hard-serial hot file, so the dispatch's reading of it was re-measured rather than carried forward. Built from the open PR list (18 open PRs read from the REST pulls endpoint, never from remote branches, where merged-but-undeleted refs read as phantom holders), each PR's head diffed against its own merge-base:organizations.getActiveMemberaddresses the organisation the caller NAMES, not whichever one the session has active #16761 (the dispatch's named prior holder, merged) and this branch itself, once it carried the edit. A scan whose controls do not fire is not a reading.验收备注
_dataPrefix()'s shape including its fallback discipline (§4), the negative control (§3), the unscoped half untouched (§4), and dedup not redone (triage settled it; Four SDK metadata methods build compound-name URLs against routes mounted in one arity only — measured 404, and getHistory/deleteItem encode while getAudit/getReferences/diffItem/rollbackItem do not #12106 is a different failure on the same methods and is not addressed here)._dataPrefix()and_metaPrefix()are one algorithm with the roles ofroutes.dataandroutes.metadataswapped, and a future correction to the boundary arithmetic could land in one and not the other. Not extracted here on purpose: the card, triage and the dispatch all describe a second derivation of the same shape rather than a refactor, and rewriting a p2 fix that landed hours ago inside a p3 card trades a real risk for a stylistic gain. Successor: whichever PR next touches either derivation./api/v1/meta,/api/v1/environments/:environmentId/meta) and none mentionsmetadata.prefixorcrud.dataPrefix. It is a gap, not a falsification, it predates this change, and it is equally true of the data key, so it is not this PR's to close. Successor: whoever documents theRestServerConfigendpoint-prefix keys.Generated by Claude Code