You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
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
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
A removal that names a package removes that package's object-bound slot(s) too.
A bare removal judges holders over every key of the name, bound included, and refuses a two-holder name with the D3 409.
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).
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.
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.
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/metadatametadata-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.
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 main89d815c 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:
Declare Clause-②: yes (narrowing). Each of the three moved answers goes in the PR body as FROM → TO, with the first-party census.
S7 is pinned: a DB delete that throws neither clears the registry nor announces deleted.
{
"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"
]
}
Activity
objectstack-fleet commentedon Nov 10, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: confirmed ·
domain:server· p2 ·pm:blocked· 2026-11-10T21:56ZBlocked-by: #32246
⛔ Not a claim.
domain:server(metadata: removal of an object-bound action by package). p2, under design(metadata): address behavioural metadata by package — key flows, actions, jobs and other non-object types by (package, name); re-open ADR-0028 (replaces #30806 phase 3 renames) #31527.Generated by Claude Code
objectstack-fleet commentedon Nov 10, 2026
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
DatabaseLoaderrow of actionx, 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:
unregister('action', 'x')therefore passes the manager's judgment.DatabaseLoader.deletekeys rows by package and name, so it counts two holders and throws.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
DatabaseLoaderwiring 0; bareactionremoval callers 0. #32347's body declares the exception and points here.objectstack-fleet commentedon Nov 11, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: unblocked →
pm:queue·domain:server· p2 · 2026-11-11T01:56Z⛔ Not a claim.
main.domain:server#3's 22:57Z amendment (mode S7) is scope, not a hold. No freeze.Generated by Claude Code
objectstack-fleet commentedon Nov 12, 2026
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, labelspm: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_01Utp6CZRARNYmYKvzfuviP1Account:
os-billBranch:
claude/project-thread-38zq80Worktree:
objectstack-issue-32352Domain:
domain:serverSeat:
epic:#31527#2File surface:
packages/metadatametadata-manager.tsholder view andloaders/database-loader.tsdelete row key,packages/objectqlregistry removal of bound action slots, their tests; changeset. Step 0 (measured onmainand onclaude/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: highClause-②: 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
mainand on PR #32497's head before any buildSerial constraints cleared: PR #32497 (#32183, seat 1, action package addressing) edits
metadata-manager.tsandregistry.tsand lands first; seat 1's chain #32214 → #31696 PR-3 → #31700 → #32497 owns the hot files; this card lands after #32497 and mergesmain.Generated by Claude Code
objectstack-fleet commentedon Nov 12, 2026
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 onmain89d815c 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, anddeletedis 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.Rejected options:
sys_metadata's unique key and still needs A's refusal.Conditions:
Clause-②: yes (narrowing). Each of the three moved answers goes in the PR body as FROM → TO, with the first-party census.deleted.mainfirst.Generated by Claude Code
objectstack-fleet commentedon Nov 12, 2026
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
objectstack-fleet commentedon Nov 12, 2026
epic:#31527#2 · os-bill · session_01Utp6Ca5UHVBDE3DqQXgJsW
Seat ruling amendment · #32352 · 2026-11-12T06:58Z (amends ruling 6812619720)
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 isminor.mainfirst; 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
objectstack-fleet commentedon Nov 12, 2026
epic:#31527#2 · os-bill · session_01Utp6Ca5UHVBDE3DqQXgJsW
Landed · #32352 · 2026-11-12T07:27Z
137cc36b98(before #32497, per the ruling amendment on this card)State change:
pm:dispatchedremoved now that the card is closed.Generated by Claude Code