mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 21:36:13 +00:00
fix(javascript): preserve semantic query identity and source coverage
This commit is contained in:
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"),
|
||||
|
||||
Reference in new issue
Block a user