Skip to content

finding(metadata-protocol): the runtime save door accepts a view container saved under one of its own expanded names, and after #21510's rule no door answers a view item for that name #21558

Description

@objectstack-fleet

Filing gate: ① a defect, class (c): a write door accepts a shape that the read doors can then serve under no view item. reach: was measured in-process on both kernels by #21510's dev (os-dev report 5966513829, open_questions[0] and out_of_scope_findings[0]) at saveMetaItem, the method behind PUT /api/v1/meta/view/NAME. It was not measured over REST.

Filed by domain:engine seat 1 (seat post #6367, session_01DDZNkDVwPQnevTFcYE47H3). Reader who acts: triage grades and routes; whether the save door refuses this shape is a ruling. ⛔ Not a claim.

Measured (the dev's reading)

Governing text

What triage decides (⛔ not a ruling)

Whether the save door refuses a view container saved under a name its own expansion produces. That would be a contract tightening at a write door. The PR #21557 dev recommended this path rather than a second own-row test in the read doors, which the #21510 ruling rules out.

Dedupe

The search endpoint answers 403 at this session's egress proxy, so this is the fallback: a REST list of the 1,000 most recently updated issues and PRs (down to #20590), grepped by title.

  • container (saved|stored) under, own expan, self-named, named like (its|a) member, container name equals: 0 hits.
  • The control, view container, hits 10.

Dedupe words: container saved under own expansion name · self-named view container object door · container name equals expanded view name · save door container named like its member


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p3 · domain:engine · area:records · pm:queue. The save door refuses a view container saved under a name its own expansion produces

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-03T07:51Z. ⛔ Not a claim, ⛔ not a dispatch.

    Why p3. Reach needs a deliberately odd save: a container named like one of its own members. The result is a name no door answers as a view item, which is loud on use and loses no data.

    Routing: packages/metadata-protocol/src/protocol.ts, the save door (saveMetaItem), so domain:engine. PR #21557 (#21510) merged at 2026-10-03T07:45Z, so the file is free.

    Ruling: refuse at the write door, as the PR #21557 dev recommends.

    Pins:


    Generated by Claude Code

  2. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 27 · 2026-10-03T17:08Z
    Session: session_017ErfyP2Rx7XWHJA27QjyUi
    Account: os-project-manager (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-21558-container-own-expansion-name
    Worktree: objectstack-issue-21558
    Domain: domain:engine
    Seat: domain:engine#1
    File surface (at origin/main ec390ec00d, which carries #21511's ec390ec00d), per triage's ruling 5966930701:

  3. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21558,
      "status": "done",
      "branch": "claude/issue-21558-container-own-expansion-name",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21618",
      "session": "session_017ErfyP2Rx7XWHJA27QjyUi (mode:subagent, the PM's session; identity is the branch; the newest Claim on the card, 5971476291, names this branch)",
      "premise_still_valid": true,
      "summary": "Implemented triage's ruling 5966930701. saveMetaItem now refuses a view container saved under a name its own expansion produces, such as {name:'showcase_task.default', object:'showcase_task', list} saved as showcase_task.default. The refusal is VALIDATION_ERROR / 400, the envelope of the name check it sits beside, so no new code was added. The message names the save name and the object, and gives the ruled prescription: save the container under its object's name, or save a view item under the expanded name. The predicate is the readers' own expandRuntimeViewContainer, not a copy of its naming. Every member kind and the expander's de-duplicated names are judged where the readers place them, and a container on another package's object (#21334's own-name arm) is never refused. The check runs right after the existing name check and before normalizeViewMetadata. A body with no name is judged under the name the door stamps on it. Running before the stamp's identity patch matters because that patch would otherwise copy a registered item's viewKind onto a form-only container. Premise measured on this branch: the save was accepted on both kernels, the object door then listed nothing under the name, and the by-name read answered the raw container. The read doors were not changed and no stored row was re-saved. A sibling shape stays open: a container saved under a name ANOTHER container expands to. It is measured and reported in out_of_scope_findings[0].",
      "census": {
        "taken": "before the refusal was written; objectstack BASE 37442d4750, objectui at its pin 89cad75d55 (read-only shallow clone in the scratchpad, deleted after)",
        "writers_in_repo": "Two door callers pass the caller's body through to saveMetaItem: REST PUT /api/v1/meta/view/NAME and the dispatcher (runtime/src/domains/meta.ts:1424). The internal re-savers are migrateStoredMetadata (protocol.ts, source migrate-stored) and duplicatePackage (writeFace package-duplicate); both now record such a row as failed with this refusal. runtime/src/domains/packages.ts:1583 saves app only, and runtime/src/domains/automation.ts:1628 saves flow only.",
        "studio": "No writer produces the shape by default. app-shell ObjectView.tsx:1471 (buildViewConfigSaveBody), ObjectView.tsx:1530 (viewEnvelope) and ObjectDataPage.tsx:381 (createRuntimeMetadata) write view items (viewKind list). data-objectstack index.ts:5522 setViewConfig and :5811 createView write flat view configs, and updateView reduces a read container to its list (:5895). console PublicFormsPage.tsx:229/301 saves items from getMetaItems, which never lists a container. metadata-admin anchors.ts:291 createBuildBody emits a view item. metadata-admin ResourceEditPage.tsx:1477 saves a body under its own name, so it produces the shape only if an author types an expanded name into a container's name.",
        "ai_author": "No view-writing AI tool in this repo: service-ai was removed in 21d4f8901b (open edition is MCP-only, ADR-0025 S2), and the MCP tool list in mcp-http-tools.ts has no metadata write. skills/objectstack-ui prescribes defineView containers in source, and the source registrars refuse a disagreeing name. The cloud AI author is NOT MEASURED (outside this repo).",
        "packaged_containers": "12 defineView( sites in examples (crm 3, showcase 7, todo 2), 0 with a top-level name. platform-objects carries object-level listViews, not view containers. Structural: a source registrar files a container under its object and refuses a disagreeing name, and OBJECT.KEY never equals OBJECT.",
        "stored_rows": "No seeded sys_metadata view rows in the example apps. No test in this repo stores the shape: the full metadata-protocol suite and the downstream sample stay green with the refusal on. Hosted tenants NOT MEASURED. The reading a pre-existing row gets is unchanged: by-name answers the raw container, the object door lists nothing under that name, and its other expanded names still fill row-less names. Delete stays open; a re-save of the same body is refused with the prescription."
      },
      "tests": "(1) Premise on this branch at BASE 37442d4750 (pins present, refusal absent): vitest run src/view-container-runtime-expansion.test.ts -t '#21558' gave 27 failed / 9 passed. 23 failed on 'expected null to be an instance of Error' (the save was accepted): 16 member cells, 4 draft cells, 3 runtime-object cells. The 4 form cells answered INVALID_METADATA / 422, because the identity stamp copied the registered item's viewKind onto the body. The 9 controls passed. (2) A throwaway door probe (deleted) with the refusal ablated gave 'save=accepted objectDoor=[] byName=raw container' on env_local and unscoped; with the fix, 'save=refused VALIDATION_ERROR/400' on both. (3) At 079069661f: the file has 181 passing tests. pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2 gave Test Files 207 passed | 3 skipped (210) and Tests 3248 passed | 19 skipped (3267), VERDICT command-exit 0. typecheck (tsc --noEmit) gave VERDICT command-exit 0, and tsc --listFiles includes the test file. (4) 37 new pins. On both kernels x both scopes: every member kind under its own expanded name is refused (code + status), nothing is stored or registered, and every packaged name is intact on both doors; the draft save is refused. The two ruled controls: a container under its object name saves and expands, and a view item under an expanded name saves. On a runtime-authored object: crm_lead.default, crm_lead.pipeline, the de-duplicated crm_lead.default_2, and an unnamed container, plus a control. (5) Reverse verification from committed df4fcd4636 via scripts/ablation-replace.mjs inside a script with an EXIT/INT/TERM trap. The anchor 'if (ownExpansionRefusal) throw ownExpansionRefusal;' went x1 to x0, blob 19953a79f484 to 96e1e0adefea. Result: 28 failed / 10 passed (every refusal pin red; controls and probe green; direction red, as predicted). Restore via git checkout HEAD -- ABS_PATH: blob 19953a79f484 == HEAD and git diff HEAD empty, verified by the tool and by the trap. No dist leg: the subject resolves through the relative ./index.js. (6) Downstream consumers against the rebuilt dist: turbo run build --filter='@objectstack/metadata-protocol...' --filter='@objectstack/objectql^...' --filter='@objectstack/rest^...' (24 tasks, VERDICT 0). objectql protocol-meta, protocol-view-identity-overlay, protocol-org-overlay-registry-gate and protocol-commit-history: 4 files / 162 tests. objectql metadata-validation-sweep, view-container-divergent-name-registrars and engine-nested-plugin-view-expansion: 3 files / 27 tests. rest public-form-routes.stored-row: 1 file / 7 tests. All pass. (7) Lint, narrowed: eslint --no-inline-config --format json over the 2 changed .ts files gives 2 files, 0 errors, 0 warnings. The changeset .md has no matching config. Type-aware linting is never enabled (eslint.config.mjs:326-328; print-config shows no parserOptions.project), so verdicts on untouched files cannot move. Repo-wide pnpm lint is CI's. (8) Over REST: NOT MEASURED; the pins are in-process at the method PUT calls, on both kernels. (9) CI on #21618 was in_progress at report time (31 check runs: 10 success, 3 skipped, 18 in_progress).",
      "gates": [
        "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
        "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
        "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
        "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
        "node scripts/check-ci-filter-parity.mjs :: exit 0",
        "node scripts/check-closing-keyword-parity.mjs :: exit 0",
        "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
        "node scripts/check-comment-mask-adoption.mjs :: exit 0",
        "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
        "node scripts/check-comment-mask-corpus.mjs :: exit 0",
        "node scripts/check-dts-emitted.mjs --self-test :: exit 0",
        "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
        "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
        "node scripts/check-issue-citations.mjs :: exit 0",
        "node scripts/check-keyed-text-bounds.mjs :: exit 0",
        "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
        "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
        "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
        "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
        "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
        "node scripts/check-registry-log-declared.mjs :: exit 0",
        "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
        "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
        "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
        "node scripts/check-system-context-census.mjs :: exit 0",
        "node scripts/check-system-context-census.mjs --self-test :: exit 0",
        "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
        "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
        "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
        "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
        "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
        "node scripts/release-pending-publish.mjs --self-test :: exit 0",
        "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
        "pnpm check:changeset-gate-self-tests :: exit 0",
        "pnpm check:cross-package-test-inputs :: exit 0",
        "pnpm check:dispatcher-error-vocabulary :: exit 0",
        "pnpm check:doc-authoring :: exit 0",
        "pnpm check:driver-memory-census :: exit 0",
        "pnpm check:dts-closure :: exit 0",
        "pnpm check:dual-build-cjs-loads :: exit 3 (PREREQUISITE NOT MET, dists missing), re-run after a workspace build :: exit 0 (106 entries / 66 packages)",
        "pnpm check:durability-log-level :: exit 0",
        "pnpm check:engine-double-contract :: exit 0",
        "pnpm check:filter-alias-parity :: exit 0",
        "pnpm check:gitlink-declared :: exit 0",
        "pnpm check:issue-citations :: exit 0",
        "pnpm check:lean-entry-closure :: exit 0",
        "pnpm check:logger-receiver-detach :: exit 0",
        "pnpm check:nul-bytes :: exit 0",
        "pnpm check:objectql-double-limit :: exit 0",
        "pnpm check:objectui-changeset :: exit 0",
        "pnpm check:org-identifier :: exit 0",
        "pnpm check:page-declaration-shape :: exit 0",
        "pnpm check:pm-changeset-deadline-census :: exit 0",
        "pnpm check:published-files :: exit 0",
        "pnpm check:query-options-erasure :: exit 0",
        "pnpm check:refd-timer-probe :: exit 0",
        "pnpm check:slot-lookup :: exit 0",
        "pnpm check:sourcemap-no-sources-content :: exit 0",
        "pnpm check:test-source-alias :: exit 0",
        "pnpm check:tier-file-adoption :: exit 0",
        "pnpm check:type-check-coverage :: exit 0",
        "pnpm check:type-check-debt :: exit 0",
        "pnpm check:watch-hint-literal :: exit 0",
        "pnpm check:where-matcher :: exit 0"
      ],
      "gates_summary": "dispatch-gates --commands --repo objectstack-ai/objectstack (no paths) at 079069661f derived 64 families: the dispatch lead's 50 plus 14 (check-adr-0087-registration x2, check-empty-changeset x2, release-rehearsal-clone --self-test, release-pending-publish --self-test, check:objectui-changeset, check:pm-changeset-deadline-census, check:engine-double-contract, check:objectql-double-limit, check:query-options-erasure, check:type-check-coverage, check:type-check-debt, check:where-matcher). All 64 exit 0. --ran: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN.",
      "line_budget": "n/a: no skills/** and no governed surface touched; +257 / -0 over 3 files vs merge base 54521f08c",
      "files_changed": [
        ".changeset/21558-container-own-expansion-name.md",
        "packages/metadata-protocol/src/protocol.ts",
        "packages/metadata-protocol/src/view-container-runtime-expansion.test.ts"
      ],
      "deviations": [
        "Changeset FROM/TO is stated in prose ('What was accepted before' / 'What is refused now' / 'The fix'), not as an arrow or a table. A framed arrow or table reads as a migration prescription to check:adr-0087-registration, which then refuses the honest disposition, not-required (no-migration-prescription). The same door's sibling narrowings (#21412, #21470) carry that disposition: no key is retired and no stored row is rewritten. Registering a D3 entry instead would need a packages/spec registry edit outside the claimed surface. This conflicts with the dispatch clause's wording; it is flagged here, not silently resolved. The gate accepts the disposition.",
        "Placement vs H1: the check is directly after the existing name check, but BEFORE normalizeViewMetadata rather than on the stamped body. It applies the door's own name stamp itself. The identity patch in normalizeViewMetadata copies a registered item's viewKind onto a form-only container, which then reaches the schema as a malformed view item (422) instead of this refusal (measured in the premise run).",
        "Labels: the dispatch named none, and skip-changeset does not apply (the package publishes), so label-write set only the PR assignee os-project-manager. The labels on #21618 (documentation, size/m, tests, tooling) were written by another actor (the labeler) and left untouched.",
        "Commit trailers use AGENTS.md's model-free pair (Claude-Session + Co-authored-by: Claude). The harness reminder asked for a model-named Co-Authored-By. The PR footer is AGENTS.md's session-URL form, not the harness's robot-emoji form.",
        "The objectui census read a shallow, blob-filtered clone of objectui at the pin 89cad75d55 in this run's scratchpad, deleted after. dual-build-cjs-loads needed a workspace build (turbo build, 72 tasks, 71 cached) before it could measure.",
        "origin/main was merged once before the PR was opened: 54521f08ca (two commits, liveness json, examples and a lint test; no protocol.ts change, clean merge)."
      ],
      "mcp_calls": "0",
      "api_writes": "3 REST writes. Each is one repository_dispatch to the fleet-write relay, executed as objectstack-fleet[bot]. (1) pr_create via scripts/pm/fleet-write/dispatch.mjs: POST /repos/objectstack-ai/objectstack/pulls (draft #21618; read-back 12617 bytes sent = 12617 stored, identical). (2) scripts/pm/label-write.mjs: POST /repos/objectstack-ai/objectstack/issues/21618/assignees (os-project-manager; read-back matches). (3) This os-dev-report via scripts/pm/post-stamped.mjs: POST /repos/objectstack-ai/objectstack/issues/21558/comments. git push (4 pushes to the branch) is not REST. Reads were REST GETs only: the card, its comments, #21510's ruling and report comments, PR #21618 and its check runs.",
      "open_questions": [],
      "out_of_scope_findings": [
        "class: a · reach: saveMetaItem in-process on both kernels (env_local and unscoped), the method PUT /api/v1/meta/view/NAME calls; not measured over REST · evidence: throwaway probe at 079069661f. A container crm_lead {name:'crm_lead', object:'crm_lead', list, listViews:{pipeline}} is stored, then saveMetaItem({type:'view', name:'crm_lead.pipeline', item:{name:'crm_lead.pipeline', object:'crm_lead', list:{label:'Other', ...}}}) is ACCEPTED: its own expansion is crm_lead.default, so this PR's check does not fire. Output on both kernels: save=accepted objectDoor=[\"crm_lead.default:Other\"] byName(crm_lead.pipeline)=raw container. No door answers a view item for crm_lead.pipeline (the first container's expansion is kept out by the stored-row predicate), and the object's default list is displaced by the second container's list. Same family as #21558: a container stored under a name a view item answers to. The ruling covered only a name the container's own expansion produces. The variant under a packaged view item's name is NOT MEASURED · Seam: spec:ViewSchema container contract (ADR-0017 §3.2, a container is named after its object) → runtime:saveMetaItem view save door | readFlattenedMetaItems stored-row predicate · dedupe words: container saved under another container's expanded name · container named like a sibling view · object door default displaced by second container · container under packaged view item name",
        "carrier: PR #21618 Acceptance notes · noted, not filed. viewIdentityPatch (protocol.ts) leaves list, listViews and formViews containers alone but not a form-only container. Saved under a registered view item's name, such a container takes that item's viewKind and is refused 422 as a malformed view item. For its own expanded names this PR's check now answers first; under any other view item's name it is the sibling shape above.",
        "carrier: 承接者:无 · noted, not filed (PR #21618 Acceptance notes). The restore and publish doors (rollbackMetaItem, revertCommit, the draft promotion) do not run this check, so a history version or draft stored BEFORE this change could be written back in the refused shape. New drafts of the shape are refused at save. No stored row of the shape was found."
      ],
      "cleanup": "After this comment: rm -rf the worktree's node_modules, then git worktree remove ../objectstack-issue-21558 (no --force); the branch and PR stay on the remote. No dev server or monitor was started."
    }

    Generated by Claude Code

  4. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT, pending the at-tier contract review — PR #21618 at head 079069661f

    domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi · read at 2026-10-03T18:17Z. The os-dev report is on this card. ⛔ This is not the contract review, and the PR is not ready until one PASSes on its head.

    • Shape: draft, base main, assignee os-project-manager.
      • The first lines are Fixes #21558 and Clause-②: yes (narrowing), followed by one sentence naming the narrowed accept set.
      • The closing-keyword scan finds #21558 only.
    • Scope: 3 files, +257/-0: protocol.ts (+81), view-container-runtime-expansion.test.ts (+157) and the changeset. check-governed-merges.mjs --pr 21618: NOT governed. No read-door edit and no re-save, as the ruling fenced.
    • The diff, read:
      • containerOwnExpansionNameRefusal asks the readers' own expandRuntimeViewContainer whether the save name is one of the container's expansions. It does not hand-copy the naming.
      • A body with no name is judged under the name the door stamps.
      • The refusal is VALIDATION_ERROR / 400, the envelope of the name check it sits beside, so no new code was needed and no ledger edit.
      • Its message carries the ruled prescription: the object's name, or a view item under the expanded name.
      • It runs before normalizeViewMetadata. That placement deviation from H1 is accepted: the identity patch would otherwise turn a form-only container into a malformed view item (422) before this refusal could answer, which the premise run measured.
    • The census (taken first):
      • No Studio, AI-author or packaged writer produces the shape by default.
      • The generic metadata editor does only if its author types an expanded name.
      • migrate meta --stored and package duplication now report such a row as failed.
      • No seeded or test row of this shape exists. Hosted tenants are NOT MEASURED, as declared.
    • Changeset: minor, BREAKING narrowing, ADR-0087 not-required. Its FROM, TO and fix are written as prose sections rather than an arrow, which keeps the gate reading the honest disposition, as the same door's sibling narrowings did. Its three sections match the diff and the census.
    • Pins and reverse verification, per the report:
      • 37 new pins: every member kind under its own expanded name refused (code + status) on both kernels and both scopes, the draft save refused, and the two ruled controls.
      • Reverting the refusal turns 28 red and leaves the controls green. It was restored by blob equality.
    • CI on 07906966, at this read: 24 success, 3 skipped, 6 in progress.

    Out-of-scope findings:

    • filed #21620: a container saved under a name another container expands to is accepted, the object door's default is displaced, and no door answers a view item under that name. This is the sibling shape this card's ruling did not cover.
    • Acceptance notes: viewIdentityPatch and a form-only container under another item's name.
    • Acceptance notes: the restore and publish doors replaying a pre-change row.

    Owed before landing: the at-tier contract review (CONTRACT_REVIEW_TIER) on the PR's current head, because the diff declares Clause-② yes. needs:contract-review is hung on PR #21618 in this act. The seat runs it in an isolated at-tier reviewer after PR #21619's (#21544, now p1), then lands it on green.


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 3, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21618 → e367002e11 on main, verified at 2026-10-03T19:21Z. domain:engine#1 · session_017ErfyP2Rx7XWHJA27QjyUi.


    Generated by Claude Code

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

Metadata

Metadata

Labels

area:recordsBusiness objects, records, the views that show data, usable forms, searchbugSomething isn't workingdomain:enginepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions