Files
rea/src/domain/javascript/javascriptNativeHandoff.ts
T
Ysopwnable c7f6b8bad1 fix(javascript): bind native handoffs to canonical byte identity
Require the native node owner's content digest instead of borrowing an
observation artifact digest, which can identify the containing ASAR. Keep
unavailable nodes and their path/provenance/partial facts in the trace,
without a false actionable target or native Evidence link. Remove the old
fallback and allocate the query index only for eligible native targets.

Verify real public CLI/stdio MCP workflows, known/missing companion inputs
and shipped Joplin controls. Preserve reproducible scripts, source pins and
compact correctness/resource/token records without archiving applications.

Fixes #3926. Part of #4027.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 17:08:49 +00:00

136 lines
4.6 KiB
TypeScript

import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import type { Evidence } from "../evidence.js";
import type {
ApplicationEdge,
ApplicationNode,
} from "./javascriptApplicationGraphSchemas.js";
import type { ApplicationFeatureTraceResult } from "./javascriptFeatureTraceSchemas.js";
type NativeHandoff = ApplicationFeatureTraceResult["native_handoffs"][number];
/** Build provider-neutral addon targets and link exact-subject native Evidence. */
export const buildJavaScriptNativeHandoffs = (
nodes: readonly ApplicationNode[],
edges: readonly ApplicationEdge[],
nativeEvidence: readonly Evidence[],
): NativeHandoff[] => {
let nodeById: Map<string, ApplicationNode> | undefined;
return nodes
.filter(({ kind }) => kind === "native-addon")
.flatMap((node) => {
// Observations may describe only a containing archive. A native target
// requires the node owner's exact byte identity, independently of its
// path, availability labels or the artifact supporting an observation.
if (node.identity.strategy !== "content-digest") return [];
const digest = node.identity.sha256;
const linked = nativeEvidence.filter(
({ subject }) => subject?.digest.sha256 === digest,
);
nodeById ??= new Map(
nodes.map((candidate) => [candidate.node_id, candidate]),
);
const exportNodes = nativeExportNodes(node, edges, nodeById);
return [
{
native_node_id: node.node_id,
artifact_sha256: digest,
artifact_path: artifactPath(node),
export_node_ids: exportNodes.map(({ node_id: id }) => id),
requested_exports: requestedExports(exportNodes),
status:
linked.length === 0
? ("requires-provider-analysis" as const)
: ("evidence-linked" as const),
providers: uniqueProviders(linked),
evidence_ids: linked
.map(({ evidence_id: id }) => id)
.sort(compareUnicodeCodePoints),
recommended_tools: [
"open_binary" as const,
"binary_overview" as const,
"search_procedures" as const,
"analyze_function" as const,
"xrefs" as const,
],
},
];
})
.sort((left, right) =>
compareUnicodeCodePoints(left.native_node_id, right.native_node_id),
);
};
const artifactPath = (node: ApplicationNode): string | null => {
for (const { evidence } of node.observations)
if (
evidence.location.available &&
evidence.location.value.kind === "artifact-path"
)
return evidence.location.value.path;
return null;
};
const nativeExportNodes = (
addon: ApplicationNode,
edges: readonly ApplicationEdge[],
nodeById: ReadonlyMap<string, ApplicationNode>,
): ApplicationNode[] => {
const ids = edges.flatMap((edge) => {
if (
edge.source_node_id === addon.node_id &&
["contains", "exposes"].includes(edge.relation)
)
return [edge.target_node_id];
if (
edge.target_node_id === addon.node_id &&
["imports", "loads"].includes(edge.relation)
)
return [edge.source_node_id];
return [];
});
return [...new Set(ids)]
.map((id) => nodeById.get(id))
.filter((node): node is ApplicationNode => node?.kind === "native-export")
.sort((left, right) =>
compareUnicodeCodePoints(left.node_id, right.node_id),
);
};
const requestedExports = (nodes: readonly ApplicationNode[]): string[] => {
const values = nodes.flatMap((node) =>
node.observations.flatMap((observation) => [
// Display labels can summarize several names or namespace access.
// Only legacy observations without structured member facts need them.
...(observation.label === null ||
Array.isArray(observation.properties.requested_members)
? []
: [observation.label]),
...stringArray(observation.properties.requested_members),
...stringArray(observation.properties.members),
]),
);
return [...new Set(values)].sort(compareUnicodeCodePoints);
};
const stringArray = (value: unknown): string[] =>
Array.isArray(value)
? value.filter((item): item is string => typeof item === "string")
: [];
const uniqueProviders = (
evidence: readonly Evidence[],
): NativeHandoff["providers"] =>
[
...new Map(
evidence.map(({ provider }) => [
`${provider.id}\0${provider.name}\0${provider.version ?? ""}`,
provider,
]),
).values(),
].sort((left, right) =>
compareUnicodeCodePoints(
`${left.id}\0${left.name}\0${left.version ?? ""}`,
`${right.id}\0${right.name}\0${right.version ?? ""}`,
),
);