mirror of
https://github.com/morluto/rea.git
synced 2026-10-30 11:25:39 +00:00
* fix(javascript)!: preserve operator influence in semantic traces * test(javascript): align semantic influence regressions * style(javascript): format semantic IR * test(javascript): follow literal argument seed influence * fix(javascript): keep one literal node per operator operand Reuse an existing literal for operator influence, and treat TypeScript assertion wrappers as transparent when deciding whether a call result is used. * test(mcp): assert bundle path issues by field compare_bundles reports each relative path as an invalid_format issue instead of one joined diagnostic sentence. * fix(javascript): index operator literals by range and initializer Repeated string operands rescanned every retained literal and every initializer flow. Lookup now uses those indexes, and a 1200-declaration file still projects one literal per operand. * fix(javascript): keep literal and call facts on their own occurrences Operator literals match their own source range, so a nested function does not inherit the enclosing declaration's literal. A direct argument still reaches the call, while the assigned result follows modeled returns. for-loop initializers and updates are discarded call results. * test(javascript): expect conditional operand literals beside binding values A conditional binding keeps one candidate literal per union value and one operator literal per operand token. Those are different facts, so the aggregate payload check counts four nodes and compares literal seeds in query order. * perf(javascript): index call return flows by call site projectCalls filtered every call return for each assigned result. Group those flows once while projecting returns-to-call edges. --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com>
458 lines
16 KiB
TypeScript
458 lines
16 KiB
TypeScript
import { writeFile } from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect } from "vitest";
|
|
import { z } from "zod";
|
|
|
|
import { toolContract } from "../../../src/contracts/toolContracts.js";
|
|
import {
|
|
applicationFeatureSeedSchema,
|
|
applicationFeatureTraceResultSchema,
|
|
} from "../../../src/domain/javascript/javascriptFeatureTraceSchemas.js";
|
|
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
|
|
import { findApplicationFeatureSeeds } from "../../../src/domain/javascript/javascriptFeatureSeed.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import {
|
|
cliTest,
|
|
type SharedMcpCall,
|
|
type TestCli,
|
|
} from "../../support/cli/cliFixture.js";
|
|
|
|
type FeatureSeed = z.input<typeof applicationFeatureSeedSchema>;
|
|
type Trace = z.infer<typeof applicationFeatureTraceResultSchema>;
|
|
type SeedScenario = {
|
|
readonly name: string;
|
|
readonly seed: FeatureSeed;
|
|
readonly expected: {
|
|
readonly nodeId: string;
|
|
readonly basis: "node-id" | "property";
|
|
} | null;
|
|
};
|
|
|
|
const createSeedScenarios = (
|
|
graph: z.infer<typeof javascriptApplicationAnalysisResultSchema>["graph"],
|
|
): SeedScenario[] => {
|
|
const notificationRoute = graph.nodes.find(
|
|
(node) =>
|
|
node.kind === "endpoint" &&
|
|
node.observations.some(
|
|
({ properties }) =>
|
|
properties.endpoint_kind === "route" &&
|
|
properties.value === "/accounts/preferences/notifications",
|
|
),
|
|
);
|
|
const tinyRoute = graph.nodes.find(
|
|
(node) =>
|
|
node.kind === "endpoint" &&
|
|
node.observations.some(
|
|
({ properties }) =>
|
|
properties.endpoint_kind === "route" && properties.value === "/tiny",
|
|
),
|
|
);
|
|
const labeledNode = graph.nodes.find((node) =>
|
|
node.observations.some(
|
|
({ label }) => label !== null && label !== node.node_id,
|
|
),
|
|
);
|
|
if (notificationRoute === undefined || tinyRoute === undefined)
|
|
throw new Error(
|
|
"Expected literal route endpoints in the application graph",
|
|
);
|
|
if (labeledNode === undefined)
|
|
throw new Error("Expected a labeled node in the application graph");
|
|
const label = labeledNode.observations.find(
|
|
({ label: value }) => value !== null && value !== labeledNode.node_id,
|
|
)?.label;
|
|
if (label === undefined || label === null)
|
|
throw new Error("Expected the selected graph node to have a display label");
|
|
|
|
const id = labeledNode.node_id;
|
|
return [
|
|
{
|
|
name: "node ID contains prefix",
|
|
seed: { kind: "node-id", value: id.slice(0, 24), match: "contains" },
|
|
expected: { nodeId: id, basis: "node-id" },
|
|
},
|
|
{
|
|
name: "node ID exact ignores case by default",
|
|
seed: { kind: "node-id", value: id.toUpperCase(), match: "exact" },
|
|
expected: { nodeId: id, basis: "node-id" },
|
|
},
|
|
{
|
|
name: "node ID exact can be case sensitive",
|
|
seed: {
|
|
kind: "node-id",
|
|
value: id.toUpperCase(),
|
|
match: "exact",
|
|
case_sensitive: true,
|
|
},
|
|
expected: null,
|
|
},
|
|
{
|
|
name: "node ID never falls back to a display label",
|
|
seed: { kind: "node-id", value: label, match: "exact" },
|
|
expected: null,
|
|
},
|
|
{
|
|
name: "generated route label exact",
|
|
seed: {
|
|
kind: "route",
|
|
value: "route endpoint",
|
|
match: "exact",
|
|
case_sensitive: true,
|
|
},
|
|
expected: null,
|
|
},
|
|
{
|
|
name: "generated route label substring",
|
|
seed: { kind: "route", value: "route", match: "contains" },
|
|
expected: null,
|
|
},
|
|
{
|
|
name: "preserved long route exact",
|
|
seed: {
|
|
kind: "route",
|
|
value: "/accounts/preferences/notifications",
|
|
match: "exact",
|
|
},
|
|
expected: { nodeId: notificationRoute.node_id, basis: "property" },
|
|
},
|
|
{
|
|
name: "preserved route substring",
|
|
seed: {
|
|
kind: "route",
|
|
value: "/accounts/preferences",
|
|
match: "contains",
|
|
},
|
|
expected: { nodeId: notificationRoute.node_id, basis: "property" },
|
|
},
|
|
{
|
|
name: "short preserved route exact",
|
|
seed: { kind: "route", value: "/tiny", match: "exact" },
|
|
expected: { nodeId: tinyRoute.node_id, basis: "property" },
|
|
},
|
|
];
|
|
};
|
|
|
|
const analyzeFixture = async (cli: TestCli, root: string) => {
|
|
const response = await cli.run({
|
|
arguments: [
|
|
"analyze-javascript-application",
|
|
root,
|
|
"--artifact-format",
|
|
"directory",
|
|
"--json",
|
|
],
|
|
});
|
|
expect(response.exitCode).toBe(0);
|
|
const evidence = toolContract(
|
|
"analyze_javascript_application",
|
|
).outputSchema.parse(response.json);
|
|
return {
|
|
evidence,
|
|
result: javascriptApplicationAnalysisResultSchema.parse(
|
|
evidence.normalized_result,
|
|
),
|
|
};
|
|
};
|
|
|
|
const traceThroughCli = async (
|
|
cli: TestCli,
|
|
application: unknown,
|
|
scenario: Pick<SeedScenario, "name" | "seed">,
|
|
) => {
|
|
const root = await createTestTempDirectory("rea-feature-seed-request-");
|
|
const input = join(root, "trace.json");
|
|
await writeFile(
|
|
input,
|
|
JSON.stringify({ application, seed: scenario.seed, direction: "both" }),
|
|
);
|
|
const response = await cli.run({
|
|
arguments: ["trace-application-feature", input, "--json"],
|
|
});
|
|
expect(response.exitCode, `${scenario.name} via CLI`).toBe(0);
|
|
const evidence = toolContract("trace_application_feature").outputSchema.parse(
|
|
response.json,
|
|
);
|
|
return {
|
|
evidence,
|
|
result: applicationFeatureTraceResultSchema.parse(
|
|
evidence.normalized_result,
|
|
),
|
|
};
|
|
};
|
|
|
|
const traceThroughMcp = async (
|
|
call: SharedMcpCall,
|
|
application: unknown,
|
|
scenario: Pick<SeedScenario, "name" | "seed">,
|
|
) => {
|
|
const response = await call("trace_application_feature", {
|
|
application,
|
|
seed: scenario.seed,
|
|
direction: "both",
|
|
});
|
|
expect(response.isError, `${scenario.name} via MCP`).not.toBe(true);
|
|
const evidence = toolContract("trace_application_feature").outputSchema.parse(
|
|
response.structuredContent,
|
|
);
|
|
return {
|
|
evidence,
|
|
result: applicationFeatureTraceResultSchema.parse(
|
|
evidence.normalized_result,
|
|
),
|
|
};
|
|
};
|
|
|
|
const expectTrace = (trace: Trace, scenario: SeedScenario): void => {
|
|
if (scenario.expected === null) {
|
|
expect(trace.seed_matches).toEqual([]);
|
|
expect(trace.summary.matched_seeds).toBe(0);
|
|
expect(trace.coverage.status).toBe("no-match");
|
|
return;
|
|
}
|
|
expect(trace.seed_matches).toHaveLength(1);
|
|
expect(trace.seed_matches[0]).toMatchObject({
|
|
node_id: scenario.expected.nodeId,
|
|
basis: scenario.expected.basis,
|
|
});
|
|
expect(trace.summary.matched_seeds).toBe(1);
|
|
expect(trace.coverage.status).toBe("complete-within-source");
|
|
if (scenario.seed.kind === "node-id")
|
|
expect(trace.seed_matches[0]).toMatchObject({
|
|
basis: "node-id",
|
|
field: "node_id",
|
|
});
|
|
else
|
|
expect(trace.seed_matches[0]?.field).toMatch(
|
|
/\.properties\.(value|path)$/u,
|
|
);
|
|
};
|
|
|
|
cliTest(
|
|
"discovers and traces direct call argument literals",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-call-literal-");
|
|
await writeFile(
|
|
join(root, "module.js"),
|
|
'client.rpc("proc_named", 7, false, unknown);\n',
|
|
);
|
|
const { evidence, result } = await analyzeFixture(cli, root);
|
|
const callSite = result.semantic_graph.nodes.find(
|
|
(node) =>
|
|
node.kind === "call-site" &&
|
|
Array.isArray(node.properties.arguments) &&
|
|
node.properties.arguments.some(
|
|
(entry) =>
|
|
entry !== null &&
|
|
typeof entry === "object" &&
|
|
!Array.isArray(entry) &&
|
|
entry.value === "proc_named",
|
|
),
|
|
);
|
|
if (callSite === undefined)
|
|
throw new Error("Expected retained call argument");
|
|
const call = await cli.sharedMcpCall?.();
|
|
if (call === undefined) throw new Error("Expected MCP session");
|
|
const feature = await call("trace_application_feature", {
|
|
application: evidence,
|
|
seed: { kind: "string", value: "proc_named", match: "exact" },
|
|
direction: "both",
|
|
});
|
|
expect(feature.isError).not.toBe(true);
|
|
const featureEvidence = toolContract(
|
|
"trace_application_feature",
|
|
).outputSchema.parse(feature.structuredContent);
|
|
const trace = applicationFeatureTraceResultSchema.parse(
|
|
featureEvidence.normalized_result,
|
|
);
|
|
expect(trace.semantic_discovery?.matches).toContainEqual({
|
|
node_id: callSite.node_id,
|
|
kind: "call-site",
|
|
trace_seed: { kind: "semantic-node", node_id: callSite.node_id },
|
|
});
|
|
const semantic = await call("trace_javascript_semantics", {
|
|
application: evidence,
|
|
query: {
|
|
seed: { kind: "literal", value: "proc_named" },
|
|
direction: "forward-influence",
|
|
},
|
|
});
|
|
expect(semantic.isError).not.toBe(true);
|
|
const semanticEvidence = toolContract(
|
|
"trace_javascript_semantics",
|
|
).outputSchema.parse(semantic.structuredContent);
|
|
const argument = result.semantic_graph.nodes.find(
|
|
(node) =>
|
|
node.kind === "expression" &&
|
|
node.properties.literal_argument_value === "proc_named" &&
|
|
node.properties.literal_argument_call_site_node_id === callSite.node_id,
|
|
);
|
|
if (argument === undefined)
|
|
throw new Error("Expected literal argument node");
|
|
expect(semanticEvidence.normalized_result).toMatchObject({
|
|
seed_node_ids: expect.arrayContaining([argument.node_id]),
|
|
nodes: expect.arrayContaining([
|
|
expect.objectContaining({ node_id: callSite.node_id }),
|
|
]),
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"discovers semantic-only labels with paged CLI and MCP follow-up",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-feature-semantic-label-");
|
|
await writeFile(
|
|
join(root, "module.js"),
|
|
"function uniqueSemanticCandidate(uniqueSemanticCandidate) { return uniqueSemanticCandidate; }\nuniqueSemanticCandidate(1);\n",
|
|
);
|
|
const { evidence, result } = await analyzeFixture(cli, root);
|
|
const seed = applicationFeatureSeedSchema.parse({
|
|
kind: "string",
|
|
value: "uniqueSemanticCandidate",
|
|
match: "exact",
|
|
case_sensitive: true,
|
|
});
|
|
expect(findApplicationFeatureSeeds(result.graph.nodes, seed)).toEqual([]);
|
|
const expected = result.semantic_graph.nodes.filter(
|
|
(node) => node.label === seed.value,
|
|
);
|
|
expect(expected.length).toBeGreaterThan(0);
|
|
const request = {
|
|
application: evidence,
|
|
seed,
|
|
direction: "both",
|
|
semantic_page: { offset: 0, limit: 1 },
|
|
};
|
|
const input = join(root, "request.json");
|
|
await writeFile(input, JSON.stringify(request));
|
|
const cliResult = await cli.run({
|
|
arguments: ["trace-application-feature", input, "--json"],
|
|
});
|
|
expect(cliResult.exitCode).toBe(0);
|
|
const cliEvidence = toolContract(
|
|
"trace_application_feature",
|
|
).outputSchema.parse(cliResult.json);
|
|
const trace = applicationFeatureTraceResultSchema.parse(
|
|
cliEvidence.normalized_result,
|
|
);
|
|
expect(trace.coverage.status).toBe("no-match");
|
|
expect(trace.semantic_discovery).toMatchObject({
|
|
source_graph_id: result.semantic_graph.graph_id,
|
|
source_coverage: result.semantic_graph.coverage.status,
|
|
total: expected.length,
|
|
offset: 0,
|
|
limit: 1,
|
|
next_offset: expected.length > 1 ? 1 : null,
|
|
matches: [{ node_id: expected[0]?.node_id, kind: expected[0]?.kind }],
|
|
});
|
|
const call = await cli.sharedMcpCall?.();
|
|
if (call === undefined) throw new Error("Expected file MCP session");
|
|
const mcpResult = await call("trace_application_feature", request);
|
|
expect(mcpResult.isError).not.toBe(true);
|
|
const mcpEvidence = toolContract(
|
|
"trace_application_feature",
|
|
).outputSchema.parse(mcpResult.structuredContent);
|
|
expect(mcpEvidence.normalized_result).toEqual(trace);
|
|
const afterFirst = await call("trace_application_feature", {
|
|
...request,
|
|
semantic_page: { offset: 1, limit: 1 },
|
|
});
|
|
expect(afterFirst.isError).not.toBe(true);
|
|
const afterFirstEvidence = toolContract(
|
|
"trace_application_feature",
|
|
).outputSchema.parse(afterFirst.structuredContent);
|
|
const afterFirstTrace = applicationFeatureTraceResultSchema.parse(
|
|
afterFirstEvidence.normalized_result,
|
|
);
|
|
expect(afterFirstTrace.semantic_discovery).toMatchObject({
|
|
total: expected.length,
|
|
offset: 1,
|
|
matches: expected
|
|
.slice(1, 2)
|
|
.map(({ node_id, kind }) => ({ node_id, kind })),
|
|
});
|
|
const match = trace.semantic_discovery?.matches[0];
|
|
if (match === undefined) throw new Error("Expected semantic match");
|
|
const semantic = await call("trace_javascript_semantics", {
|
|
application: evidence,
|
|
query: { seed: match.trace_seed, direction: "backward-provenance" },
|
|
});
|
|
expect(semantic.isError).not.toBe(true);
|
|
const semanticEvidence = toolContract(
|
|
"trace_javascript_semantics",
|
|
).outputSchema.parse(semantic.structuredContent);
|
|
expect(semanticEvidence.normalized_result).toMatchObject({
|
|
seed_node_ids: [match.node_id],
|
|
});
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"matches feature seeds against node IDs and preserved route values through CLI and MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-feature-seed-selection-");
|
|
await writeFile(
|
|
join(root, "routes.js"),
|
|
`const notificationRoute = { path: "/accounts/preferences/notifications" };\nconst tinyRoute = { path: "/tiny" };\nmodule.exports = { notificationRoute, tinyRoute };\n`,
|
|
);
|
|
const { evidence, result } = await analyzeFixture(cli, root);
|
|
const call = await cli.sharedMcpCall?.();
|
|
if (call === undefined) throw new Error("Expected file MCP session");
|
|
for (const scenario of createSeedScenarios(result.graph)) {
|
|
const { result: cliTrace } = await traceThroughCli(
|
|
cli,
|
|
evidence,
|
|
scenario,
|
|
);
|
|
const { result: mcpTrace } = await traceThroughMcp(
|
|
call,
|
|
evidence,
|
|
scenario,
|
|
);
|
|
expectTrace(cliTrace, scenario);
|
|
expectTrace(mcpTrace, scenario);
|
|
expect(mcpTrace.seed_matches).toEqual(cliTrace.seed_matches);
|
|
expect(mcpTrace.coverage).toEqual(cliTrace.coverage);
|
|
}
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"normalizes API seed defaults before deriving matching and missing trace identities through CLI and MCP",
|
|
async ({ cli }) => {
|
|
const root = await createTestTempDirectory("rea-feature-seed-defaults-");
|
|
await writeFile(
|
|
join(root, "preload.js"),
|
|
`
|
|
const { contextBridge } = require("electron");
|
|
contextBridge.exposeInMainWorld("desktop", { openProject: () => true });
|
|
`,
|
|
);
|
|
const { evidence } = await analyzeFixture(cli, root);
|
|
const call = await cli.sharedMcpCall?.();
|
|
if (call === undefined) throw new Error("Expected file MCP session");
|
|
for (const value of ["openProject", "missingProject"]) {
|
|
const omitted = { name: value, seed: { kind: "api" as const, value } };
|
|
const explicit = {
|
|
...omitted,
|
|
seed: { ...omitted.seed, match: null, case_sensitive: false },
|
|
};
|
|
const omittedCli = await traceThroughCli(cli, evidence, omitted);
|
|
const explicitCli = await traceThroughCli(cli, evidence, explicit);
|
|
const omittedMcp = await traceThroughMcp(call, evidence, omitted);
|
|
const explicitMcp = await traceThroughMcp(call, evidence, explicit);
|
|
expect(omittedCli, value).toEqual(explicitCli);
|
|
expect(omittedMcp, value).toEqual(explicitMcp);
|
|
expect(omittedMcp, value).toEqual(omittedCli);
|
|
expect(omittedCli.result.seed).toEqual(explicit.seed);
|
|
if (value === "openProject")
|
|
expect(omittedCli.result.summary.matched_seeds).toBeGreaterThan(0);
|
|
else {
|
|
expect(omittedCli.result.summary.matched_seeds).toBe(0);
|
|
expect(omittedCli.result.coverage.status).toBe("no-match");
|
|
}
|
|
}
|
|
},
|
|
);
|