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-protocol: registry heal and code-layer carry-forward read an action bare, and a refusal is swallowed silently #32366
Carrier filed from contract review 6781770671 on PR #32361 (card #32244, ③ out-of-scope finding 2, confirmed). Child of the ADR-0150 epic #31527.
restoreArtifactRegistryView and readCodeLayerForCarryForward (packages/metadata-protocol/src/protocol-registry-hydration.ts) read the metadata service by bare name, with no package. Once #32361 makes a bare read of a name two packages hold refuse, two things happen:
The read at protocol-registry-hydration.ts:1421 (PR head ddcaeda0) sits inside that function's silent outer catch. After an overlay DELETE on such a name, the registry heal is skipped with no log line.
On main it registered whichever package's item was written last, which is also wrong.
Fix: pass the package the slot belongs to on both reads. Never read bare, and never swallow a package-scoped refusal silently. Pin a DELETE-then-heal on a two-package name.
Governing text: ADR-0150 D3 and D5 (no silent pick).
Re-check:git grep -n "restoreArtifactRegistryView\|readCodeLayerForCarryForward" packages/metadata-protocol/src, then read whether the service read passes a package.
Claim: PM loop round 1
Session: session_012MmDsNv6qb5gT8Pd98AmCB
Account: os-lizheng (the seat's linked user as GET /user answers it; the card's assignee)
Branch: claude/issue-32366-registry-heal-package-read
Worktree: objectstack-issue-32366
Domain: domain:server
Seat: domain:server#4
File surface: packages/metadata-protocol/src/protocol-registry-hydration.ts (restoreArtifactRegistryView and its outer catch), packages/metadata-protocol/src/protocol-discovery-and-bases.ts (readCodeLayerForCarryForward), the callers that must pass a package (protocol-credential-carry-forward.ts, protocol-diff-delete-load.ts, protocol-package-commits.ts, protocol-class-head.ts, protocol-packageless-overlay-migration.ts) only where the slot's package is not already passed, their tests; root .changeset/. ⛔ packages/spec/**, packages/objectql/**, packages/metadata/**, ui/**, skills/**, docs/adr/** (stop on breach; explain in the report)
Container & model: M, mode:subagent, model: default tier — dispatch-gates --tier: no path-derived mandate, effort: high
Clause-②: no
Metadata-protocol: none
Responsibility: metadata-protocol's registry heal and carry-forward reads in objectstack (platform card) | none — #32361 made the bare read refuse but left these two callers bare | any environment holding one name in two packages (ADR-0150 D3); no first-party pair known
Thread-read: 6788345196
Premise: holds (re-read on origin/main7d82b2d at 2026-11-11T12:54Z)
Serial constraints cleared: #32396 (seat 6) and #30020 (seat 3) also edit packages/metadata-protocol/src/**, in protocol-history-and-publish.ts / protocol.ts / protocol-host.ts — disjoint files; #32183 (seat 6, PR #32327) — no hunk in this surface's files at this stamp. Merge main before opening the PR
{
"issue": 32366,
"status": "done",
"branch": "claude/issue-32366-registry-heal-package-read",
"pr": "https://github.com/objectstack-ai/objectstack/pull/32447",
"session": "session_012MmDsNv6qb5gT8Pd98AmCB",
"premise_still_valid": true,
"summary": "restoreArtifactRegistryView takes a new REQUIRED packageId (string | null) after organizationId. Its tier-2 MetadataService baseline read now names the package of the removed row, so a reset of package P's row on a name two packages hold restores P's baseline instead of hitting the bare D3 refusal. Callers pass: the delete door's resolved address (two repository-path sites), boundPackageOf(existing.package_id) on the legacy path, commitPackageId in revertCommit, and the step-4 binding in migratePackagelessOverlays. Two callers pass null and say why at the call site: reconcileViewExpansions (a view name is a global literal, bucket 1) and applyRemoteMetadataMutation (the cluster signal carries no binding). The silent outer catch now logs one warn line through registryHealStoppedMessage, and a package-scoped refusal is named as one, with its holders. Premise holds for restoreArtifactRegistryView (the bare read at hydration.ts tier 2, the card's :1421). It does NOT hold for readCodeLayerForCarryForward: on origin/main 7d82b2da it already reads readItemFromMetadataService(type, name, pkg) with the save's package (positive control: args.packageId at the carry-forward site). It reads bare only for a package-less save, where D3's bare read is the right question, and it has no catch, so a refusal there is loud. It lives in protocol-discovery-and-bases.ts, and no slotted type has a redactor (measured getMetadataTypeRedactor: api/agent/skill/action/permission all none). It is unchanged.",
"tests": "New packages/metadata-protocol/src/protocol.registry-heal-package-read.test.ts: a real MetadataManager with two packages' action items, a recording registry double, a control-plane kernel. On head 9bcce232: `Tests 6 passed (6)` (lock VERDICT command-exit 0). Whole package at the fix plus the pin (code identical to head; later commits are test-only, plus a main merge touching packages/verify only): `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2` gave `Test Files 334 passed | 5 skipped (339)`, `Tests 22681 passed | 29 skipped (22710)`, VERDICT command-exit 0. `pnpm --filter @objectstack/metadata-protocol typecheck` exit 0 on head (an earlier run was a lock queue-timeout exit 99, NOT MEASURED, and was retried). Ablation through scripts/ablation-replace.mjs, against source imported by relative path (no build): A, tier-2 read back to bare: mutation landed (blob e40047537b4b to e9a937434180); 2 failed and 4 passed, on `expected [] to deeply equal [ [ 'action', 'alpha', … ] ]` and the beta twin; restored, blob equals HEAD and git diff HEAD is empty. B, the catch's console.warn removed: 2 failed and 4 passed (the package-less refusal case and the other-throw case), on `expected [] to have a length of 1 but got +0`; restored the same way. Both directions were predicted.",
"gates": "Local: node scripts/pm/dispatch-gates.mjs --commands derived 79 families against merge-base 7d82b2dab (after merging main 0dc84b02). `dispatch-gates --ran` with `:: exit N` lines: '79 derived famil(ies) accounted for — 76 run, 3 NOT-MEASURED'. The 3 NOT MEASURED exited 3 (prerequisite): check:dual-build-cjs-loads and check:lean-entry-closure need a full build; check-plugin-teardown-shape --self-test needs an unshallow clone. check:engine-double-contract exited 1 first (the new pin's double was not in the ledger), was fixed with --write plus a commit, and exits 0 on 9bcce232. check:test-source-alias exited 1 first (no module-top import of @objectstack/lint), was fixed, and exits 0. All other families exit 0, including check:durability-log-level, check:startup-registry-verdict, check:facade-delegation-only, check:nul-bytes, check:doc-authoring and check:closing-keyword-parity. Lint: eslint --no-inline-config on the 7 changed .ts files, exit 0; the repo-wide `pnpm lint` was left to CI (see deviations). CI on PR head 9bcce232 when this report was written: Governed Surface Queue Guard success. Lint (ESLint, PM dispatch-gates, docs & code guards, repo & tooling gates, CI/release/engine gates, scoped), Type Check (workspace, source, consumer, debt ledger), Check Changeset, Check PR Size and PR-body guards were in_progress. Test Core, Build Core and the UI jobs had not reported yet.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (Clause-②: no)",
"mcp_calls": "0",
"api_writes": "2 — (1) fleet-write relay repository_dispatch carrying pr_create (POST /repos/objectstack-ai/objectstack/pulls draft, plus POST /repos/objectstack-ai/objectstack/issues/32447/assignees os-lizheng), run 43037353879 success, body read back byte-identical (9513 bytes); (2) this os-dev-report comment via post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/32366/comments). git push is not counted.",
"deviations": "(1) Surface beyond the claim: packages/metadata-protocol/src/protocol.ts gets the class member's signature only, a one-line forwarder of the new parameter that the facade split makes unavoidable; scripts/engine-double-contract.pinned.json gets three rows for the new pin's double, written by the gate's --write. #30020 also edits protocol.ts, so a textual overlap there is possible: the hunk is the restoreArtifactRegistryView forwarder only. (2) The dispatch asked for `pnpm lint`. os-dev.md says the repo-wide lint is CI's, and os-dev.md wins on a conflict, so eslint was run on the changed files only (a declared narrowing). (3) protocol.view-container-reset-expansions.test.ts:478 still calls the member with 3 args through `as any`. At runtime packageId is undefined, which is read as null; it was left unedited.",
"files_changed": [
".changeset/32366-registry-heal-package-read.md",
"packages/metadata-protocol/src/protocol-class-head.ts",
"packages/metadata-protocol/src/protocol-diff-delete-load.ts",
"packages/metadata-protocol/src/protocol-package-commits.ts",
"packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts",
"packages/metadata-protocol/src/protocol-registry-hydration.ts",
"packages/metadata-protocol/src/protocol.registry-heal-package-read.test.ts",
"packages/metadata-protocol/src/protocol.ts",
"scripts/engine-double-contract.pinned.json"
],
"line_budget": "n/a — no skills/** or ratcheted ledger edited",
"open_questions": [
{
"question": "The peer convergence heal (applyRemoteMetadataMutation) cannot name the removed row's package. The binding is gone from the peer's store, and MetadataMutationEvent carries an address and no binding (its docblock says the wire carries no binding, and must not). On a name two packages hold, its tier-2 read is refused; with this PR that is logged with the holders, where before it was swallowed. Should the signal carry the package?",
"options": [
"A: leave it as this PR does. A logged skip; the peer's entry heals on its next reload or the next write of the name. Cost: none now. A multi-replica deployment with a two-holder slotted name keeps a stale registry entry on peers until reload.",
"B: add the removed row's package to MetadataMutationEvent and ClusterMetadataMutationPayload, and have the peer pass it. Cost: a cluster wire-contract change (exported types) that reverses the recorded no-binding rule, so it needs a ruling and its own card; small code, about one day with pins."
],
"recommendation": "A, because no first-party deployment holds a slotted name in two packages (ADR-0150 measured 0 of 297 on HotCRM), the stale peer entry is now visible in the log rather than silent, and B reverses a recorded wire rule for a case with no measured user. Startup focus: no expansion without pull."
}
],
"security_findings": "none",
"out_of_scope_findings": [
"carrier: none · noted in the PR's Acceptance notes, not filed: applyRemoteMetadataMutation's active-row re-read (repo.get with no packageId) is package-agnostic on a name two packages hold. This is a read-only inference, not reproduced, so it was not filed."
]
}
PR #32447 at 9bcce232: seat checklist clean (record 6798420830 on the PR). ACCEPT waits on the epic #31527 red-line read, owed by epic:#31527#1 (post reads vacant); asked on mailbox #32234 at 2026-11-11T14:26Z.
{
"issue": 32366,
"status": "done",
"round": "patch round 1 (delta)",
"branch": "claude/issue-32366-registry-heal-package-read",
"pr": "https://github.com/objectstack-ai/objectstack/pull/32447",
"head": "bf53824e5c198a8150261336a4bcb05dc895f201",
"session": "session_012MmDsNv6qb5gT8Pd98AmCB",
"premise_still_valid": true,
"summary": "S1: tier 2 now reads through readHealBaseline in protocol-registry-hydration.ts. It reads the row's own package first. On a settled miss it reads with no package, as main did. A name one package holds therefore restores the one holder's baseline, for an App's pair row as for any non-holder row. On a two-holder refusal, the base is chosen by the pair relation from the refusal's package candidates (isPairRowRemoval, the delete door's predicate, with installedManifestsOfEngine). Exactly one base means its baseline is read; otherwise the refusal is rethrown and logged, and nothing is picked or retired. A clean miss with no holder still retires (tier 3). N1: the heal-stopped line is console.error, and its first line carries the consequence and the fix ('the next reload (or restart) re-hydrates the registry from the stored rows and heals it'). N2: the read and the message use boundPackageOf, and the message prints the refused read's own callerPackageId, so '', 'sys_metadata' and an omitted argument print as 'no package'. N3/S4: the changeset names PACKAGE_SLOTTED_STORE_TYPES, says error not warn, and states the pair-row heal; the test header names the set. Main merge (f0f729e71): the three restoreArtifactRegistryView callers in protocol-history-and-publish.ts (undoLandedPromotions, #32454) passed item.d.member in the new packageId position, the TS2345 CI caught. Each now passes item.packageId (the row restored, the row deleted, and the active row the undo left), with a reason at the call site. S2/S3 untouched (applyRemoteMetadataMutation and package-slotted-store.ts not edited).",
"tests": "protocol.registry-heal-package-read.test.ts now has 12 cases. New: S1, a one-holder pair row (tier1 [false], tier2 [alpha], tier3 []); the one-holder CONTROL with a row bound to a non-holder that pairs nobody (the same as main); a two-holder pair row giving the base, never beta; a two-holder non-pair row whose refusal is logged with nothing picked or retired; the no-holder CONTROL that retires in tier 3; and N2, with '', 'sys_metadata' and undefined each printing 'no package'. The logging cases also assert no line at warn. protocol-publish-drafts-all-or-nothing.test.ts, undo case: every heal call's packageId argument equals the published package. On head bf53824e: `pnpm --filter @objectstack/metadata-protocol typecheck && … vitest run --maxWorkers=2` gave VERDICT command-exit 0, `Test Files 335 passed | 5 skipped (340)`, `Tests 22696 passed | 29 skipped (22725)` (the first attempt was a lock queue-timeout, exit 99, retried under the same slot). Ablation through ablation-replace.mjs: S1, the readHealBaseline early return made unconditional (blob 15e06179633b to 9035f64a9a25), gave 4 failed and 8 passed, the four non-holder cases; the no-holder control stayed green as predicted; restored, blob equals HEAD. An earlier attempt was refused by the tool, because the replacement text already occurred in the file, so nothing ran; it was re-run with a unique replacement. Publish undo: the three item.packageId arguments set to null (anchor 3 to 0) gave 1 failed and 8 passed, the undo case; restored, blob equals HEAD.",
"gates": "dispatch-gates --commands on bf53824e: 79 families. `--ran`: '79 derived famil(ies) accounted for — 76 run, 3 NOT-MEASURED'. check:dual-build-cjs-loads and check:lean-entry-closure exited 3 (they need a full build). check:type-check-debt was NOT MEASURED: a 540 s local timeout fired while its closure build waited on the shared verify lock; CI's Type Check · debt ledger passed. Every other family exits 0. CI on PR head bf53824e: all completed success. The required set: Lint & Repo Gates, TypeScript Type Check, Test Core (8/8 shards), Build Core, Governed Surface Queue Guard, Test UI, Live E2E UI and Dogfood Regression Gate. Also Type Check · workspace/source/consumer/debt ledger, Check Changeset, Check PR Size and PR-body guards. The TS2345 red of 293359b5 is cleared.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (Clause-②: no)",
"mcp_calls": "0",
"api_writes": "1 this round — this delta os-dev-report via post-stamped.mjs (relay, POST /repos/objectstack-ai/objectstack/issues/32366/comments). git push is not counted.",
"deviations": "(1) The PR body still says 'warn' and has no S1 or publish-site text. A dev writes the body only at pr_create, so the seat should bring it to this head through seat-post-protocol. (2) Surface added this round: protocol-history-and-publish.ts (three caller arguments) and protocol-publish-drafts-all-or-nothing.test.ts (one assertion block in the existing undo case). Both are forced by the main merge's new callers. (3) In the round-1 session, the S1 ablation was not run before the usage-limit stop; it was run this round, on bf53824e.",
"files_changed": [
".changeset/32366-registry-heal-package-read.md",
"packages/metadata-protocol/src/protocol-class-head.ts",
"packages/metadata-protocol/src/protocol-diff-delete-load.ts",
"packages/metadata-protocol/src/protocol-history-and-publish.ts",
"packages/metadata-protocol/src/protocol-package-commits.ts",
"packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts",
"packages/metadata-protocol/src/protocol-publish-drafts-all-or-nothing.test.ts",
"packages/metadata-protocol/src/protocol-registry-hydration.ts",
"packages/metadata-protocol/src/protocol.registry-heal-package-read.test.ts",
"packages/metadata-protocol/src/protocol.ts",
"scripts/engine-double-contract.pinned.json"
],
"line_budget": "n/a — no skills/** or ratcheted ledger edited",
"open_questions": [],
"security_findings": "none",
"out_of_scope_findings": []
}
PR #32447: patch round 1 clean at bf53824e; patch round 2 carries epic:#31527#2 ruling 6812637203 (#32365 Q2: retire the bare-slot entry after a refused package-less heal, plus two pins). Record on the PR at 2026-11-12T06:19Z.
{
"issue": 32366,
"status": "done",
"round": "patch round 2 (delta)",
"branch": "claude/issue-32366-registry-heal-package-read",
"pr": "https://github.com/objectstack-ai/objectstack/pull/32447",
"head": "6f806d9f437f0f1bfb797d96a5d47888f06ceae7",
"session": "session_012MmDsNv6qb5gT8Pd98AmCB",
"premise_still_valid": true,
"summary": "Ruling 6812637203 Q2 is carried. In restoreArtifactRegistryView's tier 2, when the row is package-less (boundPackageOf(packageId) is undefined, so '' and 'sys_metadata' count) and packageScopedRefusalOf recognises the refused bare read, the removed row's bare-slot entry is retired (removeOverlayEntry on the singular and plural keys, the tier-3 verb, which still refuses a packaged artifact) and no baseline is registered. The error line still prints, now saying the entry was retired (registryHealStoppedMessage's new 'retired' form). A row bound to a package (S1's two-holder non-pair case) keeps the logged stop with the entry kept: the ruling names the package-less heal only (see open_questions). Main merge 0f9b565395 brought a fourth new caller, undoBatchRowWrite in batch-object-registration.ts (#32474), which called with three arguments. It now passes row.packageId. The changeset gains one sentence for Q2.",
"tests": "Head 6f806d9f, one locked run (VERDICT command-exit 0; two earlier attempts were lock queue-timeouts, exit 99, retried under the same slot): metadata-protocol typecheck exit 0; objectql typecheck exit 0; objectql src/protocol-registry-heal-packageless-refusal.test.ts `Tests 2 passed (2)`; metadata-protocol whole package `Test Files 337 passed | 7 skipped (344)`, `Tests 22750 passed | 36 skipped (22786)`. Pin 2 (objectql, real SchemaRegistry plus real MetadataManager, holders only in the service): DELETE of the package-less row leaves the bare slot empty, and the next bare read is 409 RESOURCE_CONFLICT ambiguous-in-environment with candidates exactly [package alpha, package beta]; one error line names both. Pin 3 (objectql, the #32551 shape: alpha and beta in composite slots, a stale package-less bare entry, no row): before the heal the bare read's 409 candidates are [alpha, beta, platform]; after restoreArtifactRegistryView(action, digest, null, null) the bare slot is empty and the candidates are [alpha, beta]. metadata-protocol pins updated: the package-less two-holder delete now retires `action|digest`; N2's '', 'sys_metadata' and undefined each retire too; the two-holder non-pair row keeps tier3 []. Ablation, via ablation-replace --hold, then build, then ablation-dist-preflight: the Q2 guard forced to rethrow (marker ABLATED_Q2_32366 present in dist index.cjs and index.js). objectql pin 2 red, pin 3 green; metadata-protocol package-less and N2 red, 10 green. Restored: blob equals HEAD, git diff HEAD empty, rebuilt, marker absent from all 24 dist files, tree clean. Pin 3 staying green under that ablation is the measurement below, not a gap: in that shape tier 1 lifts the entry.",
"gates": "dispatch-gates --commands on 6f806d9f: 80 families. `--ran`: '80 derived famil(ies) accounted for — 77 run, 3 NOT-MEASURED (2 DERIVED from a recorded exit 3, 1 claimed)'. dual-build-cjs-loads and lean-entry-closure exited 3 (they need a full build). type-check-debt was not run locally: the lock queued past 9 min twice this round; CI's Type Check · debt ledger carries it. check-comment-mask-corpus and check:query-options-erasure first hit a 150/240 s local timeout on the contended box; re-run at 570 s, both exit 0. The three gates CI flagged on 14c8a70b now exit 0 locally: check:engine-double-contract (ledger rows written by --write, commit db6b4f4c), check:where-matcher, and check:objectql-double-limit (the double now refuses any combinator or operator besides equality and $in, and honours limit, the matcher of package-scoped-exact-qualifier.test.ts). CI on PR head 6f806d9f: 35 completed success, 0 failed, the rest skipped.",
"dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (Clause-②: no)",
"mcp_calls": "0",
"api_writes": "1 this round — this delta os-dev-report via post-stamped.mjs (relay, POST /repos/objectstack-ai/objectstack/issues/32366/comments). The PR body was not written (os-dev.md: a dev writes the body only at pr_create); the Patch round 2 text is in deviations for the seat to write.",
"deviations": "(1) SURFACE BREACH, kept and explained: packages/objectql/src/protocol-registry-heal-packageless-refusal.test.ts (test only, no objectql source edited) plus its ledger rows. Why it cannot live in metadata-protocol: the 409 candidate set a bare read names, including the `platform` classification pin 3 asserts, is computed by SchemaRegistry (refuseAmbiguousObjectLessAction and packageScopedHoldersOf, objectql/src/registry.ts). metadata-protocol cannot import objectql, which depends on it (a turbo cycle; the existing #6808 heal pins split the same way, with their behavioural half in objectql). A registry double in metadata-protocol would have to re-implement that classification. The metadata-protocol half of Q2 (the retire call, N2's no-package forms) is pinned beside protocol.registry-heal-package-read.test.ts. (2) Measurement against Zone 1 item 3: in the #32551 shape, the heal's TIER 1 (removeRuntimeShadow) already lifts the stale bare entry. Measured: removeRuntimeShadow returns true, and the keys go from [alpha:digest, beta:digest, digest] to [alpha:digest, beta:digest]. That was true before this change too, so the Q2 retire step is never reached in that shape, and putting the Q2 retire back does not turn pin 3 red. Also, the delete door does not reach the heal for a package-less row of a name code packages ship: it is refused 403 NOT_OVERRIDABLE, sealed against removal. Pin 3 therefore calls the heal directly. The Q2 step's reach is the case where the holders are in the service and not in registry composite slots (pin 2). Measured at the pre-change head: there the bare read already answered the store's 409 after the delete, and the entry stayed in the bare slot until Q2. (3) Proposed PR body section, for the seat to write: '## Patch round 2 (seat ruling 6812637203 Q2). On a refused package-less heal (packageId null, or a value the store reads as none), restoreArtifactRegistryView retires the removed row's bare-slot entry (removeOverlayEntry, which still refuses a packaged artifact) and registers no baseline; the error line says the entry was retired. A row bound to a package keeps the logged stop. Pins: objectql protocol-registry-heal-packageless-refusal.test.ts (pin 2: after DELETE, the bare slot is empty and the next bare read is the store 409 naming exactly the two package holders; pin 3: the #32551 shape, platform named before the heal and not after; in that shape tier 1 lifts the entry); metadata-protocol protocol.registry-heal-package-read.test.ts (the package-less retire and the N2 forms). Ablation: the Q2 guard forced to rethrow, with the marker proven in dist, turned pin 2 and the two metadata-protocol cases red, pin 3 green (tier 1); restored, dist clean. The main merge brought undoBatchRowWrite (batch-object-registration.ts), which now passes row.packageId. The objectql test file is outside the claim surface because the 409 candidate set is computed by objectql SchemaRegistry, which metadata-protocol cannot import.'",
"files_changed": [
".changeset/32366-registry-heal-package-read.md",
"packages/metadata-protocol/src/batch-object-registration.ts",
"packages/metadata-protocol/src/protocol-class-head.ts",
"packages/metadata-protocol/src/protocol-diff-delete-load.ts",
"packages/metadata-protocol/src/protocol-history-and-publish.ts",
"packages/metadata-protocol/src/protocol-package-commits.ts",
"packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts",
"packages/metadata-protocol/src/protocol-publish-drafts-all-or-nothing.test.ts",
"packages/metadata-protocol/src/protocol-registry-hydration.ts",
"packages/metadata-protocol/src/protocol.registry-heal-package-read.test.ts",
"packages/metadata-protocol/src/protocol.ts",
"packages/objectql/src/protocol-registry-heal-packageless-refusal.test.ts",
"scripts/engine-double-contract.pinned.json"
],
"line_budget": "n/a — no skills/** or ratcheted ledger edited",
"open_questions": [
{
"question": "Should Q2's retirement also apply to a row bound to a package that holds no item of the name and pairs no holder, on a name two packages hold? This is S1's no-single-base path. Its bare read is refused just like a package-less row's, and today it stops logged with the removed row's entry kept in the bare slot. The ruling's words name 'a package-less heal' only, so this PR does not extend it.",
"options": [
"A: leave it as is (logged stop, entry kept). Cost: none now; such a row's entry stays in the bare slot until reload.",
"B: extend the retirement to it (the same two removeOverlayEntry calls, plus a pin). Cost: a few lines and one pin; it widens a ruled behaviour beyond its words."
],
"recommendation": "B, because that entry is the removed row's own and nothing serves it any more. Retiring it lets the bare read answer the store's 409 naming every holder, which is the ruling's stated end state. Keeping it serves a deleted body from the registry. But it extends a ruling's words, so it is the seat's call."
}
],
"security_findings": "none",
"out_of_scope_findings": []
}
PR #32447 merged through the queue as 0c8097ec07; Fixes #32366 closed this card as completed. pm:dispatched removed in this stroke; type, priority and domain labels stay. Follow-up carried by #32620 (epic ruling 6818436831, the non-holder retire), now unblocked.
Activity
objectstack-fleet commentedon Nov 11, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage:
findingpromoted →domain:server· p2 ·pm:blocked· 2026-11-11T00:00ZBlocked-by: #32361
⛔ Not a claim.
domain:server(packages/metadata-protocol). It is linked as a child of 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 11, 2026
triage#1 · os-zhuang · session_01AavokzJ5DndAwitDXvKy4U
Path: none
Triage: unblocked →
pm:queue·domain:server· p2 · 2026-11-11T05:57Z⛔ Not a claim.
Generated by Claude Code
objectstack-fleet commentedon Nov 11, 2026
domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB
Claim: PM loop round 1
Session:
session_012MmDsNv6qb5gT8Pd98AmCBAccount:
os-lizheng(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/issue-32366-registry-heal-package-readWorktree:
objectstack-issue-32366Domain:
domain:serverSeat:
domain:server#4File surface:
packages/metadata-protocol/src/protocol-registry-hydration.ts(restoreArtifactRegistryViewand its outer catch),packages/metadata-protocol/src/protocol-discovery-and-bases.ts(readCodeLayerForCarryForward), the callers that must pass a package (protocol-credential-carry-forward.ts,protocol-diff-delete-load.ts,protocol-package-commits.ts,protocol-class-head.ts,protocol-packageless-overlay-migration.ts) only where the slot's package is not already passed, their tests; root.changeset/. ⛔packages/spec/**,packages/objectql/**,packages/metadata/**,ui/**,skills/**,docs/adr/**(stop on breach; explain in the report)Container & model:
M,mode:subagent,model: default tier — dispatch-gates --tier: no path-derived mandate,effort: highClause-②: no
Metadata-protocol: none
Responsibility:
metadata-protocol's registry heal and carry-forward reads in objectstack (platform card) | none — #32361 made the bare read refuse but left these two callers bare | any environment holding one name in two packages (ADR-0150 D3); no first-party pair knownThread-read: 6788345196
Premise: holds (re-read on
origin/main7d82b2d at 2026-11-11T12:54Z)Serial constraints cleared: #32396 (seat 6) and #30020 (seat 3) also edit
packages/metadata-protocol/src/**, inprotocol-history-and-publish.ts/protocol.ts/protocol-host.ts— disjoint files; #32183 (seat 6, PR #32327) — no hunk in this surface's files at this stamp. Mergemainbefore opening the PRobjectstack-fleet commentedon Nov 11, 2026
domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB
os-dev-report
{ "issue": 32366, "status": "done", "branch": "claude/issue-32366-registry-heal-package-read", "pr": "https://github.com/objectstack-ai/objectstack/pull/32447", "session": "session_012MmDsNv6qb5gT8Pd98AmCB", "premise_still_valid": true, "summary": "restoreArtifactRegistryView takes a new REQUIRED packageId (string | null) after organizationId. Its tier-2 MetadataService baseline read now names the package of the removed row, so a reset of package P's row on a name two packages hold restores P's baseline instead of hitting the bare D3 refusal. Callers pass: the delete door's resolved address (two repository-path sites), boundPackageOf(existing.package_id) on the legacy path, commitPackageId in revertCommit, and the step-4 binding in migratePackagelessOverlays. Two callers pass null and say why at the call site: reconcileViewExpansions (a view name is a global literal, bucket 1) and applyRemoteMetadataMutation (the cluster signal carries no binding). The silent outer catch now logs one warn line through registryHealStoppedMessage, and a package-scoped refusal is named as one, with its holders. Premise holds for restoreArtifactRegistryView (the bare read at hydration.ts tier 2, the card's :1421). It does NOT hold for readCodeLayerForCarryForward: on origin/main 7d82b2da it already reads readItemFromMetadataService(type, name, pkg) with the save's package (positive control: args.packageId at the carry-forward site). It reads bare only for a package-less save, where D3's bare read is the right question, and it has no catch, so a refusal there is loud. It lives in protocol-discovery-and-bases.ts, and no slotted type has a redactor (measured getMetadataTypeRedactor: api/agent/skill/action/permission all none). It is unchanged.", "tests": "New packages/metadata-protocol/src/protocol.registry-heal-package-read.test.ts: a real MetadataManager with two packages' action items, a recording registry double, a control-plane kernel. On head 9bcce232: `Tests 6 passed (6)` (lock VERDICT command-exit 0). Whole package at the fix plus the pin (code identical to head; later commits are test-only, plus a main merge touching packages/verify only): `pnpm --filter @objectstack/metadata-protocol exec vitest run --maxWorkers=2` gave `Test Files 334 passed | 5 skipped (339)`, `Tests 22681 passed | 29 skipped (22710)`, VERDICT command-exit 0. `pnpm --filter @objectstack/metadata-protocol typecheck` exit 0 on head (an earlier run was a lock queue-timeout exit 99, NOT MEASURED, and was retried). Ablation through scripts/ablation-replace.mjs, against source imported by relative path (no build): A, tier-2 read back to bare: mutation landed (blob e40047537b4b to e9a937434180); 2 failed and 4 passed, on `expected [] to deeply equal [ [ 'action', 'alpha', … ] ]` and the beta twin; restored, blob equals HEAD and git diff HEAD is empty. B, the catch's console.warn removed: 2 failed and 4 passed (the package-less refusal case and the other-throw case), on `expected [] to have a length of 1 but got +0`; restored the same way. Both directions were predicted.", "gates": "Local: node scripts/pm/dispatch-gates.mjs --commands derived 79 families against merge-base 7d82b2dab (after merging main 0dc84b02). `dispatch-gates --ran` with `:: exit N` lines: '79 derived famil(ies) accounted for — 76 run, 3 NOT-MEASURED'. The 3 NOT MEASURED exited 3 (prerequisite): check:dual-build-cjs-loads and check:lean-entry-closure need a full build; check-plugin-teardown-shape --self-test needs an unshallow clone. check:engine-double-contract exited 1 first (the new pin's double was not in the ledger), was fixed with --write plus a commit, and exits 0 on 9bcce232. check:test-source-alias exited 1 first (no module-top import of @objectstack/lint), was fixed, and exits 0. All other families exit 0, including check:durability-log-level, check:startup-registry-verdict, check:facade-delegation-only, check:nul-bytes, check:doc-authoring and check:closing-keyword-parity. Lint: eslint --no-inline-config on the 7 changed .ts files, exit 0; the repo-wide `pnpm lint` was left to CI (see deviations). CI on PR head 9bcce232 when this report was written: Governed Surface Queue Guard success. Lint (ESLint, PM dispatch-gates, docs & code guards, repo & tooling gates, CI/release/engine gates, scoped), Type Check (workspace, source, consumer, debt ledger), Check Changeset, Check PR Size and PR-body guards were in_progress. Test Core, Build Core and the UI jobs had not reported yet.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (Clause-②: no)", "mcp_calls": "0", "api_writes": "2 — (1) fleet-write relay repository_dispatch carrying pr_create (POST /repos/objectstack-ai/objectstack/pulls draft, plus POST /repos/objectstack-ai/objectstack/issues/32447/assignees os-lizheng), run 43037353879 success, body read back byte-identical (9513 bytes); (2) this os-dev-report comment via post-stamped.mjs (POST /repos/objectstack-ai/objectstack/issues/32366/comments). git push is not counted.", "deviations": "(1) Surface beyond the claim: packages/metadata-protocol/src/protocol.ts gets the class member's signature only, a one-line forwarder of the new parameter that the facade split makes unavoidable; scripts/engine-double-contract.pinned.json gets three rows for the new pin's double, written by the gate's --write. #30020 also edits protocol.ts, so a textual overlap there is possible: the hunk is the restoreArtifactRegistryView forwarder only. (2) The dispatch asked for `pnpm lint`. os-dev.md says the repo-wide lint is CI's, and os-dev.md wins on a conflict, so eslint was run on the changed files only (a declared narrowing). (3) protocol.view-container-reset-expansions.test.ts:478 still calls the member with 3 args through `as any`. At runtime packageId is undefined, which is read as null; it was left unedited.", "files_changed": [ ".changeset/32366-registry-heal-package-read.md", "packages/metadata-protocol/src/protocol-class-head.ts", "packages/metadata-protocol/src/protocol-diff-delete-load.ts", "packages/metadata-protocol/src/protocol-package-commits.ts", "packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts", "packages/metadata-protocol/src/protocol-registry-hydration.ts", "packages/metadata-protocol/src/protocol.registry-heal-package-read.test.ts", "packages/metadata-protocol/src/protocol.ts", "scripts/engine-double-contract.pinned.json" ], "line_budget": "n/a — no skills/** or ratcheted ledger edited", "open_questions": [ { "question": "The peer convergence heal (applyRemoteMetadataMutation) cannot name the removed row's package. The binding is gone from the peer's store, and MetadataMutationEvent carries an address and no binding (its docblock says the wire carries no binding, and must not). On a name two packages hold, its tier-2 read is refused; with this PR that is logged with the holders, where before it was swallowed. Should the signal carry the package?", "options": [ "A: leave it as this PR does. A logged skip; the peer's entry heals on its next reload or the next write of the name. Cost: none now. A multi-replica deployment with a two-holder slotted name keeps a stale registry entry on peers until reload.", "B: add the removed row's package to MetadataMutationEvent and ClusterMetadataMutationPayload, and have the peer pass it. Cost: a cluster wire-contract change (exported types) that reverses the recorded no-binding rule, so it needs a ruling and its own card; small code, about one day with pins." ], "recommendation": "A, because no first-party deployment holds a slotted name in two packages (ADR-0150 measured 0 of 297 on HotCRM), the stale peer entry is now visible in the log rather than silent, and B reverses a recorded wire rule for a case with no measured user. Startup focus: no expansion without pull." } ], "security_findings": "none", "out_of_scope_findings": [ "carrier: none · noted in the PR's Acceptance notes, not filed: applyRemoteMetadataMutation's active-row re-read (repo.get with no packageId) is package-agnostic on a name two packages hold. This is a read-only inference, not reproduced, so it was not filed." ] }objectstack-fleet commentedon Nov 11, 2026
domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB
PR #32447 at
9bcce232: seat checklist clean (record 6798420830 on the PR). ACCEPT waits on the epic #31527 red-line read, owed byepic:#31527#1(post reads vacant); asked on mailbox #32234 at 2026-11-11T14:26Z.objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB
os-dev-report
{ "issue": 32366, "status": "done", "round": "patch round 1 (delta)", "branch": "claude/issue-32366-registry-heal-package-read", "pr": "https://github.com/objectstack-ai/objectstack/pull/32447", "head": "bf53824e5c198a8150261336a4bcb05dc895f201", "session": "session_012MmDsNv6qb5gT8Pd98AmCB", "premise_still_valid": true, "summary": "S1: tier 2 now reads through readHealBaseline in protocol-registry-hydration.ts. It reads the row's own package first. On a settled miss it reads with no package, as main did. A name one package holds therefore restores the one holder's baseline, for an App's pair row as for any non-holder row. On a two-holder refusal, the base is chosen by the pair relation from the refusal's package candidates (isPairRowRemoval, the delete door's predicate, with installedManifestsOfEngine). Exactly one base means its baseline is read; otherwise the refusal is rethrown and logged, and nothing is picked or retired. A clean miss with no holder still retires (tier 3). N1: the heal-stopped line is console.error, and its first line carries the consequence and the fix ('the next reload (or restart) re-hydrates the registry from the stored rows and heals it'). N2: the read and the message use boundPackageOf, and the message prints the refused read's own callerPackageId, so '', 'sys_metadata' and an omitted argument print as 'no package'. N3/S4: the changeset names PACKAGE_SLOTTED_STORE_TYPES, says error not warn, and states the pair-row heal; the test header names the set. Main merge (f0f729e71): the three restoreArtifactRegistryView callers in protocol-history-and-publish.ts (undoLandedPromotions, #32454) passed item.d.member in the new packageId position, the TS2345 CI caught. Each now passes item.packageId (the row restored, the row deleted, and the active row the undo left), with a reason at the call site. S2/S3 untouched (applyRemoteMetadataMutation and package-slotted-store.ts not edited).", "tests": "protocol.registry-heal-package-read.test.ts now has 12 cases. New: S1, a one-holder pair row (tier1 [false], tier2 [alpha], tier3 []); the one-holder CONTROL with a row bound to a non-holder that pairs nobody (the same as main); a two-holder pair row giving the base, never beta; a two-holder non-pair row whose refusal is logged with nothing picked or retired; the no-holder CONTROL that retires in tier 3; and N2, with '', 'sys_metadata' and undefined each printing 'no package'. The logging cases also assert no line at warn. protocol-publish-drafts-all-or-nothing.test.ts, undo case: every heal call's packageId argument equals the published package. On head bf53824e: `pnpm --filter @objectstack/metadata-protocol typecheck && … vitest run --maxWorkers=2` gave VERDICT command-exit 0, `Test Files 335 passed | 5 skipped (340)`, `Tests 22696 passed | 29 skipped (22725)` (the first attempt was a lock queue-timeout, exit 99, retried under the same slot). Ablation through ablation-replace.mjs: S1, the readHealBaseline early return made unconditional (blob 15e06179633b to 9035f64a9a25), gave 4 failed and 8 passed, the four non-holder cases; the no-holder control stayed green as predicted; restored, blob equals HEAD. An earlier attempt was refused by the tool, because the replacement text already occurred in the file, so nothing ran; it was re-run with a unique replacement. Publish undo: the three item.packageId arguments set to null (anchor 3 to 0) gave 1 failed and 8 passed, the undo case; restored, blob equals HEAD.", "gates": "dispatch-gates --commands on bf53824e: 79 families. `--ran`: '79 derived famil(ies) accounted for — 76 run, 3 NOT-MEASURED'. check:dual-build-cjs-loads and check:lean-entry-closure exited 3 (they need a full build). check:type-check-debt was NOT MEASURED: a 540 s local timeout fired while its closure build waited on the shared verify lock; CI's Type Check · debt ledger passed. Every other family exits 0. CI on PR head bf53824e: all completed success. The required set: Lint & Repo Gates, TypeScript Type Check, Test Core (8/8 shards), Build Core, Governed Surface Queue Guard, Test UI, Live E2E UI and Dogfood Regression Gate. Also Type Check · workspace/source/consumer/debt ledger, Check Changeset, Check PR Size and PR-body guards. The TS2345 red of 293359b5 is cleared.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (Clause-②: no)", "mcp_calls": "0", "api_writes": "1 this round — this delta os-dev-report via post-stamped.mjs (relay, POST /repos/objectstack-ai/objectstack/issues/32366/comments). git push is not counted.", "deviations": "(1) The PR body still says 'warn' and has no S1 or publish-site text. A dev writes the body only at pr_create, so the seat should bring it to this head through seat-post-protocol. (2) Surface added this round: protocol-history-and-publish.ts (three caller arguments) and protocol-publish-drafts-all-or-nothing.test.ts (one assertion block in the existing undo case). Both are forced by the main merge's new callers. (3) In the round-1 session, the S1 ablation was not run before the usage-limit stop; it was run this round, on bf53824e.", "files_changed": [ ".changeset/32366-registry-heal-package-read.md", "packages/metadata-protocol/src/protocol-class-head.ts", "packages/metadata-protocol/src/protocol-diff-delete-load.ts", "packages/metadata-protocol/src/protocol-history-and-publish.ts", "packages/metadata-protocol/src/protocol-package-commits.ts", "packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts", "packages/metadata-protocol/src/protocol-publish-drafts-all-or-nothing.test.ts", "packages/metadata-protocol/src/protocol-registry-hydration.ts", "packages/metadata-protocol/src/protocol.registry-heal-package-read.test.ts", "packages/metadata-protocol/src/protocol.ts", "scripts/engine-double-contract.pinned.json" ], "line_budget": "n/a — no skills/** or ratcheted ledger edited", "open_questions": [], "security_findings": "none", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB
PR #32447: patch round 1 clean at
bf53824e; patch round 2 carriesepic:#31527#2ruling 6812637203 (#32365 Q2: retire the bare-slot entry after a refused package-less heal, plus two pins). Record on the PR at 2026-11-12T06:19Z.objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB
os-dev-report
{ "issue": 32366, "status": "done", "round": "patch round 2 (delta)", "branch": "claude/issue-32366-registry-heal-package-read", "pr": "https://github.com/objectstack-ai/objectstack/pull/32447", "head": "6f806d9f437f0f1bfb797d96a5d47888f06ceae7", "session": "session_012MmDsNv6qb5gT8Pd98AmCB", "premise_still_valid": true, "summary": "Ruling 6812637203 Q2 is carried. In restoreArtifactRegistryView's tier 2, when the row is package-less (boundPackageOf(packageId) is undefined, so '' and 'sys_metadata' count) and packageScopedRefusalOf recognises the refused bare read, the removed row's bare-slot entry is retired (removeOverlayEntry on the singular and plural keys, the tier-3 verb, which still refuses a packaged artifact) and no baseline is registered. The error line still prints, now saying the entry was retired (registryHealStoppedMessage's new 'retired' form). A row bound to a package (S1's two-holder non-pair case) keeps the logged stop with the entry kept: the ruling names the package-less heal only (see open_questions). Main merge 0f9b565395 brought a fourth new caller, undoBatchRowWrite in batch-object-registration.ts (#32474), which called with three arguments. It now passes row.packageId. The changeset gains one sentence for Q2.", "tests": "Head 6f806d9f, one locked run (VERDICT command-exit 0; two earlier attempts were lock queue-timeouts, exit 99, retried under the same slot): metadata-protocol typecheck exit 0; objectql typecheck exit 0; objectql src/protocol-registry-heal-packageless-refusal.test.ts `Tests 2 passed (2)`; metadata-protocol whole package `Test Files 337 passed | 7 skipped (344)`, `Tests 22750 passed | 36 skipped (22786)`. Pin 2 (objectql, real SchemaRegistry plus real MetadataManager, holders only in the service): DELETE of the package-less row leaves the bare slot empty, and the next bare read is 409 RESOURCE_CONFLICT ambiguous-in-environment with candidates exactly [package alpha, package beta]; one error line names both. Pin 3 (objectql, the #32551 shape: alpha and beta in composite slots, a stale package-less bare entry, no row): before the heal the bare read's 409 candidates are [alpha, beta, platform]; after restoreArtifactRegistryView(action, digest, null, null) the bare slot is empty and the candidates are [alpha, beta]. metadata-protocol pins updated: the package-less two-holder delete now retires `action|digest`; N2's '', 'sys_metadata' and undefined each retire too; the two-holder non-pair row keeps tier3 []. Ablation, via ablation-replace --hold, then build, then ablation-dist-preflight: the Q2 guard forced to rethrow (marker ABLATED_Q2_32366 present in dist index.cjs and index.js). objectql pin 2 red, pin 3 green; metadata-protocol package-less and N2 red, 10 green. Restored: blob equals HEAD, git diff HEAD empty, rebuilt, marker absent from all 24 dist files, tree clean. Pin 3 staying green under that ablation is the measurement below, not a gap: in that shape tier 1 lifts the entry.", "gates": "dispatch-gates --commands on 6f806d9f: 80 families. `--ran`: '80 derived famil(ies) accounted for — 77 run, 3 NOT-MEASURED (2 DERIVED from a recorded exit 3, 1 claimed)'. dual-build-cjs-loads and lean-entry-closure exited 3 (they need a full build). type-check-debt was not run locally: the lock queued past 9 min twice this round; CI's Type Check · debt ledger carries it. check-comment-mask-corpus and check:query-options-erasure first hit a 150/240 s local timeout on the contended box; re-run at 570 s, both exit 0. The three gates CI flagged on 14c8a70b now exit 0 locally: check:engine-double-contract (ledger rows written by --write, commit db6b4f4c), check:where-matcher, and check:objectql-double-limit (the double now refuses any combinator or operator besides equality and $in, and honours limit, the matcher of package-scoped-exact-qualifier.test.ts). CI on PR head 6f806d9f: 35 completed success, 0 failed, the rest skipped.", "dogfood_battery": "n/a — no `!` in the PR title and no (narrowing) arm (Clause-②: no)", "mcp_calls": "0", "api_writes": "1 this round — this delta os-dev-report via post-stamped.mjs (relay, POST /repos/objectstack-ai/objectstack/issues/32366/comments). The PR body was not written (os-dev.md: a dev writes the body only at pr_create); the Patch round 2 text is in deviations for the seat to write.", "deviations": "(1) SURFACE BREACH, kept and explained: packages/objectql/src/protocol-registry-heal-packageless-refusal.test.ts (test only, no objectql source edited) plus its ledger rows. Why it cannot live in metadata-protocol: the 409 candidate set a bare read names, including the `platform` classification pin 3 asserts, is computed by SchemaRegistry (refuseAmbiguousObjectLessAction and packageScopedHoldersOf, objectql/src/registry.ts). metadata-protocol cannot import objectql, which depends on it (a turbo cycle; the existing #6808 heal pins split the same way, with their behavioural half in objectql). A registry double in metadata-protocol would have to re-implement that classification. The metadata-protocol half of Q2 (the retire call, N2's no-package forms) is pinned beside protocol.registry-heal-package-read.test.ts. (2) Measurement against Zone 1 item 3: in the #32551 shape, the heal's TIER 1 (removeRuntimeShadow) already lifts the stale bare entry. Measured: removeRuntimeShadow returns true, and the keys go from [alpha:digest, beta:digest, digest] to [alpha:digest, beta:digest]. That was true before this change too, so the Q2 retire step is never reached in that shape, and putting the Q2 retire back does not turn pin 3 red. Also, the delete door does not reach the heal for a package-less row of a name code packages ship: it is refused 403 NOT_OVERRIDABLE, sealed against removal. Pin 3 therefore calls the heal directly. The Q2 step's reach is the case where the holders are in the service and not in registry composite slots (pin 2). Measured at the pre-change head: there the bare read already answered the store's 409 after the delete, and the entry stayed in the bare slot until Q2. (3) Proposed PR body section, for the seat to write: '## Patch round 2 (seat ruling 6812637203 Q2). On a refused package-less heal (packageId null, or a value the store reads as none), restoreArtifactRegistryView retires the removed row's bare-slot entry (removeOverlayEntry, which still refuses a packaged artifact) and registers no baseline; the error line says the entry was retired. A row bound to a package keeps the logged stop. Pins: objectql protocol-registry-heal-packageless-refusal.test.ts (pin 2: after DELETE, the bare slot is empty and the next bare read is the store 409 naming exactly the two package holders; pin 3: the #32551 shape, platform named before the heal and not after; in that shape tier 1 lifts the entry); metadata-protocol protocol.registry-heal-package-read.test.ts (the package-less retire and the N2 forms). Ablation: the Q2 guard forced to rethrow, with the marker proven in dist, turned pin 2 and the two metadata-protocol cases red, pin 3 green (tier 1); restored, dist clean. The main merge brought undoBatchRowWrite (batch-object-registration.ts), which now passes row.packageId. The objectql test file is outside the claim surface because the 409 candidate set is computed by objectql SchemaRegistry, which metadata-protocol cannot import.'", "files_changed": [ ".changeset/32366-registry-heal-package-read.md", "packages/metadata-protocol/src/batch-object-registration.ts", "packages/metadata-protocol/src/protocol-class-head.ts", "packages/metadata-protocol/src/protocol-diff-delete-load.ts", "packages/metadata-protocol/src/protocol-history-and-publish.ts", "packages/metadata-protocol/src/protocol-package-commits.ts", "packages/metadata-protocol/src/protocol-packageless-overlay-migration.ts", "packages/metadata-protocol/src/protocol-publish-drafts-all-or-nothing.test.ts", "packages/metadata-protocol/src/protocol-registry-hydration.ts", "packages/metadata-protocol/src/protocol.registry-heal-package-read.test.ts", "packages/metadata-protocol/src/protocol.ts", "packages/objectql/src/protocol-registry-heal-packageless-refusal.test.ts", "scripts/engine-double-contract.pinned.json" ], "line_budget": "n/a — no skills/** or ratcheted ledger edited", "open_questions": [ { "question": "Should Q2's retirement also apply to a row bound to a package that holds no item of the name and pairs no holder, on a name two packages hold? This is S1's no-single-base path. Its bare read is refused just like a package-less row's, and today it stops logged with the removed row's entry kept in the bare slot. The ruling's words name 'a package-less heal' only, so this PR does not extend it.", "options": [ "A: leave it as is (logged stop, entry kept). Cost: none now; such a row's entry stays in the bare slot until reload.", "B: extend the retirement to it (the same two removeOverlayEntry calls, plus a pin). Cost: a few lines and one pin; it widens a ruled behaviour beyond its words." ], "recommendation": "B, because that entry is the removed row's own and nothing serves it any more. Retiring it lets the bare read answer the store's 409 naming every holder, which is the ruling's stated end state. Keeping it serves a deleted body from the registry. But it extends a ruling's words, so it is the seat's call." } ], "security_findings": "none", "out_of_scope_findings": [] }objectstack-fleet commentedon Nov 12, 2026
domain:server#4 · os-lizheng · session_012MmDsNv6qb5gT8Pd98AmCB
Landed · 2026-11-12T11:59Z
PR #32447 merged through the queue as
0c8097ec07;Fixes #32366closed this card as completed.pm:dispatchedremoved in this stroke; type, priority and domain labels stay. Follow-up carried by #32620 (epic ruling 6818436831, the non-holder retire), now unblocked.fix: pass the declined row's known package-less binding to the heal w…