fix(javascript): keep exact empty values in graph identities

#839 displayed empty artifact-local keys and source-map original names as
`""` inside node identities, so `fetch("")` and `fetch('""')` (or
`sources: ["", "\"\""]`) produced the same node ID and merged two
distinct facts. Function nodes likewise exposed only a display label, so
`['']()` and `['""']()` were indistinguishable.

Admit the exact empty string in `artifact-local-key.key` and
`source-map-original.original_source`, keep the `""` substitution for
labels only, and record each function node's exact `name` in its
properties. Regenerate the catalog digests for the relaxed output schema.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Iwakurakaoru0822 2026-10-06 21:00:57 -07:00
parent e7cbfe04fd
commit 63012eeb60
9 changed files with 58 additions and 41 deletions

View File

@ -637,9 +637,9 @@
"mcp_prompts": 6
},
"digests": {
"tools_sha256": "569db1182f19869a3bdb22cf217bc75526f317b59635c8866d0e62741077cd52",
"tools_sha256": "860472c8261d77c235f72eea3d1380a024df68efc30e095ab2a8a4856c050ec1",
"prompts_sha256": "ef2de8814390b340623c32001610aa2709d90bed51537b6e562ab3abf144ab4d",
"combined_sha256": "e21455dd7d64af94e0b07aff55ab82f03a37e46729ece51be38b50083d2d5b55",
"combined_sha256": "e9e061962e5d50b1793890284c2431206950da3947211f6986ba05a60b7cd9dc",
"providers_sha256": "7949ddcf2eb5ba41b022bb3dfa1951ab96dbf5640285fa7fd5d35b0e4a5bdab3"
}
}

View File

