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:
morluto authored and GitHub committed 2026-10-29 14:02:35 +08:00
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. */