mirror of
https://github.com/morluto/rea
synced 2026-10-27 06:33:32 -04:00
refactor(javascript): group domain semantics and contracts (#907)
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
import { compareCodePoints } from "../canonicalOrdering.js";
|
||||
import type { ApplicationNode } from "./javascriptApplicationGraph.js";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
|
||||
/** One deterministic one-to-one match between two graph versions. */
|
||||
type ApplicationVersionPairMatch =
|
||||
| {
|
||||
readonly basis:
|
||||
| "exact-node-identity"
|
||||
| "exact-content-digest"
|
||||
| "exact-module-source-digest";
|
||||
readonly confidence: "exact";
|
||||
}
|
||||
| {
|
||||
readonly basis: "source-map-identity";
|
||||
readonly confidence: "high";
|
||||
}
|
||||
| {
|
||||
readonly basis: "structural-fingerprint" | "semantic-key";
|
||||
readonly confidence: "medium";
|
||||
};
|
||||
|
||||
export type ApplicationVersionNodePair = {
|
||||
readonly left: ApplicationNode;
|
||||
readonly right: ApplicationNode;
|
||||
} & ApplicationVersionPairMatch;
|
||||
|
||||
/** Matched nodes plus ambiguity candidates that were never forced into pairs. */
|
||||
export interface ApplicationVersionMatchingProjection {
|
||||
readonly pairs: ApplicationVersionNodePair[];
|
||||
readonly unmatchedLeft: ApplicationNode[];
|
||||
readonly unmatchedRight: ApplicationNode[];
|
||||
readonly candidatesForLeft: ReadonlyMap<string, readonly string[]>;
|
||||
readonly candidatesForRight: ReadonlyMap<string, readonly string[]>;
|
||||
}
|
||||
|
||||
interface MatchTier {
|
||||
readonly match: ApplicationVersionPairMatch;
|
||||
readonly key: (node: ApplicationNode) => string | null;
|
||||
}
|
||||
|
||||
const matchTiers = (): readonly MatchTier[] => [
|
||||
{
|
||||
match: { basis: "exact-content-digest", confidence: "exact" },
|
||||
key: contentDigestKey,
|
||||
},
|
||||
{
|
||||
match: { basis: "exact-module-source-digest", confidence: "exact" },
|
||||
key: moduleSourceDigestKey,
|
||||
},
|
||||
{
|
||||
match: { basis: "exact-node-identity", confidence: "exact" },
|
||||
key: nodeIdentityKey,
|
||||
},
|
||||
{
|
||||
match: { basis: "source-map-identity", confidence: "high" },
|
||||
key: sourceMapKey,
|
||||
},
|
||||
{
|
||||
match: { basis: "structural-fingerprint", confidence: "medium" },
|
||||
key: structuralFingerprintKey,
|
||||
},
|
||||
{
|
||||
match: { basis: "semantic-key", confidence: "medium" },
|
||||
key: semanticKey,
|
||||
},
|
||||
];
|
||||
|
||||
/** Apply ordered, unique-only matching tiers without fuzzy pairing. */
|
||||
export const matchJavaScriptApplicationVersions = (
|
||||
leftNodes: readonly ApplicationNode[],
|
||||
rightNodes: readonly ApplicationNode[],
|
||||
): ApplicationVersionMatchingProjection => {
|
||||
const left = new Map(leftNodes.map((node) => [node.node_id, node]));
|
||||
const right = new Map(rightNodes.map((node) => [node.node_id, node]));
|
||||
const leftCandidates = new Map<string, Set<string>>();
|
||||
const rightCandidates = new Map<string, Set<string>>();
|
||||
const pairs: ApplicationVersionNodePair[] = [];
|
||||
for (const tier of matchTiers())
|
||||
applyTier({ left, right, pairs, leftCandidates, rightCandidates, tier });
|
||||
const remainingLeft = sortedNodes(left.values());
|
||||
const remainingRight = sortedNodes(right.values());
|
||||
const leftIds = new Set(remainingLeft.map(({ node_id: id }) => id));
|
||||
const rightIds = new Set(remainingRight.map(({ node_id: id }) => id));
|
||||
return {
|
||||
pairs: pairs.sort((a, b) =>
|
||||
compareCodePoints(
|
||||
`${a.left.node_id}\0${a.right.node_id}`,
|
||||
`${b.left.node_id}\0${b.right.node_id}`,
|
||||
),
|
||||
),
|
||||
unmatchedLeft: remainingLeft,
|
||||
unmatchedRight: remainingRight,
|
||||
candidatesForLeft: filteredCandidates(leftCandidates, rightIds),
|
||||
candidatesForRight: filteredCandidates(rightCandidates, leftIds),
|
||||
};
|
||||
};
|
||||
|
||||
interface TierContext {
|
||||
readonly left: Map<string, ApplicationNode>;
|
||||
readonly right: Map<string, ApplicationNode>;
|
||||
readonly pairs: ApplicationVersionNodePair[];
|
||||
readonly leftCandidates: Map<string, Set<string>>;
|
||||
readonly rightCandidates: Map<string, Set<string>>;
|
||||
readonly tier: MatchTier;
|
||||
}
|
||||
|
||||
const applyTier = (context: TierContext): void => {
|
||||
const leftByKey = nodesByKey(context.left.values(), context.tier.key);
|
||||
const rightByKey = nodesByKey(context.right.values(), context.tier.key);
|
||||
for (const key of [...leftByKey.keys()].sort(compareCodePoints)) {
|
||||
const leftGroup = leftByKey.get(key) ?? [];
|
||||
const rightGroup = rightByKey.get(key) ?? [];
|
||||
if (rightGroup.length === 0) continue;
|
||||
recordCandidates(
|
||||
leftGroup,
|
||||
rightGroup,
|
||||
context.leftCandidates,
|
||||
context.rightCandidates,
|
||||
);
|
||||
if (leftGroup.length !== 1 || rightGroup.length !== 1) continue;
|
||||
const leftNode = leftGroup[0];
|
||||
const rightNode = rightGroup[0];
|
||||
if (leftNode === undefined || rightNode === undefined) continue;
|
||||
context.pairs.push({
|
||||
left: leftNode,
|
||||
right: rightNode,
|
||||
...context.tier.match,
|
||||
});
|
||||
context.left.delete(leftNode.node_id);
|
||||
context.right.delete(rightNode.node_id);
|
||||
}
|
||||
};
|
||||
|
||||
const nodesByKey = (
|
||||
nodes: Iterable<ApplicationNode>,
|
||||
keyFor: (node: ApplicationNode) => string | null,
|
||||
): Map<string, ApplicationNode[]> => {
|
||||
const output = new Map<string, ApplicationNode[]>();
|
||||
for (const node of nodes) {
|
||||
const key = keyFor(node);
|
||||
if (key === null) continue;
|
||||
output.set(key, [...(output.get(key) ?? []), node]);
|
||||
}
|
||||
return output;
|
||||
};
|
||||
|
||||
const recordCandidates = (
|
||||
left: readonly ApplicationNode[],
|
||||
right: readonly ApplicationNode[],
|
||||
leftCandidates: Map<string, Set<string>>,
|
||||
rightCandidates: Map<string, Set<string>>,
|
||||
): void => {
|
||||
for (const leftNode of left) {
|
||||
const candidates =
|
||||
leftCandidates.get(leftNode.node_id) ?? new Set<string>();
|
||||
for (const rightNode of right) candidates.add(rightNode.node_id);
|
||||
leftCandidates.set(leftNode.node_id, candidates);
|
||||
}
|
||||
for (const rightNode of right) {
|
||||
const candidates =
|
||||
rightCandidates.get(rightNode.node_id) ?? new Set<string>();
|
||||
for (const leftNode of left) candidates.add(leftNode.node_id);
|
||||
rightCandidates.set(rightNode.node_id, candidates);
|
||||
}
|
||||
};
|
||||
|
||||
const filteredCandidates = (
|
||||
candidates: ReadonlyMap<string, ReadonlySet<string>>,
|
||||
retained: ReadonlySet<string>,
|
||||
): ReadonlyMap<string, readonly string[]> =>
|
||||
new Map(
|
||||
[...candidates.entries()].flatMap(([nodeId, values]) => {
|
||||
const filtered = [...values]
|
||||
.filter((value) => retained.has(value))
|
||||
.sort(compareCodePoints);
|
||||
return filtered.length === 0 ? [] : [[nodeId, filtered] as const];
|
||||
}),
|
||||
);
|
||||
|
||||
const nodeIdentityKey = (node: ApplicationNode): string =>
|
||||
`node\0${node.kind}\0${node.node_id}`;
|
||||
|
||||
const contentDigestKey = (node: ApplicationNode): string | null =>
|
||||
node.identity.strategy === "content-digest"
|
||||
? `content\0${node.kind}\0${node.identity.sha256}`
|
||||
: null;
|
||||
|
||||
const moduleSourceDigestKey = (node: ApplicationNode): string | null => {
|
||||
if (node.kind !== "javascript-module") return null;
|
||||
const value = uniqueStringProperty(node, "source_sha256");
|
||||
return value === null ? null : `module-source\0${node.kind}\0${value}`;
|
||||
};
|
||||
|
||||
const sourceMapKey = (node: ApplicationNode): string | null =>
|
||||
node.kind === "source-module" &&
|
||||
node.identity.strategy === "source-map-original"
|
||||
? `source-map\0${node.identity.original_source}`
|
||||
: null;
|
||||
|
||||
const structuralFingerprintKey = (node: ApplicationNode): string | null => {
|
||||
if (node.kind !== "javascript-module") return null;
|
||||
const digest = uniqueStringProperty(node, "structural_fingerprint_sha256");
|
||||
const algorithm = uniqueStringProperty(
|
||||
node,
|
||||
"structural_fingerprint_algorithm",
|
||||
);
|
||||
return digest === null || algorithm === null
|
||||
? null
|
||||
: `structural\0${node.kind}\0${algorithm}\0${digest}`;
|
||||
};
|
||||
|
||||
const semanticKey = (node: ApplicationNode): string | null => {
|
||||
if (
|
||||
[
|
||||
"javascript-module",
|
||||
"javascript-chunk",
|
||||
"runtime-script-instance",
|
||||
"unknown",
|
||||
].includes(node.kind)
|
||||
)
|
||||
return null;
|
||||
const properties = firstProperties(node);
|
||||
const value = semanticValue(node, properties);
|
||||
return value === null ? null : `semantic\0${node.kind}\0${value}`;
|
||||
};
|
||||
|
||||
const semanticValue = (
|
||||
node: ApplicationNode,
|
||||
properties: Readonly<Record<string, JsonValue>>,
|
||||
): string | null => {
|
||||
if (node.kind === "endpoint") {
|
||||
const kind = nonEmptyString(properties.endpoint_kind);
|
||||
const value = exactString(properties.value);
|
||||
return kind === null || value === null ? null : `${kind}\0${value}`;
|
||||
}
|
||||
if (node.kind === "storage")
|
||||
return joined(
|
||||
properties.storage_kind,
|
||||
properties.name ?? properties.mechanism,
|
||||
);
|
||||
if (node.kind === "ipc-channel")
|
||||
return properties.resolution === "dynamic"
|
||||
? null
|
||||
: (exactString(properties.channel) ?? firstLabel(node));
|
||||
if (node.kind === "context-bridge-api")
|
||||
return (
|
||||
firstLabel(node) ??
|
||||
exactString(properties.api_key) ??
|
||||
nonEmptyString(properties.api_name)
|
||||
);
|
||||
if (node.kind === "native-export")
|
||||
return (
|
||||
joined(
|
||||
properties.specifier,
|
||||
stringList(properties.requested_members).join(","),
|
||||
) ?? firstLabel(node)
|
||||
);
|
||||
if (
|
||||
[
|
||||
"electron-main",
|
||||
"electron-preload",
|
||||
"electron-renderer",
|
||||
"electron-utility",
|
||||
].includes(node.kind)
|
||||
)
|
||||
return joined(properties.declared_path, properties.resolved_path);
|
||||
if (node.kind === "package") return nonEmptyString(properties.name);
|
||||
return canonicalPath(node) ?? nonEmptyString(properties.path);
|
||||
};
|
||||
|
||||
const canonicalPath = (node: ApplicationNode): string | null =>
|
||||
node.identity.strategy === "canonical-path" ? node.identity.path : null;
|
||||
|
||||
const firstProperties = (node: ApplicationNode) =>
|
||||
node.observations[0]?.properties ?? {};
|
||||
|
||||
const firstLabel = (node: ApplicationNode): string | null =>
|
||||
node.observations.find(({ label }) => label !== null)?.label ?? null;
|
||||
|
||||
const uniqueStringProperty = (
|
||||
node: ApplicationNode,
|
||||
name: string,
|
||||
): string | null => {
|
||||
const values = [
|
||||
...new Set(
|
||||
node.observations
|
||||
.map(({ properties }) => properties[name])
|
||||
.filter((value): value is string => typeof value === "string"),
|
||||
),
|
||||
];
|
||||
return values.length === 1 ? (values[0] ?? null) : null;
|
||||
};
|
||||
|
||||
const joined = (...values: readonly unknown[]): string | null => {
|
||||
const strings = values.map(nonEmptyString);
|
||||
return strings.some((value) => value === null) ? null : strings.join("\0");
|
||||
};
|
||||
|
||||
const nonEmptyString = (value: unknown): string | null =>
|
||||
typeof value === "string" && value !== "" ? value : null;
|
||||
|
||||
/** A literal value such as `ipcMain.handle("")` keeps its exact empty text. */
|
||||
const exactString = (value: unknown): string | null =>
|
||||
typeof value === "string" ? value : null;
|
||||
|
||||
const stringList = (value: unknown): string[] =>
|
||||
Array.isArray(value)
|
||||
? value
|
||||
.filter((item): item is string => typeof item === "string")
|
||||
.sort(compareCodePoints)
|
||||
: [];
|
||||
|
||||
const sortedNodes = (nodes: Iterable<ApplicationNode>): ApplicationNode[] =>
|
||||
[...nodes].sort((left, right) =>
|
||||
compareCodePoints(left.node_id, right.node_id),
|
||||
);
|
||||
Reference in New Issue
Block a user