Repository navigation
saveMetaItem is a declared REQUIRED member whose request schema declares 3 of the ~11 members the REST PUT door sends, so the call-site literal is cast as any #12004
Description
Activity
Triage:
domain:spec,pm:blocked, type Task.Blocked-by: #11678
Same grading basis as #12005 (the #11006 option-B ruled pattern, executed by #11426/#11679): the straight-lineage members (
organizationId/parentVersion/actor/force/packageId/mode) are inheritance, not a new decision. Two members carry their own verdicts and must not be smuggled:writeFace(server-stated per #10888 — needs the four-axis walk; if it does not converge during implementation, fork that one member to a decision card and land the rest) andsource(never sent by this door — the #11426 publish precedent leaves it undeclared unless a producer pulls it). Hard serial behind in-flight PR #12003 (same files). Fold candidate with #12005 per that card's triage comment. Clause-②: yes — contract-review tier applies.
Generated by Claude Code
claude commented
on Aug 30, 2026 claudeboton Aug 30, 2026 – with ClaudeContributorAuthorMore actionsUnlock scan (patrol 14:32Z): Blocked-by #11678 is closed (completed) — returning to
pm:queue. (session_01KX8wnyjStaZcuMyAMNsy3N)
Generated by Claude Code
Claim: PM loop round 1 (2026-08-31 shift)
Session:session_01PBjwYLS6BciTQW3c9xQiD2
Branch:claude/issue-12004-save-meta-item-request-schema
Worktree:objectstack-issue-12004
Domain:domain:spec
File surface:packages/spec/src/api/protocol.zod.ts(SaveMetaItemRequestSchema + member declarations),packages/rest/src/rest-server.ts(PUT /meta/:type/:name door de-cast), spec/rest test files for these members,.changeset/*.packages/metadata-protocol/src/protocol.tsis READ-ONLY mirror source — editing it is a stop-and-report breach (it is also in-flight #13382's claim area) (stop on breach; explain in the report)
Container & model: M,mode:subagent,model: claude-fable-5—dispatch-gates.mjs --tierderivation same as the twin (#12005 claim of 2026-08-30): no path-derived mandate, clause-② SUSPECTpackages/spec/src/**; content limb YES (declaring request members widens the declared public surface) ⇒ fable-mandatory. TriageClause-②: yesadopted.
Clause-②: yes
Serial constraints cleared: the ruled hard-serial predecessor PR #13521 (twin door, same two files) is MERGED (ec4c4d2on origin/main) — the serialization that HELD this card is lifted, and its member-map is now in-tree precedent; my other two in-flight dispatches (#13405:spec/src/data/datasource-credential-redaction.ts+ service-datasource; #13540/#13531:spec/src/data/filter-logic-conformance.ts) are file-disjoint; cross-lane in-flight #13382 editsmetadata-protocol/src/protocol.ts(value normalisation) — no overlap with this card's edit surface and no assertion this card pins.Re-pricing note (post-#13521): this card got easier, not harder — the twin PR settled the request/response transcription pattern, the
TransportScopedMetaRequestreset-door spelling forenvironmentId, and the changeset class (spec minor + rest patch).Parking commitment: PR opens DRAFT;
needs:contract-reviewgoes on both carriers in the same stroke the PR exists. Review chain owns enqueue.
Generated by Claude Code
Claiming for implementation — session_01PBjwYLS6BciTQW3c9xQiD2, branch
claude/issue-12004-save-meta-item-request-schema(worktree from origin/main at 098a08f, which includes PR #13521's history-door precedent ec4c4d2). Following the #11006 option-B ruled pattern per the 2026-08-25 triage verdicts; clause-② handling (draft + needs:contract-review on both carriers).
Generated by Claude Code
os-dev-report
{ "issue": 12004, "status": "done", "branch": "claude/issue-12004-save-meta-item-request-schema", "pr": "https://github.com/objectstack-ai/objectstack/pull/13599", "premise_still_valid": true, "summary": "SaveMetaItemRequestSchema widened from 3 to 10 declared members, mirroring the implementation parameter type member for member (the #11006 option-B pattern, PR #13521 as nearest precedent); the REST PUT door literal de-cast through the TransportScopedMetaRequest wrapper with a byte-identical wire payload; clause-2 parking done (draft PR #13599 + needs:contract-review on both carriers, both read back). PM mechanism assumptions re-derived on my checkout: the cast sat at rest-server.ts:5823 (moved from ~:5630 by PR #13521, as predicted) and the implementation signature at protocol.ts:13450 (moved from :13140); schema still declared exactly type/name/item. One rider the twins also paid: the door diff shifted eight rest-server.ts isSystem anchors on the census page (+1 import line, +17 door block); --fix refused (as in the history landing), anchors re-pointed by hand, gate green. Merge-tree against fresh origin/main (fd543f91 base b9972720) is conflict-free; main's same-day churn to the same census page touches only security-plugin rows, disjoint from mine.", "member_map": { "organizationId": "DECLARED optional string — #11426/#11679 lineage; selects the ADR-0005 overlay partition + audit-row scope; org-scoped write of a non-overridable type refused 403 (#6190)", "parentVersion": "DECLARED optional nullable string — implementation declares string-or-null; unlike the reset twin (folds present null to current hash, so PR #12003 declared plain optional), save passes null through to the repository conflict check: null = first-write pin, absent = unpinned; nullability mirrors the implementation per the PR #13521 organizationId reasoning ('because that is the implementation parameter type')", "actor": "DECLARED optional string — #11679 lineage; history event recorded_by + audit row; REST door's one producer is the authenticated identity (#7749/#7941)", "force": "DECLARED optional boolean — #11426/#11679 lineage; destructive-change acknowledgement (?force=true); only object saves reach the diff (pinned in protocol.destructive-gate-reachable-types.test.ts)", "packageId": "DECLARED optional nullable string — ADR-0048; absent-vs-null distinction per the publish schema's documentation (#11426); named read-only base package refused", "mode": "DECLARED optional closed enum draft|publish — ADR-0005 lifecycle; implementation reads anything-but-draft as publish (legacy default)", "writeFace": "DECLARED optional closed enum package-duplicate|meta-envelope|meta-dispatch — own verdict, four-axis walk below; server-stated per #10888", "environmentId": "NOT DECLARED — transport-level (#9741 ruling); rides TransportScopedMetaRequest; pinned shape-absent in protocol.test.ts", "source": "NOT DECLARED — never sent by any door (sole producer is the implementation's own migrateStoredMetadata internal call); #11426 publish precedent leaves it out until a producer on this contract pulls it; pinned shape-absent in protocol.test.ts", "item": "UNTOUCHED but newly pinned — measured key-REQUIRED at parse on origin/main (Zod refuses the absent key; present explicit null parses, then the implementation's own #8818 guard refuses 400); test pins both halves so the behaviour stops being silent" }, "writeface_four_axis": "CONVERGED: declare. (1) Real need: three measured server-side producers (REST PUT door meta-envelope, runtime dispatcher meta-dispatch in domains/meta.ts, duplicatePackage internal package-duplicate) and two measured consumers (409 destructiveChangeRemedy, 422 specValidationFindings) branch on it. (2) Long-term soundness: the #9741 transport test answers OPPOSITE here — the implementation READS it, and one producer is not a transport at all; the wrapper's own contract forbids protocol members riding it ('a key that belongs to the request belongs in the spec schema'), and the face is sent unconditionally so the cast could never come off otherwise. (3) AI-hard-to-get-wrong: closed enum + describe() records the server-stated property (no door spreads a wire body into the request, so a client-sent face is never read) — the identical treatment the MERGED reset twin gives actor, so declaring does not make it client-authorable. (4) Startup scope: records what ships, adds zero capability. Not smuggled: the verdict is executed with its evidence in the schema TSDoc, the PR body, and a closed-vocabulary pin.", "decast_proof": "From the COMMITTED fix (7eb98b18): injected smuggledKey into the saveRequest literal (mutation confirmed on disk: grep -c = 1 for the injected text); rest tsc went red exactly as predicted — 'src/rest-server.ts(5831,25): error TS2353: Object literal may only specify known properties, and smuggledKey does not exist in type TransportScopedMetaRequest...' — and the error text prints the widened member list (organizationId?, parentVersion?: string|null, mode?: draft|publish, writeFace?...) proving the compile reads the REBUILT spec dist, not a cache. Restore leg: git checkout HEAD -- path (explicit HEAD), then proven by git diff HEAD empty AND worktree blob hash == HEAD blob hash (0920a4d9) AND zero grep hits. No dist-mediated ablation was needed: the mutation is a source-level type probe and rest tsc compiles spec via its built dist, which was rebuilt (34/34 declaration files) before the probe.", "tests": "All verdicts quoted from their gates' own output; heavy runs serialized through os-verify-lock (slot issue-12004). At 397d9eaf: spec build 'check-dts-emitted: @objectstack/spec - 34/34 declared declaration file(s) present.' VERDICT command-exit 0; spec typecheck 'check:test-typecheck: OK' VERDICT command-exit 0; spec targeted suites (protocol.test.ts + type-alias-convention.pin.test.ts) 'Test Files 2 passed (2) / Tests 181 passed (181)' — Iso135 isomorphism pin holds, pin count unchanged at 835 (no new schemas); turbo build of rest's 26-package graph 'Tasks: 25 successful, 25 total'; rest typecheck 'check:test-typecheck: OK' VERDICT command-exit 0; rest door suites 'Test Files 6 passed / Tests 110 passed' + 'Test Files 3 passed / Tests 261 passed'. Final-head union: the full derived-gate union (dispatch-gates.mjs run with NO paths from the worktree — its own stderr banner: gate list derived from the tree of objectstack-ai/objectstack at commit 397d9eaf — yielding 62 commands including check:nul-bytes) re-ran to completion at git rev-parse --short HEAD = 722b069f after a full 'turbo run build' (Tasks: 72 successful, 72 total): 60/62 exit-0 with per-gate exits captured pre-pipe; the three formerly build-prerequisite-blocked gates now measured green — check-dev-prereqs '67 package build artifacts present', spec check:skill-examples '260 prose examples type-check across 3 surfaces', check:dual-build-cjs-loads floors held (102/66/610/1 vs floors 90/58/520/1); census 'check-system-context-census: OK — 109 elevation read sites in 20 packages across 45 files, all anchored; 145 anchors resolve, 27 declared non-read.'; nul-bytes 'check-nul-bytes: OK (scanned 7533 text file(s) ... no raw ASCII control bytes)'; spec check:docs '230 generated files in sync with packages/spec'. The only two non-zero are exit-3 by-design non-measurements (next field). Merge-tree probe against fresh origin/main (base b9972720): conflict-free.", "gates_not_measured_locally": "check-test-completeness and pm/check-half-states exit 3 by their own design ('NOT a finding... nothing was measured') — CI shard attestation and the PM board sweep own them; repo-wide eslint (pnpm lint) is a CI-owned run per the local-verification contract and was not run or narrowed here.", "mcp_calls": "11 — claim comment, dedup positive control search, dedup search, created-at recency list, finding create, PR create, card label read, card label union-write, PR label write, card label read-back, report comment. Issue body + comments and the PR-label read-backs came through the zero-quota public-page payload channel; git carried everything else.", "deviations": [ "Channel switch declared: the repo-scoped REST probe answered 403 'GitHub access is not enabled for this session' (the exact gated-session signature), so all GitHub writes and the one dedup search went through MCP; card/comment reads and PR-label read-backs used the public-page payload channel (data-name= spelling on the PR side).", "Three derived gates (check-dev-prereqs, spec check:skill-examples, check:dual-build-cjs-loads) are full-workspace-build prerequisites; a full turbo build was run locally to measure them rather than leaving them prerequisite-failed.", "The census-anchor repair (content/docs/permissions/system-context.mdx, 8 anchors) is a rider commit the diff itself forced — the same rider the PR #13521 landing carried; scope otherwise unchanged.", "PR #13599 body reports first-pass gate results at 397d9eaf and points to this card report for the final-head union; the code diff is unchanged between 397d9eaf and final head (the only later commit is the docs anchor repair)." ], "files_changed": [ "packages/spec/src/api/protocol.zod.ts (+108/-3: widened SaveMetaItemRequestSchema + TSDoc with the two deliberate non-declarations)", "packages/spec/src/api/protocol.test.ts (+185: request mirror suite + member-shape type pins with ts-expect-error on environmentId/source/misspelt)", "packages/rest/src/rest-server.ts (+19/-2: import + named-const de-cast, wire byte-identical)", "packages/spec/authorable-surface/api.json (+7 member rows, regenerated)", "content/docs/references/api/protocol.mdx (+7 rows, gen:docs)", "content/docs/permissions/system-context.mdx (8 anchors re-pointed)", ".changeset/spec-save-meta-item-request-members.md (spec minor)", ".changeset/rest-meta-save-cast-retired.md (rest patch)" ], "open_questions": [], "blockers": [], "out_of_scope_findings": [ "filed as #13598: runtime packages domain reaches getMetaItems/saveMetaItem through an untyped (protocol as any) service handle — every key it sends is declared once this lands, so the residual cast is a service-resolution-seam typing question (sibling of closed #9846), not request-shape smuggling; finding label, unassigned" ] }Generated by Claude Code
Generated by Claude Code
PM verdict: ACCEPT — parked, handed to the review chain. (
session_01PBjwYLS6BciTQW3c9xQiD2)Verified on GitHub against the review checklist, not the report's self-description — PR #13599:
- Form: draft ✓ · base
main✓ ·Fixes #12004first line ✓ ·needs:contract-reviewon PR and card (both read back) ✓ · 8 changed files, all accounted for: schema + door + tests + changeset pair (spec minor + rest patch, mirroring the merged twin PR feat(spec): declare MetadataProtocol.historyMetaItem and de-cast the REST history door #13521's class), two generated artifacts (authorable-surface/api.json,references/api/protocol.mdx— regenerated by the gates, not hand-edited), and thesystem-context.mdxline-number re-anchoring rider (+4/−4) — the same rider class the twin landing paid, hand-re-anchored after--fixcorrectly refused. The claim's stop-line held:packages/metadata-protocol/protocol.tsuntouched. - Diff spot-checks: seven members declared mirroring the implementation's parameter type member for member, each with its precedent cited in the TSDoc;
parentVersionnullable is a REASONED divergence from the reset twin (this verb passes a presentnullthrough to the conflict check — the mirror-the-implementation rule, the same reasoning PR feat(spec): declare MetadataProtocol.historyMetaItem and de-cast the REST history door #13521 recorded fororganizationId);environmentIdandsourcedeliberately OUT with shape-absent parse pins AND@ts-expect-errortype pins naming the rulings; parse pins assert preserved VALUES (non-strict object — baresuccesswould be exactly the silent-strip state this family closes); wire payload byte-identical (the door diff is one type annotation + comment). - The
writeFacewalk: triage's instruction was walk-or-fork; it CONVERGED to declare on measured evidence (three server-side producers, two refusal renderers branching on it; the [finding] Meta-read request schemas still omit previewDrafts / state / environmentId, so the REST call-site casts survive the organizationId catch-up #9741 transport test answers opposite here since the implementation reads it) with the server-stated property recorded in the member's describe() and a closed-vocabulary pin — executed openly in schema TSDoc, PR body, and tests, not smuggled. Flagged here for the review chain to re-judge as the one member carrying its own verdict. - De-cast proof: predicted-red observed-red from the committed fix (TS2353 naming the injected key, error text printing the widened member list from the rebuilt dist — proving compilation against regenerated declarations, not a cache); restore proven by empty
git diff HEAD+ blob-hash equality. - Gates: quoted verdicts at both heads incl. the full 62-command union at final head
722b069f(60 exit-0; the two non-zero are by-design local non-measurements), census OK line, merge-tree probe conflict-free against fresh origin/main. - Out-of-scope finding: [finding] runtime packages domain reaches getMetaItems/saveMetaItem through an untyped
(protocol as any)service handle although every key it sends is declared #13598 filed unassigned (runtime service-handle cast seam) — correct routing, left for triage.
Enqueue is NOT this seat's: clause-② both limbs, dispatch tier
claude-fable-5, parked underneeds:contract-reviewon both carriers. The card closes viaFixeswhen the review chain releases and the queue lands the PR; landing watch stays with this seat's patrol.
Generated by Claude Code
- Form: draft ✓ · base
- added a commit that references this issue
on Sep 1, 2026 - added a commit that references this issue
on Sep 28, 2026 - added a commit that references this issue
on Oct 7, 2026
Found while implementing #11679 (the
deleteMetaItemrequest-schema catch-up; PR #12003) — the save door is the same request-shape gap one door over, and it is the biggest remaining one in the meta write family. Filed rather than touched: per-member protocol-vs-transport verdicts are apackages/speccontract decision (the #11006 ruled pattern), out of the fold's declared scope.Measured on the PR #12003 merge head
2c8a3183(based63b0143)SaveMetaItemRequestSchema(packages/spec/src/api/protocol.zod.ts) declares exactly three members:The REST
PUT /meta/:type/:namedoor (packages/rest/src/rest-server.ts,p.saveMetaItem({ ... } as any)— the closing cast sits at ~:5630 on that head) sends up to eleven:type,name,item,organizationId,writeFace: 'meta-envelope', and conditionallyenvironmentId,parentVersion,actor,force,packageId,mode: 'draft'.The implementation's parameter type (
ObjectStackProtocolImplementation.saveMetaItem,packages/metadata-protocol/src/protocol.ts:13140) declares{ type, name, item?, organizationId?, parentVersion?, actor?, force?, mode?, packageId?, source?, writeFace? }— so unlike the pre-#11679 delete door, every key the door sends IS read and enforced; only the spec contract is starved.saveMetaItemis a REQUIRED protocol member, so the cast here is pure request-shape smuggling (theTS2353half, as measured for the delete door in PR #12003), not feature detection.The decision (why filed, not fixed)
Same as #11679, on a bigger and subtler surface — per member, protocol vs transport:
environmentIdis already ruled transport-level ([finding] Meta-read request schemas still omit previewDrafts / state / environmentId, so the REST call-site casts survive the organizationId catch-up #9741, 2026-08-18) and ridesTransportScopedMetaRequest— OUT.writeFaceneeds its own verdict: the door comment records it is server-stated ("a client cannot smuggle a face in", saveMetaItem's spec-validation 422 message restates itsissues[]on the envelope, but message-only faces (duplicatePackagefailed[].error) forbid the #10524 trim until they declare a structured channel #10888) — an argument it is REST-layer coordination rather than a wire-authorable contract member; but it is read by the implementation and shapes the 422 envelope. Not obvious, needs the four-axis walk.sourceexists on the implementation but is never sent by this door — the publish precedent (feat(spec): declare publishMetaItem as an optional MetadataProtocol member with PublishMetaItemRequest #11426) would leave it undeclared unless another producer pulls it.organizationId/parentVersion/actor/force/packageId/modelook like straight feat(spec): declare publishMetaItem as an optional MetadataProtocol member with PublishMetaItemRequest #11426/deleteMetaItemis a declared member whose request schema declares 2 of the 8 members the REST reset door sends, so the call-site cast cannot come off #11679-lineage declares (each read and enforced;packageId's absent-vs-null distinction is already documented on the publish schema).Precedents: #11006 (maintainer ruling 2026-08-22, option B) · #11426 (publish request landing) · PR #12003 (#11678 audit + #11679 delete — the immediately preceding doors in this series).
⛔ Nothing about this door is addressed in PR #12003; its casts and payload are byte-identical there.
Generated by Claude Code