refactor(errors): require a reason for every analysis cancellation (#10566)

* refactor(errors): require a reason for every analysis cancellation

Add cancelledBy, the one constructor for AnalysisCancelledError. Its
reason argument is required: an aborted signal, an earlier cancellation,
a reason carried from an inner failure, or an OwnerCancellation when REA
closed the work's owner itself.

Closed adb, apktool, firmware, sqlite, gdb, web-bundle analyzer and
binary-session record owners now report OwnerCancellation.ownerClosed
instead of an absent reason. Firmware's predecessor wait returns its
cancellation as a Result, removing an unreachable reason-less fallback.

An architecture guard rejects new direct constructions, with a counted
per-file baseline that can only shrink.

* refactor(errors): share one aborted-signal predicate with cancelledBy

* refactor(process): return a departing caller's reason from a shared startup wait

* refactor(providers): build decompiler-provider cancellations with a required reason (#10587)

Migrate every AnalysisCancelledError construction under src/ghidra,
src/ida, src/cutter, src/jeb and src/kuna to cancelledBy and lower the
guard baseline from 671 to 572. Closed Kuna target commands report
OwnerCancellation.ownerClosed instead of an absent reason, and Kuna and
JEB close abort their lifetimes with that typed reason.

* fix(errors): read the reason of any signal-shaped cancellation source

* test(ci): cover the counted-baseline comparison behind the architecture guard
This commit is contained in:
morluto authored and GitHub committed 2026-11-14 15:20:03 -05:00
1 parent 2f4c588584
commit 85da0bd4ee
51 files changed
+900 -488

No files matched your search

@@ -0,0 +1,49 @@
import { expect, it } from "vitest";
import { compareCountedBaseline } from "../../../scripts/verify/counted-baseline.mjs";
it("rejects a file that gains copies or is missing from the baseline", () => {
const compared = compareCountedBaseline(
new Map([
["src/a.ts", 3],
["src/new.ts", 1],
]),
{ "src/a.ts": 2 },
false,
);
expect(compared.grown).toEqual([
{ path: "src/a.ts", count: 3, allowed: 2 },
{ path: "src/new.ts", count: 1, allowed: 0 },
]);
});
it("reports removed copies as stale until the baseline is lowered", () => {
const baseline = { "src/a.ts": 2, "src/gone.ts": 1 };
const compared = compareCountedBaseline(
new Map([["src/a.ts", 1]]),
baseline,
false,
);
expect(compared.grown).toEqual([]);
expect(compared.stale).toEqual([
{ path: "src/a.ts", count: 1, allowed: 2 },
{ path: "src/gone.ts", count: 0, allowed: 1 },
]);
expect(compared.baseline).toBe(baseline);
});
it("only lowers counts when shrinking, never raising one that grew", () => {
const baseline = { "src/a.ts": 2, "src/b.ts": 1, "src/gone.ts": 4 };
const compared = compareCountedBaseline(
new Map([
["src/a.ts", 1],
["src/b.ts", 5],
]),
baseline,
true,
);
expect(compared.baseline).toEqual({ "src/a.ts": 1, "src/b.ts": 1 });
expect(compared.grown).toEqual([{ path: "src/b.ts", count: 5, allowed: 1 }]);
expect(compared.stale).toEqual([]);
expect(baseline).toEqual({ "src/a.ts": 2, "src/b.ts": 1, "src/gone.ts": 4 });
});
@@ -2,6 +2,7 @@ import { expect, it as test, vi } from "vitest";
import type { KunaProcessLauncher } from "../../../../src/kuna/KunaCommand.js";
import { projectAnalysisError } from "../../../../src/domain/analysisErrorProjection.js";
import { OwnerCancellation } from "../../../../src/domain/analysisErrorCore.js";
import { ProcessOwnershipInspectionCancelled } from "../../../../src/process/ProcessInspectionCancellation.js";
import { inspectKunaInstallationOwned as inspectKunaInstallation } from "./kunaResourceScope.js";
import { KunaTargetCommands } from "../../../../src/kuna/KunaTargetCommands.js";
@@ -216,7 +217,7 @@ it("preserves the lifetime reason when workspace preparation is already cancelle
}
});
it("does not invent a caller cause when closed without cancellation", async () => {
it("reports REA's owner close, not an invented caller cause, when closed without cancellation", async () => {
const { kuna, target, cacheRoot } = await session("rea-kuna-command-closed-");
const installed = await inspectKunaInstallation({
REA_KUNA_COMMAND: kuna,
@@ -233,7 +234,7 @@ it("does not invent a caller cause when closed without cancellation", async () =
const preparation = await commands.prepare();
if (preparation.ok) throw new Error("Expected closed preparation to cancel");
expect(preparation.error._tag).toBe("AnalysisCancelledError");
expect(preparation.error.cause).toBeUndefined();
expect(preparation.error.cause).toBe(OwnerCancellation.ownerClosed);
const result = await commands.run({
installation: installed.value,
operation: "list_procedures",
@@ -245,7 +246,7 @@ it("does not invent a caller cause when closed without cancellation", async () =
});
if (result.ok) throw new Error("Expected closed command to cancel");
expect(result.error._tag).toBe("AnalysisCancelledError");
expect(result.error.cause).toBeUndefined();
expect(result.error.cause).toBe(OwnerCancellation.ownerClosed);
expect(releaseRun).toHaveBeenCalledOnce();
expect(await fakeKunaCalls(kuna)).toEqual(callsBefore);
expect(await commands.close()).toEqual({ ok: true, value: null });