Skip to content

fix(rest,client): the /meta FSM state route is singular — retire the plural registration (#10077) - #10177

Merged
qq9340100 merged 1 commit into
mainfrom
claude/issue-10077-meta-state-route-singular
Aug 20, 2026
Merged

fix(rest,client): the /meta FSM state route is singular — retire the plural registration (#10077)#10177
qq9340100 merged 1 commit into
mainfrom
claude/issue-10077-meta-state-route-singular

Conversation

@os-elon

Copy link
Copy Markdown
Collaborator

Fixes#10077. Part of the #9180 ruling (step 2 of three).

Maintainer re-weigh, 2026-08-17 (#9180 comment 5311434183), verbatim and untranslated:

「② 照原样做;只需要修正 objectstack objectui cloud 中错误的写法。」

What changed

sitechange
packages/client/src/index.tsmeta.getLegalNextStates requests /meta/object/:name/state/:field — one route string, nothing else in the method
packages/rest/src/rest-server.tsthe ['objects', 'object'] registration loop becomes a single singular mount
packages/rest/src/rest-route-ledger.tsthe plural row is deleted; the singular row inherits disposition: 'sdk' + client: 'meta.getLegalNextStates'
packages/runtime/src/route-ledger.tsthe dispatcher-ledger mirror row follows the client to the singular spelling
packages/rest/src/meta-route-registration-order.test.tsthe plural's arm of the collision pin is deleted; a new pin asserts the plural is no longer registered

The scope fence, measured rather than assumed

The card forbids widening the refusal set: META_URL_TO_SINGULAR stays, and if deleting the
plural 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:

  • The plural was reachable through a literal path segment — rest-server.ts registered the
    two spellings from a hard-coded array, and runtime/src/domains/meta.ts matches
    parts[0] === 'objects' || parts[0] === 'object'. Neither call site consults
    canonicalMetaUrlType / META_URL_TO_SINGULAR; the fold applies to the :typeparam of
    /meta/:type/..., and this route has no :type param at all.
  • So the fold's accept set is byte-identical before and after this PR. What is withdrawn is a
    declared route — which is how [Ruling] The /meta type segment is ALWAYS singular — retire every plural spelling, no exception, in three steps #9180's own sweep classified it: "the
    /meta/objects/:name/state/:field hits are a declared route, not a tolerance"
    — and
    withdrawing it is the ruled content of step 2, priced by the ruling as its one piece of
    external-break exposure.
  • Nothing else picks the retired spelling back up on a REST-fronted boot: the dispatcher plugin
    mounts no /meta route, and its setFallbackHandler seam returns immediately for any path
    outside the /apps/** carve-out (isAppEndpointPath), so the transport's own 404 stands.

Two consequences worth naming for review rather than leaving to be discovered:

  1. GET /api/v1/meta/objects/:name/state/:field now answers the transport 404 (pinned in dogfood
    against an unmounted control, so a handler 404 would fail the pin).
  2. The one shape that changes hands instead of 404ing is a field literally named published:
    /meta/objects/x/state/published now matches /meta/:type/:section/:name/published. That was
    the 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:

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 behavioural
    cases 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 router
    resolves the singular pattern, and resolves the plural to nothing.
  • docs/qa/platform-checklist/areas/api-backend.json — the checklist item fires an executable
    route, 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 whose
refusal is the point), and packages/rest/src/meta-object-fls.test.ts's
GET /meta/objects/:name?state=draft case — that one is the fold's plural tolerance on a
:type param, which this PR must not touch.

Not in this PR

  • The boundary fold META_URL_TO_SINGULAR (2026-08-17 re-weigh, item 3 — deferred with no
    scheduled window).
  • The legacy dispatcher branch in 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.
  • Docs prose naming the plural state route (content/docs/protocol/objectql/state-machine.mdx:126)
    — the card routes those to the docs card.
  • packages/client/src/index.ts item-route typing (getItem / saveItem / deleteItem /
    getHistory) — hard-serialised with the step-3 card and untouched here.

Review posture

This PR carries needs:contract-review and is deliberately not enqueued: it was dispatched
below 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 whose
dependency 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); runtime 177 passed (177) /
    2649 passed (2649); client 23 passed (23) / 314 passed (314).
  • … typecheck — all three packages, tsc --noEmit clean (the run echoes each package's script,
    so this is not a zero-match green).
  • Gates, each exiting 0: check:authz-resolver · check:cross-package-test-inputs (and
    scripts/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 family
    check:empty-state / check:liveness / check:strictness-ledger / check:variant-docs.
  • node scripts/pm/dispatch-gates.mjs was re-derived from the actual diff (10 paths). It named
    families 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):

  • The two dogfood files were not executed locally. Their closure is 62 packages, and the shared
    verify lock returned queue-timeout twice at 9m00s apiece. What their pins assert is covered
    from the other side by meta-route-registration-order.test.ts, which ran green and pins the
    plural'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 NOT
    MEASURED, not a pass — measuring it needs the same full-workspace build the lock contention
    blocked.
  • Repo-wide pnpm lint was narrowed to ESLint over the changed .ts files (exit 0) for the same
    reason.

Generated by Claude Code


Generated by Claude Code

…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
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 3 package(s): @objectstack/client, @objectstack/rest, @objectstack/runtime, touching 12 documentable anchor(s).

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

  • content/docs/kernel/contracts/metadata-service.mdx(via /api/v1/meta/:type/:section/:name (route))
  • content/docs/plugins/adding-a-metadata-type.mdx(via /api/v1/meta/:type/:section/:name (route))
  • content/docs/protocol/objectql/state-machine.mdx(via /:name/state/:field (route), /api/v1/meta/:type/:section/:name (route), /api/v1/meta/objects/:name/state/:field (route), /meta/objects/:name/state/:field (route), /state/:field (route))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via meta.getLegalNextStates (literal))

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)

Coarse fallback — 35 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 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1packageMentionDocs.

Which tree this was computed on

This run read content/docs from f403ac235122eb047a351e4310c04ccf83fb8118 — the merge of head 2569a99e9d879fd73f0f8ae9c12acc705aeb0ee4 into base 19f98fa1fffbeb305bdcb6af64bc826fb25a46b1, 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 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

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

@github-actionsgithub-actionsBot added documentation Improvements or additions to documentation tests tooling labels Aug 20, 2026
@qq9340100
qq9340100 marked this pull request as ready for review August 20, 2026 14:37
@qq9340100
qq9340100 added this pull request to the merge queueAug 20, 2026
@qq9340100Claude

Copy link
Copy Markdown
Collaborator

Landing note (skills seat, session session_019T1sSZbQTnLhrK9HhNdNiB): marked ready + enqueued on the maintainer's direct instruction in this seat's chat, 2026-08-20, verbatim: 「10177 也帮我合并入队」. Contract review (clause-② compensating review, PASS) is recorded on #10077.


Generated by Claude Code

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/mteststooling

Projects

None yet

3 participants

@os-elon@qq9340100@claude