Uh oh!
There was an error while loading. Please reload this page.
fix(rest,client): the /meta FSM state route is singular — retire the plural registration (#10077) - #10177
Conversation
…plural registration (#10077) Step 2 of the #9180 ruling (the /meta type segment is ALWAYS singular), maintainer re-weigh 2026-08-17, verbatim: 「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」 - client.meta.getLegalNextStates now calls GET /api/v1/meta/object/:name/state/:field - the plural /meta/objects/:name/state/:field registration is deleted - both route ledgers follow the mount and the SDK - the registration-order pin keeps its singular arm: the collision with the compound /published route outlives the retired spelling META_URL_TO_SINGULAR is untouched — the retired route matched a literal segment and never consulted the fold, so the boundary accept set is unchanged (2026-08-17 re-weigh, item 3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019bmVFqoQPq63zhKrxdYG1r
📓 Docs Drift CheckThis PR changes 3 package(s): 3 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 — 35 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 f403ac235122eb047a351e4310c04ccf83fb8118 && git checkout f403ac235122eb047a351e4310c04ccf83fb8118
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1 2569a99e9d879fd73f0f8ae9c12acc705aeb0ee4 && git checkout -B drift-repro 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1 && git merge --no-ff 2569a99e9d879fd73f0f8ae9c12acc705aeb0ee4
node scripts/docs-audit/affected-docs.mjs --json 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1
|
qq9340100
commented
Aug 20, 2026
Landing note (skills seat, session Generated by Claude Code |
Fixes#10077. Part of the #9180 ruling (step 2 of three).
Maintainer re-weigh, 2026-08-17 (#9180 comment 5311434183), verbatim and untranslated:
What changed
packages/client/src/index.tsmeta.getLegalNextStatesrequests/meta/object/:name/state/:field— one route string, nothing else in the methodpackages/rest/src/rest-server.ts['objects', 'object']registration loop becomes a single singular mountpackages/rest/src/rest-route-ledger.tsdisposition: 'sdk'+client: 'meta.getLegalNextStates'packages/runtime/src/route-ledger.tspackages/rest/src/meta-route-registration-order.test.tsThe scope fence, measured rather than assumed
The card forbids widening the refusal set:
META_URL_TO_SINGULARstays, and if deleting theplural registration would newly refuse a spelling the fold accepts today, the instruction was
to stop and report the fork. It does not, and here is the measurement that settles it:
rest-server.tsregistered thetwo spellings from a hard-coded array, and
runtime/src/domains/meta.tsmatchesparts[0] === 'objects' || parts[0] === 'object'. Neither call site consultscanonicalMetaUrlType/META_URL_TO_SINGULAR; the fold applies to the:typeparam of/meta/:type/..., and this route has no:typeparam at all.declared route — which is how [Ruling] The
/metatype segment is ALWAYS singular — retire every plural spelling, no exception, in three steps #9180's own sweep classified it: "the/meta/objects/:name/state/:fieldhits are a declared route, not a tolerance" — andwithdrawing it is the ruled content of step 2, priced by the ruling as its one piece of
external-break exposure.
mounts no
/metaroute, and itssetFallbackHandlerseam returns immediately for any pathoutside the
/apps/**carve-out (isAppEndpointPath), so the transport's own 404 stands.Two consequences worth naming for review rather than leaving to be discovered:
GET /api/v1/meta/objects/:name/state/:fieldnow answers the transport 404 (pinned in dogfoodagainst an unmounted control, so a handler 404 would fail the pin).
published:/meta/objects/x/state/publishednow matches/meta/:type/:section/:name/published. That wasthe entire reason the registration-order pin existed.
Why the order pin was not deleted outright
The card describes the file as "the order pin that exists only to manage the collision". Measured
against the tree, that is a lead rather than a spec, in two ways:
/:type/:section/:name/publishedroute, and/meta/object/x/state/publishedcollides exactly as/meta/objects/x/state/publisheddid. Deleting the pin would leave the surviving routeunguarded against the Three ledgered /meta routes are never mounted and die in the
/meta/:typecatch-all — the route audit can't see this class because it treats the ledger as ground truth for what's mounted #7526 defect (mounted, shadowed, unreachable) that the pin exists to catch.GET /meta/:typecatch-all, and every three-segment literal ahead ofGET /meta/:type/:section/:name— which have nothing to do with this spelling.So what was deleted is the plural's own line in the pin, which is what the collision required and
no more.
Pin sweep
The ruling flips public semantics, so the pins move with it and assert the new semantics rather
than being dropped:
packages/rest/src/meta-route-registration-order.test.ts— singular arm kept; new pin:the plural is absent from the mount table.
packages/client/src/meta-automation-descriptors.test.ts— the SDK's on-the-wire URL pin.packages/qa/dogfood/test/meta-published-and-state-routes.dogfood.test.ts— the behaviouralcases move to the singular; the old "accepts the singular spelling too" case becomes the
retirement pin (plural = transport 404, byte-identical to an unmounted path, with the singular
200 as its control).
packages/qa/dogfood/test/route-ledger-live-mount-parity.dogfood.test.ts— the live routerresolves the singular pattern, and resolves the plural to nothing.
docs/qa/platform-checklist/areas/api-backend.json— the checklist item fires an executableroute, so a stale plural there is a false FAIL for the next QA run, not prose.
Kept verbatim, deliberately: the dogfood control
/meta/objects/showcase_task/state/status/definitely/not/mounted(a genuinely illegal shape whoserefusal is the point), and
packages/rest/src/meta-object-fls.test.ts'sGET /meta/objects/:name?state=draftcase — that one is the fold's plural tolerance on a:typeparam, which this PR must not touch.Not in this PR
META_URL_TO_SINGULAR(2026-08-17 re-weigh, item 3 — deferred with noscheduled window).
runtime/src/domains/meta.ts, which still matches both literals.Narrowing it would be a second surface's new refusal; the ledger row carries a note saying so.
content/docs/protocol/objectql/state-machine.mdx:126)— the card routes those to the docs card.
packages/client/src/index.tsitem-route typing (getItem/saveItem/deleteItem/getHistory) — hard-serialised with the step-3 card and untouched here.Review posture
This PR carries
needs:contract-reviewand is deliberately not enqueued: it was dispatchedbelow the contract-review tier under the quota-exhaustion exemption, so it stays a draft with no
auto-merge until a different seat at that tier clears the label.
Verification
All of the below ran against
2569a99e9, this branch's head and final commit, in a worktree whosedependency closure was built first.
pnpm --filter @objectstack/rest --filter @objectstack/client --filter @objectstack/runtime test— rest
Test Files 131 passed (131)/Tests 2135 passed (2135); runtime177 passed (177)/2649 passed (2649); client23 passed (23)/314 passed (314).… typecheck— all three packages,tsc --noEmitclean (the run echoes each package's script,so this is not a zero-match green).
check:authz-resolver·check:cross-package-test-inputs(andscripts/check-cross-package-test-inputs.mjs) ·check:dispatcher-error-vocabulary·check:route-envelope·check:slot-lookup·check:nul-bytes·scripts/docs-audit/check-affected-docs.mjs·check:engine-double-contract·check:where-matcher(264 matcher(s) discovered … 264 answer the combinator battery correctly or refuse it loudly) ·check:query-options-erasure(ratchet holds … none new) ·check:type-check-coverage·check:test-source-alias·check:type-source-resolution·check:changeset-gate-self-tests·check:objectui-changeset·check-adr-0087-registration.mjs(this PR adds no declared-breaking changeset) ·check-changeset-no-major.mjs·check-empty-changeset.mjs· and the spec-liveness familycheck:empty-state/check:liveness/check:strictness-ledger/check:variant-docs.node scripts/pm/dispatch-gates.mjswas re-derived from the actual diff (10 paths). It namedfamilies the dispatch list did not: the changeset family,
check:objectui-changeset,check:test-source-alias,check:type-source-resolution, and the four spec-liveness checks —all run above.
Declared narrowings (CI runs the full farm regardless):
verify lock returned
queue-timeouttwice at 9m00s apiece. What their pins assert is coveredfrom the other side by
meta-route-registration-order.test.ts, which ran green and pins theplural's absence from the live mount table.
pnpm check:type-check-debt --re-measurerefused rather than failed:24 workspace dependenc(ies) of the ledgered packages have no built type entry point on disk. That is NOTMEASURED, not a pass — measuring it needs the same full-workspace build the lock contention
blocked.
pnpm lintwas narrowed to ESLint over the changed.tsfiles (exit 0) for the samereason.
Generated by Claude Code
Generated by Claude Code