fix(javascript): preserve semantic query identity and source coverage

This commit is contained in:
morluto committed 2026-10-24 03:00:08 +08:00
1 parent 34552ff0df
commit cf5e4f6fa3
16 files changed
+362 -51

No files matched your search

@@ -34,6 +34,18 @@ export interface JavaScriptArtifactFile {
};
}
/** Producer text exclusions shared by full and incremental source projections. */
export const javaScriptArtifactTextExclusion = (
file: JavaScriptArtifactFile,
): string | null => {
if (file.text.included || file.text.reason === "not-applicable") return null;
if (file.text.reason === "invalid-utf8" && file.kind !== "javascript")
return null;
return file.text.reason === "outside-analysis-scope"
? "Source was not analyzed because it is outside the caller-selected source_paths."
: "Source bytes are not valid UTF-8; JavaScript semantics were not analyzed.";
};
/**
* One inventoried regular file not backed by analyzed source bytes. Its inventory
* occurrence remains available even when content is opaque or unavailable.
@@ -313,6 +313,24 @@ function* checkCoverageSteps(
path: ["coverage", "limits"],
message: "Truncated graph coverage must identify an effective limit",
});
if (graph.coverage.truncated && graph.coverage.status !== "partial")
context.addIssue({
code: "custom",
path: ["coverage", "status"],
message: "Truncated semantic graph coverage must be partial",
});
if (
graph.coverage.truncated &&
graph.coverage.omitted_nodes === 0 &&
graph.coverage.omitted_relations === 0 &&
omittedUnknowns.length === 0
)
context.addIssue({
code: "custom",
path: ["coverage"],
message:
"Truncated semantic graph coverage must acknowledge omitted facts",
});
const relationsByFamily = new Map(
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES.map((family) => [family, 0]),
);
@@ -37,6 +37,68 @@ import {
import { queryJavaScriptSemanticGraph } from "./javascriptSemanticQuery.js";
const SHA = "a".repeat(64);
it.each(["unknown", "unavailable", "partial"] as const)(
"rejects contradictory truncated %s graph coverage",
(status) => {
const input = semanticFixtureInput();
expect(() =>
createJavaScriptSemanticGraph({
...input,
coverage: {
...input.coverage,
status,
truncated: true,
limits: [{ name: "nodes", value: 1, unit: "items" }],
},
}),
).toThrow(/Truncated semantic graph coverage/u);
},
);
it("accepts truncated partial coverage with unknown omission extents", () => {
const input = semanticFixtureInput();
expect(
createJavaScriptSemanticGraph({
...input,
coverage: {
...input.coverage,
status: "partial",
truncated: true,
omitted_nodes: null,
omitted_relations: null,
limits: [{ name: "nodes", value: 1, unit: "items" }],
},
}).coverage,
).toMatchObject({ status: "partial", truncated: true });
});
it("accepts omitted unknown occurrences as the reason for semantic truncation", () => {
const input = semanticFixtureInput();
const family = input.coverage.families[0]!.family;
const graph = createJavaScriptSemanticGraph({
...input,
coverage: {
...input.coverage,
status: "partial",
truncated: true,
omitted_unknown_occurrences: [
{ family, reason: "resource-limit", occurrences: 1 },
],
limits: [{ name: "unknowns", value: 1, unit: "items" }],
families: input.coverage.families.map((coverage) =>
coverage.family === family
? { ...coverage, status: "partial" }
: coverage,
),
},
});
expect(graph.coverage).toMatchObject({
truncated: true,
omitted_nodes: 0,
omitted_relations: 0,
});
});
const node = (
kind: (typeof JAVASCRIPT_SEMANTIC_NODE_KINDS)[number],
role: string,
@@ -48,8 +48,17 @@ export const resolveJavaScriptSemanticQuerySeeds = (
.map(({ function_node_id }) => function_node_id)
: [],
);
const unresolvedAccesses = new Set(
input.seed.kind === "property"
? graph.unknowns.flatMap(({ family, node_id }) =>
family === "object-flow" && node_id !== null ? [node_id] : [],
)
: [],
);
return graph.nodes
.filter((node) => seedMatches(node, input, fingerprintFunctions))
.filter((node) =>
seedMatches(node, input, fingerprintFunctions, unresolvedAccesses),
)
.map(({ node_id }) => node_id)
.sort(compareUnicodeCodePoints);
};
@@ -58,6 +67,7 @@ const seedMatches = (
node: JavaScriptSemanticGraphNode,
input: JavaScriptSemanticQueryInput,
fingerprintFunctions: ReadonlySet<string>,
unresolvedAccesses: ReadonlySet<string>,
): boolean => {
const seed = input.seed;
if (seed.kind === "semantic-node") return node.node_id === seed.node_id;
@@ -66,8 +76,20 @@ const seedMatches = (
if (seed.kind === "function") return fingerprintFunctions.has(node.node_id);
if (seed.kind === "literal")
return node.kind === "literal" && node.properties.value === seed.value;
if (seed.kind === "property")
return node.kind === "property-slot" && node.properties.name === seed.name;
if (seed.kind === "property") {
if (node.kind === "property-slot")
return node.properties.name === seed.name;
if (node.kind !== "expression") return false;
const path = node.properties.property_path;
// Older retained graphs did not record slot resolution. Their existing
// source-bound unknown establishes an unresolved access without guessing
// from an absent or resource-omitted relation.
const slotless =
node.properties.property_slot_resolution === "unresolved" ||
(node.properties.property_slot_resolution === undefined &&
unresolvedAccesses.has(node.node_id));
return slotless && Array.isArray(path) && path.at(-1) === seed.name;
}
if (seed.kind === "endpoint")
return (
(node.kind === "request" || node.kind === "response") &&
@@ -49,7 +49,7 @@ export const javaScriptSemanticQuerySeedSchema = z.discriminatedUnion("kind", [
}),
z.strictObject({
kind: z.literal("event"),
name: z.string().min(1),
name: z.string(),
}),
z.strictObject({
kind: z.literal("boundary-field"),