[PR #1458] [MERGED] perf(javascript): derive the application graph ID once #2900

Closed
opened 2026-10-17 21:11:36 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/1458
Author: @keemsisi
Created: 10/10/2026
Status: ✅ Merged
Merged: 10/10/2026
Merged by: @N0zoM1z0

Base: main ← Head: perf/graph-id-once


📝 Commits (1)

  • d8d1f19 perf(javascript): derive the application graph ID once

📊 Changes

2 files changed (+31 additions, -6 deletions)

View changed files

📝 src/domain/javascript/javascriptApplicationGraph.test.ts (+15 -0)
📝 src/domain/javascript/javascriptApplicationGraph.ts (+16 -6)

📄 Description

Summary

createJavaScriptApplicationGraph hashed the whole graph twice. It computed graph_id as the canonical SHA-256 of the graph content, then parsed the result with the public javascriptApplicationGraphSchema, whose invariant check hashed the same content again to verify the ID it had just been given. This change validates the content with every other invariant first, then derives graph_id once from the validated copy. Created graphs are byte-identical. The public schema still verifies graph_id for stored and untrusted graphs.

Measured effect:

  • compareJavaScriptApplicationVersions, which builds a change graph, is 1.07–1.23× faster in 35 of 36 measured configurations and 1.01× in the remaining one, with no slowdown.
  • Real analyze-javascript-application runs on three npm packages show no measurable end-to-end change: graph-ID hashing is a small share of a 40–50 s analysis. Their Evidence IDs are identical.

Related performance roadmap: #1065. No dedicated issue is linked; this is a focused performance fix, and the measurements below describe the problem and the result.

Problem and expected behavior

Trigger: any call to createJavaScriptApplicationGraph. This includes application analysis, feature traces, runtime reconciliation, and the change graph built by every version comparison.

In src/domain/javascript/javascriptApplicationGraph.ts:

return javascriptApplicationGraphSchema.parse({
  ...semantic,
  graph_id: `jag_${digestCanonicalValue(semantic, "JavaScript Application Graph")}`,
});

javascriptApplicationGraphSchema is javascriptApplicationGraphRecordSchema.superRefine(checkGraphInvariants), and checkGraphInvariants ends with:

const { graph_id: identifier, ...semantic } = graph;
if (identifier !== `jag_${digestCanonicalValue(semantic, "JavaScript Application Graph")}`) ...

So every graph created by the factory was canonically serialized and hashed twice. On a CPU profile of a 4,000-module version comparison (current main plus #1434), these two whole-graph digests were about 13% and 15% of the comparison's time.

Expected: the factory hashes the validated content once. Every other invariant still runs. Stored and untrusted graphs are still fully verified, including graph_id.

Change and scope

  • checkGraphInvariants is split into checkGraphContent, which holds every previous check except the graph-ID comparison (same messages and paths), and the graph-ID comparison. checkGraphInvariants runs both, so the public javascriptApplicationGraphSchema behaves exactly as before.
  • A private javascriptApplicationGraphContentSchema (javascriptApplicationGraphInputSchema.superRefine(checkGraphContent)) validates the normalized content in the factory. The factory then returns { ...content, graph_id }, with graph_id digested from that validated copy. The key order matches the record schema (graph_id last), so the serialized graph is unchanged.
  • The input and record schemas share the same node, edge, coverage and properties schemas, and re-parsing already parsed content is idempotent. So the digest of the validated copy equals the digest the old code verified, for every input the old code accepted.

Not included:

  • No change to node, edge or observation ID derivation or verification. Edge and observation IDs are still recomputed and checked during the content parse.
  • No change to parseJavaScriptApplicationGraph, computeJavaScriptApplicationGraphSha256, serialization, the owned immutable graph steps, or any result contract.
  • The new content schema is not exported. The public and owned validation split from #1370 stays explicit.

Contract and boundary impact

  • Semantic owner and earliest changed stage: domain, src/domain/javascript/javascriptApplicationGraph.ts.
  • CLI and MCP/tool-catalog contract: none; inputs and results are unchanged.
  • Provider, bridge, target-format, or platform compatibility: none.
  • Evidence, artifact, provenance, or reconstruction contract: none. Created graphs, graph IDs and Evidence IDs are byte-identical, and stored graphs are verified exactly as before.
  • Process execution, authorization, cleanup, or containment impact: none.
  • Generated metadata (docs/public/product-catalog.json), package, or installation impact: none.

Evidence and regression coverage

  • Test added in src/domain/javascript/javascriptApplicationGraph.test.ts, "creates exactly the graph the public schema accepts":

    • the factory's output serializes to exactly the same JSON, including key order, as javascriptApplicationGraphSchema.parse of a clone of it;
    • the factory still rejects content with a stale edge ID (Edge identifier).

    It passes on main and on this branch. Existing tests continue to cover the factory's rejection of duplicate nodes, missing roots, dangling and self edges, and the public parser's rejection of stale graph, node, observation and edge IDs.

  • Mutants applied to this change, run against the graph test, src/domain/javascript/javascriptApplicationVersionComparison.test.ts and tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts:

    Mutant Result
    skip the content parse (digest the unvalidated input) 7 of 32 tests fail
    put graph_id first in the returned object 1 of 32 tests fails
    digest the content without limitations 13 of 32 tests fail
  • Public workflow parity: the compiled CLI ran node scripts/rea.mjs analyze-javascript-application <dir> --json on copies of three real npm packages, on main (33b9b31a6) and on this branch. The run order was interleaved: base, fix, fix, base.

    Package Nodes / edges Evidence ID (main and this PR) graph_id Wall time main / this PR
    zod 7,973 / 11,575 ev_76643e66…c0642b275 jag_19d03a92fce57c7e… 40.5–43.2 s / 39.6–41.0 s
    knip 5,336 / 11,352 ev_bc37338b…ecdb6719 jag_360b76780c58b114… 37.5–39.3 s / 38.1–39.5 s
    playwright-core 1,331 / 4,214 ev_2de650ca…ad3ba jag_e534b08c23aac69e… 46.5–49.6 s / 46.3–51.1 s (6 runs each)

    Evidence IDs and graph IDs are identical. Wall-time differences are within run-to-run noise; analysis time is dominated by parsing and extraction, not by graph-ID hashing.

  • Performance: end-to-end compareJavaScriptApplicationVersions on synthetic version pairs with N matched modules, using the built dist of main (c29931ce8) and of this branch.

    • Runs are interleaved base/fix/fix/base.
    • Each run is 1 warm-up plus 5 repetitions (3 at 16,000).
    • Values are best / median in ms.
    • The full result JSON sha256 is identical for every configuration.

    Every module changed:

Edges per node N main best / median This PR best / median Speedup (best)
~2 10 4.3 / 4.6 3.8 / 3.9 1.15×
~2 100 49.3 / 53.8 42.9 / 44.4 1.15×
~2 1,000 559.3 / 593.5 463.4 / 483.3 1.21×
~2 4,000 2,470.4 / 2,751.1 2,141.3 / 2,466.2 1.15×
~2 8,000 5,271.3 / 5,808.1 4,914.3 / 5,325.3 1.07×
~2 16,000 12,278.0 / 12,727.1 11,120.8 / 11,473.1 1.10×
~6 10 6.8 / 7.4 6.0 / 7.1 1.14×
~6 100 86.3 / 87.0 71.2 / 78.4 1.21×
~6 1,000 979.1 / 1,001.3 797.2 / 858.8 1.23×
~6 4,000 4,242.3 / 4,521.2 3,614.4 / 3,858.5 1.17×
~6 8,000 9,679.0 / 10,533.2 8,627.2 / 9,506.8 1.12×
~6 16,000 22,578.9 / 22,845.0 21,935.5 / 22,526.5 1.03×

Identical versions:

Edges per node N main best / median This PR best / median Speedup (best)
~2 10 3.4 / 3.6 2.8 / 2.9 1.22×
~2 100 39.2 / 44.2 33.0 / 35.7 1.19×
~2 1,000 416.2 / 436.6 358.2 / 405.2 1.16×
~2 4,000 1,603.5 / 1,763.9 1,595.4 / 1,794.0 1.01×
~6 10 5.6 / 6.1 4.9 / 5.2 1.14×
~6 100 74.1 / 83.1 63.0 / 69.5 1.18×
~6 1,000 855.9 / 908.5 713.6 / 742.1 1.20×
~6 4,000 3,508.6 / 3,681.7 3,201.8 / 3,360.6 1.10×

With createApplicationVersionComparisonEvidence, every module changed:

Edges per node N main best / median This PR best / median Speedup (best)
~2 10 6.2 / 7.0 5.6 / 5.7 1.10×
~2 100 71.1 / 77.6 64.1 / 72.4 1.11×
~2 1,000 771.1 / 842.5 721.2 / 768.6 1.07×
~2 4,000 3,443.6 / 3,487.0 3,188.7 / 3,339.2 1.08×
~6 10 10.4 / 11.2 9.2 / 10.4 1.13×
~6 100 125.2 / 132.3 112.3 / 116.5 1.11×
~6 1,000 1,388.2 / 1,540.4 1,278.3 / 1,316.4 1.09×
~6 4,000 6,378.3 / 6,572.7 5,739.6 / 6,162.0 1.11×

With createApplicationVersionComparisonEvidence, identical versions:

Edges per node N main best / median This PR best / median Speedup (best)
~2 10 4.7 / 5.2 4.3 / 4.8 1.09×
~2 100 55.7 / 57.8 47.8 / 51.3 1.17×
~2 1,000 580.3 / 601.2 534.6 / 555.0 1.09×
~2 4,000 2,574.5 / 2,718.1 2,375.1 / 2,442.7 1.08×
~6 10 9.0 / 9.4 8.5 / 9.1 1.06×
~6 100 120.8 / 124.1 111.5 / 120.7 1.08×
~6 1,000 1,258.1 / 1,347.8 1,110.2 / 1,217.3 1.13×
~6 4,000 5,819.9 / 6,270.6 4,950.6 / 5,340.4 1.18×
Bench script (set DIST to a built dist directory)
// DIST=path/to/dist SHAPE=2|6 SIZES=1000,8000 SAME=0|1 EVIDENCE=0|1 REPS=5 node bench.mjs
import { createHash } from "node:crypto";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
const D = pathToFileURL(resolve(process.env.DIST)).href;
const G = await import(`${D}/domain/javascript/javascriptApplicationGraph.js`);
const { compareJavaScriptApplicationVersions: cmp } = await import(`${D}/domain/javascript/javascriptApplicationVersionComparison.js`);
const W = await import(`${D}/application/javascript/JavaScriptApplicationWorkflowEvidence.js`);
const hex = (n) => n.toString(16).padStart(64, "0");
const ev = (sha, path) => ({ authority: "artifact-bytes", state: "observed", confidence: "exact",
  artifact: { available: true, artifact_id: `art_${sha}`, sha256: sha },
  location: { available: true, value: { kind: "artifact-path", path } },
  extractor: { name: "b", version: "1", operation: "o", executable_sha256: null },
  coverage: { status: "complete", truncated: false, omitted_count: 0, limits: [] }, limitations: [], evidence_ids: [] });
const K = Number(process.env.SHAPE ?? 2), SAME = process.env.SAME === "1", EVID = process.env.EVIDENCE === "1";
const REPS = Number(process.env.REPS ?? 5);
const edgesOf = (nodes, art, ver) => {
  const e = [];
  for (let i = 0; i < nodes.length; i++) for (let k = 0; k < K; k++) {
    const j = k === 0 ? i + 1 : (i * (31 + 2 * k) + 7 * k) % nodes.length;
    if (j >= nodes.length || j === i) continue;
    e.push(G.createJavaScriptApplicationEdge({ source_node_id: nodes[i].node_id, target_node_id: nodes[j].node_id,
      relation: (i + j + (ver && i % 5 === 0 ? 1 : 0)) % 3 ? "imports" : "contains", properties: {}, evidence: ev(art, "m/" + i + ".js") }));
  }
  return [...new Map(e.map((x) => [x.edge_id, x])).values()];
};
const graph = (n, art, ver) => {
  const nodes = [];
  for (let i = 0; i < n; i++) nodes.push(G.createJavaScriptApplicationNode({ kind: "javascript-module",
    identity: { strategy: "canonical-path", stability: "artifact-version", artifact_sha256: art, path: `m/${i}.js` },
    observations: [{ label: `m${i}`, properties: { structural_fingerprint_sha256: hex(i + 1), structural_fingerprint_algorithm: "x", source_sha256: hex(i * 7 + ver) }, evidence: ev(art, `m/${i}.js`) }] }));
  return G.createJavaScriptApplicationGraph({ schema: "JavaScriptApplicationGraph", root_node_ids: [nodes[0].node_id], nodes,
    edges: edgesOf(nodes, art, ver), coverage: { status: "complete", truncated: false, omitted_count: 0, limits: [] }, limitations: [] });
};
const run = (input) => { const r = cmp(input); return EVID ? W.createApplicationVersionComparisonEvidence({ left_evidence_id: input.left.evidenceId, right_evidence_id: input.right.evidenceId, left_native_evidence_ids: [], right_native_evidence_ids: [] }, r) : r; };
const med = (a) => [...a].sort((x, y) => x - y)[Math.floor(a.length / 2)];
for (const n of (process.env.SIZES ?? "1000").split(",").map(Number)) {
  const L = graph(n, hex(900001), 0), R = graph(n, hex(900002), SAME ? 0 : 1);
  const input = { left: { evidenceId: "ev_" + hex(11), rootArtifactSha256: hex(900001), graph: L },
    right: { evidenceId: "ev_" + hex(12), rootArtifactSha256: hex(900002), graph: R }, leftNativeEvidence: [], rightNativeEvidence: [] };
  let res = run(input); const inner = Math.max(1, Math.floor(2000 / n));
  const ts = [];
  for (let k = 0; k < REPS; k++) { const t = performance.now(); for (let q = 0; q < inner; q++) res = run(input); ts.push((performance.now() - t) / inner); }
  const json = JSON.stringify(res);
  console.log(JSON.stringify({ dist: process.env.DIST, shape: K, same: SAME, evidence: EVID, n, best: +Math.min(...ts).toFixed(2), median: +med(ts).toFixed(2), digest: createHash("sha256").update(json).digest("hex").slice(0, 16) }));
}
  • User-visible CLI/MCP output: unchanged.
  • Remaining proof gaps: the comparison speedups are on synthetic graphs. On real analysis runs the change is not measurable end to end.

For evidence-bearing changes:

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, source provenance, and failed attempts remain preserved.
  • Unsupported, incomplete, unavailable, or uncertain outcomes remain visible.

Validation performed

Run in a Linux arm64 container with Node 24, on this branch and on unchanged main at c29931ce8:

Command This branch main
npm run build:cached 3 of 3 tasks successful 3 of 3 tasks successful
npx vitest run src/domain/javascript/javascriptApplicationGraph.test.ts src/domain/javascript/javascriptApplicationVersionComparison.test.ts tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts tests/acceptance/applications/applicationWorkflowCli.test.ts 4 files, 46 tests passed 4 files, 45 tests passed
npm run check 6 of 6 tasks successful 6 of 6 tasks successful
npm run verify:test-discovery all 705 test files discovered once all 705 test files discovered once
npx vitest run --project mcp-boundary (after npm run artifacts:mcp-catalog) 317 passed, 2 skipped 317 passed, 2 skipped
npm run test:fast 3,611 passed, 4 skipped 3,610 passed, 4 skipped

The only difference is the one new test. After rebasing onto 33b9b31a6 and again onto the PR base c89c8f531, npm run build:cached, the four focused files (46 tests) and npm run check pass.

Compatibility, safety, and release

  • Breaking changes or migration steps: none.
  • Real Hopper/Ghidra, browser, or OS coverage: not applicable; pure domain change, no provider involved.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: none affected. Untrusted and stored graphs still pass through the full public schema, including the graph_id check.

Review checklist

  • This PR addresses a concrete problem or an agreed enhancement.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/1458 **Author:** [@keemsisi](https://github.com/keemsisi) **Created:** 10/10/2026 **Status:** ✅ Merged **Merged:** 10/10/2026 **Merged by:** [@N0zoM1z0](https://github.com/N0zoM1z0) **Base:** `main` ← **Head:** `perf/graph-id-once` --- ### 📝 Commits (1) - [`d8d1f19`](https://github.com/morluto/rea/commit/d8d1f19184a5ef6ef2c397e2a7add8a3bc451f7a) perf(javascript): derive the application graph ID once ### 📊 Changes **2 files changed** (+31 additions, -6 deletions) <details> <summary>View changed files</summary> 📝 `src/domain/javascript/javascriptApplicationGraph.test.ts` (+15 -0) 📝 `src/domain/javascript/javascriptApplicationGraph.ts` (+16 -6) </details> ### 📄 Description ## Summary `createJavaScriptApplicationGraph` hashed the whole graph twice. It computed `graph_id` as the canonical SHA-256 of the graph content, then parsed the result with the public `javascriptApplicationGraphSchema`, whose invariant check hashed the same content again to verify the ID it had just been given. This change validates the content with every other invariant first, then derives `graph_id` once from the validated copy. Created graphs are byte-identical. The public schema still verifies `graph_id` for stored and untrusted graphs. Measured effect: - `compareJavaScriptApplicationVersions`, which builds a change graph, is 1.07–1.23× faster in 35 of 36 measured configurations and 1.01× in the remaining one, with no slowdown. - Real `analyze-javascript-application` runs on three npm packages show no measurable end-to-end change: graph-ID hashing is a small share of a 40–50 s analysis. Their Evidence IDs are identical. Related performance roadmap: #1065. No dedicated issue is linked; this is a focused performance fix, and the measurements below describe the problem and the result. ## Problem and expected behavior Trigger: any call to `createJavaScriptApplicationGraph`. This includes application analysis, feature traces, runtime reconciliation, and the change graph built by every version comparison. In `src/domain/javascript/javascriptApplicationGraph.ts`: ```ts return javascriptApplicationGraphSchema.parse({ ...semantic, graph_id: `jag_${digestCanonicalValue(semantic, "JavaScript Application Graph")}`, }); ``` `javascriptApplicationGraphSchema` is `javascriptApplicationGraphRecordSchema.superRefine(checkGraphInvariants)`, and `checkGraphInvariants` ends with: ```ts const { graph_id: identifier, ...semantic } = graph; if (identifier !== `jag_${digestCanonicalValue(semantic, "JavaScript Application Graph")}`) ... ``` So every graph created by the factory was canonically serialized and hashed twice. On a CPU profile of a 4,000-module version comparison (current `main` plus #1434), these two whole-graph digests were about 13% and 15% of the comparison's time. Expected: the factory hashes the validated content once. Every other invariant still runs. Stored and untrusted graphs are still fully verified, including `graph_id`. ## Change and scope - `checkGraphInvariants` is split into `checkGraphContent`, which holds every previous check except the graph-ID comparison (same messages and paths), and the graph-ID comparison. `checkGraphInvariants` runs both, so the public `javascriptApplicationGraphSchema` behaves exactly as before. - A private `javascriptApplicationGraphContentSchema` (`javascriptApplicationGraphInputSchema.superRefine(checkGraphContent)`) validates the normalized content in the factory. The factory then returns `{ ...content, graph_id }`, with `graph_id` digested from that validated copy. The key order matches the record schema (`graph_id` last), so the serialized graph is unchanged. - The input and record schemas share the same node, edge, coverage and properties schemas, and re-parsing already parsed content is idempotent. So the digest of the validated copy equals the digest the old code verified, for every input the old code accepted. Not included: - No change to node, edge or observation ID derivation or verification. Edge and observation IDs are still recomputed and checked during the content parse. - No change to `parseJavaScriptApplicationGraph`, `computeJavaScriptApplicationGraphSha256`, serialization, the owned immutable graph steps, or any result contract. - The new content schema is not exported. The public and owned validation split from #1370 stays explicit. ## Contract and boundary impact - Semantic owner and earliest changed stage: domain, `src/domain/javascript/javascriptApplicationGraph.ts`. - CLI and MCP/tool-catalog contract: none; inputs and results are unchanged. - Provider, bridge, target-format, or platform compatibility: none. - Evidence, artifact, provenance, or reconstruction contract: none. Created graphs, graph IDs and Evidence IDs are byte-identical, and stored graphs are verified exactly as before. - Process execution, authorization, cleanup, or containment impact: none. - Generated metadata (`docs/public/product-catalog.json`), package, or installation impact: none. ## Evidence and regression coverage - Test added in `src/domain/javascript/javascriptApplicationGraph.test.ts`, "creates exactly the graph the public schema accepts": - the factory's output serializes to exactly the same JSON, including key order, as `javascriptApplicationGraphSchema.parse` of a clone of it; - the factory still rejects content with a stale edge ID (`Edge identifier`). It passes on `main` and on this branch. Existing tests continue to cover the factory's rejection of duplicate nodes, missing roots, dangling and self edges, and the public parser's rejection of stale graph, node, observation and edge IDs. - Mutants applied to this change, run against the graph test, `src/domain/javascript/javascriptApplicationVersionComparison.test.ts` and `tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts`: | Mutant | Result | |---|---| | skip the content parse (digest the unvalidated input) | 7 of 32 tests fail | | put `graph_id` first in the returned object | 1 of 32 tests fails | | digest the content without `limitations` | 13 of 32 tests fail | - Public workflow parity: the compiled CLI ran `node scripts/rea.mjs analyze-javascript-application <dir> --json` on copies of three real npm packages, on `main` (`33b9b31a6`) and on this branch. The run order was interleaved: base, fix, fix, base. | Package | Nodes / edges | Evidence ID (`main` and this PR) | `graph_id` | Wall time `main` / this PR | |---|---|---|---|---| | `zod` | 7,973 / 11,575 | `ev_76643e66…c0642b275` | `jag_19d03a92fce57c7e…` | 40.5–43.2 s / 39.6–41.0 s | | `knip` | 5,336 / 11,352 | `ev_bc37338b…ecdb6719` | `jag_360b76780c58b114…` | 37.5–39.3 s / 38.1–39.5 s | | `playwright-core` | 1,331 / 4,214 | `ev_2de650ca…ad3ba` | `jag_e534b08c23aac69e…` | 46.5–49.6 s / 46.3–51.1 s (6 runs each) | Evidence IDs and graph IDs are identical. Wall-time differences are within run-to-run noise; analysis time is dominated by parsing and extraction, not by graph-ID hashing. - Performance: end-to-end `compareJavaScriptApplicationVersions` on synthetic version pairs with N matched modules, using the built `dist` of `main` (`c29931ce8`) and of this branch. - Runs are interleaved base/fix/fix/base. - Each run is 1 warm-up plus 5 repetitions (3 at 16,000). - Values are best / median in ms. - The full result JSON sha256 is identical for every configuration. Every module changed: | Edges per node | N | `main` best / median | This PR best / median | Speedup (best) | |---|---|---|---|---| | ~2 | 10 | 4.3 / 4.6 | 3.8 / 3.9 | 1.15× | | ~2 | 100 | 49.3 / 53.8 | 42.9 / 44.4 | 1.15× | | ~2 | 1,000 | 559.3 / 593.5 | 463.4 / 483.3 | 1.21× | | ~2 | 4,000 | 2,470.4 / 2,751.1 | 2,141.3 / 2,466.2 | 1.15× | | ~2 | 8,000 | 5,271.3 / 5,808.1 | 4,914.3 / 5,325.3 | 1.07× | | ~2 | 16,000 | 12,278.0 / 12,727.1 | 11,120.8 / 11,473.1 | 1.10× | | ~6 | 10 | 6.8 / 7.4 | 6.0 / 7.1 | 1.14× | | ~6 | 100 | 86.3 / 87.0 | 71.2 / 78.4 | 1.21× | | ~6 | 1,000 | 979.1 / 1,001.3 | 797.2 / 858.8 | 1.23× | | ~6 | 4,000 | 4,242.3 / 4,521.2 | 3,614.4 / 3,858.5 | 1.17× | | ~6 | 8,000 | 9,679.0 / 10,533.2 | 8,627.2 / 9,506.8 | 1.12× | | ~6 | 16,000 | 22,578.9 / 22,845.0 | 21,935.5 / 22,526.5 | 1.03× | Identical versions: | Edges per node | N | `main` best / median | This PR best / median | Speedup (best) | |---|---|---|---|---| | ~2 | 10 | 3.4 / 3.6 | 2.8 / 2.9 | 1.22× | | ~2 | 100 | 39.2 / 44.2 | 33.0 / 35.7 | 1.19× | | ~2 | 1,000 | 416.2 / 436.6 | 358.2 / 405.2 | 1.16× | | ~2 | 4,000 | 1,603.5 / 1,763.9 | 1,595.4 / 1,794.0 | 1.01× | | ~6 | 10 | 5.6 / 6.1 | 4.9 / 5.2 | 1.14× | | ~6 | 100 | 74.1 / 83.1 | 63.0 / 69.5 | 1.18× | | ~6 | 1,000 | 855.9 / 908.5 | 713.6 / 742.1 | 1.20× | | ~6 | 4,000 | 3,508.6 / 3,681.7 | 3,201.8 / 3,360.6 | 1.10× | With `createApplicationVersionComparisonEvidence`, every module changed: | Edges per node | N | `main` best / median | This PR best / median | Speedup (best) | |---|---|---|---|---| | ~2 | 10 | 6.2 / 7.0 | 5.6 / 5.7 | 1.10× | | ~2 | 100 | 71.1 / 77.6 | 64.1 / 72.4 | 1.11× | | ~2 | 1,000 | 771.1 / 842.5 | 721.2 / 768.6 | 1.07× | | ~2 | 4,000 | 3,443.6 / 3,487.0 | 3,188.7 / 3,339.2 | 1.08× | | ~6 | 10 | 10.4 / 11.2 | 9.2 / 10.4 | 1.13× | | ~6 | 100 | 125.2 / 132.3 | 112.3 / 116.5 | 1.11× | | ~6 | 1,000 | 1,388.2 / 1,540.4 | 1,278.3 / 1,316.4 | 1.09× | | ~6 | 4,000 | 6,378.3 / 6,572.7 | 5,739.6 / 6,162.0 | 1.11× | With `createApplicationVersionComparisonEvidence`, identical versions: | Edges per node | N | `main` best / median | This PR best / median | Speedup (best) | |---|---|---|---|---| | ~2 | 10 | 4.7 / 5.2 | 4.3 / 4.8 | 1.09× | | ~2 | 100 | 55.7 / 57.8 | 47.8 / 51.3 | 1.17× | | ~2 | 1,000 | 580.3 / 601.2 | 534.6 / 555.0 | 1.09× | | ~2 | 4,000 | 2,574.5 / 2,718.1 | 2,375.1 / 2,442.7 | 1.08× | | ~6 | 10 | 9.0 / 9.4 | 8.5 / 9.1 | 1.06× | | ~6 | 100 | 120.8 / 124.1 | 111.5 / 120.7 | 1.08× | | ~6 | 1,000 | 1,258.1 / 1,347.8 | 1,110.2 / 1,217.3 | 1.13× | | ~6 | 4,000 | 5,819.9 / 6,270.6 | 4,950.6 / 5,340.4 | 1.18× | <details><summary>Bench script (set <code>DIST</code> to a built <code>dist</code> directory)</summary> ```js // DIST=path/to/dist SHAPE=2|6 SIZES=1000,8000 SAME=0|1 EVIDENCE=0|1 REPS=5 node bench.mjs import { createHash } from "node:crypto"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; const D = pathToFileURL(resolve(process.env.DIST)).href; const G = await import(`${D}/domain/javascript/javascriptApplicationGraph.js`); const { compareJavaScriptApplicationVersions: cmp } = await import(`${D}/domain/javascript/javascriptApplicationVersionComparison.js`); const W = await import(`${D}/application/javascript/JavaScriptApplicationWorkflowEvidence.js`); const hex = (n) => n.toString(16).padStart(64, "0"); const ev = (sha, path) => ({ authority: "artifact-bytes", state: "observed", confidence: "exact", artifact: { available: true, artifact_id: `art_${sha}`, sha256: sha }, location: { available: true, value: { kind: "artifact-path", path } }, extractor: { name: "b", version: "1", operation: "o", executable_sha256: null }, coverage: { status: "complete", truncated: false, omitted_count: 0, limits: [] }, limitations: [], evidence_ids: [] }); const K = Number(process.env.SHAPE ?? 2), SAME = process.env.SAME === "1", EVID = process.env.EVIDENCE === "1"; const REPS = Number(process.env.REPS ?? 5); const edgesOf = (nodes, art, ver) => { const e = []; for (let i = 0; i < nodes.length; i++) for (let k = 0; k < K; k++) { const j = k === 0 ? i + 1 : (i * (31 + 2 * k) + 7 * k) % nodes.length; if (j >= nodes.length || j === i) continue; e.push(G.createJavaScriptApplicationEdge({ source_node_id: nodes[i].node_id, target_node_id: nodes[j].node_id, relation: (i + j + (ver && i % 5 === 0 ? 1 : 0)) % 3 ? "imports" : "contains", properties: {}, evidence: ev(art, "m/" + i + ".js") })); } return [...new Map(e.map((x) => [x.edge_id, x])).values()]; }; const graph = (n, art, ver) => { const nodes = []; for (let i = 0; i < n; i++) nodes.push(G.createJavaScriptApplicationNode({ kind: "javascript-module", identity: { strategy: "canonical-path", stability: "artifact-version", artifact_sha256: art, path: `m/${i}.js` }, observations: [{ label: `m${i}`, properties: { structural_fingerprint_sha256: hex(i + 1), structural_fingerprint_algorithm: "x", source_sha256: hex(i * 7 + ver) }, evidence: ev(art, `m/${i}.js`) }] })); return G.createJavaScriptApplicationGraph({ schema: "JavaScriptApplicationGraph", root_node_ids: [nodes[0].node_id], nodes, edges: edgesOf(nodes, art, ver), coverage: { status: "complete", truncated: false, omitted_count: 0, limits: [] }, limitations: [] }); }; const run = (input) => { const r = cmp(input); return EVID ? W.createApplicationVersionComparisonEvidence({ left_evidence_id: input.left.evidenceId, right_evidence_id: input.right.evidenceId, left_native_evidence_ids: [], right_native_evidence_ids: [] }, r) : r; }; const med = (a) => [...a].sort((x, y) => x - y)[Math.floor(a.length / 2)]; for (const n of (process.env.SIZES ?? "1000").split(",").map(Number)) { const L = graph(n, hex(900001), 0), R = graph(n, hex(900002), SAME ? 0 : 1); const input = { left: { evidenceId: "ev_" + hex(11), rootArtifactSha256: hex(900001), graph: L }, right: { evidenceId: "ev_" + hex(12), rootArtifactSha256: hex(900002), graph: R }, leftNativeEvidence: [], rightNativeEvidence: [] }; let res = run(input); const inner = Math.max(1, Math.floor(2000 / n)); const ts = []; for (let k = 0; k < REPS; k++) { const t = performance.now(); for (let q = 0; q < inner; q++) res = run(input); ts.push((performance.now() - t) / inner); } const json = JSON.stringify(res); console.log(JSON.stringify({ dist: process.env.DIST, shape: K, same: SAME, evidence: EVID, n, best: +Math.min(...ts).toFixed(2), median: +med(ts).toFixed(2), digest: createHash("sha256").update(json).digest("hex").slice(0, 16) })); } ``` </details> - User-visible CLI/MCP output: unchanged. - Remaining proof gaps: the comparison speedups are on synthetic graphs. On real analysis runs the change is not measurable end to end. For evidence-bearing changes: - [x] Observed, derived, and inferred claims remain distinguishable. - [x] Artifact identity, source provenance, and failed attempts remain preserved. - [x] Unsupported, incomplete, unavailable, or uncertain outcomes remain visible. ## Validation performed Run in a Linux arm64 container with Node 24, on this branch and on unchanged `main` at `c29931ce8`: | Command | This branch | `main` | |---|---|---| | `npm run build:cached` | 3 of 3 tasks successful | 3 of 3 tasks successful | | `npx vitest run src/domain/javascript/javascriptApplicationGraph.test.ts src/domain/javascript/javascriptApplicationVersionComparison.test.ts tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts tests/acceptance/applications/applicationWorkflowCli.test.ts` | 4 files, 46 tests passed | 4 files, 45 tests passed | | `npm run check` | 6 of 6 tasks successful | 6 of 6 tasks successful | | `npm run verify:test-discovery` | all 705 test files discovered once | all 705 test files discovered once | | `npx vitest run --project mcp-boundary` (after `npm run artifacts:mcp-catalog`) | 317 passed, 2 skipped | 317 passed, 2 skipped | | `npm run test:fast` | 3,611 passed, 4 skipped | 3,610 passed, 4 skipped | The only difference is the one new test. After rebasing onto `33b9b31a6` and again onto the PR base `c89c8f531`, `npm run build:cached`, the four focused files (46 tests) and `npm run check` pass. ## Compatibility, safety, and release - Breaking changes or migration steps: none. - Real Hopper/Ghidra, browser, or OS coverage: not applicable; pure domain change, no provider involved. - Package or release metadata impact: none. - Security, privacy, process, or containment review: none affected. Untrusted and stored graphs still pass through the full public schema, including the `graph_id` check. ## Review checklist - [x] This PR addresses a concrete problem or an agreed enhancement. - [x] The PR has one focused outcome and the title follows `type(scope): outcome`. - [x] Related issue is linked, or the reason for not linking one is stated above. - [x] Tests cover changed observable behavior and meaningful failure paths. - [x] Owning docs, contracts, and generated metadata are updated where needed. - [x] User-visible CLI/MCP changes include representative output. - [x] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-17 21:11:36 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#2900
No description provided.