diff --git a/.oxlintrc.json b/.oxlintrc.json index db7907b215..33c616f4d7 100644 --- a/.oxlintrc.json +++ b/.oxlintrc.json @@ -63,6 +63,18 @@ "rea/no-shared-module-mock": "error" }, "overrides": [ + { + "files": ["src/domain/**/*.ts"], + "rules": { "rea/no-locale-compare": "error" } + }, + { + "files": [ + "src/domain/**/*.test.ts", + "src/domain/**/*.spec.ts", + "src/domain/**/*.fixture.ts" + ], + "rules": { "rea/no-locale-compare": "off" } + }, { "files": [ "scripts/electron-active-hook*.cjs", @@ -76,12 +88,19 @@ { "files": ["src/**/*.ts"], "rules": { + "rea/no-babel-visitor-keys": "error", "typescript/no-misused-promises": "error" } }, { - "files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"], + "files": [ + "tests/**/*.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.fixture.ts" + ], "rules": { + "rea/no-babel-visitor-keys": "off", "max-lines": "off", "max-lines-per-function": [ "error", @@ -94,6 +113,10 @@ "typescript/no-misused-promises": "warn" } }, + { + "files": ["src/domain/javascript/javascriptSemanticTraversal.ts"], + "rules": { "rea/no-babel-visitor-keys": "off" } + }, { "files": ["src/application/**/*.test.ts"], "rules": { diff --git a/docs/http3-capture-lab.md b/docs/http3-capture-lab.md index 77891f19bd..a672915fa7 100644 --- a/docs/http3-capture-lab.md +++ b/docs/http3-capture-lab.md @@ -174,7 +174,9 @@ Official references: [mitmproxy protocol support](https://github.com/mitmproxy/m ## Verifier regressions -After building REA, run these inside the same isolated guest: +The built process-boundary test suite runs the Node verifier regressions on +POSIX hosts. To run them directly after building REA, and to run the Python +laboratory regressions with its pinned dependencies, use the isolated guest: ```sh node --test scripts/lib/http3-verifier-lifecycle.test.mjs scripts/lib/http3-capture-assertions.test.mjs diff --git a/scripts/check-dependency-install.mjs b/scripts/check-dependency-install.mjs index dcdbd64dd6..800907878a 100644 --- a/scripts/check-dependency-install.mjs +++ b/scripts/check-dependency-install.mjs @@ -14,7 +14,7 @@ const directDependencies = (rootPackage) => ({ const installedPackageKey = (name) => `node_modules/${name}`; /** Compare root direct dependencies with npm's hidden installed lockfile. */ -export const dependencyInstallProblems = (rootLock, installedLock) => { +const dependencyInstallProblems = (rootLock, installedLock) => { const rootPackage = rootLock.packages?.[""]; if (rootPackage === undefined) return ["package-lock.json does not contain a root package entry"]; diff --git a/scripts/ci/cache-retention.d.mts b/scripts/ci/cache-retention.d.mts index d5d02c19c8..12c7d67172 100644 --- a/scripts/ci/cache-retention.d.mts +++ b/scripts/ci/cache-retention.d.mts @@ -26,8 +26,6 @@ export type CacheRequest = ( endpoint: string, ) => Promise; -export function assertRetentionPrefix(prefix: string): void; - export function selectPrefixDeletions( caches: readonly unknown[], family?: RetentionFamily, diff --git a/scripts/ci/cache-retention.mjs b/scripts/ci/cache-retention.mjs index dce987bd86..24abf4fdae 100644 --- a/scripts/ci/cache-retention.mjs +++ b/scripts/ci/cache-retention.mjs @@ -22,7 +22,7 @@ const PREFIX_PATTERNS = [ ]; /** Reject prefixes that could match npm downloads or another platform. */ -export function assertRetentionPrefix(prefix) { +function assertRetentionPrefix(prefix) { if ( typeof prefix !== "string" || !PREFIX_PATTERNS.some((pattern) => pattern.test(prefix)) diff --git a/scripts/ci/failure-report.d.mts b/scripts/ci/failure-report.d.mts index 5d24ae8ace..c8170a9c82 100644 --- a/scripts/ci/failure-report.d.mts +++ b/scripts/ci/failure-report.d.mts @@ -99,9 +99,3 @@ export function createGitHubApi(options?: { budget: { requests: number; startedAt: number; limitMs: number }, ) => Promise; }): FailureReportApi; - -export function writeFailureReport( - report: CiFailureReport, - outputDirectory?: string, - summaryPath?: string, -): Promise<{ jsonPath: string; markdownPath: string }>; diff --git a/scripts/ci/failure-report.mjs b/scripts/ci/failure-report.mjs index 01dcc0d700..09d2977d28 100644 --- a/scripts/ci/failure-report.mjs +++ b/scripts/ci/failure-report.mjs @@ -467,7 +467,7 @@ function errorCode(error) { return error instanceof Error ? error.message.slice(0, 160) : "unknown_error"; } -export async function writeFailureReport( +async function writeFailureReport( report, outputDirectory = OUTPUT_DIRECTORY, summaryPath, diff --git a/scripts/ci/pr-ci-health.d.mts b/scripts/ci/pr-ci-health.d.mts index 0db2454bdd..b00a74aeba 100644 --- a/scripts/ci/pr-ci-health.d.mts +++ b/scripts/ci/pr-ci-health.d.mts @@ -91,5 +91,4 @@ export function writePrCiHealthReport( outputDirectory?: string, ): Promise<{ jsonPath: string; markdownPath: string }>; -export function renderMarkdownSummary(report: PrCiHealthReport): string; import type { GitHubApiRequest } from "./github-api.mjs"; diff --git a/scripts/ci/pr-ci-health.mjs b/scripts/ci/pr-ci-health.mjs index d312dcecd9..a982e792f5 100644 --- a/scripts/ci/pr-ci-health.mjs +++ b/scripts/ci/pr-ci-health.mjs @@ -656,7 +656,7 @@ export async function writePrCiHealthReport( return { jsonPath, markdownPath }; } -export function renderMarkdownSummary(report) { +function renderMarkdownSummary(report) { const rows = report.pullRequests.map( (pull) => `| #${String(pull.number ?? "?")} | ${escapeCell(pull.head.repository ?? "unknown")} | ${escapeCell(pull.head.ref ?? "unknown")} | \`${pull.head.sha ?? "unknown"}\` | ${pull.ci.category} | ${escapeCell(pull.ci.reason)} |`, diff --git a/scripts/ci/production-reachability-report.d.mts b/scripts/ci/production-reachability-report.d.mts index 3970ca390f..523812eada 100644 --- a/scripts/ci/production-reachability-report.d.mts +++ b/scripts/ci/production-reachability-report.d.mts @@ -28,6 +28,3 @@ export interface ProductionReachabilityReport { export function parseProductionReachabilityReport( text: string, ): ProductionReachabilityReport; -export function recordProductionReachabilityReport( - root?: string, -): Promise; diff --git a/scripts/ci/production-reachability-report.mjs b/scripts/ci/production-reachability-report.mjs index 8758e6a126..e7aae89bfc 100644 --- a/scripts/ci/production-reachability-report.mjs +++ b/scripts/ci/production-reachability-report.mjs @@ -34,9 +34,7 @@ export const parseProductionReachabilityReport = (text) => reportSchema.parse(JSON.parse(text)); /** Run production reachability without turning candidates into failures. */ -export const recordProductionReachabilityReport = async ( - root = process.cwd(), -) => { +const recordProductionReachabilityReport = async (root = process.cwd()) => { const reportDirectory = join(root, ".cache", "static-checks"); const reportPath = join(reportDirectory, "production-reachability.json"); diff --git a/scripts/fixtures/apple/macos-bundle.mjs b/scripts/fixtures/apple/macos-bundle.mjs index adb1a2c108..de32463616 100644 --- a/scripts/fixtures/apple/macos-bundle.mjs +++ b/scripts/fixtures/apple/macos-bundle.mjs @@ -6,7 +6,7 @@ import { promisify } from "node:util"; const exec = promisify(execFile); /** Commands the macOS bundle fixture needs from Command Line Tools. */ -export const MACOS_BUNDLE_FIXTURE_COMMANDS = [ +const MACOS_BUNDLE_FIXTURE_COMMANDS = [ ["/usr/bin/xcrun", ["--find", "clang"]], ["/usr/bin/xcrun", ["--find", "codesign"]], ["/usr/bin/ditto", ["-h"]], diff --git a/scripts/lib/browser-verifier-assertions.mjs b/scripts/lib/browser-verifier-assertions.mjs index 2ca8360116..f5a7a7d702 100644 --- a/scripts/lib/browser-verifier-assertions.mjs +++ b/scripts/lib/browser-verifier-assertions.mjs @@ -1,4 +1,4 @@ -export function sourceMapSummary(sourceMaps) { +function sourceMapSummary(sourceMaps) { return { status: sourceMaps.status, requested: sourceMaps.requested, diff --git a/scripts/lib/cursor-plugin-bundle.d.mts b/scripts/lib/cursor-plugin-bundle.d.mts index 9c9acceb29..b83b43a5c5 100644 --- a/scripts/lib/cursor-plugin-bundle.d.mts +++ b/scripts/lib/cursor-plugin-bundle.d.mts @@ -1,11 +1,3 @@ -export const CURSOR_PLUGIN_MANIFEST: ".cursor-plugin/plugin.json"; -export const CURSOR_PLUGIN_MCP: "mcp.json"; -export const AUTHORED_SKILL_DIRECTORY: ".agents/skills/reverse-engineer-anything"; -export const CURSOR_PLUGIN_SKILLS: ".cursor-plugin/skills"; -export const CURSOR_PLUGIN_SKILL: "reverse-engineer-anything"; -export const CURSOR_PLUGIN_LOGO: "assets/rea-plugin.png"; -export const CURSOR_PLUGIN_REPOSITORY: "https://github.com/morluto/rea"; - export interface CursorPluginBundleIssue { readonly field: string; } @@ -56,11 +48,6 @@ export interface CursorPluginInspection { readonly issues: readonly CursorPluginBundleIssue[]; } -export function cursorPluginSkillPaths( - directory: string, - prefix?: string, -): Promise; - export function cursorPluginBundleIssues( input: CursorPluginBundleInput, ): readonly CursorPluginBundleIssue[]; diff --git a/scripts/lib/cursor-plugin-bundle.mjs b/scripts/lib/cursor-plugin-bundle.mjs index 6b8359e6dd..5dd7453172 100644 --- a/scripts/lib/cursor-plugin-bundle.mjs +++ b/scripts/lib/cursor-plugin-bundle.mjs @@ -7,14 +7,13 @@ import { ensureGeneratedFile } from "./generated-file.mjs"; // publication (name, release version, repository, skill, and icon). The // Marketplace checkout has no dist/, so the server is the pinned npm command // instead of the bundled launcher. -export const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json"; -export const CURSOR_PLUGIN_MCP = "mcp.json"; -export const AUTHORED_SKILL_DIRECTORY = - ".agents/skills/reverse-engineer-anything"; -export const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills"; -export const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything"; -export const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png"; -export const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea"; +const CURSOR_PLUGIN_MANIFEST = ".cursor-plugin/plugin.json"; +const CURSOR_PLUGIN_MCP = "mcp.json"; +const AUTHORED_SKILL_DIRECTORY = ".agents/skills/reverse-engineer-anything"; +const CURSOR_PLUGIN_SKILLS = ".cursor-plugin/skills"; +const CURSOR_PLUGIN_SKILL = "reverse-engineer-anything"; +const CURSOR_PLUGIN_LOGO = "assets/rea-plugin.png"; +const CURSOR_PLUGIN_REPOSITORY = "https://github.com/morluto/rea"; const GENERATE_COMMAND = "node scripts/generate-cursor-plugin-skill.mjs"; const INSTALL_LINK_MARKERS = ["install-mcp", "deeplink/mcp", "mcp-install"]; @@ -27,7 +26,7 @@ const failure = (code, detail) => { const cursorServerArgs = (version) => ["-y", `rea-agents@${version}`, "mcp"]; /** Relative file inventory for one skill directory, in stable order. */ -export const cursorPluginSkillPaths = async (directory, prefix = "") => { +const cursorPluginSkillPaths = async (directory, prefix = "") => { const paths = []; let entries; try { diff --git a/scripts/lib/ghidra-windows-byte-search.mjs b/scripts/lib/ghidra-windows-byte-search.mjs index 25dc5e5e6a..c52b15f275 100644 --- a/scripts/lib/ghidra-windows-byte-search.mjs +++ b/scripts/lib/ghidra-windows-byte-search.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; -export const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({ +const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({ pattern: "a0 ?? c0", mask: "f0 ff f0", startFileOffset: 0x380, diff --git a/scripts/lib/hopper-deadline-cleanup.d.mts b/scripts/lib/hopper-deadline-cleanup.d.mts new file mode 100644 index 0000000000..cf2779d6d3 --- /dev/null +++ b/scripts/lib/hopper-deadline-cleanup.d.mts @@ -0,0 +1,16 @@ +import type { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js"; + +export class HopperDeadlineCleanupFailure extends AggregateError { + readonly _tag: "HopperDeadlineCleanupFailure"; + readonly cleanupOwner: HopperDeadlineCleanup; + constructor(owner: HopperDeadlineCleanup, failures: unknown[]); +} + +export class HopperDeadlineCleanup { + readonly workspace: string; + constructor(workspace: string, profile: ArtifactResourceScope); + setClient(client: { + close(): Promise<{ ok: true } | { ok: false; error: unknown }>; + }): void; + close(): Promise; +} diff --git a/scripts/lib/hopper-deadline-cleanup.mjs b/scripts/lib/hopper-deadline-cleanup.mjs new file mode 100644 index 0000000000..b6ee0dd723 --- /dev/null +++ b/scripts/lib/hopper-deadline-cleanup.mjs @@ -0,0 +1,71 @@ +import { rm } from "node:fs/promises"; + +export class HopperDeadlineCleanupFailure extends AggregateError { + constructor(owner, failures) { + super( + failures, + `Hopper cleanup incomplete; workspace may remain at ${owner.workspace}`, + ); + this._tag = "HopperDeadlineCleanupFailure"; + this.cleanupOwner = owner; + } +} + +/** Own the client, profile scope, and workspace until all cleanup completes. */ +export class HopperDeadlineCleanup { + #client; + #profile; + #pending; + #removed = false; + + constructor(workspace, profile) { + this.workspace = workspace; + this.#profile = profile; + } + + setClient(client) { + this.#client = client; + } + + close() { + if (this.#pending !== undefined) return this.#pending; + const attempt = this.#close(); + const pending = attempt.finally(() => { + if (this.#pending === pending) this.#pending = undefined; + }); + this.#pending = pending; + return pending; + } + + async #close() { + if (this.#removed) return; + const failures = []; + if (this.#client !== undefined) { + try { + const result = await this.#client.close(); + if (!result.ok) throw result.error; + this.#client = undefined; + } catch (cause) { + failures.push(cause); + } + } + if (this.#profile !== undefined) { + try { + await this.#profile.close(); + this.#profile = undefined; + } catch (cause) { + failures.push(cause); + } + } + if (failures.length === 0) { + try { + await rm(this.workspace, { recursive: true, force: true }); + this.#removed = true; + return; + } catch (cause) { + failures.push(cause); + } + } + throw new HopperDeadlineCleanupFailure(this, failures); + } +} diff --git a/scripts/lib/http3-capture-assertions.mjs b/scripts/lib/http3-capture-assertions.mjs index ca08d32ccf..ac7c7862a8 100644 --- a/scripts/lib/http3-capture-assertions.mjs +++ b/scripts/lib/http3-capture-assertions.mjs @@ -64,7 +64,7 @@ function assertHeaders(record, side, headers) { } } -export function assertEndpointHeaders(actual, expected) { +function assertEndpointHeaders(actual, expected) { // The proxy rebuilds pseudo fields from its HTTP state. Compare their values // by name while retaining the exact order and duplicates of regular headers. const partition = (headers) => { diff --git a/scripts/lib/javascript-application-analysis.mjs b/scripts/lib/javascript-application-analysis.mjs index e9b4c88226..2d35f668d8 100644 --- a/scripts/lib/javascript-application-analysis.mjs +++ b/scripts/lib/javascript-application-analysis.mjs @@ -1,28 +1,10 @@ import { analyzeJavaScriptApplication } from "../../dist/application/javascript/JavaScriptApplicationService.js"; -import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js"; +import { withVerifierArtifactScope } from "./verifier-artifact-scope.mjs"; /** Analyze one JavaScript application and close its caller-owned resources. */ -export const analyzeJavaScriptApplicationWithOwnedScope = async (input) => { - const scope = new ArtifactResourceScope(); - let outcome; - try { +export const analyzeJavaScriptApplicationWithOwnedScope = (input) => + withVerifierArtifactScope(async (scope) => { const result = await analyzeJavaScriptApplication(input, scope); - outcome = result.ok - ? { kind: "completed", value: result.value } - : { kind: "failed", cause: result.error }; - } catch (cause) { - outcome = { kind: "failed", cause }; - } - try { - await scope.close(); - } catch (cleanupCause) { - if (outcome.kind === "failed") - throw new AggregateError( - [outcome.cause, cleanupCause], - "JavaScript analysis and artifact cleanup both failed", - ); - throw cleanupCause; - } - if (outcome.kind === "failed") throw outcome.cause; - return outcome.value; -}; + if (!result.ok) throw result.error; + return result.value; + }); diff --git a/scripts/lib/mcp-verifier-results.mjs b/scripts/lib/mcp-verifier-results.mjs index efef3efd65..75b1163e4e 100644 --- a/scripts/lib/mcp-verifier-results.mjs +++ b/scripts/lib/mcp-verifier-results.mjs @@ -78,7 +78,7 @@ export const expandMcpSessionStatus = (status) => ({ }); /** Expand provider capability groups while retaining candidate identity/state. */ -export const expandProviderCandidates = (candidates) => +const expandProviderCandidates = (candidates) => candidates.map((candidate) => ({ ...candidate, capabilities: expandProviderCapabilities( diff --git a/scripts/lib/module-boundaries.test.mjs b/scripts/lib/module-boundaries.test.mjs deleted file mode 100644 index 3177637570..0000000000 --- a/scripts/lib/module-boundaries.test.mjs +++ /dev/null @@ -1,176 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; -import { tmpdir } from "node:os"; -import { join } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { test } from "node:test"; - -import { inspectModuleBoundaries } from "./module-boundaries.mjs"; - -const fixture = async () => { - const root = await mkdtemp(join(tmpdir(), "rea-module-boundaries-")); - await mkdir(join(root, "src/domain"), { recursive: true }); - await mkdir(join(root, "src/composition"), { recursive: true }); - await writeFile( - join(root, "src/composition/worker.ts"), - "export default 1;\n", - ); - await writeFile( - join(root, "package.json"), - JSON.stringify({ imports: { "#worker": "./dist/composition/worker.js" } }), - ); - return root; -}; - -await test("reports a pure-layer composition import written relatively", async () => { - const root = await fixture(); - try { - const violations = inspectModuleBoundaries( - "src/domain/probe.ts", - 'import worker from "../composition/worker.js";\nexport default worker;\n', - root, - ); - assert.deepEqual( - violations.map(({ boundary, target }) => ({ boundary, target })), - [{ boundary: "pure-layer", target: "src/composition/worker.js" }], - ); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); - -await test("reports a pure-layer composition import written through the package map", async () => { - const root = await fixture(); - try { - const violations = inspectModuleBoundaries( - "src/domain/probe.ts", - 'import worker from "#worker";\nexport default worker;\n', - root, - ); - assert.deepEqual( - violations.map(({ boundary, target }) => ({ boundary, target })), - [{ boundary: "pure-layer", target: "src/composition/worker.ts" }], - ); - } finally { - await rm(root, { recursive: true, force: true }); - } -}); - -for (const [name, specifier, imports] of [ - [ - "conditional", - "#worker", - { - "#worker": { - import: "./dist/composition/worker.js", - default: "./dist/composition/other.js", - }, - }, - ], - ["array", "#worker", { "#worker": ["./dist/composition/worker.js"] }], - ["pattern", "#workers/worker", { "#workers/*": "./dist/composition/*.js" }], -]) { - await test(`refuses an unsupported valid ${name} package map rather than skipping its edge`, async () => { - const root = await fixture(); - try { - await writeFile(join(root, "package.json"), JSON.stringify({ imports })); - const file = "src/domain/probe.ts"; - await writeFile( - join(root, file), - `import ${JSON.stringify(specifier)};\n`, - ); - // Resolve only: no worker module or authored target source is executed. - const resolved = spawnSync( - process.execPath, - [ - "--experimental-import-meta-resolve", - "--input-type=module", - "-e", - "console.log(import.meta.resolve(process.argv[1], process.argv[2]))", - specifier, - pathToFileURL(join(root, file)).href, - ], - { encoding: "utf8" }, - ); - if (resolved.error !== undefined) throw resolved.error; - assert.equal(resolved.status, 0, resolved.stderr); - assert.equal( - resolved.stdout.trim(), - pathToFileURL(join(root, "dist/composition/worker.js")).href, - ); - const checked = spawnSync( - process.execPath, - [ - fileURLToPath( - new URL("../verify-module-boundaries.mjs", import.meta.url), - ), - file, - ], - { cwd: root, encoding: "utf8" }, - ); - if (checked.error !== undefined) throw checked.error; - assert.equal(checked.status, 1, checked.stdout); - assert.match(checked.stderr, /Could not inspect src\/domain\/probe.ts/u); - assert.match(checked.stderr, /Cannot resolve package import/u); - assert.match(checked.stderr, /relative source import/u); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -} - -for (const target of [ - "./dist/composition/worker.js", - "./dist/%63omposition/worker.js", - "./%64ist/composition/worker.js", -]) { - await test(`checks the URL-normalized source owner of ${target}`, async () => { - const root = await fixture(); - try { - await writeFile( - join(root, "package.json"), - JSON.stringify({ imports: { "#worker": target } }), - ); - await mkdir(join(root, "src/application"), { recursive: true }); - const file = "src/application/probe.ts"; - await writeFile(join(root, file), 'import "#worker";\n'); - const resolved = spawnSync( - process.execPath, - [ - "--experimental-import-meta-resolve", - "--input-type=module", - "-e", - "console.log(import.meta.resolve(process.argv[1], process.argv[2]))", - "#worker", - pathToFileURL(join(root, file)).href, - ], - { encoding: "utf8" }, - ); - if (resolved.error !== undefined) throw resolved.error; - assert.equal(resolved.status, 0, resolved.stderr); - assert.equal( - fileURLToPath(resolved.stdout.trim()), - join(root, "dist/composition/worker.js"), - ); - const checked = spawnSync( - process.execPath, - [ - fileURLToPath( - new URL("../verify-module-boundaries.mjs", import.meta.url), - ), - file, - ], - { cwd: root, encoding: "utf8" }, - ); - if (checked.error !== undefined) throw checked.error; - assert.equal(checked.status, 1, checked.stdout); - assert.match( - checked.stderr, - /application-composition: #worker resolves to src\/composition\/worker.ts/u, - ); - } finally { - await rm(root, { recursive: true, force: true }); - } - }); -} diff --git a/scripts/lib/owned-verifier-terminal.d.mts b/scripts/lib/owned-verifier-terminal.d.mts index 6febc16766..de65ae5b78 100644 --- a/scripts/lib/owned-verifier-terminal.d.mts +++ b/scripts/lib/owned-verifier-terminal.d.mts @@ -1,8 +1,5 @@ export { runOwnedVerifierTerminal } from "./verifier-terminal-owner.mjs"; export type { VerifierTerminalOptions, - VerifierTerminalDependencies, VerifierTerminalResult, - VerifierTerminalFailure, - VerifierTerminalCleanupFailure, } from "./verifier-terminal-owner.mjs"; diff --git a/scripts/lib/real-hopper-cleanup.d.mts b/scripts/lib/real-hopper-cleanup.d.mts index 8ccc9e3187..095fc052d7 100644 --- a/scripts/lib/real-hopper-cleanup.d.mts +++ b/scripts/lib/real-hopper-cleanup.d.mts @@ -1,6 +1,10 @@ -/** Observe concrete Hopper runtime resources for a before/after cleanup check. */ +/** Observe possible Hopper resources, including entries with unresolved ownership. */ export function snapshotHopperRuntime( parent: string, targetLeaseDirectory: string, - ownedProcessIds?: ReadonlySet, -): Promise>; + expectedRunId?: string, +): Promise<{ + readonly paths: Set; + readonly confirmedSessions: Set; + readonly unresolvedSessions: Set; +}>; diff --git a/scripts/lib/real-hopper-cleanup.mjs b/scripts/lib/real-hopper-cleanup.mjs index 85d0a8f336..decbc3fd26 100644 --- a/scripts/lib/real-hopper-cleanup.mjs +++ b/scripts/lib/real-hopper-cleanup.mjs @@ -1,35 +1,38 @@ import { lstat, readFile, readdir } from "node:fs/promises"; import { join } from "node:path"; -/** Observe session paths and target leases without treating their shared parent as a session. */ +/** Observe possible sessions and target leases without discarding uncertainty. */ export async function snapshotHopperRuntime( parent, targetLeaseDirectory, - ownedProcessIds, + expectedRunId, ) { - const resources = new Set(); + const paths = new Set(); + const confirmedSessions = new Set(); + const unresolvedSessions = new Set(); for (const entry of await readdir(parent, { withFileTypes: true })) { const path = join(parent, entry.name); if (!entry.name.startsWith("rea-") || path === targetLeaseDirectory) continue; - if ( - ownedProcessIds !== undefined && - (!entry.isDirectory() || - !(await belongsToOwnedProcess(path, ownedProcessIds))) - ) - continue; - resources.add(path); + const confirmed = + typeof expectedRunId === "string" && + expectedRunId.length > 0 && + entry.isDirectory() && + (await hasRunIdentity(path, expectedRunId)); + (confirmed ? confirmedSessions : unresolvedSessions).add(path); + paths.add(path); } const leaseRoot = await lstat(targetLeaseDirectory).catch(missingIsAbsent); - if (leaseRoot === undefined) return resources; - if (!leaseRoot.isDirectory() || leaseRoot.isSymbolicLink()) { - resources.add(targetLeaseDirectory); - return resources; + if (leaseRoot !== undefined) { + if (!leaseRoot.isDirectory() || leaseRoot.isSymbolicLink()) + paths.add(targetLeaseDirectory); + else { + const leases = await readdir(targetLeaseDirectory).catch(missingIsAbsent); + for (const name of leases ?? []) + paths.add(join(targetLeaseDirectory, name)); + } } - const leases = await readdir(targetLeaseDirectory).catch(missingIsAbsent); - for (const name of leases ?? []) - resources.add(join(targetLeaseDirectory, name)); - return resources; + return { paths, confirmedSessions, unresolvedSessions }; } const missingIsAbsent = (cause) => { @@ -37,16 +40,21 @@ const missingIsAbsent = (cause) => { throw cause; }; -const belongsToOwnedProcess = async (path, ownedProcessIds) => { +const hasRunIdentity = async (path, expectedRunId) => { const encoded = await readFile(join(path, "ownership.json"), "utf8").catch( missingIsAbsent, ); if (encoded === undefined) return false; - const ownership = JSON.parse(encoded); + let ownership; + try { + ownership = JSON.parse(encoded); + } catch (cause) { + if (cause instanceof SyntaxError) return false; + throw cause; + } return ( ownership !== null && typeof ownership === "object" && - Number.isSafeInteger(ownership.parent_pid) && - ownedProcessIds.has(ownership.parent_pid) + ownership.run_id === expectedRunId ); }; diff --git a/scripts/lib/real-hopper-semantic.d.mts b/scripts/lib/real-hopper-semantic.d.mts index 8be63c510a..0c0082dbfe 100644 --- a/scripts/lib/real-hopper-semantic.d.mts +++ b/scripts/lib/real-hopper-semantic.d.mts @@ -1,4 +1,4 @@ -export interface FixtureProcedure { +interface FixtureProcedure { readonly address: string; readonly name: string; } @@ -26,13 +26,6 @@ export function requireCurrentDocument( normalizedResult: (value: unknown, operation: string) => unknown, ): Promise; -export function analyzeFixtureProcedure( - client: ToolClient, - options: unknown, - procedure: FixtureProcedure, - normalizedResult: (value: unknown, operation: string) => unknown, -): Promise; - export function verifyRealHopperFixture(options: { readonly client: ToolClient; readonly options: unknown; diff --git a/scripts/lib/real-hopper-semantic.mjs b/scripts/lib/real-hopper-semantic.mjs index d6e7752fb1..f07d0b313d 100644 --- a/scripts/lib/real-hopper-semantic.mjs +++ b/scripts/lib/real-hopper-semantic.mjs @@ -310,7 +310,7 @@ export async function resolveFixtureProcedure( } /** Request the assembly evidence required by the fixture oracle. */ -export const analyzeFixtureProcedure = async ( +const analyzeFixtureProcedure = async ( client, options, procedure, diff --git a/scripts/lib/runtime-admission.d.mts b/scripts/lib/runtime-admission.d.mts index 76d8727d01..02040fe769 100644 --- a/scripts/lib/runtime-admission.d.mts +++ b/scripts/lib/runtime-admission.d.mts @@ -6,6 +6,3 @@ export function unsupportedRuntimeMessage( range: string, version: string, ): string | undefined; - -/** Human-readable release families for an `engines.node` range. */ -export function supportedReleaseProse(range: string): string; diff --git a/scripts/lib/runtime-admission.mjs b/scripts/lib/runtime-admission.mjs index 5ba168383f..aadb9f58ca 100644 --- a/scripts/lib/runtime-admission.mjs +++ b/scripts/lib/runtime-admission.mjs @@ -21,7 +21,7 @@ export const unsupportedRuntimeMessage = (range, version) => { }; /** Human-readable release families for an `engines.node` range. */ -export const supportedReleaseProse = (range) => +const supportedReleaseProse = (range) => `Node.js ${range .split("||") .map((clause) => { diff --git a/scripts/lib/verifier-artifact-scope.d.mts b/scripts/lib/verifier-artifact-scope.d.mts new file mode 100644 index 0000000000..fdf76198f2 --- /dev/null +++ b/scripts/lib/verifier-artifact-scope.d.mts @@ -0,0 +1,15 @@ +import type { ArtifactResourceScope } from "../../src/artifacts/ArtifactResourceScope.js"; + +export class VerifierArtifactCleanupFailure extends AggregateError { + readonly _tag: "VerifierArtifactCleanupFailure"; + readonly cleanupOwner: ArtifactResourceScope; + constructor( + scope: ArtifactResourceScope, + failures: unknown[], + cause: unknown, + ); +} + +export function withVerifierArtifactScope( + operation: (scope: ArtifactResourceScope) => Promise, +): Promise; diff --git a/scripts/lib/verifier-artifact-scope.mjs b/scripts/lib/verifier-artifact-scope.mjs new file mode 100644 index 0000000000..d90976fc38 --- /dev/null +++ b/scripts/lib/verifier-artifact-scope.mjs @@ -0,0 +1,32 @@ +import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js"; + +/** A failed close retains the scope that can retry all of its owners. */ +export class VerifierArtifactCleanupFailure extends AggregateError { + constructor(scope, failures, cause) { + super(failures, "Verifier cleanup failed", { cause }); + this._tag = "VerifierArtifactCleanupFailure"; + this.cleanupOwner = scope; + } +} + +/** Run one verifier operation and retain its exact scope if close fails. */ +export async function withVerifierArtifactScope(operation) { + const scope = new ArtifactResourceScope(); + let outcome; + try { + outcome = { ok: true, value: await operation(scope) }; + } catch (cause) { + outcome = { ok: false, cause }; + } + try { + await scope.close(); + } catch (cleanupCause) { + throw new VerifierArtifactCleanupFailure( + scope, + outcome.ok ? [cleanupCause] : [outcome.cause, cleanupCause], + outcome.ok ? cleanupCause : outcome.cause, + ); + } + if (!outcome.ok) throw outcome.cause; + return outcome.value; +} diff --git a/scripts/lib/verifier-terminal-owner.mts b/scripts/lib/verifier-terminal-owner.mts index d0a0055c4f..d541c78fb3 100644 --- a/scripts/lib/verifier-terminal-owner.mts +++ b/scripts/lib/verifier-terminal-owner.mts @@ -40,7 +40,7 @@ export interface VerifierTerminalOptions { readonly signal?: AbortSignal; } -export interface VerifierTerminalDependencies { +interface VerifierTerminalDependencies { readonly loadPty?: () => Promise; readonly cleanupProcessGroup?: typeof cleanupOwnedProcessGroup; } @@ -59,7 +59,7 @@ type TerminalFailureReason = | "unsupported_platform"; /** Expected terminal failures retain the decoded output observed at failure. */ -export class VerifierTerminalFailure extends Error { +class VerifierTerminalFailure extends Error { readonly _tag = "VerifierTerminalFailure"; constructor( @@ -74,7 +74,7 @@ export class VerifierTerminalFailure extends Error { } /** The adopted owner remains the exact authority for retrying this release. */ -export class VerifierTerminalCleanupFailure extends Error { +class VerifierTerminalCleanupFailure extends Error { readonly _tag = "VerifierTerminalCleanupFailure"; readonly reason = "cleanup_incomplete"; diff --git a/scripts/lint/index.mjs b/scripts/lint/index.mjs index 66b7f47cd3..72075f0698 100644 --- a/scripts/lint/index.mjs +++ b/scripts/lint/index.mjs @@ -5,6 +5,8 @@ import noEvidenceJsonPreadmission from "./rules/no-evidence-json-preadmission.mj import noFixtureRootCleanup from "./rules/no-fixture-root-cleanup.mjs"; import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs"; import noSharedModuleMock from "./rules/no-shared-module-mock.mjs"; +import noLocaleCompare from "./rules/no-locale-compare.mjs"; +import noBabelVisitorKeys from "./rules/no-babel-visitor-keys.mjs"; export default { meta: { name: "rea" }, @@ -15,5 +17,7 @@ export default { "no-fixture-root-cleanup": noFixtureRootCleanup, "no-positional-tool-contract": noPositionalToolContract, "no-shared-module-mock": noSharedModuleMock, + "no-locale-compare": noLocaleCompare, + "no-babel-visitor-keys": noBabelVisitorKeys, }, }; diff --git a/scripts/lint/rules/no-babel-visitor-keys.d.mts b/scripts/lint/rules/no-babel-visitor-keys.d.mts new file mode 100644 index 0000000000..86d61729e2 --- /dev/null +++ b/scripts/lint/rules/no-babel-visitor-keys.d.mts @@ -0,0 +1,4 @@ +import type { RuleTester } from "oxlint/plugins-dev"; + +declare const rule: Parameters[1]; +export default rule; diff --git a/scripts/lint/rules/no-babel-visitor-keys.mjs b/scripts/lint/rules/no-babel-visitor-keys.mjs new file mode 100644 index 0000000000..588233f4ca --- /dev/null +++ b/scripts/lint/rules/no-babel-visitor-keys.mjs @@ -0,0 +1,73 @@ +import { findBinding } from "../bindings.mjs"; + +const nameOf = (node) => + node.type === "Identifier" + ? node.name + : node.type === "Literal" + ? node.value + : node.type === "TemplateLiteral" && node.expressions.length === 0 + ? node.quasis[0]?.value.cooked + : undefined; + +export default { + meta: { + type: "problem", + docs: { description: "Keep Babel child expansion in its shared owner." }, + schema: [], + messages: { + owner: + "Use childNodes or traverseJavaScriptAst instead of Babel VISITOR_KEYS.", + }, + }, + create(context) { + const source = context.sourceCode; + const isBabelNamespace = (node) => { + if (node?.type !== "Identifier") return false; + const definition = findBinding(source, node)?.defs[0]; + if ( + definition?.type !== "ImportBinding" || + definition.node.type !== "ImportNamespaceSpecifier" + ) + return false; + const declaration = definition.parent ?? definition.node.parent; + return declaration?.source?.value === "@babel/types"; + }; + return { + ImportSpecifier(node) { + if ( + node.parent.source.value === "@babel/types" && + (node.imported.name ?? node.imported.value) === "VISITOR_KEYS" + ) + context.report({ node, messageId: "owner" }); + }, + ExportNamedDeclaration(node) { + if (node.source?.value !== "@babel/types") return; + for (const specifier of node.specifiers) + if ( + specifier.type === "ExportSpecifier" && + nameOf(specifier.local) === "VISITOR_KEYS" + ) + context.report({ node: specifier, messageId: "owner" }); + }, + MemberExpression(node) { + if ( + (!node.computed || node.property.type !== "Identifier") && + nameOf(node.property) === "VISITOR_KEYS" && + isBabelNamespace(node.object) + ) + context.report({ node, messageId: "owner" }); + }, + VariableDeclarator(node) { + if (node.id.type !== "ObjectPattern" || !isBabelNamespace(node.init)) + return; + for (const property of node.id.properties) + if ( + property.type === "Property" && + (!property.computed || property.key.type !== "Identifier") && + nameOf(property.key) === "VISITOR_KEYS" + ) + context.report({ node: property, messageId: "owner" }); + }, + }; + }, +}; diff --git a/scripts/lint/rules/no-locale-compare.d.mts b/scripts/lint/rules/no-locale-compare.d.mts new file mode 100644 index 0000000000..86d61729e2 --- /dev/null +++ b/scripts/lint/rules/no-locale-compare.d.mts @@ -0,0 +1,4 @@ +import type { RuleTester } from "oxlint/plugins-dev"; + +declare const rule: Parameters[1]; +export default rule; diff --git a/scripts/lint/rules/no-locale-compare.mjs b/scripts/lint/rules/no-locale-compare.mjs new file mode 100644 index 0000000000..41c01e67fa --- /dev/null +++ b/scripts/lint/rules/no-locale-compare.mjs @@ -0,0 +1,34 @@ +export default { + meta: { + type: "problem", + docs: { + description: "Keep domain ordering independent of the host locale.", + }, + schema: [], + messages: { + locale: + "Use compareUnicodeCodePoints instead of localeCompare for domain ordering.", + }, + }, + create(context) { + return { + CallExpression(node) { + const member = node.callee; + if (member.type !== "MemberExpression") return; + if ( + (!member.computed && + member.property.type === "Identifier" && + member.property.name === "localeCompare") || + (member.computed && + member.property.type === "Literal" && + member.property.value === "localeCompare") || + (member.computed && + member.property.type === "TemplateLiteral" && + member.property.expressions.length === 0 && + member.property.quasis[0]?.value.cooked === "localeCompare") + ) + context.report({ node, messageId: "locale" }); + }, + }; + }, +}; diff --git a/scripts/verify-package-environment.d.mts b/scripts/verify-package-environment.d.mts index 9f1b3c55fc..0a17e6565c 100644 --- a/scripts/verify-package-environment.d.mts +++ b/scripts/verify-package-environment.d.mts @@ -1,13 +1,3 @@ -export interface PackageHopperEnvironment { - readonly HOPPER_LAUNCHER_PATH: string; - readonly HOPPER_LOADER_ARGS_JSON: string; -} - -export function packageHopperEnvironment( - root: string, - platform?: string, -): PackageHopperEnvironment; - export function verifyPackageEnvironment(input: { readonly root: string; readonly workspace: string; diff --git a/scripts/verify-package-environment.mjs b/scripts/verify-package-environment.mjs index 91834d3fa1..5a0424c19b 100644 --- a/scripts/verify-package-environment.mjs +++ b/scripts/verify-package-environment.mjs @@ -117,7 +117,7 @@ export async function verifyPackageEnvironment({ }; } -export const packageHopperEnvironment = (root, platform = process.platform) => { +const packageHopperEnvironment = (root, platform = process.platform) => { const fakeLauncher = join(root, "tests/fixtures/fakeLauncher.mjs"); if (platform !== "linux") return { diff --git a/scripts/verify-real-ghidra-assertions.mjs b/scripts/verify-real-ghidra-assertions.mjs index c5fed71b0a..445d739569 100644 --- a/scripts/verify-real-ghidra-assertions.mjs +++ b/scripts/verify-real-ghidra-assertions.mjs @@ -414,7 +414,7 @@ export function matchesSymbol(value, expected) { return tail === expected || tail === `_${expected}`; } -export function symbolTail(value) { +function symbolTail(value) { if (typeof value !== "string") return undefined; return value.split("::").at(-1); } diff --git a/scripts/verify-real-ghidra-dol.mjs b/scripts/verify-real-ghidra-dol.mjs index d92d6ea452..bdc7a5444d 100644 --- a/scripts/verify-real-ghidra-dol.mjs +++ b/scripts/verify-real-ghidra-dol.mjs @@ -9,7 +9,7 @@ import { promisify } from "node:util"; import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; -import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js"; +import { withVerifierArtifactScope } from "./lib/verifier-artifact-scope.mjs"; import { readGhidraLanguageCatalog } from "../dist/ghidra/GhidraLanguageCatalog.js"; import { parseEvidence } from "../dist/domain/evidence.js"; import { functionDossierSchema } from "../dist/domain/native/functionDossier.js"; @@ -82,16 +82,13 @@ for (const setting of ["GHIDRA_HEADLESS_MAXMEM", "GHIDRA_MAXMEM"]) { * installation catalog reader it uses. User extensions are not in that catalog. */ async function languageInstalled(languageId) { - const scope = new ArtifactResourceScope(); - try { + return withVerifierArtifactScope(async (scope) => { const catalog = await readGhidraLanguageCatalog( installation.installDir, scope, ); return catalog.some((definition) => definition.id === languageId); - } finally { - await scope.close(); - } + }); } /** One MCP session plus CLI check. `languageOverride` is set only for the Gekko pass. */ diff --git a/scripts/verify-real-hopper-callers.mjs b/scripts/verify-real-hopper-callers.mjs index 7407114e6c..3f9cca6414 100644 --- a/scripts/verify-real-hopper-callers.mjs +++ b/scripts/verify-real-hopper-callers.mjs @@ -11,6 +11,10 @@ import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { artifactMcpCall } from "./lib/artifact-e2e.mjs"; import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs"; +import { + closeVerifierResources, + withVerifierCleanup, +} from "./lib/verifier-cleanup.mjs"; import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js"; assert.equal(process.platform, "darwin", "requires macOS"); @@ -48,11 +52,9 @@ const digest = async (path) => createHash("sha256") .update(await readFile(path)) .digest("hex"); -const sessionsBefore = await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, -); -const ownedProcessIds = new Set(); +const sessionsBefore = ( + await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY) +).paths; const client = new Client({ name: "hopper-callers-verifier", version: "1" }); const transport = new StdioClientTransport({ command: process.execPath, @@ -93,7 +95,7 @@ const matchingRef = (ref, site, from, to) => ref.source_procedure?.address === from && ref.target_procedure?.address === to && ref.call?.classification === "direct"; -try { +await withVerifierCleanup(async () => { await run(clang, [ "-isysroot", sdk, @@ -129,7 +131,6 @@ try { await client.connect(transport); if (transport.pid === null) throw new Error("MCP process identity unavailable"); - ownedProcessIds.add(transport.pid); let notifications = 0; const original = transport.onmessage; transport.onmessage = (message) => { @@ -287,23 +288,36 @@ try { sha256: beforeLarge, }), ); -} finally { - await client.close(); - await transport.close(); - await new Promise((resolve) => setTimeout(resolve, 500)); - const leaked = [ - ...(await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, - ownedProcessIds, - )), - ].filter((path) => !sessionsBefore.has(path)); - if (leaked.length === 0) +}, [ + async () => { + try { + await closeVerifierResources([ + () => client.close(), + () => transport.close(), + async () => { + await new Promise((resolve) => setTimeout(resolve, 500)); + const leaked = [ + ...( + await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY) + ).paths, + ].filter((path) => !sessionsBefore.has(path)); + assert.deepEqual( + leaked, + [], + `Hopper resources remain: ${leaked.join(", ")}`, + ); + }, + ]); + } catch (cause) { + throw new Error( + `Hopper cleanup failed; fixture retained at ${directory}`, + { + cause, + }, + ); + } finally { + if (stderr) process.stderr.write(stderr); + } await rm(directory, { recursive: true, force: true }); - if (stderr) process.stderr.write(stderr); - assert.deepEqual( - leaked, - [], - `Hopper resources remain: ${leaked.join(", ")}; fixture retained at ${directory}`, - ); -} + }, +]); diff --git a/scripts/verify-real-hopper-deadlines.mjs b/scripts/verify-real-hopper-deadlines.mjs index 77f3b6b329..2cbf81df81 100644 --- a/scripts/verify-real-hopper-deadlines.mjs +++ b/scripts/verify-real-hopper-deadlines.mjs @@ -1,10 +1,11 @@ import assert from "node:assert/strict"; -import { copyFile, mkdtemp, realpath, rm } from "node:fs/promises"; +import { copyFile, mkdtemp, realpath } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { firstProcedureAddress } from "./lib/real-hopper-assertions.mjs"; +import { HopperDeadlineCleanup } from "./lib/hopper-deadline-cleanup.mjs"; import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js"; import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js"; import { AnalysisCleanupLease } from "../dist/artifacts/AnalysisCleanupLease.js"; @@ -29,6 +30,7 @@ const root = await realpath( ); const target = join(root, "deadline-fixture"); const profileResources = new ArtifactResourceScope(); +const cleanupOwner = new HopperDeadlineCleanup(root, profileResources); const profileResourceScope = { retain: (owner, resource) => profileResources.retain({ @@ -40,6 +42,7 @@ const profileResourceScope = { }; let client; let primaryFailure; +let operationFailed = false; try { await copyFile(fixtures.primary.path, target); const parsed = await parseBinaryTarget(target); @@ -148,31 +151,19 @@ try { ), ); } catch (cause) { + operationFailed = true; primaryFailure = cause; throw cause; } finally { - let cleanupFailure; - if (client !== undefined) { - const closed = await client.close(); - if (!closed.ok) cleanupFailure = closed.error; - } try { - await profileResources.close(); - } catch (cause) { - cleanupFailure = - cleanupFailure === undefined - ? cause - : new AggregateError( - [cleanupFailure, cause], - "Hopper profile and client cleanup failed", - ); - } - await rm(root, { recursive: true, force: true }); - if (cleanupFailure !== undefined) - throw primaryFailure === undefined - ? cleanupFailure - : new AggregateError( + if (client !== undefined) cleanupOwner.setClient(client); + await cleanupOwner.close(); + } catch (cleanupFailure) { + throw operationFailed + ? new AggregateError( [primaryFailure, cleanupFailure], "Hopper verification and cleanup failed", - ); + ) + : cleanupFailure; + } } diff --git a/scripts/verify-real-hopper-fat.mjs b/scripts/verify-real-hopper-fat.mjs index 930935ef7c..99dd1330f2 100644 --- a/scripts/verify-real-hopper-fat.mjs +++ b/scripts/verify-real-hopper-fat.mjs @@ -23,6 +23,10 @@ import { requireMcpOperationResult, } from "./lib/mcp-verifier-results.mjs"; import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs"; +import { + closeVerifierResources, + withVerifierCleanup, +} from "./lib/verifier-cleanup.mjs"; import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/process/HopperLease.js"; import { parseConfig } from "../dist/config/parseConfig.js"; import { @@ -70,14 +74,12 @@ const intel = join(directory, "c-x86_64"); const fat = join(directory, "c-fat"); const fat64 = join(directory, "c-fat64"); const singleFat = join(directory, "c-single-fat"); -const owned = new Set(); const extraFat64 = []; const invalidFat64 = []; let preparedImagePath; -const before = await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, -); +const before = ( + await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY) +).paths; const client = new Client({ name: "hopper-fat-verifier", version: "1" }); const transport = new StdioClientTransport({ command: process.execPath, @@ -94,7 +96,7 @@ const ajv = new Ajv({ strict: false }); addFormats(ajv); const observations = []; const sourceBinding = []; -try { +await withVerifierCleanup(async () => { await run(toolPaths.clang, [ "-isysroot", sdk, @@ -183,9 +185,17 @@ try { "overlap", /slices overlap/u, (bytes) => { - const cpu = process.arch === "arm64" ? 0x100000c : 0x1000007; - const base = bytes.readUInt32BE(8) === cpu ? 8 : 40; - bytes.copy(bytes, base === 8 ? 40 : 8, base, base + 32); + const firstOffset = bytes.readBigUInt64BE(16); + const secondOffset = bytes.readBigUInt64BE(48); + const earlier = firstOffset < secondOffset ? 8 : 40; + const laterOffset = + firstOffset < secondOffset ? secondOffset : firstOffset; + const earlierOffset = + firstOffset < secondOffset ? firstOffset : secondOffset; + bytes.writeBigUInt64BE( + laterOffset - earlierOffset + 16n, + earlier + 16, + ); }, ], ]) { @@ -211,11 +221,14 @@ try { } const path = join(directory, "c-fat64-ambiguous"); await writeFile(path, ambiguous); - invalidFat64.push({ path, reason: /selection.*ambiguous/u }); + invalidFat64.push({ + path, + reason: + /FAT binary declares several (?:arm64|x86_64) slices; no slice was selected/u, + }); } await client.connect(transport); assert.ok(transport.pid !== null); - owned.add(transport.pid); const catalog = await client.listTools(); const call = async (name, args = {}) => { const reply = await client.callTool( @@ -359,14 +372,27 @@ try { if (verifyFat64) { await call("open_binary", { path: fat64 }); await call("list_procedures"); - const resources = [ - ...(await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, - owned, - )), - ].filter((path) => !before.has(path)); - for (const root of resources) { + const session = await call("binary_session"); + const runId = session.analysis_run?.run_id; + assert.ok( + typeof runId === "string" && runId.length > 0, + "Missing analysis run identity", + ); + const resources = await snapshotHopperRuntime( + "/tmp", + HOPPER_TARGET_LEASE_DIRECTORY, + runId, + ); + const unresolvedNew = [...resources.unresolvedSessions].filter( + (path) => !before.has(path), + ); + assert.deepEqual( + unresolvedNew, + [], + "Cannot identify a prepared image while new Hopper resource ownership is unresolved", + ); + for (const root of resources.confirmedSessions) { + if (before.has(root)) continue; const image = join(root, "image.macho"); if ( await access(image).then( @@ -403,27 +429,43 @@ try { }, ); assert.ok(pending.child.pid !== undefined); - owned.add(pending.child.pid); const cli = JSON.parse((await pending).stdout); assert.deepEqual(cli.normalized_result, expected.mapping); } -} finally { - await client.close(); - await transport.close(); - await rm(directory, { recursive: true, force: true }); -} -const retained = [ - ...(await snapshotHopperRuntime( - "/tmp", - HOPPER_TARGET_LEASE_DIRECTORY, - owned, - )), -].filter((path) => !before.has(path)); -assert.deepEqual( - retained, - [], - "FAT verification retained owned Hopper sessions or leases", -); +}, [ + async () => { + try { + await closeVerifierResources([ + () => client.close(), + () => transport.close(), + ]); + } catch (cause) { + throw new Error( + `Hopper cleanup failed; fixture retained at ${directory}`, + { + cause, + }, + ); + } + try { + const retained = [ + ...(await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY)) + .paths, + ].filter((path) => !before.has(path)); + assert.deepEqual( + retained, + [], + "Hopper resources remain or are unresolved", + ); + } catch (cause) { + throw new Error( + `Hopper resource verification failed; fixture retained at ${directory}`, + { cause }, + ); + } + await rm(directory, { recursive: true, force: true }); + }, +]); console.log( JSON.stringify( { diff --git a/scripts/verify-real-hopper.mjs b/scripts/verify-real-hopper.mjs index 6e6cce01e5..ca183ffd0b 100644 --- a/scripts/verify-real-hopper.mjs +++ b/scripts/verify-real-hopper.mjs @@ -62,10 +62,9 @@ const parseServerArgs = (encoded) => { }; const runtimeParent = process.platform === "darwin" ? "/tmp" : tmpdir(); -const sessionsBefore = await snapshotHopperRuntime( - runtimeParent, - HOPPER_TARGET_LEASE_DIRECTORY, -); +const sessionsBefore = ( + await snapshotHopperRuntime(runtimeParent, HOPPER_TARGET_LEASE_DIRECTORY) +).paths; const ownedProcessIds = new Set(); const textValue = mcpTextValue; @@ -557,11 +556,8 @@ try { await new Promise((resolve) => setTimeout(resolve, 500)); const sessionsAfter = [ - ...(await snapshotHopperRuntime( - runtimeParent, - HOPPER_TARGET_LEASE_DIRECTORY, - ownedProcessIds, - )), + ...(await snapshotHopperRuntime(runtimeParent, HOPPER_TARGET_LEASE_DIRECTORY)) + .paths, ] .filter((path) => !sessionsBefore.has(path)) .sort(); @@ -573,7 +569,7 @@ if (sessionsAfter.length > 0) { retainedRuntimePaths: sessionsAfter, }); throw new Error( - `The MCP runtime retained Hopper resources: ${sessionsAfter.join(", ")}`, + `Hopper resources remain or have unresolved ownership: ${sessionsAfter.join(", ")}`, ); } if (summary === undefined) diff --git a/scripts/verify-test-discovery.mjs b/scripts/verify-test-discovery.mjs index 5db6905852..57d6d177ca 100644 --- a/scripts/verify-test-discovery.mjs +++ b/scripts/verify-test-discovery.mjs @@ -40,7 +40,7 @@ try { const expected = []; for (const path of files.stdout .split("\0") - .filter((path) => path.endsWith(".test.ts"))) { + .filter((path) => /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(path))) { try { if ((await stat(join(root, path))).isFile()) expected.push(path); } catch (cause) { @@ -65,4 +65,6 @@ try { await rm(temporary, { recursive: true, force: true }); } if (discovered !== undefined) - console.log(`Discovered all ${discovered} test files exactly once.`); + console.log( + `Discovered all ${discovered} test and spec files in src and tests exactly once.`, + ); diff --git a/scripts/verify/apple/dispatch.mjs b/scripts/verify/apple/dispatch.mjs index cfb77eff32..7021a80bbf 100644 --- a/scripts/verify/apple/dispatch.mjs +++ b/scripts/verify/apple/dispatch.mjs @@ -6,7 +6,7 @@ import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js"; -import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js"; +import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs"; import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js"; import { artifactCliEvidence, @@ -57,8 +57,7 @@ const fixupCoverage = (metadata) => const inspect = async (path) => { const target = await parseBinaryTarget(path); if (!target.ok) throw target.error; - const resources = new ArtifactResourceScope(); - try { + return withVerifierArtifactScope(async (resources) => { return { target: target.value, metadata: ( @@ -67,10 +66,7 @@ const inspect = async (path) => { ) ).result, }; - } finally { - // Retry any descriptor whose close failed, surfacing unresolved cleanup. - await resources.close(); - } + }); }; const checkObjc = (metadata, mode) => { diff --git a/scripts/verify/architecture-guards.mjs b/scripts/verify/architecture-guards.mjs index c3b51552fb..540c72f76f 100644 --- a/scripts/verify/architecture-guards.mjs +++ b/scripts/verify/architecture-guards.mjs @@ -5,17 +5,12 @@ import { join, relative, sep } from "node:path"; import { fileURLToPath } from "node:url"; // Architecture guards for the strict-identity / best-effort-inference split. -// - Evidence ordering must be locale-independent (ICU varies by host), so -// src/domain orders with compareUnicodeCodePoints, never localeCompare. // - Composite dedup keys must not NUL-join variable-length or free-form // MIDDLE fields: ["a\0b"] collapses with ["a", "b"] and drops a finding. -// compositeKey (JSON tuple) is the single owner for composite keys. +// The text check below only flags one spelling; it is not a complete audit. // - Transparent wrappers unwrap in exactly one place // (javascriptAstValues.unwrapJavaScriptExpression); direct isTS* unwrapping // elsewhere drifts (the `satisfies` class of bug). -// - AST walks go through traverseJavaScriptAst/childNodes: bespoke -// Object.values walkers diverge on loc/comment fields and overflow the -// stack on deep generated chains. const root = fileURLToPath(new URL("../../", import.meta.url)); @@ -27,6 +22,7 @@ const listFiles = async (dir) => { else if ( entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") && + !entry.name.endsWith(".spec.ts") && !entry.name.endsWith(".fixture.ts") ) output.push(path); @@ -48,8 +44,6 @@ const files = await Promise.all( content: stripComments(await readFile(path, "utf8")), })), ); -const inDir = (dir) => files.filter((file) => file.path.startsWith(dir)); - const failures = []; const check = (label, selected, pattern, allow = []) => { const offenders = selected.filter( @@ -68,21 +62,7 @@ check( /providers\/ProviderInspectionScope\.js/, ); -check( - "src/domain uses localeCompare (use compareUnicodeCodePoints)", - inDir("src/domain/"), - /\.localeCompare\(/, -); - -check( - "NUL-joined list key (use compositeKey)", - files, - /\.join\(["']\\0["']\)/, - [ - // Sorted pair of constrained trace IDs; documented in plan. - "src/domain/process/processTraceSpecification.ts", - ], -); +check("NUL-joined list key (use compositeKey)", files, /\.join\(["']\\0["']\)/); check( "direct transparent-wrapper unwrap outside javascriptAstValues (use unwrapJavaScriptExpression)", @@ -91,13 +71,6 @@ check( ["src/domain/javascript/javascriptAstValues.ts"], ); -check( - "bespoke AST child walker (use traverseJavaScriptAst/childNodes)", - files, - /VISITOR_KEYS/, - ["src/domain/javascript/javascriptSemanticTraversal.ts"], -); - // A producer that pins both the soft and hard ceiling of every resource takes // the vector from one owner. prlimitCeilingArguments is the command-less prefix // of prlimitArgumentVector; both live in src/process/LinuxResourceLimits.ts. @@ -163,7 +136,6 @@ check( "src/application/GuidanceAccess.ts", "src/application/javascript/ElectronBoundaryAnalysis.ts", "src/application/javascript/ElectronBoundaryGraphContext.ts", - "src/application/javascript/JavaScriptArtifactGraphContext.ts", "src/browser/CdpCaptureCompleteness.ts", "src/browser/CdpCaptureEventHandlers.ts", "src/browser/CdpPageCapture.ts", @@ -179,7 +151,6 @@ check( "src/domain/javascript/javascriptNativeHandoff.ts", "src/domain/javascript/javascriptRuntimeReconciliationMatching.ts", "src/domain/javascript/javascriptRuntimeReconciliationSchemas.ts", - "src/domain/javascript/javascriptSemanticQuery.ts", "src/domain/javascript/javascriptStaticAnalysisBundler.ts", "src/domain/javascript/javascriptStaticAnalysisCalls.ts", "src/domain/javascript/javascriptStaticAnalysisFindings.ts", diff --git a/scripts/verify/javascript/runtime-observation.mjs b/scripts/verify/javascript/runtime-observation.mjs index bd1781ca52..d77d6790fa 100644 --- a/scripts/verify/javascript/runtime-observation.mjs +++ b/scripts/verify/javascript/runtime-observation.mjs @@ -5,7 +5,6 @@ import { parseArgs } from "node:util"; import { readStableArtifact } from "../../../dist/artifacts/readStableArtifact.js"; import { ArtifactReaderFailure } from "../../../dist/artifacts/ArtifactReader.js"; -import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js"; import { ArtifactFileCleanupFailure, artifactFileCompletion, @@ -14,6 +13,7 @@ import { import { reconcileJavaScriptRuntimeEvidence } from "../../../dist/application/javascript/JavaScriptRuntimeReconciliationService.js"; import { analyzeJavaScriptApplicationWithOwnedScope } from "../../lib/javascript-application-analysis.mjs"; +import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs"; import { completeVerifierRun, createVerifierRun, @@ -106,33 +106,23 @@ process.stdout.write( ); async function readEvidenceFile(path) { - const scope = new ArtifactResourceScope(); let outcome; try { outcome = { ok: true, - value: await readStableArtifact( - await realpath(path), - 64 * 1024 * 1024, - undefined, - undefined, - scope, + value: await withVerifierArtifactScope(async (scope) => + readStableArtifact( + await realpath(path), + 64 * 1024 * 1024, + undefined, + undefined, + scope, + ), ), }; } catch (cause) { outcome = { ok: false, cause }; } - try { - await scope.close(); - } catch (cleanupCause) { - if (!outcome.ok) - throw new AggregateError( - [outcome.cause, cleanupCause], - "Verifier cleanup failed", - { cause: outcome.cause }, - ); - throw cleanupCause; - } if (!outcome.ok && outcome.cause instanceof ArtifactFileCleanupFailure) { const completion = artifactFileCompletion(outcome.cause); outcome = diff --git a/scripts/verify/managed/manifest.mjs b/scripts/verify/managed/manifest.mjs index 23be723fd6..77d82f0571 100644 --- a/scripts/verify/managed/manifest.mjs +++ b/scripts/verify/managed/manifest.mjs @@ -1,10 +1,9 @@ import { basename, dirname, isAbsolute, resolve } from "node:path"; -import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js"; import { readManagedSnapshot, verifyManagedSourceIdentity, } from "../../../dist/dotnet/ManagedSnapshot.js"; -import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs"; +import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs"; import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js"; import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js"; @@ -24,13 +23,10 @@ import { const READ_FILE_MAX_BYTES = 0x7fff_ffff; export async function verifyManagedAppManifest(rawManifest, manifestPath) { - const resources = new ArtifactResourceScope(); - return withVerifierCleanup( - () => - resources.run(() => - verifyManifestSnapshot(rawManifest, manifestPath, resources), - ), - [() => resources.close()], + return withVerifierArtifactScope((resources) => + resources.run(() => + verifyManifestSnapshot(rawManifest, manifestPath, resources), + ), ); } diff --git a/scripts/verify/rea-numeric-limits.d.mts b/scripts/verify/rea-numeric-limits.d.mts index f61bd401c8..9b3762519c 100644 --- a/scripts/verify/rea-numeric-limits.d.mts +++ b/scripts/verify/rea-numeric-limits.d.mts @@ -1,13 +1,6 @@ /** Relative path of the registry. Its text is a declaration, not a producer. */ export const NUMERIC_LIMIT_DECLARATION: "src/domain/numericLimitRegistry.ts"; -/** Limits that must become analysis settings and cannot be registry exceptions. */ -export const REQUIRED_RUNTIME_LIMITS: readonly [ - "REA_NTFS_MFT_MAX_FILE_BYTES", - "REA_REGF_HIVE_MAX_FILE_BYTES", - "REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES", -]; - /** True for a numeric `REA_` limit, including the documented endpoint-candidate count. */ export function isNumericLimit(name: string): boolean; @@ -55,6 +48,8 @@ export function discoverNumericLimitUses( readonly unresolved: readonly { readonly path: string; readonly offset: number; + readonly line: number; + readonly column: number; readonly reason: string; }[]; }; diff --git a/scripts/verify/rea-numeric-limits.mjs b/scripts/verify/rea-numeric-limits.mjs index 60cfdf21af..53f981ae6f 100644 --- a/scripts/verify/rea-numeric-limits.mjs +++ b/scripts/verify/rea-numeric-limits.mjs @@ -25,6 +25,7 @@ const listFiles = async (dir) => { } else if ( entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") && + !entry.name.endsWith(".spec.ts") && !entry.name.endsWith(".fixture.ts") ) output.push(path); @@ -39,7 +40,7 @@ export const NUMERIC_LIMIT_DECLARATION = "src/domain/numericLimitRegistry.ts"; * These limits are moving onto runtime analysis settings on their own PRs. * A producer or a registry entry cannot satisfy that requirement. */ -export const REQUIRED_RUNTIME_LIMITS = [ +const REQUIRED_RUNTIME_LIMITS = [ "REA_NTFS_MFT_MAX_FILE_BYTES", "REA_REGF_HIVE_MAX_FILE_BYTES", "REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES", @@ -170,8 +171,16 @@ const stringBindings = (sourceFile, symbols, unresolved) => { item.offset === offset && item.reason === reason, ) - ) - unresolved.push({ path, offset, reason }); + ) { + const location = sourceFile.getLineAndCharacterOfPosition(offset); + unresolved.push({ + path, + offset, + line: location.line + 1, + column: location.character + 1, + reason, + }); + } }; const initializer = (identifier) => { if (!candidates.has(identifier.text)) return undefined; @@ -949,7 +958,7 @@ if (direct) { for (const issue of discovered.unresolved) console.log( - `${issue.path}:${issue.offset}: numeric limit candidate unresolved (${issue.reason})`, + `${issue.path}:${issue.line}:${issue.column}: numeric limit candidate unresolved (${issue.reason})`, ); for (const [name, file] of [...discovered].sort(([left], [right]) => diff --git a/src/android/AndroidNativeInventory.ts b/src/android/AndroidNativeInventory.ts index 1000c37e23..0b11746e7c 100644 --- a/src/android/AndroidNativeInventory.ts +++ b/src/android/AndroidNativeInventory.ts @@ -1,6 +1,8 @@ import type { JadxToolPort } from "./JadxAnalysis.js"; -import type { AndroidNativeDeclarations } from "../domain/android/androidNativeDeclarations.js"; -import { jadxNativeDeclarationsSchema } from "./JadxProtocol.js"; +import { + androidNativeDeclarationsSchema, + type AndroidNativeDeclarations, +} from "../domain/android/androidNativeDeclarations.js"; import { AnalysisOutputError } from "../domain/analysisErrorCore.js"; /** Validate the native-only batch at its producing boundary, preserving raw facts. */ @@ -9,7 +11,7 @@ export const readAndroidNativeInventory = async ( snapshot: string, ): Promise => { const raw = await tools.json("rea_native_declarations", { path: snapshot }); - const parsed = jadxNativeDeclarationsSchema.safeParse(raw); + const parsed = androidNativeDeclarationsSchema.safeParse(raw); if (!parsed.success) throw new AnalysisOutputError( "inspect_android_jni_bindings", diff --git a/src/android/JadxProtocol.ts b/src/android/JadxProtocol.ts index 278bc2dd42..66bfbd39ed 100644 --- a/src/android/JadxProtocol.ts +++ b/src/android/JadxProtocol.ts @@ -10,11 +10,6 @@ import { dexDescriptorSchema, dexTypeSchema, } from "../domain/android/dexTypes.js"; -import { androidNativeDeclarationsSchema } from "../domain/android/androidNativeDeclarations.js"; - -/** Native-only raw DEX facts authored before decompiler deduplication. */ -export const jadxNativeDeclarationsSchema = androidNativeDeclarationsSchema; - const count = z.int().nonnegative(); /** Typed allocation failures authored before upstream erases the JVM exception. */ const jadxMemoryFailureSchema = z.strictObject({ diff --git a/src/apkSignature/apkSignatureFailures.ts b/src/apkSignature/apkSignatureFailures.ts index d63a2942b2..4e29403e84 100644 --- a/src/apkSignature/apkSignatureFailures.ts +++ b/src/apkSignature/apkSignatureFailures.ts @@ -1,6 +1,6 @@ import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; -import { stableArtifactInputRecovery } from "../artifacts/StableArtifactFailure.js"; +import { stableArtifactInputFailure } from "../artifacts/StableArtifactFailure.js"; import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, @@ -231,11 +231,7 @@ const artifactReaderFailure = ( }, ); case "path": - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); default: return adapterFailure(cause, path); } diff --git a/src/application/ClientRegistration.ts b/src/application/ClientRegistration.ts index a0824ccc1f..6fb7fa6aa2 100644 --- a/src/application/ClientRegistration.ts +++ b/src/application/ClientRegistration.ts @@ -18,7 +18,10 @@ import { type ClientRegistrationDialect, } from "./ClientConfigurationDocument.js"; import type { SetupClient } from "./SupportedClients.js"; -import type { ClientRegistrationTarget } from "./ClientRegistrationProfile.js"; +import { + stdioSelectingFields, + type ClientRegistrationTarget, +} from "./ClientRegistrationProfile.js"; import { classifyClientRegistrationCommand, equivalentClientRegistrationCommands, @@ -277,12 +280,18 @@ export const clientRegistrationSourceFacts = ( ), ); } else { + const launcherFields = new Set(fields); for (const field of connection.kind === "cline" ? ["transport", "type", "transportType", "url"] : connection.kind === "commandcode" ? ["type", "url"] : connection.fields) fields.add(field); + // An existing selector that already chooses stdio stays, including one + // inherited through a YAML merge; only remote selectors are superseded. + if (connection.kind === "flat") + for (const field of stdioSelectingFields(source ?? {}, connection)) + if (!launcherFields.has(field)) fields.delete(field); } return ok({ ...parsed, connectionFields: fields }); }; diff --git a/src/application/ClientRegistrationProfile.ts b/src/application/ClientRegistrationProfile.ts index 31904a31ca..0365750d04 100644 --- a/src/application/ClientRegistrationProfile.ts +++ b/src/application/ClientRegistrationProfile.ts @@ -51,3 +51,28 @@ export interface ClientRegistrationTarget { readonly dialect: ClientRegistrationDialect; readonly profile: ClientRegistrationProfile; } + +/** + * Enum selectors whose current value already chooses stdio. Replacing the + * launcher with a stdio launcher leaves them correct, so a refresh keeps them + * instead of superseding them like a remote selector. + */ +export const stdioSelectingFields = ( + source: Readonly>, + connection: Extract, +): ReadonlySet => + new Set( + connection.stdio.flatMap((condition) => { + if ( + condition.kind !== "required-enum" && + condition.kind !== "optional-enum" + ) + return []; + const value = Object.hasOwn(source, condition.field) + ? source[condition.field] + : undefined; + return typeof value === "string" && condition.values.includes(value) + ? [condition.field] + : []; + }), + ); diff --git a/src/application/GooseConfigurationYaml.test.ts b/src/application/GooseConfigurationYaml.test.ts index c12ceb116b..727196ef2d 100644 --- a/src/application/GooseConfigurationYaml.test.ts +++ b/src/application/GooseConfigurationYaml.test.ts @@ -18,6 +18,123 @@ const extension = { timeout: 300, }; +describe("Goose YAML source key retention", () => { + it("updates and removes an extension selected through aliased path keys", () => { + const source = + "table: &table extensions\nname: &name rea\n? *table\n:\n ? *name\n : {cmd: old, type: stdio}\n"; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + cmd: "rea", + type: "stdio", + }); + expect(updated).toContain("*table"); + expect(updated).toContain("*name"); + expect(parseGooseConfigurationYaml(updated)).toMatchObject({ + extensions: { rea: { cmd: "rea", type: "stdio" } }, + }); + const removed = editGooseConfigurationYaml( + updated, + ["extensions", "rea"], + undefined, + ); + expect(removed).not.toContain("*name"); + expect(parseGooseConfigurationYaml(removed)).toMatchObject({ + extensions: {}, + }); + const ambiguous = + "name: &name rea\nextensions:\n ? *name\n : {cmd: first}\n rea: {cmd: second}\n"; + expect(() => + editGooseConfigurationYaml(ambiguous, ["extensions", "rea"], { + cmd: "rea", + }), + ).toThrow(/more than one key resolving to "rea"/); + }); + + it("retains a collection mapping key when an adjacent field changes", () => { + const source = + "extensions:\n rea:\n cmd: old\n ? [one, two]\n : retained\n"; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...parsed.extensions.rea, + cmd: "rea", + }); + const document = parseDocument(updated); + expect(document.errors).toEqual([]); + const native = document.toJS({ mapAsMap: true }) as Map; + const extensions = native.get("extensions") as Map; + const entry = extensions.get("rea") as Map; + expect([...entry.entries()]).toContainEqual([["one", "two"], "retained"]); + expect(entry.get("cmd")).toBe("rea"); + }); + + it("retains an aliased mapping key when an adjacent launcher field changes", () => { + const source = + 'label: &label 1\nextensions:\n rea:\n cmd: old\n ? *label\n : numeric\n "1": string\n'; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...parsed.extensions.rea, + cmd: "rea", + }); + expect(updated).toContain("*label"); + expect(updated).toContain("numeric"); + expect(updated).toContain('"1": string'); + }); + + it("preserves the order of an anchor and a retained non-string key alias", () => { + const source = + 'extensions:\n rea:\n cmd: old\n shared: &seed value\n 1: *seed\n "1": string\n'; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...parsed.extensions.rea, + cmd: "rea", + }); + expect(updated.indexOf("&seed")).toBeLessThan(updated.indexOf("*seed")); + expect(parseGooseConfigurationYaml(updated)).toMatchObject({ + extensions: { rea: { shared: "value", cmd: "rea" } }, + }); + }); + + it("retains distinct keys beside an edited field and rejects an ambiguous edit", () => { + const source = + 'extensions:\n rea:\n cmd: old\n 1: numeric\n "1": string\n'; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const entry = parsed.extensions.rea; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...entry, + cmd: "rea", + }); + expect(updated).toContain(' 1: numeric\n "1": string'); + expect(updated).toContain("cmd: rea"); + expect(() => + editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...entry, + "1": "changed", + }), + ).toThrow(/source identity is not a plain string key/); + }); + + it("keeps distinct native mapping keys in unchanged extension settings", () => { + const source = + 'extensions:\n rea:\n cmd: old\n metadata:\n 1: numeric\n "1": string\n'; + const parsed = parseGooseConfigurationYaml(source) as { + extensions: { rea: Record }; + }; + const updated = editGooseConfigurationYaml(source, ["extensions", "rea"], { + ...parsed.extensions.rea, + cmd: "rea", + }); + expect(updated).toContain(' 1: numeric\n "1": string'); + expect(updated).toContain("cmd: rea"); + }); +}); + describe("Goose YAML 1.2 source facts", () => { it("keeps native strings and exact large integer facts distinct", () => { const source = @@ -344,3 +461,45 @@ describe("pinned Goose serde_yaml scalar resolution", () => { }, ); }); + +it("materializes an alias whose sibling anchor the refresh replaces", () => { + const source = + "extensions:\n rea:\n cmd: &launcher /old/rea\n envs: {REA_LAUNCHER: *launcher}\n"; + const out = editGooseConfigurationYaml(source, ["extensions", "rea"], { + cmd: "/new/rea", + envs: { REA_LAUNCHER: "/old/rea" }, + }); + expect(parseGooseConfigurationYaml(out)).toMatchObject({ + extensions: { + rea: { cmd: "/new/rea", envs: { REA_LAUNCHER: "/old/rea" } }, + }, + }); +}); + +it("edits a field whose key is an alias to a shared string, keeping the alias", () => { + const source = + "field: &field envs\nextensions:\n rea:\n cmd: rea\n *field : {A: '1'}\n"; + const out = editGooseConfigurationYaml(source, ["extensions", "rea"], { + cmd: "rea", + envs: { A: "2" }, + }); + expect(out).toContain("*field"); + expect(parseGooseConfigurationYaml(out)).toMatchObject({ + field: "envs", + extensions: { rea: { cmd: "rea", envs: { A: "2" } } }, + }); +}); + +it("keeps an anchored value with an aliased key ahead of its later aliases", () => { + const source = + "field: &field envs\nextensions:\n rea:\n cmd: old\n *field : &settings {A: '1'}\n 1: *settings\n \"1\": string\n"; + const out = editGooseConfigurationYaml(source, ["extensions", "rea"], { + cmd: "new", + envs: { A: "1" }, + 1: "string", + }); + expect(out.indexOf("&settings")).toBeLessThan(out.indexOf("*settings")); + expect(parseGooseConfigurationYaml(out)).toMatchObject({ + extensions: { rea: { cmd: "new", envs: { A: "1" } } }, + }); +}); diff --git a/src/application/GooseConfigurationYaml.ts b/src/application/GooseConfigurationYaml.ts index 8d1ae5cd78..2f77b3704e 100644 --- a/src/application/GooseConfigurationYaml.ts +++ b/src/application/GooseConfigurationYaml.ts @@ -13,9 +13,12 @@ import { ConfigurationError } from "../domain/configurationErrors.js"; import { copyYamlAliasSource, preserveYamlAliasValues, + resolveYamlEditPath, } from "./YamlConfigurationAliases.js"; import { + materializeUnresolvedYamlAliases, retainYamlConfigurationSource, + yamlAliasValues, retainYamlScalarSource, } from "./YamlConfigurationSource.js"; @@ -251,7 +254,7 @@ export const editGooseConfigurationYaml = ( if (!isMap(document.contents)) throw new ConfigurationError("Goose configuration root must be a mapping"); for (let length = 1; length < path.length; length++) { - const parent = path.slice(0, length); + const parent = resolveYamlEditPath(document, path.slice(0, length)); let previous = document.getIn(parent, true); if (isAlias(previous)) { previous = copyYamlAliasSource(previous, document); @@ -262,30 +265,33 @@ export const editGooseConfigurationYaml = ( document.setIn(parent, document.createNode({})); } else if (!isMap(previous)) throw new ConfigurationError( - `Goose configuration ${parent.join(".")} must be a mapping`, + `Goose configuration ${path.slice(0, length).join(".")} must be a mapping`, ); } - const previous = document.getIn(path, true); - const parent = document.getIn(path.slice(0, -1), true); + const sourcePath = resolveYamlEditPath(document, path); + const previous = document.getIn(sourcePath, true); + const parent = document.getIn(sourcePath.slice(0, -1), true); const removedKey = value === undefined && isMap(parent) - ? parent.items.find( - (pair) => isScalar(pair.key) && pair.key.value === path.at(-1), - )?.key + ? parent.items.find((pair) => pair.key === sourcePath.at(-1))?.key : undefined; + const aliasValues = yamlAliasValues(previous, document); const retained = preserveYamlAliasValues(document, [previous, removedKey]); if (value === undefined) { - if (!document.deleteIn(path)) return text; - } else + if (!document.deleteIn(sourcePath)) return text; + } else { document.setIn( - path, + sourcePath, isMap(previous) || isSeq(previous) ? retainYamlConfigurationSource( previous, document.createNode(value), retained, + document, ) : value, ); + materializeUnresolvedYamlAliases(document, aliasValues); + } return document.toString({ lineWidth: 0 }); }; diff --git a/src/application/HermesConfigurationYaml.test.ts b/src/application/HermesConfigurationYaml.test.ts index 01e5d5e528..1b07b013eb 100644 --- a/src/application/HermesConfigurationYaml.test.ts +++ b/src/application/HermesConfigurationYaml.test.ts @@ -1,10 +1,13 @@ import { describe, expect, it } from "vitest"; import { parse } from "yaml"; import { + effectiveClientServer, parseClientConfiguration, serializeClientConfiguration, withClientServers, } from "./ClientConfigurationDocument.js"; +import { refreshClientRegistration } from "./ClientRegistration.js"; +import { supportedClients } from "./SupportedClients.js"; import { editHermesConfigurationYaml, @@ -12,6 +15,69 @@ import { stringifyHermesConfigurationYaml, } from "./HermesConfigurationYaml.js"; +it("uses the first inherited mapping when creating an override", () => { + const source = + "first: &first\n mcp_servers:\n one: {command: first}\nsecond: &second\n mcp_servers:\n two: {command: second}\n<<: [*first, *second]\n"; + const updated = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + }); + const parsed = parseHermesConfigurationYaml(updated) as { + mcp_servers: Record; + }; + expect(parsed.mcp_servers).toEqual({ + one: { command: "first" }, + rea: { command: "rea" }, + }); + expect( + parseHermesConfigurationYaml( + updated.replace("command: first", "command: changed"), + ), + ).toMatchObject({ + mcp_servers: { one: { command: "changed" } }, + }); +}); + +it("refuses an inherited override when an existing anchor name is shadowed", () => { + const source = + "shared: &shared\n mcp_servers: &table\n other: {command: first}\nshadow: &table {other: {command: second}}\n<<: *shared\n"; + expect(() => + editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + }), + ).toThrow(/anchor is shadowed/); +}); + +it("retains inherited YAML settings when updating a registration", () => { + const source = + "defaults: &defaults\n enabled: false\nmcp_servers:\n rea:\n <<: *defaults\n command: old\n"; + const parsed = parseHermesConfigurationYaml(source) as { + mcp_servers: { rea: Record }; + }; + const updated = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + ...parsed.mcp_servers.rea, + command: "rea", + }); + expect(updated).toContain("<<: *defaults"); + const changedDefault = updated.replace("enabled: false", "enabled: true"); + expect(parseHermesConfigurationYaml(changedDefault)).toMatchObject({ + mcp_servers: { rea: { command: "rea", enabled: true } }, + }); + const overridden = editHermesConfigurationYaml( + source, + ["mcp_servers", "rea"], + { ...parsed.mcp_servers.rea, command: "rea", enabled: true }, + ); + expect(overridden).toContain("<<: *defaults"); + expect(parseHermesConfigurationYaml(overridden)).toMatchObject({ + mcp_servers: { rea: { command: "rea", enabled: true } }, + }); + expect(() => + editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + }), + ).toThrow(/Cannot remove inherited YAML setting/); +}); + it("preserves annotations on changed registration mappings and scalar fields", () => { const source = "mcp_servers:\n rea: # registration note\n command: old # command note\n env: # environment note\n MODE: old # mode note\n args: # arguments note\n - old # argument note\n"; @@ -601,3 +667,193 @@ it("keeps argument annotations with unique values and retains removed annotation ]) expect(updated.split(comment)).toHaveLength(2); }); + +describe("Hermes registration refresh around anchors and merges", () => { + it("keeps an alias's value when an earlier same-name anchor would capture it", () => { + const source = + "defaults: &launcher /other/rea\nmcp_servers:\n rea:\n command: &launcher /old/rea\n env: {REA_LAUNCHER: *launcher}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "/new/rea", + env: { REA_LAUNCHER: "/old/rea" }, + }); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + defaults: "/other/rea", + mcp_servers: { + rea: { command: "/new/rea", env: { REA_LAUNCHER: "/old/rea" } }, + }, + }); + }); + + it("keeps the comment on an alias the refresh materializes", () => { + const source = + "mcp_servers:\n rea:\n command: &launcher /old/rea\n env:\n REA_LAUNCHER: *launcher # explanation\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "/new/rea", + env: { REA_LAUNCHER: "/old/rea" }, + }); + expect(out).toContain("# explanation"); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + mcp_servers: { rea: { env: { REA_LAUNCHER: "/old/rea" } } }, + }); + }); + + it("keeps an alias to a sibling anchor that survives the refresh", () => { + const source = + "mcp_servers:\n rea:\n command: old\n setting: &setting keep\n metadata: {nested: *setting}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "new", + setting: "keep", + metadata: { nested: "keep" }, + }); + expect(out).toContain("nested: *setting"); + expect( + parseHermesConfigurationYaml( + out.replace("&setting keep", "&setting changed"), + ), + ).toMatchObject({ + mcp_servers: { rea: { metadata: { nested: "changed" } } }, + }); + }); +}); + +describe("Hermes registration refresh through inherited merges", () => { + it("refuses to drop locally overridden settings that its merge would restore", () => { + const source = + "remote: &remote\n url: https://example.invalid\n transport: sse\nmcp_servers:\n rea:\n <<: *remote\n url: https://local.invalid\n transport: sse\n"; + expect(() => + editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + args: ["mcp"], + }), + ).toThrow(/Cannot remove inherited YAML setting/); + }); + + it("replaces an inherited remote registration without its remote selectors", () => { + const source = + "shared: &shared\n rea: {url: https://example.invalid, transport: sse}\nmcp_servers:\n <<: *shared\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + args: ["mcp"], + }); + const parsed = parseHermesConfigurationYaml(out) as { + mcp_servers: { rea: Record }; + }; + expect(parsed.mcp_servers.rea).toEqual({ command: "rea", args: ["mcp"] }); + }); + + it("refuses to edit a field whose aliased and plain keys name the same setting", () => { + const source = + "field: &field env\nmcp_servers:\n rea:\n command: rea\n *field : {A: first}\n env: {A: second}\n"; + expect(() => + editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + env: { A: "third" }, + }), + ).toThrow(/env/); + }); + + it("edits a field whose key is an alias to a shared string, keeping the alias", () => { + const source = + "field: &field env\nmcp_servers:\n rea:\n command: rea\n *field : {A: '1'}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + env: { A: "2" }, + }); + expect(out).toContain("*field"); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + field: "env", + mcp_servers: { rea: { command: "rea", env: { A: "2" } } }, + }); + }); + + it("writes a plain key when an aliased key's anchor is rebuilt by the same edit", () => { + const source = + "mcp_servers:\n rea:\n command: &field env\n *field : {A: '1'}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + env: { A: "2" }, + }); + expect(out).not.toContain("*field"); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + mcp_servers: { rea: { command: "rea", env: { A: "2" } } }, + }); + }); + + it("materializes an alias whose sibling anchor the refresh replaces", () => { + const source = + "mcp_servers:\n rea:\n command: &launcher /old/rea\n env: {REA_LAUNCHER: *launcher}\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "/new/rea", + env: { REA_LAUNCHER: "/old/rea" }, + }); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + mcp_servers: { + rea: { command: "/new/rea", env: { REA_LAUNCHER: "/old/rea" } }, + }, + }); + }); + + it("materializes an alias to an anchor moved to an outside consumer", () => { + const source = + "mcp_servers:\n rea:\n command: rea\n setting: &setting keep\n metadata: {nested: *setting}\nobserver: *setting\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea2", + setting: "changed", + metadata: { nested: "keep" }, + }); + expect(parseHermesConfigurationYaml(out)).toMatchObject({ + observer: "keep", + mcp_servers: { + rea: { setting: "changed", metadata: { nested: "keep" } }, + }, + }); + }); + + it("keeps an inherited stdio transport through a launcher refresh", () => { + const source = + "defaults: &defaults\n transport: stdio\n command: rea\n args: [mcp]\nmcp_servers:\n rea:\n <<: *defaults\n"; + const client = supportedClients("/home/rea-test").find( + ({ name }) => name === "hermes", + )!; + const parsed = parseClientConfiguration(source, "hermes"); + const registration = refreshClientRegistration( + client, + "hermes", + ["rea", "mcp"], + { A: "1" }, + effectiveClientServer(parsed, "rea"), + { + analysisProvider: "auto", + ghidraStartupTimeoutMs: 1, + hopperStartupTimeoutMs: 1, + }, + ); + if (!registration.ok) throw registration.error; + const written = serializeClientConfiguration( + withClientServers(parsed, { ...parsed.servers, rea: registration.value }), + "hermes", + source, + ); + if (!written.ok) throw written.error; + expect(written.value).toContain("<<: *defaults"); + expect(parseHermesConfigurationYaml(written.value)).toMatchObject({ + mcp_servers: { + rea: { transport: "stdio", command: "rea", env: { A: "1" } }, + }, + }); + }); + + it("overrides an inherited registration with only its changed fields", () => { + const source = + "shared: &shared\n rea: {command: rea, metadata: {marker: old}}\nmcp_servers:\n <<: *shared\n"; + const out = editHermesConfigurationYaml(source, ["mcp_servers", "rea"], { + command: "rea", + metadata: { marker: "old" }, + env: { A: "1" }, + }); + const changed = out.replace("marker: old", "marker: new"); + expect(parseHermesConfigurationYaml(changed)).toMatchObject({ + mcp_servers: { rea: { metadata: { marker: "new" }, env: { A: "1" } } }, + }); + }); +}); diff --git a/src/application/HermesConfigurationYaml.ts b/src/application/HermesConfigurationYaml.ts index d88437d586..a91a41db91 100644 --- a/src/application/HermesConfigurationYaml.ts +++ b/src/application/HermesConfigurationYaml.ts @@ -3,19 +3,26 @@ import { isMap, isScalar, isSeq, + Pair, parseDocument, Document, Schema, + YAMLMap, + type Node, type ParsedNode, type ScalarTag, type Tags, } from "yaml"; +import { ConfigurationError } from "../domain/configurationErrors.js"; import { preserveYamlAliasValues, copyYamlAliasSource, + resolveYamlEditPath, } from "./YamlConfigurationAliases.js"; import { + materializeUnresolvedYamlAliases, retainYamlConfigurationSource, + yamlAliasValues, retainYamlScalarSource, yamlNumericValuesEqual, } from "./YamlConfigurationSource.js"; @@ -268,6 +275,108 @@ const parseHermesDocument = (text: string) => { return document; }; +/** The value a path resolves to, merges included, or `absent`. */ +const projectedYamlValue = ( + document: Document, + path: readonly string[], +): { readonly value: unknown } | "absent" => { + let current: unknown = document.toJS(); + for (const segment of path) { + if ( + typeof current !== "object" || + current === null || + !Object.hasOwn(current, segment) + ) + return "absent"; + current = (current as Record)[segment]; + } + return { value: current }; +}; + +const projectedYamlPathExists = ( + document: Document, + path: readonly string[], +): boolean => projectedYamlValue(document, path) !== "absent"; + +const inheritedRegistrationRemovalError = () => + new ConfigurationError( + "The registration is inherited from a YAML merge. Remove it at the shared source before rerunning uninstall", + ); + +/** Find the source node selected by YAML merge precedence. */ +const inheritedYamlValue = ( + document: Document, + source: unknown, + name: string, + visiting = new Set(), +): unknown => { + const node = isAlias(source) ? source.resolve(document) : source; + if (!isMap(node) || visiting.has(node)) return undefined; + visiting.add(node); + try { + const matches = node.items.filter((pair) => { + const key = isAlias(pair.key) ? pair.key.resolve(document) : pair.key; + return isScalar(key) && key.value === name; + }); + if (matches.length > 1) + throw new ConfigurationError( + `YAML mapping contains more than one key resolving to ${JSON.stringify(name)}`, + ); + if (matches[0] !== undefined) return matches[0].value; + for (const pair of node.items) { + if ( + !isScalar(pair.key) || + typeof pair.key.value !== "symbol" || + pair.key.source !== "<<" + ) + continue; + const merged = isAlias(pair.value) + ? pair.value.resolve(document) + : pair.value; + const sources = isSeq(merged) ? merged.items : [merged]; + for (const candidate of sources) { + const found = inheritedYamlValue(document, candidate, name, visiting); + if (found !== undefined) return found; + } + } + return undefined; + } finally { + visiting.delete(node); + } +}; + +/** Create a local override that continues to inherit the shared mapping. */ +const inheritedYamlMapOverride = ( + document: Document, + parent: readonly (string | Node)[], + name: string, +): void => { + const owner = + parent.length === 1 + ? document.contents + : document.getIn(parent.slice(0, -1), true); + let inherited = inheritedYamlValue(document, owner, name); + if (isAlias(inherited)) inherited = inherited.resolve(document); + if (!isMap(inherited)) + throw new ConfigurationError( + `Inherited YAML setting ${JSON.stringify(name)} is not a mapping`, + ); + const mergeDocument = parseHermesDocument("<<: {}\n"); + if ( + !isMap(mergeDocument.contents) || + mergeDocument.contents.items[0] === undefined + ) + throw new ConfigurationError("Cannot create a YAML merge key"); + const alias = document.createAlias(inherited, "rea_inherited"); + const override = new YAMLMap(document.schema); + override.items.push(new Pair(mergeDocument.contents.items[0].key, alias)); + document.setIn(parent, override); + if (alias.resolve(document) !== inherited) + throw new ConfigurationError( + "The inherited YAML anchor is shadowed; edit its shared source before rerunning setup", + ); +}; + /** Parse a Hermes `config.yaml`, preserving the distinction between no root and explicit null. */ export const parseHermesConfigurationYaml = (text: string): unknown => { const document = parseHermesDocument(text); @@ -299,7 +408,7 @@ export const editHermesConfigurationYaml = ( document.contents = map; } for (let length = 1; length < path.length; length++) { - const parent = path.slice(0, length); + const parent = resolveYamlEditPath(document, path.slice(0, length)); const previous = document.getIn(parent, true); if (isAlias(previous)) { const copied = copyYamlAliasSource(previous, document); @@ -308,38 +417,74 @@ export const editHermesConfigurationYaml = ( document.setIn(parent, copied); } } else if (value !== undefined && !document.hasIn(parent)) { + if (projectedYamlPathExists(document, path.slice(0, length))) { + inheritedYamlMapOverride(document, parent, path[length - 1]!); + continue; + } // Implicit JS Map parents serialize as !!omap in YAML 1.1. document.setIn(parent, document.createNode({})); } } - const previous = document.getIn(path, true); + // A registration inherited whole through a merge keeps that inheritance: + // override it with the merge, then write only the fields that differ. + const inheritedSourcePath = resolveYamlEditPath(document, path); + // An explicit replacement that drops inherited fields, such as remote + // connection selectors, writes a local registration instead. + const inheritedRegistration = document.hasIn(inheritedSourcePath) + ? "absent" + : projectedYamlValue(document, path); + if ( + typeof value === "object" && + value !== null && + !Array.isArray(value) && + inheritedRegistration !== "absent" && + typeof inheritedRegistration.value === "object" && + inheritedRegistration.value !== null && + Object.keys(inheritedRegistration.value).every((key) => + Object.hasOwn(value, key), + ) + ) + inheritedYamlMapOverride(document, inheritedSourcePath, path.at(-1)!); + const sourcePath = resolveYamlEditPath(document, path); + const previous = document.getIn(sourcePath, true); // Deletion removes the pair's key too; replacement retains that key node. const parent = - value === undefined ? document.getIn(path.slice(0, -1), true) : undefined; + value === undefined + ? document.getIn(sourcePath.slice(0, -1), true) + : undefined; + if ( + value === undefined && + !document.hasIn(sourcePath) && + projectedYamlPathExists(document, path) + ) + throw inheritedRegistrationRemovalError(); const removedKey = isMap(parent) - ? parent.items.find( - (pair) => isScalar(pair.key) && pair.key.value === path.at(-1), - )?.key + ? parent.items.find((pair) => pair.key === sourcePath.at(-1))?.key : undefined; + const aliasValues = yamlAliasValues(previous, document); const retainedSources = preserveYamlAliasValues(document, [ previous, removedKey, ]); if (value === undefined) { - if (!document.deleteIn(path)) return text; + if (!document.deleteIn(sourcePath)) return text; + if (projectedYamlPathExists(document, path)) + throw inheritedRegistrationRemovalError(); } else { // Let YAML's scalar update preserve numeric formats for a direct edit. // Replacing containers needs explicit retention of unchanged child nodes. document.setIn( - path, + sourcePath, isMap(previous) || isSeq(previous) ? retainYamlConfigurationSource( previous, document.createNode(value), retainedSources, + document, ) : value, ); + materializeUnresolvedYamlAliases(document, aliasValues); } // Editing REA must not reflow unrelated user prompts or other YAML scalars. return document.toString({ diff --git a/src/application/Uninstall.ts b/src/application/Uninstall.ts index 5cc8a452db..b128764c1e 100644 --- a/src/application/Uninstall.ts +++ b/src/application/Uninstall.ts @@ -1089,11 +1089,13 @@ const removeClient = async ( // clientRegistrationRemoval admitted only names whose repeats REA owns. { removeShadowedMembersAt: editedPaths }, ); - } catch { + } catch (cause: unknown) { return item( client.name, "failed", - "Configuration could not be updated, so no change was made. Repair the configuration, then rerun uninstall.", + cause instanceof ConfigurationError + ? `Configuration could not be updated, so no change was made. ${cause.message}.` + : "Configuration could not be updated, so no change was made. Repair the configuration, then rerun uninstall.", ); } if (!serialized.ok) diff --git a/src/application/YamlConfigurationAliases.ts b/src/application/YamlConfigurationAliases.ts index 9f3b8a8283..1dd01f8786 100644 --- a/src/application/YamlConfigurationAliases.ts +++ b/src/application/YamlConfigurationAliases.ts @@ -9,6 +9,36 @@ import { type Document, type Node, } from "yaml"; +import { ConfigurationError } from "../domain/configurationErrors.js"; + +/** Resolve literal and aliased string keys to their source nodes for editing. */ +export const resolveYamlEditPath = ( + document: Document, + path: readonly string[], +): (string | Node)[] => { + const resolved: (string | Node)[] = []; + let current: unknown = document.contents; + for (const name of path) { + if (isAlias(current)) current = current.resolve(document); + if (!isMap(current)) { + resolved.push(name); + current = undefined; + continue; + } + const matches = current.items.filter((pair) => { + const key = isAlias(pair.key) ? pair.key.resolve(document) : pair.key; + return isScalar(key) && key.value === name; + }); + if (matches.length > 1) + throw new ConfigurationError( + `YAML mapping contains more than one key resolving to ${JSON.stringify(name)}.`, + ); + const pair = matches[0]; + resolved.push(pair !== undefined && isNode(pair.key) ? pair.key : name); + current = pair?.value; + } + return resolved; +}; interface AliasCopies { readonly targets: ReadonlyMap; diff --git a/src/application/YamlConfigurationSource.ts b/src/application/YamlConfigurationSource.ts index 58c127fd67..0483279680 100644 --- a/src/application/YamlConfigurationSource.ts +++ b/src/application/YamlConfigurationSource.ts @@ -1,12 +1,21 @@ import { + isAlias, isMap, isNode, isScalar, isSeq, + Pair, + visit, + YAMLMap, + type Document, type Node, + type Alias, type ScalarTag, type Tags, + type YAMLSeq, } from "yaml"; +import { isDeepStrictEqual } from "node:util"; +import { ConfigurationError } from "../domain/configurationErrors.js"; import { retainYamlSequenceAnnotations } from "./YamlSequenceAnnotations.js"; export const yamlNumericValuesEqual = ( @@ -26,12 +35,99 @@ export const yamlNumericValuesEqual = ( ); }; -/** Keep source annotations and unchanged scalars when updating a value. */ +const normalizeYamlInteger = (_key: unknown, value: unknown): unknown => + typeof value === "bigint" && Number.isSafeInteger(Number(value)) + ? Number(value) + : value; + +const containsRetainedSource = ( + roots: readonly unknown[], + retainedSources: ReadonlySet, +): boolean => { + if (retainedSources.size === 0) return false; + let found = false; + for (const root of roots) { + if (!isNode(root)) continue; + visit(root, { + Value: (_key, node) => { + if (retainedSources.has(node)) found = true; + }, + }); + } + return found; +}; + +/** What each alias inside `root` resolves to before an edit. */ +export interface YamlAliasBinding { + readonly target: Node; + readonly value: unknown; +} + +/** The node and value each alias inside `root` resolves to before an edit. */ +export const yamlAliasValues = ( + root: unknown, + document: Document, +): ReadonlyMap => { + const bindings = new Map(); + if (!isNode(root)) return bindings; + visit(root, { + Alias: (_key, alias) => { + const target = alias.resolve(document); + if (target !== undefined) + bindings.set(alias, { target, value: target.toJS(document) }); + }, + }); + return bindings; +}; + +/** + * Replace each alias that no longer resolves to its original node, whether + * its anchor is gone or an earlier same-name anchor now captures it, with the + * value it resolved to before the edit. Its comments and spacing are kept. + */ +export const materializeUnresolvedYamlAliases = ( + document: Document, + bindings: ReadonlyMap, +): void => { + if (bindings.size === 0) return; + visit(document, { + Alias: (_key, alias) => { + const binding = bindings.get(alias); + if (binding === undefined || alias.resolve(document) === binding.target) + return undefined; + const replacement = document.createNode(binding.value); + if (alias.comment !== undefined) replacement.comment = alias.comment; + if (alias.commentBefore !== undefined) + replacement.commentBefore = alias.commentBefore; + if (alias.spaceBefore !== undefined) + replacement.spaceBefore = alias.spaceBefore; + return replacement; + }, + }); +}; + +/** Keep source nodes and annotations when updating a value. */ export const retainYamlConfigurationSource = ( previous: unknown, next: Node, retainedSources: ReadonlySet, + document: Document, ): Node => { + if ( + (isMap(previous) || isSeq(previous)) && + ((isMap(previous) && isMap(next)) || (isSeq(previous) && isSeq(next))) + ) { + // Object projection can collapse distinct YAML keys. Reuse an unchanged + // collection before reconstructing its children from that projection. + if ( + !containsRetainedSource([previous], retainedSources) && + isDeepStrictEqual( + previous.toJS(document, { reviver: normalizeYamlInteger }), + next.toJS(document, { reviver: normalizeYamlInteger }), + ) + ) + return previous; + } if ( isScalar(previous) && isScalar(next) && @@ -60,53 +156,273 @@ export const retainYamlConfigurationSource = ( next.spaceBefore = previous.spaceBefore; } if (isMap(previous) && isMap(next)) { - const entries = new Map( - previous.items.flatMap((pair) => - isScalar(pair.key) && typeof pair.key.value === "string" - ? [[pair.key.value, pair] as const] - : [], - ), - ); - for (const pair of next.items) { - const key = isScalar(pair.key) ? pair.key.value : undefined; - const prior = typeof key === "string" ? entries.get(key) : undefined; - if (prior === undefined) continue; - pair.key = isNode(pair.key) - ? retainYamlConfigurationSource(prior.key, pair.key, retainedSources) - : prior.key; - if (isNode(pair.value)) - pair.value = retainYamlConfigurationSource( - prior.value, - pair.value, - retainedSources, - ); - } + retainYamlMapSource(previous, next, retainedSources, document); } else if (isSeq(previous) && isSeq(next)) { - const stringItems = (items: readonly unknown[]) => - items.every((item) => isScalar(item) && typeof item.value === "string"); - if (stringItems(previous.items) && stringItems(next.items)) - retainYamlSequenceAnnotations( - previous, - next, - (item) => (isScalar(item) ? item.value : undefined), - (source, target) => - retainYamlConfigurationSource(source, target, retainedSources), - (source) => !retainedSources.has(source), - ); - else - next.items = next.items.map((item, index) => - isNode(item) - ? retainYamlConfigurationSource( - previous.items[index], - item, - retainedSources, - ) - : item, - ); + retainYamlSequenceSource(previous, next, retainedSources, document); } return next; }; +const retainYamlSequenceSource = ( + previous: YAMLSeq, + next: YAMLSeq, + retainedSources: ReadonlySet, + document: Document, +): void => { + const stringItems = (items: readonly unknown[]) => + items.every((item) => isScalar(item) && typeof item.value === "string"); + if (stringItems(previous.items) && stringItems(next.items)) { + retainYamlSequenceAnnotations( + previous, + next, + (item) => (isScalar(item) ? item.value : undefined), + (source, target) => + retainYamlConfigurationSource( + source, + target, + retainedSources, + document, + ), + (source) => !retainedSources.has(source), + ); + return; + } + next.items = next.items.map((item, index) => + isNode(item) + ? retainYamlConfigurationSource( + previous.items[index], + item, + retainedSources, + document, + ) + : item, + ); +}; + +/** + * Each pair's key as the string YAML resolves it to. A key written as an alias + * of a string scalar counts as that string only when no other pair in the map + * resolves to the same string; an ambiguous one stays a source-only key. + */ +const resolvedStringKeys = ( + map: YAMLMap, + document: Document, +): Map<(typeof map.items)[number], string> => { + const resolved = new Map<(typeof map.items)[number], string>(); + const aliased = new Map<(typeof map.items)[number], string>(); + for (const pair of map.items) { + if (isScalar(pair.key) && typeof pair.key.value === "string") + resolved.set(pair, pair.key.value); + else if (isAlias(pair.key)) { + const target = pair.key.resolve(document); + if (isScalar(target) && typeof target.value === "string") + aliased.set(pair, target.value); + } + } + const counts = new Map(); + for (const key of [...resolved.values(), ...aliased.values()]) + counts.set(key, (counts.get(key) ?? 0) + 1); + for (const [pair, key] of aliased) + if (counts.get(key) === 1) resolved.set(pair, key); + return resolved; +}; + +const retainYamlMapSource = ( + previous: YAMLMap, + next: YAMLMap, + retainedSources: ReadonlySet, + document: Document, +): void => { + const hasMerges = retainYamlMergeSources(previous, next, document); + const hasSourceOnlyKeys = retainProjectedYamlKeys( + previous, + next, + retainedSources, + document, + ); + const previousKeys = resolvedStringKeys(previous, document); + // The source pair each rewritten pair replaces, including aliased keys. + const matched = new Map< + (typeof next.items)[number], + (typeof previous.items)[number] + >(); + const entries = new Map( + [...previousKeys].map(([pair, key]) => [key, pair] as const), + ); + for (const pair of next.items) { + if (previous.items.includes(pair)) continue; + const key = isScalar(pair.key) ? pair.key.value : undefined; + const prior = typeof key === "string" ? entries.get(key) : undefined; + if (prior === undefined) continue; + matched.set(pair, prior); + // An aliased key keeps its spelling; a later repair materializes it if + // the edit leaves it without an anchor. + pair.key = isAlias(prior.key) + ? prior.key + : isNode(pair.key) + ? retainYamlConfigurationSource( + prior.key, + pair.key, + retainedSources, + document, + ) + : prior.key; + if (isNode(pair.value)) + pair.value = retainYamlConfigurationSource( + prior.value, + pair.value, + retainedSources, + document, + ); + } + if (hasSourceOnlyKeys || hasMerges) { + // Object property ordering moves integer-like keys ahead of their + // source neighbors. Restore source order so aliases stay after anchors. + const positions = new Map( + previous.items.map((pair, index) => [pair, index] as const), + ); + const position = (pair: (typeof next.items)[number]): number => { + const key = isScalar(pair.key) ? pair.key.value : undefined; + const original = positions.has(pair) + ? pair + : (matched.get(pair) ?? + (typeof key === "string" ? entries.get(key) : undefined)); + return original === undefined + ? Infinity + : (positions.get(original) ?? Infinity); + }; + next.items.sort((left, right) => position(left) - position(right)); + } +}; + +const isYamlMergeKey = (key: unknown): boolean => + isScalar(key) && typeof key.value === "symbol" && key.source === "<<"; + +/** Retain inheritance while materializing only explicit changes to it. */ +const retainYamlMergeSources = ( + previous: YAMLMap, + next: YAMLMap, + document: Document, +): boolean => { + const merges = previous.items.filter((pair) => isYamlMergeKey(pair.key)); + if (merges.length === 0) return false; + const inherited = new YAMLMap(document.schema); + inherited.items = merges; + const direct = new YAMLMap(document.schema); + direct.items = previous.items.filter((pair) => !isYamlMergeKey(pair.key)); + const project = (map: YAMLMap): Record => + map.toJS(document, { reviver: normalizeYamlInteger }) as Record< + string, + unknown + >; + const inheritedValues = project(inherited); + const directValues = project(direct); + const desired = project(next); + const inheritedUnchanged = new Set(); + for (const [key, value] of Object.entries(inheritedValues)) { + // Removing a key the merge supplies would resurrect it, even when the + // mapping also overrides it locally. + if (!Object.hasOwn(desired, key)) + throw new ConfigurationError( + `Cannot remove inherited YAML setting ${JSON.stringify(key)} while retaining its merge.`, + ); + if (Object.hasOwn(directValues, key)) continue; + if (isDeepStrictEqual(value, desired[key])) inheritedUnchanged.add(key); + } + next.items = [ + ...merges, + ...next.items.filter((pair) => { + const key = isScalar(pair.key) ? pair.key.value : undefined; + return typeof key !== "string" || !inheritedUnchanged.has(key); + }), + ]; + return true; +}; + +const retainProjectedYamlKeys = ( + previous: YAMLMap, + next: YAMLMap, + retainedSources: ReadonlySet, + document: Document, +): boolean => { + // A JS property can represent several distinct native YAML keys. Keep + // those source pairs together when the property was not edited. + const groups = new Map(); + const sourceOnlyKeys = new Set(); + const stringKeys = resolvedStringKeys(previous, document); + for (const pair of previous.items) { + if (isYamlMergeKey(pair.key)) continue; + let key: string; + const stringKey = stringKeys.get(pair); + if (stringKey !== undefined) key = stringKey; + else { + const singleton = new YAMLMap(document.schema); + singleton.items.push(new Pair(pair.key, null)); + const projected: unknown = singleton.toJS(document); + if (typeof projected !== "object" || projected === null) + throw new ConfigurationError( + "YAML mapping key could not be resolved safely", + ); + const [projectedKey] = Object.keys(projected); + if (projectedKey === undefined || Object.keys(projected).length !== 1) + throw new ConfigurationError( + "YAML mapping key could not be resolved safely", + ); + key = projectedKey; + sourceOnlyKeys.add(key); + } + const group = groups.get(key) ?? []; + group.push(pair); + groups.set(key, group); + } + if (sourceOnlyKeys.size > 0) { + const oldValue: unknown = previous.toJS(document, { + reviver: normalizeYamlInteger, + }); + const newValue: unknown = next.toJS(document, { + reviver: normalizeYamlInteger, + }); + if ( + typeof oldValue !== "object" || + oldValue === null || + typeof newValue !== "object" || + newValue === null + ) + throw new ConfigurationError("YAML mapping could not be compared safely"); + const oldProperties = oldValue as Record; + const newProperties = newValue as Record; + const replacements = new Map(); + for (const key of sourceOnlyKeys) { + const group = groups.get(key); + if (group === undefined) continue; + if ( + !Object.hasOwn(newProperties, key) || + !isDeepStrictEqual(oldProperties[key], newProperties[key]) + ) + throw new ConfigurationError( + `Cannot safely change YAML mapping key ${JSON.stringify(key)} because its source identity is not a plain string key.`, + ); + if ( + containsRetainedSource( + group.flatMap((pair) => [pair.key, pair.value]), + retainedSources, + ) + ) + throw new ConfigurationError( + `Cannot safely preserve YAML mapping key ${JSON.stringify(key)} while its anchor is moved.`, + ); + replacements.set(key, group); + } + next.items = next.items.flatMap((pair) => { + const key = isScalar(pair.key) ? pair.key.value : undefined; + const replacement = + typeof key === "string" ? replacements.get(key) : undefined; + return replacement ?? [pair]; + }); + } + return sourceOnlyKeys.size > 0; +}; + /** Keep a source spelling only while the selected schema resolves its value unchanged. */ const sameResolvedScalarValue = (left: unknown, right: unknown): boolean => Object.is(left, right) || diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index e60bf12000..ca94588e65 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -21,7 +21,7 @@ import { parseEvidence, rebindEvidenceToTargetPath, } from "../../domain/evidence.js"; -import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisViewRequest.js"; import { functionDossierSchema } from "../../domain/native/functionDossier.js"; import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js"; import { diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index c84b91a885..acdb51d639 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -28,11 +28,11 @@ import { } from "../../domain/evidence.js"; import { inspectAnalysisViewInputSchema, - prepareAnalysisViewProjection, - type AnalysisViewResult, type AnalysisViewRequest, type InspectAnalysisViewInput, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewRequest.js"; +import { prepareAnalysisViewProjection } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewResult } from "../../domain/analysisView/analysisViewResult.js"; import { immutableJsonSnapshotMember, isImmutableJsonSnapshot, diff --git a/src/application/analysisView/BrowserScenarioViews.test.ts b/src/application/analysisView/BrowserScenarioViews.test.ts index 82b2c3f9a3..26e2a27880 100644 --- a/src/application/analysisView/BrowserScenarioViews.test.ts +++ b/src/application/analysisView/BrowserScenarioViews.test.ts @@ -15,7 +15,7 @@ import { inspectAnalysisView } from "./AnalysisViewService.js"; import { projectAnalysisError } from "../../domain/analysisErrorProjection.js"; import { sanitizeBrowserUrl } from "../../domain/browserObservation.js"; import type { BrowserScenarioCapture } from "../../domain/browserScenarioCapture.js"; -import type { AnalysisViewRequest } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../../domain/analysisView/analysisViewRequest.js"; import { projectBrowserScenarioView } from "../../domain/analysisView/browserScenarioView.js"; const tree = (text: string) => ({ diff --git a/src/application/analysisView/JavaScriptProvenanceViews.test.ts b/src/application/analysisView/JavaScriptProvenanceViews.test.ts index 194dc28a90..ff6853e657 100644 --- a/src/application/analysisView/JavaScriptProvenanceViews.test.ts +++ b/src/application/analysisView/JavaScriptProvenanceViews.test.ts @@ -6,7 +6,7 @@ import { analysisViewJavaScriptEvidence, } from "../../../tests/fixtures/analysisView.js"; import { SOURCE_TO_BUNDLE_COMPARISON_EXAMPLE } from "../../contracts/javascript/javascriptApplicationWorkflowExamples.js"; -import { analysisViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import type { Evidence } from "../../domain/evidence.js"; import { jsonValueValidationIssue } from "../../domain/jsonValue.js"; import { compareSourceToBundle } from "../../domain/javascript/sourceToBundleComparison.js"; diff --git a/src/application/analysisView/NativeLoadImageViews.test.ts b/src/application/analysisView/NativeLoadImageViews.test.ts index 53563d78ea..cf8643147c 100644 --- a/src/application/analysisView/NativeLoadImageViews.test.ts +++ b/src/application/analysisView/NativeLoadImageViews.test.ts @@ -1,9 +1,7 @@ import { expect, it } from "vitest"; import { createEvidence } from "../../domain/evidence.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, -} from "../../domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisViewRequest.js"; +import { analysisViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { RELOCATION_SYMBOL_UNRECORDED, projectNativeLoadImageView, diff --git a/src/application/analysisView/NativeObservationViews.test.ts b/src/application/analysisView/NativeObservationViews.test.ts index 7673a47b33..c34ade9b59 100644 --- a/src/application/analysisView/NativeObservationViews.test.ts +++ b/src/application/analysisView/NativeObservationViews.test.ts @@ -13,11 +13,11 @@ import { nativeCallGraphSchema, type NativeCallGraph, } from "../../domain/native/nativeCallGraph.js"; +import type { AnalysisViewRequest } from "../../domain/analysisView/analysisViewRequest.js"; import { analysisViewResultSchema, - type AnalysisViewRequest, type AnalysisViewResult, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { inspectAnalysisView, projectRetainedAnalysis, diff --git a/src/application/artifacts/ArtifactComparisonEvidence.test.ts b/src/application/artifacts/ArtifactComparisonEvidence.test.ts index 5410cbff0f..9c0d7f16a4 100644 --- a/src/application/artifacts/ArtifactComparisonEvidence.test.ts +++ b/src/application/artifacts/ArtifactComparisonEvidence.test.ts @@ -2,10 +2,8 @@ import { expect, it } from "vitest"; import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js"; import { ARTIFACT_COMPARISON_EXAMPLE } from "../../contracts/artifactComparisonExample.js"; -import { - artifactInventorySummaryViewResultSchema, - sealAnalysisView, -} from "../../domain/analysisView/analysisView.js"; +import { sealAnalysisView } from "../../domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence, diff --git a/src/application/artifacts/ArtifactComparisonEvidence.ts b/src/application/artifacts/ArtifactComparisonEvidence.ts index 94d2b7f7c4..dd3d4c727e 100644 --- a/src/application/artifacts/ArtifactComparisonEvidence.ts +++ b/src/application/artifacts/ArtifactComparisonEvidence.ts @@ -9,7 +9,7 @@ import { inspectAnalysisViewValidated } from "../analysisView/AnalysisViewServic import type { EvidenceFileReadOptions } from "../analysisView/evidenceFileSource.js"; import { artifactComparisonInputSchema } from "../../contracts/artifactComparisonContracts.js"; import { parseAnalysisInput } from "../AnalysisInput.js"; -import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { compareArtifacts, type ArtifactComparisonResult, diff --git a/src/application/binary/BinarySession.ts b/src/application/binary/BinarySession.ts index 05199df91e..2c50ef3ec4 100644 --- a/src/application/binary/BinarySession.ts +++ b/src/application/binary/BinarySession.ts @@ -7,7 +7,10 @@ import { } from "../EvidenceBundleFiles.js"; import type { ResetInvestigationReceipt } from "../../domain/investigationLifecycle.js"; -import { retainSavedAnalysisSnapshot } from "../../domain/savedAnalysisSnapshot.js"; +import { + retainSavedAnalysisSnapshot, + type SavedAnalysisSnapshot, +} from "../../domain/savedAnalysisSnapshot.js"; import type { BinaryTarget } from "../../domain/binaryTargetTypes.js"; import { @@ -39,7 +42,6 @@ import type { BinarySessionPort, BinaryOpenResult, OpenSelectionReceipt, - SavedAnalysisSnapshot, } from "./BinarySessionPort.js"; import { binaryTargetSummary } from "./BinaryTargetSummary.js"; import { diff --git a/src/application/binary/BinarySessionPort.ts b/src/application/binary/BinarySessionPort.ts index 0cff1a77d2..b96a4ba8a9 100644 --- a/src/application/binary/BinarySessionPort.ts +++ b/src/application/binary/BinarySessionPort.ts @@ -30,7 +30,6 @@ import type { } from "../AnalysisProvider.js"; import type { SavedAnalysisSnapshot } from "../../domain/savedAnalysisSnapshot.js"; -export type { SavedAnalysisSnapshot } from "../../domain/savedAnalysisSnapshot.js"; /** Selection observed at the end of one serialized open attempt. */ export type OpenSelectionReceipt = diff --git a/src/application/desktop/DesktopBuildCapture.ts b/src/application/desktop/DesktopBuildCapture.ts index c3652f4fed..668511c6ad 100644 --- a/src/application/desktop/DesktopBuildCapture.ts +++ b/src/application/desktop/DesktopBuildCapture.ts @@ -28,7 +28,11 @@ import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { err, ok, type Result } from "../../domain/result.js"; import { AGENTENV_PROVIDER } from "../../desktop/AgentEnvComputerHelper.js"; import type { AgentEnvComputer } from "../../desktop/AgentEnvComputer.js"; -import type { ControlledComputer } from "../../desktop/BuildComputerController.js"; +import { + controller, + type Answer, + type ControlledComputer, +} from "../../desktop/BuildComputerController.js"; import { BuildComputerLongRuns } from "../../desktop/BuildComputerLongRuns.js"; import { endedCapture, @@ -37,12 +41,10 @@ import { } from "./DesktopBuildCaptureFailure.js"; import { adapterError, - controller, errorText, refused, runRea, WORK, - type Answer, } from "./DesktopBuildCommands.js"; import { exportBuild, diff --git a/src/application/desktop/DesktopBuildCaptureFailure.ts b/src/application/desktop/DesktopBuildCaptureFailure.ts index 4358aba483..e9fe9f0d10 100644 --- a/src/application/desktop/DesktopBuildCaptureFailure.ts +++ b/src/application/desktop/DesktopBuildCaptureFailure.ts @@ -7,12 +7,14 @@ import { } from "../../domain/desktop/desktopCapture.js"; import { ProviderAdapterError } from "../../domain/providerAdapterError.js"; import { AGENTENV_PROVIDER } from "../../desktop/AgentEnvComputerHelper.js"; -import type { ControlledComputer } from "../../desktop/BuildComputerController.js"; +import type { + Answer, + ControlledComputer, +} from "../../desktop/BuildComputerController.js"; import { adapterError, readOutput, reportedFailure, - type Answer, } from "./DesktopBuildCommands.js"; /** The most lines, and characters, a message quotes from the end of the app's stderr. */ diff --git a/src/application/desktop/DesktopBuildCommands.ts b/src/application/desktop/DesktopBuildCommands.ts index ac4fb4f4e2..175670b6a1 100644 --- a/src/application/desktop/DesktopBuildCommands.ts +++ b/src/application/desktop/DesktopBuildCommands.ts @@ -22,11 +22,6 @@ import { type ControlledComputer, } from "../../desktop/BuildComputerController.js"; -export { - controller, - type Answer, -} from "../../desktop/BuildComputerController.js"; - /** The work folder REA keeps under the computer user's home; every capture leaves it out. */ export const WORK = ".rea-build"; /** The checkpoint options the draft and the export read the seed and the end with. */ diff --git a/src/application/environment/environmentSource.ts b/src/application/environment/environmentSource.ts index 4c56cd6382..3299a12347 100644 --- a/src/application/environment/environmentSource.ts +++ b/src/application/environment/environmentSource.ts @@ -8,7 +8,7 @@ import { import { analysisViewResultSchema, type AnalysisViewResult, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { scenarioCaptureRecords } from "../../domain/analysisView/scenarioCaptureRecords.js"; import { readRecordedResponseBody, diff --git a/src/application/environment/environmentState.ts b/src/application/environment/environmentState.ts index af5ed7d782..cac8541a75 100644 --- a/src/application/environment/environmentState.ts +++ b/src/application/environment/environmentState.ts @@ -1,4 +1,4 @@ -import type { AnalysisViewResult } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewResult } from "../../domain/analysisView/analysisViewResult.js"; import { changesState, type StateDocument, diff --git a/src/application/native/NativeFunctionMatchingViews.test.ts b/src/application/native/NativeFunctionMatchingViews.test.ts index b80dc719ad..1f306908ea 100644 --- a/src/application/native/NativeFunctionMatchingViews.test.ts +++ b/src/application/native/NativeFunctionMatchingViews.test.ts @@ -1,7 +1,5 @@ -import { - projectAnalysisView, - analysisViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +import { projectAnalysisView } from "../../domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatching.js"; import { expect, it } from "vitest"; import { matchNativeFunctions } from "./NativeFunctionMatchingService.js"; diff --git a/src/artifacts/StableArtifactFailure.ts b/src/artifacts/StableArtifactFailure.ts index a5b9615443..5977ac8019 100644 --- a/src/artifacts/StableArtifactFailure.ts +++ b/src/artifacts/StableArtifactFailure.ts @@ -3,14 +3,43 @@ import type { AnalysisError } from "../domain/analysisErrorBase.js"; import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; import { ProviderCleanupError } from "../domain/providerCleanupError.js"; import { ArtifactReaderFailure } from "./ArtifactReader.js"; -import { bindArtifactFileCleanup } from "./ArtifactFileOperation.js"; +import { + artifactFileCancellationCause, + bindArtifactFileCleanup, + type ArtifactFilePrimaryFacts, +} from "./ArtifactFileOperation.js"; import { completedStableArtifactObservation } from "./StableArtifactObservation.js"; import { isSelectedRegularFileError } from "../filesystem/RegularFile.js"; import { AnalysisAccessDeniedError, + AnalysisArtifactChangedError, + AnalysisCancelledError, AnalysisInputError, } from "../domain/analysisErrorCore.js"; +/** Preserve ownership classification for stable-file identity and cancellation. */ +export const projectOwnedArtifactIdentityFailure = ( + facts: ArtifactFilePrimaryFacts, + operation: string, + path: string, +): AnalysisError | undefined => { + const { cause, origin } = facts; + if ( + origin !== "owned-or-classified" || + !(cause instanceof ArtifactReaderFailure) + ) + return undefined; + if (cause.reason === "integrity") + return new AnalysisArtifactChangedError(operation, path, cause.message, { + cause, + }); + if (cause.reason === "cancelled") + return new AnalysisCancelledError(operation, { + cause: artifactFileCancellationCause(cause), + }); + return undefined; +}; + /** Project filesystem errors that identify a refused or unreadable selected file. */ export const projectSelectedArtifactFileFailure = ( cause: unknown, @@ -57,6 +86,18 @@ export const stableArtifactInputRecovery = (cause: ArtifactReaderFailure) => } : {}; +/** Report a rejected artifact path or format with its source-provided recovery. */ +export const stableArtifactInputFailure = ( + cause: ArtifactReaderFailure, + operation: string, + field = "path", +): AnalysisInputError => + new AnalysisInputError( + operation, + { cause, ...stableArtifactInputRecovery(cause) }, + [{ path: [field], reason: "invalid_format", message: cause.message }], + ); + const artifactCleanupFailure = ( cause: unknown, providerId: string, diff --git a/src/artifacts/createArtifactReader.ts b/src/artifacts/createArtifactReader.ts index 3cb90b7602..739e489e8c 100644 --- a/src/artifacts/createArtifactReader.ts +++ b/src/artifacts/createArtifactReader.ts @@ -5,8 +5,6 @@ import { type ArtifactReaderConstruction, } from "./artifactFormats.js"; -export type { ArtifactReaderConstruction } from "./artifactFormats.js"; - /** Formats with an artifact entry reader; availability policy belongs to the caller. */ export type ArtifactReaderFormat = { [Id in keyof typeof ARTIFACT_FORMATS]: (typeof ARTIFACT_FORMATS)[Id] extends { diff --git a/src/browser/history/playwrightTrace/TraceFailure.ts b/src/browser/history/playwrightTrace/TraceFailure.ts index 0cd225ca5e..54ce863b7f 100644 --- a/src/browser/history/playwrightTrace/TraceFailure.ts +++ b/src/browser/history/playwrightTrace/TraceFailure.ts @@ -11,7 +11,7 @@ import { PLAYWRIGHT_TRACE_OPERATION as operation } from "../../../domain/playwri import { ArtifactReaderFailure } from "../../../artifacts/ArtifactReader.js"; import { artifactFileCancellationCause } from "../../../artifacts/ArtifactFileOperation.js"; import { isSelectedRegularFileError } from "../../../filesystem/RegularFile.js"; -import { stableArtifactInputRecovery } from "../../../artifacts/StableArtifactFailure.js"; +import { stableArtifactInputFailure } from "../../../artifacts/StableArtifactFailure.js"; /** Classify the archive owner's facts without interpreting opaque cancellation reasons. */ export const traceFailure = (cause: unknown, path: string): AnalysisError => { @@ -38,11 +38,7 @@ export const traceFailure = (cause: unknown, path: string): AnalysisError => { }, ); if (cause.reason === "format" || cause.reason === "path") - return new AnalysisInputError( - operation, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, operation); return new ArtifactOperationError( operation, cause.reason, diff --git a/src/cli/analysisEvidenceDelivery.ts b/src/cli/analysisEvidenceDelivery.ts index 5b637d8c80..9d85c6c7c8 100644 --- a/src/cli/analysisEvidenceDelivery.ts +++ b/src/cli/analysisEvidenceDelivery.ts @@ -3,7 +3,7 @@ import { analysisEvidenceSources, undeliveredSourceClosure, } from "../application/AnalysisEvidenceSources.js"; -import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js"; import { z } from "incur"; import { isImmutableEvidence, diff --git a/src/cli/javascriptApplicationAnalysis.ts b/src/cli/javascriptApplicationAnalysis.ts index d26bdbabbb..5f444855c6 100644 --- a/src/cli/javascriptApplicationAnalysis.ts +++ b/src/cli/javascriptApplicationAnalysis.ts @@ -1,4 +1,4 @@ -import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js"; import { publishCliAnalysisEvidence } from "./analysisEvidenceDelivery.js"; import { JAVASCRIPT_APPLICATION_PROVIDER } from "../domain/investigationProviders.js"; import { createProgressReporter } from "../application/ProgressReporter.js"; diff --git a/src/composition/binaryDiagnostics.ts b/src/composition/binaryDiagnostics.ts index 381789750a..8ee7221c46 100644 --- a/src/composition/binaryDiagnostics.ts +++ b/src/composition/binaryDiagnostics.ts @@ -7,7 +7,6 @@ import { RecordedCrashService } from "../application/binaryDiagnostics/RecordedC import { PwntoolsRecordedCrashProvider } from "../native/pwntools/PwntoolsRecordedCrashProvider.js"; import { BinaryLayoutService } from "../application/binaryDiagnostics/BinaryLayoutService.js"; import { PwntoolsLayoutProvider } from "../native/pwntools/PwntoolsLayoutProvider.js"; -import { pwntoolsHostSupported } from "../native/pwntools/PwntoolsDecoder.js"; import { PeResourcesService } from "../application/binaryDiagnostics/PeResourcesService.js"; import { PeResourcesProvider } from "../native/pe/PeResourcesProvider.js"; import { PeCertificatesService } from "../application/binaryDiagnostics/PeCertificatesService.js"; @@ -37,8 +36,6 @@ import { DevicetreeProvider } from "../firmware/devicetree/DevicetreeProvider.js export const createDevicetreeService = (): DevicetreeService => new DevicetreeService(new DevicetreeProvider()); -export { pwntoolsHostSupported }; - /** Portable PE inspection with no engine, installation or target acquisition. */ export const createPeResourcesService = (): PeResourcesService => new PeResourcesService(new PeResourcesProvider()); diff --git a/src/composition/capabilityServices.ts b/src/composition/capabilityServices.ts index ae5e5cd80d..205f281f16 100644 --- a/src/composition/capabilityServices.ts +++ b/src/composition/capabilityServices.ts @@ -12,18 +12,17 @@ import { } from "./capabilityDeclarations.js"; import { isAbsolute } from "node:path"; import { createGpuCodeObjectsService } from "./gpu.js"; -import { createPacketCaptureService, tsharkHostSupported } from "./captures.js"; -import { - createSignalRecordingService, - sigrokHostSupported, -} from "./signals.js"; +import { createPacketCaptureService } from "./captures.js"; +import { tsharkHostSupported } from "../captures/TsharkCaptureProvider.js"; +import { createSignalRecordingService } from "./signals.js"; +import { sigrokHostSupported } from "../signals/SigrokRecordingProvider.js"; import { wasmArtifactModule } from "./wasm.js"; import { apkSignatureModule } from "./apkSignature.js"; +import { evmInterfaceModule } from "./evm.js"; import { - evmInterfaceModule, evmHostSupported, evmLimiterAvailability, -} from "./evm.js"; +} from "../evm/EvmoleInterfaceProvider.js"; import { createSqliteDatabaseService } from "./sqlite.js"; import { createDesktopApplicationService } from "./desktop.js"; import { DesktopBuildComputers } from "../application/desktop/DesktopBuildComputers.js"; @@ -40,10 +39,8 @@ import { createWebNetworkCaptureService } from "./webNetworkCaptures.js"; import { createWebSourceLocationService } from "./webSourceLocations.js"; import { createWebModuleTraceService } from "./webModules.js"; import { createWebRuntimeService } from "./webRuntime.js"; -import { - createJavaScriptRecoveryProvider, - wakaruHostSupported, -} from "./javascriptRecovery.js"; +import { createJavaScriptRecoveryProvider } from "./javascriptRecovery.js"; +import { wakaruHostSupported } from "../javascript/recovery/WakaruCommand.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { createReverseEngineeringService } from "./reverseEngineering.js"; import type { @@ -51,10 +48,8 @@ import type { CommandProviderLaunchFailures, } from "../application/reverse/ReverseEngineeringService.js"; import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js"; -import { - createFirmwareAnalysisProvider, - firmwareHostSupported, -} from "./firmware.js"; +import { createFirmwareAnalysisProvider } from "./firmware.js"; +import { firmwareHostSupported } from "../firmware/FirmwareCommand.js"; import { createAndroidAnalysisProvider } from "./android.js"; import { createPcapAnalysisProvider } from "./pcap.js"; import { createFlutterBuildAnalysisProvider } from "./flutter.js"; @@ -79,7 +74,6 @@ import { createWindowsShortcutService, createRecordedCrashService, createBinaryLayoutService, - pwntoolsHostSupported, createDevicetreeService, createElfNotesService, createNativeInitializersService, @@ -88,6 +82,7 @@ import { createKernelModuleService, createKernelBuildConfigService, } from "./binaryDiagnostics.js"; +import { pwntoolsHostSupported } from "../native/pwntools/PwntoolsDecoder.js"; import type { PacketCaptureService } from "../application/captures/PacketCaptureService.js"; import type { SignalRecordingService } from "../application/signals/SignalRecordingService.js"; diff --git a/src/composition/captures.ts b/src/composition/captures.ts index ab033cfa3d..b8f41a0ef5 100644 --- a/src/composition/captures.ts +++ b/src/composition/captures.ts @@ -1,10 +1,5 @@ import { PacketCaptureService } from "../application/captures/PacketCaptureService.js"; -import { - TsharkCaptureProvider, - tsharkHostSupported, -} from "../captures/TsharkCaptureProvider.js"; - -export { tsharkHostSupported }; +import { TsharkCaptureProvider } from "../captures/TsharkCaptureProvider.js"; export const createPacketCaptureService = ( environment: Readonly = process.env, diff --git a/src/composition/evm.ts b/src/composition/evm.ts index 853cd73b02..f51903a0f4 100644 --- a/src/composition/evm.ts +++ b/src/composition/evm.ts @@ -1,9 +1,5 @@ import { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js"; -import { - EvmoleInterfaceProvider, - evmHostSupported, - evmLimiterAvailability, -} from "../evm/EvmoleInterfaceProvider.js"; +import { EvmoleInterfaceProvider } from "../evm/EvmoleInterfaceProvider.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; import { INSPECT_EVM_INTERFACE_CONTRACT } from "../contracts/evm/evmToolContracts.js"; @@ -13,8 +9,6 @@ export const createEvmInterfaceService = ( ): EvmInterfaceService => new EvmInterfaceService(new EvmoleInterfaceProvider(environment)); -export { evmHostSupported, evmLimiterAvailability }; - /** * Static binding of the existing operation, command and fresh service factory. * The host owns injected service identity and its retryable cleanup lifetime. diff --git a/src/composition/firmware.ts b/src/composition/firmware.ts index 78e16d9176..f3bed02938 100644 --- a/src/composition/firmware.ts +++ b/src/composition/firmware.ts @@ -1,12 +1,7 @@ import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js"; -import { - firmwareHostSupported, - type FirmwareLauncher, -} from "../firmware/FirmwareCommand.js"; +import type { FirmwareLauncher } from "../firmware/FirmwareCommand.js"; import { FirmwareProvider } from "../firmware/FirmwareProvider.js"; -export { firmwareHostSupported }; - /** Construct one firmware provider without launching or acquiring an engine. */ export const createFirmwareAnalysisProvider = ( environment: Readonly> = process.env, diff --git a/src/composition/javascriptRecovery.ts b/src/composition/javascriptRecovery.ts index f57c984d6f..f3f2ae49d4 100644 --- a/src/composition/javascriptRecovery.ts +++ b/src/composition/javascriptRecovery.ts @@ -1,11 +1,6 @@ import type { JavaScriptRecoveryPort } from "../application/javascript/JavaScriptRecoveryPort.js"; import { WakaruProvider } from "../javascript/recovery/WakaruProvider.js"; -import { - wakaruHostSupported, - type WakaruLauncher, -} from "../javascript/recovery/WakaruCommand.js"; - -export { wakaruHostSupported }; +import type { WakaruLauncher } from "../javascript/recovery/WakaruCommand.js"; /** Construct optional recovery without acquiring tools, processes or workspaces. */ export const createJavaScriptRecoveryProvider = ( diff --git a/src/composition/signals.ts b/src/composition/signals.ts index d21395389f..0bcfbfdb9a 100644 --- a/src/composition/signals.ts +++ b/src/composition/signals.ts @@ -1,10 +1,5 @@ import { SignalRecordingService } from "../application/signals/SignalRecordingService.js"; -import { - SigrokRecordingProvider, - sigrokHostSupported, -} from "../signals/SigrokRecordingProvider.js"; - -export { sigrokHostSupported }; +import { SigrokRecordingProvider } from "../signals/SigrokRecordingProvider.js"; export const createSignalRecordingService = ( environment: Readonly = process.env, diff --git a/src/config/bridgeMaxRequestBytes.test.ts b/src/config/bridgeMaxRequestBytes.test.ts index be87473a68..c0ef5a8e39 100644 --- a/src/config/bridgeMaxRequestBytes.test.ts +++ b/src/config/bridgeMaxRequestBytes.test.ts @@ -12,11 +12,13 @@ import { runWithAnalysisOperationSnapshot } from "../domain/analysisOperationSna import { BRIDGE_MAX_REQUEST_BYTES_SETTING, DEFAULT_BRIDGE_MAX_REQUEST, + parseBridgeMaxRequestBytes, +} from "./bridgeMaxRequestBytes.js"; +import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES, MAXIMUM_BRIDGE_MAX_REQUEST_BYTES, MINIMUM_BRIDGE_MAX_REQUEST_BYTES, - parseBridgeMaxRequestBytes, -} from "./bridgeMaxRequestBytes.js"; +} from "../domain/analysisResourceLimits.js"; const configured = (maximumBytes: number) => ({ maximumBytes, source: "setting" }) as const; diff --git a/src/config/bridgeMaxRequestBytes.ts b/src/config/bridgeMaxRequestBytes.ts index 91c089eeb8..c3a47dcadc 100644 --- a/src/config/bridgeMaxRequestBytes.ts +++ b/src/config/bridgeMaxRequestBytes.ts @@ -13,12 +13,6 @@ import { export const BRIDGE_MAX_REQUEST_BYTES_SETTING = "REA_BRIDGE_MAX_REQUEST_BYTES"; -export { - DEFAULT_BRIDGE_MAX_REQUEST_BYTES, - MAXIMUM_BRIDGE_MAX_REQUEST_BYTES, - MINIMUM_BRIDGE_MAX_REQUEST_BYTES, -}; - /** * Smallest ceiling that still admits the largest bounded bridge request. * Hopper symbol names stop at 1024 UTF-16 units. A name of U+0001 repeated diff --git a/src/contracts/analysisDetail.test.ts b/src/contracts/analysisDetail.test.ts index fdca656e78..674d1bdc9d 100644 --- a/src/contracts/analysisDetail.test.ts +++ b/src/contracts/analysisDetail.test.ts @@ -2,11 +2,11 @@ import { expect, it } from "vitest"; import { z } from "zod"; import { Ajv2020 } from "ajv/dist/2020.js"; +import { projectAnalysisView } from "../domain/analysisView/analysisView.js"; import { binaryLayoutSummaryViewResultSchema, javascriptApplicationSummaryViewResultSchema, - projectAnalysisView, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { analysisViewRequestSchema } from "../domain/analysisView/analysisViewRequest.js"; import { analysisViewCaptureEvidence, diff --git a/src/contracts/analysisViewToolContracts.ts b/src/contracts/analysisViewToolContracts.ts index 8cd2fd648d..71b54fc43c 100644 --- a/src/contracts/analysisViewToolContracts.ts +++ b/src/contracts/analysisViewToolContracts.ts @@ -1,9 +1,7 @@ import { API_SURFACE_EXAMPLE } from "./analysisViewApiSurfaceExample.js"; import { ENVIRONMENT_MANIFEST_EXAMPLE } from "./analysisViewEnvironmentManifestExample.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, -} from "../domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisViewRequest.js"; +import { analysisViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "./toolContractTypes.js"; import { toolContractMetadata } from "./toolEffects.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/android/androidToolContracts.ts b/src/contracts/android/androidToolContracts.ts index 057ee6f7a4..3be5889ac3 100644 --- a/src/contracts/android/androidToolContracts.ts +++ b/src/contracts/android/androidToolContracts.ts @@ -15,7 +15,7 @@ import { androidMethodSummaryViewResultSchema, androidJniSummaryViewResultSchema, androidJniPageViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "../toolEffects.js"; diff --git a/src/contracts/apiContractToolContracts.ts b/src/contracts/apiContractToolContracts.ts index 9abf548273..2f8321e204 100644 --- a/src/contracts/apiContractToolContracts.ts +++ b/src/contracts/apiContractToolContracts.ts @@ -6,7 +6,7 @@ import { apiContractSummaryViewResultSchema, apiContractPageViewResultSchema, graphqlContractPageViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "./toolEffects.js"; import type { ToolContract } from "./toolContractTypes.js"; diff --git a/src/contracts/apkSignature/apkSignatureToolContracts.ts b/src/contracts/apkSignature/apkSignatureToolContracts.ts index ce792ee9d1..4856b58f3c 100644 --- a/src/contracts/apkSignature/apkSignatureToolContracts.ts +++ b/src/contracts/apkSignature/apkSignatureToolContracts.ts @@ -4,7 +4,7 @@ import { inspectApkSignatureInputSchema } from "../../domain/apkSignature/apkSig import { apkSignaturePageViewResultSchema, apkSignatureSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/applicationToolContracts.ts b/src/contracts/applicationToolContracts.ts index bbcb1cd1a0..2e26a4a0bb 100644 --- a/src/contracts/applicationToolContracts.ts +++ b/src/contracts/applicationToolContracts.ts @@ -10,7 +10,7 @@ import { sourceToBundlePageViewResultSchema, endpointComparisonSummaryViewResultSchema, endpointComparisonPageViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { staticObservedEndpointComparisonSchema } from "../domain/javascript/staticObservedEndpointSchemas.js"; import { STATIC_OBSERVED_ENDPOINT_COMPARISON_EXAMPLE } from "./javascript/staticObservedEndpointExample.js"; import { applicationVersionComparisonResultSchema } from "../domain/javascript/javascriptApplicationVersionComparisonSchemas.js"; diff --git a/src/contracts/browserScenarioToolContracts.ts b/src/contracts/browserScenarioToolContracts.ts index b11dea37fa..3f04d935a5 100644 --- a/src/contracts/browserScenarioToolContracts.ts +++ b/src/contracts/browserScenarioToolContracts.ts @@ -4,7 +4,7 @@ import { analysisDetailOutputSchemaOf, analysisDetailSchema, } from "./analysisDetail.js"; -import { browserScenarioSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { browserScenarioSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import { browserScenarioInputSchema } from "../domain/browserScenario.js"; import { browserScenarioCaptureSchema } from "../domain/browserScenarioCapture.js"; import type { JsonValue } from "../domain/jsonValue.js"; diff --git a/src/contracts/browserToolContracts.ts b/src/contracts/browserToolContracts.ts index 5c12c273e8..4152cd6c01 100644 --- a/src/contracts/browserToolContracts.ts +++ b/src/contracts/browserToolContracts.ts @@ -7,7 +7,7 @@ import { javaScriptRoutesViewResultSchema, webBundleSummaryViewResultSchema, webBundlePageViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "./toolContractTypes.js"; import { inspectWebPageInputSchema, diff --git a/src/contracts/desktopApplicationToolContracts.ts b/src/contracts/desktopApplicationToolContracts.ts index a544d9dd97..f910d93a60 100644 --- a/src/contracts/desktopApplicationToolContracts.ts +++ b/src/contracts/desktopApplicationToolContracts.ts @@ -3,7 +3,7 @@ import { desktopApplicationSchema, inspectDesktopApplicationInputSchema, } from "../domain/desktopApplication.js"; -import { desktopApplicationSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { desktopApplicationSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "./toolEffects.js"; import type { ToolContract } from "./toolContractTypes.js"; diff --git a/src/contracts/erlang/beamToolContracts.ts b/src/contracts/erlang/beamToolContracts.ts index d6803b8a6e..e96778ba93 100644 --- a/src/contracts/erlang/beamToolContracts.ts +++ b/src/contracts/erlang/beamToolContracts.ts @@ -9,7 +9,7 @@ import { z } from "zod"; import { erlangBeamSummaryViewResultSchema, erlangBeamTableViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { erlangBeamCompleteDeliveryShape, erlangBeamSummaryDeliveryShape, diff --git a/src/contracts/evm/evmToolContracts.ts b/src/contracts/evm/evmToolContracts.ts index 4c33d3f691..59f2d59112 100644 --- a/src/contracts/evm/evmToolContracts.ts +++ b/src/contracts/evm/evmToolContracts.ts @@ -8,7 +8,7 @@ import { analysisDetailSchema, analysisDetailOutputSchemaOf, } from "../analysisDetail.js"; -import { evmInterfaceSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { evmInterfaceSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; /** Delivery selection leaves the existing provider input and original Evidence unchanged. */ const evmInterfaceInspectionInputSchema = inspectEvmInterfaceInputSchema.extend( diff --git a/src/contracts/flutter/flutterToolContracts.ts b/src/contracts/flutter/flutterToolContracts.ts index 6f3b6efee4..29426653ed 100644 --- a/src/contracts/flutter/flutterToolContracts.ts +++ b/src/contracts/flutter/flutterToolContracts.ts @@ -4,7 +4,7 @@ import { dartAotInspectionInputSchema, flutterResultSchemas, } from "../../domain/flutter/flutterBuildAnalysis.js"; -import { dartAotSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { dartAotSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "../toolEffects.js"; diff --git a/src/contracts/fridaToolContracts.ts b/src/contracts/fridaToolContracts.ts index daffd7e656..dec537e2a6 100644 --- a/src/contracts/fridaToolContracts.ts +++ b/src/contracts/fridaToolContracts.ts @@ -15,7 +15,7 @@ import { fridaSessionStatusInputSchema, fridaSessionStatusSchema, } from "../domain/fridaSessionStatus.js"; -import { fridaSessionStatusSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { fridaSessionStatusSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import { fridaDeviceListSchema, fridaProcessListSchema, diff --git a/src/contracts/gdbToolContracts.ts b/src/contracts/gdbToolContracts.ts index ee15fc43ec..b419e2579c 100644 --- a/src/contracts/gdbToolContracts.ts +++ b/src/contracts/gdbToolContracts.ts @@ -5,7 +5,7 @@ import { commandEvidenceDetailSchema, gdbCommandResultSchema, } from "../domain/commandObservation.js"; -import { gdbCommandSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { gdbCommandSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "./toolContractTypes.js"; import { analysisDetailOutputSchemaOf } from "./analysisDetail.js"; import { toolContractMetadata } from "./toolEffects.js"; diff --git a/src/contracts/go/goToolContracts.ts b/src/contracts/go/goToolContracts.ts index 6d9ee52aed..d4b33fd430 100644 --- a/src/contracts/go/goToolContracts.ts +++ b/src/contracts/go/goToolContracts.ts @@ -5,7 +5,7 @@ import { import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { analysisDetailOutputSchemaOf } from "../analysisDetail.js"; -import { goBinarySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { goBinarySummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; /** Static Go metadata is independent of active native-analysis providers and host toolchains. */ export const GO_TOOL_CONTRACTS = [ diff --git a/src/contracts/gpu/gpuToolContracts.ts b/src/contracts/gpu/gpuToolContracts.ts index a924b03f21..77bf42f2b2 100644 --- a/src/contracts/gpu/gpuToolContracts.ts +++ b/src/contracts/gpu/gpuToolContracts.ts @@ -6,7 +6,7 @@ import { import { gpuCodeObjectsSummaryViewResultSchema, gpuCodeObjectsPageViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/il2cpp/il2cppToolContracts.ts b/src/contracts/il2cpp/il2cppToolContracts.ts index c25c7e31ed..66d26eb57c 100644 --- a/src/contracts/il2cpp/il2cppToolContracts.ts +++ b/src/contracts/il2cpp/il2cppToolContracts.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { il2cppMetadataSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { il2cppMetadataSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; import { il2cppMetadataSchema, inspectIl2cppMetadataInputSchema, diff --git a/src/contracts/javascript/electronToolContracts.ts b/src/contracts/javascript/electronToolContracts.ts index 32970aee36..2bd145aeeb 100644 --- a/src/contracts/javascript/electronToolContracts.ts +++ b/src/contracts/javascript/electronToolContracts.ts @@ -22,7 +22,7 @@ import { import { javaScriptRoutesViewResultSchema, javascriptApplicationSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { analysisDetailOutputSchemaOf } from "../analysisDetail.js"; import { analyzeJavaScriptApplicationInputSchema, diff --git a/src/contracts/model/modelToolContracts.ts b/src/contracts/model/modelToolContracts.ts index c61d5dfb3d..2908d900a4 100644 --- a/src/contracts/model/modelToolContracts.ts +++ b/src/contracts/model/modelToolContracts.ts @@ -4,7 +4,7 @@ import { modelArtifactSchema } from "../../domain/model/modelArtifact.js"; import { modelArtifactPageViewResultSchema, modelArtifactSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/native/binaryDiagnosticsToolContracts.ts b/src/contracts/native/binaryDiagnosticsToolContracts.ts index 4e90729e6a..6b0d98ef2d 100644 --- a/src/contracts/native/binaryDiagnosticsToolContracts.ts +++ b/src/contracts/native/binaryDiagnosticsToolContracts.ts @@ -6,7 +6,7 @@ import { devicetreeSummaryViewResultSchema, binaryLayoutSummaryViewResultSchema, nativeExportsSummaryViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { inspectPeLoadConfigInputSchema, peLoadConfigSchema, diff --git a/src/contracts/pcap/pcapToolContracts.ts b/src/contracts/pcap/pcapToolContracts.ts index 27fb02ed87..3940c1f252 100644 --- a/src/contracts/pcap/pcapToolContracts.ts +++ b/src/contracts/pcap/pcapToolContracts.ts @@ -9,7 +9,7 @@ import { analysisDetailSchema, analysisDetailOutputSchemaOf, } from "../analysisDetail.js"; -import { pcapCaptureSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { pcapCaptureSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; /** Packet-capture analysis served by caller-supplied tshark and capinfos. */ export const PCAP_TOOL_CONTRACTS = [ diff --git a/src/contracts/playwrightTraceToolContracts.ts b/src/contracts/playwrightTraceToolContracts.ts index 482def5ad0..b0275fe78a 100644 --- a/src/contracts/playwrightTraceToolContracts.ts +++ b/src/contracts/playwrightTraceToolContracts.ts @@ -4,7 +4,7 @@ import { inspectPlaywrightTraceInputSchema } from "../domain/playwrightTrace/pla import { playwrightTraceSummaryViewResultSchema, playwrightTracePageViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js"; import { toolContractMetadata } from "./toolEffects.js"; import type { ToolContract } from "./toolContractTypes.js"; diff --git a/src/contracts/python/pythonPycToolContracts.ts b/src/contracts/python/pythonPycToolContracts.ts index 64d550f3e7..3056ccb70d 100644 --- a/src/contracts/python/pythonPycToolContracts.ts +++ b/src/contracts/python/pythonPycToolContracts.ts @@ -9,7 +9,7 @@ import { z } from "zod"; import { pythonPycSummaryViewResultSchema, pythonPycCodeObjectsViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import { pythonPycCompleteDeliveryShape, pythonPycSummaryDeliveryShape, diff --git a/src/contracts/reverseEngineeringToolContracts.ts b/src/contracts/reverseEngineeringToolContracts.ts index 73394aca40..9705c37dfa 100644 --- a/src/contracts/reverseEngineeringToolContracts.ts +++ b/src/contracts/reverseEngineeringToolContracts.ts @@ -1,5 +1,5 @@ import { commandObservationSchema } from "../domain/commandObservation.js"; -import { objdumpSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { objdumpSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; import { objdumpInputSchema, diff --git a/src/contracts/rust/rustToolContracts.ts b/src/contracts/rust/rustToolContracts.ts index 181f16dfd6..6358c5e00e 100644 --- a/src/contracts/rust/rustToolContracts.ts +++ b/src/contracts/rust/rustToolContracts.ts @@ -4,7 +4,7 @@ import { z } from "zod"; import { rustBinarySummaryViewResultSchema, rustBinaryPageViewResultSchema, -} from "../../domain/analysisView/analysisView.js"; +} from "../../domain/analysisView/analysisViewResult.js"; import type { ToolContract } from "../toolContractTypes.js"; import { toolContractMetadata } from "../toolEffects.js"; import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js"; diff --git a/src/contracts/toolOutputSchemaGroups.ts b/src/contracts/toolOutputSchemaGroups.ts index 8100e805ae..5560f9e4b7 100644 --- a/src/contracts/toolOutputSchemaGroups.ts +++ b/src/contracts/toolOutputSchemaGroups.ts @@ -20,7 +20,7 @@ import { machoSummaryViewResultSchema, managedArtifactSummaryViewResultSchema, managedMemberSummaryViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { literalFeatureTraceSchema } from "../domain/native/literalFeatureTrace.js"; import { byteEntropySchema } from "../domain/native/byteEntropy.js"; import { nativeFunctionMatchingSchema } from "../domain/native/functionMatching.js"; diff --git a/src/contracts/wasm/wasmToolContracts.ts b/src/contracts/wasm/wasmToolContracts.ts index 3d72649ece..178b5e66e7 100644 --- a/src/contracts/wasm/wasmToolContracts.ts +++ b/src/contracts/wasm/wasmToolContracts.ts @@ -9,7 +9,7 @@ import { analysisDetailSchema, analysisDetailOutputSchemaOf, } from "../analysisDetail.js"; -import { wasmArtifactSummaryViewResultSchema } from "../../domain/analysisView/analysisView.js"; +import { wasmArtifactSummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js"; /** Delivery selection does not change the provider input or original Evidence. */ export const wasmArtifactInspectionInputSchema = diff --git a/src/contracts/webNetworkCaptureToolContracts.ts b/src/contracts/webNetworkCaptureToolContracts.ts index 4104a3a3c3..0a5ada90bd 100644 --- a/src/contracts/webNetworkCaptureToolContracts.ts +++ b/src/contracts/webNetworkCaptureToolContracts.ts @@ -8,7 +8,7 @@ import { analysisDetailSchema, analysisDetailOutputSchemaOf, } from "./analysisDetail.js"; -import { webNetworkCaptureSummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; +import { webNetworkCaptureSummaryViewResultSchema } from "../domain/analysisView/analysisViewResult.js"; /** Historical inspection is distinct from live browser capture and runtime authority. */ export const WEB_NETWORK_CAPTURE_TOOL_CONTRACTS = [ diff --git a/src/contracts/webScriptToolContracts.ts b/src/contracts/webScriptToolContracts.ts index dc53259a55..fbe318f8fa 100644 --- a/src/contracts/webScriptToolContracts.ts +++ b/src/contracts/webScriptToolContracts.ts @@ -8,7 +8,7 @@ import { webSourceLocationSummaryViewResultSchema, webSourceLocationMatchesViewResultSchema, webSourceLocationTextViewResultSchema, -} from "../domain/analysisView/analysisView.js"; +} from "../domain/analysisView/analysisViewResult.js"; import { exportWebScriptsInputSchema, webScriptExportResultSchema, diff --git a/src/cutter/CutterBridgeClient.ts b/src/cutter/CutterBridgeClient.ts index cef5949892..d26c714a03 100644 --- a/src/cutter/CutterBridgeClient.ts +++ b/src/cutter/CutterBridgeClient.ts @@ -50,10 +50,10 @@ import { BRIDGE_MAX_REQUEST_BYTES_SETTING, bridgeRequestLimitMismatchError, bridgeRequestOversizeError, - DEFAULT_BRIDGE_MAX_REQUEST_BYTES, parseBridgeMaxRequestBytes, type BridgeMaxRequestBytes, } from "../config/bridgeMaxRequestBytes.js"; +import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../domain/analysisResourceLimits.js"; import { ConfigurationError } from "../domain/configurationErrors.js"; import { currentAnalysisOperationSnapshot } from "../domain/analysisOperationSnapshot.js"; import type { AnalysisSettingsReader } from "../domain/analysisSettings.js"; diff --git a/src/desktop/BuildComputerEnvironment.ts b/src/desktop/BuildComputerEnvironment.ts index 84ed1c541d..270671a436 100644 --- a/src/desktop/BuildComputerEnvironment.ts +++ b/src/desktop/BuildComputerEnvironment.ts @@ -8,9 +8,6 @@ import { PRODUCT_IDENTITY } from "../identity.js"; import { BUILD_COMPUTER_SETTINGS } from "../domain/desktop/buildComputerSettings.js"; import { resolveProgram } from "./ProgramLookup.js"; -/** The settings a build computer reads where the REA server starts. */ -export { BUILD_COMPUTER_SETTINGS }; - /** One of the build computer's settings. */ export type BuildComputerSetting = (typeof BUILD_COMPUTER_SETTINGS)[number]; diff --git a/src/desktop/DesktopBuildDoctorFacts.ts b/src/desktop/DesktopBuildDoctorFacts.ts index 3a2f4f9a55..23daea238d 100644 --- a/src/desktop/DesktopBuildDoctorFacts.ts +++ b/src/desktop/DesktopBuildDoctorFacts.ts @@ -21,12 +21,12 @@ import { execFileOutputFailure } from "../process/ExecFileOutput.js"; import { agentEnvInterpreter } from "./AgentEnvComputerHelper.js"; import { agentEnvReportedVersion, - BUILD_COMPUTER_SETTINGS, defaultBuildComputerArchive, programOnPath, resolveBuildComputerProgram, type BuildComputerSetting, } from "./BuildComputerEnvironment.js"; +import { BUILD_COMPUTER_SETTINGS } from "../domain/desktop/buildComputerSettings.js"; const DEFAULT_DOCKER_ROOT = "/var/lib/docker"; const CONFIG_BYTES = 1024 * 1024; diff --git a/src/desktop/DesktopBuildSetupHost.ts b/src/desktop/DesktopBuildSetupHost.ts index 94c6de0782..7e69512eff 100644 --- a/src/desktop/DesktopBuildSetupHost.ts +++ b/src/desktop/DesktopBuildSetupHost.ts @@ -23,11 +23,11 @@ import { } from "../process/ExecFileOutput.js"; import { agentEnvReportedVersion, - BUILD_COMPUTER_SETTINGS, buildComputerArchiveDirectory, defaultBuildComputerArchive, programOnPath, } from "./BuildComputerEnvironment.js"; +import { BUILD_COMPUTER_SETTINGS } from "../domain/desktop/buildComputerSettings.js"; import { desktopBuildOwnedCommandReason, runDesktopBuildOwnedCommand, diff --git a/src/domain/analysisView/analysisView.test.ts b/src/domain/analysisView/analysisView.test.ts index 9a87346563..69f1bf0dc3 100644 --- a/src/domain/analysisView/analysisView.test.ts +++ b/src/domain/analysisView/analysisView.test.ts @@ -27,12 +27,10 @@ import { } from "../javascript/javascriptSemanticGraph.js"; import { sourceMapReference } from "../javascript/javascriptApplicationGraph.fixture.js"; import { createArtifactInspection } from "../artifactInspection.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { inspectAnalysisViewInputSchema } from "./analysisViewRequest.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { analysisViewResultSchema } from "./analysisViewResult.js"; const layoutParent = (): AnalysisViewParent => { const evidence = analysisViewLayoutEvidence(); diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index ecc0634134..a37adea080 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -100,91 +100,8 @@ import { projectIl2cppMetadataView } from "./il2cppMetadataView.js"; import type { AnalysisViewParent } from "./analysisViewParent.js"; import type { GroupingViewKind } from "./analysisViewShape.js"; +import type { AnalysisViewRequest } from "./analysisViewRequest.js"; import { - inspectAnalysisViewInputSchema, - type AnalysisViewRequest, - type InspectAnalysisViewInput, -} from "./analysisViewRequest.js"; -import { - analysisViewResultSchema, - type AnalysisViewResult, - type UnsignedAnalysisView, -} from "./analysisViewResult.js"; -export { - inspectAnalysisViewInputSchema, - type AnalysisViewRequest, - type InspectAnalysisViewInput, - type AnalysisViewParent, -}; -export { - webSourceLocationSummaryViewResultSchema, - webSourceLocationMatchesViewResultSchema, - webSourceLocationTextViewResultSchema, - procedurePseudocodeSummaryViewResultSchema, - nativeObservationSummaryViewResultSchemas, - nativeFeatureTraceSummaryViewResultSchema, - byteEntropySummaryViewResultSchema, - nativeExportsSummaryViewResultSchema, - devicetreeSummaryViewResultSchema, - erlangBeamSummaryViewResultSchema, - erlangBeamTableViewResultSchema, - pythonPycSummaryViewResultSchema, - pythonPycCodeObjectsViewResultSchema, - gdbCommandSummaryViewResultSchema, - objdumpSummaryViewResultSchema, - fridaSessionStatusSummaryViewResultSchema, - goBinarySummaryViewResultSchema, - rustBinarySummaryViewResultSchema, - rustBinaryPageViewResultSchema, - binaryLayoutSummaryViewResultSchema, - javascriptApplicationSummaryViewResultSchema, - artifactInventorySummaryViewResultSchema, - desktopApplicationSummaryViewResultSchema, - wasmArtifactSummaryViewResultSchema, - machoSummaryViewResultSchema, - interfaceBuilderSummaryViewResultSchema, - evmInterfaceSummaryViewResultSchema, - managedArtifactSummaryViewResultSchema, - managedMemberSummaryViewResultSchema, - webNetworkCaptureSummaryViewResultSchema, - browserScenarioSummaryViewResultSchema, - androidPackageSummaryViewResultSchema, - androidJniSummaryViewResultSchema, - androidJniPageViewResultSchema, - androidClassSummaryViewResultSchema, - androidMethodSummaryViewResultSchema, - androidManifestViewResultSchema, - androidComponentsViewResultSchema, - androidComponentSummaryViewResultSchema, - pcapCaptureSummaryViewResultSchema, - dartAotSummaryViewResultSchema, - javaScriptRoutesViewResultSchema, - webBundleSummaryViewResultSchema, - webBundlePageViewResultSchema, - gpuCodeObjectsSummaryViewResultSchema, - gpuCodeObjectsPageViewResultSchema, - apkSignatureSummaryViewResultSchema, - apkSignaturePageViewResultSchema, - modelArtifactSummaryViewResultSchema, - modelArtifactPageViewResultSchema, - il2cppMetadataSummaryViewResultSchema, - nativeInventorySummaryViewResultSchema, - nativeProcedureReferencesSummaryViewResultSchema, - nativeLoadImageSummaryViewResultSchema, - nativeFunctionMatchingSummaryViewResultSchema, - nativeFunctionSummaryViewResultSchema, - sourceToBundleSummaryViewResultSchema, - sourceToBundlePageViewResultSchema, - endpointComparisonSummaryViewResultSchema, - apiContractSummaryViewResultSchema, - playwrightTraceSummaryViewResultSchema, - playwrightTracePageViewResultSchema, - apiContractPageViewResultSchema, - graphqlContractPageViewResultSchema, - endpointComparisonPageViewResultSchema, - featureTraceSummaryViewResultSchema, - batchDecompileSummaryViewResultSchema, - featureTraceViewResultSchema, analysisViewResultSchema, type AnalysisViewResult, type UnsignedAnalysisView, diff --git a/src/domain/analysisView/analysisViewParent.test.ts b/src/domain/analysisView/analysisViewParent.test.ts index b0d037f225..8eefab63a8 100644 --- a/src/domain/analysisView/analysisViewParent.test.ts +++ b/src/domain/analysisView/analysisViewParent.test.ts @@ -4,7 +4,7 @@ import { freezeOwnedJsonTreeSnapshot, isImmutableJsonSnapshot, } from "../immutableJson.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { createAnalysisViewResultReader } from "./analysisViewParent.js"; const parent = (normalizedResult: unknown): AnalysisViewParent => ({ diff --git a/src/domain/analysisView/analysisViewRequest.ts b/src/domain/analysisView/analysisViewRequest.ts index a041f33f45..ce0ecb9a94 100644 --- a/src/domain/analysisView/analysisViewRequest.ts +++ b/src/domain/analysisView/analysisViewRequest.ts @@ -40,7 +40,7 @@ import { javascriptModuleReferencesRequestSchema } from "./javascriptModuleRefer import { javascriptFactsViewRequestSchema } from "./javascriptFactsView.js"; import { evidenceSchema } from "../evidence.js"; import { declareEvidenceReferenceAlternative } from "../evidenceInputContract.js"; -import { apiSurfaceViewSchema } from "./webNetworkApiSurfaceView.js"; +import { apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; import { evmViewRequestSchema } from "./evmInterfaceView.js"; import { artifactInventoryViewRequestSchema } from "./artifactInventoryView.js"; import { managedViewRequestSchema } from "./managedArtifactView.js"; @@ -54,7 +54,7 @@ import { environmentStateViewRequestSchema } from "./environmentStateView.js"; import { environmentCoverageViewRequestSchema } from "./environmentCoverageView.js"; import { pcapViewRequestSchema } from "./pcapCaptureView.js"; import { qlogViewRequestSchema } from "./qlogCaptureView.js"; -import { androidViewRequestSchema } from "./androidPackageView.js"; +import { androidViewRequestSchema } from "../android/androidPackageQuery.js"; import { androidJniViewRequestSchema } from "../android/androidJniQuery.js"; import { dartAotViewRequestSchema } from "./dartAotView.js"; import { il2cppMetadataViewRequestSchema } from "./il2cppMetadataView.js"; diff --git a/src/domain/analysisView/analysisViewResult.ts b/src/domain/analysisView/analysisViewResult.ts index 32188128cd..98918bf728 100644 --- a/src/domain/analysisView/analysisViewResult.ts +++ b/src/domain/analysisView/analysisViewResult.ts @@ -213,10 +213,7 @@ import { artifactGraphManifestSchema, artifactInventoryCoverageSchema, } from "../artifactGraph.js"; -import { - apiSurfaceFields, - apiSurfaceViewSchema, -} from "./webNetworkApiSurfaceView.js"; +import { apiSurfaceFields, apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; import { evmViewRequestSchema, evmViewItemSchema, @@ -261,10 +258,8 @@ import { captureViewItemsSchema, captureSummarySchema, } from "./webNetworkCaptureView.js"; -import { - environmentManifestFields, - environmentManifestViewRequestSchema, -} from "./environmentManifestView.js"; +import { environmentManifestFields } from "./environmentManifestSchema.js"; +import { environmentManifestViewRequestSchema } from "./environmentManifestView.js"; import { environmentStateFields, environmentStateViewRequestSchema, @@ -286,13 +281,15 @@ import { import { androidPackageSummarySchema, androidManifestViewItemSchema, - androidManifestViewRequestSchema, - androidComponentsViewRequestSchema, - androidComponentSummaryViewRequestSchema, androidComponentViewRowSchema, androidComponentViewContextSchema, androidComponentSummarySchema, } from "./androidPackageView.js"; +import { + androidManifestViewRequestSchema, + androidComponentsViewRequestSchema, + androidComponentSummaryViewRequestSchema, +} from "../android/androidPackageQuery.js"; import { androidClassSummarySchema, androidMethodSummarySchema, diff --git a/src/domain/analysisView/androidPackageView.test.ts b/src/domain/analysisView/androidPackageView.test.ts index a82e5c65e3..53bb2f050a 100644 --- a/src/domain/analysisView/androidPackageView.test.ts +++ b/src/domain/analysisView/androidPackageView.test.ts @@ -2,12 +2,12 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; import type { JsonValue } from "../jsonValue.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { androidComponentsViewResultSchema, androidComponentSummaryViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +} from "./analysisViewResult.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const component = (name: string, extra: Record = {}) => ({ diff --git a/src/domain/analysisView/androidPackageView.ts b/src/domain/analysisView/androidPackageView.ts index ec6809186d..db0cf0437b 100644 --- a/src/domain/analysisView/androidPackageView.ts +++ b/src/domain/analysisView/androidPackageView.ts @@ -32,13 +32,6 @@ import { analysisTextPageItemSchema, } from "./analysisViewText.js"; -export { - androidViewRequestSchema, - androidManifestViewRequestSchema, - androidComponentsViewRequestSchema, - androidComponentSummaryViewRequestSchema, -} from "../android/androidPackageQuery.js"; - const packageSchema = androidResultSchemas.inspect_android_package; const readReport = createAnalysisViewResultReader(packageSchema); const PAGE_BYTES = 1024 * 1024; diff --git a/src/domain/analysisView/androidSymbolView.test.ts b/src/domain/analysisView/androidSymbolView.test.ts index c0cbe5f530..6d923c1fd9 100644 --- a/src/domain/analysisView/androidSymbolView.test.ts +++ b/src/domain/analysisView/androidSymbolView.test.ts @@ -3,12 +3,12 @@ import { z } from "zod"; import { androidComponentsViewRequestSchema } from "../android/androidPackageQuery.js"; import { createEvidence } from "../evidence.js"; import { immutableJsonSnapshotMember } from "../immutableJson.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { androidClassSummaryViewResultSchema, androidMethodSummaryViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +} from "./analysisViewResult.js"; import type { AdmittedView } from "./analysisViewShape.js"; import { androidClassSummarySchema, diff --git a/src/domain/analysisView/artifactAvailabilityView.test.ts b/src/domain/analysisView/artifactAvailabilityView.test.ts index 485a9f5e09..233c14387b 100644 --- a/src/domain/analysisView/artifactAvailabilityView.test.ts +++ b/src/domain/analysisView/artifactAvailabilityView.test.ts @@ -2,10 +2,8 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; import type { ErlangBeam } from "../erlang/beam.js"; import type { ModelArtifact } from "../model/modelArtifact.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const parent = ( operation: string, diff --git a/src/domain/analysisView/artifactInventorySummaryProjection.ts b/src/domain/analysisView/artifactInventorySummaryProjection.ts index dc9b02e673..7ad2a10074 100644 --- a/src/domain/analysisView/artifactInventorySummaryProjection.ts +++ b/src/domain/analysisView/artifactInventorySummaryProjection.ts @@ -1,7 +1,5 @@ -import { - artifactInventorySummaryViewResultSchema, - sealAnalysisView, -} from "./analysisView.js"; +import { sealAnalysisView } from "./analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "./analysisViewResult.js"; import { ARTIFACT_INVENTORY_VIEW_CAVEATS } from "./artifactInventoryViewCaveats.js"; import { digestCanonicalValue } from "../canonicalDigest.js"; import { evidenceSemanticContent, type Evidence } from "../evidence.js"; diff --git a/src/domain/analysisView/artifactInventoryView.test.ts b/src/domain/analysisView/artifactInventoryView.test.ts index 1b334345a0..0e85b89b50 100644 --- a/src/domain/analysisView/artifactInventoryView.test.ts +++ b/src/domain/analysisView/artifactInventoryView.test.ts @@ -13,7 +13,7 @@ import { artifactInventoryViewRequestSchema, projectArtifactInventoryView, } from "./artifactInventoryView.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const carrier = ARTIFACT_COMPARISON_EXAMPLE.left; const freshInventory = (version: "current" | "historical" = "current") => { diff --git a/src/domain/analysisView/environmentCoverageView.test.ts b/src/domain/analysisView/environmentCoverageView.test.ts index 54f68dd646..c929e4f477 100644 --- a/src/domain/analysisView/environmentCoverageView.test.ts +++ b/src/domain/analysisView/environmentCoverageView.test.ts @@ -10,11 +10,9 @@ import { scenarioCaptureFixture, } from "../../../tests/fixtures/browserScenarioCapture.js"; import type { Evidence } from "../evidence.js"; -import { - projectAnalysisView, - type AnalysisViewParent, - type AnalysisViewResult, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import type { AnalysisViewResult } from "./analysisViewResult.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; import { WITHHELD_WRITE_HINTS, diff --git a/src/domain/analysisView/environmentManifestView.test.ts b/src/domain/analysisView/environmentManifestView.test.ts index 37b7767683..466c30a4bd 100644 --- a/src/domain/analysisView/environmentManifestView.test.ts +++ b/src/domain/analysisView/environmentManifestView.test.ts @@ -4,10 +4,8 @@ import { analysisViewLayoutEvidence, environmentManifestCaptureFixture, } from "../../../tests/fixtures/analysisView.js"; -import { - projectAnalysisView, - type AnalysisViewRequest, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewRequest } from "./analysisViewRequest.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; type ManifestView = Extract< diff --git a/src/domain/analysisView/environmentManifestView.ts b/src/domain/analysisView/environmentManifestView.ts index cabf1085d4..243131657f 100644 --- a/src/domain/analysisView/environmentManifestView.ts +++ b/src/domain/analysisView/environmentManifestView.ts @@ -16,10 +16,6 @@ import { admitScenarioCaptureParent, type ScenarioResponseBodyRetention, } from "./scenarioCaptureRecords.js"; -import { environmentManifestFields } from "./environmentManifestSchema.js"; - -export { environmentManifestFields }; - /** Opt-in template rules, each listed at most once; shared by every view that groups tools. */ export const templateRulesSchema = z .array(z.enum(TEMPLATE_RULES)) diff --git a/src/domain/analysisView/environmentStateView.test.ts b/src/domain/analysisView/environmentStateView.test.ts index 1e77daf221..c9b1a0bd95 100644 --- a/src/domain/analysisView/environmentStateView.test.ts +++ b/src/domain/analysisView/environmentStateView.test.ts @@ -14,12 +14,12 @@ import { import type { Evidence } from "../evidence.js"; import { ownedJsonObjectSchema, type JsonValue } from "../jsonValue.js"; import { webNetworkCaptureSchema } from "../webNetworkCapture.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { analysisViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, type AnalysisViewResult, -} from "./analysisView.js"; +} from "./analysisViewResult.js"; import { EnvironmentStateAccumulator } from "./EnvironmentStateAccumulator.js"; import type { ObservedEntity } from "./environmentStateMerge.js"; import { diff --git a/src/domain/analysisView/goBinaryView.test.ts b/src/domain/analysisView/goBinaryView.test.ts index 239a33b324..751e3ba550 100644 --- a/src/domain/analysisView/goBinaryView.test.ts +++ b/src/domain/analysisView/goBinaryView.test.ts @@ -3,10 +3,8 @@ import { expect, it } from "vitest"; import { AnalysisInputError } from "../analysisErrorCore.js"; import { createEvidence } from "../evidence.js"; import type { GoBinary } from "../go/goBinary.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const report: GoBinary = { artifact: { path: "/artifacts/program", sha256: "a".repeat(64), bytes: 512 }, diff --git a/src/domain/analysisView/gpuCodeObjectsView.test.ts b/src/domain/analysisView/gpuCodeObjectsView.test.ts index a800cee49c..4471ba416c 100644 --- a/src/domain/analysisView/gpuCodeObjectsView.test.ts +++ b/src/domain/analysisView/gpuCodeObjectsView.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; import { freezeOwnedJsonTreeSnapshot } from "../immutableJson.js"; import { gpuCodeObjectsSchema } from "../gpu/gpuCodeObjects.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { gpuCodeObjectsItemsSchema, projectGpuCodeObjectsView, diff --git a/src/domain/analysisView/interfaceBuilderView.test.ts b/src/domain/analysisView/interfaceBuilderView.test.ts index 9c9921be59..c8e9562a7b 100644 --- a/src/domain/analysisView/interfaceBuilderView.test.ts +++ b/src/domain/analysisView/interfaceBuilderView.test.ts @@ -2,11 +2,9 @@ import { describe, expect, it, vi } from "vitest"; import { interfaceBuilderAnalysisSchema } from "../apple/interfaceBuilderGraph.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; -import { - analysisViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { analysisViewResultSchema } from "./analysisViewResult.js"; const target = "a".repeat(64); const resourceRoot = { diff --git a/src/domain/analysisView/javascriptRoutesView.test.ts b/src/domain/analysisView/javascriptRoutesView.test.ts index d972591d35..1442c9785d 100644 --- a/src/domain/analysisView/javascriptRoutesView.test.ts +++ b/src/domain/analysisView/javascriptRoutesView.test.ts @@ -4,10 +4,8 @@ import { completeSemanticSteps } from "../javascript/javascriptSemanticTraversal import { buildJavaScriptRouteInventorySteps } from "../javascript/javascriptRouteInventory.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const inventory = ( diff --git a/src/domain/analysisView/machoView.test.ts b/src/domain/analysisView/machoView.test.ts index 1c84e1da4d..a9818507df 100644 --- a/src/domain/analysisView/machoView.test.ts +++ b/src/domain/analysisView/machoView.test.ts @@ -1,11 +1,9 @@ import { describe, expect, it } from "vitest"; import { inspectMachoSchema } from "../native/nativeInspection.js"; -import { - analysisViewResultSchema, - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; +import { analysisViewResultSchema } from "./analysisViewResult.js"; it("binds every Mach-O facet to its rows, including empty pages", () => { for (const facet of [ diff --git a/src/domain/analysisView/managedMemberView.test.ts b/src/domain/analysisView/managedMemberView.test.ts index 122f27640a..5f95aebc0b 100644 --- a/src/domain/analysisView/managedMemberView.test.ts +++ b/src/domain/analysisView/managedMemberView.test.ts @@ -2,7 +2,7 @@ import { expect, it } from "vitest"; import { MANAGED_RECONSTRUCTION_IMPORT_EXAMPLE } from "../../contracts/managed/managedWorkflowExamples.js"; import { freezeOwnedJsonTreeSnapshot } from "../immutableJson.js"; import { managedMemberInspectionSchema } from "../managed/managedArtifact.js"; -import type { AnalysisViewParent } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { managedMembersViewRequestSchema, managedMembersViewPageSchema, diff --git a/src/domain/analysisView/nativeInventoryView.test.ts b/src/domain/analysisView/nativeInventoryView.test.ts index ae07597581..425f151632 100644 --- a/src/domain/analysisView/nativeInventoryView.test.ts +++ b/src/domain/analysisView/nativeInventoryView.test.ts @@ -1,10 +1,8 @@ import { afterEach, expect, it, vi } from "vitest"; import { freezeJsonSnapshot } from "../immutableJson.js"; import { nativeInventoryRowsSchemas } from "../native/nativeInventory.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; afterEach(() => vi.restoreAllMocks()); diff --git a/src/domain/analysisView/pcapCaptureView.test.ts b/src/domain/analysisView/pcapCaptureView.test.ts index 1e16f2b02d..1f0152d4a9 100644 --- a/src/domain/analysisView/pcapCaptureView.test.ts +++ b/src/domain/analysisView/pcapCaptureView.test.ts @@ -1,9 +1,7 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const capture = ( tsharkStderr: string, diff --git a/src/domain/analysisView/procedurePseudocodeView.test.ts b/src/domain/analysisView/procedurePseudocodeView.test.ts index e5a5fe1100..cac5124817 100644 --- a/src/domain/analysisView/procedurePseudocodeView.test.ts +++ b/src/domain/analysisView/procedurePseudocodeView.test.ts @@ -2,10 +2,8 @@ import { expect, it } from "vitest"; import { ANALYSIS_EVIDENCE_PREDICATE } from "../evidence.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const parent = (normalizedResult: unknown): AnalysisViewParent => ({ evidenceId: `ev_${"a".repeat(64)}`, diff --git a/src/domain/analysisView/rustBinaryView.test.ts b/src/domain/analysisView/rustBinaryView.test.ts index 9cb8d8d898..8e47b109b1 100644 --- a/src/domain/analysisView/rustBinaryView.test.ts +++ b/src/domain/analysisView/rustBinaryView.test.ts @@ -2,10 +2,8 @@ import { expect, it } from "vitest"; import { createEvidence } from "../evidence.js"; import { freezeJsonSnapshot } from "../immutableJson.js"; import type { RustBinary } from "../rust/rustBinary.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; const report: RustBinary = { artifact: { path: "/artifacts/rust", sha256: "a".repeat(64), bytes: 512 }, diff --git a/src/domain/analysisView/webBundleView.test.ts b/src/domain/analysisView/webBundleView.test.ts index 25639df204..599b378b83 100644 --- a/src/domain/analysisView/webBundleView.test.ts +++ b/src/domain/analysisView/webBundleView.test.ts @@ -3,10 +3,8 @@ import type { WebBundleAnalysis } from "../webBundleAnalysis.js"; import { sampleWebBundleAnalysis } from "../../../tests/fixtures/webBundleAnalysis.js"; import { AnalysisInputError } from "../analysisErrorCore.js"; import { projectAnalysisError } from "../analysisErrorProjection.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "./analysisView.js"; +import { projectAnalysisView } from "./analysisView.js"; +import type { AnalysisViewParent } from "./analysisViewParent.js"; import { analysisViewRequestSchema } from "./analysisViewRequest.js"; const makeParent = ( diff --git a/src/domain/analysisView/webNetworkApiSurfaceView.ts b/src/domain/analysisView/webNetworkApiSurfaceView.ts index d1bd0224c7..29830efe0d 100644 --- a/src/domain/analysisView/webNetworkApiSurfaceView.ts +++ b/src/domain/analysisView/webNetworkApiSurfaceView.ts @@ -10,9 +10,7 @@ import { import { WebNetworkApiSurfaceAccumulator } from "./WebNetworkApiSurfaceAccumulator.js"; import { admitHarCaptureParent } from "./WebNetworkProjectionAccumulator.js"; -import { apiSurfaceFields, apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; - -export { apiSurfaceFields, apiSurfaceViewSchema }; +import { apiSurfaceViewSchema } from "./apiSurfaceSchema.js"; /** Create the budget and query-name policy shared by admission and projection. */ export const createApiSurfaceAccumulator = ( diff --git a/src/domain/desktop/appBuild.ts b/src/domain/desktop/appBuild.ts index 02c8e4f4c5..5f3d067061 100644 --- a/src/domain/desktop/appBuild.ts +++ b/src/domain/desktop/appBuild.ts @@ -15,8 +15,8 @@ import { renderEnvToml, renderInstallScript, renderSmokeTask, - shellQuote, } from "./appExport.js"; +import { shellQuote } from "./shellQuote.js"; import { COMPUTER_ARCHITECTURES, DOWNLOAD_INSTALL_SCRIPT, diff --git a/src/domain/desktop/appExport.ts b/src/domain/desktop/appExport.ts index e2b1505425..01ddb6216f 100644 --- a/src/domain/desktop/appExport.ts +++ b/src/domain/desktop/appExport.ts @@ -44,7 +44,6 @@ import { capturedFileUrl, type LaunchDocument } from "./seedDocument.js"; import { posix } from "node:path"; import { roleFacts, stateDirectoryOf } from "./stateRoles.js"; import { shellQuote } from "./shellQuote.js"; -export { shellQuote } from "./shellQuote.js"; /** agentenv-framework release targeted by the exported class. */ export const DESKTOP_EXPORT_FRAMEWORK_VERSION = "0.9.1485"; diff --git a/src/domain/javascript/javascriptAstFingerprint.ts b/src/domain/javascript/javascriptAstFingerprint.ts index f7d4e5be44..b664fefd4b 100644 --- a/src/domain/javascript/javascriptAstFingerprint.ts +++ b/src/domain/javascript/javascriptAstFingerprint.ts @@ -3,7 +3,7 @@ import { createHash } from "node:crypto"; import * as t from "@babel/types"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { traverseJavaScriptAst } from "./javascriptSemanticTraversal.js"; diff --git a/src/domain/javascript/javascriptBundledRequireOrigin.ts b/src/domain/javascript/javascriptBundledRequireOrigin.ts index e61c4e596b..88b1d53b48 100644 --- a/src/domain/javascript/javascriptBundledRequireOrigin.ts +++ b/src/domain/javascript/javascriptBundledRequireOrigin.ts @@ -12,10 +12,8 @@ import { import { collectJavaScriptIdentityWritesSteps } from "./javascriptElectronMemberWrites.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; -import { - semanticRequireOriginCandidate, - type JavaScriptRequireOrigin, -} from "./javascriptSemanticRequireOrigin.js"; +import { semanticRequireOriginCandidate } from "./javascriptSemanticRequireOrigin.js"; +import type { JavaScriptRequireOrigin } from "./javascriptSemanticRequireOriginSlot.js"; import type { JavaScriptModuleOrigin } from "./javascriptSemanticIr.js"; import { resolveSemanticBindingState, diff --git a/src/domain/javascript/javascriptCommonJsExportReceivers.ts b/src/domain/javascript/javascriptCommonJsExportReceivers.ts index 49d8146c8d..9b1c869210 100644 --- a/src/domain/javascript/javascriptCommonJsExportReceivers.ts +++ b/src/domain/javascript/javascriptCommonJsExportReceivers.ts @@ -11,7 +11,7 @@ import { semanticResolutionBlocked, type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { traverseJavaScriptValueAstSteps } from "./javascriptSemanticTraversal.js"; import { semanticPropertyPathKeyMatches } from "./javascriptSemanticPropertyPaths.js"; import { createSemanticModuleInterop } from "./javascriptModuleInterop.js"; diff --git a/src/domain/javascript/javascriptNetworkRequests.ts b/src/domain/javascript/javascriptNetworkRequests.ts index a2b4cfc8ec..41dbe94c71 100644 --- a/src/domain/javascript/javascriptNetworkRequests.ts +++ b/src/domain/javascript/javascriptNetworkRequests.ts @@ -12,7 +12,7 @@ import { type JavaScriptSemanticAnalysisState, } from "./javascriptSemanticState.js"; import * as t from "@babel/types"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; export const isFetchCall = ( node: t.Node, diff --git a/src/domain/javascript/javascriptRouteScan.ts b/src/domain/javascript/javascriptRouteScan.ts index 175ea164b7..8766aca9e8 100644 --- a/src/domain/javascript/javascriptRouteScan.ts +++ b/src/domain/javascript/javascriptRouteScan.ts @@ -35,8 +35,8 @@ import { javaScriptRuntimeContext } from "./javascriptSourceParser.js"; import { semanticObjectPropertyReferences, semanticArrayPropertyReferences, - type JavaScriptSemanticObjectReference, } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; import { classifyNextAppRoutePath, routePath } from "./javascriptRoutePaths.js"; import type { JavaScriptRouteCandidate, diff --git a/src/domain/javascript/javascriptRpcEndpoints.ts b/src/domain/javascript/javascriptRpcEndpoints.ts index 548bb4bba2..2b303467b9 100644 --- a/src/domain/javascript/javascriptRpcEndpoints.ts +++ b/src/domain/javascript/javascriptRpcEndpoints.ts @@ -14,8 +14,6 @@ import type { JavaScriptRpcScan, } from "./javascriptRpcEndpoint.js"; -export type { JavaScriptRpcScan } from "./javascriptRpcEndpoint.js"; - // tRPC proxies preserve router and procedure names as property names, so they // survive identifier minification. Method names follow @trpc/react-query and // @trpc/tanstack-react-query 11.19 documentation; cache-key helpers such as diff --git a/src/domain/javascript/javascriptSemanticAmbientWrites.ts b/src/domain/javascript/javascriptSemanticAmbientWrites.ts index 4482b9f2f3..ee6cabec83 100644 --- a/src/domain/javascript/javascriptSemanticAmbientWrites.ts +++ b/src/domain/javascript/javascriptSemanticAmbientWrites.ts @@ -21,10 +21,8 @@ import { semanticDefiniteEffectPrecedesReference, semanticEffectTiming, } from "./javascriptSemanticMutationInitializers.js"; -import { - semanticIterationSources, - type SemanticIterationSource, -} from "./javascriptSemanticIterationSources.js"; +import { semanticIterationSources } from "./javascriptSemanticIterationSources.js"; +import type { SemanticIterationSource } from "./javascriptSemanticIterationFacts.js"; import { joinSemanticPropertyPaths, semanticPropertyPathKeyMatches, @@ -42,11 +40,6 @@ import { } from "./javascriptSemanticReferenceSources.js"; import { semanticCopiedReferencePath } from "./javascriptSemanticCopiedReferencePaths.js"; -export { - isUnmodifiedGlobalReference, - isUnmodifiedImportMetaUrl, -} from "./javascriptSemanticGlobalReference.js"; - /** Global self references retain object identity, rather than adding a property. */ const canonicalAmbientPath = ( path: readonly (string | null)[], diff --git a/src/domain/javascript/javascriptSemanticAnalysis.ts b/src/domain/javascript/javascriptSemanticAnalysis.ts index c29f75c0a3..a671268f08 100644 --- a/src/domain/javascript/javascriptSemanticAnalysis.ts +++ b/src/domain/javascript/javascriptSemanticAnalysis.ts @@ -88,8 +88,8 @@ import { import { createAmbientPathResolver, indexJavaScriptAmbientWritesSteps, - isUnmodifiedGlobalReference, } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { hasUncertainTypeOnlyModuleSideEffect, TYPE_ONLY_MODULE_SIDE_EFFECT_LIMITATION, @@ -522,7 +522,6 @@ export function* analyzeParsedJavaScriptSemanticsSteps( * Receiver identity facts for the overloaded `.open` syntax, plus * `configuration` for `.get` reads of a `getConfiguration(...)` result. */ -export type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js"; /** Lexical receiver identities that select static endpoint interpretations. */ export interface JavaScriptReceiverFacts { diff --git a/src/domain/javascript/javascriptSemanticAsyncEffects.ts b/src/domain/javascript/javascriptSemanticAsyncEffects.ts index 6fb46d96e5..10aec8401f 100644 --- a/src/domain/javascript/javascriptSemanticAsyncEffects.ts +++ b/src/domain/javascript/javascriptSemanticAsyncEffects.ts @@ -19,10 +19,8 @@ import { } from "./javascriptSemanticState.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js"; -import { - createGlobalReferenceReader, - isUnmodifiedGlobalReference, -} from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; +import { createGlobalReferenceReader } from "./javascriptSemanticAmbientWrites.js"; import { semanticOpaqueGlobalMayHaveChanged, semanticOpaqueNameMayHaveChanged, diff --git a/src/domain/javascript/javascriptSemanticDataEffects.ts b/src/domain/javascript/javascriptSemanticDataEffects.ts index f63782365a..9373b2b45c 100644 --- a/src/domain/javascript/javascriptSemanticDataEffects.ts +++ b/src/domain/javascript/javascriptSemanticDataEffects.ts @@ -38,7 +38,7 @@ import { traverseDataEffects as traverseWithContext, type DataEffectTraversalContext as TraversalContext, } from "./javascriptSemanticDataEffectHelpers.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; import { semanticPrimitiveSet } from "./javascriptSemanticPrimitives.js"; import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js"; diff --git a/src/domain/javascript/javascriptSemanticGetterResults.ts b/src/domain/javascript/javascriptSemanticGetterResults.ts index ab47ec150b..c6f24e8245 100644 --- a/src/domain/javascript/javascriptSemanticGetterResults.ts +++ b/src/domain/javascript/javascriptSemanticGetterResults.ts @@ -2,8 +2,8 @@ import * as t from "@babel/types"; import { semanticObjectPropertyReferences, semanticArrayPropertyReferences, - type JavaScriptSemanticObjectReference as Reference, } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference as Reference } from "./javascriptSemanticObjectReference.js"; import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; import { SemanticGetterQuery } from "./javascriptSemanticGetterQueries.js"; import { GetterResultWalker } from "./javascriptSemanticGetterResultWalker.js"; diff --git a/src/domain/javascript/javascriptSemanticIterationSources.ts b/src/domain/javascript/javascriptSemanticIterationSources.ts index a69e440f55..6b9d4c9052 100644 --- a/src/domain/javascript/javascriptSemanticIterationSources.ts +++ b/src/domain/javascript/javascriptSemanticIterationSources.ts @@ -20,11 +20,6 @@ import type { SemanticIterationSource, } from "./javascriptSemanticIterationFacts.js"; -export type { - IterationSourceValue, - SemanticIterationSource, -} from "./javascriptSemanticIterationFacts.js"; - /** A property value obtained by destructuring or an own-enumerable copy. */ export interface SemanticPropertyReadSource extends IterationSourceValue, SourceGuards { diff --git a/src/domain/javascript/javascriptSemanticMapOperations.ts b/src/domain/javascript/javascriptSemanticMapOperations.ts index ce559fe3ba..469ff7c04e 100644 --- a/src/domain/javascript/javascriptSemanticMapOperations.ts +++ b/src/domain/javascript/javascriptSemanticMapOperations.ts @@ -4,7 +4,7 @@ import { semanticStaticPropertyName, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { semanticOpaqueGlobalMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { diff --git a/src/domain/javascript/javascriptSemanticMemberMutations.ts b/src/domain/javascript/javascriptSemanticMemberMutations.ts index b4bf18206f..bf12e3354a 100644 --- a/src/domain/javascript/javascriptSemanticMemberMutations.ts +++ b/src/domain/javascript/javascriptSemanticMemberMutations.ts @@ -52,14 +52,14 @@ import { semanticObjectPropertyReferences, semanticObjectAccessorReferences, semanticNativePrototypeChain, - type JavaScriptSemanticObjectReference, } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; import { semanticIterationSources, semanticPropertyReadSources, - type SemanticIterationSource, type SemanticPropertyReadSource, } from "./javascriptSemanticIterationSources.js"; +import type { SemanticIterationSource } from "./javascriptSemanticIterationFacts.js"; import { semanticGetterReceiverReferences } from "./javascriptSemanticGetterResults.js"; import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js"; import { createSemanticReturnedReferences } from "./javascriptSemanticReturnedReferences.js"; diff --git a/src/domain/javascript/javascriptSemanticObjectReferences.ts b/src/domain/javascript/javascriptSemanticObjectReferences.ts index 9012dc8ef4..291c5cbb22 100644 --- a/src/domain/javascript/javascriptSemanticObjectReferences.ts +++ b/src/domain/javascript/javascriptSemanticObjectReferences.ts @@ -23,8 +23,6 @@ import { expandSemanticGetterResult } from "./javascriptSemanticGetterResultWalk import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; -export type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; - export const semanticClassCandidates = ( root: t.Node, state: JavaScriptSemanticAnalysisState, diff --git a/src/domain/javascript/javascriptSemanticProjectedSources.ts b/src/domain/javascript/javascriptSemanticProjectedSources.ts index b2cf9c7db9..304658bee7 100644 --- a/src/domain/javascript/javascriptSemanticProjectedSources.ts +++ b/src/domain/javascript/javascriptSemanticProjectedSources.ts @@ -8,8 +8,8 @@ import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; import { semanticObjectPropertyReferences, semanticArrayPropertyReferences, - type JavaScriptSemanticObjectReference, } from "./javascriptSemanticObjectReferences.js"; +import type { JavaScriptSemanticObjectReference } from "./javascriptSemanticObjectReference.js"; import { semanticBindingAtRead, semanticDefiniteEffectPrecedesReference, diff --git a/src/domain/javascript/javascriptSemanticPromises.ts b/src/domain/javascript/javascriptSemanticPromises.ts index 80aae5e75d..b8767cfdc0 100644 --- a/src/domain/javascript/javascriptSemanticPromises.ts +++ b/src/domain/javascript/javascriptSemanticPromises.ts @@ -19,7 +19,7 @@ import { import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js"; import { traverseJavaScriptValueAst } from "./javascriptSemanticTraversal.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { range } from "./javascriptStaticAnalysisHelpers.js"; import { assignedSemanticResultBindings, diff --git a/src/domain/javascript/javascriptSemanticRequireOrigin.ts b/src/domain/javascript/javascriptSemanticRequireOrigin.ts index 5a2dccebaf..ba7068aefe 100644 --- a/src/domain/javascript/javascriptSemanticRequireOrigin.ts +++ b/src/domain/javascript/javascriptSemanticRequireOrigin.ts @@ -63,12 +63,7 @@ import { import { isUnmodifiedGlobalReference, isUnmodifiedImportMetaUrl, -} from "./javascriptSemanticAmbientWrites.js"; - -export type { - JavaScriptRequireLoaderFact, - JavaScriptRequireOrigin, -} from "./javascriptSemanticRequireOriginSlot.js"; +} from "./javascriptSemanticGlobalReference.js"; const possibleCommonJsDelegateWriteStates = new WeakSet(); diff --git a/src/domain/javascript/javascriptSemanticResources.ts b/src/domain/javascript/javascriptSemanticResources.ts index 358d54c601..646023620d 100644 --- a/src/domain/javascript/javascriptSemanticResources.ts +++ b/src/domain/javascript/javascriptSemanticResources.ts @@ -20,7 +20,7 @@ import { staticObjectCopyPropertyReader, unwrapJavaScriptExpression, } from "./javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { semanticOpaqueBindingReadMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js"; import { semanticBindingAtRead } from "./javascriptSemanticMutationInitializers.js"; import { diff --git a/src/domain/javascript/javascriptSemanticTypedArrays.ts b/src/domain/javascript/javascriptSemanticTypedArrays.ts index 5bda6960c1..7be386c6c3 100644 --- a/src/domain/javascript/javascriptSemanticTypedArrays.ts +++ b/src/domain/javascript/javascriptSemanticTypedArrays.ts @@ -1,7 +1,7 @@ import * as t from "@babel/types"; import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascriptSemanticGlobalReference.js"; import { semanticOpaqueGlobalMayHaveChanged } from "./javascriptSemanticOpaqueEffects.js"; import type { JavaScriptSemanticAnalysisState } from "./javascriptSemanticState.js"; diff --git a/src/domain/javascript/javascriptStaticAnalysisCalls.ts b/src/domain/javascript/javascriptStaticAnalysisCalls.ts index 4a25d8d23d..2edbc99c8b 100644 --- a/src/domain/javascript/javascriptStaticAnalysisCalls.ts +++ b/src/domain/javascript/javascriptStaticAnalysisCalls.ts @@ -51,7 +51,7 @@ import { import { isUnmodifiedGlobalReference, isUnmodifiedImportMetaUrl, -} from "./javascriptSemanticAmbientWrites.js"; +} from "./javascriptSemanticGlobalReference.js"; import { evaluateSemanticExpression, evaluateSemanticExpressionProvenance, diff --git a/src/domain/javascript/javascriptStaticAnalysisState.ts b/src/domain/javascript/javascriptStaticAnalysisState.ts index 3025fc01fd..099ef9f5a6 100644 --- a/src/domain/javascript/javascriptStaticAnalysisState.ts +++ b/src/domain/javascript/javascriptStaticAnalysisState.ts @@ -1,7 +1,7 @@ import type * as t from "@babel/types"; import type { JavaScriptBundlerFactoryRoles } from "./javascriptBundlerFactoryRoles.js"; import type { JavaScriptOpenReceiverFact } from "./javascriptSemanticReceiverFacts.js"; -import type { JavaScriptRpcScan } from "./javascriptRpcEndpoints.js"; +import type { JavaScriptRpcScan } from "./javascriptRpcEndpoint.js"; import type { JavaScriptStaticPathIntrinsicQuery } from "./javascriptStaticAnalysisHelpers.js"; import type { JavaScriptExpressionShapeResolver } from "./javascriptSemanticExpressionShapes.js"; diff --git a/src/domain/javascript/javascriptStaticPathIntrinsics.ts b/src/domain/javascript/javascriptStaticPathIntrinsics.ts index bc363bb4d0..61fc7dc8d8 100644 --- a/src/domain/javascript/javascriptStaticPathIntrinsics.ts +++ b/src/domain/javascript/javascriptStaticPathIntrinsics.ts @@ -7,7 +7,7 @@ import { evaluateSemanticExpressionProvenance } from "./javascriptSemanticValues import { isUnmodifiedGlobalReference, isUnmodifiedImportMetaUrl, -} from "./javascriptSemanticAmbientWrites.js"; +} from "./javascriptSemanticGlobalReference.js"; import { unwrapJavaScriptExpression } from "./javascriptAstValues.js"; import type { JavaScriptStaticPathIntrinsicQuery } from "./javascriptStaticAnalysisHelpers.js"; diff --git a/src/domain/referenceSourceRequireBindings.ts b/src/domain/referenceSourceRequireBindings.ts index 323dbd014a..3c81617003 100644 --- a/src/domain/referenceSourceRequireBindings.ts +++ b/src/domain/referenceSourceRequireBindings.ts @@ -6,7 +6,7 @@ import { semanticStaticPropertyKey, unwrapJavaScriptExpression, } from "./javascript/javascriptAstValues.js"; -import { isUnmodifiedGlobalReference } from "./javascript/javascriptSemanticAmbientWrites.js"; +import { isUnmodifiedGlobalReference } from "./javascript/javascriptSemanticGlobalReference.js"; import { indexJavaScriptBindingsWithReceiversSteps, type JavaScriptReceiverIndex, diff --git a/src/domain/webBundleAnalyzerInspection.ts b/src/domain/webBundleAnalyzerInspection.ts index 30565524a9..776b68662b 100644 --- a/src/domain/webBundleAnalyzerInspection.ts +++ b/src/domain/webBundleAnalyzerInspection.ts @@ -20,7 +20,7 @@ import { completeSemanticSteps, } from "./javascript/javascriptSemanticTraversal.js"; import { indexJavaScriptBindingsWithReceiversSteps } from "./javascript/javascriptSemanticAnalysis.js"; -import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemanticAnalysis.js"; +import type { JavaScriptOpenReceiverFact } from "./javascript/javascriptSemanticReceiverFacts.js"; import { isIntrinsicMapLookup } from "./javascript/javascriptSemanticMapOperations.js"; import { semanticStaticPropertyKey, diff --git a/src/domain/webSourceLocation.ts b/src/domain/webSourceLocation.ts index f22e9b64b2..c61deafb37 100644 --- a/src/domain/webSourceLocation.ts +++ b/src/domain/webSourceLocation.ts @@ -2,7 +2,6 @@ import { localSourceMapLimitsSchema, WEB_SOURCE_MAP_LIMITS, } from "./sourceMapLimits.js"; -export { WEB_SOURCE_MAP_LIMITS } from "./sourceMapLimits.js"; import { z } from "zod"; import { browserDeadlineIncrease, diff --git a/src/domain/windows/ntfsMft.ts b/src/domain/windows/ntfsMft.ts index 2cec504f92..182eda821d 100644 --- a/src/domain/windows/ntfsMft.ts +++ b/src/domain/windows/ntfsMft.ts @@ -8,7 +8,6 @@ import { MFT_INLINE_ATTRIBUTE_REFERENCE_LIMIT, type MftFileReference, } from "./ntfsMftAttributes.js"; -export type { MftFileReference } from "./ntfsMftAttributes.js"; import { err, ok, type Result } from "../result.js"; import type { RegionDecodeFailure, diff --git a/src/erlang/BeamProvider.ts b/src/erlang/BeamProvider.ts index 76419eec11..3e4d6867a0 100644 --- a/src/erlang/BeamProvider.ts +++ b/src/erlang/BeamProvider.ts @@ -1,7 +1,8 @@ import { ERLANG_BEAM_PROVIDER_IDENTITY } from "./ErlangBeamProviderIdentity.js"; import { + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; @@ -13,7 +14,6 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, artifactSelectedPathErrno, - artifactFileCancellationCause, } from "../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import type { BeamPort } from "../application/erlang/BeamPort.js"; @@ -21,8 +21,6 @@ import type { ExecutionOptions } from "../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../domain/artifactOperationError.js"; import { AnalysisAccessDeniedError, - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, } from "../domain/analysisErrorCore.js"; @@ -111,27 +109,12 @@ const beamFailurePrimary = ( input: InspectErlangBeamInput, ) => { const { cause, origin } = facts; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -159,17 +142,7 @@ const beamFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } if (cause instanceof Error && "code" in cause) { if (cause.code === "EACCES" || cause.code === "EPERM") diff --git a/src/firmware/devicetree/DevicetreeProvider.ts b/src/firmware/devicetree/DevicetreeProvider.ts index 385fe4820c..ee14aba535 100644 --- a/src/firmware/devicetree/DevicetreeProvider.ts +++ b/src/firmware/devicetree/DevicetreeProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -12,15 +13,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { DevicetreePort } from "../../application/binaryDiagnostics/DevicetreePort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -114,27 +112,12 @@ const devicetreeFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -157,17 +140,7 @@ const devicetreeFailurePrimary = ( } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/go/GoBinaryProvider.ts b/src/go/GoBinaryProvider.ts index b658d3635b..3c028e5a5b 100644 --- a/src/go/GoBinaryProvider.ts +++ b/src/go/GoBinaryProvider.ts @@ -2,9 +2,10 @@ import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; import { GO_BINARY_PROVIDER_IDENTITY } from "./GoBinaryProviderIdentity.js"; import { projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, projectArtifactCloseFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, } from "../artifacts/StableArtifactFailure.js"; import { type StableArtifactSnapshot, @@ -14,7 +15,6 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; @@ -22,8 +22,6 @@ import type { ExecutionOptions } from "../application/AnalysisProvider.js"; import type { GoBinaryPort } from "../application/go/GoBinaryPort.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisOutputError, AnalysisResourceConstraintError, @@ -66,22 +64,12 @@ const failureForPrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError(OPERATION, path, cause.message, { - cause, - }); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof BinaryResourceFailure) { const parameter = cause.boundary === "search" @@ -146,11 +134,7 @@ const failureForPrimary = ( }, ); if (cause.reason === "path") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/hopper/BridgeLauncher.ts b/src/hopper/BridgeLauncher.ts index 485c7f2687..110fb5a869 100644 --- a/src/hopper/BridgeLauncher.ts +++ b/src/hopper/BridgeLauncher.ts @@ -53,8 +53,8 @@ import type { OwnedLease } from "../process/OwnedLease.js"; import { MAXIMUM_BRIDGE_MAX_REQUEST_BYTES, MINIMUM_BRIDGE_MAX_REQUEST_BYTES, - type BridgeMaxRequestBytes, -} from "../config/bridgeMaxRequestBytes.js"; +} from "../domain/analysisResourceLimits.js"; +import type { BridgeMaxRequestBytes } from "../config/bridgeMaxRequestBytes.js"; import { HOPPER_SHUTDOWN_TIMEOUT_MS } from "./HopperSessionValues.js"; import { verifyMacHopperNativeHelper } from "./HopperMacPublisher.js"; import { inspectLinuxHopperLauncher } from "../application/LinuxHopper.js"; diff --git a/src/hopper/HopperSessionValues.test.ts b/src/hopper/HopperSessionValues.test.ts index b692085115..ae6d718785 100644 --- a/src/hopper/HopperSessionValues.test.ts +++ b/src/hopper/HopperSessionValues.test.ts @@ -1,6 +1,6 @@ import { expect, it } from "vitest"; -import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../config/bridgeMaxRequestBytes.js"; +import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../domain/analysisResourceLimits.js"; import { parseHopperServerInfo } from "./HopperSessionValues.js"; const health = { diff --git a/src/hopper/HopperSessionValues.ts b/src/hopper/HopperSessionValues.ts index 477060081b..09768af56e 100644 --- a/src/hopper/HopperSessionValues.ts +++ b/src/hopper/HopperSessionValues.ts @@ -1,4 +1,4 @@ -import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../config/bridgeMaxRequestBytes.js"; +import { DEFAULT_BRIDGE_MAX_REQUEST_BYTES } from "../domain/analysisResourceLimits.js"; import { HopperProtocolError } from "../domain/hopperErrors.js"; import type { JsonValue } from "../domain/jsonValue.js"; import { err, ok, type Result } from "../domain/result.js"; diff --git a/src/il2cpp/Il2cppMetadataProvider.ts b/src/il2cpp/Il2cppMetadataProvider.ts index 8ee9b18a73..1c489ff96d 100644 --- a/src/il2cpp/Il2cppMetadataProvider.ts +++ b/src/il2cpp/Il2cppMetadataProvider.ts @@ -15,15 +15,15 @@ import { import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js"; import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, } from "../artifacts/StableArtifactFailure.js"; import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, artifactFileCompletion, bindArtifactFileCleanup, - artifactFileCancellationCause, assertArtifactReadActive, } from "../artifacts/ArtifactFileOperation.js"; import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js"; @@ -35,7 +35,6 @@ import { createIl2cppMetadataAdmission } from "./Il2cppMetadataAdmission.js"; import { hexAddress as hex } from "./Il2cppBinaryImage.js"; import { jsonValueByteLength } from "../domain/jsonValueByteLength.js"; import { - AnalysisArtifactChangedError, AnalysisCancelledError, AnalysisCapabilityUnavailableError, AnalysisInputError, @@ -117,22 +116,12 @@ const failureForPrimary = ( }, ]); } - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError(OPERATION, path, cause.message, { - cause, - }); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -149,11 +138,7 @@ const failureForPrimary = ( }, ); if (cause.reason === "path") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: [field], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION, field); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/javascript/analysis/JavaScriptAnalysisTransfer.ts b/src/javascript/analysis/JavaScriptAnalysisTransfer.ts index 9b89cd5b80..5bd990be47 100644 --- a/src/javascript/analysis/JavaScriptAnalysisTransfer.ts +++ b/src/javascript/analysis/JavaScriptAnalysisTransfer.ts @@ -26,7 +26,6 @@ import { } from "./JavaScriptAnalysisWorkerSchemas.js"; // Budgets bound transfer representation, independently of source length. -export { JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES } from "../../domain/javascript/javascriptAnalysisResourceControls.js"; const FRAME_BYTES = JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES; const TRANSFER_BYTES = 512 * 1024 * 1024; const MAX_TRANSFER_BYTES = 2 * 1024 * 1024 * 1024; diff --git a/src/javascript/analysis/JavaScriptAnalysisWorker.ts b/src/javascript/analysis/JavaScriptAnalysisWorker.ts index 837294d91b..dc4af6c852 100644 --- a/src/javascript/analysis/JavaScriptAnalysisWorker.ts +++ b/src/javascript/analysis/JavaScriptAnalysisWorker.ts @@ -67,11 +67,11 @@ import { } from "../../process/ProviderDeadline.js"; import { readJavaScriptAnalysisTransfer, - JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES, javaScriptForwardedTransferRemediation, javaScriptTransferRetentionBytes, } from "./JavaScriptAnalysisTransfer.js"; import { + JAVASCRIPT_ANALYSIS_TRANSFER_RECORD_BYTES, JAVASCRIPT_PARENT_CHECKPOINT_HEAP_REMEDIATION, JAVASCRIPT_PARENT_RESULT_BUDGET_REMEDIATION, JAVASCRIPT_WORKER_HEAP_MIB_MAXIMUM, diff --git a/src/javascript/sourceMaps/SourceMapCommand.test.ts b/src/javascript/sourceMaps/SourceMapCommand.test.ts index 02cdc5aeab..7206f65bf8 100644 --- a/src/javascript/sourceMaps/SourceMapCommand.test.ts +++ b/src/javascript/sourceMaps/SourceMapCommand.test.ts @@ -17,10 +17,8 @@ import { ProviderProcessSupervisor, spawnOwnedProviderProcess, } from "../../process/ProviderProcess.js"; -import { - WEB_SOURCE_MAP_LIMITS, - type WebSourceMapReport, -} from "../../domain/webSourceLocation.js"; +import { WEB_SOURCE_MAP_LIMITS } from "../../domain/sourceMapLimits.js"; +import type { WebSourceMapReport } from "../../domain/webSourceLocation.js"; import { traceSourceMap } from "./TraceSourceMap.js"; import { runSourceMapCommand, diff --git a/src/javascript/sourceMaps/SourceMapCommand.ts b/src/javascript/sourceMaps/SourceMapCommand.ts index a1810bafb2..137ac06f5b 100644 --- a/src/javascript/sourceMaps/SourceMapCommand.ts +++ b/src/javascript/sourceMaps/SourceMapCommand.ts @@ -1,4 +1,5 @@ import { + WEB_SOURCE_MAP_LIMITS, sourceMapLimitSelection, sourceMapLimitRecovery, type SourceMapLimits, @@ -23,7 +24,6 @@ import type { JsonValue } from "../../domain/jsonValue.js"; import { err, type Result } from "../../domain/result.js"; import { DEFAULT_WEB_SOURCE_MAP_DECODE_TIMEOUT_MS, - WEB_SOURCE_MAP_LIMITS, webSourceMapDecodeTimeoutRecovery, type WebSourceMapReport, } from "../../domain/webSourceLocation.js"; diff --git a/src/native/apple/AppleIpsProvider.ts b/src/native/apple/AppleIpsProvider.ts index d917fc52b5..dcbb999dda 100644 --- a/src/native/apple/AppleIpsProvider.ts +++ b/src/native/apple/AppleIpsProvider.ts @@ -14,9 +14,10 @@ import { } from "../../domain/native/appleIps.js"; import type { StableArtifactSnapshot } from "../../artifacts/readStableArtifact.js"; import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -25,15 +26,12 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { AppleIpsPort } from "../../application/binaryDiagnostics/AppleIpsPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -262,25 +260,12 @@ const appleIpsFailurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof AppleIpsResourceFailure) return new AnalysisResourceConstraintError( OPERATION, @@ -325,11 +310,7 @@ const appleIpsFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: [field], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION, field); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/elf/ElfNotesProvider.ts b/src/native/elf/ElfNotesProvider.ts index dbc875b738..4994990b0a 100644 --- a/src/native/elf/ElfNotesProvider.ts +++ b/src/native/elf/ElfNotesProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -12,15 +13,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { ElfNotesPort } from "../../application/binaryDiagnostics/ElfNotesPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -144,27 +142,12 @@ const elfNotesFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -199,17 +182,7 @@ const elfNotesFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/elf/ElfUnwindRangesProvider.ts b/src/native/elf/ElfUnwindRangesProvider.ts index 86044d8dad..4a68a87e48 100644 --- a/src/native/elf/ElfUnwindRangesProvider.ts +++ b/src/native/elf/ElfUnwindRangesProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -9,15 +10,12 @@ import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { ElfUnwindRangesPort } from "../../application/binaryDiagnostics/ElfUnwindRangesPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -149,25 +147,12 @@ const elfUnwindFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -202,17 +187,7 @@ const elfUnwindFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/elf/KernelBuildConfigProvider.ts b/src/native/elf/KernelBuildConfigProvider.ts index 0181da8151..49f3a74f6b 100644 --- a/src/native/elf/KernelBuildConfigProvider.ts +++ b/src/native/elf/KernelBuildConfigProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -9,15 +10,12 @@ import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { KernelBuildConfigPort } from "../../application/binaryDiagnostics/KernelBuildConfigPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, } from "../../domain/analysisErrorCore.js"; @@ -136,25 +134,12 @@ const kernelBuildConfigFailurePrimary = ( input: InspectKernelBuildConfigInput, ) => { const { cause, origin } = facts; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -186,17 +171,7 @@ const kernelBuildConfigFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/elf/KernelModuleProvider.ts b/src/native/elf/KernelModuleProvider.ts index 4902614fdf..f13e1beebf 100644 --- a/src/native/elf/KernelModuleProvider.ts +++ b/src/native/elf/KernelModuleProvider.ts @@ -1,7 +1,8 @@ import { + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -12,15 +13,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { KernelModulePort } from "../../application/binaryDiagnostics/KernelModulePort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -136,27 +134,12 @@ const kernelModuleFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -191,17 +174,7 @@ const kernelModuleFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/exports/NativeExportsProvider.ts b/src/native/exports/NativeExportsProvider.ts index 7f6b0d2762..a8efd05bc9 100644 --- a/src/native/exports/NativeExportsProvider.ts +++ b/src/native/exports/NativeExportsProvider.ts @@ -1,8 +1,9 @@ import { imageFileSizeRemediation, projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -14,15 +15,12 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { NativeExportsPort } from "../../application/binaryDiagnostics/NativeExportsPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -93,25 +91,12 @@ const nativeExportsFailurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof BinaryResourceFailure) return new AnalysisResourceConstraintError( OPERATION, @@ -163,11 +148,7 @@ const nativeExportsFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/exports/NativeExportsView.test.ts b/src/native/exports/NativeExportsView.test.ts index a23c434138..6623493d6d 100644 --- a/src/native/exports/NativeExportsView.test.ts +++ b/src/native/exports/NativeExportsView.test.ts @@ -6,10 +6,8 @@ import { } from "../../../tests/fixtures/binaryDiagnostics/nativeExports.js"; import { freezeJsonSnapshot } from "../../domain/immutableJson.js"; import { digestCanonicalValue } from "../../domain/canonicalDigest.js"; -import { - projectAnalysisView, - type AnalysisViewParent, -} from "../../domain/analysisView/analysisView.js"; +import { projectAnalysisView } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewParent } from "../../domain/analysisView/analysisViewParent.js"; import { analysisViewRequestSchema } from "../../domain/analysisView/analysisViewRequest.js"; const artifact = { diff --git a/src/native/hardening/BinaryHardeningProvider.ts b/src/native/hardening/BinaryHardeningProvider.ts index 9e798c3932..d2ac7d2d74 100644 --- a/src/native/hardening/BinaryHardeningProvider.ts +++ b/src/native/hardening/BinaryHardeningProvider.ts @@ -1,7 +1,8 @@ import { imageFileSizeRemediation, + projectOwnedArtifactIdentityFailure, projectSelectedArtifactFileFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectStableArtifactFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; @@ -14,15 +15,12 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { BinaryHardeningPort } from "../../application/binaryDiagnostics/BinaryHardeningPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -139,25 +137,12 @@ const binaryHardeningFailurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof BinaryResourceFailure) return new AnalysisResourceConstraintError( OPERATION, @@ -207,11 +192,7 @@ const binaryHardeningFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/initializers/NativeInitializersProvider.ts b/src/native/initializers/NativeInitializersProvider.ts index db1e4c06c5..8761b9ad2a 100644 --- a/src/native/initializers/NativeInitializersProvider.ts +++ b/src/native/initializers/NativeInitializersProvider.ts @@ -1,14 +1,14 @@ import { imageFileSizeRemediation, projectSelectedArtifactFileFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; import { readStableArtifact } from "../../artifacts/readStableArtifact.js"; import { - artifactFileCancellationCause, artifactFilePrimaryFacts, assertArtifactReadActive, type ArtifactFilePrimaryFacts, @@ -18,8 +18,6 @@ import type { NativeInitializersPort } from "../../application/binaryDiagnostics import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -192,25 +190,12 @@ const failurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( cause instanceof ElfPointerRelocationFailure && cause.reason === "native-index-capacity" @@ -277,11 +262,7 @@ const failurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/pe/PeBinaryFailure.ts b/src/native/pe/PeBinaryFailure.ts index a111f3cdcd..8b0b99c4dd 100644 --- a/src/native/pe/PeBinaryFailure.ts +++ b/src/native/pe/PeBinaryFailure.ts @@ -1,6 +1,6 @@ import { projectSelectedArtifactFileFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectStableArtifactFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -107,11 +107,7 @@ const peBinaryFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - operation, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, operation); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/pe/PeCertificatesProvider.ts b/src/native/pe/PeCertificatesProvider.ts index 67df806ce1..d0eac0fcfe 100644 --- a/src/native/pe/PeCertificatesProvider.ts +++ b/src/native/pe/PeCertificatesProvider.ts @@ -1,8 +1,9 @@ import { BinaryFormatFailure } from "../metadata/BinaryContainer.js"; import { projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js"; @@ -13,15 +14,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { PeCertificatesPort } from "../../application/binaryDiagnostics/PeCertificatesPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -152,27 +150,12 @@ const peCertificatesFailurePrimary = ( { cause }, ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof PeCertificateAsn1Failure && @@ -206,17 +189,7 @@ const peCertificatesFailurePrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/native/pe/PeResourcesProvider.ts b/src/native/pe/PeResourcesProvider.ts index 4de6c1b626..784bab490c 100644 --- a/src/native/pe/PeResourcesProvider.ts +++ b/src/native/pe/PeResourcesProvider.ts @@ -2,8 +2,9 @@ import { BinaryFormatFailure } from "../metadata/BinaryContainer.js"; import { imageFileSizeRemediation, projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; import { fixedNativeAllowance } from "../../domain/native/nativeDeadlines.js"; @@ -15,15 +16,12 @@ import { import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { PeResourcesPort } from "../../application/binaryDiagnostics/PeResourcesPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { ArtifactOperationError } from "../../domain/artifactOperationError.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -116,25 +114,12 @@ const peResourcesFailurePrimary = ( { cause }, ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - operation, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(operation, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + operation, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && @@ -178,17 +163,7 @@ const peResourcesFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - operation, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, operation); } const fileFailure = projectSelectedArtifactFileFailure( diff --git a/src/native/pwntools/PwntoolsDecoder.ts b/src/native/pwntools/PwntoolsDecoder.ts index 574c0bcd2d..378d22b353 100644 --- a/src/native/pwntools/PwntoolsDecoder.ts +++ b/src/native/pwntools/PwntoolsDecoder.ts @@ -323,12 +323,11 @@ export class PwntoolsDecoder< let replyFinalization: AnalysisError | undefined; let phase: "configuration" | "artifact-read" | "decoder" = "configuration"; const executablePath = environment.REA_PWNTOOLS_PYTHON ?? ""; - let selectedPath = executablePath; const failure = (cause: unknown) => pwntoolsDecoderFailure( cause, phase, - selectedPath, + phase === "configuration" ? executablePath : input.path, executablePath, {}, context, @@ -350,20 +349,20 @@ export class PwntoolsDecoder< { userMessage: requirement }, ); } - if (!isAbsolute(selectedPath)) + if (!isAbsolute(executablePath)) throw pwntoolsUnavailable( "Set REA_PWNTOOLS_PYTHON to an absolute caller-supplied Python executable with pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2; REA never installs Python or packages.", - selectedPath, + executablePath, undefined, undefined, context, ); - await access(selectedPath, constants.X_OK); - if (!(await stat(selectedPath)).isFile()) + await access(executablePath, constants.X_OK); + if (!(await stat(executablePath)).isFile()) throw pwntoolsUnavailable( "Selected Python executable must be a regular executable file, not a directory: " + - selectedPath, - selectedPath, + executablePath, + executablePath, undefined, undefined, context, @@ -379,7 +378,7 @@ export class PwntoolsDecoder< if (!inputLimit.configured) throw pwntoolsUnavailable( inputLimit.error, - selectedPath, + executablePath, undefined, undefined, { @@ -393,7 +392,6 @@ export class PwntoolsDecoder< ...resolvePwntoolsLimits(selectedLimits.data, inputLimit.bytes), command_timeout_ms: timeoutMs, }; - selectedPath = input.path; const snapshot = await readStableArtifact( input.path, inputLimit.bytes, @@ -684,7 +682,7 @@ export class PwntoolsDecoder< const primary = pwntoolsDecoderFailure( cause, phase, - selectedPath, + phase === "configuration" ? executablePath : input.path, executablePath, evidence, context, diff --git a/src/native/windows/WindowsShortcutProvider.ts b/src/native/windows/WindowsShortcutProvider.ts index 76a8cde9ff..f51c8a6ab0 100644 --- a/src/native/windows/WindowsShortcutProvider.ts +++ b/src/native/windows/WindowsShortcutProvider.ts @@ -1,6 +1,7 @@ import { lstat, open } from "node:fs/promises"; import { projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, projectArtifactCloseFailure, } from "../../artifacts/StableArtifactFailure.js"; @@ -15,15 +16,12 @@ import { artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, assertArtifactReadActive, - artifactFileCancellationCause, } from "../../artifacts/ArtifactFileOperation.js"; import { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js"; import type { WindowsShortcutPort } from "../../application/binaryDiagnostics/WindowsShortcutPort.js"; import type { ExecutionOptions } from "../../application/AnalysisProvider.js"; import { AnalysisError } from "../../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -281,25 +279,12 @@ const windowsShortcutFailurePrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof WindowsShortcutResourceFailure) return new AnalysisResourceConstraintError( OPERATION, diff --git a/src/python/PythonPycProvider.ts b/src/python/PythonPycProvider.ts index 30b2da8fc6..842715a92d 100644 --- a/src/python/PythonPycProvider.ts +++ b/src/python/PythonPycProvider.ts @@ -1,12 +1,12 @@ import { PYTHON_PYC_PROVIDER_IDENTITY } from "./PythonPycProviderIdentity.js"; import { projectSelectedArtifactFileFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../artifacts/StableArtifactFailure.js"; import { - artifactFileCancellationCause, artifactFilePrimaryFacts, type ArtifactFilePrimaryFacts, } from "../artifacts/ArtifactFileOperation.js"; @@ -20,8 +20,6 @@ import type { PythonPycPort } from "../application/python/PythonPycPort.js"; import type { ExecutionOptions } from "../application/AnalysisProvider.js"; import { AnalysisError } from "../domain/analysisErrorBase.js"; import { - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -71,25 +69,12 @@ const failureForPrimary = ( const { cause, origin } = facts; if (origin === "owned-or-classified" && cause instanceof AnalysisError) return cause; - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { cause }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if (cause instanceof PythonPycResourceFailure) return new AnalysisResourceConstraintError( OPERATION, @@ -129,17 +114,7 @@ const failureForPrimary = ( }, ); if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } const fileFailure = projectSelectedArtifactFileFailure( cause, diff --git a/src/rust/RustBinaryProvider.ts b/src/rust/RustBinaryProvider.ts index 08a33cb150..3bf7347783 100644 --- a/src/rust/RustBinaryProvider.ts +++ b/src/rust/RustBinaryProvider.ts @@ -2,7 +2,7 @@ import { RUST_BINARY_PROVIDER_IDENTITY } from "./RustBinaryProviderIdentity.js"; import { projectSelectedArtifactFileFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, projectArtifactCloseFailure, } from "../artifacts/StableArtifactFailure.js"; import { @@ -436,11 +436,7 @@ const artifactReaderFailure = ( }, ); if (cause.reason === "path") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [{ path: ["path"], reason: "invalid_format", message: cause.message }], - ); + return stableArtifactInputFailure(cause, OPERATION); return new ProviderAdapterError(RUST_BINARY_PROVIDER_IDENTITY.id, OPERATION, { cause, diagnostics: { path, reason: cause.message }, diff --git a/src/server/contractToolHandler.ts b/src/server/contractToolHandler.ts index 6662ca77bd..c9bdedf869 100644 --- a/src/server/contractToolHandler.ts +++ b/src/server/contractToolHandler.ts @@ -17,7 +17,7 @@ import { requireAnalysisViewRetention, type AnalysisViewRetention, } from "../application/analysisView/AnalysisViewService.js"; -import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js"; import type { EvidenceWriter } from "../application/investigation/InvestigationRecordPort.js"; import type { ToolContract } from "../contracts/toolContractTypes.js"; import type { AnalysisError } from "../domain/analysisErrorBase.js"; diff --git a/src/server/registerApplicationTools/helpers.ts b/src/server/registerApplicationTools/helpers.ts index 9313a45220..dbe2419b54 100644 --- a/src/server/registerApplicationTools/helpers.ts +++ b/src/server/registerApplicationTools/helpers.ts @@ -5,7 +5,7 @@ import { APPLICATION_TOOL_CONTRACTS } from "../../contracts/applicationToolContr import { inspectAnalysisViewValidated } from "../../application/analysisView/AnalysisViewService.js"; import { recordEvidenceSources } from "../../application/investigation/recordEvidenceSources.js"; import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js"; -import type { AnalysisViewRequest } from "../../domain/analysisView/analysisView.js"; +import type { AnalysisViewRequest } from "../../domain/analysisView/analysisViewRequest.js"; import type { Evidence } from "../../domain/evidence.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; import { ok } from "../../domain/result.js"; diff --git a/src/server/toolResult.ts b/src/server/toolResult.ts index 459b051776..90933004d1 100644 --- a/src/server/toolResult.ts +++ b/src/server/toolResult.ts @@ -3,7 +3,7 @@ import { isNativeInventoryOperation } from "../domain/native/nativeInventory.js" import { STDIO_DEFAULT_MAX_BUFFER_SIZE } from "@modelcontextprotocol/server"; import { ToolResultDelivery } from "./ToolResultDelivery.js"; import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../domain/analysisView/analysisViewErrors.js"; -import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisViewRequest.js"; import { smallerBatchDecompileSelection } from "../domain/analysisView/batchDecompileView.js"; import type { Evidence } from "../domain/evidence.js"; import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js"; diff --git a/src/sqlite/SqliteDatabaseFailures.ts b/src/sqlite/SqliteDatabaseFailures.ts index da6220b6e4..b6e0f9e917 100644 --- a/src/sqlite/SqliteDatabaseFailures.ts +++ b/src/sqlite/SqliteDatabaseFailures.ts @@ -1,5 +1,5 @@ import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js"; -import { stableArtifactInputRecovery } from "../artifacts/StableArtifactFailure.js"; +import { stableArtifactInputFailure } from "../artifacts/StableArtifactFailure.js"; import { ArtifactFileCleanupFailure, bindArtifactFileCleanup, @@ -169,17 +169,7 @@ export const sqliteDatabaseFailure = ( return new AnalysisCancelledError(operation, { cause: artifactFileCancellationCause(cause), }); - return new AnalysisInputError( - operation, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, operation); } if (cause instanceof Error && cause.name === "AbortError") return new AnalysisCancelledError(operation, { diff --git a/src/v8/V8CodeCacheProvider.ts b/src/v8/V8CodeCacheProvider.ts index 37c60c8551..20622f3d0d 100644 --- a/src/v8/V8CodeCacheProvider.ts +++ b/src/v8/V8CodeCacheProvider.ts @@ -5,22 +5,20 @@ import { readStableArtifact, } from "../artifacts/readStableArtifact.js"; import { - artifactFileCancellationCause, artifactFilePrimaryFacts, artifactSelectedPathErrno, type ArtifactFilePrimaryFacts, } from "../artifacts/ArtifactFileOperation.js"; import { projectArtifactCloseFailure, + projectOwnedArtifactIdentityFailure, projectStableArtifactFailure, - stableArtifactInputRecovery, + stableArtifactInputFailure, } from "../artifacts/StableArtifactFailure.js"; import type { ExecutionOptions } from "../application/AnalysisProvider.js"; import type { V8CodeCachePort } from "../application/v8CodeCache/V8CodeCachePort.js"; import { AnalysisAccessDeniedError, - AnalysisArtifactChangedError, - AnalysisCancelledError, AnalysisInputError, AnalysisResourceConstraintError, AnalysisUnsupportedTargetError, @@ -200,27 +198,12 @@ const v8CodeCacheFailurePrimary = ( message: cause.message, }, ]); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "integrity" - ) - return new AnalysisArtifactChangedError( - OPERATION, - input.path, - cause.message, - { - cause, - }, - ); - if ( - origin === "owned-or-classified" && - cause instanceof ArtifactReaderFailure && - cause.reason === "cancelled" - ) - return new AnalysisCancelledError(OPERATION, { - cause: artifactFileCancellationCause(cause), - }); + const identityFailure = projectOwnedArtifactIdentityFailure( + facts, + OPERATION, + input.path, + ); + if (identityFailure !== undefined) return identityFailure; if ( origin === "owned-or-classified" && cause instanceof ArtifactReaderFailure @@ -244,17 +227,7 @@ const v8CodeCacheFailurePrimary = ( ); } if (cause.reason === "path" || cause.reason === "format") - return new AnalysisInputError( - OPERATION, - { cause, ...stableArtifactInputRecovery(cause) }, - [ - { - path: ["path"], - reason: "invalid_format", - message: cause.message, - }, - ], - ); + return stableArtifactInputFailure(cause, OPERATION); } if (cause instanceof Error && "code" in cause) { if (cause.code === "EACCES" || cause.code === "EPERM") diff --git a/tests/acceptance/analysisViewCallerPath.test.ts b/tests/acceptance/analysisViewCallerPath.test.ts index ef19e38abf..20a06db34b 100644 --- a/tests/acceptance/analysisViewCallerPath.test.ts +++ b/tests/acceptance/analysisViewCallerPath.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { toolContract } from "../../src/contracts/toolContracts.js"; import { parseEvidence } from "../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { analysisViewJavaScriptEvidence, analysisViewLayoutEvidence, diff --git a/tests/acceptance/apkSignatureLimitsCliMcp.test.ts b/tests/acceptance/apkSignatureLimitsCliMcp.test.ts index ef85387154..8537dad4e1 100644 --- a/tests/acceptance/apkSignatureLimitsCliMcp.test.ts +++ b/tests/acceptance/apkSignatureLimitsCliMcp.test.ts @@ -5,7 +5,7 @@ import { expect } from "vitest"; import { parseEvidence } from "../../src/domain/evidence.js"; import { analysisErrorProjectionSchema } from "../../src/contracts/errorSchemas.js"; -import { apkSignatureSummaryViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { apkSignatureSummaryViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { parseMcpToolError } from "../fixtures/mcpToolError.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; diff --git a/tests/acceptance/apkSignatureViewsCliMcp.test.ts b/tests/acceptance/apkSignatureViewsCliMcp.test.ts index ba60352800..cc508281a7 100644 --- a/tests/acceptance/apkSignatureViewsCliMcp.test.ts +++ b/tests/acceptance/apkSignatureViewsCliMcp.test.ts @@ -6,7 +6,7 @@ import { parseEvidence } from "../../src/domain/evidence.js"; import { apkSignaturePageViewResultSchema, apkSignatureSummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { apkDigestRowSchema, type apkSignatureViewRequestSchema, diff --git a/tests/acceptance/applications/apiContract.test.ts b/tests/acceptance/applications/apiContract.test.ts index 0302a05e2d..d5b69828cb 100644 --- a/tests/acceptance/applications/apiContract.test.ts +++ b/tests/acceptance/applications/apiContract.test.ts @@ -12,7 +12,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; import { apiContractSchema } from "../../../src/domain/apiContract/apiContract.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; type McpCall = Awaited>["call"]; const fixture = new URL( diff --git a/tests/acceptance/applications/erlangBeamViews.test.ts b/tests/acceptance/applications/erlangBeamViews.test.ts index 02bb9ea33a..2f70934a90 100644 --- a/tests/acceptance/applications/erlangBeamViews.test.ts +++ b/tests/acceptance/applications/erlangBeamViews.test.ts @@ -7,7 +7,7 @@ import { toolContract } from "../../../src/contracts/toolContracts.js"; import { erlangBeamSchema } from "../../../src/domain/erlang/beam.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { erlangBeamFixture, erlangBeamChunksFixture, diff --git a/tests/acceptance/applications/graphqlContract.test.ts b/tests/acceptance/applications/graphqlContract.test.ts index cc21b4c963..0813b95bb7 100644 --- a/tests/acceptance/applications/graphqlContract.test.ts +++ b/tests/acceptance/applications/graphqlContract.test.ts @@ -11,7 +11,7 @@ import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; import { graphqlContractSchema } from "../../../src/domain/apiContract/graphqlContract.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; const fixture = new URL( "../../fixtures/graphql/swapi.graphql.txt", diff --git a/tests/acceptance/applications/javascriptContentDeduplicatedModulePaths.test.ts b/tests/acceptance/applications/javascriptContentDeduplicatedModulePaths.test.ts index ae95a675b2..6d67e7e740 100644 --- a/tests/acceptance/applications/javascriptContentDeduplicatedModulePaths.test.ts +++ b/tests/acceptance/applications/javascriptContentDeduplicatedModulePaths.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; diff --git a/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts b/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts index f496b35277..42ab01bb52 100644 --- a/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts +++ b/tests/acceptance/applications/javascriptExportBindingModuleView.test.ts @@ -3,7 +3,7 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; diff --git a/tests/acceptance/applications/javascriptWindowOptionsCli.test.ts b/tests/acceptance/applications/javascriptWindowOptionsCli.test.ts index 2024250681..847c77693d 100644 --- a/tests/acceptance/applications/javascriptWindowOptionsCli.test.ts +++ b/tests/acceptance/applications/javascriptWindowOptionsCli.test.ts @@ -5,7 +5,7 @@ import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { writeFixtureFiles } from "../../support/javascriptApplicationFixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; cliTest( "retains constant window preferences and inferred source provenance through CLI, MCP and fact views", diff --git a/tests/acceptance/applications/nativeExportsViews.test.ts b/tests/acceptance/applications/nativeExportsViews.test.ts index d249d36a9c..ed828cca72 100644 --- a/tests/acceptance/applications/nativeExportsViews.test.ts +++ b/tests/acceptance/applications/nativeExportsViews.test.ts @@ -6,7 +6,7 @@ import { readEvidenceBundle } from "../../../src/application/EvidenceBundleFiles import { toolContract } from "../../../src/contracts/toolContracts.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { elfExportImage, diff --git a/tests/acceptance/applications/playwrightTrace.test.ts b/tests/acceptance/applications/playwrightTrace.test.ts index 10a5b21e82..9c3b72ae25 100644 --- a/tests/acceptance/applications/playwrightTrace.test.ts +++ b/tests/acceptance/applications/playwrightTrace.test.ts @@ -12,7 +12,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { playwrightTraceSchema } from "../../../src/domain/playwrightTrace/playwrightTrace.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; type McpCall = Awaited>["call"]; diff --git a/tests/acceptance/applications/progressiveAnalysisViews.test.ts b/tests/acceptance/applications/progressiveAnalysisViews.test.ts index a6c84d772c..e217fb5c3e 100644 --- a/tests/acceptance/applications/progressiveAnalysisViews.test.ts +++ b/tests/acceptance/applications/progressiveAnalysisViews.test.ts @@ -6,7 +6,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { evidenceBundleSchema } from "../../../src/domain/evidenceBundle.js"; import { apiContractSchema } from "../../../src/domain/apiContract/apiContract.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { toolOutputSchemaWithMetadata } from "../../../src/contracts/toolSchemaMetadata.js"; diff --git a/tests/acceptance/applications/pythonPycViews.test.ts b/tests/acceptance/applications/pythonPycViews.test.ts index 486420d166..aaf094c9b0 100644 --- a/tests/acceptance/applications/pythonPycViews.test.ts +++ b/tests/acceptance/applications/pythonPycViews.test.ts @@ -7,7 +7,7 @@ import { toolContract } from "../../../src/contracts/toolContracts.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; import { pythonPycSchema } from "../../../src/domain/python/pythonPyc.js"; import { digestCanonicalValue } from "../../../src/domain/canonicalDigest.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { marshalCode, pythonPycHeader, diff --git a/tests/acceptance/applications/staticObservedEndpoints.test.ts b/tests/acceptance/applications/staticObservedEndpoints.test.ts index b5581fc496..9905ce3b2a 100644 --- a/tests/acceptance/applications/staticObservedEndpoints.test.ts +++ b/tests/acceptance/applications/staticObservedEndpoints.test.ts @@ -20,7 +20,7 @@ import { staticObservedEndpointComparisonSchema, type StaticObservedEndpointComparison, } from "../../../src/domain/javascript/staticObservedEndpointSchemas.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js"; const prepareInputs = async (cli: TestCli) => { diff --git a/tests/acceptance/artifactComparisonEspImage.test.ts b/tests/acceptance/artifactComparisonEspImage.test.ts index ee566a1c75..2c297871b8 100644 --- a/tests/acceptance/artifactComparisonEspImage.test.ts +++ b/tests/acceptance/artifactComparisonEspImage.test.ts @@ -3,7 +3,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { ESP32C3_APP_SEGMENTS, espImage } from "../fixtures/espImage.js"; -import { artifactInventorySummaryViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { artifactComparisonResultSchema } from "../../src/domain/artifactComparison.js"; import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js"; import { artifactInventoryResultSchema } from "../../src/domain/artifactGraph.js"; diff --git a/tests/acceptance/artifactComparisonHistoricalSummary.test.ts b/tests/acceptance/artifactComparisonHistoricalSummary.test.ts index a86c668946..f3860482a3 100644 --- a/tests/acceptance/artifactComparisonHistoricalSummary.test.ts +++ b/tests/acceptance/artifactComparisonHistoricalSummary.test.ts @@ -2,10 +2,8 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect, it } from "vitest"; -import { - artifactInventorySummaryViewResultSchema, - sealAnalysisView, -} from "../../src/domain/analysisView/analysisView.js"; +import { sealAnalysisView } from "../../src/domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { artifactComparisonResultSchema } from "../../src/domain/artifactComparison.js"; import { digestCanonicalValue } from "../../src/domain/canonicalDigest.js"; import { diff --git a/tests/acceptance/dotnetBundleJourney.test.ts b/tests/acceptance/dotnetBundleJourney.test.ts index f448357943..2e946e94bf 100644 --- a/tests/acceptance/dotnetBundleJourney.test.ts +++ b/tests/acceptance/dotnetBundleJourney.test.ts @@ -14,7 +14,7 @@ import { parseEvidence } from "../../src/domain/evidence.js"; import { analysisViewResultSchema, artifactInventorySummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { workspaceCliTest as test } from "../support/cli/workspaceCliFixture.js"; import { createMcpSchemaValidation } from "../fixtures/mcpSchemaValidation.js"; diff --git a/tests/acceptance/godotPck.test.ts b/tests/acceptance/godotPck.test.ts index bb0d56e9f9..32c4a030af 100644 --- a/tests/acceptance/godotPck.test.ts +++ b/tests/acceptance/godotPck.test.ts @@ -12,7 +12,7 @@ import { } from "../fixtures/godotPck.js"; import { elf } from "../../src/domain/binaryTarget.fixture.js"; import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js"; -import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../src/domain/evidence.js"; import { artifactInspectionResultSchema } from "../../src/domain/artifactInspection.js"; import { diff --git a/tests/acceptance/gpuCodeObjectsCli.test.ts b/tests/acceptance/gpuCodeObjectsCli.test.ts index 2967daf08f..c713630779 100644 --- a/tests/acceptance/gpuCodeObjectsCli.test.ts +++ b/tests/acceptance/gpuCodeObjectsCli.test.ts @@ -8,7 +8,7 @@ import { gpuCodeObjectsSchema } from "../../src/domain/gpu/gpuCodeObjects.js"; import { gpuCodeObjectsPageViewResultSchema, gpuCodeObjectsSummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence, createEvidence } from "../../src/domain/evidence.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; diff --git a/tests/acceptance/il2cppMetadataCli.test.ts b/tests/acceptance/il2cppMetadataCli.test.ts index a90187d1ed..9e162c60cd 100644 --- a/tests/acceptance/il2cppMetadataCli.test.ts +++ b/tests/acceptance/il2cppMetadataCli.test.ts @@ -15,7 +15,7 @@ import { toolContract } from "../../src/contracts/toolContracts.js"; import { analysisViewResultSchema, il2cppMetadataSummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence } from "../../src/domain/evidence.js"; import { createEvidenceBundle, diff --git a/tests/acceptance/inesContainer.test.ts b/tests/acceptance/inesContainer.test.ts index 589d4d63cc..74e82be581 100644 --- a/tests/acceptance/inesContainer.test.ts +++ b/tests/acceptance/inesContainer.test.ts @@ -7,7 +7,7 @@ import { artifactInventoryResultSchema, artifactExtractionResultSchema, } from "../../src/domain/artifactGraph.js"; -import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../src/domain/evidence.js"; import { cliTest } from "../support/cli/cliFixture.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; diff --git a/tests/acceptance/javascriptRoutesCli.test.ts b/tests/acceptance/javascriptRoutesCli.test.ts index 786ff5253c..dfb744b66e 100644 --- a/tests/acceptance/javascriptRoutesCli.test.ts +++ b/tests/acceptance/javascriptRoutesCli.test.ts @@ -7,7 +7,7 @@ import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest } from "../support/cli/cliFixture.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; import { parseEvidence } from "../../src/domain/evidence.js"; -import { javaScriptRoutesViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { javaScriptRoutesViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; const routeViewFromEvidence = (evidence: unknown) => javaScriptRoutesViewResultSchema.parse( diff --git a/tests/acceptance/managedMemberRecovery.test.ts b/tests/acceptance/managedMemberRecovery.test.ts index 88d31f562d..904039c4b0 100644 --- a/tests/acceptance/managedMemberRecovery.test.ts +++ b/tests/acceptance/managedMemberRecovery.test.ts @@ -13,7 +13,7 @@ import { managedMemberInspectionSchema } from "../../src/domain/managed/managedA import { analysisViewResultSchema, managedMemberSummaryViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { buildManagedMemberCapacityFixture } from "../fixtures/managedMemberCapacity.js"; import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; import { cliTest } from "../support/cli/cliFixture.js"; diff --git a/tests/acceptance/modelArtifactCli.test.ts b/tests/acceptance/modelArtifactCli.test.ts index d6d39fc5fd..fcf58c8c25 100644 --- a/tests/acceptance/modelArtifactCli.test.ts +++ b/tests/acceptance/modelArtifactCli.test.ts @@ -10,7 +10,7 @@ import { import { modelArtifactSummaryViewResultSchema, analysisViewResultSchema, -} from "../../src/domain/analysisView/analysisView.js"; +} from "../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence } from "../../src/domain/evidence.js"; import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; diff --git a/tests/acceptance/rustDependencyViews.test.ts b/tests/acceptance/rustDependencyViews.test.ts index 612d5d2aff..a5f68bda09 100644 --- a/tests/acceptance/rustDependencyViews.test.ts +++ b/tests/acceptance/rustDependencyViews.test.ts @@ -4,7 +4,7 @@ import { expect } from "vitest"; import { parseEvidence, createEvidence } from "../../src/domain/evidence.js"; import { createEvidenceBundle } from "../../src/domain/evidenceBundle.js"; import { rustBinarySchema } from "../../src/domain/rust/rustBinary.js"; -import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../src/domain/analysisView/analysisViewResult.js"; import { readEvidenceBundle } from "../../src/application/EvidenceBundleFiles.js"; import { connectLocalToolsMcp } from "../fixtures/localToolsMcp.js"; import { parseMcpToolError } from "../fixtures/mcpToolError.js"; diff --git a/tests/acceptance/tools/protobufCapture.test.ts b/tests/acceptance/tools/protobufCapture.test.ts index c161b36352..f072d19179 100644 --- a/tests/acceptance/tools/protobufCapture.test.ts +++ b/tests/acceptance/tools/protobufCapture.test.ts @@ -24,7 +24,7 @@ import { inspectWebNetworkCaptureInputSchema, } from "../../../src/domain/webNetworkCapture.js"; import { readEvidenceBundle } from "../../../src/application/EvidenceBundleFiles.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { connectLocalToolsMcp } from "../../fixtures/localToolsMcp.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; diff --git a/tests/acceptance/tools/protobufDescriptorFiles.test.ts b/tests/acceptance/tools/protobufDescriptorFiles.test.ts index f37f1fead7..b65b98ea3a 100644 --- a/tests/acceptance/tools/protobufDescriptorFiles.test.ts +++ b/tests/acceptance/tools/protobufDescriptorFiles.test.ts @@ -13,7 +13,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { z } from "zod"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js"; import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; diff --git a/tests/acceptance/tools/protobufDescriptorIdentity.test.ts b/tests/acceptance/tools/protobufDescriptorIdentity.test.ts index d57476ad7e..0fb0fc5b99 100644 --- a/tests/acceptance/tools/protobufDescriptorIdentity.test.ts +++ b/tests/acceptance/tools/protobufDescriptorIdentity.test.ts @@ -10,7 +10,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { z } from "zod"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { webNetworkCaptureSchema } from "../../../src/domain/webNetworkCapture.js"; import { protobufHar } from "../../fixtures/protobufCapture.js"; diff --git a/tests/acceptance/tools/qlogCapture.test.ts b/tests/acceptance/tools/qlogCapture.test.ts index 4bda480872..eea8eedca1 100644 --- a/tests/acceptance/tools/qlogCapture.test.ts +++ b/tests/acceptance/tools/qlogCapture.test.ts @@ -8,7 +8,7 @@ import { CAPTURE_DECODER_HAS_MEMORY_BOUND as hasMemoryBound, CAPTURE_RUNTIME_SUPPORTED as runtimeSupported, } from "../../fixtures/captureDecoderLimits.js"; -import { webNetworkCaptureSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { webNetworkCaptureSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { webNetworkCaptureSchema, diff --git a/tests/acceptance/tools/unavailableNativeCollectionViews.test.ts b/tests/acceptance/tools/unavailableNativeCollectionViews.test.ts index 245048f02e..c44d47d826 100644 --- a/tests/acceptance/tools/unavailableNativeCollectionViews.test.ts +++ b/tests/acceptance/tools/unavailableNativeCollectionViews.test.ts @@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { nativeLoadImageObservationFixture, diff --git a/tests/boundary/browser/webSourceMapHttpStatus.test.ts b/tests/boundary/browser/webSourceMapHttpStatus.test.ts index 13ec2a408e..9339acddab 100644 --- a/tests/boundary/browser/webSourceMapHttpStatus.test.ts +++ b/tests/boundary/browser/webSourceMapHttpStatus.test.ts @@ -9,7 +9,7 @@ import { analyzeWebBundleInputSchema, webSourceMapsSchema, } from "../../../src/domain/webBundleAnalysis.js"; -import { WEB_SOURCE_MAP_LIMITS } from "../../../src/domain/webSourceLocation.js"; +import { WEB_SOURCE_MAP_LIMITS } from "../../../src/domain/sourceMapLimits.js"; it.each([ { status: 300, followsRedirect: false }, diff --git a/tests/boundary/cli/electronWrappedCallPaths.test.ts b/tests/boundary/cli/electronWrappedCallPaths.test.ts index 40909faeb3..943246ac6c 100644 --- a/tests/boundary/cli/electronWrappedCallPaths.test.ts +++ b/tests/boundary/cli/electronWrappedCallPaths.test.ts @@ -14,7 +14,7 @@ import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplicat import { analysisViewResultSchema, javascriptApplicationSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; const sourcePositionSchema = z.object({ line: z.number(), column: z.number() }); const callPathConditionSchema = z.object({ diff --git a/tests/boundary/cli/fridaStatusViewCli.test.ts b/tests/boundary/cli/fridaStatusViewCli.test.ts index 67fb617c5f..501367d585 100644 --- a/tests/boundary/cli/fridaStatusViewCli.test.ts +++ b/tests/boundary/cli/fridaStatusViewCli.test.ts @@ -2,7 +2,7 @@ import { writeFile } from "node:fs/promises"; import { join } from "node:path"; import { expect } from "vitest"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { cliTest } from "../../support/cli/cliFixture.js"; diff --git a/tests/boundary/cli/managedMemberSummaryCli.test.ts b/tests/boundary/cli/managedMemberSummaryCli.test.ts index 5f74d5118d..8a571c1438 100644 --- a/tests/boundary/cli/managedMemberSummaryCli.test.ts +++ b/tests/boundary/cli/managedMemberSummaryCli.test.ts @@ -19,7 +19,7 @@ import { import { analysisViewResultSchema, managedMemberSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { parseEvidenceBundle } from "../../../src/domain/evidenceBundle.js"; import { diff --git a/tests/boundary/filesystem/ciChangeScope.test.ts b/tests/boundary/filesystem/ciChangeScope.test.ts index 01bec4bd68..425baf87e2 100644 --- a/tests/boundary/filesystem/ciChangeScope.test.ts +++ b/tests/boundary/filesystem/ciChangeScope.test.ts @@ -106,7 +106,7 @@ async function classify( it.each([ ["bridge/native/rea_lldb_tracer.py", "python"], ["bridge/pwndbg/test_core_context.py", "python"], - ["tests/support/analysis-efficiency/test_signal_oracle.py", "python"], + ["tests/support/analysis-efficiency/test_catalog_eval.py", "python"], ["scripts/run-python-tests.py", "python"], ["scripts/verify-browser-request.mjs", "browser"], ["scripts/lib/browser-request-e2e.mjs", "browser"], diff --git a/tests/boundary/filesystem/hermesClientConfiguration.test.ts b/tests/boundary/filesystem/hermesClientConfiguration.test.ts index 19aa865c92..3ab316f37c 100644 --- a/tests/boundary/filesystem/hermesClientConfiguration.test.ts +++ b/tests/boundary/filesystem/hermesClientConfiguration.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { userInfo } from "node:os"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { parse as parseYaml } from "yaml"; +import { isMap, parse as parseYaml, parseDocument } from "yaml"; import { readClientRegistrationStatuses } from "../../../src/application/ClientRegistrationStatus.js"; @@ -42,7 +42,257 @@ const hermesPath = ( ({ name }) => name === "hermes", ); +describe("Hermes registration source retention", () => { + it("round-trips setup and removal while other servers remain inherited", async () => { + const home = await createTestTempDirectory( + "rea-hermes-inherited-lifecycle-", + ); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "shared: &shared\n mcp_servers:\n other: {command: other-server}\n<<: *shared\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, {}, command), + ).toMatchObject({ + status: "configured", + }); + expect( + await configureClientConfiguration(client, {}, command), + ).toMatchObject({ + status: "unchanged", + }); + expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({ + status: "removed", + }); + const written = await readFile(path, "utf8"); + const document = parseYaml(written, { version: "1.1" }) as { + mcp_servers: Record; + }; + expect(document.mcp_servers).toEqual({ + other: { command: "other-server" }, + }); + expect( + parseYaml(written.replace("other-server", "new-server"), { + version: "1.1", + }), + ).toMatchObject({ + mcp_servers: { other: { command: "new-server" } }, + }); + }); + + it("keeps other servers inherited when setup updates REA", async () => { + const home = await createTestTempDirectory("rea-hermes-inherited-setup-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "shared: &shared\n mcp_servers:\n rea: {command: rea, args: [mcp]}\n other: {command: other-server}\n<<: *shared\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const written = await readFile(path, "utf8"); + expect(parseYaml(written, { version: "1.1" })).toMatchObject({ + mcp_servers: { + rea: { env: { CUSTOM: "value" } }, + other: { command: "other-server" }, + }, + }); + expect( + parseYaml(written.replace("other-server", "new-server"), { + version: "1.1", + }), + ).toMatchObject({ mcp_servers: { other: { command: "new-server" } } }); + expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original); + }); +}); + +describe("Hermes inherited registration removal", () => { + it("does not remove an override if that would reactivate an inherited registration", async () => { + const home = await createTestTempDirectory( + "rea-hermes-inherited-override-", + ); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "shared: &shared\n mcp_servers:\n rea: {command: rea, args: [mcp]}\n other: {command: other-server}\n<<: *shared\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const configured = await readFile(path, "utf8"); + expect(await systemUninstallHost(home).removeClient(client)).toMatchObject({ + status: "failed", + detail: expect.stringContaining("inherited from a YAML merge"), + }); + expect(await readFile(path, "utf8")).toBe(configured); + }); + + it.each([ + "shared: &shared\n rea: {command: rea, args: [mcp]}\nmcp_servers:\n <<: *shared\n", + "shared: &shared\n mcp_servers:\n rea: {command: rea, args: [mcp]}\n<<: *shared\n", + ])( + "reports an inherited registration without writing its shared source", + async (original) => { + const home = await createTestTempDirectory( + "rea-hermes-inherited-server-", + ); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await systemUninstallHost(home).removeClient(client), + ).toMatchObject({ + status: "failed", + detail: expect.stringContaining("inherited from a YAML merge"), + }); + expect(await readFile(path, "utf8")).toBe(original); + await expect(readFile(`${path}.rea.backup`, "utf8")).rejects.toThrow(); + }, + ); + + it("updates a registration selected through an aliased YAML key", async () => { + const home = await createTestTempDirectory("rea-hermes-key-alias-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "server_name: &server_name rea\nmcp_servers:\n ? *server_name\n :\n command: rea\n args: [mcp]\n marker: retained\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const written = await readFile(path, "utf8"); + expect(written).toContain("*server_name"); + const servers = parseDocument(written, { version: "1.1" }).get( + "mcp_servers", + true, + ); + expect(isMap(servers) && servers.items).toHaveLength(1); + expect(parseYaml(written, { version: "1.1" })).toMatchObject({ + mcp_servers: { rea: { marker: "retained", env: { CUSTOM: "value" } } }, + }); + }); + + it("retains inherited registration settings through setup", async () => { + const home = await createTestTempDirectory("rea-hermes-merged-fields-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "defaults: &defaults\n metadata: {marker: old}\nmcp_servers:\n rea:\n <<: *defaults\n command: rea\n args: [mcp]\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const written = await readFile(path, "utf8"); + expect(written).toContain("<<: *defaults"); + const changed = written.replace("marker: old", "marker: new"); + expect(parseYaml(changed, { version: "1.1" })).toMatchObject({ + mcp_servers: { rea: { metadata: { marker: "new" } } }, + }); + expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original); + }); + + it("preserves distinct keys beside a changed launcher field", async () => { + const home = await createTestTempDirectory("rea-hermes-mixed-fields-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + 'mcp_servers:\n rea:\n command: rea\n args: [mcp]\n 1: numeric\n "1": string\n'; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, { CUSTOM: "value" }, command), + ).toMatchObject({ status: "configured" }); + const written = await readFile(path, "utf8"); + expect(written).toContain(' 1: numeric\n "1": string'); + }); + + it.each([ + ' 1: numeric\n "1": string', + " 2026-10-14T00:00:00.123456Z: first\n 2026-10-14T00:00:00.123457Z: second", + ])( + "retains distinct YAML keys in an existing registration: %s", + async (keys) => { + const home = await createTestTempDirectory("rea-hermes-mixed-keys-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = `mcp_servers:\n rea:\n command: rea\n args: [mcp]\n metadata:\n${keys}\n`; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + const result = await configureClientConfiguration( + client, + { CUSTOM: "value" }, + command, + ); + expect(result.status, JSON.stringify(result)).toBe("configured"); + const written = await readFile(path, "utf8"); + expect(written).toContain(keys); + expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original); + }, + ); +}); + describe("Hermes configuration paths", () => { + it("preserves unrelated distinct timestamp keys during registration", async () => { + const home = await createTestTempDirectory("rea-hermes-timestamp-keys-"); + const directory = join(home, ".hermes"); + await mkdir(directory); + const path = join(directory, "config.yaml"); + const original = + "history:\n 2026-10-14T00:00:00.123456Z: first\n 2026-10-14T00:00:00.123457Z: second\n"; + await writeFile(path, original); + const client = supportedClients(home, process.platform, { + HERMES_HOME: directory, + }).find(({ name }) => name === "hermes"); + if (client === undefined) throw new Error("missing Hermes client"); + expect( + await configureClientConfiguration(client, {}, command), + ).toMatchObject({ + status: "configured", + }); + const written = await readFile(path, "utf8"); + expect(written).toContain(original.trimEnd()); + expect(await readFile(`${path}.rea.backup`, "utf8")).toBe(original); + }); + it.skipIf(process.platform === "win32")( "configures the filesystem-selected Hermes home through symlink parent traversal", async () => { diff --git a/tests/boundary/filesystem/kimiClientConfiguration.test.ts b/tests/boundary/filesystem/kimiClientConfiguration.test.ts index 2602171063..925e5212ad 100644 --- a/tests/boundary/filesystem/kimiClientConfiguration.test.ts +++ b/tests/boundary/filesystem/kimiClientConfiguration.test.ts @@ -474,15 +474,18 @@ describe("Kimi Code transport validation", () => { }) ).find(({ client: name }) => name === "kimi"), ).toMatchObject({ state: "aligned", command }); + // The explicit selector already chooses stdio, so setup keeps it and + // has nothing to rewrite. + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); + expect(await configureClientConfiguration(client, {}, command)).toEqual({ + status: "unchanged", + }); expect( - (await configureClientConfiguration(client, {}, command)).status, - ).toBe("configured"); - expect(await configureClientConfiguration(client, {}, command)).toEqual({ - status: "unchanged", - }); - expect(await configureClientConfiguration(client, {}, command)).toEqual({ - status: "unchanged", - }); + JSON.parse(await readFile(client.configPath, "utf8")).mcpServers.rea + .transport, + ).toBe("stdio"); expect( ( await readClientRegistrationStatuses(home, undefined, { diff --git a/tests/boundary/filesystem/webSourceLocationArtifacts.test.ts b/tests/boundary/filesystem/webSourceLocationArtifacts.test.ts index c443a88aca..cc90bebce4 100644 --- a/tests/boundary/filesystem/webSourceLocationArtifacts.test.ts +++ b/tests/boundary/filesystem/webSourceLocationArtifacts.test.ts @@ -5,7 +5,7 @@ import { expect, it } from "vitest"; import { LocalWebSourceLocationArtifacts } from "../../../src/browser/assets/WebSourceLocationArtifacts.js"; import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js"; import { ok } from "../../../src/domain/result.js"; -import { WEB_SOURCE_MAP_LIMITS } from "../../../src/domain/webSourceLocation.js"; +import { WEB_SOURCE_MAP_LIMITS } from "../../../src/domain/sourceMapLimits.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { webSourceLocationArgs, diff --git a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts index f49bee07cb..273163d6dd 100644 --- a/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts +++ b/tests/boundary/mcp/analysisSummaryDetailMcp.test.ts @@ -1,6 +1,6 @@ import { cliTest } from "../../support/cli/cliFixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { byteEntropySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { byteEntropySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { writeFile } from "node:fs/promises"; import { join } from "node:path"; diff --git a/tests/boundary/mcp/analysisViewMcp.test.ts b/tests/boundary/mcp/analysisViewMcp.test.ts index fe2e4f4d08..dd6561c597 100644 --- a/tests/boundary/mcp/analysisViewMcp.test.ts +++ b/tests/boundary/mcp/analysisViewMcp.test.ts @@ -59,7 +59,7 @@ import { import { analysisViewResultSchema, il2cppMetadataSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { BINARY_LAYOUT_TEST_PROVIDER, binaryLayoutFixture, diff --git a/tests/boundary/mcp/androidPackageSummaryMcp.test.ts b/tests/boundary/mcp/androidPackageSummaryMcp.test.ts index b9eb64f367..9503e75870 100644 --- a/tests/boundary/mcp/androidPackageSummaryMcp.test.ts +++ b/tests/boundary/mcp/androidPackageSummaryMcp.test.ts @@ -21,7 +21,7 @@ import { androidJniSummaryViewResultSchema, androidJniPageViewResultSchema, analysisViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence, diff --git a/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts b/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts index aa1f277ff6..2c19a30ad7 100644 --- a/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts +++ b/tests/boundary/mcp/artifactInspectionCoverageMcp.test.ts @@ -10,7 +10,7 @@ import type { OnTestFinishedHandler } from "vitest"; import { machoImage } from "../../../src/artifacts/apple/MachoImage.fixture.js"; import { createArtifactBinarySession } from "../../../src/composition/binary.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createServer } from "../../../src/server/createServer.js"; diff --git a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts index 1208ff60de..bf58eed860 100644 --- a/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts +++ b/tests/boundary/mcp/artifactMcp.inlineInventory.test.ts @@ -27,7 +27,7 @@ import { artifactInventoryResultSchema } from "../../../src/domain/artifactGraph import { analysisViewResultSchema, type AnalysisViewResult, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; const schemaValidation = createMcpSchemaValidation(); afterAll(() => schemaValidation.clear()); diff --git a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts index 30847dfd1c..b3709b2837 100644 --- a/tests/boundary/mcp/batchDecompileViewsMcp.test.ts +++ b/tests/boundary/mcp/batchDecompileViewsMcp.test.ts @@ -5,7 +5,7 @@ import type { CallToolResult } from "@modelcontextprotocol/server"; import { afterAll, expect, it, onTestFinished, vi } from "vitest"; import { z } from "zod"; import { ANALYSIS_VIEW_TOOL_CONTRACTS } from "../../../src/contracts/analysisViewToolContracts.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { AnalysisOutputError } from "../../../src/domain/analysisErrorCore.js"; import { createEvidence, diff --git a/tests/boundary/mcp/bunStandaloneMcp.test.ts b/tests/boundary/mcp/bunStandaloneMcp.test.ts index 892250b021..a2201897c2 100644 --- a/tests/boundary/mcp/bunStandaloneMcp.test.ts +++ b/tests/boundary/mcp/bunStandaloneMcp.test.ts @@ -7,7 +7,7 @@ import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; import { createServer } from "../../../src/server/createServer.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { bunModuleGraphPayload, elfAppendedBunPayload, diff --git a/tests/boundary/mcp/dartAotSummaryMcp.test.ts b/tests/boundary/mcp/dartAotSummaryMcp.test.ts index 70fa9fd11c..5e88924df4 100644 --- a/tests/boundary/mcp/dartAotSummaryMcp.test.ts +++ b/tests/boundary/mcp/dartAotSummaryMcp.test.ts @@ -9,7 +9,7 @@ import { createFlutterBuildAnalysisProvider } from "../../../src/composition/flu import { analysisViewResultSchema, dartAotSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { flutterResultSchemas } from "../../../src/domain/flutter/flutterBuildAnalysis.js"; import type { JsonValue } from "../../../src/domain/jsonValue.js"; diff --git a/tests/boundary/mcp/deferredRuntime.test.ts b/tests/boundary/mcp/deferredRuntime.test.ts index a0735b1054..2025a6f4db 100644 --- a/tests/boundary/mcp/deferredRuntime.test.ts +++ b/tests/boundary/mcp/deferredRuntime.test.ts @@ -20,7 +20,7 @@ import { GENERATED_MCP_TOOL_CATALOG } from "../../fixtures/mcpToolCatalog.js"; import { PROMPT_CONTRACTS } from "../../../src/contracts/promptContracts.js"; import { TOOL_FAMILY_DEFINITIONS } from "../../../src/contracts/toolFamilies.js"; import { evidenceSchema } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { SERVER_START_FAILED } from "../../../src/main/messages.js"; import { HOT_TOOL_NAMES } from "../../../src/catalogDiscovery.js"; import { DatabaseSync } from "node:sqlite"; diff --git a/tests/boundary/mcp/denoStandaloneMcp.test.ts b/tests/boundary/mcp/denoStandaloneMcp.test.ts index 80f86fc1a3..73d7af8e57 100644 --- a/tests/boundary/mcp/denoStandaloneMcp.test.ts +++ b/tests/boundary/mcp/denoStandaloneMcp.test.ts @@ -7,7 +7,7 @@ import { expect, it } from "vitest"; import { artifactMcpCall } from "../../../scripts/lib/artifact-e2e.mjs"; import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; import { denoElf } from "../../../src/artifacts/deno/DenoStandalone.fixture.js"; -import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { artifactInventorySummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; diff --git a/tests/boundary/mcp/desktopApplicationMcp.test.ts b/tests/boundary/mcp/desktopApplicationMcp.test.ts index 3dc9937e77..9d99d5b6df 100644 --- a/tests/boundary/mcp/desktopApplicationMcp.test.ts +++ b/tests/boundary/mcp/desktopApplicationMcp.test.ts @@ -8,7 +8,7 @@ import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { desktopApplicationSchema } from "../../../src/domain/desktopApplication.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { buildNativePeFixture } from "../../../src/dotnet/ManagedPe.fixture.js"; import { ArtifactProvider } from "../../../src/artifacts/ArtifactProvider.js"; diff --git a/tests/boundary/mcp/espImageMcp.test.ts b/tests/boundary/mcp/espImageMcp.test.ts index bc15db92ea..1d1082bf7e 100644 --- a/tests/boundary/mcp/espImageMcp.test.ts +++ b/tests/boundary/mcp/espImageMcp.test.ts @@ -14,7 +14,7 @@ import { import { ESP8266_REFUSAL } from "../../../src/domain/firmware/espImage.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. diff --git a/tests/boundary/mcp/evmInterfaceMcp.test.ts b/tests/boundary/mcp/evmInterfaceMcp.test.ts index 808742e557..240b3bc0d0 100644 --- a/tests/boundary/mcp/evmInterfaceMcp.test.ts +++ b/tests/boundary/mcp/evmInterfaceMcp.test.ts @@ -14,7 +14,7 @@ import { AnalysisCapabilityUnavailableError, } from "../../../src/domain/analysisErrorCore.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { evmInterfaceSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { evmInterfaceSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { inspectSolidityMetadata } from "../../../src/evm/SolidityMetadata.js"; import { err, ok } from "../../../src/domain/result.js"; import { silentLogger } from "../../../src/logger.js"; diff --git a/tests/boundary/mcp/featureTraceViewsMcp.test.ts b/tests/boundary/mcp/featureTraceViewsMcp.test.ts index 70f12810ea..98190b91b4 100644 --- a/tests/boundary/mcp/featureTraceViewsMcp.test.ts +++ b/tests/boundary/mcp/featureTraceViewsMcp.test.ts @@ -5,7 +5,7 @@ import { afterAll, expect, it, onTestFinished } from "vitest"; import { z } from "zod"; import { JAVASCRIPT_FEATURE_TRACE_EXAMPLE } from "../../../src/contracts/javascript/javascriptApplicationWorkflowExamples.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { applicationFeatureTraceResultSchema } from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js"; import { jsonObjectSchema } from "../../../src/domain/jsonValue.js"; diff --git a/tests/boundary/mcp/fitImageMcp.test.ts b/tests/boundary/mcp/fitImageMcp.test.ts index d0e31d33c3..6d41900333 100644 --- a/tests/boundary/mcp/fitImageMcp.test.ts +++ b/tests/boundary/mcp/fitImageMcp.test.ts @@ -11,7 +11,7 @@ import { } from "../../../src/artifacts/fit/FitImage.fixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. diff --git a/tests/boundary/mcp/fridaMcp.test.ts b/tests/boundary/mcp/fridaMcp.test.ts index 6bdc8f6203..49ec795df4 100644 --- a/tests/boundary/mcp/fridaMcp.test.ts +++ b/tests/boundary/mcp/fridaMcp.test.ts @@ -10,7 +10,7 @@ import { expandToolAvailability } from "../../../src/contracts/toolOutputSchemaP import { FridaInstrumentationManager } from "../../../src/frida/FridaInstrumentationManager.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; -import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { fridaSessionStatusSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { fridaSessionStatusSchema } from "../../../src/domain/fridaSessionStatus.js"; import { evidenceBundleSummarySchema } from "../../../src/domain/evidenceRecordSummary.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; diff --git a/tests/boundary/mcp/ghidraNativeApiMcp.test.ts b/tests/boundary/mcp/ghidraNativeApiMcp.test.ts index 39c78d2dc7..fdfeb8f423 100644 --- a/tests/boundary/mcp/ghidraNativeApiMcp.test.ts +++ b/tests/boundary/mcp/ghidraNativeApiMcp.test.ts @@ -15,7 +15,7 @@ import { ok } from "../../../src/domain/result.js"; import { ghidraFunctionDossier } from "../../../src/domain/ghidraValues.fixture.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; import { Ajv2020 } from "ajv/dist/2020.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; it("retains complete load-image facts and pages selected relocations through the advertised SDK contracts", async () => { const fixture = fixtureDosOverlapLoadImage(); diff --git a/tests/boundary/mcp/goBinaryViews.test.ts b/tests/boundary/mcp/goBinaryViews.test.ts index 78cbd5d7a7..b2a2e65579 100644 --- a/tests/boundary/mcp/goBinaryViews.test.ts +++ b/tests/boundary/mcp/goBinaryViews.test.ts @@ -11,7 +11,7 @@ import { goBinarySchema } from "../../../src/domain/go/goBinary.js"; import { analysisViewResultSchema, goBinarySummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { diff --git a/tests/boundary/mcp/godotPckMcp.test.ts b/tests/boundary/mcp/godotPckMcp.test.ts index bff61f7029..35b85033b8 100644 --- a/tests/boundary/mcp/godotPckMcp.test.ts +++ b/tests/boundary/mcp/godotPckMcp.test.ts @@ -19,7 +19,7 @@ import type { ArtifactOccurrence } from "../../../src/domain/artifactGraph.js"; import { artifactInventorySummaryViewResultSchema, analysisViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { canBorrowParsedJsonSnapshot, immutableJsonSnapshotMember, diff --git a/tests/boundary/mcp/hexRecordsMcp.test.ts b/tests/boundary/mcp/hexRecordsMcp.test.ts index 388a9369c4..0f481def0e 100644 --- a/tests/boundary/mcp/hexRecordsMcp.test.ts +++ b/tests/boundary/mcp/hexRecordsMcp.test.ts @@ -8,7 +8,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { SREC_S37 } from "../../../src/domain/firmware/hexRecords.fixture.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; // The compiled stdio fixture checks advertised schemas against their dialect and // validates actual tool responses, avoiding a second source server module graph. diff --git a/tests/boundary/mcp/javascriptFactsViewMcp.test.ts b/tests/boundary/mcp/javascriptFactsViewMcp.test.ts index cdf36533bf..44ca4d2125 100644 --- a/tests/boundary/mcp/javascriptFactsViewMcp.test.ts +++ b/tests/boundary/mcp/javascriptFactsViewMcp.test.ts @@ -17,7 +17,7 @@ import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; import { analysisViewResultSchema, type AnalysisViewResult, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { createJavaScriptApplicationGraph, createJavaScriptApplicationNode, diff --git a/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts b/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts index 29613f874d..7decd73b54 100644 --- a/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts +++ b/tests/boundary/mcp/javascriptNativeHandoffIdentity.test.ts @@ -4,7 +4,7 @@ import { join } from "node:path"; import { expect } from "vitest"; import { toolContract } from "../../../src/contracts/toolContracts.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createEvidence } from "../../../src/domain/evidence.js"; import { applicationFeatureTraceResultSchema, diff --git a/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts b/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts index 8ecd613779..43fa6d342c 100644 --- a/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedArtifactSummaryMcp.test.ts @@ -11,7 +11,7 @@ import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { managedArtifactSummaryViewResultSchema, analysisViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { managedArtifactInspectionSchema } from "../../../src/domain/managed/managedArtifact.js"; import { createServer } from "../../../src/server/createServer.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; diff --git a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts index 43092a482f..4ead74b429 100644 --- a/tests/boundary/mcp/managedMemberSummaryMcp.test.ts +++ b/tests/boundary/mcp/managedMemberSummaryMcp.test.ts @@ -15,7 +15,7 @@ import { import { analysisViewResultSchema, managedMemberSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { managedMemberInspectionSchema, type ManagedMemberInspection, diff --git a/tests/boundary/mcp/nativeFeatureTraceViews.test.ts b/tests/boundary/mcp/nativeFeatureTraceViews.test.ts index 07885b3533..42df465c91 100644 --- a/tests/boundary/mcp/nativeFeatureTraceViews.test.ts +++ b/tests/boundary/mcp/nativeFeatureTraceViews.test.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/nativeInstructionReferencesView.test.ts b/tests/boundary/mcp/nativeInstructionReferencesView.test.ts index aa5858e5e7..45b3456725 100644 --- a/tests/boundary/mcp/nativeInstructionReferencesView.test.ts +++ b/tests/boundary/mcp/nativeInstructionReferencesView.test.ts @@ -6,10 +6,8 @@ import { createAnalysisExecution } from "../../../src/application/AnalysisProvid import { createServer } from "../../../src/server/createServer.js"; import { createToolResultDelivery } from "../../../src/server/toolResult.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisViewRequest.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { ok } from "../../../src/domain/result.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts b/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts index 70e9027a11..e01f23dd6b 100644 --- a/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts +++ b/tests/boundary/mcp/nativeInventoryViewsMcp.test.ts @@ -4,7 +4,7 @@ import { expect, it, onTestFinished } from "vitest"; import { z } from "zod"; import { createAnalysisExecution } from "../../../src/application/AnalysisProvider.js"; import { toolContract } from "../../../src/contracts/toolContracts.js"; -import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisViewRequest.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { NATIVE_INVENTORY_OPERATIONS } from "../../../src/domain/native/nativeInventory.js"; import { ok } from "../../../src/domain/result.js"; diff --git a/tests/boundary/mcp/nativeLoadImageViewsMcp.test.ts b/tests/boundary/mcp/nativeLoadImageViewsMcp.test.ts index d52a2cb33e..e822344c30 100644 --- a/tests/boundary/mcp/nativeLoadImageViewsMcp.test.ts +++ b/tests/boundary/mcp/nativeLoadImageViewsMcp.test.ts @@ -6,7 +6,7 @@ import { ok } from "../../../src/domain/result.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { nativeDolLoadImageFixture, nativeRawLoadImageFixture, diff --git a/tests/boundary/mcp/nativeObservationViewsMcp.test.ts b/tests/boundary/mcp/nativeObservationViewsMcp.test.ts index 9d83777bc8..d7ce1ca7c7 100644 --- a/tests/boundary/mcp/nativeObservationViewsMcp.test.ts +++ b/tests/boundary/mcp/nativeObservationViewsMcp.test.ts @@ -8,10 +8,8 @@ import { ok } from "../../../src/domain/result.js"; import { z } from "zod"; import { afterAll, expect, it } from "vitest"; import { parseEvidence } from "../../../src/domain/evidence.js"; -import { - analysisViewResultSchema, - inspectAnalysisViewInputSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +import { inspectAnalysisViewInputSchema } from "../../../src/domain/analysisView/analysisViewRequest.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { NATIVE_PRIMITIVE_OBSERVATION_OPERATIONS } from "../../../src/domain/native/nativeObservation.js"; import { connectGhidraMcp } from "./ghidraMcpHarness.js"; import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js"; diff --git a/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts b/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts index 56726093b2..c8a55f72d1 100644 --- a/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts +++ b/tests/boundary/mcp/nativeProcedureReferenceViews.test.ts @@ -1,7 +1,7 @@ import { join } from "node:path"; import { expect, it } from "vitest"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/nodeSeaMcp.test.ts b/tests/boundary/mcp/nodeSeaMcp.test.ts index 31472f4c4a..6db7aef411 100644 --- a/tests/boundary/mcp/nodeSeaMcp.test.ts +++ b/tests/boundary/mcp/nodeSeaMcp.test.ts @@ -14,7 +14,7 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { analysisViewResultSchema, artifactInventorySummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; const expectBlobAdmissionFailure = ( reply: Parameters[0], diff --git a/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts b/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts index eab462af30..8e5e40d738 100644 --- a/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts +++ b/tests/boundary/mcp/pyinstallerArtifactMcp.test.ts @@ -18,7 +18,7 @@ import { import { artifactExtractionResultSchema } from "../../../src/domain/artifactGraph.js"; import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js"; import { parseEvidence, type Evidence } from "../../../src/domain/evidence.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts b/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts index 627f0b7a6d..78ef80c96f 100644 --- a/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts +++ b/tests/boundary/mcp/sourceToBundleViewsMcp.test.ts @@ -5,7 +5,7 @@ import { createMcpSchemaValidation } from "../../fixtures/mcpSchemaValidation.js import { createApplicationMcpHarness } from "../../fixtures/applicationMcpHarness.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { sourceToBundleComparisonResultSchema } from "../../../src/domain/javascript/sourceToBundleComparisonSchemas.js"; -import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { analysisViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { importReferenceSource } from "../../support/referenceSourceResourceScope.js"; diff --git a/tests/boundary/mcp/wasmArtifactMcp.test.ts b/tests/boundary/mcp/wasmArtifactMcp.test.ts index 85314d2890..ace97422b5 100644 --- a/tests/boundary/mcp/wasmArtifactMcp.test.ts +++ b/tests/boundary/mcp/wasmArtifactMcp.test.ts @@ -10,7 +10,7 @@ import { readFile, realpath, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { WasmArtifactService } from "../../../src/application/wasm/WasmArtifactService.js"; import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; -import { wasmArtifactSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisView.js"; +import { wasmArtifactSummaryViewResultSchema } from "../../../src/domain/analysisView/analysisViewResult.js"; import { createHash } from "node:crypto"; import { z } from "zod"; import { createServer } from "../../../src/server/createServer.js"; diff --git a/tests/boundary/mcp/webBundleViewsMcp.test.ts b/tests/boundary/mcp/webBundleViewsMcp.test.ts index 867258559e..79554ca932 100644 --- a/tests/boundary/mcp/webBundleViewsMcp.test.ts +++ b/tests/boundary/mcp/webBundleViewsMcp.test.ts @@ -6,7 +6,7 @@ import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; import { webBundlePageViewResultSchema, webBundleSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { sampleWebBundleAnalysis } from "../../fixtures/webBundleAnalysis.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; diff --git a/tests/boundary/process/androidMappingMcp.test.ts b/tests/boundary/process/androidMappingMcp.test.ts index 1f40065c36..a073fa01f4 100644 --- a/tests/boundary/process/androidMappingMcp.test.ts +++ b/tests/boundary/process/androidMappingMcp.test.ts @@ -11,7 +11,7 @@ import { androidResultSchemas } from "../../../src/domain/android/androidAnalysi import { androidClassSummaryViewResultSchema, androidMethodSummaryViewResultSchema, -} from "../../../src/domain/analysisView/analysisView.js"; +} from "../../../src/domain/analysisView/analysisViewResult.js"; import { parseEvidence } from "../../../src/domain/evidence.js"; import { createServer } from "../../../src/server/createServer.js"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; diff --git a/tests/boundary/process/hopperDeadlineCleanup.test.ts b/tests/boundary/process/hopperDeadlineCleanup.test.ts new file mode 100644 index 0000000000..f27283346f --- /dev/null +++ b/tests/boundary/process/hopperDeadlineCleanup.test.ts @@ -0,0 +1,81 @@ +import { access } from "node:fs/promises"; +import { expect, it } from "vitest"; +import { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { HopperDeadlineCleanup } from "../../../scripts/lib/hopper-deadline-cleanup.mjs"; +import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js"; +import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; + +it("retains the workspace and exact cleanup owner until failed profile cleanup succeeds", async () => { + const workspace = await createTestTempDirectory( + "rea-hopper-deadline-cleanup-", + ); + const profile = new ArtifactResourceScope(); + const denial = new Error("profile close denied"); + let attempts = 0; + profile.addCloseHook(async () => { + if (++attempts === 1) throw denial; + }); + const owner = new HopperDeadlineCleanup(workspace, profile); + let failure: unknown; + try { + await owner.close(); + } catch (cause) { + failure = cause; + } + expect(failure).toBeInstanceOf(AggregateError); + expect(failure).toMatchObject({ cleanupOwner: owner, errors: [denial] }); + await expect(access(workspace)).resolves.toBeUndefined(); + await owner.close(); + expect(attempts).toBe(2); + await expect(access(workspace)).rejects.toMatchObject({ code: "ENOENT" }); +}); + +it("attempts both owners and retries them after cancellation during cleanup", async () => { + const workspace = await createTestTempDirectory("rea-hopper-deadline-mixed-"); + const profile = new ArtifactResourceScope(); + const controller = new AbortController(); + const clientFailure = new Error("client close denied"); + const profileFailure = new Error("profile close interrupted"); + let release: (() => void) | undefined; + let entered: (() => void) | undefined; + const started = new Promise((resolve) => { + entered = resolve; + }); + const gate = new Promise((resolve) => { + release = resolve; + }); + let profileAttempts = 0; + profile.addCloseHook(async () => { + if (++profileAttempts === 1) { + entered?.(); + await gate; + if (controller.signal.aborted) throw profileFailure; + } + }); + let clientAttempts = 0; + const owner = new HopperDeadlineCleanup(workspace, profile); + owner.setClient({ + close: async () => + ++clientAttempts === 1 + ? { ok: false, error: clientFailure } + : { ok: true }, + }); + const first = owner.close(); + try { + expect(owner.close()).toBe(first); + await awaitAcknowledgement(started, first, "Hopper profile close hook"); + controller.abort(); + } finally { + // Release and join the parked cleanup even when an assertion fails. + release?.(); + await first.catch(() => undefined); + } + await expect(first).rejects.toMatchObject({ + cleanupOwner: owner, + errors: [clientFailure, profileFailure], + }); + await expect(access(workspace)).resolves.toBeUndefined(); + await owner.close(); + expect([clientAttempts, profileAttempts]).toEqual([2, 2]); + await expect(access(workspace)).rejects.toMatchObject({ code: "ENOENT" }); +}); diff --git a/tests/boundary/process/http3VerifierNodeTests.test.ts b/tests/boundary/process/http3VerifierNodeTests.test.ts new file mode 100644 index 0000000000..0e7be7fa6e --- /dev/null +++ b/tests/boundary/process/http3VerifierNodeTests.test.ts @@ -0,0 +1,31 @@ +import { randomUUID } from "node:crypto"; +import { expect, it } from "vitest"; +import { runOwnedCommand } from "../../../src/process/OwnedCommand.js"; + +// These verifier tests use Node's test runner and exercise the actual script +// modules. Run them in the built lane because the lifecycle module imports dist. +const runNodeTests = async (file: string): Promise => { + const result = await runOwnedCommand( + { + command: process.execPath, + arguments: ["--test", file], + cwd: process.cwd(), + hostEnvironment: process.env, + runId: `rea-http3-node-tests-${randomUUID()}`, + }, + { timeoutMs: 30_000, diagnosticBytes: 64 * 1024, acceptNonZeroExit: true }, + ); + expect(result.exitCode, result.stdout.text + result.stderr.text).toBe(0); +}; + +it("runs HTTP/3 capture assertion regressions", async () => { + await runNodeTests("scripts/lib/http3-capture-assertions.test.mjs"); +}, 60_000); + +it.skipIf(process.platform === "win32")( + "runs POSIX HTTP/3 lifecycle regressions", + async () => { + await runNodeTests("scripts/lib/http3-verifier-lifecycle.test.mjs"); + }, + 60_000, +); diff --git a/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts b/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts index 6837842304..7baac6f168 100644 --- a/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts +++ b/tests/boundary/process/javascriptVerifierBoundedEvidence.test.ts @@ -88,16 +88,12 @@ process.on('exit', () => fs.writeSync(2, 'Evidence close attempts: ' + attempts throw new Error("Missing verifier diagnostics"); if (denial === "always") { expect(result.stderr).toContain("authored Evidence close denial"); - expect(result.stderr).toContain( - "AggregateError: Verifier cleanup failed", - ); + expect(result.stderr).toContain("Verifier cleanup failed"); expect(result.stderr).toContain("cleanupLeases"); } else { expect(result.stderr).toContain("authored Evidence read failure"); expect(result.stderr).not.toContain("authored Evidence close denial"); - expect(result.stderr).not.toContain( - "AggregateError: Verifier cleanup failed", - ); + expect(result.stderr).not.toContain("Verifier cleanup failed"); } }, ); diff --git a/tests/boundary/process/verifierArtifactScope.test.ts b/tests/boundary/process/verifierArtifactScope.test.ts new file mode 100644 index 0000000000..68f1ad829c --- /dev/null +++ b/tests/boundary/process/verifierArtifactScope.test.ts @@ -0,0 +1,107 @@ +import { expect, it } from "vitest"; +import { + VerifierArtifactCleanupFailure, + withVerifierArtifactScope, +} from "../../../scripts/lib/verifier-artifact-scope.mjs"; +import type { ArtifactResourceScope } from "../../../src/artifacts/ArtifactResourceScope.js"; +import { awaitAcknowledgement } from "../../fixtures/lifecycleAcknowledgement.js"; + +it("retains the exact scope for retry and preserves the operation failure", async () => { + const primary = new Error("analysis failed"); + const cleanup = new Error("worker close failed"); + let attempts = 0; + let scope: ArtifactResourceScope | undefined; + let failure: unknown; + try { + await withVerifierArtifactScope(async (owner) => { + scope = owner; + owner.addCloseHook(async () => { + if (++attempts === 1) throw cleanup; + }); + throw primary; + }); + } catch (cause) { + failure = cause; + } + expect(failure).toBeInstanceOf(VerifierArtifactCleanupFailure); + const aggregate = failure as VerifierArtifactCleanupFailure; + expect(aggregate.errors).toEqual([primary, cleanup]); + expect(aggregate.cause).toBe(primary); + expect(aggregate.cleanupOwner).toBe(scope); + await aggregate.cleanupOwner.close(); + expect(attempts).toBe(2); +}); + +it("retains cleanup authority when the operation succeeds", async () => { + let attempts = 0; + let failure: unknown; + try { + await withVerifierArtifactScope(async (scope) => { + scope.addCloseHook(async () => { + if (++attempts === 1) throw new Error("close failed"); + }); + return "result"; + }); + } catch (cause) { + failure = cause; + } + expect(failure).toBeInstanceOf(VerifierArtifactCleanupFailure); + const aggregate = failure as VerifierArtifactCleanupFailure; + expect(aggregate.errors).toHaveLength(1); + await aggregate.cleanupOwner.close(); + expect(attempts).toBe(2); +}); + +it("finishes every close hook when cancellation arrives during final cleanup", async () => { + const controller = new AbortController(); + const cleanup = new Error("first close failed"); + let release: (() => void) | undefined; + let entered: (() => void) | undefined; + const started = new Promise((resolve) => { + entered = resolve; + }); + const gate = new Promise((resolve) => { + release = resolve; + }); + const calls: string[] = []; + let attempts = 0; + const operation = withVerifierArtifactScope(async (scope) => { + scope.addCloseHook(async () => { + calls.push("first"); + if (++attempts === 1) { + entered?.(); + await gate; + if (controller.signal.aborted) throw cleanup; + } + }); + scope.addCloseHook(async () => { + calls.push("second"); + }); + return "finished"; + }); + try { + await awaitAcknowledgement( + started, + operation, + "verifier artifact close hook", + ); + controller.abort(); + } finally { + // Release and join the parked cleanup even when the boundary is missed. + release?.(); + await operation.catch(() => undefined); + } + let failure: unknown; + try { + await operation; + } catch (cause) { + failure = cause; + } + expect(controller.signal.aborted).toBe(true); + expect(failure).toBeInstanceOf(VerifierArtifactCleanupFailure); + expect(calls).toEqual(["first", "second"]); + const aggregate = failure as VerifierArtifactCleanupFailure; + expect(aggregate.errors).toEqual([cleanup]); + await aggregate.cleanupOwner.close(); + expect(calls).toEqual(["first", "second", "first", "second"]); +}); diff --git a/tests/boundary/providers/hopper/hopperVerificationCleanup.test.ts b/tests/boundary/providers/hopper/hopperVerificationCleanup.test.ts index 2678c57026..704672da28 100644 --- a/tests/boundary/providers/hopper/hopperVerificationCleanup.test.ts +++ b/tests/boundary/providers/hopper/hopperVerificationCleanup.test.ts @@ -47,16 +47,18 @@ it.skipIf(process.platform === "win32")( try { const children = await readdir(directory); expect(children).toHaveLength(1); - expect(await snapshotHopperRuntime(parent, directory)).toEqual( + const snapshot = await snapshotHopperRuntime(parent, directory); + expect(snapshot.paths).toEqual( new Set(children.map((name) => join(directory, name))), ); + expect(snapshot.confirmedSessions.size).toBe(0); } finally { await acquired.lease.release(); } }, ); -it("reports retained sessions belonging to observed MCP processes", async () => { +it("reports sessions with unresolved identity without calling them foreign", async () => { const parent = await createTestTempDirectory("rea-cleanup-retained-session-"); const directory = join(parent, "rea-hopper-501"); const previous = join(parent, "rea-existing-session"); @@ -65,7 +67,10 @@ it("reports retained sessions belonging to observed MCP processes", async () => const retained = join(parent, "rea-retained-session"); const unrelated = join(parent, "rea-unrelated-session"); const unknown = join(parent, "rea-no-ownership-record"); - for (const path of [retained, unrelated, unknown]) await mkdir(path); + const malformed = join(parent, "rea-invalid-ownership-record"); + const invalidJson = join(parent, "rea-invalid-ownership-json"); + for (const path of [retained, unrelated, unknown, malformed, invalidJson]) + await mkdir(path); await writeFile(join(retained, "bootstrap.py"), "source-owned fixture"); await writeFile( join(retained, "ownership.json"), @@ -75,13 +80,18 @@ it("reports retained sessions belonging to observed MCP processes", async () => join(unrelated, "ownership.json"), JSON.stringify({ parent_pid: process.pid + 1 }), ); - const after = await snapshotHopperRuntime( - parent, - directory, - new Set([process.pid]), - ); + await writeFile(join(malformed, "ownership.json"), "{}"); + await writeFile(join(invalidJson, "ownership.json"), "{"); + const after = await snapshotHopperRuntime(parent, directory); - expect([...after].filter((path) => !before.has(path))).toEqual([retained]); + expect( + [...after.paths].filter((path) => !before.paths.has(path)).sort(), + ).toEqual([invalidJson, malformed, unknown, unrelated, retained].sort()); + expect( + [...after.unresolvedSessions] + .filter((path) => !before.paths.has(path)) + .sort(), + ).toEqual([invalidJson, malformed, unknown, unrelated, retained].sort()); }); it("reports an unexpected file at the shared lease root", async () => { @@ -89,11 +99,40 @@ it("reports an unexpected file at the shared lease root", async () => { const directory = join(parent, "rea-hopper-501"); await writeFile(directory, "unexpected lease root file"); - expect(await snapshotHopperRuntime(parent, directory)).toEqual( + expect((await snapshotHopperRuntime(parent, directory)).paths).toEqual( new Set([directory]), ); }); +it("requires the selected run identity before treating a session as identified", async () => { + const parent = await createTestTempDirectory("rea-hopper-run-identity-"); + const leases = join(parent, "rea-hopper-501"); + const selected = join(parent, "rea-selected"); + const reusedPid = join(parent, "rea-other-run"); + const missing = join(parent, "rea-missing-run"); + for (const path of [selected, reusedPid, missing]) await mkdir(path); + await writeFile( + join(selected, "ownership.json"), + JSON.stringify({ parent_pid: process.pid, run_id: "selected-run" }), + ); + await writeFile( + join(reusedPid, "ownership.json"), + JSON.stringify({ parent_pid: process.pid, run_id: "other-run" }), + ); + await writeFile( + join(missing, "ownership.json"), + JSON.stringify({ parent_pid: process.pid }), + ); + const observed = await snapshotHopperRuntime(parent, leases, "selected-run"); + expect([...observed.paths].sort()).toEqual( + [selected, reusedPid, missing].sort(), + ); + expect([...observed.confirmedSessions]).toEqual([selected]); + expect([...observed.unresolvedSessions].sort()).toEqual( + [reusedPid, missing].sort(), + ); +}); + const createLeaseWorkspace = async (): Promise => { // Unix socket paths must fit the host sockaddr_un limit, independently of // the test runner's potentially long redirected temporary directory. diff --git a/tests/evaluation/importCycleGuard.test.ts b/tests/evaluation/importCycleGuard.test.ts new file mode 100644 index 0000000000..dd3f8d4dc1 --- /dev/null +++ b/tests/evaluation/importCycleGuard.test.ts @@ -0,0 +1,31 @@ +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect, it } from "vitest"; +import { expectImportCycles } from "../fixtures/importCycles.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; + +it.each(["./self.js", "./%73elf.js?view=1#fragment"])( + "detects self-import %s and tracks its allowed edge", + async (specifier) => { + const root = await createTestTempDirectory("rea-import-cycle-"); + await writeFile( + join(root, "self.ts"), + `import ${JSON.stringify(specifier)};\n`, + ); + expect(() => expectImportCycles(root, [], [])).toThrow(); + expectImportCycles(root, [["self.ts"]], ["self.ts -> self.ts"]); + expect(() => expectImportCycles(root, [["self.ts"]], [])).toThrow(); + }, +); + +it("does not include cycles in test-only fixtures", async () => { + const root = await createTestTempDirectory("rea-import-cycle-"); + await writeFile( + join(root, "sample.fixture.ts"), + 'import "./sample.fixture.js";\n', + ); + await writeFile(join(root, "sample.test.ts"), 'import "./sample.test.js";\n'); + await writeFile(join(root, "sample.spec.ts"), 'import "./sample.spec.js";\n'); + await writeFile(join(root, "production.ts"), "export const value = 1;\n"); + expectImportCycles(root, [], []); +}); diff --git a/tests/evaluation/moduleBoundaries.test.ts b/tests/evaluation/moduleBoundaries.test.ts index 75f0bd4f4c..deacf4f5bb 100644 --- a/tests/evaluation/moduleBoundaries.test.ts +++ b/tests/evaluation/moduleBoundaries.test.ts @@ -1,7 +1,7 @@ import { execFile, spawnSync } from "node:child_process"; import { mkdir, rename, writeFile } from "node:fs/promises"; import { dirname, join, resolve } from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import { describe, expect, it } from "vitest"; @@ -176,6 +176,28 @@ describe("live capability constructor ownership", () => { const execute = promisify(execFile); const verifier = resolve("scripts/verify-module-boundaries.mjs"); +const resolvePackageImport = ( + root: string, + file: string, + specifier: string, +) => { + const result = spawnSync( + process.execPath, + [ + "--experimental-import-meta-resolve", + "--input-type=module", + "-e", + "console.log(import.meta.resolve(process.argv[1], process.argv[2]))", + specifier, + pathToFileURL(join(root, file)).href, + ], + { encoding: "utf8" }, + ); + if (result.error !== undefined) throw result.error; + expect(result.status, result.stderr).toBe(0); + return fileURLToPath(result.stdout.trim()); +}; + describe("incremental module import boundaries", () => { it.each([ ["src/domain/probe.ts", "../application/Workflow.js", false], @@ -284,6 +306,72 @@ describe("incremental module import boundaries", () => { }); }); +describe("package-map module ownership", () => { + it.each([ + ["./dist/composition/worker.js"], + ["./dist/%63omposition/worker.js"], + ["./%64ist/composition/worker.js"], + ])("checks the package-map source owner for %s", async (target) => { + const root = await createTestTempDirectory("rea-module-package-"); + await mkdir(join(root, "src/application"), { recursive: true }); + await writeFile( + join(root, "package.json"), + JSON.stringify({ imports: { "#worker": target } }), + ); + const file = "src/application/probe.ts"; + await writeFile(join(root, file), 'import "#worker";\n'); + expect(resolvePackageImport(root, file, "#worker")).toBe( + join(root, "dist/composition/worker.js"), + ); + const checked = spawnSync(process.execPath, [verifier, file], { + cwd: root, + encoding: "utf8", + }); + if (checked.error !== undefined) throw checked.error; + expect(checked.status).toBe(1); + expect(checked.stderr).toContain( + "application-composition: #worker resolves to src/composition/worker.ts", + ); + }); + + it.each([ + [ + "#worker", + { + "#worker": { + import: "./dist/composition/worker.js", + default: "./dist/composition/other.js", + }, + }, + ], + ["#worker", { "#worker": ["./dist/composition/worker.js"] }], + ["#workers/worker", { "#workers/*": "./dist/composition/*.js" }], + ])( + "fails closed for unsupported package map %s", + async (specifier, imports) => { + const root = await createTestTempDirectory("rea-module-package-"); + await mkdir(join(root, "src/domain"), { recursive: true }); + await writeFile(join(root, "package.json"), JSON.stringify({ imports })); + const file = "src/domain/probe.ts"; + await writeFile( + join(root, file), + `import ${JSON.stringify(specifier)};\n`, + ); + expect(resolvePackageImport(root, file, specifier)).toBe( + join(root, "dist/composition/worker.js"), + ); + const checked = spawnSync(process.execPath, [verifier, file], { + cwd: root, + encoding: "utf8", + }); + if (checked.error !== undefined) throw checked.error; + expect(checked.status).toBe(1); + expect(checked.stderr).toContain("Cannot resolve package import"); + expect(checked.stderr).toContain("relative source import"); + }, + ); +}); + describe("module import syntax and source discovery", () => { it.each([ 'import { value } from "../android/Provider.js";', diff --git a/tests/evaluation/noBabelVisitorKeysRule.test.ts b/tests/evaluation/noBabelVisitorKeysRule.test.ts new file mode 100644 index 0000000000..c650a69395 --- /dev/null +++ b/tests/evaluation/noBabelVisitorKeysRule.test.ts @@ -0,0 +1,84 @@ +import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { RuleTester } from "oxlint/plugins-dev"; +import rule from "../../scripts/lint/rules/no-babel-visitor-keys.mjs"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { writeRepositoryOxlintConfiguration } from "../fixtures/oxlintConfiguration.js"; + +RuleTester.describe = describe; +RuleTester.it = it; +new RuleTester({ languageOptions: { parserOptions: { lang: "ts" } } }).run( + "no-babel-visitor-keys", + rule, + { + valid: [ + 'const text = "VISITOR_KEYS";', + "const config = { VISITOR_KEYS: 1 }; config.VISITOR_KEYS;", + 'import * as types from "@babel/types"; types.isIdentifier(node);', + 'import { VISITOR_KEYS } from "unrelated";', + 'import * as types from "@babel/types"; function f(types: { VISITOR_KEYS: number }) { return types.VISITOR_KEYS; }', + ], + invalid: [ + { + code: 'import { VISITOR_KEYS as keys } from "@babel/types";', + errors: [{ messageId: "owner" }], + }, + { + code: 'export { VISITOR_KEYS as keys } from "@babel/types";', + errors: [{ messageId: "owner" }], + }, + { + code: 'import * as t from "@babel/types"; t.VISITOR_KEYS;', + errors: [{ messageId: "owner" }], + }, + { + code: 'import * as t from "@babel/types"; t["VISITOR_KEYS"];', + errors: [{ messageId: "owner" }], + }, + { + code: 'import * as t from "@babel/types"; const { VISITOR_KEYS: keys } = t;', + errors: [{ messageId: "owner" }], + }, + ], + }, +); + +it("limits the shared Babel child expansion exception to its owner", async () => { + const root = await createTestTempDirectory("rea-visitor-lint-"); + const config = await writeRepositoryOxlintConfiguration(root); + await mkdir(join(root, "src/domain/javascript"), { recursive: true }); + const source = + 'import * as t from "@babel/types"; export const keys = t.VISITOR_KEYS;\n'; + await writeFile( + join(root, "src/domain/javascript/javascriptSemanticTraversal.ts"), + source, + ); + await writeFile(join(root, "src/domain/javascript/other.ts"), source); + await writeFile(join(root, "src/domain/javascript/other.spec.ts"), source); + const result = spawnSync( + process.execPath, + [ + resolve("node_modules/oxlint/bin/oxlint"), + "-c", + config, + "--format", + "json", + "src/domain/javascript/javascriptSemanticTraversal.ts", + "src/domain/javascript/other.ts", + "src/domain/javascript/other.spec.ts", + ], + { cwd: root, encoding: "utf8" }, + ); + if (result.error !== undefined) throw result.error; + expect(result.status, result.stderr).toBe(1); + const report = JSON.parse(result.stdout) as { + diagnostics: Array<{ filename: string; code: string }>; + }; + expect( + report.diagnostics + .filter(({ code }) => code.includes("no-babel-visitor-keys")) + .map(({ filename }) => filename.replaceAll("\\", "/")), + ).toEqual(["src/domain/javascript/other.ts"]); +}); diff --git a/tests/evaluation/noLocaleCompareRule.test.ts b/tests/evaluation/noLocaleCompareRule.test.ts new file mode 100644 index 0000000000..ec89a3005c --- /dev/null +++ b/tests/evaluation/noLocaleCompareRule.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { spawnSync } from "node:child_process"; +import { mkdir, writeFile } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { RuleTester } from "oxlint/plugins-dev"; +import rule from "../../scripts/lint/rules/no-locale-compare.mjs"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { writeRepositoryOxlintConfiguration } from "../fixtures/oxlintConfiguration.js"; + +RuleTester.describe = describe; +RuleTester.it = it; + +new RuleTester({ languageOptions: { parserOptions: { lang: "ts" } } }).run( + "no-locale-compare", + rule, + { + valid: [ + 'const example = "a.localeCompare(b)";', + "// a.localeCompare(b)\nconst x = a < b;", + "compareUnicodeCodePoints(a, b)", + ], + invalid: [ + { code: "a.localeCompare(b)", errors: [{ messageId: "locale" }] }, + { code: "a.localeCompare (b)", errors: [{ messageId: "locale" }] }, + { code: 'a["localeCompare"](b)', errors: [{ messageId: "locale" }] }, + { code: "a[`localeCompare`](b)", errors: [{ messageId: "locale" }] }, + { code: "a?.localeCompare(b)", errors: [{ messageId: "locale" }] }, + ], + }, +); + +it("applies the ordering rule to domain production files and excludes tests", async () => { + const root = await createTestTempDirectory("rea-ordering-lint-"); + const config = await writeRepositoryOxlintConfiguration(root); + await mkdir(join(root, "src/domain"), { recursive: true }); + const source = + "export const compare = (a: string, b: string) => a.localeCompare(b);\n"; + await writeFile(join(root, "src/domain/ordering.ts"), source); + await writeFile(join(root, "src/domain/ordering.test.ts"), source); + await writeFile(join(root, "src/domain/ordering.spec.ts"), source); + const result = spawnSync( + process.execPath, + [ + resolve("node_modules/oxlint/bin/oxlint"), + "-c", + config, + "--format", + "json", + "src/domain/ordering.ts", + "src/domain/ordering.test.ts", + "src/domain/ordering.spec.ts", + ], + { cwd: root, encoding: "utf8" }, + ); + if (result.error !== undefined) throw result.error; + expect(result.status, result.stderr).toBe(1); + const report = JSON.parse(result.stdout) as { + diagnostics: Array<{ filename: string; code: string }>; + }; + expect( + report.diagnostics + .filter(({ code }) => code.includes("no-locale-compare")) + .map(({ filename }) => filename.replaceAll("\\", "/")), + ).toEqual(["src/domain/ordering.ts"]); +}); diff --git a/tests/evaluation/numericLimitCheck.test.ts b/tests/evaluation/numericLimitCheck.test.ts index 9ff48209e7..3d08c61145 100644 --- a/tests/evaluation/numericLimitCheck.test.ts +++ b/tests/evaluation/numericLimitCheck.test.ts @@ -282,8 +282,16 @@ it("reports each mutable alias once while keeping dynamic reads unresolved", () (issue) => issue.reason === "mutable setting-key or value alias", ), ).toEqual([ - expect.objectContaining({ offset: source.indexOf("key =") }), - expect.objectContaining({ offset: source.indexOf("other =") }), + expect.objectContaining({ + offset: source.indexOf("key ="), + line: 2, + column: 9, + }), + expect.objectContaining({ + offset: source.indexOf("other ="), + line: 6, + column: 9, + }), ]); expect(discovered.unresolved).toContainEqual( expect.objectContaining({ @@ -292,6 +300,22 @@ it("reports each mutable alias once while keeping dynamic reads unresolved", () ); }); +it("retains uncertainty for mutable suffixless settings passed to other functions", () => { + const discovered = discoverNumericLimitUses([ + { + path: "src/aliases.ts", + source: ` + let size = process.env.REA_SIZE; + consume(size); + `, + }, + ]); + const mutable = discovered.unresolved.filter( + (issue) => issue.reason === "mutable setting-key or value alias", + ); + expect(mutable.map((issue) => issue.line)).toEqual([2]); +}); + it("does not attribute an unrelated same-named local to a numeric limit", () => { const discovered = discoverNumericLimitUses([ { diff --git a/tests/evaluation/packetWorkflowSemantics.test.ts b/tests/evaluation/packetWorkflowSemantics.test.ts deleted file mode 100644 index f79076904f..0000000000 --- a/tests/evaluation/packetWorkflowSemantics.test.ts +++ /dev/null @@ -1,32 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { test } from "vitest"; - -test("packet page verification rejects skipped matches with unchanged counts and monotone frames", () => { - const moduleUrl = new URL( - "../../tests/support/analysis-efficiency/packet-entry.mjs", - import.meta.url, - ).href; - const source = ` - import assert from 'node:assert/strict'; - import {verifyPacketPage} from ${JSON.stringify(moduleUrl)}; - const rows = Array.from({length:36}, (_,i)=>({frame:i+1, source_port:1000,destination_port:5432,flags:16})); - const sha256 = 'a'.repeat(64); - const report = (selected)=>({artifact:{sha256},output_window:{matching_packets:36},packets:selected.map(row=>({ - 'frame.number':[String(row.frame)],'tcp.srcport':['1000'],'tcp.dstport':['5432'],'tcp.flags':['0x0010'] - }))}); - const oracle={sha256,matching_packets:36,rows:rows.slice(0,24)}; - const first=report(rows.slice(0,12)); - verifyPacketPage(first,report(rows.slice(12,24)),oracle); - assert.throws(()=>verifyPacketPage(first,report(rows.slice(24,36)),oracle),/independently parsed/); - assert.throws(()=>verifyPacketPage(first,report(rows.slice(12,24).reverse()),oracle),/independently parsed/); - assert.throws(()=>verifyPacketPage(first,{...report(rows.slice(12,24)),artifact:{sha256:'b'.repeat(64)}},oracle),/independently parsed/); - `; - // Semantic checker only; synthetic rows do not establish provider behavior. - const result = spawnSync(process.execPath, ["--input-type=module", "-"], { - input: source, - encoding: "utf8", - }); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stderr); -}); diff --git a/tests/evaluation/sazWorkflowSemantics.test.ts b/tests/evaluation/sazWorkflowSemantics.test.ts deleted file mode 100644 index f0098caa79..0000000000 --- a/tests/evaluation/sazWorkflowSemantics.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { test } from "vitest"; - -test("SAZ verification preserves source identities, duplicate headers and encoded empty bodies", () => { - const moduleUrl = new URL( - "../../tests/support/analysis-efficiency/saz-entry.mjs", - import.meta.url, - ).href; - const source = ` - import assert from 'node:assert/strict'; - import {verifySazRecords,verifySazOracleIdentity,sazRequest,sazEvidenceParameters} from ${JSON.stringify(moduleUrl)}; - const omitted=sazRequest('xbox-clear_nomp.saz','/tmp/sample.saz','baseline'); - assert.equal(Object.hasOwn(omitted,'record_ordinals'),false); - assert.deepEqual(sazEvidenceParameters(omitted,3),{capture_path:'/tmp/sample.saz',format:'saz',record_ordinals:[0,1,2],sensitive_value_count:0}); - assert.deepEqual(sazEvidenceParameters(sazRequest('xbox-clear_nomp.saz','/tmp/sample.saz','focused'),13).record_ordinals,[12,0,6]); - const empty={bytes:0,sha256:'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',content_base64:'',text:''}; - const raw=[12,0,6].map(ordinal=>{ - const session_id=String(ordinal+1).padStart(2,'0'); - return {ordinal,session_id,members:['raw/'+session_id+'_c.txt','raw/'+session_id+'_s.txt'], - request:{member:'raw/'+session_id+'_c.txt',method:'GET',url:'/',http_version:'HTTP/1.1',headers:[],body:empty}, - response:{member:'raw/'+session_id+'_s.txt',status:'200',reason:'OK',http_version:'HTTP/1.1',headers:[{name:'Content-Type',value:' text/plain'},{name:'content-type',value:''}],body:empty}}; - }); - const oracle={kind:'saz',artifact:'xbox-clear_nomp.saz',sha256:'3e808663a99cbb9160186f7419633248200fbc6863d2c897f5d4d9972dde0375',total_records:13,record_ordinals:[12,0,6],records:raw}; - const rows=raw.map(row=>({ordinal:row.ordinal,location:{kind:'archive-members',members:row.members}, - reported:{session_id:row.session_id,request:{...row.request,body:''},response:{...row.response,body:''}}, - binary_fields:['request','response'].map(side=>({pointer:'/'+side+'/body',representation:'producer-bytes',state:'retained',...empty}))})); - verifySazOracleIdentity('xbox-clear_nomp.saz',oracle); - const facts=verifySazRecords(rows,oracle);assert.deepEqual(facts.map(row=>row.ordinal),[12,0,6]); - assert.equal(facts[0].response.content_types.length,2); - assert.throws(()=>verifySazRecords(rows.slice(1),oracle),/coverage/); - assert.throws(()=>verifySazRecords(rows.toReversed(),oracle),/order/); - const changed=mutate=>{const copy=structuredClone(rows);mutate(copy);return copy;}; - assert.throws(()=>verifySazRecords(changed(copy=>copy[1].reported.session_id='1'),oracle),/identity/); - assert.throws(()=>verifySazRecords(changed(copy=>copy[0].reported.response.headers.pop()),oracle),/Content-Type/); - assert.throws(()=>verifySazRecords(changed(copy=>copy[0].binary_fields[1].content_base64='WA=='),oracle),/encoded body/); - assert.throws(()=>verifySazRecords(changed(copy=>copy[0].reported.response=null),oracle)); - assert.throws(()=>verifySazRecords(changed(copy=>copy[0].location.members=['raw/other_s.txt']),oracle),/coordinates/); - assert.throws(()=>verifySazOracleIdentity('xbox-clear_nomp.saz',{...oracle,sha256:'a'.repeat(64)}),/corpus/); - assert.throws(()=>sazRequest('__proto__','/tmp/sample.saz','focused'),/pinned/); - `; - // Checker-only synthesized rows; actual-provider verification is separate. - const result = spawnSync(process.execPath, ["--input-type=module", "-"], { - input: source, - encoding: "utf8", - }); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stderr); -}); diff --git a/tests/evaluation/signalWorkflowSemantics.test.ts b/tests/evaluation/signalWorkflowSemantics.test.ts deleted file mode 100644 index 33d6f1f6de..0000000000 --- a/tests/evaluation/signalWorkflowSemantics.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import assert from "node:assert/strict"; -import { spawnSync } from "node:child_process"; -import { test } from "vitest"; - -test("signal verification rejects skipped identical UART bytes and reordered I2C samples", () => { - const moduleUrl = new URL( - "../../tests/support/analysis-efficiency/signal-entry.mjs", - import.meta.url, - ).href; - const source = ` - import assert from 'node:assert/strict'; - import {verifySignalRows,signalDataRows,verifySignalOracleIdentity} from ${JSON.stringify(moduleUrl)}; - const sha256='a'.repeat(64); - const rows=Array.from({length:8},(_,i)=>({start:i*100+10,end:i*100+80,byte:72})); - const uart={kind:'uart',sha256,total_samples:2000,rows:rows.map((_,i)=>({start:i*100,end_exclusive:i*100+90,first_data_sample:i*100+15,last_data_sample:i*100+75,byte:72}))}; - verifySignalRows('uart',rows,uart,sha256); - verifySignalRows('uart',rows.map((row,i)=>({...row,start:i*100+15,end:i*100+75})),uart,sha256); - assert.throws(()=>verifySignalRows('uart',[{...rows[0],start:0,end:0},...rows.slice(1)],uart,sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',[{...rows[0],end:74},...rows.slice(1)],uart,sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',[{...rows[0],start:16},...rows.slice(1)],uart,sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',rows,{...uart,rows:uart.rows.map(({first_data_sample,...row})=>row)},sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',rows.slice(1).concat({start:810,end:880,byte:72}),uart,sha256),/interval/); - assert.throws(()=>verifySignalRows('uart',rows.slice(1),uart,sha256),/coverage/); - assert.throws(()=>verifySignalRows('uart',rows,uart,'b'.repeat(64)),/identity/); - const data=Array.from({length:18},(_,i)=>({start:i*100+10,end:i*100+90,byte:117})); - const raw=data.map((_,i)=>({byte:117,first_clock:i*100+20,last_clock:i*100+80})); - const i2c={kind:'i2c',sha256,total_samples:2000,reads:[{bytes:[{byte:165,last_clock:0},...raw],stop:1900}]}; - verifySignalRows('i2c',data,i2c,sha256); - assert.throws(()=>verifySignalRows('i2c',data.toReversed(),i2c,sha256),/interval/); - assert.throws(()=>verifySignalRows('i2c',[{...data[0],byte:118},...data.slice(1)],i2c,sha256),/sample bits/); - assert.throws(()=>verifySignalRows('i2c',data.map((row,i)=>({...row,start:i,end:1999})),i2c,sha256),/interval/); - assert.throws(()=>verifySignalRows('i2c',[{...data[0],start:0},...data.slice(1)],i2c,sha256),/interval/); - assert.throws(()=>verifySignalRows('i2c',[...data.slice(0,-1),{...data.at(-1),end:1900}],i2c,sha256),/interval/); - verifySignalOracleIdentity('sigrok-uart-max3232e.sr',{kind:'uart',sha256:'f797d60d6b8e12d2a1549b9a5a499ef389f44cb9e870e8610af26b57c3b2be76'}); - assert.throws(()=>verifySignalOracleIdentity('sigrok-uart-max3232e.sr',uart),/pinned signal corpus/); - assert.throws(()=>verifySignalOracleIdentity('sigrok-i2c-nunchuk.sr',{kind:'uart',sha256:'0fa6adb8297d7f3fe5ba19c94ee655c9b0f952a8b5e48a17aaa499a2f6d10d70'}),/pinned signal corpus/); - assert.throws(()=>signalDataRows('uart','9007199254740993-9007199254740994 uart-1: 48'),/sample interval/); - `; - // Checker semantics only; synthesized rows do not establish provider behavior. - const result = spawnSync(process.execPath, ["--input-type=module", "-"], { - input: source, - encoding: "utf8", - }); - assert.ifError(result.error); - assert.equal(result.status, 0, result.stderr); -}); diff --git a/tests/fixtures/environmentStateWrites.ts b/tests/fixtures/environmentStateWrites.ts index 12f8d092d1..c6afe38595 100644 --- a/tests/fixtures/environmentStateWrites.ts +++ b/tests/fixtures/environmentStateWrites.ts @@ -1,7 +1,5 @@ -import { - projectAnalysisView, - type AnalysisViewResult, -} from "../../src/domain/analysisView/analysisView.js"; +import { projectAnalysisView } from "../../src/domain/analysisView/analysisView.js"; +import type { AnalysisViewResult } from "../../src/domain/analysisView/analysisViewResult.js"; import type { TemplateRule } from "../../src/domain/analysisView/environmentManifestRules.js"; import type { Evidence } from "../../src/domain/evidence.js"; import type { JsonValue } from "../../src/domain/jsonValue.js"; diff --git a/tests/fixtures/importCycles.ts b/tests/fixtures/importCycles.ts index 51ba28ed3a..d177d87fd4 100644 --- a/tests/fixtures/importCycles.ts +++ b/tests/fixtures/importCycles.ts @@ -1,5 +1,6 @@ import { readdirSync, readFileSync, statSync } from "node:fs"; -import { dirname, join, relative, resolve } from "node:path"; +import { join, relative } from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { expect } from "vitest"; import { readImportSpecifiers } from "../../scripts/lib/import-specifiers.mjs"; @@ -13,7 +14,12 @@ const productionFiles = (directory: string): string[] => { for (const name of readdirSync(directory)) { const path = join(directory, name); if (statSync(path).isDirectory()) files.push(...productionFiles(path)); - else if (name.endsWith(".ts") && !name.includes(".test.")) files.push(path); + else if ( + name.endsWith(".ts") && + !/\.(?:test|spec)\.ts$/u.test(name) && + !name.endsWith(".fixture.ts") + ) + files.push(path); } return files; }; @@ -27,10 +33,9 @@ const importGraph = (root: string): Map> => { const source = readFileSync(join(root, file), "utf8"); const edges = new Set(); for (const spec of relativeSpecifiers(source)) { - const resolved = resolve(dirname(join(root, file)), spec).replace( - /\.js$/, - ".ts", - ); + const resolved = fileURLToPath( + new URL(spec, pathToFileURL(join(root, file))), + ).replace(/\.js$/, ".ts"); const target = relative(root, resolved); if (files.has(target)) edges.add(target); } @@ -73,7 +78,8 @@ const stronglyConnected = ( onStack.delete(member); component.push(member); } - if (component.length > 1) components.push(component.sort()); + if (component.length > 1 || graph.get(node)?.has(node) === true) + components.push(component.sort()); } }; for (const node of [...graph.keys()].sort()) { diff --git a/tests/support/analysis-efficiency/README.md b/tests/support/analysis-efficiency/README.md index 1970ab7074..0d4e3d049a 100644 --- a/tests/support/analysis-efficiency/README.md +++ b/tests/support/analysis-efficiency/README.md @@ -5,8 +5,7 @@ real local Codex CLI and the retained resource/agent runners. See [MCP catalog evaluation](../../../docs/mcp-catalog-evaluation.md) for its matrix, observed-token accounting, isolation and completed-task limits. -These files support the existing comparison, independent-oracle, receipt and -cleanup regressions. The TypeScript consumers live in `tests/boundary/`, +These files support the existing comparison, receipt and cleanup regressions. The TypeScript consumers live in `tests/boundary/`, `tests/evaluation/` and `tests/conformance/`; `npm run test:python` discovers the Python checks here. Build first for checks that use the compiled runtime. diff --git a/tests/support/analysis-efficiency/compare.mjs b/tests/support/analysis-efficiency/compare.mjs index 633057fdd5..74784ecc61 100644 --- a/tests/support/analysis-efficiency/compare.mjs +++ b/tests/support/analysis-efficiency/compare.mjs @@ -1,8 +1,16 @@ import { isDeepStrictEqual, parseArgs } from "node:util"; import { readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; -import { PACKET_ARTIFACTS } from "./packet-entry.mjs"; -import { SIGNAL_ARTIFACTS } from "./signal-entry.mjs"; + +const packetArtifacts = new Set([ + "wireshark-pgsql-jdbc.pcap", + "wireshark-dns.cap", + "wireshark-dhcp.pcap", +]); +const signalArtifacts = new Set([ + "sigrok-uart-max3232e.sr", + "sigrok-i2c-nunchuk.sr", +]); const { values } = parseArgs({ options: { @@ -81,8 +89,8 @@ for (const [side, answer] of [ // coverage. Repeated equivalent deliveries must also match every observation // field except the receipt and parent IDs, which identify their own invocations. if ( - Object.hasOwn(PACKET_ARTIFACTS, left.artifact) || - Object.hasOwn(SIGNAL_ARTIFACTS, left.artifact) || + packetArtifacts.has(left.artifact) || + signalArtifacts.has(left.artifact) || ["wireshark-http.cap", "wireshark-http_redirects.pcapng"].includes( left.artifact, ) @@ -103,9 +111,9 @@ if ( }); if ( left.delivery === right.delivery && - (!Object.hasOwn(PACKET_ARTIFACTS, left.artifact) || + (!packetArtifacts.has(left.artifact) || left.packet_selection === right.packet_selection) && - (!Object.hasOwn(SIGNAL_ARTIFACTS, left.artifact) || + (!signalArtifacts.has(left.artifact) || left.signal_selection === right.signal_selection) ) { answer.delivery_observations = answer.delivery_observations.map( diff --git a/tests/support/analysis-efficiency/native-entry.mjs b/tests/support/analysis-efficiency/native-entry.mjs index 84bc934ff6..caaf6a802f 100644 --- a/tests/support/analysis-efficiency/native-entry.mjs +++ b/tests/support/analysis-efficiency/native-entry.mjs @@ -1,7 +1,5 @@ -import { open, readFile } from "node:fs/promises"; import { isDeepStrictEqual } from "node:util"; -export const NATIVE_ARTIFACTS = ["jq", "sharex.exe", "freedos.edlin.exe"]; const normalized = (value) => value.normalized_result; const retainedEntryViews = async ( @@ -233,40 +231,3 @@ export const measureNativeEntry = async ({ await call("close_binary", {}); } }; - -export const nativeEntrySeed = async (artifact, path) => { - if (artifact === "jq") { - const file = await open(path, "r"); - let header; - try { - header = (await file.read(Buffer.alloc(32), 0, 32, 0)).buffer; - } finally { - await file.close(); - } - if (header.subarray(0, 6).toString("hex") !== "7f454c460201") - throw new Error("Expected ELF64 little endian entry seed"); - return `0x${header.readBigUInt64LE(24).toString(16)}`; - } - if (artifact === "sharex.exe") { - const bytes = await readFile(path); - const pe = bytes.readUInt32LE(0x3c); - const optional = pe + 24; - if ( - bytes.subarray(pe, pe + 4).toString("hex") !== "50450000" || - bytes.readUInt16LE(pe + 4) !== 0x8664 || - bytes.readUInt16LE(optional) !== 0x20b || - bytes.readUInt32LE(optional + 108) <= 14 || - bytes.readUInt32LE(optional + 112 + 14 * 8) !== 0 || - bytes.readUInt32LE(optional + 116 + 14 * 8) !== 0 - ) - throw new Error( - "Expected the pinned native PE32+ entry seed without CLR metadata", - ); - const address = - bytes.readBigUInt64LE(optional + 24) + - BigInt(bytes.readUInt32LE(optional + 16)); - return `0x${address.toString(16)}`; - } - if (artifact === "freedos.edlin.exe") return undefined; - throw new Error("Select a pinned native artifact"); -}; diff --git a/tests/support/analysis-efficiency/packet-entry.mjs b/tests/support/analysis-efficiency/packet-entry.mjs deleted file mode 100644 index 23505b702a..0000000000 --- a/tests/support/analysis-efficiency/packet-entry.mjs +++ /dev/null @@ -1,314 +0,0 @@ -import { isDeepStrictEqual, promisify } from "node:util"; -import { resolve } from "node:path"; -import { pathToFileURL, fileURLToPath } from "node:url"; -import { execFile } from "node:child_process"; - -export const verifyPacketPage = (first, next, oracle) => { - if ( - first.artifact.sha256 !== oracle.sha256 || - next.artifact.sha256 !== oracle.sha256 || - first.output_window.matching_packets !== oracle.matching_packets || - next.output_window.matching_packets !== oracle.matching_packets || - !isDeepStrictEqual( - packetFacts("postgres", first).rows, - oracle.rows.slice(0, 12), - ) || - !isDeepStrictEqual( - packetFacts("postgres", next).rows, - oracle.rows.slice(12, 24), - ) - ) - throw new Error( - "Packet pages differ from independently parsed original matches", - ); -}; - -export const PACKET_ARTIFACTS = { - "wireshark-pgsql-jdbc.pcap": { - kind: "postgres", - fields: ["tcp.srcport", "tcp.dstport", "tcp.flags"], - extra: ["tcp.len"], - protocol: "tcp", - query: "tcp.flags", - discovered: "tcp.flags", - filter: "tcp.port == 5432", - }, - "wireshark-dns.cap": { - kind: "dns", - fields: [ - "dns.id", - "dns.flags.response", - "dns.flags.rcode", - "dns.qry.name", - "dns.qry.type", - ], - extra: ["dns.resp.name", "dns.resp.ttl", "udp.srcport", "udp.dstport"], - protocol: "dns", - query: "dns.flags.rcode", - discovered: "dns.flags.rcode", - filter: "dns.flags.response == 1 && dns.flags.rcode != 0", - }, - "wireshark-dhcp.pcap": { - kind: "dhcp", - fields: [ - "dhcp.id", - "dhcp.option.dhcp", - "dhcp.ip.your", - "dhcp.option.dhcp_server_id", - ], - extra: [ - "dhcp.type", - "dhcp.ip.client", - "dhcp.hw.mac_addr", - "udp.srcport", - "udp.dstport", - ], - protocol: "dhcp", - query: "Option 53:", - discovered: "dhcp.option.dhcp", - filter: "dhcp", - }, -}; - -export const packetRequest = (artifact, path, selection) => { - const descriptor = PACKET_ARTIFACTS[artifact]; - if (!descriptor) throw new Error("Unknown pinned packet question"); - const baseline = selection === "baseline"; - return { - path, - fields: baseline - ? [...descriptor.fields, ...descriptor.extra] - : descriptor.fields, - display_filter: - descriptor.kind === "postgres" && baseline - ? `${descriptor.filter} && frame.number <= 2000` - : descriptor.kind === "dns" && baseline - ? "dns" - : descriptor.filter, - ...(descriptor.kind === "postgres" && !baseline - ? { packet_limit: 2000, match_limit: 12 } - : {}), - }; -}; - -const number = (row, field) => { - const value = row[field]; - if ( - !Array.isArray(value) || - value.length !== 1 || - !/^(?:[0-9]+|0x[0-9a-f]+)$/iu.test(value[0]) - ) - throw new Error(`Expected one recorded integer for ${field}`); - const parsed = Number(value[0]); - if (!Number.isSafeInteger(parsed)) - throw new Error(`Unsafe recorded integer for ${field}`); - return parsed; -}; - -export const packetFacts = (kind, report) => { - let packets = report.packets; - if (kind === "postgres") packets = packets.slice(0, 12); - if (kind === "dns") - packets = packets.filter( - (row) => - number(row, "dns.flags.response") === 1 && - number(row, "dns.flags.rcode") !== 0, - ); - return { - kind, - matching_packets: - kind === "dns" ? packets.length : report.output_window.matching_packets, - rows: packets.map((row) => { - const frame = number(row, "frame.number"); - if (kind === "postgres") - return { - frame, - source_port: number(row, "tcp.srcport"), - destination_port: number(row, "tcp.dstport"), - flags: number(row, "tcp.flags"), - }; - if (kind === "dns") { - const names = row["dns.qry.name"]; - const types = row["dns.qry.type"]; - if ( - !Array.isArray(names) || - names.length === 0 || - !Array.isArray(types) || - types.length !== names.length - ) - throw new Error( - "The fixed DNS question requires recorded names and types; missing fields are unknown", - ); - return { - frame, - id: number(row, "dns.id"), - rcode: number(row, "dns.flags.rcode"), - names, - types: types.map((value) => number({ type: [value] }, "type")), - }; - } - const server = row["dhcp.option.dhcp_server_id"]; - const address = row["dhcp.ip.your"]; - if ( - !Array.isArray(address) || - address.length !== 1 || - (server !== undefined && - (!Array.isArray(server) || server.length !== 1)) - ) - throw new Error("Expected recorded DHCP address scalar"); - return { - frame, - id: number(row, "dhcp.id"), - message_type: number(row, "dhcp.option.dhcp"), - your_address: address[0], - ...(server === undefined ? {} : { server_identifier: server[0] }), - }; - }), - }; -}; - -export const measurePacketEntry = async ({ - values, - call, - inputSha, - catalogDefinitions, -}) => { - const descriptor = PACKET_ARTIFACTS[values.artifact]; - const { parseEvidence } = await import( - pathToFileURL(resolve(values["runtime-root"], "dist/domain/evidence.js")) - .href - ); - const { inspectPacketCaptureInputSchema, packetCaptureSchema } = await import( - pathToFileURL( - resolve(values["runtime-root"], "dist/domain/captures/packetCapture.js"), - ).href - ); - const { inspectPacketFieldsInputSchema, packetFieldsCatalogSchema } = - await import( - pathToFileURL( - resolve(values["runtime-root"], "dist/domain/captures/packetFields.js"), - ).href - ); - const observations = []; - const selection = values["packet-selection"]; - const checked = (evidence, operation, input, schema) => { - parseEvidence(evidence); - const report = schema.parse(evidence.normalized_result); - if ( - evidence.operation !== operation || - !isDeepStrictEqual(evidence.parameters, input) || - !isDeepStrictEqual(report.selection, input) - ) - throw new Error( - "Packet Evidence operation or choices differs from the request", - ); - if ( - operation === "inspect_packet_capture" && - (evidence.subject?.local_path !== values.path || - evidence.subject?.digest?.sha256 !== inputSha || - report.artifact.path !== values.path || - report.artifact.sha256 !== inputSha) - ) - throw new Error("Packet Evidence identifies different capture bytes"); - if ( - operation === "inspect_packet_fields" && - (evidence.subject?.local_path !== report.tool.path || - evidence.subject?.digest?.sha256 !== report.tool.sha256) - ) - throw new Error("Field discovery identifies a different executable"); - observations.push({ - evidence_id: evidence.evidence_id, - operation, - selection: report.selection, - output_window: report.output_window, - read_scope: report.read_scope ?? null, - limitations: evidence.limitations, - }); - return report; - }; - if (selection === "discovery") { - const input = inspectPacketFieldsInputSchema.parse({ - kind: "fields", - protocol: descriptor.protocol, - query: descriptor.query, - limit: 1, - }); - const found = checked( - await call("inspect_packet_fields", input), - "inspect_packet_fields", - input, - packetFieldsCatalogSchema, - ); - if ( - !found.records.some((row) => row.abbreviation === descriptor.discovered) - ) - throw new Error( - "Selected installation does not advertise the required protocol field", - ); - } - const input = inspectPacketCaptureInputSchema.parse( - packetRequest(values.artifact, values.path, selection), - ); - for (const name of [ - "inspect_packet_capture", - ...(selection === "discovery" ? ["inspect_packet_fields"] : []), - ]) - if (!catalogDefinitions.has(name)) - throw new Error(`Missing live packet contract ${name}`); - const report = checked( - await call("inspect_packet_capture", input), - "inspect_packet_capture", - input, - packetCaptureSchema, - ); - const facts = packetFacts(descriptor.kind, report); - if (selection === "pages") { - const { stdout } = await promisify(execFile)( - "python3", - [ - fileURLToPath(new URL("./packet-oracle.py", import.meta.url)), - "postgres", - values.path, - "--row-limit", - "24", - ], - { timeout: 30_000, maxBuffer: 128 * 1024 }, - ); - const oracle = JSON.parse(stdout); - const nextInput = inspectPacketCaptureInputSchema.parse({ - ...input, - match_offset: 12, - }); - const next = checked( - await call("inspect_packet_capture", nextInput), - "inspect_packet_capture", - nextInput, - packetCaptureSchema, - ); - const repeated = checked( - await call("inspect_packet_capture", input), - "inspect_packet_capture", - input, - packetCaptureSchema, - ); - verifyPacketPage(report, next, oracle); - if ( - !isDeepStrictEqual(report.packets, repeated.packets) || - !isDeepStrictEqual(report.output_window, repeated.output_window) || - next.artifact.sha256 !== report.artifact.sha256 || - next.output_window.matching_packets !== - report.output_window.matching_packets || - Number(next.packets[0]?.["frame.number"]?.[0]) <= - Number(report.packets.at(-1)?.["frame.number"]?.[0]) - ) - throw new Error( - "Packet page order, digest or repeated observations changed", - ); - } - return { - artifact_sha256: inputSha, - facts, - delivery_observations: observations, - evidence_ids: [...new Set(observations.map((row) => row.evidence_id))], - }; -}; diff --git a/tests/support/analysis-efficiency/packet-oracle.py b/tests/support/analysis-efficiency/packet-oracle.py deleted file mode 100644 index db5797b5f6..0000000000 --- a/tests/support/analysis-efficiency/packet-oracle.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Independent, narrow oracle for the three pinned Ethernet/IPv4 PCAPs. - -Reads header bytes, not TShark output. No PCAPNG, IP fragmentation or TCP -reassembly support is claimed. DNS compression follows RFC 1035; DHCP options -53/54 follow RFC 2132. Unsupported selected traffic fails rather than guessing. -""" -import argparse -import hashlib -import json -import struct -from pathlib import Path - - -def require(condition, reason): - if not condition: - raise ValueError(reason) - - -def packets(data): - require(len(data) >= 24, 'truncated PCAP header') - magic = data[:4] - require(magic in (b'\xd4\xc3\xb2\xa1', b'\xa1\xb2\xc3\xd4'), - 'oracle requires microsecond classic PCAP') - endian = '<' if magic == b'\xd4\xc3\xb2\xa1' else '>' - require(struct.unpack_from(endian + 'HH', data, 4) == (2, 4), - 'unsupported PCAP version') - require(struct.unpack_from(endian + 'I', data, 20)[0] == 1, - 'oracle requires Ethernet') - offset, frame = 24, 0 - while offset < len(data): - require(offset + 16 <= len(data), 'truncated PCAP record header') - captured, original = struct.unpack_from(endian + 'II', data, offset + 8) - offset += 16 - require(captured <= original and offset + captured <= len(data), - 'invalid captured packet length') - frame += 1 - yield frame, data[offset:offset + captured] - offset += captured - - -def transport(packet): - require(len(packet) >= 14, 'truncated Ethernet header') - if packet[12:14] == b'\x08\x06': - return None # ARP cannot contain the selected TCP/UDP messages. - require(packet[12:14] == b'\x08\x00', 'unsupported Ethernet protocol') - ip = packet[14:] - require(len(ip) >= 20 and ip[0] >> 4 == 4, 'invalid IPv4 header') - header = (ip[0] & 15) * 4 - total = struct.unpack_from('!H', ip, 2)[0] - require(20 <= header <= len(ip) and total >= header, 'invalid IPv4 lengths') - require(total <= len(ip), 'truncated IPv4 packet') - require(struct.unpack_from('!H', ip, 6)[0] & 0x3fff == 0, - 'fragmentation is outside oracle scope') - return ip[9], ip[header:min(total, len(ip))] - - -def dns_name(message, offset): - labels, visited, end = [], set(), None - while True: - require(offset < len(message) and offset not in visited, - 'invalid DNS name or compression cycle') - visited.add(offset) - size = message[offset] - if size & 0xc0 == 0xc0: - require(offset + 1 < len(message), 'truncated DNS compression pointer') - if end is None: - end = offset + 2 - offset = ((size & 0x3f) << 8) | message[offset + 1] - continue - require(size & 0xc0 == 0 and offset + 1 + size <= len(message), - 'invalid DNS label') - offset += 1 - if size == 0: - return '.'.join(labels), end if end is not None else offset - labels.append(message[offset:offset + size].decode('ascii')) - require(sum(len(label) + 1 for label in labels) <= 255, - 'DNS name exceeds protocol length') - offset += size - - -def dhcp_options(payload): - require(len(payload) >= 240 and payload[236:240] == b'\x63\x82\x53\x63', - 'invalid DHCP fixed header or cookie') - options, offset = {}, 240 - while offset < len(payload): - kind = payload[offset] - offset += 1 - if kind == 255: - return options - if kind == 0: - continue - require(offset < len(payload), 'missing DHCP option length') - size = payload[offset] - offset += 1 - require(offset + size <= len(payload), 'truncated DHCP option') - require(kind not in options, 'duplicate DHCP options outside oracle scope') - options[kind] = payload[offset:offset + size] - offset += size - raise ValueError('DHCP end option missing') - - -def analyze(data, kind, row_limit=12): - rows, matching, count = [], 0, 0 - for frame, packet in packets(data): - count = frame - decoded = transport(packet) - if decoded is None: - continue - protocol, segment = decoded - if kind == 'postgres' and protocol == 6 and frame <= 2000: - require(len(segment) >= 20, 'truncated TCP header') - tcp_header = (segment[12] >> 4) * 4 - require(20 <= tcp_header <= len(segment), 'invalid or truncated TCP header/options') - source, destination = struct.unpack_from('!HH', segment) - if 5432 not in (source, destination): - continue - matching += 1 - if len(rows) < row_limit: - rows.append({'frame': frame, 'source_port': source, - 'destination_port': destination, - 'flags': struct.unpack_from('!H', segment, 12)[0] & 0xfff}) - elif kind in ('dns', 'dhcp') and protocol == 17: - require(len(segment) >= 8, 'truncated UDP header') - source, destination, length = struct.unpack_from('!HHH', segment) - selected = 53 in (source, destination) if kind == 'dns' else ( - source, destination) in ((67, 68), (68, 67)) - if not selected: - continue - require(8 <= length <= len(segment), 'truncated selected UDP message') - payload = segment[8:length] - if kind == 'dns': - require(len(payload) >= 12, 'truncated DNS header') - identifier, flags, questions = struct.unpack_from('!HHH', payload) - if not flags & 0x8000 or flags & 15 == 0: - continue - matching += 1 - offset, names, types = 12, [], [] - for _ in range(questions): - name, offset = dns_name(payload, offset) - require(offset + 4 <= len(payload), 'truncated DNS question') - types.append(struct.unpack_from('!H', payload, offset)[0]) - names.append(name) - offset += 4 - rows.append({'frame': frame, 'id': identifier, 'rcode': flags & 15, - 'names': names, 'types': types}) - else: - options = dhcp_options(payload) - require(53 in options and len(options[53]) == 1, - 'missing DHCP message type') - server = options.get(54) - require(server is None or len(server) == 4, 'invalid DHCP server ID') - require(52 not in options, 'DHCP overload outside oracle scope') - matching += 1 - row = {'frame': frame, 'id': struct.unpack_from('!I', payload, 4)[0], - 'message_type': options[53][0], - 'your_address': '.'.join(map(str, payload[16:20]))} - if server is not None: - row['server_identifier'] = '.'.join(map(str, server)) - rows.append(row) - return {'kind': kind, 'capture_frames': count, 'matching_packets': matching, - 'question_input_frames': min(2000, count) if kind == 'postgres' else count, - 'rows': rows, 'scope': 'selected recorded headers; no network completeness claim'} - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument('kind', choices=['postgres', 'dns', 'dhcp']) - parser.add_argument('path', type=Path) - parser.add_argument('--row-limit', type=int, choices=[12, 24], default=12, - help='PostgreSQL rows; 24 independently checks the second page') - args = parser.parse_args() - with args.path.open('rb') as source: - data = source.read(32 * 1024 * 1024 + 1) - require(len(data) <= 32 * 1024 * 1024, 'oracle input budget exceeded') - print(json.dumps({'sha256': hashlib.sha256(data).hexdigest(), - **analyze(data, args.kind, args.row_limit)}, indent=2)) - - -if __name__ == '__main__': - main() diff --git a/tests/support/analysis-efficiency/saz-entry.mjs b/tests/support/analysis-efficiency/saz-entry.mjs deleted file mode 100644 index 4952fcd5b1..0000000000 --- a/tests/support/analysis-efficiency/saz-entry.mjs +++ /dev/null @@ -1,326 +0,0 @@ -import assert from "node:assert/strict"; -import { execFile } from "node:child_process"; -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { promisify } from "node:util"; - -const corpus = JSON.parse( - await readFile(new URL("./corpus.json", import.meta.url), "utf8"), -); -export const SAZ_ARTIFACTS = Object.fromEntries( - corpus.artifacts - .filter((entry) => entry.saz_question) - .map((entry) => [entry.id, entry]), -); - -export function sazRequest(artifact, path, selection) { - const question = Object.hasOwn(SAZ_ARTIFACTS, artifact) - ? SAZ_ARTIFACTS[artifact].saz_question - : undefined; - if (!question) throw new Error("Select a pinned SAZ question"); - return { - capture_path: path, - format: "saz", - ...(selection === "baseline" - ? {} - : { record_ordinals: question.record_ordinals }), - sensitive_values: [], - }; -} - -export function verifySazOracleIdentity(artifact, oracle) { - assert.ok( - Object.hasOwn(SAZ_ARTIFACTS, artifact), - "Unknown pinned SAZ question", - ); - const entry = SAZ_ARTIFACTS[artifact]; - assert.equal(oracle.kind, "saz"); - assert.equal(oracle.artifact, artifact); - assert.equal(oracle.sha256, entry.sha256, "Wrong immutable corpus"); - assert.equal(oracle.total_records, entry.saz_question.total_records); - assert.deepEqual(oracle.record_ordinals, entry.saz_question.record_ordinals); -} - -export function sazEvidenceParameters(input, totalRecords) { - return { - capture_path: input.capture_path, - format: input.format, - record_ordinals: - input.record_ordinals ?? - Array.from({ length: totalRecords }, (_, index) => index), - sensitive_value_count: input.sensitive_values.length, - }; -} - -export function verifySazRecords(records, oracle) { - assert.equal(oracle.kind, "saz", "Wrong independent question"); - assert.equal( - records.length, - oracle.records.length, - "Selected coverage differs", - ); - return records.map((record, index) => { - const raw = oracle.records[index]; - assert.equal(record.ordinal, raw.ordinal, "Caller order/ordinal differs"); - assert.equal( - record.reported.session_id, - raw.session_id, - "Padded source identity differs", - ); - assert.deepEqual( - record.location, - { kind: "archive-members", members: raw.members }, - "Original member coordinates differ", - ); - for (const side of ["request", "response"]) { - const expected = raw[side], - reported = record.reported[side]; - if (expected === null) { - assert.equal(reported, null, "Absent message invented"); - assert.ok( - !record.binary_fields.some( - (field) => field.pointer === `/${side}/body`, - ), - "Absent body invented", - ); - continue; - } - assert.equal(reported.member, expected.member); - for (const field of side === "request" - ? ["method", "url", "http_version"] - : ["status", "reason", "http_version"]) - assert.equal( - reported[field], - expected[field], - "Stored HTTP start line differs", - ); - const contentTypes = (headers) => - headers.filter( - (header) => header.name.toLowerCase() === "content-type", - ); - assert.deepEqual( - contentTypes(reported.headers), - contentTypes(expected.headers), - "Content-Type order/spelling/values differ", - ); - const bodies = record.binary_fields.filter( - (field) => field.pointer === `/${side}/body`, - ); - assert.equal(bodies.length, 1, "Stored body representation differs"); - const body = bodies[0]; - assert.equal(body.state, "retained", "Stored body unexpectedly unknown"); - assert.equal( - body.representation, - "producer-bytes", - "Encoded source representation differs", - ); - for (const field of ["bytes", "sha256", "content_base64"]) - assert.equal( - body[field], - expected.body[field], - "Stored encoded body bytes differ", - ); - assert.equal( - reported.body, - expected.body.text, - "UTF-8 projection or empty body differs", - ); - } - const response = raw.response; - return { - ordinal: record.ordinal, - session_id: raw.session_id, - members: raw.members, - request: - raw.request === null - ? null - : { method: raw.request.method, url: raw.request.url }, - response: - response === null - ? null - : { - status: response.status, - http_version: response.http_version, - content_types: response.headers.filter( - (header) => header.name.toLowerCase() === "content-type", - ), - body: { - bytes: response.body.bytes, - sha256: response.body.sha256, - }, - }, - }; - }); -} - -export async function measureSazEntry({ values, call, inputSha }) { - const runtime = (file) => - pathToFileURL(resolve(values["runtime-root"], file)).href; - const { parseEvidence } = await import(runtime("dist/domain/evidence.js")); - const { inspectWebNetworkCaptureInputSchema, webNetworkCaptureSchema } = - await import(runtime("dist/domain/webNetworkCapture.js")); - const { inspectAnalysisViewInputSchema, analysisViewResultSchema } = - await import(runtime("dist/domain/analysisView/analysisView.js")); - const read = await promisify(execFile)( - "python3", - [ - fileURLToPath(new URL("./saz-oracle.py", import.meta.url)), - values.artifact, - values.path, - ], - { maxBuffer: 2 * 1024 * 1024, timeout: 30_000 }, - ); - const oracle = JSON.parse(read.stdout); - verifySazOracleIdentity(values.artifact, oracle); - assert.equal(oracle.sha256, inputSha); - const input = inspectWebNetworkCaptureInputSchema.parse( - sazRequest(values.artifact, values.path, values["saz-selection"]), - ); - const observations = []; - const checked = (evidence, schema) => { - evidence = parseEvidence(evidence); - assert.equal(evidence.subject.local_path, values.path); - assert.equal(evidence.subject.digest.sha256, inputSha); - const report = schema.parse(evidence.normalized_result); - observations.push({ - evidence_id: evidence.evidence_id, - operation: evidence.operation, - parameters: evidence.parameters, - limitations: evidence.limitations, - coverage: report.coverage ?? { - total_records: report.total_records, - retained_records: report.records.length, - }, - }); - return report; - }; - const readView = async (source, view) => { - const request = inspectAnalysisViewInputSchema.parse({ source, view }); - const evidence = await call("inspect_analysis_view", request); - assert.equal(evidence.operation, "inspect_analysis_view"); - assert.deepEqual(evidence.parameters, { - parent_evidence_id: source.evidence_id, - view: request.view, - }); - const result = checked(evidence, analysisViewResultSchema); - assert.equal(result.kind, request.view.kind); - assert.equal(result.parent_evidence_id, source.evidence_id); - assert.equal(result.parent_operation, "inspect_web_network_capture"); - assert.deepEqual(result.view, request.view); - return result; - }; - let records, decoder; - if (values["saz-selection"] !== "retained") { - let evidence; - try { - evidence = await call("inspect_web_network_capture", input); - } catch (failure) { - const error = failure.value?.error; - const limits = error?.details?.reported_limits; - if ( - values["saz-selection"] !== "baseline" || - error?.code !== "resource_constraint" || - error.details.operation !== "inspect_web_network_capture" || - limits?.boundary !== "mcp-response" || - !limits.evidence_reference - ) - throw failure; - // Recover only the exact retained reference returned by this failed call. - // The failed RPC remains in the shared measurement log and task costs. - const source = limits.evidence_reference; - assert.equal(source.kind, "retained-evidence"); - const summary = await readView(source, { kind: "summary" }); - assert.equal(summary.summary.format, "saz"); - assert.equal(summary.summary.total_records, oracle.total_records); - assert.equal(summary.summary.retained_records, oracle.total_records); - decoder = summary.summary.decoder; - records = []; - for (const ordinal of oracle.record_ordinals) { - const page = await readView(source, { - kind: "capture", - detail: "complete", - offset: ordinal, - limit: 1, - }); - assert.equal(page.coverage.total, oracle.total_records); - assert.equal(page.items.length, 1); - records.push(page.items[0]); - } - } - if (evidence !== undefined) { - assert.equal(evidence.operation, "inspect_web_network_capture"); - assert.deepEqual( - evidence.parameters, - sazEvidenceParameters(input, oracle.total_records), - ); - const report = checked(evidence, webNetworkCaptureSchema); - assert.equal(report.format, "saz"); - assert.equal(report.total_records, oracle.total_records); - assert.equal( - report.records.length, - values["saz-selection"] === "baseline" - ? oracle.total_records - : oracle.records.length, - ); - if (values["saz-selection"] === "baseline") - assert.deepEqual( - report.records.map((record) => record.ordinal), - Array.from({ length: oracle.total_records }, (_, index) => index), - ); - records = - values["saz-selection"] === "baseline" - ? oracle.record_ordinals.map((ordinal) => - report.records.find((record) => record.ordinal === ordinal), - ) - : report.records; - decoder = report.decoder; - } - } else { - const initial = await call("inspect_web_network_capture", { - ...input, - detail: "summary", - }); - const summary = checked(initial, analysisViewResultSchema); - assert.equal(summary.kind, "summary"); - assert.equal(initial.operation, "inspect_analysis_view"); - assert.deepEqual(initial.parameters, { - parent_evidence_id: summary.parent_evidence_id, - view: { kind: "summary" }, - }); - assert.equal(summary.summary.format, "saz"); - assert.equal(summary.parent_operation, "inspect_web_network_capture"); - assert.equal(summary.summary.total_records, oracle.total_records); - assert.equal(summary.summary.retained_records, oracle.records.length); - decoder = summary.summary.decoder; - const source = { - kind: "retained-evidence", - evidence_id: summary.parent_evidence_id, - }; - const page = async (detail, offset, limit) => { - return readView(source, { kind: "capture", detail, offset, limit }); - }; - const preview = await page("preview", 0, 3); - assert.deepEqual( - preview.items.map((record) => record.ordinal), - oracle.record_ordinals, - ); - const first = await page("complete", 0, 2), - next = await page("complete", 2, 2), - repeated = await page("complete", 0, 2); - assert.deepEqual(first.items, repeated.items); - assert.deepEqual(first.coverage, repeated.coverage); - assert.equal(first.coverage.next_offset, 2); - assert.equal(first.coverage.exhausted, false); - assert.equal(next.coverage.exhausted, true); - assert.equal(next.coverage.next_offset, null); - records = [...first.items, ...next.items]; - } - return { - artifact_sha256: inputSha, - total_records: oracle.total_records, - decoder, - facts: verifySazRecords(records, oracle), - delivery_observations: observations, - }; -} diff --git a/tests/support/analysis-efficiency/saz-oracle.py b/tests/support/analysis-efficiency/saz-oracle.py deleted file mode 100644 index 6722289a2a..0000000000 --- a/tests/support/analysis-efficiency/saz-oracle.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Independent stored-HTTP facts for two immutable public Fiddler archives. - -Only digest-pinned inputs are admitted before ZIP object allocation. Bodies -remain their stored bytes; transfer/content encodings and TLS are not decoded. -""" - -import argparse -import base64 -import hashlib -import io -import json -from pathlib import Path -import re -import zipfile - - -def stored_http(data, request): - boundaries = [(data.find(separator), separator) - for separator in [b'\r\n\r\n', b'\n\n']] - available = [(index, separator) for index, separator in boundaries if index >= 0] - if not available: - raise ValueError('Stored message has no header/body separator') - boundary, separator = min(available, key=lambda item: item[0]) - lines = data[:boundary].split(b'\r\n' if separator.startswith(b'\r') else b'\n') - start = lines[0].decode('latin1') - fields = start.split(' ') - if request: - if len(fields) != 3 or fields[2] not in ['HTTP/1.0', 'HTTP/1.1']: - raise ValueError('Unsupported stored request line') - result = dict(zip(['method', 'url', 'http_version'], fields)) - else: - fields = start.split(' ', 2) - if len(fields) < 2 or fields[0] not in ['HTTP/1.0', 'HTTP/1.1'] or not re.fullmatch(r'[0-9]{3}', fields[1]): - raise ValueError('Unsupported stored response line') - result = {'http_version': fields[0], 'status': fields[1], - 'reason': fields[2] if len(fields) == 3 else ''} - headers = [] - newline = '\r\n' if separator.startswith(b'\r') else '\n' - for raw in lines[1:]: - line = raw.decode('latin1') - if line.startswith((' ', '\t')): - if not headers: - raise ValueError('Header continuation has no owning field') - headers[-1]['value'] += newline + line - continue - name, delimiter, value = line.partition(':') - if not delimiter or not name: - raise ValueError('Stored header has no field delimiter') - headers.append({'name': name, 'value': value}) - body = data[boundary + len(separator):] - try: - body_text = body.decode('utf8', errors='strict') - except UnicodeDecodeError: - body_text = None - return result | {'headers': headers, 'body': { - 'content_base64': base64.b64encode(body).decode('ascii'), - 'bytes': len(body), 'sha256': hashlib.sha256(body).hexdigest(), - 'text': body_text, - }} - - -def read_saz(artifact, path): - corpus = json.loads(Path(__file__).with_name('corpus.json').read_text()) - entry = next((item for item in corpus['artifacts'] - if item['id'] == artifact and 'saz_question' in item), None) - if entry is None: - raise ValueError('Select a pinned SAZ artifact') - # Real source sizes bound the snapshot; size/digest gate ZIP allocation. - if path.stat().st_size != entry['bytes']: - raise ValueError('Selected file size differs from pinned SAZ corpus') - with path.open('rb') as source: - data = source.read(entry['bytes'] + 1) - digest = hashlib.sha256(data).hexdigest() - if len(data) != entry['bytes'] or digest != entry['sha256']: - raise ValueError('Selected bytes differ from pinned SAZ corpus') - question = entry['saz_question'] - with zipfile.ZipFile(io.BytesIO(data)) as archive: - inventory = archive.infolist() - if (len(inventory) != question['members'] or - sum(item.file_size for item in inventory) != question['expanded_bytes']): - raise ValueError('Pinned directory coverage differs') - groups = {} - for item in inventory: - match = re.fullmatch(r'raw/(\d+)_(c\.txt|s\.txt|m\.xml|w\.txt|g\.txt)', item.filename) - if match: - groups.setdefault(match[1], []).append(item) - if len(groups) != question['total_records']: - raise ValueError('Pinned session coverage differs') - ordered = list(groups.items()) - records = [] - for ordinal in question['record_ordinals']: - session_id, members = ordered[ordinal] - result = {'ordinal': ordinal, 'session_id': session_id, - 'members': [item.filename for item in members], - 'request': None, 'response': None} - for item in members: - side = 'request' if item.filename.endswith('_c.txt') else 'response' if item.filename.endswith('_s.txt') else None - if side is None: - continue - with archive.open(item) as source: - content = source.read(item.file_size + 1) - if len(content) != item.file_size: - raise ValueError('Stored member size differs') - result[side] = {'member': item.filename, **stored_http(content, side == 'request')} - records.append(result) - with path.open('rb') as source: - after = source.read(entry['bytes'] + 1) - if hashlib.sha256(after).hexdigest() != digest: - raise ValueError('Selected archive changed during independent read') - return {'kind': 'saz', 'artifact': artifact, 'sha256': digest, - 'total_records': len(groups), 'record_ordinals': question['record_ordinals'], - 'records': records, - 'scope': 'Stored HTTP bytes and ZIP member coordinates; no body decompression, live attribution or certificate interpretation'} - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('artifact') - parser.add_argument('path', type=Path) - args = parser.parse_args() - print(json.dumps(read_saz(args.artifact, args.path))) diff --git a/tests/support/analysis-efficiency/signal-entry.mjs b/tests/support/analysis-efficiency/signal-entry.mjs deleted file mode 100644 index b44d4d66a0..0000000000 --- a/tests/support/analysis-efficiency/signal-entry.mjs +++ /dev/null @@ -1,264 +0,0 @@ -import { execFile } from "node:child_process"; -import { readFile } from "node:fs/promises"; -import { resolve } from "node:path"; -import { fileURLToPath, pathToFileURL } from "node:url"; -import { isDeepStrictEqual, promisify } from "node:util"; - -const corpus = JSON.parse( - await readFile(new URL("./corpus.json", import.meta.url), "utf8"), -); - -export const SIGNAL_ARTIFACTS = { - "sigrok-uart-max3232e.sr": { - kind: "uart", - decoders: "uart:rx=MAX3232E DOUT1:baudrate=57600:format=hex:invert_rx=yes", - annotations: "uart=rx-data", - query: "rx-data", - firstPage: 32, - }, - "sigrok-i2c-nunchuk.sr": { - kind: "i2c", - decoders: "i2c:scl=SCL:sda=SDA", - annotations: "i2c=data-read", - query: "data-read", - firstPage: 9, - }, -}; - -export function signalRequest(artifact, path, selection) { - const descriptor = SIGNAL_ARTIFACTS[artifact]; - if (!descriptor) throw new Error("Unknown fixed signal question"); - return { - path, - decoders: descriptor.decoders, - input_format: null, - annotations: selection === "baseline" ? null : descriptor.annotations, - line_offset: 0, - line_limit: selection === "pages" ? descriptor.firstPage : null, - }; -} - -export function signalDataRows(kind, text) { - const pattern = - kind === "uart" - ? /^(\d+)-(\d+) uart-1: ([0-9A-F]{2})$/ - : /^(\d+)-(\d+) i2c-1: Data read: ([0-9A-F]{2})$/; - const rows = []; - for (const line of text.split("\n")) { - const match = pattern.exec(line); - if (!match) continue; - const start = Number(match[1]), - end = Number(match[2]); - if ( - !Number.isSafeInteger(start) || - !Number.isSafeInteger(end) || - start > end - ) - throw new Error("Unsupported decoded sample interval"); - rows.push({ start, end, byte: Number.parseInt(match[3], 16) }); - } - return rows; -} - -export function signalFacts(kind, rows) { - if (kind === "uart") return { kind, bytes: rows.map((row) => row.byte) }; - if (kind !== "i2c" || rows.length !== 18) - throw new Error( - "The fixed I2C question requires three complete six-byte reads", - ); - return { - kind, - reads: [0, 6, 12].map((offset) => - rows.slice(offset, offset + 6).map((row) => row.byte), - ), - }; -} - -export function verifySignalOracleIdentity(artifact, oracle) { - const pinned = corpus.artifacts.find((entry) => entry.id === artifact); - if ( - !pinned || - oracle.kind !== SIGNAL_ARTIFACTS[artifact]?.kind || - oracle.sha256 !== pinned.sha256 - ) - throw new Error( - "Independent oracle does not identify the pinned signal corpus", - ); -} - -export function verifySignalRows(kind, rows, oracle, digest) { - const expected = - kind === "uart" - ? oracle.rows - : oracle.reads.flatMap((read) => - read.bytes.slice(1).map((byte, index) => ({ - ...byte, - after_previous: read.bytes[index].last_clock, - before_next: read.bytes[index + 2]?.first_clock ?? read.stop, - })), - ); - if ( - oracle.kind !== kind || - oracle.sha256 !== digest || - rows.length !== expected.length - ) - throw new Error( - "Signal observations differ from independent sample identity/coverage", - ); - for (let index = 0; index < rows.length; index++) { - const observed = rows[index], - raw = expected[index]; - if ( - observed.byte !== raw.byte || - observed.start < 0 || - observed.end >= oracle.total_samples - ) - throw new Error("Decoded data differs from independent sample bits"); - if ( - kind === "uart" - ? !Number.isSafeInteger(raw.first_data_sample) || - !Number.isSafeInteger(raw.last_data_sample) || - observed.start < raw.start || - observed.end >= raw.end_exclusive || - observed.start > raw.first_data_sample || - observed.end < raw.last_data_sample - : !Number.isSafeInteger(raw.after_previous) || - !Number.isSafeInteger(raw.before_next) || - observed.start <= raw.after_previous || - observed.end >= raw.before_next || - observed.start > raw.first_clock || - observed.end < raw.last_clock - ) - throw new Error( - "Decoded interval does not identify the independently read byte", - ); - } -} - -export async function measureSignalEntry({ - values, - call, - inputSha, - catalogDefinitions, -}) { - const descriptor = SIGNAL_ARTIFACTS[values.artifact]; - const runtime = (file) => - pathToFileURL(resolve(values["runtime-root"], file)).href; - const { parseEvidence } = await import(runtime("dist/domain/evidence.js")); - const { decodeSignalRecordingInputSchema, signalRecordingSchema } = - await import(runtime("dist/domain/signals/signalRecording.js")); - const { inspectSignalDecodersInputSchema, signalDecodersSchema } = - await import(runtime("dist/domain/signals/signalDecoders.js")); - const observations = []; - const checked = (evidence, operation, input, schema) => { - parseEvidence(evidence); - const report = schema.parse(evidence.normalized_result); - if ( - evidence.operation !== operation || - !isDeepStrictEqual(evidence.parameters, input) || - !isDeepStrictEqual(report.selection, input) - ) - throw new Error( - "Signal Evidence differs from the actual requested operation or choices", - ); - if ( - operation === "decode_signal_recording" - ? evidence.subject?.local_path !== values.path || - evidence.subject?.digest?.sha256 !== inputSha || - report.artifact.path !== values.path || - report.artifact.sha256 !== inputSha - : evidence.subject?.local_path !== report.tool.path || - evidence.subject?.digest?.sha256 !== report.tool.sha256 - ) - throw new Error( - "Signal Evidence identifies different recording/tool bytes", - ); - observations.push({ - evidence_id: evidence.evidence_id, - operation, - selection: report.selection, - output_window: report.output_window, - limitations: evidence.limitations, - }); - return report; - }; - const selection = values["signal-selection"]; - for (const name of [ - "decode_signal_recording", - ...(selection === "discovery" ? ["inspect_signal_decoders"] : []), - ]) - if (!catalogDefinitions.has(name)) - throw new Error(`Missing live signal contract ${name}`); - if (selection === "discovery") { - const input = inspectSignalDecodersInputSchema.parse({ - decoder: descriptor.kind, - query: descriptor.query, - limit: 5, - }); - const report = checked( - await call("inspect_signal_decoders", input), - "inspect_signal_decoders", - input, - signalDecodersSchema, - ); - if (!report.help_lines.some((line) => line.text.includes(descriptor.query))) - throw new Error( - "Installed decoder help does not establish the selected annotation class", - ); - } - const input = decodeSignalRecordingInputSchema.parse( - signalRequest(values.artifact, values.path, selection), - ); - const first = checked( - await call("decode_signal_recording", input), - "decode_signal_recording", - input, - signalRecordingSchema, - ); - let rows = signalDataRows(descriptor.kind, first.annotations_text); - if (selection === "pages") { - const nextInput = decodeSignalRecordingInputSchema.parse({ - ...input, - line_offset: descriptor.firstPage, - line_limit: null, - }); - const next = checked( - await call("decode_signal_recording", nextInput), - "decode_signal_recording", - nextInput, - signalRecordingSchema, - ); - const repeated = checked( - await call("decode_signal_recording", input), - "decode_signal_recording", - input, - signalRecordingSchema, - ); - if ( - first.annotations_text !== repeated.annotations_text || - !isDeepStrictEqual(first.output_window, repeated.output_window) || - first.output_window.total_lines !== next.output_window.total_lines - ) - throw new Error("Repeated/next display pages changed observation scope"); - rows = [...rows, ...signalDataRows(descriptor.kind, next.annotations_text)]; - } - const { stdout } = await promisify(execFile)( - "python3", - [ - fileURLToPath(new URL("./signal-oracle.py", import.meta.url)), - descriptor.kind, - values.path, - ...(descriptor.kind === "uart" ? ["--invert"] : []), - ], - { timeout: 30_000, maxBuffer: 256 * 1024 }, - ); - const oracle = JSON.parse(stdout); - verifySignalOracleIdentity(values.artifact, oracle); - verifySignalRows(descriptor.kind, rows, oracle, inputSha); - return { - artifact_sha256: inputSha, - facts: signalFacts(descriptor.kind, rows), - delivery_observations: observations, - evidence_ids: [...new Set(observations.map((item) => item.evidence_id))], - }; -} diff --git a/tests/support/analysis-efficiency/signal-oracle.py b/tests/support/analysis-efficiency/signal-oracle.py deleted file mode 100644 index dbccf01a24..0000000000 --- a/tests/support/analysis-efficiency/signal-oracle.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Independent readers for the two pinned, single-device digital SR recordings.""" - -import argparse -from bisect import bisect_right -import configparser -import hashlib -import io -import json -from pathlib import Path -import re -import struct -import zipfile - - -def read_edges(path, channels): - size = path.stat().st_size - if size > 32 * 1024 * 1024: - raise ValueError('Compressed SR input exceeds the experiment budget') - with path.open('rb') as source: - admitted = source.read(size + 1) - if len(admitted) != size: - raise ValueError('SR input size changed during admission') - marker = admitted.rfind(b'PK\x05\x06', max(0, len(admitted) - 65557)) - if marker < 0 or len(admitted) - marker < 22: - raise ValueError('Missing classic ZIP directory receipt') - header = struct.unpack_from('<4s4H2IH', admitted, marker) - if (marker >= 20 and admitted[marker-20:marker-16] == b'PK\x06\x07'): - raise ValueError('ZIP64 directory is unsupported') - if header[1] or header[2] or header[3] != header[4] or not 1 <= header[4] <= 64 or header[5] > 65536 or marker + 22 + header[7] != len(admitted) or header[6] + header[5] != marker: - raise ValueError('Unsupported or excessive ZIP directory representation') - directory = memoryview(admitted)[header[6]:marker] - position = count = 0 - while position < len(directory): - if count >= 64 or len(directory) - position < 46 or directory[position:position+4] != b'PK\x01\x02': - raise ValueError('Unsupported or excessive actual ZIP directory records') - name_size, extra_size, comment_size = struct.unpack_from('<3H', directory, position + 28) - end = position + 46 + name_size + extra_size + comment_size - if end > len(directory): - raise ValueError('Incomplete actual ZIP directory record') - extra = position + 46 + name_size - extra_end = extra + extra_size - while extra < extra_end: - if extra_end - extra < 4: - raise ValueError('Incomplete ZIP extra field') - tag, length = struct.unpack_from('<2H', directory, extra) - if tag == 1: - raise ValueError('ZIP64 member is unsupported') - extra += 4 + length - if extra > extra_end: - raise ValueError('Incomplete ZIP extra field') - count += 1 - position = end - if count != header[4]: - raise ValueError('Declared and actual ZIP directory counts differ') - before = hashlib.sha256(admitted).hexdigest() - # Admission and ZipFile parse the same bounded snapshot, not a reopened path. - with zipfile.ZipFile(io.BytesIO(admitted), allowZip64=False) as archive: - entries = archive.infolist() - names = [item.filename for item in entries] - if len(names) != len(set(names)) or len(names) > 64: - raise ValueError("Ambiguous or excessive SR members") - if sum(item.file_size for item in entries) > 32 * 1024 * 1024: - raise ValueError("Expanded SR input exceeds the experiment budget") - if archive.getinfo('metadata').file_size > 8192: - raise ValueError("Excessive SR metadata") - # Declared sizes are untrusted. A bounded read also caps each inflater - # step; logic members below likewise use bounded block reads. - with archive.open('metadata') as source: - metadata_bytes = source.read(8193) - if len(metadata_bytes) > 8192: - raise ValueError("Expanded SR metadata exceeds the experiment budget") - metadata = metadata_bytes.decode('utf-8') - config = configparser.ConfigParser(interpolation=None) - config.read_string(metadata) - if set(config.sections()) != {'global', 'device 1'}: - raise ValueError("Only the pinned single-device profile is supported") - device = config['device 1'] - unit = int(device['unitsize']) - probes = int(device['total probes']) - if unit not in [1, 2] or not 1 <= probes <= unit * 8: - raise ValueError("Unsupported digital sample geometry") - rate = re.fullmatch(r'(\d+)\s+(Hz|kHz|MHz)', device['samplerate']) - if rate is None or int(rate[1]) <= 0: - raise ValueError("Unsupported sample-rate representation") - samplerate = int(rate[1]) * {'Hz': 1, 'kHz': 1000, 'MHz': 1000000}[rate[2]] - bits = [] - for channel in channels: - matches = [index - 1 for index in range(1, probes + 1) - if device.get(f'probe{index}') == channel] - if len(matches) != 1: - raise ValueError("Channel identity is missing or ambiguous") - bits.append(matches[0]) - prefix = device['capturefile'] - chunks = [name for name in names if re.fullmatch(re.escape(prefix) + r'-\d+', name)] - if prefix in names and chunks: - raise ValueError("Ambiguous direct and chunked logic data") - if prefix in names: - chunks = [prefix] - else: - chunks.sort(key=lambda name: int(name.rsplit('-', 1)[1])) - if [int(name.rsplit('-', 1)[1]) for name in chunks] != list(range(1, len(chunks) + 1)): - raise ValueError("Missing or ambiguous logic chunk order") - if not chunks or any(archive.getinfo(name).file_size % unit for name in chunks): - raise ValueError("Missing or incomplete digital samples") - edges = [[] for _ in channels] - previous = None - sample = 0 - mask = sum(1 << bit for bit in bits) - for name in chunks: - with archive.open(name) as source: - while block := source.read(65536): - values = block if unit == 1 else (item[0] for item in struct.iter_unpack('> bit) & 1 - if not edges[index] or edges[index][-1][1] != level: - if sum(map(len, edges)) >= 65536: - raise ValueError('Selected transitions exceed the experiment representation budget') - edges[index].append((sample, level)) - previous = selected - sample += 1 - with path.open('rb') as source: - after = source.read(size + 1) - if len(after) != size or hashlib.sha256(after).hexdigest() != before: - raise ValueError("Recording bytes changed during the independent read") - return {'sha256': before, 'metadata': metadata, 'sample_rate_hz': samplerate, - 'total_samples': sample, 'channels': dict(zip(channels, bits))}, edges - - -def uart(path, invert): - metadata, traces = read_edges(path, ['MAX3232E DOUT1']) - edges = [(sample, level ^ invert) for sample, level in traces[0]] - positions = [sample for sample, _ in edges] - rate, baud = metadata['sample_rate_hz'], 57600 - def level(sample): - return edges[bisect_right(positions, sample) - 1][1] - rows = [] - busy_until = 0 - for index, (start, value) in enumerate(edges): - if index == 0 or value != 0 or start < busy_until: - continue - center = lambda halfbits: start + (halfbits * rate) // (2 * baud) - if center(19) >= metadata['total_samples']: - raise ValueError("Incomplete UART frame at the capture boundary") - if level(center(1)) != 0 or level(center(19)) != 1: - raise ValueError(f"UART framing differs from selected polarity at sample {start}") - byte = sum(level(center(3 + 2 * bit)) << bit for bit in range(8)) - # A UART receiver becomes ready after sampling the stop bit. Waiting - # a full nominal ten bits would miss a valid slightly faster sender. - rows.append({'start': start, 'end_exclusive': center(19) + 1, 'byte': byte, - 'first_data_sample': center(3), 'last_data_sample': center(17)}) - busy_until = center(19) + 1 - return metadata | {'kind': 'uart', 'baud_rate': baud, 'invert': bool(invert), - 'complete_frames': len(rows), 'rows': rows} - - -def i2c(path): - metadata, traces = read_edges(path, ['SCL', 'SDA']) - transitions = {} - for channel, edges in enumerate(traces): - for sample, value in edges: - transitions.setdefault(sample, {})[channel] = value - clock = data = None - active = None - bits = [] - transactions = [] - for sample, changes in sorted(transitions.items()): - next_clock, next_data = changes.get(0, clock), changes.get(1, data) - if clock is not None and data is not None: - if clock == next_clock == 1 and next_data != data: - if next_data == 0: - if active is not None: - raise ValueError("The pinned profile does not contain repeated START") - active = {'start': sample, 'bytes': []} - bits = [] - elif active is not None: - # SCL may rise with SDA low to prepare STOP; that final - # rise is not the first bit of a new data byte. - if len(bits) == 1 and bits[0][1] == 0: - bits = [] - if bits: - raise ValueError("Incomplete I2C byte before STOP") - active['stop'] = sample - transactions.append(active) - active = None - elif active is not None and clock == 0 and next_clock == 1: - if next_data != data: - raise ValueError("Ambiguous simultaneous clock/data transition") - bits.append((sample, next_data)) - if len(bits) == 9: - byte = sum(bit << (7-index) for index, (_, bit) in enumerate(bits[:8])) - active['bytes'].append({'byte': byte, 'first_clock': bits[0][0], - 'last_clock': bits[7][0], 'ack': bits[8][1] == 0}) - bits = [] - clock, data = next_clock, next_data - if active is not None or bits: - raise ValueError("Incomplete I2C transaction at capture end") - reads = [transaction for transaction in transactions - if transaction['bytes'] and transaction['bytes'][0]['byte'] & 1] - return metadata | {'kind': 'i2c', 'transactions': transactions, 'reads': reads} - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('kind', choices=['uart', 'i2c']) - parser.add_argument('path', type=Path) - parser.add_argument('--invert', action='store_true') - args = parser.parse_args() - print(json.dumps(uart(args.path, args.invert) if args.kind == 'uart' else i2c(args.path))) diff --git a/tests/support/analysis-efficiency/test_packet_oracle.py b/tests/support/analysis-efficiency/test_packet_oracle.py deleted file mode 100644 index 6727b3b7eb..0000000000 --- a/tests/support/analysis-efficiency/test_packet_oracle.py +++ /dev/null @@ -1,67 +0,0 @@ -import importlib.util -import struct -import unittest -from pathlib import Path - -spec = importlib.util.spec_from_file_location('packet_oracle', Path(__file__).with_name('packet-oracle.py')) -oracle = importlib.util.module_from_spec(spec) -spec.loader.exec_module(oracle) - - -class PacketOracleTest(unittest.TestCase): - def test_compression_pointer_preserves_question_cursor(self): - message = b'\x03www\x07example\x03com\0' + b'\xc0\0' + b'\0\x01\0\x01' - self.assertEqual(oracle.dns_name(message, 17), ('www.example.com', 19)) - - def test_compression_cycle_and_missing_pointer_byte_fail(self): - for message in (b'\xc0\0', b'\xc0', b'\xc0\x02\xc0\0'): - with self.subTest(message=message), self.assertRaises(ValueError): - oracle.dns_name(message, 0) - - def test_truncated_pcap_record_is_not_exhaustion(self): - header = struct.pack('> index) & 1 for index in range(8)], 1]: - levels.extend([bit] * 17) - levels.extend([1] * 100) - data = b''.join(struct.pack('> index) & 1 for index in reversed(range(8))] + [ack] - if incomplete and byte == 0x75: - values = values[:4] - for bit in values: - states.extend([(0, bit), (1, bit)]) - states.extend([(0, 0), (1, 0), (1, 1)]) - return bytes(clock | data << 1 for clock, data in states) - - def test_i2c_stop_setup_is_distinct_from_another_data_byte(self): - self.recording(self.i2c_levels(), 1, ['SCL', 'SDA']) - result = oracle.i2c(self.path) - self.assertEqual(len(result['reads']), 1) - self.assertEqual([row['byte'] for row in result['reads'][0]['bytes']], [0xA5, 0x75]) - self.assertFalse(result['reads'][0]['bytes'][-1]['ack']) - - def test_incomplete_i2c_byte_is_not_invented_at_stop(self): - self.recording(self.i2c_levels(incomplete=True), 1, ['SCL', 'SDA']) - with self.assertRaisesRegex(ValueError, 'Incomplete I2C byte'): - oracle.i2c(self.path) - - def test_channel_names_must_identify_one_stored_bit(self): - self.recording(b'\x00', 1, ['SCL', 'SCL']) - with self.assertRaisesRegex(ValueError, 'ambiguous'): - oracle.read_edges(self.path, ['SCL']) - - def test_missing_chunk_is_not_renumbered(self): - self.recording(b'\x00', 1, ['SCL', 'SDA'], chunk='logic-1-2') - with self.assertRaisesRegex(ValueError, 'chunk order'): - oracle.read_edges(self.path, ['SCL', 'SDA']) - - def test_partial_sample_word_is_rejected(self): - self.recording(b'\x00', 2, ['unused', 'MAX3232E DOUT1']) - with self.assertRaisesRegex(ValueError, 'incomplete digital samples'): - oracle.read_edges(self.path, ['MAX3232E DOUT1']) - - def test_directory_budget_precedes_zip_object_allocation(self): - self.recording(b'\x00', 1, ['SCL', 'SDA'], extra=62) - with patch.object(oracle.zipfile, 'ZipFile', side_effect=AssertionError('Too late')): - with self.assertRaisesRegex(ValueError, 'excessive ZIP directory'): - oracle.read_edges(self.path, ['SCL', 'SDA']) - - def test_declared_expansion_is_rejected_before_decompression(self): - self.recording(b'\x00', 1, ['SCL', 'SDA']) - data = bytearray(self.path.read_bytes()) - directory = data.index(b'PK\x01\x02') - struct.pack_into('