@ -1,5 +1,5 @@
{
"manifest_id": "ecm_d68a1575bdaa1a044ccce8e27c5dd335612f909bc81c52ce3089b576fd772940",
"manifest_id": "ecm_0e087361a14f9b7a3d8e01dcb61d68cccb438e07b09c237568b678608856de98",
"verifier": {
"id": "rea-managed-conformance",
"version": "1"
@ -13,7 +13,7 @@
"skill_digests": [
{
"skill_id": "reverse-engineer-anything",
"sha256": "d2d971cc67bb0f7b80b4e39382a6bd1146445ed1dbf52043f89e2da2985c97ba"
"sha256": "d93aff007a8a5a8ade101f84d891ce969756fca753679d94b75ec8451764e8cd"
}
],
"claims": [

View File

@ -4,7 +4,7 @@ description: Reverse engineer native, managed, Electron/JavaScript, packaged, fi
metadata:
version: "26"
tool_count: 126
catalog_digest: "e21455dd7d64af94e0b07aff55ab82f03a37e46729ece51be38b50083d2d5b55"
catalog_digest: "e9e061962e5d50b1793890284c2431206950da3947211f6986ba05a60b7cd9dc"
---
# REA

View File

@ -52,7 +52,7 @@ it.each([
it("displays empty literals as nonempty text while retaining exact values", async () => {
const output = await analyzeSource(
"fetch(''); fetch('/api'); process.on('', () => 1);",
"fetch(''); fetch('\"\"'); fetch('/api'); process.on('', () => 1);",
);
const endpoints = output.graph.nodes
.filter(({ kind }) => kind === "endpoint")
@ -61,12 +61,16 @@ it("displays empty literals as nonempty text while retaining exact values", asyn
labels: observations.map(({ label }) => label),
values: observations.map(({ properties }) => properties.value),
}));
// The empty endpoint and a literal two-quote endpoint share display text
// but remain distinct nodes with exact identities.
expect(endpoints).toEqual(
expect.arrayContaining([
{ key: '""', labels: ['""'], values: [""] },
{ key: "", labels: ['""'], values: [""] },
{ key: '""', labels: ['""'], values: ['""'] },
{ key: "/api", labels: ["/api"], values: ["/api"] },
]),
);
expect(endpoints).toHaveLength(3);
expect(
output.semantic_graph?.nodes
.filter(({ kind }) => kind === "event")
@ -101,5 +105,26 @@ it.each([
: null,
sources: observations.map(({ properties }) => properties.source),
}));
expect(originals).toEqual([{ originalSource: '""', sources: [""] }]);
expect(originals).toEqual([{ originalSource: "", sources: [""] }]);
});
it("keeps empty and two-quote source-map names distinct", async () => {
const output = await analyzeSource("//# sourceMappingURL=app.js.map", {
"app.js.map": JSON.stringify({
version: 3,
sources: ["", '""'],
names: [],
mappings: "AAAA,CCAA",
}),
});
expect(
output.graph.nodes
.filter(({ kind }) => kind === "source-module")
.map(({ identity }) =>
identity.strategy === "source-map-original"
? identity.original_source
: null,
)
.sort(),
).toEqual(["", '""']);
});

View File

@ -68,25 +68,19 @@ const observationInput = (
/** Node fields whose display form is normalized before validation. */
interface DisplayableNodeInput {
readonly identity: {
readonly strategy: string;
readonly key?: string;
readonly original_source?: string;
};
readonly observations: readonly { readonly label: string | null }[];
}
/**
* Static findings can carry legal empty strings, such as `fetch("")`,
* `require("")`, or a source map's `sources: [""]`. Labels, artifact-local
* keys, and original source names must be nonempty, so display the empty value
* as `""`; each observation keeps the exact value in its properties.
* `require("")`, or a source map's `sources: [""]`. Labels are nonempty display
* text, so the empty value is shown as `""`. Identities and observation
* properties keep the exact value, so distinct values never merge.
*/
const displayableNode = (
input: DisplayableNodeInput,
): DisplayableNodeInput => ({
...input,
identity: displayableIdentity(input.identity),
observations: input.observations.map((observation) => ({
...observation,
label:
@ -95,19 +89,3 @@ const displayableNode = (
: javascriptDisplayText(observation.label),
})),
});
const displayableIdentity = (
identity: DisplayableNodeInput["identity"],
): DisplayableNodeInput["identity"] => {
if (identity.strategy === "artifact-local-key" && identity.key !== undefined)
return { ...identity, key: javascriptDisplayText(identity.key) };
if (
identity.strategy === "source-map-original" &&
identity.original_source !== undefined
)
return {
...identity,
original_source: javascriptDisplayText(identity.original_source),
};
return identity;
};

View File

@ -370,14 +370,24 @@ it("keeps exact empty environment keys distinct from dynamic keys", () => {
it("labels exact empty method names without inventing computed names", () => {
const graph = graphFor(`
const target = { ['']() { return 42; }, [key]() { return 1; } };
const target = {
['']() { return 42; },
['""']() { return 2; },
[key]() { return 1; },
};
`);
const labels = graph.nodes
const functions = graph.nodes
.filter(({ kind }) => kind === "function")
.map(({ label }) => label);
expect(labels).toContain('""');
.map(({ label, properties }) => ({ label, name: properties.name }));
// Both exact names display as `""`; only `properties.name` tells them apart.
expect(functions).toEqual(
expect.arrayContaining([
{ label: '""', name: "" },
{ label: '""', name: '""' },
]),
);
expect(
labels.filter((label) => label?.startsWith("[computed@")),
functions.filter(({ label }) => label?.startsWith("[computed@")),
).toHaveLength(1);
});

View File

@ -167,6 +167,8 @@ const projectFile = (
location: callable.location,
label: callable.name,
functionNodeId: null,
// The label is display text; keep the exact name, which may be "".
properties: { name: callable.name },
},
state,
),

View File

@ -419,7 +419,8 @@ export const applicationNodeIdentitySchema = z.discriminatedUnion("strategy", [
strategy: z.literal("source-map-original"),
stability: z.literal("source-map-exact"),
source_map_sha256: digestSchema,
original_source: boundedTextSchema,
// The exact `sources` entry; `""` is a legal relative reference.
original_source: z.string(),
source_sha256: digestSchema.nullable(),
}),
z.strictObject({
@ -451,7 +452,8 @@ export const applicationNodeIdentitySchema = z.discriminatedUnion("strategy", [
stability: z.literal("artifact-version"),
artifact_sha256: digestSchema,
namespace: boundedKeySchema,
key: boundedTextSchema,
// The exact artifact-local value, such as `""` from `fetch("")`.
key: z.string(),
}),
z.strictObject({
strategy: z.literal("runtime-instance"),

File diff suppressed because one or more lines are too long