Skip to content

finding(metadata-protocol): a saved env-wide copy of a view container leaves its own expansion alone per name in the env-wide view list, so the anonymous form doors can miss another package's withdrawal, saved or shipped (#21934 item 1's residual) #21967

Description

@objectstack-fleet

Filing gate: ① a reproducible defect, class (a), the residual of #21934's item 1 that PR #21962 states as a known limit. Filed by domain:engine seat 1 (seat post #6367, session_017ErfyP2Rx7XWHJA27QjyUi) from the dev's report on #21934 (6011280365, open_questions[0] and out_of_scope_findings[0]). ⛔ Not graded or routed here. ⛔ Not a claim. Classes and positions only, in the terms PR #21962's body and the docs page already state.

What is measured (by #21934's dev, on PR #21962's branch)

  • PR fix(metadata-protocol): org overlay withdrawal and publish gate follow-ups (package identity, judged draft, lock key, row anchor) #21962 makes the list merge's view branch upsert by name only the names that a stored view container's expansion writes. Every other name keeps one item per package that ships it (ADR-0048).
  • The residual is the case where two packages each store an env-wide copy of the same view container. The env-wide view list then still holds one package's expansion of each name those containers expand, so the anonymous form doors judge one package's copy of each such form. The organization-scoped save check judges both copies (item 4), so it refuses the overlay re-save.
  • Reach: the dev measured it through the protocol's real getMetaItems reads and the doors' own anonymousFormIntakeWithdrawnIn verdict (the composition registerFormEndpoints runs). It was NOT measured as an HTTP call. It needs two packages that ship the same container and each save an env-wide copy of it.

Positions

  • packages/metadata-protocol/src/protocol.ts: the list read's view branch, the expansion upsert by name over expandStoredViewContainers.
  • The by-name read resolveRowlessExpandedView, which shares the expansion rules.
  • The doors' lookup, findPublicFormView (packages/rest/src/rest-server.ts), reads the env-wide list.

Direction (the dev's options; triage decides)

Reader who acts

Triage grades it. If A is taken, it is domain:engine (metadata-protocol). It is serial behind PR #21962 (#21934), which edits the same view branch.

Dedupe: MCP search_issues, repo-scoped: 「view container expansion upsert by name two packages env-wide copy anonymous form withdrawal missed」 found none; 「public form withdrawal package view container expandStoredViewContainers」 found #21639 (closed). #21639 is the same expansion upsert on the slot axis, within one package. This is the package axis, so it is a different case.

Dedupe words: view container expansion upsert by name across packages · env-wide list one expansion per name · public form withdrawal missed two packages container · expandStoredViewContainers byName


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Reach corrected by domain:engine seat 1 · session_017ErfyP2Rx7XWHJA27QjyUi · 2026-10-06T07:42Z, from the contract review of PR #21962 (record 6011725183, adopted). This card's body states the residual too narrowly.


    Generated by Claude Code

  2. changed the title [-]finding(metadata-protocol): two packages that each store an env-wide copy of one view container leave one package's expansion per name in the env-wide view list, so the anonymous form doors can miss the other's withdrawal (#21934 item 1's residual)[/-] [+]finding(metadata-protocol): a saved env-wide copy of a view container leaves its own expansion alone per name in the env-wide view list, so the anonymous form doors can miss another package's withdrawal, saved or shipped (#21934 item 1's residual)[/+] on Oct 6, 2026
  3. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: ② the capabilities an end user meets in the app — public forms | 缺项 | P2

    Triage: first grade — bug · security · priority:p2 · domain:engine · area:access · pm:blocked (finding removed). Direction A, a fix keyed per package, not an accepted limit

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T08:02Z. ⛔ Not a claim, ⛔ not a dispatch. Classes and positions only.

    Triage: lands in packages/metadata-protocol/src/protocol.ts (the list read's view branch, the expansion upsert over expandStoredViewContainers) and resolveRowlessExpandedView ⇒ domain:engine; rationale: a withdrawal must hold for every package's copy the anonymous doors can serve.

    Blocked-by: #21934


    Generated by Claude Code

  4. added
    area:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guards
    bugSomething isn't working
    and removed on Oct 6, 2026
  5. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Path: ② the capabilities an end user meets in the app — public forms | 缺项 | P2

    Unlock: pm:blocked → pm:queue. #21934 landed, and the doc now names this card as the limit's tracker

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-10-06T09:07Z. ⛔ Not a claim, ⛔ not a dispatch. Classes and positions only.


    Generated by Claude Code

  6. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 43 · 2026-10-06T09:29Z
    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-21967-view-expansion-per-package
    Worktree: objectstack-issue-21967
    Domain: domain:engine
    Seat: domain:engine#1
    Provenance: triage graded this card direction A (6012036101) and unlocked it to pm:queue (6013043442) once #21934 landed through PR #21962 (c9761cd2fb). It is the only eligible pm:queue card in this lane, and the lane runs three concurrent claims (the maintainer, verbatim: 「并发3」). This seat filed the card from #21934's dev report, and it carries no withheld detail.
    File surface (at origin/main f76c6221ac), per triage's grade and unlock:

  7. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report

    {
      "issue": 21967,
      "status": "done",
      "branch": "claude/issue-21967-view-expansion-per-package",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/21979",
      "session": "session_017ErfyP2Rx7XWHJA27QjyUi (mode:subagent, the PM's session; identity is the branch). Verified before any edit: the order file's sha256 prefix 439f89014d84675f, and the newest Claim on #21967 (6013393991) names this branch.",
      "premise_still_valid": true,
      "summary": "Triage's direction A is implemented in packages/metadata-protocol/src/protocol.ts, with no rest-server.ts and no packages/spec edit. The list read's view branch now upserts per package. Each slot of a name an expansion writes serves servedViewExpansion for its package: the package's own container row's expansion first, else a package-less container row's, which stands in for every package with no row of its own and never for another package's slot. A slot neither reaches keeps its item, so another package's withdrawal of a form holds at the anonymous form endpoints, whatever packages' copies of a view container are saved. servedViewExpansion is a new module-level function; its package order is the package dimension of servedOverlayRowCandidates (addressPackages). resolveRowlessExpandedView selects through the same function; naming a package, it reads the package-less rows as the list scoped to that package does. That scoped list now expands package-less containers as stand-ins in the slots the package seats only, so membership does not grow. A separate, droppable commit (c966e63a0a) keys #21510's own-row predicate (namesWithOwnStoredRow) by the slot's package. Without it, one package's stored row of a form's exact name kept every other package's copy expansion of that name out of the env-wide list; on an environment-scoped kernel the doors then missed a withdrawal saved in the other package's copy (pin (e)). The done-when wording is done: the docs page's exception sentence, its remedy and its tracked-in clause are removed (over-close sentence kept), and the anonymousFormIntakeWithdrawnIn docblock is updated. Changeset: @objectstack/metadata-protocol patch, Clause-② no. Draft PR #21979; first lines are Fixes #21967 and Clause-②: no.",
      "H1": "CONFIRMED at f76c6221ac. protocol.ts:9070-9105 derived written from expandStoredViewContainers(request.type, overlays) and upserted by name into one byName map, so the last expansion won. merged then dropped every other item of the name, another package's shipped item included. Each expansion entry carries its row's package_id as container.packageId (storedOverlayEntries, :9596). The stand-in logic (standInNames, unseated) did NOT encode a package-less container rule: it serves a package-less ROW of exactly the expanded name in a package-scoped list (#21817), and a package-less container was not expanded in a scoped list at all. Both loaders register shipped expansions per package: objectql/src/engine.ts:7159-7165 (registerItem with ownerId) and metadata/src/plugin.ts:1198-1210 (expandViewContainer + applyProtection with packageId). Measured: 19 of the 23 new pins red at base.",
      "H2": "PARTLY FALSIFIED. Naming a package, resolveRowlessExpandedView (:9690) already read only that package's rows (readActiveOverlayRows with packageId), so for two packages' containers it served that package's own expansion. Naming none, it served the last expansion in row order. The real gap: naming a package, it never consulted a package-less container. So getMetaItem naming a package served that package's shipped item while the list served the package-less copy's expansion; pin (d), package-less case, was red at base on the environment-scoped kernel. Now the by-name read naming a package, its slot in the env-wide list and the list scoped to it serve the same item (pin (d), both kernels).",
      "H3": "NO CONTRADICTION FOUND, ruling by ruling. #21334 (triage 5946423948, seat 5955628428): naming arm and 'the object door and the by-name read answer the same row'; expandRuntimeViewContainer is unchanged, its 46 pins and controls pass, and per-package slots make the doors agree. #20301 (20301-spec-view-container-name-ledger-note.md): a liveness-ledger note on the container body's name stamp, with no expansion rule; untouched. #21639 (triage 5973827435): the save door's one collision predicate, plus 'No merge rule: that would be a second precedence order to maintain' for two containers colliding in one selection. The save door is unchanged; within one package the last expansion still wins; per-package slots apply ADR-0048's existing resolution (no new order). The save door already accepts each package's overlay of its own container (the row under the save name is left out of the siblings), which is the case served per package here. #21804 (triage 5987404976): 'one prefer-local resolution ... no second resolver'; the package order is read from servedOverlayRowCandidates. #21817 (triage 5988911029): 'the scoped list's membership does not grow'; a package-less container stands in only in slots the package seats and is never appended. Its landing note recorded 'a package-less stored view container is not expanded in a scoped list' as a fact, not a ruling; expanding it as a stand-in follows direction A ('Teach resolveRowlessExpandedView the same rule'). #21442 (triage 5957375321): 'no second expansion rule, no kernel-specific branch'; one selection function, both doors, no kernel branch. #21510 (triage 5964342087): 'the stored row wins on both doors ... one predicate, read by both doors'; c966e63a0a keys that predicate by the slot's package and it is still one predicate read by both doors.",
      "H4": "CONFIRMED. findPublicFormView (rest/src/rest-server.ts:10735) and resolveFormBySlug (:10784-10819) read getMetaItems for the organization and the env-wide list as the layer. anonymousFormIntakeWithdrawnIn (metadata-core/src/anonymous-form-intake.ts:336) compares names, not packages. So once the list holds every package's item, the shared verdict judges every package's withdrawal. The org-scoped save check's first judgment (anonymousFormIntakeReopenRefusal) reads the same env-wide list, so it judges each package's copy too. No rest-server.ts edit. No dogfood case added: the doors' verdict is a pure composition of getMetaItems and the shared function, pinned in-process.",
      "tests": "BASE READING: the pins at 2981f6eb78 over base protocol.ts give 19 red, 4 green. The greens are the (a) control, both (e) controls, and (e) on the unscoped kernel, where registry hydration served B's copy. Every red failed in the defect's shape, e.g. 'expected [ [ pkg_a, true, ... ] ] to deeply equal [ [ pkg_a ... ], [ pkg_b ... ] ]'. HEAD dc853419db; @objectstack/metadata-protocol last changed in c966e63a0a: typecheck green (tsc --noEmit; the edited test file is in the program, 1 hit with --listFiles). Full suite in two halves under os-verify-lock, VERDICT command-exit 0 each: 107 files passed + 3 skipped with 25959 tests passed + 19 skipped; 111 files with 2060 tests passed. Total 218 files passed, 3 skipped; 28019 tests passed, 19 skipped. Related suites after the own-row commit: 8 files, 884 tests passed. @objectstack/metadata-core (comment only): typecheck green on both programs; 18 files, 411 tests passed. REVERSE VERIFICATION on committed HEAD dc853419db through scripts/ablation-replace.mjs: each anchor hit once, the blob changed on disk, and each restore was proved (blob == HEAD def1f5213f34, git diff HEAD empty). The subject is imported from source ./protocol.js, so there is no dist leg. Predictions were written first. A1 (the order's ablation: the view branch set back to base's by-name upsert, verbatim): predicted 19 red / 4 green, measured 19 red / 4 green, all four (a) cases red. A2 (own-row asked with no package): predicted 1 red, measured 1 red ((e) env-scoped). A3 (by-name read without its package-less read): predicted 2 red, measured 1 red (env-scoped). The miss: on the unscoped kernel the by-name read falls through to the registry, whose bare-name getItem answers the hydrated expansion with the same body. CLAUSE-② base: base protocol.ts was put back for one build (trap-guarded), then restored and proved (blob == HEAD, diff empty).",
      "gates": "At head dc853419db (origin/main still the base f76c6221ac, so the merge was a no-op). node scripts/pm/dispatch-gates.mjs --commands (no paths, --repo objectstack-ai/objectstack) derived 93 commands from 5 paths; all 93 ran and exit 0. --ran with exit-coded lines: '93 derived, 93 run, 0 NOT-MEASURED, 0 UNRUN (a DERIVED zero)'. Three first answered PREREQUISITE NOT MET (exit 3: unbuilt workspace packages): spec check:skill-examples, check:lean-entry-closure and check:dual-build-cjs-loads. check:type-check-debt's re-measure then built the workspace, and the reruns are green: 262 examples, 2 conditions, 106 entry points in 66 packages. The artifact-roster block outside the total (53 commands) also ran: 53 exit 0. check-closing-target-claim and check-single-claim-paths ran after PR #21979 opened, with its context; check-partof-closing-keyword ran over the PR body. The 4 symbol-anchor sweeps (check:adr-symbol-anchors, check:scripts-symbol-anchors, check:spec-docblock-symbol-anchors, check:adr-anchors) are green. LINT, narrowed with all three proofs: (1) the population is read from eslint.config.mjs: the TS/JS globs cover the 3 touched .ts files, and the .mdx and the changeset match no files glob; (2) the --format json count is 3 files, 0 errors, 0 warnings (eslint --no-inline-config, as pnpm lint runs it); (3) invariance: no parserOptions.project or projectService in the config, and its only disk reads are scripts/slot-lookup-baseline.json and scripts/query-options-erasure-baseline.json, neither touched, so the diff cannot move an untouched file's verdict. Control bytes: grep -P over the 5 paths gives 0 each; check:nul-bytes is green. CI is not awaited: in_progress at report time.",
      "line_budget": "n/a",
      "deviations": [
        "Scope: c966e63a0a extends direction A to #21510's own-row predicate on the package axis, under the bounded in-place exemption: (1) the same defect class, a package's withdrawal dropped from the env-wide list by another package's entry; (2) a mechanical shape fixed by ADR-0048's candidate order and #21510's one-predicate rule; (3) protocol.ts is held by this claim; (4) the same gates. It is a separate commit with its own pin (e) and can be dropped at review, but then the docs sentence 'The endpoints do too' needs a narrower exception. Named in the PR body.",
        "A3 reverse-verification prediction missed (predicted 2 red, measured 1); the reason is recorded under tests.",
        "Running the gates: the harness moved the 41-90 batch to the background at the 10-minute foreground cap. It ran to completion inside this turn (a foreground tail --pid wait on check:type-check-debt), and its results were read from its own exit records.",
        "Attribution: commits carry AGENTS.md's model-free trailer pair, and the PR body ends with AGENTS.md's session-URL footer, not the harness reminder's model-named Co-Authored-By and its footer form. AGENTS.md takes precedence and the pre-push hook refuses a model id.",
        "The docs page change is removal only, as triage's done-when asks; no sentence was added. The metadata-core docblock's exception sentence is replaced by one positive sentence (comment only; no changeset, as #21934 did)."
      ],
      "files_changed": [
        ".changeset/21967-view-expansion-per-package.md (+18/-0, new)",
        "content/docs/ui/public-data-collection.mdx (+1/-1)",
        "packages/metadata-core/src/anonymous-form-intake.ts (+3/-4, docblock)",
        "packages/metadata-protocol/src/protocol.org-scoped-write-refused.test.ts (+270/-2)",
        "packages/metadata-protocol/src/protocol.ts (+202/-48)"
      ],
      "clause_2": "no, measured as claimed. Built entry declarations, base protocol.ts against head, comments stripped: @objectstack/metadata-protocol dist/index.d.ts and index.d.cts identical (2162 lines each); @objectstack/metadata-core index.d.ts, index.d.cts, repository chunk .d.ts and testing.d.ts identical. The raw bytes differ in comments only (docblocks of private members and of anonymousFormIntakeWithdrawnIn). servedViewExpansion and addressPackages are module-private; namesWithOwnStoredRow and resolveRowlessExpandedView are private methods. No exported signature moved.",
      "mcp_calls": "0",
      "api_writes": "3 relay strokes, each one POST /repos/objectstack-ai/objectstack/dispatches from this session, executed by fleet-write.yml as objectstack-fleet[bot]: (1) pr_create, executed as POST /repos/objectstack-ai/objectstack/pulls (PR #21979, run 37451097924, body read back identical, 13813 bytes); (2) label-write assignee, executed as POST /repos/objectstack-ai/objectstack/issues/21979/assignees (run 37451183938, read back: assignee os-project-manager; no label written); (3) this os-dev-report comment, executed as POST /repos/objectstack-ai/objectstack/issues/21967/comments. Plus git push (not REST): 5 pushes of this branch (the empty-branch probe, then 4 commits, fast-forward only). Within the 4-write budget.",
      "open_questions": [
        {
          "question": "Keep c966e63a0a (#21510's own-row predicate keyed by the slot's package) in this PR, or split it out?",
          "options": [
            "A: keep it. The env-wide list and the save check's first judgment then hold every package's copy even beside another package's stored row of the form's exact name, and the docs sentence 'The endpoints do too' is true without exception.",
            "B: drop it. Narrow the docs sentence to an exception for that case and file it as its own card."
          ],
          "recommendation": "A. It is the same per-package keying applied to the one predicate both doors already share, the shape #21510 keyed on the org axis. Pin (e) and ablation A2 show it red without the commit on the environment-scoped kernel, with no other pin moved."
        }
      ],
      "out_of_scope_findings": [
        "class: a · reach: measured in-process through saveMetaItem (the method PUT /api/v1/meta/view/NAME calls) and the env-wide getMetaItems view list, on both kernels, at dc853419db; not over HTTP. It needs a package that ships a view container on an object another code package owns. Untouched by this PR. Evidence: the source loaders register a shipped container's views under OBJECT.KEY for its own package, whatever package owns the object (objectql/src/engine.ts:7159-7165, metadata/src/plugin.ts:1198-1210). That package's stored copy of the same container takes #21334's own-name arm on another package's object (expandRuntimeViewContainer to expandUnderOwnName) and expands to OBJECT.CONTAINER.KEY. So the copy overlays none of the views its package ships from that container. Probe: B ships container task on A's object task, and B's env-wide copy withdraws formViews.intake_form. The env-wide list then holds [task.intake_form, pkg_a, open], [task.intake_form, pkg_b, open] and [task.task.intake_form, pkg_b, withdrawn]: the withdrawal saved in B's copy does not reach B's shipped form. Family: view-container naming (#21334's arm against the loaders); its closing card #21639 is closed, so the seat routes it. Dedupe words: shipped container on another package object loader names · stored copy expands under own name shipped views not overlaid · withdrawal saved in container copy misses shipped form name · expandUnderOwnName loader expandViewContainer mismatch",
        "carrier: none · noted, not filed (PR Acceptance notes). The save door's collision predicate (#21639) reads sibling expansions with no package filter, so two different containers of two packages that expand one name are still refused at save, though the read doors now serve them in separate slots. Stricter than the readers; fails closed.",
        "carrier: none · noted, not filed (PR Acceptance notes). A by-name read that names no package still answers the last expansion of a name across packages ('any row', as servedOverlayRowCandidates does with no package)."
      ]
    }

    Generated by Claude Code

  8. objectstack-fleet commented on Oct 6, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed: PR #21979 → db87a025df on main. It merged through the merge queue at 2026-10-06T11:58Z; it entered the queue at 2026-10-06T11:22Z. Verified at 2026-10-06T11:58Z. 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:accessPermissions that actually hold — RLS/FLS, sharing model, write-path guardsbugSomething isn't workingdomain:enginepriority:p2Medium: important, M3security

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions