Skip to content

fix(rest)!: PUT and the five by-name /meta sub-doors declare closed query sets, so ?member= is refused, never dropped (#23072, RestServer half) - #23133

Draft
objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/issue-23072-meta-doors-closed-sets
Draft

objectstack-fleet[bot] wants to merge 5 commits into
mainfrom
claude/issue-23072-meta-doors-closed-sets

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Part of #23072
Clause-②: no (narrowing)

This is the RestServer half of #23072. PUT /meta/:type/:name and the five by-name sub-doors outside ruling 6116165299's list (/references, /layers, /audit, /diff, /published) now each declare a closed query-parameter set (AGENTS.md route rule 5). So the bundle member selector ?member=, and any other name a door does not read, gets a located 400 VALIDATION_ERROR instead of being dropped.

#23072 remains open for the other half: the runtime dispatcher's /meta item GET. That half needs META_ITEM_READ_PARAMS and the refusal sentence from @objectstack/rest. Neither is on the package's published entry, so reaching them needs a NEW public export. The dispatch reserved that decision, so the half goes back to the seat as needs_decision, with the measurement below. No dispatcher code is in this PR.

Patch round 1: the changeset declares the narrowing (new head d64fc8e605)

The contract review of d813364338 (6138381862) failed on ② only. The changeset graded six doors' accept-set narrowing patch, under a bare Clause-②: no and with no ADR-0087 disposition, so every gate read it as non-breaking. This round edits .changeset/23072-meta-doors-closed-sets.md only (commit 49994b3335), then merges origin/main (merge commit d64fc8e605, the new head). No source file moves, and the merge brought nothing under packages/rest.

  • '@objectstack/rest': minor (was patch).
  • The summary line is fix(rest)!: …, and this PR's title carries the same !.
  • Clause-②: no (narrowing) (was Clause-②: no), in the changeset and on line 2 above.
  • Exactly one ADR-0087 disposition marker, in the HTML-comment form: adr-0087: not-required (no-migration-prescription). Its facts, each checked against the diff:
    • No path under packages/spec, no metadata schema and no stored shape is in the diff.
    • The six closed sets are module constants that packages/rest/src/index.ts does not re-export. The build entry is src/index.ts alone, and exports holds only ..
    • The one removed source line is inside unknownQueryParamMessage, which the entry does not export either.
    • The diff names no ADR-0087 id, and @objectstack/rest publishes.
  • The launch-window banner, **BREAKING** (an accept-set narrowing), shipped as minor.
  • The per-door remedies stay as written.

Gates at d64fc8e605, each exit captured before any pipe:

  • node scripts/check-adr-0087-registration.mjs --base origin/main: exit 0, check-adr-0087-registration: 1 declared-breaking changeset(s), each carrying an ADR-0087 disposition. The signals read are BREAKING+bang+clause-②-narrowing, and the disposition is not-required (no-migration-prescription).
  • node scripts/check-changeset-no-major.mjs --base origin/main: exit 0, This diff introduces no major bump. Locally the level axis reads NOT APPLICABLE, because there is no pull_request payload. Driven offline with --event carrying this title and body: exit 0, LEVEL AXIS: this PR declares clause-② no (narrowing), and no package whose published source … it moves is graded patch. Control, the same body against the old head d813364338, whose changeset still says patch: exit 1, clause-② declares no (narrowing) while no moved package is graded minor or above. So line 2 and the changeset have to move together, and at d64fc8e605 they do.
  • node scripts/check-empty-changeset.mjs --base origin/main: exit 0.
  • pnpm check:changeset-gate-self-tests: exit 0 (170, 548 and 401 assertions).
  • dispatch-gates --commands with no paths, at d64fc8e605: 65 commands over 5 paths against merge base dc93781c1, and all 65 exit 0. On the first pass check:dts-closure, check:dual-build-cjs-loads and check:lean-entry-closure exited 3 (PREREQUISITE NOT MET: a fresh worktree has no dist/). After turbo run build --filter='!@objectstack/docs' --concurrency=2 through the lock (74 successful, VERDICT command-exit 0) all three exit 0. check:sourcemap-no-sources-content had run while that build was still writing, so it was re-run on the finished build: exit 0. dispatch-gates --ran: 65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED.
  • origin/main moved to e540e7ffb9 during the run, 5 commits, none under packages/rest and none in the changeset gates. The same derivation over this PR's 5 paths, on a throwaway checkout of e540e7ffb9, gives the identical 65 commands.
  • The @objectstack/rest tests were not re-run: the merge brought no change under packages/rest, and this round moves no source file.

The defect, per door

None of the six declared a set. Each read the names it knew and ignored the rest. PUT /meta/email_template/x?member=zh-CN with an en-US body wrote en-US and answered 200. The five GETs answered for the canonical member a request that named another. Both are plausible answers about the wrong row, and no status, header or field told them apart.

The sets, measured from each handler's read points

All six live in registerMetadataEndpointsInner, on base bcebd8409d. Each door's helpers were read too, not just the handler body. The whole-request helpers resolveExecCtx, resolveProtocol (route, hostname, X-Environment-Id), resolveObjectMasker, resolveMetadataService, walledCatalogRefusal, resolveMetaWriteActor and the read-gate sources read no query key. The cloud host's kernel-resolver strategy is not in this tree, so it was not read.

door read points before after (exported beside META_ITEM_*_PARAMS)
PUT /meta/:type/:name force (handler), package (metaItemPackageBinding), mode (a frame down, metaSaveRequestOptions, meta-save-request.ts:118) none META_ITEM_SAVE_PARAMS = force, package, mode
GET …/references none none META_ITEM_REFERENCES_PARAMS = (empty)
GET …/layers package (a frame down, serveMetaItemLayered) none META_ITEM_LAYERS_PARAMS = package
GET …/audit limit (readDeclaredQueryNumber) none META_ITEM_AUDIT_PARAMS = limit
GET …/diff from, to, fromVersion, toVersion (readDeclaredQueryNumber) none META_ITEM_DIFF_PARAMS = those four
GET …/published none none META_ITEM_PUBLISHED_PARAMS = (empty)

PUT, /audit and /diff already had a multiplicity guard over exactly these names. Recognition runs just before it, after the capability and authoring gates, which is the placement the five ruled doors use. /references, /layers and /published open with recognition, as the item GET does. The refusal is the file's own refuseUnknownQueryParams, which writes the nested envelope { error: { code: 'VALIDATION_ERROR', message } }. That is the envelope the five ruled doors answer for a name they do not know.

PUT stays body-addressed (ruling 6116165299): it refuses member and does not read it. None of the five sub-doors starts honouring member, because their protocol requests do not declare it. They refuse it.

One edit outside the claimed file surface: query-allowlist.ts

/references and /published read no query parameter, so their closed sets are empty. They are the first empty sets in the repository. With an empty set, unknownQueryParamMessage ended Supported parameters: ., which reads as a truncated answer and would have shipped with this PR. The function now ends This endpoint takes no query parameters. when the set is empty. Every non-empty set's sentence is byte-identical to before: the three existing pins of that message (rest-server-approvals-unknown-filter.test.ts, rest-server-closed-query-params.test.ts, rest-server-meta-member-selector.test.ts) pass unchanged. This file is not in the claim's file surface. It is declared here, and in the report to the seat, rather than worked around with a second refusal dialect.

Caller census: every in-repo caller of these doors, and what it sends

caller sends verdict
@objectstack/client meta.saveItem (both clients, one metaSaveQuery builder) force, package, mode green
@objectstack/client getReferences / getPublished nothing green
@objectstack/client getAudit / diffItem limit / from, to green
objectui at the pin 1fb1ced36243, @object-ui/data-objectstack MetadataClient (save, layered, references, diff, audit), shallow-fetched read-only force, mode, package / package / nothing / from, to / limit; no /published caller green
objectui UnpublishedAppBar.tsx, metadata-admin/external/api.ts (PUT by fetch) no query green
dogfood: 9 files PUT with a query string package ×21, mode ×16, force ×1 green, measured below
dogfood: the 7 /layers and /published callers no query green, measured below
packages/rest/src/meta-item-save-capability-gate.test.ts "query/body-smuggled organization ids" ?organizationId= on PUT, a name the door never read inert. Red under the new set; fixed here

The one inert caller is a test pin written while PUT dropped unknown names: it asserted that a query-smuggled organizationId "does not reach the write" by watching it get dropped. It is split in two. The body half is unchanged: 200, and organizationId is not in the save request. The query half is now the located 400, with saveMetaItem never called. That is the same invariant, made loud. No SDK or console caller sends a name outside a new set, so the census found no needs_decision.

Not here: the runtime dispatcher's /meta item GET

Measured on bcebd8409d, packages/runtime/src/domains/meta.ts handleMetadataRequest:

  • Read points: state, preview, package, layers (wantsMetaItemLayers) and locale (metaRequestLocale, inside createMetaItemAnswer). member is never read and never forwarded.
  • One source for the set: @objectstack/runtime imports from @objectstack/rest (a single . export). Its index exports metaItemPackageBinding, metaSaveRequestOptions, repeatedQueryParamMessage and the read-gate family. It does not export META_ITEM_READ_PARAMS, META_ITEM_MEMBER_PARAM, metaMemberSelector, refuseUnknownQueryParams or unknownQueryParamMessage. Reaching any of them needs a new public export, so this PR stops there.
  • The envelope: RestServer writes { error: { code: 'VALIDATION_ERROR', message } }. The dispatcher's deps.error(message, 400) writes { success: false, error: { code: 'VALIDATION_ERROR', message, … } } through buildApiError. The rest index already records that split for the multiplicity rule ("a dispatcher domain takes the message and builds its own body"). So "the same envelope" means the same error.code and the same message, each in its own transport's body.
  • What honouring member costs there, beyond the export: three getMetaItem calls need it (the object branch, the generic branch and readPendingDraft). Two lookups wrap getMetaItem in a bare catch {}, which would swallow the protocol's located 400 for a bad selector and fall through to the registry or MetadataService. The MetadataService fallback (getItem(type, name, packageId)) keys by name alone, so for a member the protocol answers absent it would serve the canonical member. The ?layers= with member refusal is a sentence in rest-server.ts too.

The options and their analysis are in the report on #23072.

For the spec lane: could PR #23070's reader serve member on the five sub-doors?

This is read from source, not built. resolveBundleMemberSelector resolves the selector on every door. What differs is whether the door's store can address a member:

  • /references: no, permanently. findReferencesToMeta sweeps the graph for items that name (type, name). A reference names an item, not one member of a bundle, so there is nothing to select. The refusal is the right answer, not a gap.
  • /layers: yes, with threading. The overlay read findServedOverlayRow already takes member, and so do the code reads lookupArtifactItem and registry.getItem. getMetaItemLayered passes none. The MetadataService code read (readItemFromMetadataService) would need the addressed-member check getMetaItem applies. overlayLockLayerAt reads every member's rows by name. GetMetaItemLayeredRequestSchema would need the field.
  • /audit: no, not with the reader alone. sys_metadata_audit has no member column (type, name, organization_id, operation, …), so every member's attempts share one trail. Serving member needs a column, the writer stamping it, and AuditMetaItemRequestSchema.member.
  • /diff: yes, with threading. historyRowsOfMember(type, rows, member) already takes a member, and diffMetaItem passes none. The default to (the active version) would read a member-scoped active row, and RestServer's per-caller gate reads the canonical current document (fetchCurrentMetaDocument). No spec request is declared for the door.
  • /published: partly. The overlay half rides getMetaItemLayered, as /layers does. The code and package snapshot fallback getPublished reads MetadataService, which keys by name and holds one member per name.

Evidence

The measurements below name the head they ran on. Code commit f0f77809f4 is the source every rest and runtime run below built from. d813364338 adds only the census pin's split.

  • Build: pnpm turbo run build --filter='@objectstack/dogfood^...' --filter='@objectstack/runtime' --concurrency=2 gave Tasks: 64 successful, 64 total, lock VERDICT command-exit 0. Then the 8 packages check:dual-build-cjs-loads named as unbuilt gave 58 cached, 58 total, BUILD8_EXIT=0.
  • @objectstack/rest typecheck (tsc --noEmit && check:test-typecheck): TYPECHECK_EXIT=0, check:test-typecheck: OK … 0 file(s) / 0 error(s).
  • @objectstack/rest test, whole local project, at 52b332b6d5: Test Files 1 failed | 275 passed (276), Tests 1 failed | 5781 passed | 326 skipped (6108). The one red is the census caller above. After its split, at d813364338: the new pins, that file and the ruled-doors file (rest-server-meta-door-closed-sets, meta-item-save-capability-gate, rest-server-meta-member-selector) gave Test Files 3 passed (3), Tests 72 passed (72). The repo project gave Test Files 5 passed (5), Tests 213 passed | 2 skipped (215). Between 52b332b6d5 and d813364338 only that test file changed (packages/rest/src is otherwise byte-identical). So the whole-package line at the final head is a declared narrowing: the changed file was re-run green, and CI runs the full set.
  • @objectstack/runtime typecheck: TYPECHECK_EXIT=0. Test local: Test Files 363 passed (363), Tests 5174 passed | 19 skipped (5193). repo, which includes meta-list-projection-parity.test.ts with its /layers read census over rest-server.ts: Test Files 3 passed (3), Tests 751 passed (751). Lock VERDICT command-exit 0.
  • CONTROL, the five ruled doors: rest-server-meta-member-selector.test.ts is unchanged and green in the 72 above.
  • Dogfood, the 18 census files: pnpm exec vitest run --maxWorkers=2 over the 9 PUT-with-query files, the 7 /layers and /published callers, email-template-two-locales-survive-boot and flow-shipped-name-by-name-read, at d813364338: Test Files 18 passed (18), Tests 156 passed (156), lock VERDICT command-exit 0. The first lock attempt was exit 99 queue-timeout, which is NOT MEASURED; the re-run used the same slot.
  • Ablation, on committed d813364338, inside os-verify-lock.sh with a restore trap, through scripts/ablation-replace.mjs. The mutation drops PUT's refuseUnknownQueryParams(req, res, META_ITEM_SAVE_PARAMS), with marker __ABLATION_23072_SAVE_RECOGNITION_DROPPED__. Counts on disk: before anchor 1 / marker 0, during anchor 0 / marker 1. Blob 9a64fac24d3c → f1b89f68cd07. Result: Tests 3 failed | 18 passed (21). Exactly PUT's three refusal pins are red, and every other door's and every preservation pin stays green. Restored: blob 9a64fac24d3c == HEAD, git diff HEAD is empty, and after the restore the counts are anchor 1 / marker 0. The rest test file imports ./rest-server.js from src/, so no dist/ is in this loop. The expected direction was red, and red was observed.
  • Gates: the dispatch-gates --commands union, 65 commands, run at d813364338 with each exit code captured before any pipe: all 65 exit 0. dispatch-gates --ran: 65 derived famil(ies) accounted for — 65 run, 0 NOT-MEASURED. An earlier pass had check:dual-build-cjs-loads at exit 3 (PREREQUISITE NOT MET, 8 packages unbuilt). After building them it exits 0 (108 published require entry point(s) across 67 package(s) load). Added, as the dispatch asked: pnpm check:route-ledger-census exit 0 (all 1 census sentence(s) match their arrays), and pnpm check:route-envelope exit 0, which is also in the 65. The per-gate verdict lines are in the report on meta doors: the bundle-member selector member is dropped silently by the runtime dispatcher GET, by PUT, and by the by-name doors outside the ruling (no closed query sets); honour it or refuse it loudly #23072.
  • Lint, proven narrowing: this is a narrowing; the whole-repo pnpm lint is CI's. ① Population, read from eslint.config.mjs: files: ['**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'] minus NEVER_LINTED, and isPathIgnored is false for all four touched .ts files. ② Count, from eslint --no-inline-config --format json: 4 files, 0 errors, 0 warnings, 0 fatal, exit 0, at d813364338. ③ Invariance: the resolved config has parserOptions.project null and projectService null, so type-aware linting is off. The enabled rules (no-restricted-imports, slot-lookup/no-any-assignment, no-restricted-syntax, query-options/no-any-erasure, verify-stand-in/no-asserted-driver-argument, comment-swallow/no-code-inside-block-comment) are per-file AST rules, so this diff cannot move any untouched file's verdict.

Acceptance notes

  • Merged with origin/main in patch round 1 (d64fc8e605, a merge commit over dc93781c19). The 15 incoming commits touch nothing under packages/rest. The branch's delta against origin/main is still these 5 files, +357 / −3 (the changeset is now 23 lines), and packages/spec is byte-identical to origin/main's.
  • The dispatcher's /layers, /published and PUT branches declare no closed set either. Its PUT drops ?member= (and, by its own settled ruling, ?force=) exactly as RestServer's did. These are the same family as meta doors: the bundle-member selector member is dropped silently by the runtime dispatcher GET, by PUT, and by the by-name doors outside the ruling (no closed query sets); honour it or refuse it loudly #23072 but outside its population: the card names the dispatcher's item GET only. They are named in the report for the seat.
  • The changeset grades @objectstack/rest minor under Clause-②: no (narrowing), with one ADR-0087 disposition (patch round 1, above). The claim's patch and bare Clause-②: no under-declared the narrowing. @objectstack/runtime is not in it, because no runtime file changed.

Generated by Claude Code

…are closed query sets

PUT /meta/:type/:name and GET .../references, /layers, /audit, /diff and
/published each open with refuseUnknownQueryParams over a set measured from
their read points, so ?member= (and any other name they do not read) is a
located 400 instead of being dropped. The unknown-parameter message says
"This endpoint takes no query parameters." for an empty set.

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
…cated 400

The census caller: the pin sent an inert ?organizationId= that PUT never
read; the door's closed set refuses it, so the pin splits into the body
half (still 200, nothing reaches the save) and the query half (400,
nothing written).

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/m documentation Improvements or additions to documentation tests tooling labels Oct 13, 2026
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/rest, touching 23 documentable anchor(s).

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

  • content/docs/api/client-sdk.mdx (via diffItem (sdk, the bare tail of client method meta.diffItem, bound to GET /api/v1/meta/:type/:name/diff), getAudit (sdk, the bare tail of client method meta.getAudit, bound to GET /api/v1/meta/:type/:name/audit), getReferences (sdk, the bare tail of client method meta.getReferences, bound to GET /api/v1/meta/:type/:name/references), meta.diffItem (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/diff, selected by route anchor /:type/:name/diff), meta.getAudit (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/audit, selected by route anchor /:type/:name/audit), meta.getReferences (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/references, selected by route anchor /meta/:type/:name/references))
  • content/docs/kernel/contracts/metadata-service.mdx (via getPublished (sdk, the bare tail of client method meta.getPublished, bound to GET /api/v1/meta/:type/:name/published; the bare tail of client method meta.getPublished, bound to GET /meta/:type/:name/published))
  • content/docs/ui/react-pages.mdx (via /:type/:name/references (route, a path literal in a comment on a changed line))
  • content/docs/upgrading.mdx (via fromVersion (literal, a string literal in META_ITEM_DIFF_PARAMS), toVersion (literal, a string literal in META_ITEM_DIFF_PARAMS))

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

  • content/docs/releases/v17/17-1.mdx (via /:type/:name/diff (route, a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-3.mdx (via /:type/:name/published (route, a path literal in a comment on a changed line))
  • content/docs/releases/v17/17-5.mdx (via getAudit (sdk, the bare tail of client method meta.getAudit, bound to GET /api/v1/meta/:type/:name/audit), meta.getAudit (sdk, the route ledger binds it to GET /api/v1/meta/:type/:name/audit, selected by route anchor /:type/:name/audit), /:type/:name/diff (route, a path literal in a comment on a changed line))

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
  • 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 — 17 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 dc93781c1947b53cff50a550ad551e462a464594 → packageMentionDocs.

Which tree this was computed on

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

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

⚠️ 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 dc93781c1947b53cff50a550ad551e462a464594 → 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: d813364338e8cf0bd674c93bcd2d4c5865f2b594
Local-runs: none

Rendered 2026-10-13T04:40Z by the adopting seat's session, read-only: the three-dot diff against origin/main (5 files, +351 / -3), the card thread (triage 6129664008, serial notes 6130072486 and 6134242418, claim 6136144688, report 6137337775, and the ACCEPT 6138208712 read as a claim to test), ruling 6116165299, PR #23070 (038d4c9588), the head's check-runs, packages/client/src/index.ts, and objectui at the console pin 1fb1ced36243 (a blobless read of the pinned tree). Nothing was built, run or re-run.

① Derived judgments

Every accept-set and public-surface change the diff implies, judged against each handler's ACTUAL read points at this head (packages/rest/src/rest-server.ts), every helper that takes req or req.query included.

  1. PUT /meta/:type/:name = force, package, mode — right. Reads: req.query?.force (:7185), metaItemPackageBinding(req.query?.package) (:7194), and mode a frame down in metaSaveRequestOptions (meta-save-request.ts:118, that module's only query read). resolveExecCtx / computeExecCtx, walledCatalogRefusal, resolveProtocol / resolveRequestEnvironmentId (route, hostname, X-Environment-Id header) and resolveMetaWriteActor read no query key, and no token-in-query auth read exists anywhere in the file. No admitted name is inert; no read name is omitted. member is refused, never read — body-addressed, as ruled.
  2. /references = empty — right. The handler reads req.params only; resolveProtocol reads no query; findReferencesToMeta is asked of type and name alone.
  3. /layers = package — right. Read a frame down in serveMetaItemLayered (:4083). resolveObjectMasker reads the session; createMetaLayeredAnswer takes MetaItemReadGateSources (resolveCaller, resolveSecurityService, listMetaItems, serviceProbe, navPruneLogged), which carries no requestLocale, and the layered view is "not translated and not cached" by design — so locale is correctly NOT in this set.
  4. /audit = limit — right. readDeclaredQueryNumber(req.query, 'limit', …) (:7663); refuseNonAuthoringCaller, eventDoorRefusal, fetchCurrentMetaDocument and metaItemReadGate read the session and the store, no query.
  5. /diff = from, fromVersion, to, toVersion — right. Both spellings are read (:8097-:8102); the mask posture, the current-document fetch and the gate read no query.
  6. /published = empty — right. resolveObjectMasker, resolveProtocol, getMetaItemLayered({ type, name }), metaItemReadGate, resolveMetadataService and getPublished(type, name) read no query key.
  7. Where each refusal sits — right, and it matches the ruled doors. PUT: after the capability 403, the walled-catalog 403 and the 501, before resolveMetaWriteActor and saveMetaItem (the DELETE / publish / rollback precedent) — nothing is written, and nothing a prior gate refuses is leaked. /audit and /diff: after refuseNonAuthoringCaller and the 501, before the per-caller gate and any read (the /history precedent). /references, /layers, /published: first statement, before the protocol, the masker and the read gate (the ruled item GET's placement). The refusal body is static — the unknown name and the set — so none of the six becomes an existence or capability oracle. Recognition precedes each door's refuseRepeatedQueryParams, both in the nested VALIDATION_ERROR envelope: no new dialect.
  8. Caller census — holds, spot-checked. SDK (packages/client/src/index.ts): metaSaveQuery emits only force, package, mode (:787-:800); getAudit limit (:2170); diffItem from, to (:2232-:2233); getReferences / getPublished send nothing; the SDK has no /layers caller. objectui at 1fb1ced36243, packages/data-objectstack/src/metadata-client.ts: save force / mode / package (:1228-:1230), layered package (:1461), references nothing (:1602), diff from / to (:1732-:1733), audit limit (:1774), no /published caller; the two direct fetch PUTs (preview/UnpublishedAppBar.tsx:89, metadata-admin/external/api.ts:247) carry no query; preview/DraftChangesPanel.tsx:202 is the item GET (state, package), not one of these doors. In-repo: every dogfood PUT with a query sends package, mode or force; client.test.ts sends audit?limit=20 and diff?from=2&to=5; the ?include=, ?packageId= and &severity= spellings in the tree are on /meta/doc, /meta/_drafts and /meta/diagnostics. CI's six Test Core shards and the dogfood gate are green on this head, which answers the rest and runtime suites in full.
  9. The split census test — the security property is kept and made loud. The body case still pins that organization_id / organizationId in the body never reach saveMetaItem (200, the key absent — the request is built from named req values, never a body spread). The query case now pins the located 400 VALIDATION_ERROR with saveMetaItem never called, which is stronger than watching the name get dropped. Nothing the old case asserted is unasserted now.
  10. unknownQueryParamMessage — right. The subject sentence and the middle clause are untouched; for a non-empty set the tail is the same Supported parameters: a, b. template, byte for byte; only supported.length === 0 takes the new sentence. The three existing pins of the message are not in the diff. These are the first empty sets in the repo, so the old Supported parameters: . had no prior reader to break.
  11. The five ruled doors — unchanged. META_ITEM_READ/RESET/HISTORY/PUBLISH/ROLLBACK_PARAMS (:667-:701) are identical on origin/main and the head; rest-server-meta-member-selector.test.ts is not in the diff; no packages/runtime, client, spec or metadata-protocol file changes.
  12. The changeset's text — truthful, door by door. PUT's set and the body-addressed remedy (ruling Q2 i); /layers shows the canonical member's layers (serveMetaItemLayered passes no member and GetMetaItemLayeredRequest declares none) and the GET ?member= remedy exists (feat(metadata-protocol): one language of an email template is addressed by name — the by-name /meta doors accept a bundle member selector (#22877, stage 2) #23070); /audit's "every member's attempts in one trail" is true (sys-metadata-audit.object.ts declares type, name, organization_id, operation, outcome, … and no member column); /diff's /history?member= remedy exists (feat(metadata-protocol): one language of an email template is addressed by name — the by-name /meta doors accept a bundle member selector (#22877, stage 2) #23070); /published and /references take none and the refusal sentence is quoted exactly; the envelope and the "SDK and Studio client send only accepted names" sentence are both measured above. What the changeset MIS-DECLARES is in ②.

② Semver level

Wrong — this is the FAIL: the changeset under-declares a breaking accept-set narrowing.

Remedy for the next head — .changeset/23072-meta-doors-closed-sets.md, plus the PR body's line 2: '@objectstack/rest': minor; Clause-②: no (narrowing); exactly one ADR-0087 disposition marker in the body, in the HTML-comment form the #20139 / #20534 entries carry (adr-0087: CATEGORY reason; pnpm check:adr-0087-registration prints the category set when it fails — the import-door precedent's not-required (no-migration-prescription) fits a change that removes no authorable key, export or stored shape). The per-door remedies already in the body are the migration text step 3 asks for and stay as written. No source file needs to move.

③ Boundary flags

  • Part of, not Fixes — right: the dispatcher half is open, and "Part-of PR must not also close its card" is green.
  • query-allowlist.ts outside the claimed surface — judged in ① item 10; the seat amended the surface in 6138208712. No second refusal dialect was minted.
  • The census test fixed — ① item 9.
  • No runtime change — verified: packages/runtime/src/domains/meta.ts is byte-identical to origin/main and to the base bcebd8409d; the changeset correctly omits @objectstack/runtime.
  • Not merged with the latest main — mergeable_state is clean now; CI ran on the merge ref 3e63880e8b; the queue re-merges. No action.
  • Two measured heads — rest-server.ts and query-allowlist.ts are byte-identical between f0f77809f4 and d813364338 (verified). The report's "adds only the census pin's split" is one file short: the changeset file also landed in between, which changes nothing measured.
  • Four lock timeouts; the whole rest suite not re-run at the final head — declared, and answered by the head's check-runs: Test Core 1/6 to 6/6 and the aggregate are success, as are every Lint, Type Check, Check Changeset, Governed Surface Queue Guard and Dogfood Regression Gate run (latest run per name); Console Pin Gate and the live-database gates are skipped, as this diff's paths warrant.
  • open_questions[0] (the dispatcher half: A / B / C) — not ruled here; it is the maintainer's. Landing this half first leaves the dispatcher exactly as today: its item GET still drops member and declares no set, and its /layers (meta.ts:1054), /published (:1094) and PUT (:1266) branches are untouched. No caller of the dispatcher sees any change; what grows is the count of doors on which the two transports answer an unknown name differently (six more), the split feat(metadata-protocol): one language of an email template is addressed by name — the by-name /meta doors accept a bundle member selector (#22877, stage 2) #23070 already opened on the item GET. No worse than today.
  • out_of_scope_findings[0] (the dispatcher's /layers, /published and PUT have no closed set either) — recorded on the card in 6137337775 and adopted into the dispatcher half by the ACCEPT. One ask for the seat: the card body names only the item GET, so the A / B / C decision put to the maintainer should name those three branches explicitly, or they get a card of their own — Prime Directive chore: version packages #10 wants a defect named where it will be acted on, not implied. [1] (the cloud host's kernel-resolver is not in this tree) — noted; no in-repo resolver reads a query key; no action.
  • Docs — the drift check lists four hand-written pages by anchor; none advertises a query name these doors now refuse (the five sub-paths with a ? have no hit in content/docs). Advisory only.

Everything in ① and ③ holds; ② alone fails this head. A new head carrying the three changeset edits and no other change can be re-recorded against ① and ③ as written here.

Implemented-by: claude/issue-23072-meta-doors-closed-sets
Reviewed-by: session_01VoSxBQujKLZKPwK2u5ehQ6

VERDICT: FAIL


Generated by Claude Code

…ept-set narrowing

Grade `@objectstack/rest` `minor`, carry the `fix(rest)!:` summary line,
declare `Clause-②: no (narrowing)`, state the ADR-0087 disposition
`not-required (no-migration-prescription)` with its facts, and add the
launch-window BREAKING line. The per-door remedies are unchanged.

Claude-Session: https://claude.ai/code/session_01VoSxBQujKLZKPwK2u5ehQ6
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot changed the title fix(rest): PUT and the five by-name /meta sub-doors declare closed query sets, so ?member= is refused, never dropped (#23072, RestServer half) fix(rest)!: PUT and the five by-name /meta sub-doors declare closed query sets, so ?member= is refused, never dropped (#23072, RestServer half) Oct 13, 2026

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants