Sitelet https://github.com/objectstack-ai/objectstack/pull/12378
Skip to content

Retire compound-name metadata addressing — un-mount the three :section arities and unify SDK URL spelling (#12176 stage 3) - #12378

Merged
os-trump merged 3 commits into
mainfrom
claude/issue-12195-retire-compound-arities
Aug 25, 2026
Merged

Retire compound-name metadata addressing — un-mount the three :section arities and unify SDK URL spelling (#12176 stage 3)#12378
os-trump merged 3 commits into
mainfrom
claude/issue-12195-retire-compound-arities

Conversation

@os-trump

Copy link
Copy Markdown
Collaborator

Fixes #12195

Part of #12176 · Part of #12106 · Part of #11932

Stage 3 of the maintainer-ruled retirement of compound (slash-bearing) metadata item names. Stage 1 (#12194, merged as #12244) declared the item-name grammar and refuses every slash-bearing name at the publish door — so the routes removed here addressed only names that can no longer be created. This is the dead-addressing removal that ruling planned.

What is removed

Three mounted arities in packages/rest, with their route-ledger rows and the ordering pins that existed only because of them:

retired replacement
GET /api/v1/meta/:type/:section/:name GET /api/v1/meta/:type/:name
PUT /api/v1/meta/:type/:section/:name PUT /api/v1/meta/:type/:name
GET /api/v1/meta/:type/:section/:name/published GET /api/v1/meta/:type/:name/published

Each folded :section and :name back into one slash-bearing key the protocol layer treated as a single opaque string. The matching folds in packages/runtime/src/domains/meta.ts go with them: the item branch requires exactly two segments and the /published branch exactly three, instead of re-joining every trailing segment.

packages/client unifies on encodeURIComponent for every /meta item address, closing the split this card was given as 6-unencoded/4-encoded.

Measurements that corrected the brief

The census's SDK split re-derived on this ref is 10 unencoded / 4 encoded, not 6/4. Anchored counts on packages/client/src/index.ts: 8 unscoped ${route}/${type}/${name} sites plus 2 scoped /meta/${type}/${name} sites were unencoded; deleteItem and getHistory were already encoded on both clients. All 14 now encode.

%2F reaches the single-segment door, and Hono decodes it. Measured on the real router rather than assumed:

/meta/lead/views/all_leads    -> COMPOUND route, section='views',        name='all_leads'
/meta/lead/views%2Fall_leads  -> SINGLE   route,                          name='views/all_leads'

This is what makes the removal capability-preserving, and it is load-bearing for a constraint the brief did not carry: #12194's landed acceptance criteria state that "reads and deleteMetaItem still answer for pre-grammar residue rows, so any stored junk name remains listable and clearable." Un-mounting the compound arities does not break that — a residue row is addressed percent-encoded through the surviving door, which is exactly what the SDK now sends. meta-published-overlay.test.ts pins it end-to-end on a stored views/all_leads row.

The dispatcher needed a decode to keep that true, and this is the one addition beyond un-mounting. packages/runtime splits the RAW path (path.split('/')) and nothing decodes its parameters, unlike the Hono routes — measured: c.req.path returns /meta/lead/views%2Fall_leads verbatim while c.req.param('name') on the same request yields views/all_leads. Un-folding without decoding would have left a residue row addressable through packages/rest and not through the dispatcher, breaking #12194's criterion on one transport. decodeMetaNameSegment closes that; the sibling domains/packages.ts already decodes its own id segments the same way, so the meta domain was the outlier.

The >= fold was also answering paths that were never compound names. /metadata/object/foo/references folded to name foo/references and was answered as a metadata READ of a name nothing stores, rather than as the not-found it is. Requiring exactly two segments ends that too — visible in meta-state-plural-tolerance.test.ts, where /meta/objectss/task/state/status moves from RESOURCE_NOT_FOUND (a statement about an item) to ROUTE_NOT_FOUND (a statement about the route), which is the accurate one.

The meta domain's tail { handled: false } was unreachable dead code until this card — 0, 1, and (through the fold) every 2+ segment path were all covered. Un-folding makes it reachable, so it becomes a located routeNotFound instead of letting the adapter answer an anonymous 404 'Not Found' on the very shape this retirement newly produces. Same form domains/ai.ts and domains/share-links.ts already use.

How the pins were reworked

Every pin that measured a retired arity is reworked, never silenced. 14 rest files + 6 runtime files + 2 client files.

Inverted to absence where the file's subject was a shadowing or divergence hazard — because a re-mount is how the hazard returns, and an order pin phrased against a retired route would go green while the retirement is undone. Each inversion keeps the reason the original pin existed: a re-mounted compound door arrives ungated (#6603/#7019), org-blind (#8805), and reading neither ?force (#11095) nor ?mode (#11712) until someone re-derives that work.

Re-aimed at the surviving door where the coverage was never about the spelling: the full ?mode and ?force contracts, the #6877 repeated-parameter guard, the #7019 capability gate and the #8842 falsy-body hole.

New post-removal pins, ADR-0112 code AND status on every one:

  • a compound path answers 404 ROUTE_NOT_FOUND at the dispatcher;
  • the ENCODED spelling still reaches the grammar's 400 INVALID_REQUEST, so "the route is gone" and "the name is illegal" stay distinguishable — they are different facts and they answer differently;
  • DELETE on a compound path answers 404, not the 405 its two-segment address gets;
  • client URL pins invert to %2F, each beside a control proving a grammar-legal name (crm_lead, crm_lead.pipeline) is byte-identical on the wire.

Verification

All runs at c6c3d98d, heavy runs through scripts/pm/os-verify-lock.sh; exit codes captured before any pipe.

Suitesos-verify-lock: VERDICT command-exit 0:

packages/rest     Test Files  147 passed (147)    Tests  2328 passed (2328)
packages/runtime  Test Files  191 passed (191)    Tests  2815 passed (2815)
packages/client   Test Files   25 passed  (25)    Tests   346 passed  (346)

typecheck green on all three (VERDICT command-exit 0 · held the lock 32s).

Red-before, direction predicted in writing. The removal was made first and the pins run against it: 14 rest files / 80 tests and 6 runtime files / 10 tests went red in exactly the predicted direction — every failure a pin naming a retired arity — then each was reworked to the statement that is true after the removal. The two client URL pins failed with precisely the predicted diff (views/all_leadsviews%2Fall_leads).

Gate family derived with node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack (self-derived change set, 27 paths). Every runnable family run, each quoting its own verdict line:

gate verdict
check-adr-0087-registration ✓ 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition.
check-changeset-no-major ✓ This diff introduces no 'major' bump.
check-empty-changeset ✓ No empty-frontmatter changeset introduced by this diff (1 declaring changeset(s) added).
check:changeset-gate-self-tests all three self-tests (118 + 212 + 116 assertions)
check:objectui-changeset all 15 cases
check:authz-resolver ✓ single shared authorization resolver intact; both entry points delegate.
check:cross-package-test-inputs OK: 18 package(s) read outside themselves, all declared, and turbo.json hashes every declared glob.
check:test-source-alias OK — 72 packages with tests scanned; 61 registered…
check:type-source-resolution OK — 93 tsc program(s) across 77 packages scanned…
check:published-files ✓ 69 publishable package(s)… declare a 'files' whitelist…
check:slot-lookup ✓ slot-lookup ratchet holds: 107 unswept site(s) in 25 file(s), none new…
check:page-declaration-shape OK — 34 page entries across 2190 sources…
check:nul-bytes OK (scanned 6844 text file(s)… no raw ASCII control bytes).
release-rehearsal-clone --self-test all cases
check:skill-examples ✅ 256 prose examples type-check across 3 surface(s)

check:skill-examples first REFUSED rather than failing — packages/client-react/dist held no declarations, so it would have reached a verdict without reading the artifact under test. Declared here rather than counted: the packages were built and it was re-run, and only the second run is reported.

Scope

packages/spec is untouched — D1 landed the grammar there and this card treats it as read-only. No content/docs/releases/ edit. The changeset is minor on all three packages with the BREAKING route table and the FROM → TO spelling, per the launch-window convention.

⚠️ Draft, carrying needs:contract-review — public routes are removed. The contract-review chain reviews and lands this; nothing here flips it ready or arms auto-merge.

Generated by Claude Code


Generated by Claude Code

claude added 2 commits August 25, 2026 20:44
Stage 3 of the #12176 maintainer-ruled retirement of slash-bearing metadata
item names. Stage 1 (#12194) declared the item-name grammar and refuses every
slash-bearing name at the publish door, so the compound arities addressed only
names that can no longer be created — this removes them.

packages/rest:
  - Un-mount the three compound arities: GET and PUT /meta/:type/:section/:name
    and GET /meta/:type/:section/:name/published. Drop their three route-ledger
    rows and the ordering pins that existed only because the three-segment
    compound route was a CATCH-ALL shadowing every literal sibling.

packages/runtime:
  - Un-fold the dispatcher: the /published branch requires exactly three
    segments and the item branch exactly two, instead of re-joining every
    trailing segment into one slash-bearing key.
  - Decode the :name segment. This dispatcher splits the RAW path and nothing
    decodes for it, unlike the Hono routes in packages/rest — measured, not
    assumed. Without it a pre-grammar residue row would be addressable through
    REST and not through the dispatcher, breaking #12194's landed acceptance
    criterion that stored junk names stay listable and clearable. The sibling
    domains/packages.ts already decodes its own id segments this way.

packages/client:
  - One URL spelling: encodeURIComponent on every /meta item address, closing
    the 10-unencoded / 4-encoded split. Encoding a name that satisfies #12194's
    grammar is a no-op, so this is byte-identical for every writable name; a
    residue name now reaches the single-segment door as %2F rather than forking
    the request onto a second door.
  - Correct the docblocks that promised unencoded compound pass-through, and
    the #11712 mode carve-out, which is closed at the source rather than here.

Capability is re-expressed, not removed: %2F matches the single-segment
pattern and Hono decodes the parameter back to the stored spelling, so residue
rows still read, write and delete through the surviving doors.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9StxQgG2DPA26XzZZqnJB
…oval

Every pin that measured the retired compound-name arities is REWORKED, never
silenced. Two shapes, chosen per file by what is actually still true:

INVERTED — files whose subject was a shadowing or divergence hazard now pin
the ABSENCE of the arity, because a re-mount is how the hazard returns and an
order pin phrased against a retired route would go green while the retirement
is undone:
  - meta-route-registration-order: the three-segment catch-all ordering pins
    become "no compound `:section` arity is mounted", plus a companion pin that
    the literals it used to shadow are all still mounted.
  - meta-item-layered-route, meta-plural-i18n, rest-server-meta-write-org-scope,
    meta-compound-save-{mode,force}-parity, meta-compound-save-and-reset-
    capability-gate, meta-501-envelope: same inversion, each keeping the reason
    the original pin existed (a re-mounted door arrives ungated, org-blind, and
    reading neither `?force` nor `?mode` until someone re-derives that work).

RE-AIMED at the surviving door — coverage that was never about the spelling:
  - the full #11712 `?mode` and #11095 `?force` contracts, the #6877 repeated-
    parameter guard, the #7019 capability gate and the #8842 falsy-body hole
    now run against `/meta/:type/:name`.
  - meta-published-overlay's residue-row read drives the single-segment
    `/published` route with the decoded name — the pin that the retirement
    costs no capability.
  - client URL pins invert to `%2F`, each with a control proving a grammar-legal
    name is byte-identical on the wire.

NEW post-removal pins, ADR-0112 code AND status on every one:
  - the compound path answers `404 ROUTE_NOT_FOUND` at the dispatcher;
  - the ENCODED spelling still reaches the item-name grammar's `400
    INVALID_REQUEST`, so "the route is gone" and "the name is illegal" stay
    distinguishable;
  - `DELETE` on a compound path answers 404, not the 405 its two-segment
    address gets — the two refusals are different facts.

Also: the meta domain's tail `{ handled: false }` was unreachable dead code
until this card (0, 1 and — through the fold — every 2+ segment path were all
covered). Un-folding makes it reachable, so it becomes a LOCATED
`routeNotFound` rather than letting the adapter answer an anonymous 404 on the
very shape this retirement newly produces. Same form `domains/ai.ts` and
`domains/share-links.ts` already use.

Changeset: minor on rest/runtime/client with the BREAKING route table and the
FROM → TO spelling, per the launch-window convention.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9StxQgG2DPA26XzZZqnJB
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/client, @objectstack/dogfood, @objectstack/rest, @objectstack/runtime, touching 29 documentable anchor(s).

26 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json c4a27b7b0e25ead095f7385782e0aab14255f9ee.

5 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 2 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 47 of 219 client-bound route-ledger rows — the other 172 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 172: 14 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 56 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 102 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 36 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 c4a27b7b0e25ead095f7385782e0aab14255f9eepackageMentionDocs.

Which tree this was computed on

This run read content/docs from b98105ea18cfec51ffa26210830e88dcfefe885b — the merge of head 2f568fd0e9858c54da995a141305a0524d3cdf63 into base c4a27b7b0e25ead095f7385782e0aab14255f9ee, 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 b98105ea18cfec51ffa26210830e88dcfefe885b && git checkout b98105ea18cfec51ffa26210830e88dcfefe885b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin c4a27b7b0e25ead095f7385782e0aab14255f9ee 2f568fd0e9858c54da995a141305a0524d3cdf63 && git checkout -B drift-repro c4a27b7b0e25ead095f7385782e0aab14255f9ee && git merge --no-ff 2f568fd0e9858c54da995a141305a0524d3cdf63

node scripts/docs-audit/affected-docs.mjs --json c4a27b7b0e25ead095f7385782e0aab14255f9ee

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

Copy link
Copy Markdown
Collaborator Author

Contract-review verdict: PASS (#12195, D3 of the #12176 program)

Reviewed by the contract-review chain at CONTRACT_REVIEW_TIER. Tier fuse, machine reading this sub-round: get_session.external_metadata.last_served_model = claude-fable-5.

Reviewed: the PR body, the dev report on #12195, and the load-bearing patches read from the branch (rest-server.ts removals, domains/meta.ts un-fold + decode, the client encoding unification, the pin-rework taxonomy).

Conformance to the ruled scope (maintainer 2026-08-25 program, recorded on #12176/#12194): all three limbs delivered — the three :section arities un-mounted with their ledger rows and the ordering pins that existed only for them; the runtime folds now exact-arity; the SDK unified on encodeURIComponent (the census's 6/4 split honestly re-measured to 10/4, all 14 sites now encode); Fixes #12195 + Part of for #12176/#12106/#11932 exactly as prescribed. packages/spec untouched.

The one addition beyond removal is ruled-in by D1's own landed contract, not scope creep: #12194's acceptance criteria require residue rows to stay listable and clearable, and the measured transport asymmetry (Hono decodes route params; the dispatcher splits the raw path undecoded) means un-mounting alone would have broken that on one transport. decodeMetaNameSegment closes it, precedented by domains/packages.ts, pinned end-to-end on a stored views/all_leads row — and the malformed-escape case is handled the right way, verified in the source: a URIError falls back to the raw segment and earns an ordinary 404, never a 500 from the split.

The contract statements the new pins make are the right ones: "the route is gone" (404 ROUTE_NOT_FOUND) and "the name is illegal" (400 INVALID_REQUEST) stay distinguishable — different facts, different answers; DELETE on a compound path answers 404 where its two-segment address gets 405; every inverted absence pin keeps the reason the original existed (a re-mounted compound door returns ungated, org-blind, and parameter-deaf), so a re-mount cannot come back quietly. The >= 2 fold's silently-wrong reading of non-compound sub-resource paths (RESOURCE_NOT_FOUND for routes nothing stores) ends with the same stroke, and the newly-reachable tail becomes a located routeNotFound in the sibling domains' form rather than an anonymous adapter 404 — the flagged judgment call is endorsed.

Evidence discipline held throughout: removal-first red (80+10+4 tests, every failure naming a retired arity, the client pair failing with the exact predicted %2F diff); ablation from a fixed base sha with the mutation proven on disk in both directions and restore proven byte-identical by object hash; two refusals declared rather than counted (check:skill-examples and the pre-commit ADR-0087 read), with only the real runs reported; the ADR-0087 gate confirming the declared-breaking changeset carries its disposition; repo-wide lint honestly "not run" rather than dressed as a narrowing.

Landing completed by the chain in the same stroke per the maintainer's standing instruction (2026-08-25, verbatim: 「审核通过你应该直接负责合并吧,还要等原始的项目经理吗」): clearing needs:contract-review on this PR and #12195, flipping ready, arming the queue. On merge: #12195 closes, D4 (#12196) unblocks and gets dispatched, and the #12106/#11932 closeout re-verification runs as the PM step the card reserved.


Generated by Claude Code

@os-trump
os-trump marked this pull request as ready for review August 25, 2026 21:31
@os-trump
os-trump enabled auto-merge August 25, 2026 21:31
…ogfood compound-door rows (#12195)

The pin rework left four dead constants and two off-type boot calls in the
rest test layer — TEST_DEBT measured 161 against the frozen 155; all six are
fixed, none ledgered. The dogfood anonymous-deny table drops the retired
compound-save row (its registered-door leg asserts .not.toBe(404), which is
what a retired route answers) and gains the retired-door case: the compound
spelling must 404 for anonymous and member alike, since an auth floor only
speaks for a door that exists. The authz-conformance matrix note re-tallies
six -> five with the retirement named.

Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H9StxQgG2DPA26XzZZqnJB
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

D3: retire compound-name addressing — remove the mounted compound arities, unify SDK URL spelling, close out the five broken methods (#12176 stage 3)

2 participants