mirror of
https://github.com/morluto/rea.git
synced 2026-11-13 18:49:25 -10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
808acf6dec | ||
|
|
2064e6a66e | ||
|
|
aeb30599af | ||
|
|
db0748389f | ||
|
|
e8c838e8bd | ||
|
|
809f0d69db |
No files matched your search
+1
-24
@@ -63,18 +63,6 @@
|
||||
"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",
|
||||
@@ -88,19 +76,12 @@
|
||||
{
|
||||
"files": ["src/**/*.ts"],
|
||||
"rules": {
|
||||
"rea/no-babel-visitor-keys": "error",
|
||||
"typescript/no-misused-promises": "error"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": [
|
||||
"tests/**/*.ts",
|
||||
"src/**/*.test.ts",
|
||||
"src/**/*.spec.ts",
|
||||
"src/**/*.fixture.ts"
|
||||
],
|
||||
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
|
||||
"rules": {
|
||||
"rea/no-babel-visitor-keys": "off",
|
||||
"max-lines": "off",
|
||||
"max-lines-per-function": [
|
||||
"error",
|
||||
@@ -113,10 +94,6 @@
|
||||
"typescript/no-misused-promises": "warn"
|
||||
}
|
||||
},
|
||||
{
|
||||
"files": ["src/domain/javascript/javascriptSemanticTraversal.ts"],
|
||||
"rules": { "rea/no-babel-visitor-keys": "off" }
|
||||
},
|
||||
{
|
||||
"files": ["src/application/**/*.test.ts"],
|
||||
"rules": {
|
||||
|
||||
@@ -307,7 +307,10 @@ assertion. `vitest.config.ts` applies the deadlines in
|
||||
and 120 seconds for built projects, with hooks sharing their project's
|
||||
deadline. Coverage doubles every deadline, and so does hosted CI, where
|
||||
CPU-bound cases share every core with other workers and their subprocesses.
|
||||
Do not pass a per-test timeout to fit a slow host. Tag a bounded workload that needs longer with
|
||||
Do not pass a per-test timeout to fit a slow host. A child run through a
|
||||
test process registry (`processTest`, `cli.run`, or `createTestProcessRegistry`)
|
||||
gets the running test's remaining deadline less a reporting reserve, so a hung
|
||||
child fails with its captured output. Pass `timeoutMs` only when the child's deadline is itself under test. Tag a bounded workload that needs longer with
|
||||
`{ tags: ["long-running"] }`, or a whole file with
|
||||
`// @module-tag long-running`. Assert elapsed time explicitly when speed is the
|
||||
behavior under test. Derive an explicit deadline only when a test must outlive
|
||||
|
||||
@@ -174,9 +174,7 @@ Official references: [mitmproxy protocol support](https://github.com/mitmproxy/m
|
||||
|
||||
## Verifier regressions
|
||||
|
||||
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:
|
||||
After building REA, run these inside the same isolated guest:
|
||||
|
||||
```sh
|
||||
node --test scripts/lib/http3-verifier-lifecycle.test.mjs scripts/lib/http3-capture-assertions.test.mjs
|
||||
|
||||
@@ -14,7 +14,7 @@ const directDependencies = (rootPackage) => ({
|
||||
const installedPackageKey = (name) => `node_modules/${name}`;
|
||||
|
||||
/** Compare root direct dependencies with npm's hidden installed lockfile. */
|
||||
const dependencyInstallProblems = (rootLock, installedLock) => {
|
||||
export const dependencyInstallProblems = (rootLock, installedLock) => {
|
||||
const rootPackage = rootLock.packages?.[""];
|
||||
if (rootPackage === undefined)
|
||||
return ["package-lock.json does not contain a root package entry"];
|
||||
|
||||
@@ -26,6 +26,8 @@ export type CacheRequest = (
|
||||
endpoint: string,
|
||||
) => Promise<unknown>;
|
||||
|
||||
export function assertRetentionPrefix(prefix: string): void;
|
||||
|
||||
export function selectPrefixDeletions(
|
||||
caches: readonly unknown[],
|
||||
family?: RetentionFamily,
|
||||
|
||||
@@ -22,7 +22,7 @@ const PREFIX_PATTERNS = [
|
||||
];
|
||||
|
||||
/** Reject prefixes that could match npm downloads or another platform. */
|
||||
function assertRetentionPrefix(prefix) {
|
||||
export function assertRetentionPrefix(prefix) {
|
||||
if (
|
||||
typeof prefix !== "string" ||
|
||||
!PREFIX_PATTERNS.some((pattern) => pattern.test(prefix))
|
||||
|
||||
@@ -99,3 +99,9 @@ export function createGitHubApi(options?: {
|
||||
budget: { requests: number; startedAt: number; limitMs: number },
|
||||
) => Promise<unknown>;
|
||||
}): FailureReportApi;
|
||||
|
||||
export function writeFailureReport(
|
||||
report: CiFailureReport,
|
||||
outputDirectory?: string,
|
||||
summaryPath?: string,
|
||||
): Promise<{ jsonPath: string; markdownPath: string }>;
|
||||
@@ -467,7 +467,7 @@ function errorCode(error) {
|
||||
return error instanceof Error ? error.message.slice(0, 160) : "unknown_error";
|
||||
}
|
||||
|
||||
async function writeFailureReport(
|
||||
export async function writeFailureReport(
|
||||
report,
|
||||
outputDirectory = OUTPUT_DIRECTORY,
|
||||
summaryPath,
|
||||
|
||||
@@ -91,4 +91,5 @@ export function writePrCiHealthReport(
|
||||
outputDirectory?: string,
|
||||
): Promise<{ jsonPath: string; markdownPath: string }>;
|
||||
|
||||
export function renderMarkdownSummary(report: PrCiHealthReport): string;
|
||||
import type { GitHubApiRequest } from "./github-api.mjs";
|
||||
@@ -656,7 +656,7 @@ export async function writePrCiHealthReport(
|
||||
return { jsonPath, markdownPath };
|
||||
}
|
||||
|
||||
function renderMarkdownSummary(report) {
|
||||
export 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)} |`,
|
||||
|
||||
@@ -28,3 +28,6 @@ export interface ProductionReachabilityReport {
|
||||
export function parseProductionReachabilityReport(
|
||||
text: string,
|
||||
): ProductionReachabilityReport;
|
||||
export function recordProductionReachabilityReport(
|
||||
root?: string,
|
||||
): Promise<ProductionReachabilityReport>;
|
||||
@@ -34,7 +34,9 @@ export const parseProductionReachabilityReport = (text) =>
|
||||
reportSchema.parse(JSON.parse(text));
|
||||
|
||||
/** Run production reachability without turning candidates into failures. */
|
||||
const recordProductionReachabilityReport = async (root = process.cwd()) => {
|
||||
export const recordProductionReachabilityReport = async (
|
||||
root = process.cwd(),
|
||||
) => {
|
||||
const reportDirectory = join(root, ".cache", "static-checks");
|
||||
const reportPath = join(reportDirectory, "production-reachability.json");
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { promisify } from "node:util";
|
||||
const exec = promisify(execFile);
|
||||
|
||||
/** Commands the macOS bundle fixture needs from Command Line Tools. */
|
||||
const MACOS_BUNDLE_FIXTURE_COMMANDS = [
|
||||
export const MACOS_BUNDLE_FIXTURE_COMMANDS = [
|
||||
["/usr/bin/xcrun", ["--find", "clang"]],
|
||||
["/usr/bin/xcrun", ["--find", "codesign"]],
|
||||
["/usr/bin/ditto", ["-h"]],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function sourceMapSummary(sourceMaps) {
|
||||
export function sourceMapSummary(sourceMaps) {
|
||||
return {
|
||||
status: sourceMaps.status,
|
||||
requested: sourceMaps.requested,
|
||||
|
||||
@@ -1,3 +1,11 @@
|
||||
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;
|
||||
}
|
||||
@@ -48,6 +56,11 @@ export interface CursorPluginInspection {
|
||||
readonly issues: readonly CursorPluginBundleIssue[];
|
||||
}
|
||||
|
||||
export function cursorPluginSkillPaths(
|
||||
directory: string,
|
||||
prefix?: string,
|
||||
): Promise<readonly string[] | null>;
|
||||
|
||||
export function cursorPluginBundleIssues(
|
||||
input: CursorPluginBundleInput,
|
||||
): readonly CursorPluginBundleIssue[];
|
||||
|
||||
@@ -7,13 +7,14 @@ 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.
|
||||
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";
|
||||
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 GENERATE_COMMAND = "node scripts/generate-cursor-plugin-skill.mjs";
|
||||
const INSTALL_LINK_MARKERS = ["install-mcp", "deeplink/mcp", "mcp-install"];
|
||||
|
||||
@@ -26,7 +27,7 @@ const failure = (code, detail) => {
|
||||
const cursorServerArgs = (version) => ["-y", `rea-agents@${version}`, "mcp"];
|
||||
|
||||
/** Relative file inventory for one skill directory, in stable order. */
|
||||
const cursorPluginSkillPaths = async (directory, prefix = "") => {
|
||||
export const cursorPluginSkillPaths = async (directory, prefix = "") => {
|
||||
const paths = [];
|
||||
let entries;
|
||||
try {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({
|
||||
export const WINDOWS_GHIDRA_BYTE_SEARCH_FIXTURE = Object.freeze({
|
||||
pattern: "a0 ?? c0",
|
||||
mask: "f0 ff f0",
|
||||
startFileOffset: 0x380,
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
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<void>;
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ function assertHeaders(record, side, headers) {
|
||||
}
|
||||
}
|
||||
|
||||
function assertEndpointHeaders(actual, expected) {
|
||||
export 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) => {
|
||||
|
||||
@@ -1,10 +1,28 @@
|
||||
import { analyzeJavaScriptApplication } from "../../dist/application/javascript/JavaScriptApplicationService.js";
|
||||
import { withVerifierArtifactScope } from "./verifier-artifact-scope.mjs";
|
||||
import { ArtifactResourceScope } from "../../dist/artifacts/ArtifactResourceScope.js";
|
||||
|
||||
/** Analyze one JavaScript application and close its caller-owned resources. */
|
||||
export const analyzeJavaScriptApplicationWithOwnedScope = (input) =>
|
||||
withVerifierArtifactScope(async (scope) => {
|
||||
export const analyzeJavaScriptApplicationWithOwnedScope = async (input) => {
|
||||
const scope = new ArtifactResourceScope();
|
||||
let outcome;
|
||||
try {
|
||||
const result = await analyzeJavaScriptApplication(input, scope);
|
||||
if (!result.ok) throw result.error;
|
||||
return result.value;
|
||||
});
|
||||
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;
|
||||
};
|
||||
@@ -78,7 +78,7 @@ export const expandMcpSessionStatus = (status) => ({
|
||||
});
|
||||
|
||||
/** Expand provider capability groups while retaining candidate identity/state. */
|
||||
const expandProviderCandidates = (candidates) =>
|
||||
export const expandProviderCandidates = (candidates) =>
|
||||
candidates.map((candidate) => ({
|
||||
...candidate,
|
||||
capabilities: expandProviderCapabilities(
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
export { runOwnedVerifierTerminal } from "./verifier-terminal-owner.mjs";
|
||||
export type {
|
||||
VerifierTerminalOptions,
|
||||
VerifierTerminalDependencies,
|
||||
VerifierTerminalResult,
|
||||
VerifierTerminalFailure,
|
||||
VerifierTerminalCleanupFailure,
|
||||
} from "./verifier-terminal-owner.mjs";
|
||||
@@ -1,10 +1,6 @@
|
||||
/** Observe possible Hopper resources, including entries with unresolved ownership. */
|
||||
/** Observe concrete Hopper runtime resources for a before/after cleanup check. */
|
||||
export function snapshotHopperRuntime(
|
||||
parent: string,
|
||||
targetLeaseDirectory: string,
|
||||
expectedRunId?: string,
|
||||
): Promise<{
|
||||
readonly paths: Set<string>;
|
||||
readonly confirmedSessions: Set<string>;
|
||||
readonly unresolvedSessions: Set<string>;
|
||||
}>;
|
||||
ownedProcessIds?: ReadonlySet<number>,
|
||||
): Promise<Set<string>>;
|
||||
@@ -1,38 +1,35 @@
|
||||
import { lstat, readFile, readdir } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
/** Observe possible sessions and target leases without discarding uncertainty. */
|
||||
/** Observe session paths and target leases without treating their shared parent as a session. */
|
||||
export async function snapshotHopperRuntime(
|
||||
parent,
|
||||
targetLeaseDirectory,
|
||||
expectedRunId,
|
||||
ownedProcessIds,
|
||||
) {
|
||||
const paths = new Set();
|
||||
const confirmedSessions = new Set();
|
||||
const unresolvedSessions = new Set();
|
||||
const resources = 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;
|
||||
const confirmed =
|
||||
typeof expectedRunId === "string" &&
|
||||
expectedRunId.length > 0 &&
|
||||
entry.isDirectory() &&
|
||||
(await hasRunIdentity(path, expectedRunId));
|
||||
(confirmed ? confirmedSessions : unresolvedSessions).add(path);
|
||||
paths.add(path);
|
||||
if (
|
||||
ownedProcessIds !== undefined &&
|
||||
(!entry.isDirectory() ||
|
||||
!(await belongsToOwnedProcess(path, ownedProcessIds)))
|
||||
)
|
||||
continue;
|
||||
resources.add(path);
|
||||
}
|
||||
const leaseRoot = await lstat(targetLeaseDirectory).catch(missingIsAbsent);
|
||||
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));
|
||||
}
|
||||
if (leaseRoot === undefined) return resources;
|
||||
if (!leaseRoot.isDirectory() || leaseRoot.isSymbolicLink()) {
|
||||
resources.add(targetLeaseDirectory);
|
||||
return resources;
|
||||
}
|
||||
return { paths, confirmedSessions, unresolvedSessions };
|
||||
const leases = await readdir(targetLeaseDirectory).catch(missingIsAbsent);
|
||||
for (const name of leases ?? [])
|
||||
resources.add(join(targetLeaseDirectory, name));
|
||||
return resources;
|
||||
}
|
||||
|
||||
const missingIsAbsent = (cause) => {
|
||||
@@ -40,21 +37,16 @@ const missingIsAbsent = (cause) => {
|
||||
throw cause;
|
||||
};
|
||||
|
||||
const hasRunIdentity = async (path, expectedRunId) => {
|
||||
const belongsToOwnedProcess = async (path, ownedProcessIds) => {
|
||||
const encoded = await readFile(join(path, "ownership.json"), "utf8").catch(
|
||||
missingIsAbsent,
|
||||
);
|
||||
if (encoded === undefined) return false;
|
||||
let ownership;
|
||||
try {
|
||||
ownership = JSON.parse(encoded);
|
||||
} catch (cause) {
|
||||
if (cause instanceof SyntaxError) return false;
|
||||
throw cause;
|
||||
}
|
||||
const ownership = JSON.parse(encoded);
|
||||
return (
|
||||
ownership !== null &&
|
||||
typeof ownership === "object" &&
|
||||
ownership.run_id === expectedRunId
|
||||
Number.isSafeInteger(ownership.parent_pid) &&
|
||||
ownedProcessIds.has(ownership.parent_pid)
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
interface FixtureProcedure {
|
||||
export interface FixtureProcedure {
|
||||
readonly address: string;
|
||||
readonly name: string;
|
||||
}
|
||||
@@ -26,6 +26,13 @@ export function requireCurrentDocument(
|
||||
normalizedResult: (value: unknown, operation: string) => unknown,
|
||||
): Promise<string>;
|
||||
|
||||
export function analyzeFixtureProcedure(
|
||||
client: ToolClient,
|
||||
options: unknown,
|
||||
procedure: FixtureProcedure,
|
||||
normalizedResult: (value: unknown, operation: string) => unknown,
|
||||
): Promise<unknown>;
|
||||
|
||||
export function verifyRealHopperFixture(options: {
|
||||
readonly client: ToolClient;
|
||||
readonly options: unknown;
|
||||
|
||||
@@ -310,7 +310,7 @@ export async function resolveFixtureProcedure(
|
||||
}
|
||||
|
||||
/** Request the assembly evidence required by the fixture oracle. */
|
||||
const analyzeFixtureProcedure = async (
|
||||
export const analyzeFixtureProcedure = async (
|
||||
client,
|
||||
options,
|
||||
procedure,
|
||||
|
||||
@@ -6,3 +6,6 @@ export function unsupportedRuntimeMessage(
|
||||
range: string,
|
||||
version: string,
|
||||
): string | undefined;
|
||||
|
||||
/** Human-readable release families for an `engines.node` range. */
|
||||
export function supportedReleaseProse(range: string): string;
|
||||
@@ -21,7 +21,7 @@ export const unsupportedRuntimeMessage = (range, version) => {
|
||||
};
|
||||
|
||||
/** Human-readable release families for an `engines.node` range. */
|
||||
const supportedReleaseProse = (range) =>
|
||||
export const supportedReleaseProse = (range) =>
|
||||
`Node.js ${range
|
||||
.split("||")
|
||||
.map((clause) => {
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
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<T>(
|
||||
operation: (scope: ArtifactResourceScope) => Promise<T>,
|
||||
): Promise<T>;
|
||||
@@ -1,32 +0,0 @@
|
||||
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;
|
||||
}
|
||||
@@ -40,7 +40,7 @@ export interface VerifierTerminalOptions {
|
||||
readonly signal?: AbortSignal;
|
||||
}
|
||||
|
||||
interface VerifierTerminalDependencies {
|
||||
export interface VerifierTerminalDependencies {
|
||||
readonly loadPty?: () => Promise<typeof import("@lydell/node-pty")>;
|
||||
readonly cleanupProcessGroup?: typeof cleanupOwnedProcessGroup;
|
||||
}
|
||||
@@ -59,7 +59,7 @@ type TerminalFailureReason =
|
||||
| "unsupported_platform";
|
||||
|
||||
/** Expected terminal failures retain the decoded output observed at failure. */
|
||||
class VerifierTerminalFailure extends Error {
|
||||
export class VerifierTerminalFailure extends Error {
|
||||
readonly _tag = "VerifierTerminalFailure";
|
||||
|
||||
constructor(
|
||||
@@ -74,7 +74,7 @@ class VerifierTerminalFailure extends Error {
|
||||
}
|
||||
|
||||
/** The adopted owner remains the exact authority for retrying this release. */
|
||||
class VerifierTerminalCleanupFailure extends Error {
|
||||
export class VerifierTerminalCleanupFailure extends Error {
|
||||
readonly _tag = "VerifierTerminalCleanupFailure";
|
||||
readonly reason = "cleanup_incomplete";
|
||||
|
||||
|
||||
@@ -5,8 +5,6 @@ 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" },
|
||||
@@ -17,7 +15,5 @@ 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,
|
||||
},
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { RuleTester } from "oxlint/plugins-dev";
|
||||
|
||||
declare const rule: Parameters<RuleTester["run"]>[1];
|
||||
export default rule;
|
||||
@@ -1,73 +0,0 @@
|
||||
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" });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,4 +0,0 @@
|
||||
import type { RuleTester } from "oxlint/plugins-dev";
|
||||
|
||||
declare const rule: Parameters<RuleTester["run"]>[1];
|
||||
export default rule;
|
||||
@@ -1,34 +0,0 @@
|
||||
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" });
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,13 @@
|
||||
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;
|
||||
|
||||
@@ -117,7 +117,7 @@ export async function verifyPackageEnvironment({
|
||||
};
|
||||
}
|
||||
|
||||
const packageHopperEnvironment = (root, platform = process.platform) => {
|
||||
export const packageHopperEnvironment = (root, platform = process.platform) => {
|
||||
const fakeLauncher = join(root, "tests/fixtures/fakeLauncher.mjs");
|
||||
if (platform !== "linux")
|
||||
return {
|
||||
|
||||
@@ -414,7 +414,7 @@ export function matchesSymbol(value, expected) {
|
||||
return tail === expected || tail === `_${expected}`;
|
||||
}
|
||||
|
||||
function symbolTail(value) {
|
||||
export function symbolTail(value) {
|
||||
if (typeof value !== "string") return undefined;
|
||||
return value.split("::").at(-1);
|
||||
}
|
||||
|
||||
@@ -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 { withVerifierArtifactScope } from "./lib/verifier-artifact-scope.mjs";
|
||||
import { ArtifactResourceScope } from "../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { readGhidraLanguageCatalog } from "../dist/ghidra/GhidraLanguageCatalog.js";
|
||||
import { parseEvidence } from "../dist/domain/evidence.js";
|
||||
import { functionDossierSchema } from "../dist/domain/native/functionDossier.js";
|
||||
@@ -82,13 +82,16 @@ 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) {
|
||||
return withVerifierArtifactScope(async (scope) => {
|
||||
const scope = new ArtifactResourceScope();
|
||||
try {
|
||||
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. */
|
||||
|
||||
@@ -11,10 +11,6 @@ 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");
|
||||
@@ -52,9 +48,11 @@ const digest = async (path) =>
|
||||
createHash("sha256")
|
||||
.update(await readFile(path))
|
||||
.digest("hex");
|
||||
const sessionsBefore = (
|
||||
await snapshotHopperRuntime("/tmp", HOPPER_TARGET_LEASE_DIRECTORY)
|
||||
).paths;
|
||||
const sessionsBefore = await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
);
|
||||
const ownedProcessIds = new Set();
|
||||
const client = new Client({ name: "hopper-callers-verifier", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
@@ -95,7 +93,7 @@ const matchingRef = (ref, site, from, to) =>
|
||||
ref.source_procedure?.address === from &&
|
||||
ref.target_procedure?.address === to &&
|
||||
ref.call?.classification === "direct";
|
||||
await withVerifierCleanup(async () => {
|
||||
try {
|
||||
await run(clang, [
|
||||
"-isysroot",
|
||||
sdk,
|
||||
@@ -131,6 +129,7 @@ await withVerifierCleanup(async () => {
|
||||
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) => {
|
||||
@@ -288,36 +287,23 @@ await withVerifierCleanup(async () => {
|
||||
sha256: beforeLarge,
|
||||
}),
|
||||
);
|
||||
}, [
|
||||
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);
|
||||
}
|
||||
} 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)
|
||||
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}`,
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { copyFile, mkdtemp, realpath } from "node:fs/promises";
|
||||
import { copyFile, mkdtemp, realpath, rm } 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";
|
||||
@@ -30,7 +29,6 @@ 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({
|
||||
@@ -42,7 +40,6 @@ const profileResourceScope = {
|
||||
};
|
||||
let client;
|
||||
let primaryFailure;
|
||||
let operationFailed = false;
|
||||
try {
|
||||
await copyFile(fixtures.primary.path, target);
|
||||
const parsed = await parseBinaryTarget(target);
|
||||
@@ -151,19 +148,31 @@ 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 {
|
||||
if (client !== undefined) cleanupOwner.setClient(client);
|
||||
await cleanupOwner.close();
|
||||
} catch (cleanupFailure) {
|
||||
throw operationFailed
|
||||
? new AggregateError(
|
||||
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(
|
||||
[primaryFailure, cleanupFailure],
|
||||
"Hopper verification and cleanup failed",
|
||||
)
|
||||
: cleanupFailure;
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -23,10 +23,6 @@ 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 {
|
||||
@@ -74,12 +70,14 @@ 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)
|
||||
).paths;
|
||||
const before = await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
);
|
||||
const client = new Client({ name: "hopper-fat-verifier", version: "1" });
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
@@ -96,7 +94,7 @@ const ajv = new Ajv({ strict: false });
|
||||
addFormats(ajv);
|
||||
const observations = [];
|
||||
const sourceBinding = [];
|
||||
await withVerifierCleanup(async () => {
|
||||
try {
|
||||
await run(toolPaths.clang, [
|
||||
"-isysroot",
|
||||
sdk,
|
||||
@@ -185,17 +183,9 @@ await withVerifierCleanup(async () => {
|
||||
"overlap",
|
||||
/slices overlap/u,
|
||||
(bytes) => {
|
||||
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,
|
||||
);
|
||||
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);
|
||||
},
|
||||
],
|
||||
]) {
|
||||
@@ -221,14 +211,11 @@ await withVerifierCleanup(async () => {
|
||||
}
|
||||
const path = join(directory, "c-fat64-ambiguous");
|
||||
await writeFile(path, ambiguous);
|
||||
invalidFat64.push({
|
||||
path,
|
||||
reason:
|
||||
/FAT binary declares several (?:arm64|x86_64) slices; no slice was selected/u,
|
||||
});
|
||||
invalidFat64.push({ path, reason: /selection.*ambiguous/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(
|
||||
@@ -372,27 +359,14 @@ await withVerifierCleanup(async () => {
|
||||
if (verifyFat64) {
|
||||
await call("open_binary", { path: fat64 });
|
||||
await call("list_procedures");
|
||||
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 resources = [
|
||||
...(await snapshotHopperRuntime(
|
||||
"/tmp",
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
owned,
|
||||
)),
|
||||
].filter((path) => !before.has(path));
|
||||
for (const root of resources) {
|
||||
const image = join(root, "image.macho");
|
||||
if (
|
||||
await access(image).then(
|
||||
@@ -429,43 +403,27 @@ await withVerifierCleanup(async () => {
|
||||
},
|
||||
);
|
||||
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);
|
||||
}
|
||||
}, [
|
||||
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 });
|
||||
},
|
||||
]);
|
||||
} 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",
|
||||
);
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
|
||||
@@ -62,9 +62,10 @@ const parseServerArgs = (encoded) => {
|
||||
};
|
||||
|
||||
const runtimeParent = process.platform === "darwin" ? "/tmp" : tmpdir();
|
||||
const sessionsBefore = (
|
||||
await snapshotHopperRuntime(runtimeParent, HOPPER_TARGET_LEASE_DIRECTORY)
|
||||
).paths;
|
||||
const sessionsBefore = await snapshotHopperRuntime(
|
||||
runtimeParent,
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
);
|
||||
const ownedProcessIds = new Set();
|
||||
const textValue = mcpTextValue;
|
||||
|
||||
@@ -556,8 +557,11 @@ try {
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
const sessionsAfter = [
|
||||
...(await snapshotHopperRuntime(runtimeParent, HOPPER_TARGET_LEASE_DIRECTORY))
|
||||
.paths,
|
||||
...(await snapshotHopperRuntime(
|
||||
runtimeParent,
|
||||
HOPPER_TARGET_LEASE_DIRECTORY,
|
||||
ownedProcessIds,
|
||||
)),
|
||||
]
|
||||
.filter((path) => !sessionsBefore.has(path))
|
||||
.sort();
|
||||
@@ -569,7 +573,7 @@ if (sessionsAfter.length > 0) {
|
||||
retainedRuntimePaths: sessionsAfter,
|
||||
});
|
||||
throw new Error(
|
||||
`Hopper resources remain or have unresolved ownership: ${sessionsAfter.join(", ")}`,
|
||||
`The MCP runtime retained Hopper resources: ${sessionsAfter.join(", ")}`,
|
||||
);
|
||||
}
|
||||
if (summary === undefined)
|
||||
|
||||
@@ -40,7 +40,7 @@ try {
|
||||
const expected = [];
|
||||
for (const path of files.stdout
|
||||
.split("\0")
|
||||
.filter((path) => /\.(?:test|spec)\.[cm]?[jt]sx?$/u.test(path))) {
|
||||
.filter((path) => path.endsWith(".test.ts"))) {
|
||||
try {
|
||||
if ((await stat(join(root, path))).isFile()) expected.push(path);
|
||||
} catch (cause) {
|
||||
@@ -65,6 +65,4 @@ try {
|
||||
await rm(temporary, { recursive: true, force: true });
|
||||
}
|
||||
if (discovered !== undefined)
|
||||
console.log(
|
||||
`Discovered all ${discovered} test and spec files in src and tests exactly once.`,
|
||||
);
|
||||
console.log(`Discovered all ${discovered} test files exactly once.`);
|
||||
@@ -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 { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs";
|
||||
import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js";
|
||||
import { inspectAppleDispatchMetadata } from "../../../dist/native/AppleDispatchMetadata.js";
|
||||
import {
|
||||
artifactCliEvidence,
|
||||
@@ -57,7 +57,8 @@ const fixupCoverage = (metadata) =>
|
||||
const inspect = async (path) => {
|
||||
const target = await parseBinaryTarget(path);
|
||||
if (!target.ok) throw target.error;
|
||||
return withVerifierArtifactScope(async (resources) => {
|
||||
const resources = new ArtifactResourceScope();
|
||||
try {
|
||||
return {
|
||||
target: target.value,
|
||||
metadata: (
|
||||
@@ -66,7 +67,10 @@ const inspect = async (path) => {
|
||||
)
|
||||
).result,
|
||||
};
|
||||
});
|
||||
} finally {
|
||||
// Retry any descriptor whose close failed, surfacing unresolved cleanup.
|
||||
await resources.close();
|
||||
}
|
||||
};
|
||||
|
||||
const checkObjc = (metadata, mode) => {
|
||||
|
||||
@@ -5,12 +5,17 @@ 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.
|
||||
// The text check below only flags one spelling; it is not a complete audit.
|
||||
// compositeKey (JSON tuple) is the single owner for composite keys.
|
||||
// - 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));
|
||||
|
||||
@@ -22,7 +27,6 @@ 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);
|
||||
@@ -44,6 +48,8 @@ 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(
|
||||
@@ -62,7 +68,21 @@ check(
|
||||
/providers\/ProviderInspectionScope\.js/,
|
||||
);
|
||||
|
||||
check("NUL-joined list key (use compositeKey)", files, /\.join\(["']\\0["']\)/);
|
||||
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(
|
||||
"direct transparent-wrapper unwrap outside javascriptAstValues (use unwrapJavaScriptExpression)",
|
||||
@@ -71,6 +91,13 @@ 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.
|
||||
@@ -136,6 +163,7 @@ 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",
|
||||
@@ -151,6 +179,7 @@ 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",
|
||||
|
||||
@@ -5,6 +5,7 @@ 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,
|
||||
@@ -13,7 +14,6 @@ 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,23 +106,33 @@ process.stdout.write(
|
||||
);
|
||||
|
||||
async function readEvidenceFile(path) {
|
||||
const scope = new ArtifactResourceScope();
|
||||
let outcome;
|
||||
try {
|
||||
outcome = {
|
||||
ok: true,
|
||||
value: await withVerifierArtifactScope(async (scope) =>
|
||||
readStableArtifact(
|
||||
await realpath(path),
|
||||
64 * 1024 * 1024,
|
||||
undefined,
|
||||
undefined,
|
||||
scope,
|
||||
),
|
||||
value: await 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 =
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { basename, dirname, isAbsolute, resolve } from "node:path";
|
||||
import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js";
|
||||
import {
|
||||
readManagedSnapshot,
|
||||
verifyManagedSourceIdentity,
|
||||
} from "../../../dist/dotnet/ManagedSnapshot.js";
|
||||
import { withVerifierArtifactScope } from "../../lib/verifier-artifact-scope.mjs";
|
||||
import { withVerifierCleanup } from "../../lib/verifier-cleanup.mjs";
|
||||
|
||||
import { parseBinaryTarget } from "../../../dist/application/BinaryTargetResolver.js";
|
||||
import { inspectManagedArtifactBytes } from "../../../dist/dotnet/ManagedArtifactInspector.js";
|
||||
@@ -23,10 +24,13 @@ import {
|
||||
const READ_FILE_MAX_BYTES = 0x7fff_ffff;
|
||||
|
||||
export async function verifyManagedAppManifest(rawManifest, manifestPath) {
|
||||
return withVerifierArtifactScope((resources) =>
|
||||
resources.run(() =>
|
||||
verifyManifestSnapshot(rawManifest, manifestPath, resources),
|
||||
),
|
||||
const resources = new ArtifactResourceScope();
|
||||
return withVerifierCleanup(
|
||||
() =>
|
||||
resources.run(() =>
|
||||
verifyManifestSnapshot(rawManifest, manifestPath, resources),
|
||||
),
|
||||
[() => resources.close()],
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
/** 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;
|
||||
|
||||
@@ -48,8 +55,6 @@ export function discoverNumericLimitUses(
|
||||
readonly unresolved: readonly {
|
||||
readonly path: string;
|
||||
readonly offset: number;
|
||||
readonly line: number;
|
||||
readonly column: number;
|
||||
readonly reason: string;
|
||||
}[];
|
||||
};
|
||||
|
||||
@@ -25,7 +25,6 @@ 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);
|
||||
@@ -40,7 +39,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.
|
||||
*/
|
||||
const REQUIRED_RUNTIME_LIMITS = [
|
||||
export const REQUIRED_RUNTIME_LIMITS = [
|
||||
"REA_NTFS_MFT_MAX_FILE_BYTES",
|
||||
"REA_REGF_HIVE_MAX_FILE_BYTES",
|
||||
"REA_WEB_BUNDLE_ENDPOINT_LITERAL_CANDIDATES",
|
||||
@@ -171,16 +170,8 @@ const stringBindings = (sourceFile, symbols, unresolved) => {
|
||||
item.offset === offset &&
|
||||
item.reason === reason,
|
||||
)
|
||||
) {
|
||||
const location = sourceFile.getLineAndCharacterOfPosition(offset);
|
||||
unresolved.push({
|
||||
path,
|
||||
offset,
|
||||
line: location.line + 1,
|
||||
column: location.character + 1,
|
||||
reason,
|
||||
});
|
||||
}
|
||||
)
|
||||
unresolved.push({ path, offset, reason });
|
||||
};
|
||||
const initializer = (identifier) => {
|
||||
if (!candidates.has(identifier.text)) return undefined;
|
||||
@@ -958,7 +949,7 @@ if (direct) {
|
||||
|
||||
for (const issue of discovered.unresolved)
|
||||
console.log(
|
||||
`${issue.path}:${issue.line}:${issue.column}: numeric limit candidate unresolved (${issue.reason})`,
|
||||
`${issue.path}:${issue.offset}: numeric limit candidate unresolved (${issue.reason})`,
|
||||
);
|
||||
|
||||
for (const [name, file] of [...discovered].sort(([left], [right]) =>
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { JadxToolPort } from "./JadxAnalysis.js";
|
||||
import {
|
||||
androidNativeDeclarationsSchema,
|
||||
type AndroidNativeDeclarations,
|
||||
} from "../domain/android/androidNativeDeclarations.js";
|
||||
import type { AndroidNativeDeclarations } from "../domain/android/androidNativeDeclarations.js";
|
||||
import { jadxNativeDeclarationsSchema } from "./JadxProtocol.js";
|
||||
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
||||
|
||||
/** Validate the native-only batch at its producing boundary, preserving raw facts. */
|
||||
@@ -11,7 +9,7 @@ export const readAndroidNativeInventory = async (
|
||||
snapshot: string,
|
||||
): Promise<AndroidNativeDeclarations> => {
|
||||
const raw = await tools.json("rea_native_declarations", { path: snapshot });
|
||||
const parsed = androidNativeDeclarationsSchema.safeParse(raw);
|
||||
const parsed = jadxNativeDeclarationsSchema.safeParse(raw);
|
||||
if (!parsed.success)
|
||||
throw new AnalysisOutputError(
|
||||
"inspect_android_jni_bindings",
|
||||
|
||||
@@ -10,6 +10,11 @@ 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({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { fixedAllowanceOptions } from "../domain/fixedAllowanceRemediation.js";
|
||||
import { ArtifactReaderFailure } from "../artifacts/ArtifactReader.js";
|
||||
import { stableArtifactInputFailure } from "../artifacts/StableArtifactFailure.js";
|
||||
import { stableArtifactInputRecovery } from "../artifacts/StableArtifactFailure.js";
|
||||
import {
|
||||
artifactFilePrimaryFacts,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
@@ -231,7 +231,11 @@ const artifactReaderFailure = (
|
||||
},
|
||||
);
|
||||
case "path":
|
||||
return stableArtifactInputFailure(cause, OPERATION);
|
||||
return new AnalysisInputError(
|
||||
OPERATION,
|
||||
{ cause, ...stableArtifactInputRecovery(cause) },
|
||||
[{ path: ["path"], reason: "invalid_format", message: cause.message }],
|
||||
);
|
||||
default:
|
||||
return adapterFailure(cause, path);
|
||||
}
|
||||
|
||||
@@ -18,123 +18,6 @@ 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<string, unknown> };
|
||||
};
|
||||
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<string, unknown>;
|
||||
const extensions = native.get("extensions") as Map<string, unknown>;
|
||||
const entry = extensions.get("rea") as Map<unknown, unknown>;
|
||||
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<string, unknown> };
|
||||
};
|
||||
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<string, unknown> };
|
||||
};
|
||||
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<string, unknown> };
|
||||
};
|
||||
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<string, unknown> };
|
||||
};
|
||||
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 =
|
||||
|
||||
@@ -13,7 +13,6 @@ import { ConfigurationError } from "../domain/configurationErrors.js";
|
||||
import {
|
||||
copyYamlAliasSource,
|
||||
preserveYamlAliasValues,
|
||||
resolveYamlEditPath,
|
||||
} from "./YamlConfigurationAliases.js";
|
||||
import {
|
||||
retainYamlConfigurationSource,
|
||||
@@ -252,7 +251,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 = resolveYamlEditPath(document, path.slice(0, length));
|
||||
const parent = path.slice(0, length);
|
||||
let previous = document.getIn(parent, true);
|
||||
if (isAlias(previous)) {
|
||||
previous = copyYamlAliasSource(previous, document);
|
||||
@@ -263,28 +262,28 @@ export const editGooseConfigurationYaml = (
|
||||
document.setIn(parent, document.createNode({}));
|
||||
} else if (!isMap(previous))
|
||||
throw new ConfigurationError(
|
||||
`Goose configuration ${path.slice(0, length).join(".")} must be a mapping`,
|
||||
`Goose configuration ${parent.join(".")} must be a mapping`,
|
||||
);
|
||||
}
|
||||
const sourcePath = resolveYamlEditPath(document, path);
|
||||
const previous = document.getIn(sourcePath, true);
|
||||
const parent = document.getIn(sourcePath.slice(0, -1), true);
|
||||
const previous = document.getIn(path, true);
|
||||
const parent = document.getIn(path.slice(0, -1), true);
|
||||
const removedKey =
|
||||
value === undefined && isMap(parent)
|
||||
? parent.items.find((pair) => pair.key === sourcePath.at(-1))?.key
|
||||
? parent.items.find(
|
||||
(pair) => isScalar(pair.key) && pair.key.value === path.at(-1),
|
||||
)?.key
|
||||
: undefined;
|
||||
const retained = preserveYamlAliasValues(document, [previous, removedKey]);
|
||||
if (value === undefined) {
|
||||
if (!document.deleteIn(sourcePath)) return text;
|
||||
if (!document.deleteIn(path)) return text;
|
||||
} else
|
||||
document.setIn(
|
||||
sourcePath,
|
||||
path,
|
||||
isMap(previous) || isSeq(previous)
|
||||
? retainYamlConfigurationSource(
|
||||
previous,
|
||||
document.createNode(value),
|
||||
retained,
|
||||
document,
|
||||
)
|
||||
: value,
|
||||
);
|
||||
|
||||
@@ -12,69 +12,6 @@ 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<string, unknown>;
|
||||
};
|
||||
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<string, unknown> };
|
||||
};
|
||||
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";
|
||||
|
||||
@@ -3,21 +3,16 @@ 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 {
|
||||
retainYamlConfigurationSource,
|
||||
@@ -273,102 +268,6 @@ const parseHermesDocument = (text: string) => {
|
||||
return document;
|
||||
};
|
||||
|
||||
const projectedYamlPathExists = (
|
||||
document: Document,
|
||||
path: readonly string[],
|
||||
): boolean => {
|
||||
let current: unknown = document.toJS();
|
||||
for (const segment of path) {
|
||||
if (
|
||||
typeof current !== "object" ||
|
||||
current === null ||
|
||||
!Object.hasOwn(current, segment)
|
||||
)
|
||||
return false;
|
||||
current = (current as Record<string, unknown>)[segment];
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
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<object>(),
|
||||
): 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);
|
||||
@@ -400,7 +299,7 @@ export const editHermesConfigurationYaml = (
|
||||
document.contents = map;
|
||||
}
|
||||
for (let length = 1; length < path.length; length++) {
|
||||
const parent = resolveYamlEditPath(document, path.slice(0, length));
|
||||
const parent = path.slice(0, length);
|
||||
const previous = document.getIn(parent, true);
|
||||
if (isAlias(previous)) {
|
||||
const copied = copyYamlAliasSource(previous, document);
|
||||
@@ -409,49 +308,35 @@ 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 sourcePath = resolveYamlEditPath(document, path);
|
||||
const previous = document.getIn(sourcePath, true);
|
||||
const previous = document.getIn(path, true);
|
||||
// Deletion removes the pair's key too; replacement retains that key node.
|
||||
const parent =
|
||||
value === undefined
|
||||
? document.getIn(sourcePath.slice(0, -1), true)
|
||||
: undefined;
|
||||
if (
|
||||
value === undefined &&
|
||||
!document.hasIn(sourcePath) &&
|
||||
projectedYamlPathExists(document, path)
|
||||
)
|
||||
throw inheritedRegistrationRemovalError();
|
||||
value === undefined ? document.getIn(path.slice(0, -1), true) : undefined;
|
||||
const removedKey = isMap(parent)
|
||||
? parent.items.find((pair) => pair.key === sourcePath.at(-1))?.key
|
||||
? parent.items.find(
|
||||
(pair) => isScalar(pair.key) && pair.key.value === path.at(-1),
|
||||
)?.key
|
||||
: undefined;
|
||||
const retainedSources = preserveYamlAliasValues(document, [
|
||||
previous,
|
||||
removedKey,
|
||||
]);
|
||||
if (value === undefined) {
|
||||
if (!document.deleteIn(sourcePath)) return text;
|
||||
if (projectedYamlPathExists(document, path))
|
||||
throw inheritedRegistrationRemovalError();
|
||||
if (!document.deleteIn(path)) return text;
|
||||
} else {
|
||||
// Let YAML's scalar update preserve numeric formats for a direct edit.
|
||||
// Replacing containers needs explicit retention of unchanged child nodes.
|
||||
document.setIn(
|
||||
sourcePath,
|
||||
path,
|
||||
isMap(previous) || isSeq(previous)
|
||||
? retainYamlConfigurationSource(
|
||||
previous,
|
||||
document.createNode(value),
|
||||
retainedSources,
|
||||
document,
|
||||
)
|
||||
: value,
|
||||
);
|
||||
|
||||
@@ -296,7 +296,6 @@ it(
|
||||
process.execPath,
|
||||
["--import", "tsx", "--input-type=module", "--eval", CATALOG_PROGRAM],
|
||||
{
|
||||
timeoutMs: 50_000,
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
SystemRoot: process.env.SystemRoot,
|
||||
|
||||
@@ -102,7 +102,6 @@ it("repairs equal-launcher Copilot JSONC syntax while strict JSON and standard J
|
||||
process.execPath,
|
||||
["--import", "tsx", entrypoint],
|
||||
{
|
||||
timeoutMs: 20_000,
|
||||
env: {
|
||||
PATH: process.env.PATH,
|
||||
SystemRoot: process.env.SystemRoot,
|
||||
|
||||
@@ -1089,13 +1089,11 @@ const removeClient = async (
|
||||
// clientRegistrationRemoval admitted only names whose repeats REA owns.
|
||||
{ removeShadowedMembersAt: editedPaths },
|
||||
);
|
||||
} catch (cause: unknown) {
|
||||
} catch {
|
||||
return item(
|
||||
client.name,
|
||||
"failed",
|
||||
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.",
|
||||
"Configuration could not be updated, so no change was made. Repair the configuration, then rerun uninstall.",
|
||||
);
|
||||
}
|
||||
if (!serialized.ok)
|
||||
|
||||
@@ -9,36 +9,6 @@ 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<Alias, Node | undefined>;
|
||||
|
||||
@@ -3,17 +3,10 @@ import {
|
||||
isNode,
|
||||
isScalar,
|
||||
isSeq,
|
||||
Pair,
|
||||
visit,
|
||||
YAMLMap,
|
||||
type Document,
|
||||
type Node,
|
||||
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 = (
|
||||
@@ -33,50 +26,12 @@ export const yamlNumericValuesEqual = (
|
||||
);
|
||||
};
|
||||
|
||||
const normalizeYamlInteger = (_key: unknown, value: unknown): unknown =>
|
||||
typeof value === "bigint" && Number.isSafeInteger(Number(value))
|
||||
? Number(value)
|
||||
: value;
|
||||
|
||||
const containsRetainedSource = (
|
||||
roots: readonly unknown[],
|
||||
retainedSources: ReadonlySet<Node>,
|
||||
): 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;
|
||||
};
|
||||
|
||||
/** Keep source nodes and annotations when updating a value. */
|
||||
/** Keep source annotations and unchanged scalars when updating a value. */
|
||||
export const retainYamlConfigurationSource = (
|
||||
previous: unknown,
|
||||
next: Node,
|
||||
retainedSources: ReadonlySet<Node>,
|
||||
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) &&
|
||||
@@ -105,235 +60,53 @@ export const retainYamlConfigurationSource = (
|
||||
next.spaceBefore = previous.spaceBefore;
|
||||
}
|
||||
if (isMap(previous) && isMap(next)) {
|
||||
retainYamlMapSource(previous, next, retainedSources, document);
|
||||
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,
|
||||
);
|
||||
}
|
||||
} else if (isSeq(previous) && isSeq(next)) {
|
||||
retainYamlSequenceSource(previous, next, retainedSources, document);
|
||||
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,
|
||||
);
|
||||
}
|
||||
return next;
|
||||
};
|
||||
|
||||
const retainYamlSequenceSource = (
|
||||
previous: YAMLSeq,
|
||||
next: YAMLSeq,
|
||||
retainedSources: ReadonlySet<Node>,
|
||||
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,
|
||||
);
|
||||
};
|
||||
|
||||
const retainYamlMapSource = (
|
||||
previous: YAMLMap,
|
||||
next: YAMLMap,
|
||||
retainedSources: ReadonlySet<Node>,
|
||||
document: Document,
|
||||
): void => {
|
||||
const hasMerges = retainYamlMergeSources(previous, next, document);
|
||||
const hasSourceOnlyKeys = retainProjectedYamlKeys(
|
||||
previous,
|
||||
next,
|
||||
retainedSources,
|
||||
document,
|
||||
);
|
||||
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) {
|
||||
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;
|
||||
pair.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) || typeof key !== "string"
|
||||
? pair
|
||||
: entries.get(key);
|
||||
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<string, unknown> =>
|
||||
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<string>();
|
||||
for (const [key, value] of Object.entries(inheritedValues)) {
|
||||
if (Object.hasOwn(directValues, key)) continue;
|
||||
if (!Object.hasOwn(desired, key))
|
||||
throw new ConfigurationError(
|
||||
`Cannot remove inherited YAML setting ${JSON.stringify(key)} while retaining its merge.`,
|
||||
);
|
||||
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<Node>,
|
||||
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<string, typeof previous.items>();
|
||||
const sourceOnlyKeys = new Set<string>();
|
||||
for (const pair of previous.items) {
|
||||
if (isYamlMergeKey(pair.key)) continue;
|
||||
let key: string;
|
||||
if (isScalar(pair.key) && typeof pair.key.value === "string")
|
||||
key = pair.key.value;
|
||||
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<string, unknown>;
|
||||
const newProperties = newValue as Record<string, unknown>;
|
||||
const replacements = new Map<string, typeof previous.items>();
|
||||
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) ||
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
parseEvidence,
|
||||
rebindEvidenceToTargetPath,
|
||||
} from "../../domain/evidence.js";
|
||||
import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisViewRequest.js";
|
||||
import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisView.js";
|
||||
import { functionDossierSchema } from "../../domain/native/functionDossier.js";
|
||||
import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
|
||||
import {
|
||||
|
||||
@@ -28,11 +28,11 @@ import {
|
||||
} from "../../domain/evidence.js";
|
||||
import {
|
||||
inspectAnalysisViewInputSchema,
|
||||
prepareAnalysisViewProjection,
|
||||
type AnalysisViewResult,
|
||||
type AnalysisViewRequest,
|
||||
type InspectAnalysisViewInput,
|
||||
} from "../../domain/analysisView/analysisViewRequest.js";
|
||||
import { prepareAnalysisViewProjection } from "../../domain/analysisView/analysisView.js";
|
||||
import type { AnalysisViewResult } from "../../domain/analysisView/analysisViewResult.js";
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
immutableJsonSnapshotMember,
|
||||
isImmutableJsonSnapshot,
|
||||
|
||||
@@ -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/analysisViewRequest.js";
|
||||
import type { AnalysisViewRequest } from "../../domain/analysisView/analysisView.js";
|
||||
import { projectBrowserScenarioView } from "../../domain/analysisView/browserScenarioView.js";
|
||||
|
||||
const tree = (text: string) => ({
|
||||
|
||||
@@ -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/analysisViewResult.js";
|
||||
import { analysisViewResultSchema } from "../../domain/analysisView/analysisView.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import { jsonValueValidationIssue } from "../../domain/jsonValue.js";
|
||||
import { compareSourceToBundle } from "../../domain/javascript/sourceToBundleComparison.js";
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { expect, it } from "vitest";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisViewRequest.js";
|
||||
import { analysisViewResultSchema } from "../../domain/analysisView/analysisViewResult.js";
|
||||
import {
|
||||
analysisViewResultSchema,
|
||||
inspectAnalysisViewInputSchema,
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
RELOCATION_SYMBOL_UNRECORDED,
|
||||
projectNativeLoadImageView,
|
||||
|
||||
@@ -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/analysisViewResult.js";
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
inspectAnalysisView,
|
||||
projectRetainedAnalysis,
|
||||
|
||||
@@ -2,8 +2,10 @@ import { expect, it } from "vitest";
|
||||
|
||||
import { inspectAnalysisView } from "../analysisView/AnalysisViewService.js";
|
||||
import { ARTIFACT_COMPARISON_EXAMPLE } from "../../contracts/artifactComparisonExample.js";
|
||||
import { sealAnalysisView } from "../../domain/analysisView/analysisView.js";
|
||||
import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisViewResult.js";
|
||||
import {
|
||||
artifactInventorySummaryViewResultSchema,
|
||||
sealAnalysisView,
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
createEvidence,
|
||||
parseEvidence,
|
||||
|
||||
@@ -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/analysisViewResult.js";
|
||||
import { artifactInventorySummaryViewResultSchema } from "../../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
compareArtifacts,
|
||||
type ArtifactComparisonResult,
|
||||
|
||||
@@ -7,10 +7,7 @@ import {
|
||||
} from "../EvidenceBundleFiles.js";
|
||||
import type { ResetInvestigationReceipt } from "../../domain/investigationLifecycle.js";
|
||||
|
||||
import {
|
||||
retainSavedAnalysisSnapshot,
|
||||
type SavedAnalysisSnapshot,
|
||||
} from "../../domain/savedAnalysisSnapshot.js";
|
||||
import { retainSavedAnalysisSnapshot } from "../../domain/savedAnalysisSnapshot.js";
|
||||
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
@@ -42,6 +39,7 @@ import type {
|
||||
BinarySessionPort,
|
||||
BinaryOpenResult,
|
||||
OpenSelectionReceipt,
|
||||
SavedAnalysisSnapshot,
|
||||
} from "./BinarySessionPort.js";
|
||||
import { binaryTargetSummary } from "./BinaryTargetSummary.js";
|
||||
import {
|
||||
|
||||
@@ -30,6 +30,7 @@ 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 =
|
||||
|
||||
@@ -28,11 +28,7 @@ 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 {
|
||||
controller,
|
||||
type Answer,
|
||||
type ControlledComputer,
|
||||
} from "../../desktop/BuildComputerController.js";
|
||||
import type { ControlledComputer } from "../../desktop/BuildComputerController.js";
|
||||
import { BuildComputerLongRuns } from "../../desktop/BuildComputerLongRuns.js";
|
||||
import {
|
||||
endedCapture,
|
||||
@@ -41,10 +37,12 @@ import {
|
||||
} from "./DesktopBuildCaptureFailure.js";
|
||||
import {
|
||||
adapterError,
|
||||
controller,
|
||||
errorText,
|
||||
refused,
|
||||
runRea,
|
||||
WORK,
|
||||
type Answer,
|
||||
} from "./DesktopBuildCommands.js";
|
||||
import {
|
||||
exportBuild,
|
||||
|
||||
@@ -7,14 +7,12 @@ import {
|
||||
} from "../../domain/desktop/desktopCapture.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import { AGENTENV_PROVIDER } from "../../desktop/AgentEnvComputerHelper.js";
|
||||
import type {
|
||||
Answer,
|
||||
ControlledComputer,
|
||||
} from "../../desktop/BuildComputerController.js";
|
||||
import type { 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. */
|
||||
|
||||
@@ -22,6 +22,11 @@ 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. */
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
import {
|
||||
analysisViewResultSchema,
|
||||
type AnalysisViewResult,
|
||||
} from "../../domain/analysisView/analysisViewResult.js";
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import { scenarioCaptureRecords } from "../../domain/analysisView/scenarioCaptureRecords.js";
|
||||
import {
|
||||
readRecordedResponseBody,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AnalysisViewResult } from "../../domain/analysisView/analysisViewResult.js";
|
||||
import type { AnalysisViewResult } from "../../domain/analysisView/analysisView.js";
|
||||
import {
|
||||
changesState,
|
||||
type StateDocument,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { projectAnalysisView } from "../../domain/analysisView/analysisView.js";
|
||||
import { analysisViewResultSchema } from "../../domain/analysisView/analysisViewResult.js";
|
||||
import {
|
||||
projectAnalysisView,
|
||||
analysisViewResultSchema,
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import { nativeFunctionMatchingSchema } from "../../domain/native/functionMatching.js";
|
||||
import { expect, it } from "vitest";
|
||||
import { matchNativeFunctions } from "./NativeFunctionMatchingService.js";
|
||||
|
||||
@@ -3,43 +3,14 @@ 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 {
|
||||
artifactFileCancellationCause,
|
||||
bindArtifactFileCleanup,
|
||||
type ArtifactFilePrimaryFacts,
|
||||
} from "./ArtifactFileOperation.js";
|
||||
import { bindArtifactFileCleanup } 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,
|
||||
@@ -86,18 +57,6 @@ 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,
|
||||
|
||||
@@ -5,6 +5,8 @@ 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 {
|
||||
|
||||
@@ -21,19 +21,15 @@ it("retains escape-heavy strings within a heap near their decoded size", async (
|
||||
if (!Array.isArray(parsed) || parsed.length !== 463 || parsed.some((value) => value !== body)) process.exit(3);
|
||||
`;
|
||||
|
||||
const child = await processes.run(
|
||||
process.execPath,
|
||||
[
|
||||
"--max-old-space-size=48",
|
||||
"--max-semi-space-size=8",
|
||||
"--import",
|
||||
"tsx",
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
program,
|
||||
],
|
||||
{ timeoutMs: 30_000 },
|
||||
);
|
||||
const child = await processes.run(process.execPath, [
|
||||
"--max-old-space-size=48",
|
||||
"--max-semi-space-size=8",
|
||||
"--import",
|
||||
"tsx",
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
program,
|
||||
]);
|
||||
|
||||
expect(child.exitCode, child.stderr).toBe(0);
|
||||
});
|
||||
@@ -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 { stableArtifactInputFailure } from "../../../artifacts/StableArtifactFailure.js";
|
||||
import { stableArtifactInputRecovery } from "../../../artifacts/StableArtifactFailure.js";
|
||||
|
||||
/** Classify the archive owner's facts without interpreting opaque cancellation reasons. */
|
||||
export const traceFailure = (cause: unknown, path: string): AnalysisError => {
|
||||
@@ -38,7 +38,11 @@ export const traceFailure = (cause: unknown, path: string): AnalysisError => {
|
||||
},
|
||||
);
|
||||
if (cause.reason === "format" || cause.reason === "path")
|
||||
return stableArtifactInputFailure(cause, operation);
|
||||
return new AnalysisInputError(
|
||||
operation,
|
||||
{ cause, ...stableArtifactInputRecovery(cause) },
|
||||
[{ path: ["path"], reason: "invalid_format", message: cause.message }],
|
||||
);
|
||||
return new ArtifactOperationError(
|
||||
operation,
|
||||
cause.reason,
|
||||
|
||||
@@ -3,7 +3,7 @@ import {
|
||||
analysisEvidenceSources,
|
||||
undeliveredSourceClosure,
|
||||
} from "../application/AnalysisEvidenceSources.js";
|
||||
import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js";
|
||||
import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js";
|
||||
import { z } from "incur";
|
||||
import {
|
||||
isImmutableEvidence,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { AnalysisViewRequest } from "../domain/analysisView/analysisViewRequest.js";
|
||||
import type { AnalysisViewRequest } from "../domain/analysisView/analysisView.js";
|
||||
import { publishCliAnalysisEvidence } from "./analysisEvidenceDelivery.js";
|
||||
import { JAVASCRIPT_APPLICATION_PROVIDER } from "../domain/investigationProviders.js";
|
||||
import { createProgressReporter } from "../application/ProgressReporter.js";
|
||||
|
||||
@@ -7,6 +7,7 @@ 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";
|
||||
@@ -36,6 +37,8 @@ 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());
|
||||
|
||||
@@ -12,17 +12,18 @@ import {
|
||||
} from "./capabilityDeclarations.js";
|
||||
import { isAbsolute } from "node:path";
|
||||
import { createGpuCodeObjectsService } from "./gpu.js";
|
||||
import { createPacketCaptureService } from "./captures.js";
|
||||
import { tsharkHostSupported } from "../captures/TsharkCaptureProvider.js";
|
||||
import { createSignalRecordingService } from "./signals.js";
|
||||
import { sigrokHostSupported } from "../signals/SigrokRecordingProvider.js";
|
||||
import { createPacketCaptureService, tsharkHostSupported } from "./captures.js";
|
||||
import {
|
||||
createSignalRecordingService,
|
||||
sigrokHostSupported,
|
||||
} from "./signals.js";
|
||||
import { wasmArtifactModule } from "./wasm.js";
|
||||
import { apkSignatureModule } from "./apkSignature.js";
|
||||
import { evmInterfaceModule } from "./evm.js";
|
||||
import {
|
||||
evmInterfaceModule,
|
||||
evmHostSupported,
|
||||
evmLimiterAvailability,
|
||||
} from "../evm/EvmoleInterfaceProvider.js";
|
||||
} from "./evm.js";
|
||||
import { createSqliteDatabaseService } from "./sqlite.js";
|
||||
import { createDesktopApplicationService } from "./desktop.js";
|
||||
import { DesktopBuildComputers } from "../application/desktop/DesktopBuildComputers.js";
|
||||
@@ -39,8 +40,10 @@ import { createWebNetworkCaptureService } from "./webNetworkCaptures.js";
|
||||
import { createWebSourceLocationService } from "./webSourceLocations.js";
|
||||
import { createWebModuleTraceService } from "./webModules.js";
|
||||
import { createWebRuntimeService } from "./webRuntime.js";
|
||||
import { createJavaScriptRecoveryProvider } from "./javascriptRecovery.js";
|
||||
import { wakaruHostSupported } from "../javascript/recovery/WakaruCommand.js";
|
||||
import {
|
||||
createJavaScriptRecoveryProvider,
|
||||
wakaruHostSupported,
|
||||
} from "./javascriptRecovery.js";
|
||||
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
|
||||
import { createReverseEngineeringService } from "./reverseEngineering.js";
|
||||
import type {
|
||||
@@ -48,8 +51,10 @@ import type {
|
||||
CommandProviderLaunchFailures,
|
||||
} from "../application/reverse/ReverseEngineeringService.js";
|
||||
import { FirmwareAnalysisService } from "../application/firmware/FirmwareAnalysisService.js";
|
||||
import { createFirmwareAnalysisProvider } from "./firmware.js";
|
||||
import { firmwareHostSupported } from "../firmware/FirmwareCommand.js";
|
||||
import {
|
||||
createFirmwareAnalysisProvider,
|
||||
firmwareHostSupported,
|
||||
} from "./firmware.js";
|
||||
import { createAndroidAnalysisProvider } from "./android.js";
|
||||
import { createPcapAnalysisProvider } from "./pcap.js";
|
||||
import { createFlutterBuildAnalysisProvider } from "./flutter.js";
|
||||
@@ -74,6 +79,7 @@ import {
|
||||
createWindowsShortcutService,
|
||||
createRecordedCrashService,
|
||||
createBinaryLayoutService,
|
||||
pwntoolsHostSupported,
|
||||
createDevicetreeService,
|
||||
createElfNotesService,
|
||||
createNativeInitializersService,
|
||||
@@ -82,7 +88,6 @@ 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";
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { PacketCaptureService } from "../application/captures/PacketCaptureService.js";
|
||||
import { TsharkCaptureProvider } from "../captures/TsharkCaptureProvider.js";
|
||||
import {
|
||||
TsharkCaptureProvider,
|
||||
tsharkHostSupported,
|
||||
} from "../captures/TsharkCaptureProvider.js";
|
||||
|
||||
export { tsharkHostSupported };
|
||||
|
||||
export const createPacketCaptureService = (
|
||||
environment: Readonly<NodeJS.ProcessEnv> = process.env,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { EvmInterfaceService } from "../application/evm/EvmInterfaceService.js";
|
||||
import { EvmoleInterfaceProvider } from "../evm/EvmoleInterfaceProvider.js";
|
||||
import {
|
||||
EvmoleInterfaceProvider,
|
||||
evmHostSupported,
|
||||
evmLimiterAvailability,
|
||||
} from "../evm/EvmoleInterfaceProvider.js";
|
||||
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
||||
import { INSPECT_EVM_INTERFACE_CONTRACT } from "../contracts/evm/evmToolContracts.js";
|
||||
|
||||
@@ -9,6 +13,8 @@ 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.
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
import type { FirmwareAnalysisPort } from "../application/firmware/FirmwareAnalysisPort.js";
|
||||
import type { FirmwareLauncher } from "../firmware/FirmwareCommand.js";
|
||||
import {
|
||||
firmwareHostSupported,
|
||||
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<Record<string, string | undefined>> = process.env,
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import type { JavaScriptRecoveryPort } from "../application/javascript/JavaScriptRecoveryPort.js";
|
||||
import { WakaruProvider } from "../javascript/recovery/WakaruProvider.js";
|
||||
import type { WakaruLauncher } from "../javascript/recovery/WakaruCommand.js";
|
||||
import {
|
||||
wakaruHostSupported,
|
||||
type WakaruLauncher,
|
||||
} from "../javascript/recovery/WakaruCommand.js";
|
||||
|
||||
export { wakaruHostSupported };
|
||||
|
||||
/** Construct optional recovery without acquiring tools, processes or workspaces. */
|
||||
export const createJavaScriptRecoveryProvider = (
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { SignalRecordingService } from "../application/signals/SignalRecordingService.js";
|
||||
import { SigrokRecordingProvider } from "../signals/SigrokRecordingProvider.js";
|
||||
import {
|
||||
SigrokRecordingProvider,
|
||||
sigrokHostSupported,
|
||||
} from "../signals/SigrokRecordingProvider.js";
|
||||
|
||||
export { sigrokHostSupported };
|
||||
|
||||
export const createSignalRecordingService = (
|
||||
environment: Readonly<NodeJS.ProcessEnv> = process.env,
|
||||
|
||||
@@ -12,13 +12,11 @@ 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,
|
||||
} from "../domain/analysisResourceLimits.js";
|
||||
parseBridgeMaxRequestBytes,
|
||||
} from "./bridgeMaxRequestBytes.js";
|
||||
|
||||
const configured = (maximumBytes: number) =>
|
||||
({ maximumBytes, source: "setting" }) as const;
|
||||
|
||||
@@ -13,6 +13,12 @@ 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
|
||||
|
||||
@@ -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,
|
||||
} from "../domain/analysisView/analysisViewResult.js";
|
||||
projectAnalysisView,
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import { analysisViewRequestSchema } from "../domain/analysisView/analysisViewRequest.js";
|
||||
import {
|
||||
analysisViewCaptureEvidence,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { API_SURFACE_EXAMPLE } from "./analysisViewApiSurfaceExample.js";
|
||||
import { ENVIRONMENT_MANIFEST_EXAMPLE } from "./analysisViewEnvironmentManifestExample.js";
|
||||
import { inspectAnalysisViewInputSchema } from "../domain/analysisView/analysisViewRequest.js";
|
||||
import { analysisViewResultSchema } from "../domain/analysisView/analysisViewResult.js";
|
||||
import {
|
||||
analysisViewResultSchema,
|
||||
inspectAnalysisViewInputSchema,
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import type { ToolContract } from "./toolContractTypes.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
androidMethodSummaryViewResultSchema,
|
||||
androidJniSummaryViewResultSchema,
|
||||
androidJniPageViewResultSchema,
|
||||
} from "../../domain/analysisView/analysisViewResult.js";
|
||||
} from "../../domain/analysisView/analysisView.js";
|
||||
import type { ToolContract } from "../toolContractTypes.js";
|
||||
import { evidenceResultOf } from "../toolOutputSchemaPrimitives.js";
|
||||
import { toolContractMetadata } from "../toolEffects.js";
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
apiContractSummaryViewResultSchema,
|
||||
apiContractPageViewResultSchema,
|
||||
graphqlContractPageViewResultSchema,
|
||||
} from "../domain/analysisView/analysisViewResult.js";
|
||||
} from "../domain/analysisView/analysisView.js";
|
||||
import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import type { ToolContract } from "./toolContractTypes.js";
|
||||
|
||||
Loaded 100 of 435 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user