feat(javascript): recover runtime semantic effects

This commit is contained in:
morluto committed 2026-07-24 13:17:38 +00:00
1 parent 697be942e8
commit 38c883bbc8
28 files changed
+5376 -27

No files matched your search

+265 -1
View File
@@ -202,7 +202,271 @@ describe("JavaScript semantic companion graph", () => {
graph.coverage.families.find(
({ family }) => family === "promise-ownership",
),
).toMatchObject({ status: "unsupported", retained_relations: 0 });
).toMatchObject({ status: "unknown", retained_relations: 0 });
});
it("projects bounded Promise ownership and unresolved sources", () => {
const graph = graphFor(`
async function run(value) {
const owned = new Promise((resolve) => resolve(value));
await Promise.resolve(owned);
Promise.resolve(value).then(work).finally(cleanup);
value.then(work);
return Promise.all([owned, Promise.resolve(value)]);
}
`);
const relations = new Set(graph.relations.map(({ relation }) => relation));
for (const expected of [
"aggregates",
"awaits",
"chains",
"creates-promise",
"detaches-task",
"owns",
"returns-task",
] as const)
expect(relations.has(expected)).toBe(true);
expect(
graph.nodes.filter(({ kind }) => kind === "promise"),
).not.toHaveLength(0);
expect(
graph.nodes.find(
({ kind, properties }) =>
kind === "promise" &&
properties.method === "all" &&
properties.ownership === "returned",
),
).toBeDefined();
expect(
graph.unknowns.some(
({ family, reason }) =>
family === "promise-ownership" && reason === "ambiguous-target",
),
).toBe(true);
expect(
graph.coverage.families.find(
({ family }) => family === "promise-ownership",
),
).toMatchObject({ status: "partial" });
});
it("keeps duplicate function fingerprints ambiguous", () => {
const graph = graphFor(`
function first(value) { return value + 1; }
function second(input) { return input + 1; }
`);
expect(graph.fingerprints).toHaveLength(2);
const digest = graph.fingerprints[0]?.fingerprint_sha256;
expect(digest).toBeDefined();
expect(graph.fingerprints[1]?.fingerprint_sha256).toBe(digest);
if (digest === undefined) throw new Error("Expected fingerprint");
const query = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "function", fingerprint_sha256: digest },
direction: "forward-influence",
});
expect(query.status).toBe("ambiguous");
expect(query.summary.total_seed_matches).toBe(2);
});
it("projects literal events and exact timer cancellation", () => {
const graph = graphFor(`
function run(bus, dynamicName) {
const handle = setInterval(tick, 10);
bus.on("ready", handler);
bus.once(dynamicName, handler);
bus.emit("ready");
bus.removeListener("ready", handler);
clearInterval(handle);
}
`);
const relations = new Set(graph.relations.map(({ relation }) => relation));
for (const expected of [
"cancels-timer",
"dispatches-candidate",
"registers-listener",
"removes-listener",
"schedules-timer",
] as const)
expect(relations.has(expected)).toBe(true);
expect(
graph.nodes.find(
({ kind, properties }) =>
kind === "event" && properties.event_name === "ready",
),
).toBeDefined();
expect(
graph.unknowns.some(
({ family, detail }) =>
family === "event" && detail.includes("Dynamic once event name"),
),
).toBe(true);
expect(
graph.coverage.families.find(({ family }) => family === "event"),
).toMatchObject({ status: "partial" });
expect(
graph.coverage.families.find(({ family }) => family === "timer"),
).toMatchObject({ status: "partial" });
});
it("projects child-process creation, I/O, listeners, and signals", () => {
const graph = graphFor(`
import { spawn } from "node:child_process";
function run(command) {
const child = spawn(
command,
["--mode", "fast"],
{ env: process.env, stdio: "pipe" },
);
child.on("exit", onExit);
child.once("error", onError);
child.kill("SIGTERM");
}
`);
const relations = new Set(graph.relations.map(({ relation }) => relation));
for (const expected of [
"connects-stdio",
"forwards-signal",
"listens-error",
"listens-exit",
"spawns",
"supplies-argv",
"supplies-env",
] as const)
expect(relations.has(expected)).toBe(true);
expect(
graph.nodes.find(
({ kind, properties }) =>
kind === "child-process" &&
properties.method === "spawn" &&
properties.command === null,
),
).toBeDefined();
expect(
graph.unknowns.some(
({ family, detail }) =>
family === "child-process" &&
detail.includes("command remains unresolved"),
),
).toBe(true);
expect(
graph.coverage.families.find(({ family }) => family === "child-process"),
).toMatchObject({ status: "partial" });
});
it("projects config precedence, request fields, and boundaries", () => {
const graph = graphFor(`
import { readFileSync } from "node:fs";
async function run(schema) {
const endpoint = process.env.API_URL ?? "https://fallback.test";
const raw = readFileSync("./config.json", "utf8");
const parsed = JSON.parse(raw);
const port = Number(process.argv[2]);
const validated = schema.parse(parsed);
const response = await fetch(endpoint, {
method: "POST",
body: validated,
});
return response.json();
}
`);
const relations = new Set(graph.relations.map(({ relation }) => relation));
for (const expected of [
"coerces",
"constructs-request",
"consumed-by",
"defaults",
"overrides",
"parses",
"reads-argv",
"reads-config",
"reads-environment",
"supplies-request-field",
] as const)
expect(relations.has(expected)).toBe(true);
expect(
graph.nodes.find(
({ kind, properties }) =>
kind === "request" && properties.method === "fetch",
),
).toBeDefined();
expect(
graph.nodes.filter(({ kind }) => kind === "boundary"),
).not.toHaveLength(0);
expect(
graph.coverage.families.find(({ family }) => family === "configuration"),
).toMatchObject({ status: "partial" });
expect(
graph.coverage.families.find(({ family }) => family === "request"),
).toMatchObject({ status: "partial" });
expect(
graph.coverage.families.find(({ family }) => family === "boundary"),
).toMatchObject({ status: "partial" });
});
it("projects built-in resource acquisition and release", () => {
const graph = graphFor(`
import { open } from "node:fs/promises";
import { connect } from "node:net";
async function run(path) {
const file = await open(path);
const socket = connect({ port: 9000 });
await file.close();
socket.end();
}
`);
const relations = new Set(graph.relations.map(({ relation }) => relation));
expect(relations.has("acquires")).toBe(true);
expect(relations.has("releases")).toBe(true);
expect(graph.nodes.filter(({ kind }) => kind === "resource")).toHaveLength(
2,
);
expect(
graph.coverage.families.find(
({ family }) => family === "resource-lifecycle",
),
).toMatchObject({ status: "partial" });
});
it("projects literal seeds and static object flow", () => {
const graph = graphFor(`
const source = { token: "TOKEN", count: 1 };
const { token } = source;
const copy = { ...source };
source.count = 2;
const read = source.token;
`);
const relations = new Set(graph.relations.map(({ relation }) => relation));
for (const expected of [
"destructures",
"reads-property",
"spreads",
"writes-property",
] as const)
expect(relations.has(expected)).toBe(true);
expect(
graph.nodes.find(
({ kind, properties }) =>
kind === "property-slot" && properties.name === "token",
),
).toBeDefined();
const literal = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "literal", value: "TOKEN" },
direction: "forward-influence",
});
expect(literal.status).toBe("ambiguous");
expect(literal.summary.total_seed_matches).toBe(3);
const property = queryJavaScriptSemanticGraph(graph, {
seed: { kind: "property", name: "token" },
direction: "forward-influence",
});
expect(property.status).toBe("found");
});
it("does not promote transformed return expressions to resolved aliases", () => {