mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
fix(javascript): retain static facts under projection pressure (#7986)
* fix(javascript): retain static facts under projection pressure * fix(javascript): scope IPC ambiguity to omitted projections * fix(javascript): preserve IPC uncertainty for omitted sources * fix(javascript): qualify IPC pairing by wrapper coverage * fix(javascript): retain provisional IPC wrapper matches
This commit is contained in:
1 parent
f60e42a9ae
commit
54f5cf7e14
13 files changed
+1174
-40
No files matched your search
@@ -11,6 +11,10 @@ import { prefixedDigestSchema } from "../digests.js";
|
||||
|
||||
const boundedTextSchema = z.string().min(1);
|
||||
|
||||
/** An observed IPC channel match whose uniqueness is not established. */
|
||||
export const IPC_COVERAGE_LIMITED_PAIRING_BASIS =
|
||||
"observed-literal-channel-incomplete-coverage";
|
||||
|
||||
/** Node kinds admitted by JavaScript Application Graph. */
|
||||
export const JAVASCRIPT_APPLICATION_NODE_KINDS = [
|
||||
"package",
|
||||
|
||||
@@ -6,6 +6,7 @@ import type {
|
||||
ApplicationEdge,
|
||||
ApplicationNode,
|
||||
} from "./javascriptApplicationGraphSchemas.js";
|
||||
import { IPC_COVERAGE_LIMITED_PAIRING_BASIS } from "./javascriptApplicationGraphSchemas.js";
|
||||
import type { JavaScriptApplicationGraph } from "./javascriptApplicationGraph.js";
|
||||
import { javaScriptFileOccurrenceSchema } from "./javascriptArtifactFiles.js";
|
||||
import { applicationEndpointCorrespondence } from "./javascriptApplicationEndpointCorrespondence.js";
|
||||
@@ -54,6 +55,7 @@ interface IndexedRelationship {
|
||||
readonly direction: "in" | "out";
|
||||
readonly relation: ApplicationEdge["relation"];
|
||||
readonly neighborId: string;
|
||||
readonly coverageLimited: boolean;
|
||||
}
|
||||
|
||||
interface RelationshipIndex {
|
||||
@@ -180,6 +182,7 @@ const matchedItem = (
|
||||
? "changed"
|
||||
: complete &&
|
||||
!relationships.unresolved &&
|
||||
!relationships.coverageLimited &&
|
||||
!endpointTargetUncertain &&
|
||||
!sourceAnalysisUncertain
|
||||
? "unchanged"
|
||||
@@ -218,6 +221,11 @@ const matchedItem = (
|
||||
"Relationship target identity remains unresolved; equal node kinds do not establish cross-version alignment.",
|
||||
]
|
||||
: []),
|
||||
...(relationships.coverageLimited
|
||||
? [
|
||||
"Incomplete source coverage leaves some candidate relationships unconfirmed.",
|
||||
]
|
||||
: []),
|
||||
],
|
||||
};
|
||||
return itemWithId(semantic);
|
||||
@@ -608,16 +616,20 @@ const indexRelationships = (
|
||||
else existing.push(relationship);
|
||||
};
|
||||
for (const edge of graph.edges) {
|
||||
const coverageLimited =
|
||||
edge.properties.pairing_basis === IPC_COVERAGE_LIMITED_PAIRING_BASIS;
|
||||
add(edge.source_node_id, {
|
||||
direction: "out",
|
||||
relation: edge.relation,
|
||||
neighborId: edge.target_node_id,
|
||||
coverageLimited,
|
||||
});
|
||||
if (edge.target_node_id !== edge.source_node_id)
|
||||
add(edge.target_node_id, {
|
||||
direction: "in",
|
||||
relation: edge.relation,
|
||||
neighborId: edge.source_node_id,
|
||||
coverageLimited,
|
||||
});
|
||||
}
|
||||
return {
|
||||
@@ -630,6 +642,7 @@ const indexRelationships = (
|
||||
interface RelationshipComparison {
|
||||
readonly different: boolean;
|
||||
readonly unresolved: boolean;
|
||||
readonly coverageLimited: boolean;
|
||||
}
|
||||
|
||||
const compareRelationships = (
|
||||
@@ -640,41 +653,95 @@ const compareRelationships = (
|
||||
const right = relationshipFacts(pair.right, "right", context);
|
||||
return {
|
||||
different:
|
||||
left.paired !== right.paired ||
|
||||
left.unpairedShape !== right.unpairedShape,
|
||||
unexplainedRelationshipDifference(
|
||||
left.paired,
|
||||
right.paired,
|
||||
left.possiblePaired,
|
||||
right.possiblePaired,
|
||||
) ||
|
||||
unexplainedRelationshipDifference(
|
||||
left.unpairedShape,
|
||||
right.unpairedShape,
|
||||
left.possibleUnpairedShape,
|
||||
right.possibleUnpairedShape,
|
||||
),
|
||||
unresolved: left.unresolved || right.unresolved,
|
||||
coverageLimited: left.coverageLimited || right.coverageLimited,
|
||||
};
|
||||
};
|
||||
|
||||
/** A candidate can explain a missing counterpart but cannot prove a change. */
|
||||
const unexplainedRelationshipDifference = (
|
||||
left: readonly string[],
|
||||
right: readonly string[],
|
||||
leftPossible: readonly string[],
|
||||
rightPossible: readonly string[],
|
||||
): boolean => {
|
||||
const counts = (values: readonly string[]) => {
|
||||
const result = new Map<string, number>();
|
||||
for (const value of values) result.set(value, (result.get(value) ?? 0) + 1);
|
||||
return result;
|
||||
};
|
||||
const leftCounts = counts(left);
|
||||
const rightCounts = counts(right);
|
||||
const leftCandidateCounts = counts(leftPossible);
|
||||
const rightCandidateCounts = counts(rightPossible);
|
||||
for (const key of new Set([...leftCounts.keys(), ...rightCounts.keys()])) {
|
||||
const difference = (leftCounts.get(key) ?? 0) - (rightCounts.get(key) ?? 0);
|
||||
if (
|
||||
difference > (rightCandidateCounts.get(key) ?? 0) ||
|
||||
-difference > (leftCandidateCounts.get(key) ?? 0)
|
||||
)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const relationshipFacts = (
|
||||
node: ApplicationNode,
|
||||
side: "left" | "right",
|
||||
context: ItemContext,
|
||||
): {
|
||||
readonly paired: string;
|
||||
readonly unpairedShape: string;
|
||||
readonly paired: readonly string[];
|
||||
readonly unpairedShape: readonly string[];
|
||||
readonly possiblePaired: readonly string[];
|
||||
readonly possibleUnpairedShape: readonly string[];
|
||||
readonly unresolved: boolean;
|
||||
readonly coverageLimited: boolean;
|
||||
} => {
|
||||
const { nodeById, adjacency } =
|
||||
side === "left" ? context.leftRelationships : context.rightRelationships;
|
||||
const paired = side === "left" ? context.pairByLeft : context.pairByRight;
|
||||
const matched: string[] = [];
|
||||
const unpaired: string[] = [];
|
||||
for (const { direction, relation, neighborId } of adjacency.get(
|
||||
node.node_id,
|
||||
) ?? []) {
|
||||
const possibleMatched: string[] = [];
|
||||
const possibleUnpaired: string[] = [];
|
||||
let coverageLimited = false;
|
||||
for (const {
|
||||
direction,
|
||||
relation,
|
||||
neighborId,
|
||||
coverageLimited: candidate,
|
||||
} of adjacency.get(node.node_id) ?? []) {
|
||||
const mapped = paired.get(neighborId);
|
||||
if (mapped !== undefined)
|
||||
matched.push(`${direction}\0${relation}\0${mapped}`);
|
||||
else
|
||||
unpaired.push(
|
||||
if (candidate) coverageLimited = true;
|
||||
if (mapped !== undefined) {
|
||||
(candidate ? possibleMatched : matched).push(
|
||||
`${direction}\0${relation}\0${mapped}`,
|
||||
);
|
||||
} else {
|
||||
(candidate ? possibleUnpaired : unpaired).push(
|
||||
`${direction}\0${relation}\0${nodeById.get(neighborId)?.kind ?? "unknown"}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return {
|
||||
paired: canonicalJson(matched.sort(compareUnicodeCodePoints)),
|
||||
unpairedShape: canonicalJson(unpaired.sort(compareUnicodeCodePoints)),
|
||||
unresolved: unpaired.length > 0,
|
||||
paired: matched,
|
||||
unpairedShape: unpaired,
|
||||
possiblePaired: possibleMatched,
|
||||
possibleUnpairedShape: possibleUnpaired,
|
||||
unresolved: unpaired.length > 0 || possibleUnpaired.length > 0,
|
||||
coverageLimited,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
createJavaScriptApplicationGraph,
|
||||
type JavaScriptApplicationGraph,
|
||||
} from "./javascriptApplicationGraph.js";
|
||||
import { IPC_COVERAGE_LIMITED_PAIRING_BASIS } from "./javascriptApplicationGraphSchemas.js";
|
||||
import { compareJavaScriptApplicationVersions } from "./javascriptApplicationVersionComparison.js";
|
||||
import { applicationVersionComparisonResultSchema } from "./javascriptApplicationVersionComparisonSchemas.js";
|
||||
|
||||
@@ -20,6 +21,9 @@ const graph = (
|
||||
content?: string;
|
||||
permission?: string;
|
||||
relation?: "handles" | "invokes";
|
||||
candidate?: boolean;
|
||||
candidateExtra?: boolean;
|
||||
noEdge?: boolean;
|
||||
} = {},
|
||||
) => {
|
||||
const evidence = {
|
||||
@@ -72,15 +76,32 @@ const graph = (
|
||||
schema: "JavaScriptApplicationGraph",
|
||||
root_node_ids: [module.node_id],
|
||||
nodes: [module, channel],
|
||||
edges: [
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: module.node_id,
|
||||
target_node_id: channel.node_id,
|
||||
relation: options.relation ?? "handles",
|
||||
properties: {},
|
||||
evidence,
|
||||
}),
|
||||
],
|
||||
edges: options.noEdge
|
||||
? []
|
||||
: [
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: module.node_id,
|
||||
target_node_id: channel.node_id,
|
||||
relation: options.relation ?? "handles",
|
||||
properties: options.candidate
|
||||
? { pairing_basis: IPC_COVERAGE_LIMITED_PAIRING_BASIS }
|
||||
: {},
|
||||
evidence,
|
||||
}),
|
||||
...(options.candidateExtra
|
||||
? [
|
||||
createJavaScriptApplicationEdge({
|
||||
source_node_id: module.node_id,
|
||||
target_node_id: channel.node_id,
|
||||
relation: "handles" as const,
|
||||
properties: {
|
||||
pairing_basis: IPC_COVERAGE_LIMITED_PAIRING_BASIS,
|
||||
},
|
||||
evidence,
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
],
|
||||
coverage,
|
||||
limitations:
|
||||
coverage.status === "complete"
|
||||
@@ -114,6 +135,55 @@ const partialCoverage: JavaScriptApplicationGraph["coverage"] = {
|
||||
omitted_count: null,
|
||||
};
|
||||
|
||||
it("does not report a candidate-only IPC edge as a definite relationship change", () => {
|
||||
const result = compare(
|
||||
graph("a".repeat(64), 10, partialCoverage, { candidate: true }),
|
||||
graph("b".repeat(64), 50, partialCoverage, {
|
||||
noEdge: true,
|
||||
permission: "changed",
|
||||
}),
|
||||
);
|
||||
const channel = result.items.find((item) => item.node_kind === "ipc-channel");
|
||||
expect(channel?.status).toBe("changed");
|
||||
expect(channel?.dimensions).toContain("properties");
|
||||
expect(channel?.dimensions).not.toContain("relationships");
|
||||
expect(channel?.limitations.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"keeps a candidate and a definite equivalent uncertain when reversed: %s",
|
||||
(reverse) => {
|
||||
const candidate = graph("a".repeat(64), 10, partialCoverage, {
|
||||
candidate: true,
|
||||
});
|
||||
const definite = graph("b".repeat(64), 50, partialCoverage, {
|
||||
permission: "changed",
|
||||
});
|
||||
const result = reverse
|
||||
? compare(definite, candidate)
|
||||
: compare(candidate, definite);
|
||||
const channel = result.items.find(
|
||||
(item) => item.node_kind === "ipc-channel",
|
||||
);
|
||||
expect(channel?.status).toBe("changed");
|
||||
expect(channel?.dimensions).not.toContain("relationships");
|
||||
},
|
||||
);
|
||||
|
||||
it("retains an independent definite relationship change beside candidate edges", () => {
|
||||
const result = compare(
|
||||
graph("a".repeat(64), 10, partialCoverage, { candidateExtra: true }),
|
||||
graph("b".repeat(64), 50, partialCoverage, {
|
||||
relation: "invokes",
|
||||
candidateExtra: true,
|
||||
permission: "changed",
|
||||
}),
|
||||
);
|
||||
const channel = result.items.find((item) => item.node_kind === "ipc-channel");
|
||||
expect(channel?.status).toBe("changed");
|
||||
expect(channel?.dimensions).toContain("relationships");
|
||||
});
|
||||
|
||||
it("keeps relocated exact modules and inferred channels unchanged while retaining both coordinates", () => {
|
||||
const left = graph("a".repeat(64), 10, completeCoverage),
|
||||
right = graph("b".repeat(64), 50, completeCoverage);
|
||||
|
||||
@@ -9,12 +9,20 @@ import type { JavaScriptArtifactFile } from "./javascriptArtifactFiles.js";
|
||||
import type { JavaScriptStaticAnalysis } from "./javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptModuleSemanticIr } from "./javascriptModuleSemanticIr.js";
|
||||
|
||||
/** An earlier static projection displaced to retain later static observations. */
|
||||
export interface JavaScriptRevokedStaticProjection {
|
||||
/** Zero-based source-analysis call index within this worker checkout. */
|
||||
readonly sourceIndex: number;
|
||||
readonly error: AnalysisResourceConstraintError;
|
||||
}
|
||||
|
||||
/** Portable facts collected from one inert source. */
|
||||
export interface JavaScriptAnalyzedSource {
|
||||
readonly javascript: JavaScriptStaticAnalysis;
|
||||
readonly module: JavaScriptModuleSemanticIr | null;
|
||||
readonly projection: JavaScriptSemanticFileProjection | null;
|
||||
readonly applicationProjectionFailure?: AnalysisResourceConstraintError;
|
||||
readonly revokedStaticProjections?: readonly JavaScriptRevokedStaticProjection[];
|
||||
}
|
||||
|
||||
/** Execution failure alongside facts already collected by the consumer. */
|
||||
@@ -24,6 +32,7 @@ export interface JavaScriptSourceAnalysisFailure {
|
||||
readonly module: JavaScriptModuleSemanticIr | null;
|
||||
readonly projection: JavaScriptSemanticFileProjection | null;
|
||||
readonly applicationProjectionFailure?: AnalysisResourceConstraintError;
|
||||
readonly revokedStaticProjections?: readonly JavaScriptRevokedStaticProjection[];
|
||||
}
|
||||
|
||||
/** Operational choices for source analysis, independent of transport framing. */
|
||||
|
||||
Reference in new issue
Block a user