Skip to content

fix(rest,runtime): ?package=all names no package on every /meta door of both hosts (#22188) - #22265

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22188-package-all-fold
Oct 8, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-22188-package-all-fold

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #22188
Clause-②: yes

What this changes

?package=all is the metadata list's "show everything" scope. #22128 (PR #22185) gave the item read, the save and the publish one reading of ?package=: metaItemPackageBinding, under which all, the empty value and a non-string name no package. The other /meta doors forwarded the raw value, so all reached the store as a package id that no item is bound to. Every door that reads ?package=, on both hosts, now reads it through that one function.

  • RestServer (packages/rest/src/rest-server.ts): the layered read (serveMetaItemLayered), the list (GET /meta/:type), the book tree (GET /meta/book/:name/tree) and the diagnostics sweep (GET /meta/diagnostics, see H3).
  • The runtime dispatcher (packages/runtime/src/domains/meta.ts, the /meta domain behind the @objectstack/hono ${prefix}/* catch-all): its book tree, layered read, item branch (the read, the ?state=draft read and the PUT) and list.
  • Widened public surface (Clause-②: yes). @objectstack/rest's package entry now exports metaItemPackageBinding, and the runtime imports it beside metaSaveRequestOptions. That is the only new export. Changesets: @objectstack/rest minor with Clause-②: yes (widening), and @objectstack/runtime patch with Clause-②: no.
  • Unchanged: a real package id still scopes each read and still binds a save. The item read's cache bypass (packageScoped, H3) still reads the raw parameter.

Readings (H1 to H5, on this branch's base ffb31fca)

H1, reproduced on the real stack, each door. The pin file below was run against the base sources before the fix. Each door has its own better-sqlite3 :memory: store with the real sys_metadata* objects and a real ObjectStackProtocolImplementation. The store is seeded through the protocol: case_grid in com.probe.pkg, other_grid in com.other.pkg, the env-local local_grid, and the book probe_book with the page probe_page in com.probe.pkg.

door request base plain-read control (no ?package=) this branch
RestServer GET /meta/view/case_grid/layers?package=all 404 RESOURCE_NOT_FOUND 200, overlay live as the control
RestServer GET /meta/view?package=all [] case_grid, local_grid, other_grid as the control
RestServer GET /meta/book/probe_book/tree?package=all 200, the implicit book: label probe_book, no entries the declared book Probe Book with probe_page as the control
RestServer GET /meta/diagnostics?type=view&package=all scannedItems: 0 scannedItems: 3, both packages in stats as the control
RestServer item read, ?state=draft read, PUT ?package=all as the control (folded by #22185) unchanged
catch-all GET /meta/view/case_grid/layers?package=all 404 RESOURCE_NOT_FOUND 200, overlay live as the control
catch-all GET /meta/view?package=all [] the three views as the control
catch-all GET /meta/book/probe_book/tree?package=all the implicit book, no entries the declared book as the control
catch-all GET /meta/view/case_grid?package=all 200, but the item is decorated _provenance: 'org' with no _packageId _packageId: 'com.probe.pkg' as the control
catch-all GET /meta/view/case_grid?state=draft&package=all 404 NO_DRAFT 200, the draft as the control
catch-all PUT /meta/view/fresh_grid?package=all row package_id: 'all' (the seat's pointer re-measured) RestServer: package_id null package_id null
catch-all POST /meta/view/case_grid/publish?package=all 404 ROUTE_NOT_FOUND, zero protocol calls unchanged: the dispatcher serves no publish, so it has no ?package= to read

H2, what "no package" means at the list. Measured on both hosts: GET /meta/view without ?package= lists case_grid (in com.probe.pkg), other_grid (in com.other.pkg) and local_grid (env-local). No package already means every package, so metaItemPackageBinding is the right fold for the list. The list needs no reading of all of its own.

H3, :5807 and :6582 (line numbers on 31d24a52).

  • :5807 is GET /meta/diagnostics, the cross-type spec-validation sweep. It hands ?package= to getMetaDiagnostics, which runs getMetaItems({ type, packageId }) for every type it sweeps. That is a list read keyed by package, so it is in the family. all reached it as a literal (scannedItems: 0, above), so it is folded and pinned. objectui at the .objectui-sha pin (a58626c8) sends that door's ?package= only after validating it against the project's packages (DirectoryPage.tsx, StudioHomePage.tsx), so no first-party caller sends all there.
  • :6582 is the item read's cache bypass, packageScoped. It is not a read keyed by package: it asks whether the raw parameter is present, to pick the uncached arm, and since fix(metadata-protocol): one head row per /meta write address — a second unpinned package-less draft save is accepted (#22128) #22185 that arm reads through the fold. So all reaches it as a literal by design, and ?package=all keeps the uncached arm and its served version. Folding it would move ?package=all into the cached arm, which serves no version. It is not changed here, and the fold's docblock now says why.

H4, the export. Kept in rest-server.ts and exported from the package entry: export { metaItemPackageBinding } from './rest-server.js'. Not moved beside metaSaveRequestOptions, for two reasons.

  • The fold serves read doors (the layered read, the list, the book tree, the sweep and the item read) as well as the save and the publish. meta-save-request.ts's header scopes that module to the save's precondition and lifecycle, so the fold does not belong there.
  • The entry already loads rest-server.js for RestServer, so the export adds no module to the entry's closure.

H5, Studio reach (triage's p1 condition): the save half does not hold. Read at objectui a58626c8, packages/app-shell/src/views/metadata-admin/ResourceEditPage.tsx:

  • The save and the publish fold all themselves (readActivePackageBinding, lines 248 to 255, used by doSave at 1493 to 1498 and doPublish at 1685 to 1688), so Studio's save omits ?package= under the "show everything" scope.
  • The layered and draft reads send the raw router ?package= (ownerPackageId, lines 433, 1009 to 1015 and 1502 to 1505), so they send ?package=all. Through the catch-all they reach the dispatcher's layered and item branches, which this PR folds.
  • Whether the hosted runtime serves /meta through that catch-all: NOT MEASURED from this session (objectstack-ai/cloud is not readable here).

Pins (packages/runtime/src/domains/meta-package-all-fold.test.ts)

One file drives both hosts over real stores, as meta-save-preconditions-parity.test.ts does. The RestServer leg calls its registered handlers. The catch-all leg repeats the catch-all's four statements over a real Fetch Request into the real HttpDispatcher.dispatch(). Each row compares a door's answer to ?package=all with the same door's answer without ?package=, and keeps a scoping control beside it.

  • Per host: the layered read (control: ?package=com.probe.pkg serves the same layers, ?package=com.other.pkg answers 404), the list (control: each real package id lists only its own view), the book tree (control: the owning package resolves the declared book, another package falls to its implicit book), the item read, the ?state=draft read of a draft that inherited the item's package, and the save (control: PUT ?package=com.probe.pkg binds the row).
  • RestServer alone: the diagnostics sweep (control: a real package id sweeps fewer items).

Reverse verification and ablations

The fix was committed first. Every leg was restored from HEAD and proven by blob hash and an empty git diff HEAD. The subject resolves by relative source import (meta.ts) and @objectstack/rest through the runtime vitest alias to source, so no dist/ leg applies.

leg result rows red
this branch 13 passed none
base rest-server.ts, index.ts and meta.ts in the tree 10 failed, 3 passed every row except RestServer's item read, draft read and save, which #22185 already folded
ablation A: RestServer's layered read back to req.query?.package or undefined 1 failed, 12 passed RestServer's layered read
ablation B: the dispatcher's item branch back to the raw read 3 failed, 10 passed the catch-all's item read, draft read and save

The ablations went through node scripts/ablation-replace.mjs. A: anchor x1 to x0, blob 4f3ef5968792 to 905c48c396c2, restored to 4f3ef5968792 == HEAD. B: anchor x1 to x0, blob b21893a63ef8 to 88f1ec5f41a7, restored to b21893a63ef8 == HEAD.

Verification

All at this branch's head db727d0d (git rev-parse --short HEAD), base ffb31fca, except where a line names a13568b4 (the fix commit; db727d0d adds only the draft-read row to the pin file). Heavy runs went through scripts/pm/os-verify-lock.sh.

  • Build. pnpm --filter '@objectstack/runtime^...' build (the dependency closure), then @objectstack/rest and @objectstack/runtime rebuilt at head: exit 0. The built entries load: require('packages/rest/dist/index.cjs') gives 35 exports, metaItemPackageBinding among them (it answers undefined for all, the empty value and an array, and com.probe.pkg for com.probe.pkg); the ESM entry exports it too; require('packages/runtime/dist/index.cjs') gives 312 exports.
  • Typecheck. pnpm --filter @objectstack/rest typecheck: exit 0. pnpm --filter @objectstack/runtime typecheck: exit 0 after the rest rebuild (before it, the stale dist/index.d.ts answered TS2305 for the new export, as expected). The test layer printed check:test-typecheck: OK — 27 file(s) / 190 error(s) / 68 pinned signature(s) held, ledger unchanged. The new pin file is in that program (tsc -p tsconfig.test.json --listFiles lists it) with no error of its own.
  • Unit tiers, at a13568b4. pnpm --filter @objectstack/rest exec vitest run --project local: 262 files passed, 4938 passed, 326 skipped. pnpm --filter @objectstack/runtime exec vitest run --project local: 336 files passed, 4747 passed, 19 skipped. The repo projects are CI's, except meta-list-projection-parity.test.ts, which reads rest-server.ts's handler source and was run by name.
  • Named pins, at db727d0d. meta-package-all-fold.test.ts 13 passed; with meta-save-preconditions-parity.test.ts, 26 passed. meta-draft-head-package-inheritance.test.ts and meta-draft-read-door-census.test.ts: 13 passed. meta-list-projection-parity.test.ts (--project repo): 682 passed.
  • Gates. node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands at db727d0d derives 64 commands (6 paths, 420 changed lines, under the 5000 threshold), the same 64 the order named. Each was run with its exit code recorded. --ran reconciliation: 64 derived famil(ies) accounted for — 63 run, 1 NOT-MEASURED, 0 unrun. 63 exit 0. The full pnpm lint (eslint . --no-inline-config, whole repository) exits 0 with no findings.
    • NOT MEASURED: pnpm check:dual-build-cjs-loads, reason: exit 3 PREREQUISITE NOT MET (its self-test passed, 93 cases). It reads every package's dist/, and 36 packages are unbuilt in this worktree. The declared narrowing in its place is the CJS and ESM entry loads of the two packages whose shipped code changed (above). CI runs the full gate.
  • Upstream. origin/main is 5 commits past the base (9f0de32a). None touches packages/rest, packages/runtime or packages/metadata-protocol, so no merge of main was taken.

Acceptance notes

Written by session session_01RWZbGvPFcRKvUqASZtunCU.


Generated by Claude Code

claude added 2 commits October 8, 2026 09:58
…of both hosts

The layered read, the list, the book tree and the diagnostics sweep on
RestServer, and the dispatcher's book tree, layered read, item read and
save, and list, forwarded ?package=all to the store as a literal package
id. The layered read answered 404, the list [], the book tree an empty
implicit book, and the dispatcher's PUT bound the row to a package called
all. Each now reads ?package= through metaItemPackageBinding, which
@objectstack/rest exports from its entry for the runtime to take.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-Authored-By: Claude <noreply@anthropic.com>
The Studio editor reads a pending draft back with its raw router
?package=, so under the list's "show everything" scope it sends
?state=draft&package=all. Both hosts now serve the draft ?state=draft
serves.

Claude-Session: https://claude.ai/code/session_01RWZbGvPFcRKvUqASZtunCU
Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m label Oct 8, 2026
@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Oct 8, 2026
@github-actions

github-actions Bot commented Oct 8, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/rest, @objectstack/runtime, touching 10 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/rest/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

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

  • content/docs/api/client-sdk.mdx (via getBookTree (sdk, the bare tail of client method meta.getBookTree, bound to GET /api/v1/meta/book/:name/tree), meta.getBookTree (sdk, the route ledger binds it to GET /api/v1/meta/book/:name/tree, selected by route anchor /book/:name/tree), meta.publishItem (sdk, the route ledger binds it to POST /api/v1/meta/:type/:name/publish, selected by route anchor /:type/:name/publish), publishItem (sdk, the bare tail of client method meta.publishItem, bound to POST /api/v1/meta/:type/:name/publish))
  • content/docs/kernel/contracts/metadata-service.mdx (via /:type/:name/publish (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))
  • content/docs/permissions/system-context.mdx (via handleMetadataRequest (symbol, a top-level function))
  • content/docs/protocol/kernel/error-handling.mdx (via /:type/:name/publish (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))
  • content/docs/ui/apps.mdx (via /book/:name/tree (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))

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

  • content/docs/releases/v14.mdx (via /book/:name/tree (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))
  • content/docs/releases/v17/17-0.mdx (via /:type/:name/publish (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))
  • content/docs/releases/v17/17-1.mdx (via /:type/:name/publish (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))
  • content/docs/releases/v17/17-2.mdx (via /:type/:name/publish (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))
  • content/docs/releases/v17/17-4.mdx (via /:type/:name/publish (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))
  • content/docs/releases/v17/17-5.mdx (via /book/:name/tree (route, bridged from symbol metaItemPackageBinding — its route source's handler names it))

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
  • 1 changed file(s) yielded no anchor (packages/rest/src/index.ts) — pages documenting those are invisible to this run
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 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.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 34 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 9f0de32a03629a021836d9216dbca86ccdc645f3 → packageMentionDocs.

Which tree this was computed on

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

node scripts/docs-audit/affected-docs.mjs --json 9f0de32a03629a021836d9216dbca86ccdc645f3

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

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: db727d0d1b23908aef12d2d622b223c244b8646a
Local-runs: none

① Derived judgments

Inputs read. Card #22188 (body and all 6 comments: the cli seat's pointer 6053861206, triage 6054416999, the unlock note 6054787159, the claim 6057144068, the os-dev-report 6058135495, the seat's ACCEPT 6058168399). PR #22265 body, file list (6 files, +409 / −11) and the net diff origin/main...db727d0d (merge-base ffb31fca, two commits: a13568b4 the fix, db727d0d test-only). The PR head read twice, 10:47Z and 10:55Z: still db727d0d, draft, base main. Check-runs on the head, below. The seat's claim and ACCEPT were judged, not adopted.

Public surface, one change. packages/rest/src/index.ts adds exactly one line of export: export { metaItemPackageBinding } from './rest-server.js'. The function (rest-server.ts:1752) goes function to export function; its body is unchanged (typeof raw === 'string' && raw !== '' && raw !== 'all' ? raw : undefined). No type export, no signature change, nothing removed. The entry already loaded rest-server.js (index.ts:4, RestServer), so the PR body's closure sentence is true. No export ledger or census test names metaSaveRequestOptions or this symbol outside the two source files, their changesets and the two parity pin files, so no ledger update was owed. Right: a purely additive widening of @objectstack/rest's public surface. @objectstack/runtime exports nothing new; it imports the symbol beside metaSaveRequestOptions (meta.ts:76).

Answer changes, each judged. The contract is the one #22128 / PR #22185 stated and pinned: ?package=all names no package on the /meta item doors (its changeset: GET ?package=all reads all as the save and publish doors read it). The spec declares no meaning for all; the list's packageId describe is "Filter by owning package", and all is no owning package, so "no filter" is what the describe already says. Each door below moves from "forward the literal all to the store as a package id" to "read it as no package", with a real package id still scoping (pinned controls).

  1. RestServer layered read (:3936): 404 for a package-bound item becomes the plain read's layers. The layered read is the item read's sibling at the same address; a 404 for an item that exists was a wrong answer, not a refusal of a request shape. Right, a correction.
  2. RestServer list (:6065): [] becomes the plain list. H2 verified in the pin: the plain list already spans both packages and the env-local overlay, so no package is already "show everything"; the list needs no reading of its own. Right.
  3. RestServer book tree (:6400): the implicit empty book of a package called all becomes the declared book. Right.
  4. RestServer diagnostics sweep (:5822): scannedItems: 0 becomes the plain sweep. Verified in-family: getMetaDiagnostics (metadata-protocol/src/protocol.ts:7986) runs getMetaItems({ type, packageId: request.packageId }) per swept type, a list read keyed by package. Triage's direction was every ?package= reader in the family; the claim named this line for a reading; the fold is disclosed as a deviation and pinned. Right.
  5. Dispatcher book tree (:1038), layered read (:1093), list (:1930): the same three corrections on the transport behind the @objectstack/hono catch-all. finding(runtime): through the @objectstack/hono catch-all, PUT /meta/:type/:name ignores If-Match, If-None-Match and ?mode=draft — a stale token writes (200, not 409) and a draft save lands ACTIVE #22141's principle, that the two doors serving one route cannot answer it differently, holds. The book tree's old reading already rejected non-strings and empties, so only all moves there. Right.
  6. Dispatcher item branch (:1264), one packageId for the read, the ?state=draft read and the PUT: the read serves the item with its _packageId as the plain read does; the draft read stops answering 404 NO_DRAFT for an inherited draft (the fix(metadata-protocol): one head row per /meta write address — a second unpinned package-less draft save is accepted (#22128) #22185 correction, now on this host); the PUT ?package=all stores the row env-local (package_id null) where it stored package_id: 'all'. The write change is a parity correction to RestServer's save door, which has folded all since before fix(metadata-protocol): one head row per /meta write address — a second unpinned package-less draft save is accepted (#22128) #22185. Right.
  7. Dispatcher non-string ?package (an array) now reads as no package where query?.package || undefined forwarded it. This is the exported function's declared arm (its docblock and the rest changeset: "a non-string value is not forwarded either"), not a reading this PR invents. Reach: the catch-all flattens its query to one string per name (adapters/hono/src/index.ts:737), and the dispatcher plugin mounts no /meta route, so no production path hands it an array; on RestServer, every door that folds ?package= sits behind refuseRepeatedQueryParams (:3934, :5671, :6062, :6398, :6450, :7146, :7958), so no RestServer answer moves on an array. Right as a consequence of the shared fold, and not an accept-set widening needing its own declaration; the pre-existing multiplicity gap is carried to ③.

The cache bypass left raw (:6600, packageScoped). It asks only whether the raw parameter is present, to pick the uncached arm; that arm reads through the fold (:6865, #22185) and serves version. Folding the presence check would send ?package=all to the cached arm, which serves no version, and would break #22185's pinned rows ("active: the read's version is the token the save compares against" under ?package=all; its changeset: "?package=all still takes the uncached arm, so it keeps serving version"). The fold's docblock (:1748) now records why. Right. One note, not a defect: the inline comment above packageScoped (:6593) still gives ADR-0048 scoping as the bypass's only reason; the docblock is the authority and now says the rest.

Nothing left unfolded. At head, every req.query?.package reader in rest-server.ts is a fold (this PR's four, #22185's three) or the presence check; every query?.package reader in meta.ts is a fold (four). GET /meta/_drafts reads ?packageId= on both hosts (rest-server.ts:5920, meta.ts:1850), a different parameter. The dispatcher's three-segment routes are state, layers, published and book/:name/tree; it has no publish branch, so the PR body's "serves no publish" is consistent with the code.

Truth of the changesets and PR body against the diff. Rest changeset: the export sentence, the four-door list, the function's contract (all, empty, non-string), "the item read, the save and the publish already read it through" (:6865, :7176, :7959), each before/after row, and the unchanged lines (real id still scopes; the bypass reads raw) are each true of the diff and pinned. Runtime changeset: the PUT row, the four read branches, "every branch reads through metaItemPackageBinding from @objectstack/rest", and the unchanged line are true. PR body: "the only new export" true; H3 both readings true; H4 both reasons verified (meta-save-request.ts header scopes it to the save's precondition and lifecycle); H5's objectui sha a58626c8 equals .objectui-sha at head; acceptance notes on flattening, _drafts and publish verified. H1's base-column measurements and the reverse-verification counts are the dev's claims, consistent with the base code (which did forward the literal) and with the pin file, whose rows count 6 per host x 2 hosts + 1 sweep = 13. Not re-run here, per the brief.

② Semver level

③ Boundary flags

  • open_questions: none in the report.
  • out_of_scope_findings[0], GET /meta/_drafts?packageId=: answered. Verified a differently named parameter on both hosts; not a ?package= reader; not this family. Noted, not filed, is right.
  • out_of_scope_findings[1], an array ?package on the dispatcher: answered in ① item 7, and escalated to the seat: the dispatcher's lack of a query-multiplicity judgment has now been recorded on two PRs (fix(runtime): the @objectstack/hono catch-all's PUT /meta honours If-Match, If-None-Match and ?mode=draft #22206's acceptance notes, fix(rest,runtime): ?package=all names no package on every /meta door of both hosts (#22188) #22265's) without a card. Whether a card is owed for parity with RestServer's refuseRepeatedQueryParams is the seat's call; it does not block this PR.
  • Deviation, pins in one runtime file: answered. The rest leg drives RestServer's real registered handlers over a real store; runtime can import both subjects and rest cannot import the dispatcher; the precedent file took the same shape.
  • Deviation, the diagnostics sweep folded under H3: answered, verified in-family in ① item 4.
  • Deviation, unit tiers at a13568b4: answered. db727d0d adds only one pin row to the new test file; the named pins were re-run at head; CI on the head is the verdict.
  • Deviation, the stray gate runner: process hygiene with no effect on the diff; the tree was shown clean after each leg. No action.
  • H5, hosted reach NOT MEASURED: the grade is the seat's, not a contract question. The reading that Studio's save folds all itself while its layered and draft reads send the raw router value is consistent with the card's named producer, and this PR fixes the read half on both hosts.
  • Escalated, mine: rows the dispatcher's PUT ?package=all bound to the package all before this fix are not migrated here. On H2's reading they stay reachable by the plain read and list, which span every package; whether any exist on a hosted store is not measurable from this review. The seat decides whether a sweep card is owed.
  • Check-runs on db727d0d, read 2026-10-08T10:55:21Z: 33 runs, 28 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke), 2 in progress (Lint & Repo Gates, Test Core 1/6), 0 failed. Earlier read 10:48:55Z: 31 runs, 18 success, 3 skipped, 10 in progress, 0 failed. Landing waits on the two still running; nothing read contradicts this verdict.
  • Docs-drift bot comment: advisory; index.ts yielded no anchor; no release-owned page is edited by this diff.

Implemented-by: claude/issue-22188-package-all-fold
Reviewed-by: session_01RWZbGvPFcRKvUqASZtunCU

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 8, 2026 11:16
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 8, 2026 11:16
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 8, 2026
Merged via the queue into main with commit fbcbcf1 Oct 8, 2026
36 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-22188-package-all-fold branch October 8, 2026 11:52
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 9, 2026
…ld permission for every caller class (objectstack-ai#22250) (objectstack-ai#22308)

Fixes objectstack-ai#22250
Clause-②: yes (widening)

## What this does

For one caller, the object metadata read and the data route disagreed
about a field that caller's field-level permission marks unreadable. The
`sortability` projection served beside the schema on `GET
/meta/object/NAME` marked that field `sortable: true` for a
platform-admin or organization-admin caller, while the data route
refuses a sort or filter on it with `403 PERMISSION_DENIED`.

After this PR, the `sortability` projection is derived from the caller's
own field permission for every caller class. The object definition
itself (`item.fields`) is unchanged: whole for the exempt classes,
because Studio and Setup authoring need it, and masked for everyone
else. The data route and the permission read are unchanged. The spec's
description of the projection is amended to match.

## Measured first: the exemption and its stated reason

- **Where it is.** `isObjectSchemaMaskExempt` in
`packages/metadata-core/src/object-schema-fls.ts` (ADR-0106 D4). It
exempts `isSystem` and any caller holding `manage_metadata`,
`studio.access` or `setup.access`. The platform-admin set holds all
three. An organization admin holds `setup.access`.
`resolveObjectSchemaMaskPosture` answers such a caller `passthrough` /
`exempt` before it asks the security service.
- **Its stated reason.** ADR-0106 D4: "Studio/Setup authoring requires
the full schema." The code adds the write-gate derivation: a projected
read sent back by a `PUT` would delete the fields it withheld. So this
is the triage ruling's first branch: keep the definition read for
authoring, and make the runtime projections agree with the caller's
field permission.
- **Dispatch assumption M2 was partly wrong.** The sortability
projection (`translateMetaEnvelope` in
`packages/rest/src/meta-item-read-gate.ts`, which both transports call)
already read the post-mask document. For an exempt caller, though, that
document is the unmasked one, so the projection inherited the exemption.
- **Picker-facing field lists on the server.** `sortability` is the only
runtime projection the object read serves beside the definition. The
list read and the layered view serve definitions only, with no
projection. `item.fields` is itself the definition the ruling keeps for
authoring. objectui handles its pickers against the permission read on
its side (objectui#11925, objectui#11943).

## Door table: `examples/app-showcase`, real boot

Setup: `showcase_client_liaison` (which marks `showcase_project.budget`,
`spent` and `budget_remaining` as `readable: false`) is bound to the
`everyone` position. Callers: the seeded platform admin; `phone.demo`
promoted to organization `admin`; `auditor.demo` as a member.

The table was measured at base `c8bb3c8d9c` and at head `8b412a83e2`,
then re-measured on the final head `a337d0cc53`, which carries
`805f1ba09a`'s change to the exemption predicate's source and the spec
text. Every head cell below reads the same on `8b412a83e2` and on
`a337d0cc53`.

| Door | Platform admin, base → head | Organization admin, base → head |
Member (control), base → head |
|---|---|---|---|
| `GET /auth/me/permissions`, `budget` | `readable: false` → same |
`readable: false` → same | `readable: false` → same |
| `GET /data/showcase_project` | field absent from rows → same | field
absent → same | field absent → same |
| `GET /data/showcase_project?sort=budget` (and `?budget=100`) | `403
PERMISSION_DENIED` → same | `403` → same | `403` → same |
| `GET /meta/object/showcase_project`, `item.fields.budget` | present
(22 fields) → present (22) | present (22) → present (22) | absent (19) →
absent (19) |
| same read, `sortability.fields.budget` | `{ sortable: true }` (23
entries) → **absent** (20 entries) | `{ sortable: true }` (23) →
**absent** (20) | absent (20) → absent (20) |
| same read, `ETag` | `"39ce56dc"` → `"39ce56dc~3c4b8092"` |
`"39ce56dc"` → `"39ce56dc~3c4b8092"` | `"39ce56dc~32c59ec2"` → same |
| `GET /meta/object/showcase_project/layers` (authoring) | `effective`
22 fields with `budget` → same | same → same | 19 without `budget` →
same |

At head, all three classes are served byte-equal `sortability`.
Conditional reads, the same on both heads:
- The platform admin's own ETag answers `304`, and so does the
organization admin's own ETag (on `a337d0cc53`).
- The pre-change ETag `"39ce56dc"` answers `200`.
- The member sending the admin's ETag gets `200`.

After unbinding the set, the unrestricted admin's ETag is `"39ce56dc"`
again (byte-identical, ADR-0106 D3), and `budget` is `{ sortable: true
}` with 23 entries.

## How

- **`@objectstack/metadata-core` (`object-schema-fls.ts`).** The
`exempt` posture of a caller exempt by capability, in a deployment with
a `security` service, carries a lazy `runtime` question. That question
asks the same `getMetadataReadableFields` the mask asks a non-exempt
caller, at most once. It never rejects: a throw or an `undefined` answer
settles `undetermined`, with one `warn`, so D4's "a sick security
service cannot fault an exempt caller" holds.
`resolveObjectSchemaRuntimeView(document, posture)` returns the view the
runtime projections are derived from: the served document for every
other caller, and for an exempt caller the `fields` their permission
reads, plus a fingerprint of what it withholds. Each withheld name in
that fingerprint carries a qualifier (`@` occurs in no machine name), so
an exempt caller and a projected caller denied the same field never
share a validator. `isObjectSchemaMaskExempt` now delegates its
capability half to a private helper, which the resolver asks alone. That
adds no second elevation read (`check:system-context-census`).
- **`@objectstack/rest`.**
- `translateMetaEnvelope` takes an optional `runtimeDocument` (default:
the served document) and derives `sortability` from it.
- `createMetaItemAnswer` step 5 computes the runtime view and passes it
through the `translateEnvelope` port, which gains an optional third
parameter.
- In `rest-server.ts`, the cached arm of `GET /meta/:type/:name` treats
a posture carrying `runtime` like a projecting posture: the protocol is
not handed `If-None-Match`, the runtime view's fingerprint is folded
into the ETag, and the `304` is judged against that folded value. The
private `translateMetaEnvelope` and `metaItemAnswerSources` forward the
view.
- **`@objectstack/runtime` (`domains/meta.ts`).** Only
`answerMetaItem`'s `translateEnvelope` port, which forwards
`runtimeDocument`. Every dispatcher item read already goes through
`createMetaItemAnswer`.

## Spec text (cross-lane, `domain:spec`)

Declared by the seat on `domain:spec`'s seat post. The change is to text
only: no shape, key, enum or accept-set change.
- **`packages/spec/src/api/sortability.zod.ts`.**
- The `ObjectSortabilitySchema.fields` `.describe()` now says: "The
domain is the served field map plus the always-provisioned `id`, less
any field the caller's own field permission withholds: a caller exempt
from the object-schema mask (ADR-0106 D4) is served such fields in
`item` and gets no entry for them; for every other caller the served map
already excludes them." The list of absent names gains "a field the
caller may not read".
- The module docblock's category list gains case 5, "Withheld by the
caller's field permission". Case 1's domain sentence carries the same
exception, which removes nothing for every caller but a D4-exempt one.
- `resolveObjectSortability`'s docblock says that, for a D4-exempt
caller, it runs on the document narrowed to the fields the caller's own
permission reads.
- **Generated.** `content/docs/references/api/sortability.mdx` and
`content/docs/references/api/protocol.mdx` were regenerated by `pnpm
--filter @objectstack/spec check:generated --fix`. That forced a spec
build, found only `check:docs` stale (14 of 15 green), and regenerated
it. A second `check:generated` run then reported all 15 artifacts
current. Nothing generated was hand-edited.
- **Changeset.** No `@objectstack/spec` entry was added, because no
changeset gate requires one for a description-only change:
- `Check Changeset` (`pr-automation.yml`) counts the changesets a PR
adds;
- `check-changeset-no-major.mjs`, driven offline with this body as its
event, reads `Clause-②: yes (widening)` and is satisfied by
`metadata-core` and `rest` graded `minor`, and it lists no ungraded spec
package;
  - `check-empty-changeset.mjs` is green.

## Tests

- **`packages/metadata-core/src/object-schema-fls.test.ts`** (new
describe block):
- per class (platform admin, organization admin, member): the runtime
view holds only the readable fields, the exempt definition is served
whole, and the member's view is the served document by reference;
- one permission gives one view whatever the class; no validator is
shared with the definition mask;
- an unrestricted exempt caller is byte-identical; the question is lazy
and asked at most once, and never for a document without `fields`;
- `undefined` and a throw both settle undetermined with a `warn`;
`isSystem` and a deployment with no service carry no question.

One existing assertion went from `toEqual` to `toMatchObject` plus a
`typeof runtime === 'function'` check, because the exempt posture now
carries the function. Its "asks nothing" pin (`asked === 0`) is kept.
- **`packages/rest/src/meta-object-sortability-caller-fls.test.ts`**
(new), per class, on the cached (default) and uncached branches:
- the definition is unchanged, `sortability` never names the unreadable
field, and it is equal across classes;
- the member control still agrees with `resolveObjectSortability(item)`;
- the cached arm's ETag is fingerprinted, the protocol is not handed the
conditional, the admin's own ETag answers `304`, the member's ETag does
not cross over, and an unrestricted admin keeps `"v1"` and its `304`;
- a throwing service answers `200` with the definition and the
unprojected `sortability`.
-
**`packages/runtime/src/domains/meta-object-sortability-caller-fls.test.ts`**
(new): the same per-class pins on the dispatcher's protocol-backed and
registry-backed item reads.

**Reverse verification (from committed state, on-disk proof by
`scripts/ablation-replace.mjs`):**
- **A.** `translateMetaEnvelope` was mutated to derive `sortability`
from the served document (the behaviour before this PR). The REST suite
went 5 failed / 8 passed. After `pnpm --filter @objectstack/rest build`,
`ablation-dist-preflight` found the marker in `dist`, and the runtime
suite went 4 failed / 4 passed. Restore: blob equal to `HEAD`, `git diff
HEAD` empty; rebuild; preflight `--absent` with a clean tree; 13/13 and
8/8 green.
- **B.** The exempt posture was mutated to never carry `runtime`. The
metadata-core file went 7 failed / 29 passed, all assertion failures.
Restored, blob equal to `HEAD`.

The first B attempt used an undefined identifier and went red on a
`ReferenceError`, so that reading is void. The second was refused by the
tool because the replacement contained the anchor, so nothing was
mutated.

## Local verification (final head `a337d0cc53`)

All suites ran through the shared verify lock. Exit codes were read from
its `VERDICT` line.

- **This round (spec text and regenerated docs).**
- `@objectstack/spec` test: 625 files, 18711 passed and 1 todo.
`typecheck` exit 0, which includes `check:scripts-typecheck` and
`check:test-typecheck`.
- `dispatch-gates --commands` derives 111 commands for the full touched
path set (11 paths). All 111 ran and exited 0.
- `--ran` reconciliation: 111 derived, 111 run, 0 NOT-MEASURED, 0 UNRUN
(a derived zero: every entry records its exit code).
- The derivation warns that one file it reads,
`scripts/platform-object-tenancy-census.json`, changed on `main` since
this branch's merge base, so that family read this branch's copy.
  - Narrowed lint on `sortability.zod.ts`:
- `eslint --no-inline-config --format json` reports 1 file, 0 errors and
0 warnings.
- The effective config (`--print-config`) enables 5 syntax rules and
`parserOptions.project` is `null`.
- With no type-aware linting, the change cannot move a verdict on a file
it does not touch.
- **Earlier round (unchanged since).**
- `@objectstack/metadata-core`: 18 files, 425 passed; `typecheck` exit
0.
- `@objectstack/rest` at `3768ffad20`: 268 files, 5142 passed and 327
skipped; `typecheck` exit 0.
- `@objectstack/runtime`: 340 files, 5508 passed and 19 skipped at
`805f1ba09a`; `typecheck` exit 0 at `3768ffad20`.
  - Narrowed lint over those 7 files: 0 errors and 0 warnings.
  - Gates: 64 of 64 exited 0 at `3768ffad20`.
- **Merge.** `main` was merged once, at `9865012548` (objectstack-ai#22265 touched
`rest-server.ts` and `runtime/src/domains/meta.ts` in a different
region). Against the current `origin/main`, `git merge-tree
--write-tree` exits 0 and no file overlaps, so no second merge was made.

## Answered by the seat

- **Clause-②.** The measurement decides: `yes (widening)`, with `minor`
for `metadata-core` and `rest` and `patch` for `runtime`. The claim's
line was revised to match. Two transport-local copies of the posture
logic were refused.
- **The spec text.** It rides this PR, not a separate card, with
`packages/spec` lifted for exactly this text. It is declared cross-lane
on `domain:spec`'s seat post.

## Acceptance notes

These are observations only, read from source and not measured. No card
is filed for them.
- **objectui's docblock.** objectui's
`packages/core/src/utils/column-sortability.ts` repeats the sentence
this PR amends ("the served field map plus the always-provisioned
`id`"). That is another repo and is not edited here. Carrier: objectui
`domain:ui` seat 1 (objectui#11925's filer).
- **Partially masked fields.** For any caller, `sortability` keys on
readability, while the security service's query guard also refuses a
predicate or sort on a field served partially masked (`maskingRule`).
Per `getQueryableFields`'s docblock, a masked field is readable but not
queryable. No in-repo app declares a `maskingRule` today (showcase
`permission-sets.ts`). Whoever touches `resolveObjectSortability` next
should weigh it.
- **External objects.** On an ADR-0015 `external` object, an
author-declared platform-injected column that the caller cannot read is
re-added by `unprovisionedInjectedColumns` as `sortable: true` plus a
caveat once the mask removes the declaration. This is the same for every
class.

---

_Generated by [Claude
Code](https://claude.ai/code/session_01EUBvqtauTDmHi2ZgY759p2)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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/m tests tooling

Projects

None yet

2 participants