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:
parent
e7cbfe04fd
commit
63012eeb60
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(["", '""']);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Reference in New Issue