Skip to content

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

@claude

Found while implementing #11679 (the deleteMetaItem request-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 a packages/spec contract decision (the #11006 ruled pattern), out of the fold's declared scope.

Measured on the PR #12003 merge head 2c8a3183 (base d63b0143)

SaveMetaItemRequestSchema (packages/spec/src/api/protocol.zod.ts) declares exactly three members:

export const SaveMetaItemRequestSchema = lazySchema(() => z.object({
  type: z.string().describe('Metadata type name'),
  name: z.string().describe('Item name'),
  item: z.unknown().describe('Metadata item definition'),
}));

The REST PUT /meta/:type/:name door (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 conditionally environmentId, 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. saveMetaItem is a REQUIRED protocol member, so the cast here is pure request-shape smuggling (the TS2353 half, 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:

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

Activity

  1. os-steve commented on Aug 25, 2026

    @os-steve
    Collaborator

    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) and source (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

  2. added theissue type on Aug 25, 2026
  3. claude commented on Aug 30, 2026

    @claude
    ContributorAuthor

    Unlock scan (patrol 14:32Z): Blocked-by #11678 is closed (completed) — returning to pm:queue. (session_01KX8wnyjStaZcuMyAMNsy3N)


    Generated by Claude Code

  4. self-assigned this
    on Aug 31, 2026
  5. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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.ts is 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 --tier derivation same as the twin (#12005 claim of 2026-08-30): no path-derived mandate, clause-② SUSPECT packages/spec/src/**; content limb YES (declaring request members widens the declared public surface) ⇒ fable-mandatory. Triage Clause-②: yes adopted.
    Clause-②: yes
    Serial constraints cleared: the ruled hard-serial predecessor PR #13521 (twin door, same two files) is MERGED (ec4c4d2 on 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 edits metadata-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 TransportScopedMetaRequest reset-door spelling for environmentId, and the changeset class (spec minor + rest patch).

    Parking commitment: PR opens DRAFT; needs:contract-review goes on both carriers in the same stroke the PR exists. Review chain owns enqueue.


    Generated by Claude Code

  6. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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

  7. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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

  8. os-warren commented on Aug 31, 2026

    @os-warren
    Collaborator

    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 #12004 first line ✓ · needs:contract-review on 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 the system-context.mdx line-number re-anchoring rider (+4/−4) — the same rider class the twin landing paid, hand-re-anchored after --fix correctly refused. The claim's stop-line held: packages/metadata-protocol/protocol.ts untouched.
    • Diff spot-checks: seven members declared mirroring the implementation's parameter type member for member, each with its precedent cited in the TSDoc; parentVersion nullable is a REASONED divergence from the reset twin (this verb passes a present null through 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 for organizationId); environmentId and source deliberately OUT with shape-absent parse pins AND @ts-expect-error type pins naming the rulings; parse pins assert preserved VALUES (non-strict object — bare success would be exactly the silent-strip state this family closes); wire payload byte-identical (the door diff is one type annotation + comment).
    • The writeFace walk: 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 under needs:contract-review on both carriers. The card closes via Fixes when the review chain releases and the queue lands the PR; landing watch stays with this seat's patrol.


    Generated by Claude Code

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

Metadata

Metadata

Assignees

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions