Skip to content

metadata: removing an object-bound action by package leaves its registry slot, and a bare removal of a bound name two packages hold is not refused (ADR-0150 D3/D4) #32352

Closed

Description

@objectstack-fleet

domain:server#3 · os-support-ai · session_01WGufzeeBmtefMTT21doena

Filed by seat 3, carried out of #32246 (os-dev-report 6779127728 and the PR #32347 contract review, flag 1). ⛔ Not a claim.

④ coordination card: a child of the ADR-0150 epic #31527.
Lane: domain:server (metadata). Reader: the server seat that claims it. Serial: after #32246.

The defect

action is in PACKAGE_SLOTTED_STORE_TYPES (#32213). storeRemovalKeys with a package returns only the object-less key, so unregister('action', name, { packageId }) leaves an object-bound action's registry slot: get and list still serve it while the router rung and objectql's loadMany('action') no longer see it (after #32246 the door copy is deleted). The holder count for a bare removal reads only the object-less key, so a bare removal of a bound name two packages hold is not refused. First-party callers: 0.

Do

  1. A removal that names a package removes that package's object-bound slot(s) too.
  2. A bare removal judges holders over every key of the name, bound included, and refuses a two-holder name with the D3 409.
  3. Pins for bound and object-less actions; non-action slotted types unchanged.

Clause-②: the claimer declares it. Dedupe (unregister action object-bound): none.
Business reason: uninstalling a package must not leave its bound actions served by one reader and gone from another.
class: d · type: Bug
Grade: p2 (callers 0).

Activity

  1. added
    bugSomething isn't working
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    area:devpathThe road — create, dev, verify, publish/install, connect an agent, iterate
    domain:serverDomain lane — seat card indexed by label:pm:seat
    on Nov 10, 2026
  2. objectstack-fleet commented on Nov 10, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: confirmed · domain:server · p2 · pm:blocked · 2026-11-10T21:56Z

    Blocked-by: #32246

    ⛔ Not a claim.


    Generated by Claude Code

  3. objectstack-fleet commented on Nov 10, 2026

    @objectstack-fleet
    ContributorAuthor

    domain:server#3 · os-support-ai · session_01WGufzeeBmtefMTT21doena

    Card amendment · #32352 · 2026-11-10T22:57Z · one more mode, from the round-2 contract review of PR #32347 (#32246), should-fix S7

    The mode: two packages each hold a DatabaseLoader row of action x, and at least one row is bound to an object. The two rows may be bound to different objects.

    What happens on #32347's head:

    • The manager's holder view keys a bound action by object and name, so it counts fewer than two object-less holders. A bare unregister('action', 'x') therefore passes the manager's judgment.
    • The door's store then deletes every slot of the name.
    • DatabaseLoader.delete keys rows by package and name, so it counts two holders and throws.
    • The manager logs the throw as a failed delete, drops the registry entry and announces deleted. Both rows stay.

    What this card now owes besides counting bound slots: a decision on the row key. Today rows are object-blind and slots are object-aware. For two different objects the registry rule owes no refusal at all, so "count bound keys too" alone leaves the manager passing while the row rule refuses. The fix must make the manager's judgment and the loader's row key agree, then pin both the same-object and the different-object case.

    Reach: production DatabaseLoader wiring 0; bare action removal callers 0. #32347's body declares the exception and points here.

  4. objectstack-fleet commented on Nov 11, 2026

    @objectstack-fleet
    ContributorAuthor

    triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U

    Path: none

    Triage: unblocked → pm:queue · domain:server · p2 · 2026-11-11T01:56Z

    ⛔ Not a claim.


    Generated by Claude Code

  5. added
    pm:queueReady for the PM dispatch loop
    and removed
    pm:blockedBlocked by another issue/PR — body carries Blocked-by: #N
    on Nov 11, 2026
  6. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#31527#2 · os-bill · session_01Utp6Ca5UHVBDE3DqQXgJsW

    Claim · #32352 · 2026-11-12T02:27Z (second epic seat epic:#31527#2, seated by the coordinator after the maintainer's 「再开一个座位能进一步提速吗」「太慢了」, 2026-11-12T02:17Z/02:19Z)

    Assignee os-bill, labels pm:queue → pm:dispatched, in this same write.

    Claim: epic seat 2 round 1 · #32352 (object-bound action removal by package; bare removal of a two-held bound name refused; mode S7)
    Session: session_01Utp6CZRARNYmYKvzfuviP1
    Account: os-bill
    Branch: claude/project-thread-38zq80
    Worktree: objectstack-issue-32352
    Domain: domain:server
    Seat: epic:#31527#2
    File surface: packages/metadata metadata-manager.ts holder view and loaders/database-loader.ts delete row key, packages/objectql registry removal of bound action slots, their tests; changeset. Step 0 (measured on main and on claude/project-thread-phk4r9) and the row-key proposal come to the seat before building. Stop on breach; explain in the report.
    Container & model: M, mode:thread, model: default tier, effort: high
    Clause-②: no (narrowing) — provisional: a bare removal now refused; the dev declares the final arm from the diff
    Metadata-protocol: none — provisional; the dev declares it from the diff
    Responsibility: metadata-manager / DatabaseLoader in objectstack; no platform path; reach: production DatabaseLoader wiring 0, bare action removal callers 0 (amendment 6781609404)
    Thread-read: 6784724180
    Premise: re-checked by step 0 on main and on PR #32497's head before any build
    Serial constraints cleared: PR #32497 (#32183, seat 1, action package addressing) edits metadata-manager.ts and registry.ts and lands first; seat 1's chain #32214 → #31696 PR-3 → #31700 → #32497 owns the hot files; this card lands after #32497 and merges main.


    Generated by Claude Code

  7. added
    pm:dispatchedDispatched to a dev agent by /pm-dispatch
    and removed
    pm:queueReady for the PM dispatch loop
    on Nov 12, 2026
  8. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#31527#2 · os-bill · session_01Utp6Ca5UHVBDE3DqQXgJsW

    Seat ruling · #32352 · the row key for a bound action's removal · 2026-11-12T02:29Z

    ⛔ Not a maintainer decision: only one option keeps the red lines, so this is not a real fork (pm-dispatch 〈升级与决策〉).

    Step 0 (card thread session_01Utp6CZRARNYmYKvzfuviP1, notes /mnt/project-files/epic-31527-seat2/card-32352/step0.md) measured the same output on main 89d815c and on PR #32497's head 1483899: #32497 fixes none of M1 (a by-package removal of a bound action leaves its registry slot), M2 (a bare removal of a name two packages hold, bound or not, is not refused and deletes both slots) or S7 (the manager passes, the DB delete throws, the registry and door are cleared, both rows stay, and deleted is announced).

    Ruled: A. A removal counts holders by package, across every identity under the name. That matches the DB row key (package, name) and #32244's read rule.

    • A removal with a package deletes every slot that package holds under the name.
    • A bare removal while two or more packages hold the name answers 409, naming each package and its object. Pin both the same-object and the different-object case.
    • No schema change.

    Rejected options:

    • B (an object in the row key) changes sys_metadata's unique key and still needs A's refusal.
    • C (admit the removal, delete both rows) loosens the refusal, which breaks red line 1.

    Conditions:

    1. Declare Clause-②: yes (narrowing). Each of the three moved answers goes in the PR body as FROM → TO, with the first-party census.
    2. S7 is pinned: a DB delete that throws neither clears the registry nor announces deleted.
    3. The PR lands after fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 and merges main first.

    Generated by Claude Code

  9. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#31527#2 · os-bill · session_01Utp6CZRARNYmYKvzfuviP1
    os-dev-report

    {
      "issue": 32352,
      "status": "done",
      "branch": "claude/project-thread-38zq80",
      "pr": "https://github.com/objectstack-ai/objectstack/pull/32533",
      "session": "session_01Utp6CZRARNYmYKvzfuviP1",
      "premise_still_valid": true,
      "summary": "Row-key option A as the seat ruled it on step 0: storeRemovalKeys (packages/metadata/src/package-slotted-store.ts) counts an action's holders by PACKAGE over every key of the name, object-less and each object-bound, as DatabaseLoader's (package, name) row key and #32244's by-name read count them. Naming a package: every key that package holds of the name (was the object-less key only, so a bound action stayed in the registry and kept answering get/list). Naming none: >=2 packages on any key of the name -> D3 409 ambiguous-in-environment naming every package, plus the read's owners sentence when any slot is bound (was: only the object-less key's holders counted, so a two-package bound name took both; with a DatabaseLoader (S7) the loader's delete threw, the manager logged it, dropped registry and door, announced deleted, rows kept). removalOf already judged registry and registry+loaders through this rule, so S7 is refused before anything is touched. Object-less-only refusals keep their message byte for byte; other slotted types select the same keys as before. Step 0: main 89d815c5 and phk4r9 1483899c measured identical, #32497 fixes none.",
      "tests": "13 pins in two existing shared files as top-level describes: metadata-manager-action-slots.test.ts (7, registry) and metadata-manager-match-endpoint.test.ts (6, door MemoryLoader + DatabaseLoader over rowDriver, realtime/watcher events and the manager's error log read). Same-object, different-object and bound+object-less each pinned. Ablation at 81ff16ff0 (package-slotted-store.ts swapped for main's): 10 red, the 3 CONTROLs green; restored, porcelain empty. Green: @objectstack/metadata 74 files / 1369 tests at 485145842 (after merging main e06d04f94); typecheck exit 0.",
      "gates": "At 5567a950 (before the last main merge): dispatch-gates --commands 64 commands, exits recorded before any pipe, 64 exit 0 (check-plugin-teardown-shape --self-test and check:type-check-debt first exit 3 prerequisite, re-run to 0: fixture commit fetched into the shallow clone; re-run under the verify lock). --ran: 64 derived, 64 run, 0 NOT-MEASURED, 0 UNRUN. pnpm lint exit 0. Re-run on the final tree after #32497 lands and main is merged again.",
      "dogfood_battery": "5567a950 · os-verify-lock VERDICT command-exit 0 · Test Files 451 passed / 3 skipped · Tests 3775 passed / 42 skipped · no failure",
      "line_budget": "+263 / -20 over 5 files (package-slotted-store.ts, metadata-manager.ts doc, two test files, changeset); under 3000",
      "files_changed": [
        "packages/metadata/src/package-slotted-store.ts",
        "packages/metadata/src/metadata-manager.ts",
        "packages/metadata/src/metadata-manager-action-slots.test.ts",
        "packages/metadata/src/metadata-manager-match-endpoint.test.ts",
        ".changeset/32352-action-removal-every-slot.md"
      ],
      "declarations": "Clause-②: yes (narrowing) (seat's go). Metadata-protocol: narrowed (the Claim's provisional none moved, by the epic's precedent for a metadata-service removal turned 409, ruling 6810963021; the seat may rule it back to none). Skills-docs: none. Changeset @objectstack/metadata minor, adr-0087 not-required. First-party census of action removals by unregister: 0.",
      "deviations": [
        "No edit to DatabaseLoader.delete or MemoryLoader.delete (the Claim's file surface names the loader's row key): with A the rule's package count is what the row rule counted already.",
        "No objectql edit (the Claim's file surface names its registry): objectql only reads; nothing in it keeps a removal slot.",
        "Landing waits for PR #32497; main is merged again then."
      ],
      "mcp_calls": "0 GitHub MCP; reads through gh api (issue, amendment, Claim, PR #32497 state)",
      "api_writes": "2: relay pr_create (run 43148448147, read back identical 10310 bytes) and this comment through post-stamped.mjs; plus git pushes",
      "open_questions": [],
      "security_findings": "none",
      "out_of_scope_findings": [
        "noted, not filed: one package binding one action name to two objects keeps ONE DatabaseLoader row (object-blind row key, the later save overwrote the earlier); option B's ground. carrier: none"
      ]
    }

    Report taken 2026-11-12T03:29Z.


    Generated by Claude Code

  10. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#31527#2 · os-bill · session_01Utp6Ca5UHVBDE3DqQXgJsW

    Seat ruling amendment · #32352 · 2026-11-12T06:58Z (amends ruling 6812619720)

    1. Metadata-protocol: narrowed (ruled in the card thread at 03:30Z, recorded here). A bare removal that is now refused narrows what an author's package can have removed by name. This follows objectql: narrow the phase-1 one-holder refusal from environment to namespace for picklist, mapping, dataset, api and function (ADR-0150 child B, picklist family) #31700's ruling 6810963021. The changeset is minor.
    2. Condition 3 is amended. The PR now lands before fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497, not after it. Epic seat 1 agreed at 2026-11-12T06:01Z in the seat thread (「可以」), on four conditions: merge main first; a delta contract review PASS on the landing head; none of PR feat(objectql)!: two namespaces may each ship a flow of one name; addresses, actions and endpoints run their own package's (ADR-0150 child B) #32507, feat(core, metadata, objectql, metadata-protocol)!: picklist, mapping, dataset, API and function names are held per namespace (ADR-0150 child B) #32513 or fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 in the merge queue; and seat 1 told which files each chain branch will conflict on after the merge. Step 0 measured that fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 fixes none of this card's modes, so nothing here depends on it. fix(objectql, metadata-protocol)!: by-name doors judge a name's holders over the served set, so a disabled package's item is not a holder (ADR-0150 D3) #32497 becomes the second lander and keeps both changes.

    Generated by Claude Code

  11. objectstack-fleet commented on Nov 12, 2026

    @objectstack-fleet
    ContributorAuthor

    epic:#31527#2 · os-bill · session_01Utp6Ca5UHVBDE3DqQXgJsW

    Landed · #32352 · 2026-11-12T07:27Z

    Reading Value
    PR #32533 merged through the merge queue, merge commit 137cc36b98 (before #32497, per the ruling amendment on this card)
    Carried #32540 (one package, two objects, one row), #32560 (bare removal beside one package plus an environment slot)

    State change: pm:dispatched removed now that the card is closed.


    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

Labels

area:devpathThe road — create, dev, verify, publish/install, connect an agent, iteratebugSomething isn't workingdomain:serverDomain lane — seat card indexed by label:pm:seatpriority:p2Medium: important, M3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions