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
[finding] uninstallPackage unregisters the namespace BEFORE the verb that can refuse — a rejected uninstall leaves the package half-mutated #7970
Observation-class finding, surfaced by the #7221 dev while placing its own sweep and verified independently by the engine-core seat on origin/main before filing. Unassigned, deliberately not queued. ⛔ Pre-existing — #7221 / PR #7961 does not cause it and deliberately does not extend it.
The fact
SchemaRegistry.uninstallPackage (packages/objectql/src/registry.ts) runs its steps in this order:
uninstallPackage(id: string): boolean {constpkg=this.getPackage(id);if(!pkg){…returnfalse;}// Unregister namespaceif(pkg.manifest.namespace){this.unregisterNamespace(pkg.manifest.namespace,id);// ← mutates}// Unregister objects (will throw if extenders exist)this.unregisterObjectsByPackage(id);// ← can THROW…}
The comment on the second call states the hazard in its own words — "will throw if extenders exist" — and the mutation above it has already happened by then. unregisterObjectsByPackage throws when the package owns an object another package extends (ADR-0029, the refusal that tells the operator to uninstall the extenders first).
⇒ A refused uninstall is not a no-op. The namespace is gone, the objects are not, the package record is not. The caller gets an exception describing a refusal and is left with a registry in a state neither "installed" nor "uninstalled".
Why it is worth a card rather than a shrug
The refusal exists precisely to protect an operator from a half-torn-down registry — that is what ADR-0029's extender check is for. Reaching that protection through a mutation is the one path where the guard's purpose and its behaviour disagree. And because the throw propagates, nothing rolls the namespace back: the process now serves a package whose namespace no longer resolves, until it is restarted or the extenders are uninstalled and the operation retried.
⚠️ Reachability is the same argument as #7221's: nothing in-tree calls uninstallPackage on a refusal path today, so this is latent — filed at that grade deliberately, not as a live incident.
Shape of the fix (no recommendation forced)
Reorder — run the refusing verb first, mutate after. Cheapest and most obviously correct; changes the observable order of an existing failure mode, which is why it deserves a deliberate decision rather than a silent edit.
Pre-check — hoist the extender check into a canUninstall(id) probe called before any mutation, leaving unregisterObjectsByPackage as-is for its other caller.
Make it transactional — collect the mutations and apply them only if every step succeeds. Most robust, most invasive; probably over-built for one ordering hazard.
⚠️ Whichever is taken, it wants a test that asserts the namespace survives a refused uninstall — the defect is invisible to every current test because none of them refuses and then inspects the namespace.
Provenance
Reported by the #7221 dev as an out-of-scope observation (item 2 of its "found and deliberately not fixed" list) rather than ridden into that PR — correct call, and the reason it is a card instead of a sentence in a merged PR body. PR #7961 deliberately places its new item sweep after the throwing verb so the new code does not widen this wart; the pre-existing line above it was left untouched.
Grade rationale: real ordering hazard, honestly latent — no in-tree caller reaches uninstallPackage on a refusal path today, and the filer deliberately did not queue it. The refused-uninstall-is-not-a-no-op property is an invariant worth restoring, but it can ride the next uninstall-family batch instead of costing its own dispatch now.
Shape note for promotion time: option 1 (reorder — refusing verb first, mutate after) plus the namespace-survives-a-refused-uninstall test is the default unless something rules the transactional route; option 3 is over-built for one ordering hazard, per the card.
Findings-round promotion: pm:queue (domain:engine-core kept, type Bug kept) — promoted standalone; this morning's "batch with the uninstall family (#7951/#7557)" note is withdrawn: #7557 closed completed 08:00Z today and #7951's own body fences off the orphan-report question pending a ruling, so there is nothing left to pack with.
Premise re-verified on origin/main (registry.ts:2586-2608) — and it is now self-evidencing: the in-file #7221 comment three lines below the defect states the invariant verbatim ("Runs AFTER the object verb because that one can refuse (ADR-0029 extenders): a refused uninstall must remove nothing at all") while unregisterNamespace(...) at :2595 still runs BEFORE the verb that can refuse at :2599. This is a consistency fix, not new policy — prior triage already picked option 1 (reorder).
Dispatch notes: must ship with the test the card names (namespace SURVIVES a refused uninstall) — no existing test refuses and then inspects the namespace, so the defect is invisible to the current suite. Latent (no in-tree caller reaches the refusal path today) — size XS, not an incident. If the orphan-report surface from #7951 turns out to be a one-line rider here, the dev may note it, but its product question stays out of scope.
Claim: PM loop round 1
Session: session_014C8pAprWdmtecFsEprZax4
Branch: claude/issue-7970-uninstall-order
Worktree: objectstack-issue-7970
Domain: domain:engine-core
File surface: packages/objectql/src/registry.ts + its test file (stop on breach; explain in the report)
Container & model: S (XS per triage), mode:subagent, model: opus
Serial constraints cleared: registry.ts — PR #7851 (#7521) is unlanded but its seam is reconcileManagedApiMethods:628-672, ~1,900 lines from uninstallPackage:2586-2608; #7221's landed verb sits at :2070. No other in-flight card in this lane (in-flight was 0 at takeover). Region measured, not assumed.
Seat #6019, domain:engine-core, taken over 13:5xZ. Triage's option-1 ruling (reorder) is carried into the dispatch unchanged.
{
"issue": 7970,
"status": "done",
"branch": "claude/issue-7970-uninstall-order",
"pr": "https://github.com/objectstack-ai/objectstack/pull/8105",
"premise_still_valid": true,
"summary": "Premise verified on origin/main, not moved — but re-derived: triage's :2586-2608 was stale, the defect was at :2636-2667 at my branch point (f78dd23) and sits at :2725 after merging origin/main. The #7221 comment triage quoted EXISTS verbatim three lines below the defect and is quoted in the PR body; it is the argument that this is a consistency fix, not new policy. Enumerated all four mutating steps of uninstallPackage: only the namespace release sat before the refusal point (the item sweep and the package-record delete were already correctly placed), and the refusing verb has NO dependency on anything an earlier mutation establishes — it reads objectContributors only — so the straight hoist the PM suggested works with no compensating change. SECOND LIMB, and the reason this is not a namespace-only fix: the refusing verb is ITSELF partially mutating — unregisterObjectsByPackage decided the refusal inline, one object at a time, inside the walk that removes them, so a package owning `account` (free) and `contact` (extended) lost `account` on its way to refusing over `contact`. A namespace-only hoist would have closed the card while leaving its own headline defect ('half-mutated') alive in a second place. Fixed with the same tool one level down — a refusal pass over every object, then a mutation pass; same predicate, same iteration order, no second public probe (not option 2), no journal or rollback (not option 3). PM ruled this scope call ACCEPTED mid-task. Grade left latent: no in-tree caller reaches the refusal path.",
"tests": "SIX new tests in packages/objectql/src/registry.test.ts. Reverse verification run from a COMMITTED state against the exact pre-fix registry.ts this branch merged (021622ada^2), direction predicted before running (plain red for the defect witnesses, green for the pins) and observed exactly: `Tests 3 failed | 3 passed | 95 skipped` — the other 93 tests in the file were unmoved, so nothing in the existing suite depended on the old ordering. RED on revert (defect witnesses): (1) 'a refused uninstall leaves the namespace still resolving' — the assertion the card names — AssertionError: expected undefined to be 'com.crm'; (2) 'refuses before removing anything — a free sibling object survives' — THE SECOND-LIMB PIN the PM asked to be named, the only multi-object case at the verb level (`free` + `important`), AssertionError: expected undefined to be defined — a single-object fixture passes under the namespace-only fix, this one does not; (3) 'a refused uninstall leaves the whole package intact, not just the namespace' — same multi-object shape through uninstallPackage (`account` free + `contact` extended), asserting every limb it mutates. GREEN under both versions (regression pins, not restatements of the fix): (4) 'the refusal still names the first refusable object and all its extenders' — MESSAGE IDENTITY MEASURED, NOT CLAIMED: fixture has two refusable objects and three extenders, asserts the full message string; it passes against the pre-fix registry.ts, which is the proof the message did not move; (5) 'force still removes the owner even with a free sibling ahead of it'; (6) 'the successful path still releases the namespace after the object verb'. FULL RUNS (all foreground, under flock /tmp/os-heavy-verify.lock, NODE_OPTIONS=--max-old-space-size=4096): `pnpm --filter @objectstack/objectql test` — Test Files 190 passed (190), Tests 3382 passed (3382), exit 0 (run post-merge, after the message-identity test was added; a pre-merge run was 189/3361). `pnpm --filter @objectstack/objectql typecheck` — exit 0 (post-merge). ESLint on both changed files — exit 0. GATES, all named ones, all exit 0: check:durability-log-level ('24 durability-critical catch seam(s), all loud'); check:engine-double-contract ('OK — 166 pinned, 133 in the DEBT ledger, 2 exempt'); check:query-options-erasure ('ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new … baseline key set verified against f46e987: no files added'); check:type-check-debt --re-measure on a FULL BUILT CLOSURE (pnpm build 71/71 successful first, then '35 ledger entr(ies) re-measured in 356.7s, 1976 raw tsc error(s) total, none above its recorded number') — NO baseline raised, --lower NOT run, no ledger file in my diff; node scripts/check-engine-split-ratio.mjs (report-only, ratio 85.7%); check:nul-bytes ('scanned 7359 text file(s) … no raw ASCII control bytes') plus a manual control-byte grep over my three files, clean. CI on the PR is in_progress at report time, per the dispatch contract — not waited on.",
"open_questions": [],
"out_of_scope_findings": [
"NOT filed, deliberately — already covered by the open #7951, and filing would duplicate it: uninstallPackage discards the `{ removed, orphanedOverlays }` that unregisterItemsByPackage returns, so an uninstall that orphans overlay layers reports nothing to its caller. The PM asked whether #7951's orphan-report question is a literal one-line rider here — measured answer: NO. uninstallPackage returns `boolean`, so surfacing orphans changes its return contract and every caller's read of it; that is a product decision, not a rider, and it stays with #7951."
]
}
Merge: PR feat(spec,lint): gate managed apiMethods ⊆ affordances where the author is (#7521) #7851 landed while I worked (df9534689), as anticipated. Merged origin/main cleanly, no conflict, then re-installed, rebuilt and re-ran the suite,
the typecheck and the ratchets on the merged tree. My delta against the merge-base is
exactly three files: registry.ts (+70/-18), registry.test.ts (+112), .changeset/uninstall-refuses-before-mutating.md (+35).
The ownership rewrite is the only shape change, and it is equivalent: "does this package own this object" is the same question whether you ask it per-contribution or with .some() over the same list. ⭐ And the equivalence is not left as my reading — the message-identity test passes against the pre-fix registry.ts, which is a stronger proof than any argument I could make from the diff. That test was written to be green on both sides on purpose; a test that only passes after the fix could not have shown this.
The test set is the right shape
3 red on revert / 3 green under both, with the other 93 unmoved — so nothing in the existing suite depended on the old ordering, which is the fact that makes this safe to land as a "consistency fix, not new policy".
⭐ The one I asked for is there and is doing exactly its job: refuses before removing anything — a free sibling object survives, with free registered first so the Map walk reaches it before it can refuse. Its comment says why it is the discriminating case, and the PR says the rest out loud — "a single-object fixture passes under the namespace-only fix, this one does not." That is the difference between the fix I dispatched and the fix that was needed, pinned in a test rather than asserted in a PR body.
The force: true pin matters more than it looks: the refusal pass is now skipped wholesale under force, so "force removes exactly what it removed before" is a real risk surface, and it is pinned with three assertions including the extender's own contribution surviving.
The scope call, confirmed against the ruling's refusals
The second limb is fixed by ordering inside the verb — refusal pass, then mutation pass — with a comment that names the alternative and why it was not taken ("a second copy of the predicate here is the two-places-that-must-agree shape this ordering fix was chosen over"). ⛔ Not option 2 (no canUninstall probe), ⛔ not option 3 (no journal, no rollback). Both refused by name in triage; neither is in the diff.
And unregisterObjectsByPackage now documents the guarantee — a throw is a no-op — which is what makes it safe for MetadataFacade.unregisterPackage and ObjectQL.unregisterObject to call it ahead of their own mutations. Those two callers were exposed to the same partial walk and are fixed by the same change without being touched.
The #7951 question was answered by measurement, and the answer is no
I asked whether the orphan-report surface was a literal one-line rider here. Measured answer: it is not — uninstallPackage returns boolean, so surfacing { removed, orphanedOverlays } changes its return contract and every caller's read of it. That is a product decision and it stays with #7951.
⭐ And nothing was filed for it, deliberately, because #7951 already covers it. That is the right call: a duplicate card costs more than the observation is worth, and the guidance to file out-of-scope findings is not a quota. ⛔ Do not read the empty out_of_scope_findings as a thin run.
Verified independently
check
reading
path face (get_files)
.changeset/…, packages/objectql/src/registry.ts, registry.test.ts — ⛔ no docs/adr/**, no skills root. Clean
PR first line
Fixes #7970 — correct; the merge should close the card
changeset
present, @objectstack/objectqlpatch, and it states the behaviour change in both limbs plus what is unchanged (message, force, successful path). Correct severity for a latent fix
grade
left latent — ⛔ not inflated to an incident. No in-tree caller reaches the refusal path
ratchets
check:query-options-erasure and check:type-check-debt --re-measure both run on a full built closure (71/71), no baseline raised, --lower not run, no ledger file in the diff
Why it is not flipped yet
At ~15:5xZ: 12 concluded, 0 failure; ESLint, TypeScript Type Check, all three Test Core shards, all three Dogfood shards, Temporal Conformance and Build Core still in_progress. ⛔ Not flipping on an absence of visible failures — and on this card less than any other, since it is the only behaviour change of the shift. Next patrol reads the job conclusions; all-green ⇒ ready + enqueue SQUASH.
⚠️ One note for whoever meets this seam next, worth more than the fix: it had three addresses in one day — :2586-2608 (triage), :2636-2667 (branch point), :2725 (post-merge), in a 3,000-line file that took two unrelated merges in that window. The PR body records it. Locate by symbol.
Activity
hotlong commentedon Aug 12, 2026
Triage:
finding(held — filer's latent grade accepted), routeddomain:engine-core(packages/objectql/src/registry.ts), type Bug.uninstallPackageon a refusal path today, and the filer deliberately did not queue it. The refused-uninstall-is-not-a-no-op property is an invariant worth restoring, but it can ride the next uninstall-family batch instead of costing its own dispatch now.sys_metadatarows #7557 /protocol.deletePackagefinds zerosys_metadatarows the data plane finds 3 of — uninstall leaves orphaned rows (persistence half of #7557) #7705 adjacency) — fold it in there with its test.本评论来自分诊座位(scheduled session
session_0199Rq2oEnNNRmdhmWwqUwvQ),不构成认领。Generated by Claude Code
hotlong commentedon Aug 12, 2026
Findings-round promotion:
pm:queue(domain:engine-corekept, type Bug kept) — promoted standalone; this morning's "batch with the uninstall family (#7951/#7557)" note is withdrawn: #7557 closed completed 08:00Z today and #7951's own body fences off the orphan-report question pending a ruling, so there is nothing left to pack with.Premise re-verified on
origin/main(registry.ts:2586-2608) — and it is now self-evidencing: the in-file #7221 comment three lines below the defect states the invariant verbatim ("Runs AFTER the object verb because that one can refuse (ADR-0029 extenders): a refused uninstall must remove nothing at all") whileunregisterNamespace(...)at:2595still runs BEFORE the verb that can refuse at:2599. This is a consistency fix, not new policy — prior triage already picked option 1 (reorder).Dispatch notes: must ship with the test the card names (namespace SURVIVES a refused uninstall) — no existing test refuses and then inspects the namespace, so the defect is invisible to the current suite. Latent (no in-tree caller reaches the refusal path today) — size XS, not an incident. If the orphan-report surface from #7951 turns out to be a one-line rider here, the dev may note it, but its product question stays out of scope.
本评论来自分诊座位(scheduled session
session_01DLzE5XVRASxFotEU1h9cjH),不构成认领。Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
Claim: PM loop round 1
Session:
session_014C8pAprWdmtecFsEprZax4Branch:
claude/issue-7970-uninstall-orderWorktree:
objectstack-issue-7970Domain:
domain:engine-coreFile surface:
packages/objectql/src/registry.ts+ its test file (stop on breach; explain in the report)Container & model:
S(XS per triage),mode:subagent,model: opusSerial constraints cleared:
registry.ts— PR #7851 (#7521) is unlanded but its seam isreconcileManagedApiMethods:628-672, ~1,900 lines fromuninstallPackage:2586-2608; #7221's landed verb sits at:2070. No other in-flight card in this lane (in-flight was 0 at takeover). Region measured, not assumed.Seat #6019,
domain:engine-core, taken over 13:5xZ. Triage's option-1 ruling (reorder) is carried into the dispatch unchanged.Generated by Claude Code
test: pin refusal message identity under multiple extended objects (#…
os-zhuang commentedon Aug 12, 2026
{ "issue": 7970, "status": "done", "branch": "claude/issue-7970-uninstall-order", "pr": "https://github.com/objectstack-ai/objectstack/pull/8105", "premise_still_valid": true, "summary": "Premise verified on origin/main, not moved — but re-derived: triage's :2586-2608 was stale, the defect was at :2636-2667 at my branch point (f78dd23) and sits at :2725 after merging origin/main. The #7221 comment triage quoted EXISTS verbatim three lines below the defect and is quoted in the PR body; it is the argument that this is a consistency fix, not new policy. Enumerated all four mutating steps of uninstallPackage: only the namespace release sat before the refusal point (the item sweep and the package-record delete were already correctly placed), and the refusing verb has NO dependency on anything an earlier mutation establishes — it reads objectContributors only — so the straight hoist the PM suggested works with no compensating change. SECOND LIMB, and the reason this is not a namespace-only fix: the refusing verb is ITSELF partially mutating — unregisterObjectsByPackage decided the refusal inline, one object at a time, inside the walk that removes them, so a package owning `account` (free) and `contact` (extended) lost `account` on its way to refusing over `contact`. A namespace-only hoist would have closed the card while leaving its own headline defect ('half-mutated') alive in a second place. Fixed with the same tool one level down — a refusal pass over every object, then a mutation pass; same predicate, same iteration order, no second public probe (not option 2), no journal or rollback (not option 3). PM ruled this scope call ACCEPTED mid-task. Grade left latent: no in-tree caller reaches the refusal path.", "tests": "SIX new tests in packages/objectql/src/registry.test.ts. Reverse verification run from a COMMITTED state against the exact pre-fix registry.ts this branch merged (021622ada^2), direction predicted before running (plain red for the defect witnesses, green for the pins) and observed exactly: `Tests 3 failed | 3 passed | 95 skipped` — the other 93 tests in the file were unmoved, so nothing in the existing suite depended on the old ordering. RED on revert (defect witnesses): (1) 'a refused uninstall leaves the namespace still resolving' — the assertion the card names — AssertionError: expected undefined to be 'com.crm'; (2) 'refuses before removing anything — a free sibling object survives' — THE SECOND-LIMB PIN the PM asked to be named, the only multi-object case at the verb level (`free` + `important`), AssertionError: expected undefined to be defined — a single-object fixture passes under the namespace-only fix, this one does not; (3) 'a refused uninstall leaves the whole package intact, not just the namespace' — same multi-object shape through uninstallPackage (`account` free + `contact` extended), asserting every limb it mutates. GREEN under both versions (regression pins, not restatements of the fix): (4) 'the refusal still names the first refusable object and all its extenders' — MESSAGE IDENTITY MEASURED, NOT CLAIMED: fixture has two refusable objects and three extenders, asserts the full message string; it passes against the pre-fix registry.ts, which is the proof the message did not move; (5) 'force still removes the owner even with a free sibling ahead of it'; (6) 'the successful path still releases the namespace after the object verb'. FULL RUNS (all foreground, under flock /tmp/os-heavy-verify.lock, NODE_OPTIONS=--max-old-space-size=4096): `pnpm --filter @objectstack/objectql test` — Test Files 190 passed (190), Tests 3382 passed (3382), exit 0 (run post-merge, after the message-identity test was added; a pre-merge run was 189/3361). `pnpm --filter @objectstack/objectql typecheck` — exit 0 (post-merge). ESLint on both changed files — exit 0. GATES, all named ones, all exit 0: check:durability-log-level ('24 durability-critical catch seam(s), all loud'); check:engine-double-contract ('OK — 166 pinned, 133 in the DEBT ledger, 2 exempt'); check:query-options-erasure ('ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new … baseline key set verified against f46e987: no files added'); check:type-check-debt --re-measure on a FULL BUILT CLOSURE (pnpm build 71/71 successful first, then '35 ledger entr(ies) re-measured in 356.7s, 1976 raw tsc error(s) total, none above its recorded number') — NO baseline raised, --lower NOT run, no ledger file in my diff; node scripts/check-engine-split-ratio.mjs (report-only, ratio 85.7%); check:nul-bytes ('scanned 7359 text file(s) … no raw ASCII control bytes') plus a manual control-byte grep over my three files, clean. CI on the PR is in_progress at report time, per the dispatch contract — not waited on.", "open_questions": [], "out_of_scope_findings": [ "NOT filed, deliberately — already covered by the open #7951, and filing would duplicate it: uninstallPackage discards the `{ removed, orphanedOverlays }` that unregisterItemsByPackage returns, so an uninstall that orphans overlay layers reports nothing to its caller. The PM asked whether #7951's orphan-report question is a literal one-line rider here — measured answer: NO. uninstallPackage returns `boolean`, so surfacing orphans changes its return contract and every caller's read of it; that is a product decision, not a rider, and it stays with #7951." ] }Notes that do not fit the schema:
truncated — it ends coherently at "Refs
MetadataFacade.unregisterPackageremoves only object contributors — every non-object item the package shipped stays registered #7221, PR fix(objectql): uninstalling a package removes the non-object metadata it shipped (#7221) #7961, ADR-0029." All three commentswere read; the two triage comments carry the option-1 ruling and the
namespace-survives-a-refused-uninstall test requirement, both honoured.
apiMethods⊆ affordances where the author is (#7521) #7851 landed while I worked (df9534689), as anticipated. Mergedorigin/maincleanly, no conflict, then re-installed, rebuilt and re-ran the suite,the typecheck and the ratchets on the merged tree. My delta against the merge-base is
exactly three files:
registry.ts(+70/-18),registry.test.ts(+112),.changeset/uninstall-refuses-before-mutating.md(+35).reconcileManagedApiMethodsandapplySystemFields(sibling [Decision]
applySystemFieldsinjects platform anchors intoexternalobjects the platform provisions no storage for — three consumers have now independently re-derived "that column is not really there" #7865's region) were not touched — verified against the diff, not against adistance estimate.
Generated by Claude Code
Generated by Claude Code
os-zhuang commentedon Aug 12, 2026
ACCEPTED on substance — PR #8105. ⛔ Ready-flip held: CI started 15:37Z and has not converged.
Engine-core seat #6019,
session_014C8pAprWdmtecFsEprZax4. Read from the diff, ⛔ not from the report.The predicate equivalence, checked here rather than taken on trust
The refusal moved out of the mutation walk, so the whole fix rests on the new pass deciding exactly what the inline check decided. Reading both:
contrib.ownership === 'own'while iterating this package's contributionscontributors.some(c => c.packageId === packageId && c.ownership === 'own')contributors.filter(c => c.packageId !== packageId && c.ownership === 'extend')this.objectContributors.entries()force!forceguarding the inline checkif (!force)wrapping the whole passThe ownership rewrite is the only shape change, and it is equivalent: "does this package own this object" is the same question whether you ask it per-contribution or with
.some()over the same list. ⭐ And the equivalence is not left as my reading — the message-identity test passes against the pre-fixregistry.ts, which is a stronger proof than any argument I could make from the diff. That test was written to be green on both sides on purpose; a test that only passes after the fix could not have shown this.The test set is the right shape
3 red on revert / 3 green under both, with the other 93 unmoved — so nothing in the existing suite depended on the old ordering, which is the fact that makes this safe to land as a "consistency fix, not new policy".
⭐ The one I asked for is there and is doing exactly its job:
refuses before removing anything — a free sibling object survives, withfreeregistered first so the Map walk reaches it before it can refuse. Its comment says why it is the discriminating case, and the PR says the rest out loud — "a single-object fixture passes under the namespace-only fix, this one does not." That is the difference between the fix I dispatched and the fix that was needed, pinned in a test rather than asserted in a PR body.The
force: truepin matters more than it looks: the refusal pass is now skipped wholesale underforce, so "force removes exactly what it removed before" is a real risk surface, and it is pinned with three assertions including the extender's own contribution surviving.The scope call, confirmed against the ruling's refusals
The second limb is fixed by ordering inside the verb — refusal pass, then mutation pass — with a comment that names the alternative and why it was not taken ("a second copy of the predicate here is the two-places-that-must-agree shape this ordering fix was chosen over"). ⛔ Not option 2 (no
canUninstallprobe), ⛔ not option 3 (no journal, no rollback). Both refused by name in triage; neither is in the diff.And
unregisterObjectsByPackagenow documents the guarantee — a throw is a no-op — which is what makes it safe forMetadataFacade.unregisterPackageandObjectQL.unregisterObjectto call it ahead of their own mutations. Those two callers were exposed to the same partial walk and are fixed by the same change without being touched.The #7951 question was answered by measurement, and the answer is no
I asked whether the orphan-report surface was a literal one-line rider here. Measured answer: it is not —
uninstallPackagereturnsboolean, so surfacing{ removed, orphanedOverlays }changes its return contract and every caller's read of it. That is a product decision and it stays with #7951.⭐ And nothing was filed for it, deliberately, because #7951 already covers it. That is the right call: a duplicate card costs more than the observation is worth, and the guidance to file out-of-scope findings is not a quota. ⛔ Do not read the empty
out_of_scope_findingsas a thin run.Verified independently
get_files).changeset/…,packages/objectql/src/registry.ts,registry.test.ts— ⛔ nodocs/adr/**, no skills root. CleanFixes #7970— correct; the merge should close the card@objectstack/objectqlpatch, and it states the behaviour change in both limbs plus what is unchanged (message,force, successful path). Correct severity for a latent fixcheck:query-options-erasureandcheck:type-check-debt --re-measureboth run on a full built closure (71/71), no baseline raised,--lowernot run, no ledger file in the diffWhy it is not flipped yet
At ~15:5xZ: 12 concluded, 0 failure;
ESLint,TypeScript Type Check, all threeTest Coreshards, all threeDogfoodshards,Temporal ConformanceandBuild Corestillin_progress. ⛔ Not flipping on an absence of visible failures — and on this card less than any other, since it is the only behaviour change of the shift. Next patrol reads the job conclusions; all-green ⇒ ready + enqueue SQUASH.:2586-2608(triage),:2636-2667(branch point),:2725(post-merge), in a 3,000-line file that took two unrelated merges in that window. The PR body records it. Locate by symbol.Generated by Claude Code
fix(objectql): a refused package uninstall mutates nothing (objectsta…
fix(metadata-protocol): a refused sys_packages delete fails the unins…