From 8f7d12e30d63f3d3b4cfe2a90336362841438f56 Mon Sep 17 00:00:00 2001 From: Ysopwnable <161784452+N0zoM1z0@users.noreply.github.com> Date: Sun, 18 Oct 2026 14:38:00 +0800 Subject: [PATCH] perf(analysis-view): reuse validated immutable parents Reuse successful format validation for exact authenticated immutable native and imported JavaScript results. Preserve schema normalization and alias topology before borrowing source data; owned JavaScript graph wrappers retain their existing metadata-normalization path. Validated with 130 source tests, 36 compiled CLI/MCP boundary tests, 27 default-deadline integration tests, full checks/docs/build, and paired real retained-Evidence tasks. The report preserves cold costs, previous negative results, incomplete resource-stopped runs, and the limits of offline token estimates. Fixes #4810 Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com> Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> --- experiments/analysis-efficiency/README.md | 5 + .../immutable-parent-validation.json | 654 ++++++++++++++++++ .../immutable-parent-validation.md | 249 +++++++ .../analysisView/AnalysisViewService.test.ts | 30 + .../analysisView/AnalysisViewService.ts | 9 +- src/domain/analysisView/analysisView.ts | 13 +- src/domain/immutableJson.test.ts | 207 +++++- src/domain/immutableJson.ts | 85 +++ src/domain/immutableJsonSteps.test.ts | 25 + .../javascriptApplicationAnalysis.ts | 43 +- ...javascriptApplicationAnalysisSteps.test.ts | 96 +++ src/domain/native/binaryLayout.test.ts | 56 +- src/domain/native/binaryLayout.ts | 23 + 13 files changed, 1473 insertions(+), 22 deletions(-) create mode 100644 experiments/analysis-efficiency/immutable-parent-validation.json create mode 100644 experiments/analysis-efficiency/immutable-parent-validation.md diff --git a/experiments/analysis-efficiency/README.md b/experiments/analysis-efficiency/README.md index 227ed379c4..75da2d5e9b 100644 --- a/experiments/analysis-efficiency/README.md +++ b/experiments/analysis-efficiency/README.md @@ -1,5 +1,10 @@ # Analysis efficiency measurements +For immutable imported native/JavaScript parents, see the +[validation-reuse checkpoint](immutable-parent-validation.md). It measures the +owning format change with the existing retained-query driver, including first +view costs, small cases, unchanged token estimates, and noisy whole-task results. + For authentic old Evidence and mixed investigations, see the [retained-query checkpoint](retained-investigation-workflows.md). `retained-investigation.mjs` compares existing complete delivery with selected diff --git a/experiments/analysis-efficiency/immutable-parent-validation.json b/experiments/analysis-efficiency/immutable-parent-validation.json new file mode 100644 index 0000000000..b544090158 --- /dev/null +++ b/experiments/analysis-efficiency/immutable-parent-validation.json @@ -0,0 +1,654 @@ +{ + "issue": "https://github.com/morluto/rea/issues/4810", + "baseline": "2c4338db1fdb7773e05376301c558cff2a0b4b80", + "candidate": "3930a640fb8387cbd3079eee13fb90f82d1881dc", + "units": { + "latency": "milliseconds", + "elapsed_cpu": "seconds", + "memory": "kernel cgroup memory.peak bytes, including charged file cache and descendants" + }, + "scope": "Three alternating before/after pairs per authentic retained native/JavaScript/small workflow; fresh stdio servers, three traversals of three two-row pages; catalog/import/oracles/export/close included in whole cost. OS caches uncontrolled. No actual model trial.", + "resource_bounds": { + "cpu_count": 1, + "hard_memory_bytes": 8589934592, + "early_stop_bytes": 7516192768, + "swap_bytes": 0, + "tasks_max": 192, + "host_start_available_bytes_min": 17179869184, + "host_stop_available_bytes_min": 8589934592, + "observer_sha256": "6f089e645459eba80c5d73e39c21606c78173a44daf49fabc3b88c4631ce51c6", + "guard_sha256s": [ + "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992", + "d2047721e50dd244900eb514d07025386e60908f77bb546bee5684baa1da2f19" + ], + "guard_observability_note": "The newer private guard only records existing host-available-memory readings and its own cgroup path. Resource thresholds, CPU affinity, observer and stop/cleanup logic are unchanged." + }, + "resource_accounting": "Final complete corpus tasks exit0/cleanup verified/oom0 at payload exit while observer remains alive. Resource-stopped and earlier failed attempts are recorded separately; terminal whole-scope counters are unavailable. Weak cache metadata/private heap not measured independently.", + "catalog": { + "tools": 207, + "bytes": 3158368, + "sha256": "1d6c9a679b7c935318f5ea59d5362ef0a06945b3f4a4f210c86997ec15ba4f4c", + "equal_across_all_18_runs": true + }, + "facts": { + "native": { + "answer_sha256": "4e28c2c2a25620576d27abe846a05835ec49386b889779e33410c31971523b54", + "calls": 25, + "query_structured_content_equal": true, + "original_records_preserved": true, + "closed_session_cleared": true + }, + "javascript": { + "answer_sha256": "0fe3ad249eb1a7233a4966a56652c173667c48fdaf395b6c08fc6901ef3b46f7", + "calls": 28, + "query_structured_content_equal": true, + "original_records_preserved": true, + "closed_session_cleared": true + }, + "small": { + "answer_sha256": "9ca5874ce7bb610e39632157304fe4f52b09401d5fbd97ec981ad0588b8da485", + "calls": 19, + "query_structured_content_equal": true, + "original_records_preserved": true, + "closed_session_cleared": true + } + }, + "runs": [ + { + "name": "topology-paired-after-javascript-1", + "first_page_ms": 2720.542831999999, + "eight_later_pages_ms": 82.69408900000417, + "elapsed_seconds": 20.713, + "family_cpu_seconds": 20.50093, + "family_peak_bytes": 1311358976, + "import_ms": 10970.411619, + "catalog_ms": 1194.7045099999996, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-after-javascript-2", + "first_page_ms": 1978.732818999999, + "eight_later_pages_ms": 84.12934999999197, + "elapsed_seconds": 20.011, + "family_cpu_seconds": 19.733879, + "family_peak_bytes": 1349349376, + "import_ms": 10382.461203, + "catalog_ms": 1436.8567430000003, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-after-javascript-4", + "first_page_ms": 1864.8931889999985, + "eight_later_pages_ms": 75.18833300000188, + "elapsed_seconds": 23.023, + "family_cpu_seconds": 22.22643, + "family_peak_bytes": 1410211840, + "import_ms": 10857.059245, + "catalog_ms": 2247.621502, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-after-native-1", + "first_page_ms": 216.87685000000056, + "eight_later_pages_ms": 85.03580100000181, + "elapsed_seconds": 17.253, + "family_cpu_seconds": 17.002728, + "family_peak_bytes": 1003585536, + "import_ms": 10361.605091, + "catalog_ms": 1289.5255869999992, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-after-native-2", + "first_page_ms": 209.0475239999996, + "eight_later_pages_ms": 89.01302899999973, + "elapsed_seconds": 14.383, + "family_cpu_seconds": 14.091764, + "family_peak_bytes": 1031806976, + "import_ms": 7696.751557, + "catalog_ms": 1175.233292, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-after-native-3", + "first_page_ms": 203.10824999999932, + "eight_later_pages_ms": 65.1013469999998, + "elapsed_seconds": 14.061, + "family_cpu_seconds": 13.773228, + "family_peak_bytes": 1098002432, + "import_ms": 7967.397724, + "catalog_ms": 1246.0458209999997, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-before-javascript-1", + "first_page_ms": 1681.2564180000008, + "eight_later_pages_ms": 10378.131188999992, + "elapsed_seconds": 31.213, + "family_cpu_seconds": 30.872607, + "family_peak_bytes": 1618685952, + "import_ms": 11051.493961999999, + "catalog_ms": 1508.7439510000004, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-before-javascript-2", + "first_page_ms": 2441.075147000003, + "eight_later_pages_ms": 15534.410540000004, + "elapsed_seconds": 40.856, + "family_cpu_seconds": 40.485251, + "family_peak_bytes": 1364238336, + "import_ms": 15032.366213, + "catalog_ms": 1319.6895839999997, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-before-javascript-4", + "first_page_ms": 2479.6895360000017, + "eight_later_pages_ms": 16864.908822999998, + "elapsed_seconds": 39.629, + "family_cpu_seconds": 39.267656, + "family_peak_bytes": 1403023360, + "import_ms": 12437.073842999998, + "catalog_ms": 1212.8024130000003, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-before-native-1", + "first_page_ms": 170.1303939999998, + "eight_later_pages_ms": 469.8933869999928, + "elapsed_seconds": 19.996, + "family_cpu_seconds": 19.55106, + "family_peak_bytes": 1063051264, + "import_ms": 11242.171146, + "catalog_ms": 1922.7818150000003, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-before-native-2", + "first_page_ms": 183.56196199999977, + "eight_later_pages_ms": 393.4744759999994, + "elapsed_seconds": 15.26, + "family_cpu_seconds": 15.008304, + "family_peak_bytes": 1058492416, + "import_ms": 8713.099498, + "catalog_ms": 1272.265849, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-paired-before-native-3", + "first_page_ms": 244.26586400000087, + "eight_later_pages_ms": 517.9172599999911, + "elapsed_seconds": 19.083, + "family_cpu_seconds": 18.752553, + "family_peak_bytes": 1109987328, + "import_ms": 9948.476274999999, + "catalog_ms": 1879.787958, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-after-small-1", + "first_page_ms": 226.6032009999999, + "eight_later_pages_ms": 57.69190299999718, + "elapsed_seconds": 12.312, + "family_cpu_seconds": 12.043895, + "family_peak_bytes": 1056452608, + "import_ms": 6572.925846000001, + "catalog_ms": 1173.0129960000004, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-after-small-2", + "first_page_ms": 556.2178050000002, + "eight_later_pages_ms": 96.23635600000307, + "elapsed_seconds": 16.246, + "family_cpu_seconds": 16.068419, + "family_peak_bytes": 1002749952, + "import_ms": 9666.640497, + "catalog_ms": 1821.1315130000003, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-after-small-4", + "first_page_ms": 111.79372199999852, + "eight_later_pages_ms": 62.58284999999887, + "elapsed_seconds": 11.901, + "family_cpu_seconds": 11.707784, + "family_peak_bytes": 976646144, + "import_ms": 6473.91882, + "catalog_ms": 1203.5203539999998, + "guard_sha256": "d2047721e50dd244900eb514d07025386e60908f77bb546bee5684baa1da2f19" + }, + { + "name": "topology-before-small-1", + "first_page_ms": 242.57812199999898, + "eight_later_pages_ms": 299.467095, + "elapsed_seconds": 17.585, + "family_cpu_seconds": 17.312434, + "family_peak_bytes": 1066864640, + "import_ms": 9102.074585, + "catalog_ms": 1829.971928, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-before-small-2", + "first_page_ms": 133.3346359999996, + "eight_later_pages_ms": 122.92684500000178, + "elapsed_seconds": 14.302, + "family_cpu_seconds": 13.936122, + "family_peak_bytes": 1062400000, + "import_ms": 7254.973107, + "catalog_ms": 1170.888121, + "guard_sha256": "760d541c60f4c0538f9c015964196e3be7bba12316f6085903244354e11aa992" + }, + { + "name": "topology-before-small-4", + "first_page_ms": 100.81227200000103, + "eight_later_pages_ms": 76.37957900000038, + "elapsed_seconds": 12.0, + "family_cpu_seconds": 11.765305, + "family_peak_bytes": 983314432, + "import_ms": 6614.461618999999, + "catalog_ms": 1138.6469730000003, + "guard_sha256": "d2047721e50dd244900eb514d07025386e60908f77bb546bee5684baa1da2f19" + } + ], + "token_estimates": { + "tokenizer": "tiktoken", + "version": "0.12.0", + "encoding": "cl100k_base", + "kind": "offline raw transport serialization estimate; one complete catalog plus each call name/arguments/reply exactly once; excludes initialization/framing, prompts, client rendering/discovery/replay, continuations and actual model usage; not an agent-token measurement", + "runs": [ + { + "run": "topology-paired-before-native-1", + "catalog_tokens_estimated": 805215, + "named_call_arguments_tokens_estimated": 1200, + "reply_tokens_estimated": 43321, + "total_tokens_estimated": 849736, + "calls": 25 + }, + { + "run": "topology-paired-after-native-1", + "catalog_tokens_estimated": 805215, + "named_call_arguments_tokens_estimated": 1200, + "reply_tokens_estimated": 43321, + "total_tokens_estimated": 849736, + "calls": 25 + }, + { + "run": "topology-paired-before-javascript-1", + "catalog_tokens_estimated": 805215, + "named_call_arguments_tokens_estimated": 1354, + "reply_tokens_estimated": 49815, + "total_tokens_estimated": 856384, + "calls": 28 + }, + { + "run": "topology-paired-after-javascript-1", + "catalog_tokens_estimated": 805215, + "named_call_arguments_tokens_estimated": 1354, + "reply_tokens_estimated": 49815, + "total_tokens_estimated": 856384, + "calls": 28 + }, + { + "run": "topology-before-small-1", + "catalog_tokens_estimated": 805215, + "named_call_arguments_tokens_estimated": 1118, + "reply_tokens_estimated": 33970, + "total_tokens_estimated": 840303, + "calls": 19 + }, + { + "run": "topology-after-small-1", + "catalog_tokens_estimated": 805215, + "named_call_arguments_tokens_estimated": 1118, + "reply_tokens_estimated": 33970, + "total_tokens_estimated": 840303, + "calls": 19 + } + ] + }, + "mixed_verification": [ + { + "scenario": "native", + "heads_checked": 64, + "pages": 32, + "revised_unknown_id": "unk_057c8f2dbf30ec98851a66ee054366bc6e2e168874581e6f99cb80673d0ff9b8", + "revision_checked": 2, + "stale_update_rejected": true, + "tampered_import_rejected_atomically": true, + "unknowns_after_close": 0, + "old_reference_refused": true + }, + { + "scenario": "javascript", + "heads_checked": 64, + "pages": 32, + "revised_unknown_id": "unk_057c8f2dbf30ec98851a66ee054366bc6e2e168874581e6f99cb80673d0ff9b8", + "revision_checked": 2, + "stale_update_rejected": true, + "tampered_import_rejected_atomically": true, + "unknowns_after_close": 0, + "old_reference_refused": true + } + ], + "cli_parity": [ + { + "scenario": "native", + "exact_json_equal": true, + "exact_bytes_equal": true, + "bytes": 7316, + "sha256_before": "eada9068db3b499da332b2c5226fdd3a48909ebcaa290fd72166325e2f664fca", + "sha256_after": "eada9068db3b499da332b2c5226fdd3a48909ebcaa290fd72166325e2f664fca", + "evidence_id": "ev_521a58e81427bb4b2c01b6a291bdfd7698068ee0eba371696daa9cc5ab32f3f0", + "evidence_links": [ + "ev_14b4582084e425b597bea6246f0df94ab4856ac3ab9502c96551147210ca9c8b" + ] + }, + { + "scenario": "javascript", + "exact_json_equal": true, + "exact_bytes_equal": true, + "bytes": 8876, + "sha256_before": "1dc023e5ece8f9c8cea736cb0db4d4f60c9418f1573e1e46ff0ac55a4273cb5a", + "sha256_after": "1dc023e5ece8f9c8cea736cb0db4d4f60c9418f1573e1e46ff0ac55a4273cb5a", + "evidence_id": "ev_97cac46d5139f6c48c3142798087814dcf36a37a9c99f7253668576bd3ae282a", + "evidence_links": [ + "ev_dd2ffe5ab2faa6e07e422886f64a87f99e35a0a1dce1acb5351b7a81cb2c9a11" + ] + }, + { + "scenario": "small", + "exact_json_equal": true, + "exact_bytes_equal": true, + "bytes": 7181, + "sha256_before": "00dd070690e7b113e0cad73f9f7bcda6f42c515f8fdcc5173e8815bd3748b084", + "sha256_after": "00dd070690e7b113e0cad73f9f7bcda6f42c515f8fdcc5173e8815bd3748b084", + "evidence_id": "ev_3dcbcc5b2002f1e6caf7df005e3fcfd06a3afe14ccd72fff924c8e76d8498914", + "evidence_links": [ + "ev_775d6cc6d0de96402b6e57a4258a0b78c337cfb1b7760aa22c39c3ee450c7dff" + ] + } + ], + "stopped_source_validation": { + "reason": "Started during initial before-javascript-3; stopped only the new owned validation scope, then retried serially. Baseline repetition3 retained but excluded from uncontended exploratory comparisons; all final cohort runs retained.", + "status": "incomplete, shell reported exit241; no payload-completion receipt", + "elapsed_seconds": 17.272, + "family_cpu_seconds_last_observed": 12.204013, + "peak_cgroup_bytes_sampled": 589766656, + "final_scope_counters": null + }, + "exploratory_costs": { + "completed_tasks": 21, + "elapsed_seconds_total": 649.262, + "family_cpu_seconds_total": 599.762599, + "family_peak_bytes_max": 1741795328, + "comparison_status": "Earlier candidate and exploratory inputs; excluded from final comparison, raw individual reports retained in private audit cache.", + "contended_baseline_task": { + "name": "before-javascript-3", + "revision": "2c4338db1fdb7773e05376301c558cff2a0b4b80", + "status": "passed", + "elapsed_seconds": 100.722, + "family_cpu_seconds": 82.656181, + "family_peak_bytes": 1290514432, + "excluded_from_final_comparison": true + } + }, + "runtime_entry_sha256": { + "before": "1586cea18f4d4702c949502cfc143a7dfd2c28414779e190ce8a42fedf90bd74", + "after": "e05c70bed21c680445a5c6e314a16cce6f22f3909f93388bf602338062e63d1a" + }, + "inputs": { + "native": { + "bundle_sha256": "180c6fe6d3706d9272c8bb33503c2dca6f145d5061e102e3457dd5f2d754b5c9", + "bundle_bytes": 1745224, + "source_evidence_id": "ev_14b4582084e425b597bea6246f0df94ab4856ac3ab9502c96551147210ca9c8b" + }, + "javascript": { + "bundle_sha256": "747ff602093a24d4876e3eb47da3f7ef68716a10ac7c09938f43c3be5c33e31d", + "bundle_bytes": 21058430, + "source_evidence_id": "ev_dd2ffe5ab2faa6e07e422886f64a87f99e35a0a1dce1acb5351b7a81cb2c9a11" + }, + "small": { + "bundle_sha256": "44ca561c2455706897d2ae8f51f45693e7276badd5e8454ec31d8c1b0cfb77eb", + "bundle_bytes": 73051, + "source_evidence_id": "ev_775d6cc6d0de96402b6e57a4258a0b78c337cfb1b7760aa22c39c3ee450c7dff" + } + }, + "validation": { + "source": { + "executed": "130 source tests across5files; check:fast; build:cached", + "elapsed_seconds": 334.264, + "family_cpu_seconds": 326.441674, + "family_peak_bytes": 4487696384, + "payload_exit_code": 0, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + }, + "compiled_boundary": { + "executed": "36 CLI/MCP tests across4files", + "elapsed_seconds": 93.343, + "family_cpu_seconds": 91.926502, + "family_peak_bytes": 2190270464, + "payload_exit_code": 0, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + }, + "full_checks": { + "source": "3930a640fb8387cbd3079eee13fb90f82d1881dc", + "executed": "npm run check; npm run docs:check (five README consistency tests)", + "elapsed_seconds": 45.552, + "family_cpu_seconds": 43.309041, + "family_peak_bytes": 1127768064, + "payload_exit_code": 0, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + }, + "integration_current": { + "source": "3930a640fb8387cbd3079eee13fb90f82d1881dc", + "executed": "npm run test:focused -- three latest-main integration files; default test deadlines; 27 passed", + "elapsed_seconds": 62.437, + "family_cpu_seconds": 61.506209, + "family_peak_bytes": 1846538240, + "payload_exit_code": 0, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + } + }, + "comparison_probe": { + "scope": "Candidate-only internal JSON-owner regression, not baseline public-provider reproduction. Reflect.ownKeys instrumentation aborts at10000 inspections rather than consuming exponential CPU.", + "objects_per_graph": 27, + "before_candidate": "e30a94a4de6a3c4ec7c577c1551d2b448d82081e", + "before_inspections": 10001, + "before_status": "probe inspection budget exhausted", + "after_candidate": "646312dad75e2cb2becb0dd4139b99a1c04d6bcc", + "after_inspections": 54, + "after_borrowed": true + }, + "main_integration": { + "main_commit": "bb212ffecb47e129cc569b9867b2b6e7c9b65685", + "merge_commit": "71e0b2739a9cf9903e1cfa63dc5a5b576040fea7", + "production_files_unchanged_from_measured_candidate": true, + "scope": "Historical integration at71e0b2739 with production source646312dad; all attempts retained. Current source-specific checks are listed separately.", + "attempts": [ + { + "case": "initial_feature", + "entrypoint": "npm run test:focused", + "test_timeout_override_ms": null, + "tests_passed": 26, + "tests_failed": 1, + "tests_skipped": 0, + "payload_exit_code": 1, + "elapsed_seconds": 139.805, + "family_cpu_seconds": 125.252141, + "family_peak_bytes": 1994223616, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + }, + { + "case": "isolated_main", + "entrypoint": "vitest run; one named test", + "test_timeout_override_ms": null, + "tests_passed": 1, + "tests_failed": 0, + "tests_skipped": 10, + "payload_exit_code": 0, + "elapsed_seconds": 25.96, + "family_cpu_seconds": 25.502365, + "family_peak_bytes": 1221394432, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + }, + { + "case": "direct_main", + "entrypoint": "vitest run; three files", + "test_timeout_override_ms": null, + "tests_passed": 27, + "tests_failed": 0, + "tests_skipped": 0, + "payload_exit_code": 0, + "elapsed_seconds": 68.868, + "family_cpu_seconds": 68.020337, + "family_peak_bytes": 1605603328, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + }, + { + "case": "extended_feature", + "entrypoint": "vitest run; three files", + "test_timeout_override_ms": 120000, + "tests_passed": 27, + "tests_failed": 0, + "tests_skipped": 0, + "payload_exit_code": 0, + "elapsed_seconds": 75.866, + "family_cpu_seconds": 75.193788, + "family_peak_bytes": 1937674240, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + }, + { + "case": "exact_main", + "entrypoint": "npm run test:focused", + "test_timeout_override_ms": null, + "tests_passed": 27, + "tests_failed": 0, + "tests_skipped": 0, + "payload_exit_code": 0, + "elapsed_seconds": 66.443, + "family_cpu_seconds": 65.304325, + "family_peak_bytes": 2055409664, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + }, + { + "case": "exact_feature", + "entrypoint": "npm run test:focused", + "test_timeout_override_ms": null, + "tests_passed": 27, + "tests_failed": 0, + "tests_skipped": 0, + "payload_exit_code": 0, + "elapsed_seconds": 66.878, + "family_cpu_seconds": 66.012242, + "family_peak_bytes": 1907380224, + "payload_cleanup_verified": true, + "oom_events_at_payload_exit": 0 + } + ], + "measured_production_commit": "646312dad75e2cb2becb0dd4139b99a1c04d6bcc" + }, + "small_cache_diagnostic": { + "scope": "Additional real public MCP run with return-preserving WeakSet method instrumentation. Timings excluded from before/after comparison. Owner identified from captured add stack; raw trace retained privately.", + "format_owner": "safeParseOwnedBinaryLayout", + "source_objects": 1, + "successful_admissions": 1, + "misses": 1, + "hits": 8, + "queries": 9, + "original_records_preserved": true, + "closed_session_cleared": true, + "small_workflow_latency_regression_retained": true, + "measured_production_commit": "646312dad75e2cb2becb0dd4139b99a1c04d6bcc" + }, + "previous_checkpoints": [ + { + "production_commit": "e30a94a4de6a3c4ec7c577c1551d2b448d82081e", + "report_commit": "3f271efdef8d71020bdcfd7b2d3d9817bf0a6ad8", + "report_url": "https://github.com/morluto/rea/blob/3f271efdef8d71020bdcfd7b2d3d9817bf0a6ad8/experiments/analysis-efficiency/immutable-parent-validation.json", + "completed_paired_tasks": 18, + "elapsed_seconds_total": 498.234, + "family_cpu_seconds_total": 477.513448, + "family_peak_bytes_max": 1519980544, + "scope": "Historical candidate before shared-node comparison memoization; every result retained, including slow npm repetition3. Not the final comparison." + }, + { + "production_commit": "646312dad75e2cb2becb0dd4139b99a1c04d6bcc", + "report_commit": "fc55be1e05555c6b295209ef5f354617f663b8f8", + "report_url": "https://github.com/morluto/rea/blob/fc55be1e05555c6b295209ef5f354617f663b8f8/experiments/analysis-efficiency/immutable-parent-validation.json", + "completed_paired_tasks": 18, + "elapsed_seconds_total": 483.511, + "family_cpu_seconds_total": 458.990461, + "family_peak_bytes_max": 1598119936, + "scope": "Historical candidate before shared-node comparison memoization; every result retained, including slow npm repetition3. Not the final comparison." + } + ], + "review_normalization_fix": { + "comment": "https://github.com/morluto/rea/pull/4962#discussion_r4305886491", + "before_source": "646312dad75e2cb2becb0dd4139b99a1c04d6bcc", + "before_pr_head": "fc55be1e05555c6b295209ef5f354617f663b8f8", + "expected_failing_regressions": 4, + "after_source": "3930a640fb8387cbd3079eee13fb90f82d1881dc", + "source_tests_passed": 130, + "structural_fix": "Two-way node identity mapping visits preserved descendants; already-owned JavaScript graph path keeps parsed metadata and avoids whole-graph comparison. Only the expensive imported fallback admits the full-result cache.", + "red_probe_cost": { + "elapsed_seconds": 5.332, + "family_cpu_seconds": 4.412336, + "family_peak_bytes": 438231040, + "payload_exit_code": 1, + "payload_cleanup_verified": true, + "expected_failures": 4 + }, + "current_dag_probe": { + "source": "3930a640fb8387cbd3079eee13fb90f82d1881dc", + "objects_per_graph": 27, + "inspections": 54, + "borrowed": true + } + }, + "resource_stops": [ + { + "task": "topology-paired-after-javascript-3", + "status": "incomplete", + "reason": "host_available_memory_early_stop", + "elapsed_seconds": 15.048, + "family_cpu_seconds_last_observed": 14.048702, + "peak_cgroup_bytes_sampled": 947695616, + "payload_completion_counters": null, + "owned_scope_after_stop": "inactive/dead", + "replacement": "fresh JavaScript pair4, after then before; final pairs1,2,4" + }, + { + "task": "topology-before-small-3", + "status": "incomplete", + "reason": "host_available_memory_early_stop", + "elapsed_seconds": 11.83, + "family_cpu_seconds_last_observed": 11.535779, + "peak_cgroup_bytes_sampled": 947818496, + "payload_completion_counters": null, + "replacement": "fresh small pair4 after memory recovered; final small pairs1,2,4" + } + ], + "unpaired_completed_attempts": [ + { + "task": "topology-paired-before-javascript-3", + "status": "passed", + "original_records_preserved": true, + "elapsed_seconds": 38.862, + "family_cpu_seconds": 38.570479, + "family_peak_bytes": 1390338048, + "reason_not_in_paired_summary": "Its paired candidate was resource-stopped; kept separately, with both sides rerun as pair4." + } + ] +} diff --git a/experiments/analysis-efficiency/immutable-parent-validation.md b/experiments/analysis-efficiency/immutable-parent-validation.md new file mode 100644 index 0000000000..b4bc0bc26b --- /dev/null +++ b/experiments/analysis-efficiency/immutable-parent-validation.md @@ -0,0 +1,249 @@ +# Reuse successful immutable parent validation + +This checkpoint addresses [#4810](https://github.com/morluto/rea/issues/4810), +found in the [retained-investigation audit](retained-investigation-workflows.md). +It changes the shared format interpretation used by CLI and MCP views, without +adding a tool, changing a schema, projecting away source data, or rerunning a +provider. + +## Question, ownership, and compatibility + +The task resumes authentic exported Evidence in a fresh production stdio MCP +server, discovers its source, reads six original symbols or modules in two-row +pages, repeats that traversal three times, queries the existing Unknowns, +exports the investigation, and closes it. The original source records must +survive the round trip exactly. Pages are compared to original retained facts. +Independent ELF byte decoding checks symbol names, values, and ranges; JavaScript +checks cover selected source bytes and the `lib/cli.js` module identity, rather +than independently proving the entire application inventory or semantic graph. + +Imported JavaScript graphs lack the producer's realm-private format proofs. +Previously each view repeated full schema, graph/hash, and cross-graph binding +validation. Native layout views likewise parsed the entire layout for every +page. An immutable Evidence root alone was insufficient: its selected normalized +result did not carry the JSON owner's private sealing proof. + +The JSON owner now transfers that proof to a selected own member without another +walk or per-node registration. Native layouts and imported JavaScript results +record a **successful full +validation of the exact immutable source** in their owning WeakSets. Already +owned JavaScript graphs keep the existing metadata normalization and binding +checks, returning parsed metadata with their original graph references. That +path avoids rewalking a large owned graph just to borrow its wrapper. There is +no second +retained parsed graph, strong cache, cross-process cache, or live-state cache. +The schema implementation is fixed for the process; a different source object +must validate independently. Weak entries do not keep Evidence alive, and session +lookup continues to gate access after close. + +Cache admission follows full validation and requires that parsing preserved the +source representation. A shared iterative JSON comparison checks own data keys, +scalar values, prototypes, and enumerability without calling accessors. Object +member order is immaterial; array order remains exact. Two-way identity maps +visit preserved descendants once and reject source-node splits or merges. Both +comparison maps are temporary and discarded on return. Parser defaults, +hidden-field removal, or other normalization retain the parser's +result and do not cache the source. Writable/configurable descriptor differences +are immaterial: the source is sealed while the temporary parse is mutable. +The JSON sealing boundary rejects proxies before reflection, so arbitrary +mutable proxy behavior cannot acquire an immutable proof. Mutable, merely +Object.freeze'd, failed, and abandoned inputs do not qualify for successful +format reuse. + +No Evidence identity, source linkage, coverage, unknown, omission, error, or +caller-selected page changes are intended. Complete source Evidence remains +accessible through the existing contracts. The first imported view still pays for full +validation and the representation comparison; a one-off CLI invocation cannot +benefit from a cache in a previous process. + +## Inputs and measurement scope + +Baseline: `2c4338db1fdb7773e05376301c558cff2a0b4b80` (REA 6.6.0). +Node 24.18.0, npm 11.16.0, Linux x86-64 (AMD EPYC 7B12, kernel 6.1.0-53-cloud). +The ELF producer reports pwntools 4.15.0, pyelftools 0.33, and Unicorn 2.1.2. +The baseline's built runtime was preserved +before production changes. Candidate and baseline use the same locked packages. + +- SQLite `/usr/lib/x86_64-linux-gnu/libsqlite3.so.0.8.6`: SHA-256 + `2e6eef9a727f081f0d453b4e5e6cbd8b9ef8b6f86cbf7681cbad444d3b0b55c8`; + 26 sections, 1,479 symbols, 2,963 relocations. Original history: 1,745,224 bytes. +- Shipped npm 11.16.0: inventory digest + `f8e576a7d162542c616168b25a95a5f62b9accf29467cc37e149905ad4b8e603`; + five explicitly selected paths (`package.json`, `bin/npm-cli.js`, `lib/cli.js`, + `lib/cli/entry.js`, `lib/commands/install.js`), 2,822 application nodes, + 1,796 semantic nodes, 13,682 semantic unknowns. The 1,416 unselected files stay + unknown. Original history: 21,058,430 bytes. +- Small case `/usr/bin/true`: SHA-256 + `c79bf44242829108e323378531f4ac839513ca1fba45efd6583643526e1e9fd2`; + 31 sections, 53 symbols, 66 relocations. Produced through the real production + `inspect_binary_layout` MCP operation. An independent raw ELF reader checked + every symbol name, value, and source range plus section/relocation counts. + +The two larger histories were produced and independently checked in the linked +previous audit. They are historical Evidence, not a claim of fresh provider +analysis during a resumed view. All tasks here are static; no target executable +is run. + +Each completed comparison includes startup, the complete 207-tool catalog, +import, queries, oracle checks, export, close, receipt writing, and SDK cleanup. +RPC timings exclude subsequent client-side oracle/receipt work. Fresh servers +separate first-view validation from later pages; OS file caches are uncontrolled. +Runs alternate baseline/candidate order. One CPU, 8 GiB hard family memory, +7 GiB early stop, no swap, and a 192-task ceiling bound each serial task. +Resource counters are observed at payload exit while the observer remains alive; +final whole-scope counters are unavailable. Kernel cgroup peaks include charged +file cache and descendants, rather than just the server's JS heap. + +## Results and executed validation + +Candidate production code: `3930a640fb8387cbd3079eee13fb90f82d1881dc`. +The [curated receipt](immutable-parent-validation.json) retains every final run, +offline token estimates, compatibility checks, and costs of previous attempts. +Values below are medians with min–max ranges across three fresh alternating pairs. + +| Source | First page, before → after (ms) | Eight later pages, before → after (ms) | +| ---------- | ----------------------------------------------------- | -------------------------------------------------- | +| SQLite ELF | 183.56 [170.13–244.27] → 209.05 [203.11–216.88] | 469.89 [393.47–517.92] → 85.04 [65.10–89.01] | +| npm | 2441.08 [1681.26–2479.69] → 1978.73 [1864.89–2720.54] | 15534.41 [10378.13–16864.91] → 82.69 [75.19–84.13] | +| Small ELF | 133.33 [100.81–242.58] → 226.60 [111.79–556.22] | 122.93 [76.38–299.47] → 62.58 [57.69–96.24] | + +| Source / runtime | Whole elapsed (s) | Family CPU (s) | Kernel family peak (MiB) | +| ------------------- | ------------------- | ------------------- | ------------------------ | +| SQLite ELF / before | 19.08 [15.26–20.00] | 18.75 [15.01–19.55] | 1013.8 [1009.5–1058.6] | +| SQLite ELF / after | 14.38 [14.06–17.25] | 14.09 [13.77–17.00] | 984.0 [957.1–1047.1] | +| npm / before | 39.63 [31.21–40.86] | 39.27 [30.87–40.49] | 1338.0 [1301.0–1543.7] | +| npm / after | 20.71 [20.01–23.02] | 20.50 [19.73–22.23] | 1286.8 [1250.6–1344.9] | +| Small ELF / before | 14.30 [12.00–17.59] | 13.94 [11.77–17.31] | 1013.2 [937.8–1017.4] | +| Small ELF / after | 12.31 [11.90–16.25] | 12.04 [11.71–16.07] | 956.3 [931.4–1007.5] | + +| Source | Paired warm-read ratio, before / after | Paired first-view change, after − before (ms) | +| ---------- | -------------------------------------- | --------------------------------------------- | +| SQLite ELF | 5.53× [4.42–7.96] | +25.49 [-41.16–+46.75] | +| npm | 184.65× [125.50–224.30] | -462.34 [-614.80–+1039.29] | +| Small ELF | 1.28× [1.22–5.19] | +10.98 [-15.97–+422.88] | + +Each pair matches the before/after runs with the same repetition number. +The paired effect can differ from the ratio of marginal medians when the host +speed changes. Positive first-view changes mean higher latency. + +The repeated-read benefit is measured per source and pair above. First-view +validation +also performs the representation comparison and can be more expensive. Whole +task, CPU and memory figures above are measured SDK workflow costs, not a +general provider-analysis or agent-investigation speedup. The previous e30 +checkpoint had a slow npm repetition on both sides, including a 65-second +candidate task; its cause was not profiled. The original checkpoint and every +fresh run remain retained. The previous `646312dad` checkpoint also had slower +small-case warm reads in two of three pairs (135.54 → 373.14 ms marginal +medians). Those negative results remain available in its linked historical +receipt; the current cohort improves all three small pairs, without establishing +a general small-input or whole-task benefit. No model-token saving, general memory improvement, +or complex-software investigation advantage is established. + +All 18 fresh workflow runs preserve original records and canonical answer +digests. Every corresponding query's structured result and complete catalog +is equal. The three public CLI source pairs are also byte-for-byte identical. +The two additional mixed-history tasks preserve all 97 original records, check +64 follow-up heads over 32 pages and revision two, reject stale updates and +tampered imports atomically, clear Unknowns on close, and refuse old references. + +Offline `tiktoken` 0.12.0 / `cl100k_base` estimates for one pass over the complete +catalog, named-call arguments and replies are **unchanged** within each pair: +849,736 native, 856,384 JavaScript, and 840,303 small-case tokens. The catalog +contributes 805,215 tokens alone. +These are raw transport serialization estimates, not actual model context or +usage; initialization/framing, prompts, client discovery, rendering/replay, +and continuations are unmeasured. Metadata paths differ from the historical +checkpoint; compare equivalent runs within each checkpoint. + +Executed checks at the final production code: + +- `npm run check` and `npm run docs:check`: pass, including five README + consistency tests. +- Three latest-main integration files through `npm run test:focused`, using + default deadlines: 27 tests pass at the current production source. +- 130 source tests across immutable JSON, native/JavaScript format validation, + and the shared application view service; `npm run check:fast` and + `npm run build:cached` pass. +- Four compiled CLI/MCP files (`evidenceBundleInline`, `serverUnknownsMcp`, + `analysisViewMcp`, and `analysisViewCli`): 36 tests pass. +- Real public stdio MCP: 18 fresh paired tasks and two additional mixed tasks; + public CLI: six invocations, with three exact output pairs. These corpus + workflows and four-file boundary checks have exit zero, verified payload + cleanup and zero observed payload-exit OOM events. + +Internal and automated review reproduced **candidate-only** defects before integration. +Generic deep equality ignored hidden fields removed by parsing; the JSON owner +comparison now preserves that normalization. Review also reproduced aliases crossing +preserved subtrees and cloned metadata; four regressions failed against the prior +source. The comparer now visits preserved descendants with two-way identity mapping. +Already-owned JavaScript graphs keep their parsed-metadata fast path, so the generic +comparison and full-result cache only apply to the expensive imported fallback. Its +initial iterative traversal +also repeatedly expanded shared nodes: a valid 27-object graph exhausted an +instrumented 10,000-inspection budget. Tracking source-to-parsed node pairs +reduces that same compiled probe to 54 inspections and successful borrowing. +A parser that splits or merges source nodes keeps its parsed result rather than +expanding repeated paths. Deterministic regressions enforce the inspection +budget instead of relying on timing or letting a regression consume exponential +CPU. These are internal JSON-owner probes, not claimed baseline public-provider +reproductions. + +The initial exploratory cohort and historical e30 paired checkpoint are not +the final comparison. One earlier source-check attempt was mistakenly started +during an initial baseline run; the newly started owned scope was stopped after +17.27 s and that check remains incomplete (exit 241). Observed counters, the +contended baseline, subsequent successful retries, and prior checkpoints remain +retained; no OOM or terminal-counter claim is inferred from that stopped task. +Two later tasks stopped because the host-available-memory guard reported its +early-stop condition: candidate JavaScript repetition three and baseline small +repetition three. Both incomplete attempts and the unmatched completed +JavaScript baseline remain separate. After memory recovered, both sides were +rerun as fresh pair four; final JavaScript and small samples use pairs 1, 2, 4. +Neither interrupted task is a completed comparison. The guard later gained +read-only host-memory/cgroup receipt fields; all resource thresholds and its +observer/stop logic remained unchanged. + +Real Ghidra, Hopper, JEB, fresh JavaScript producer performance, and actual +agent trials are outside this checkpoint. Private WeakSet metadata/heap retention +was not measured independently; the absence of a strong retained parsed graph +is source-derived. The comparison maps exist only during the first +interpretation; measured family +peaks include their temporary cost. + +## Latest-main integration and an unexplained timeout + +Merge `71e0b2739` brings in main `bb212ffec`'s three test/config/guide fixes. +The current source passes all 27 tests in those three integration files through +`npm run test:focused` with the default deadlines. The earlier `646312dad` +checkpoint initially passed 26 and timed out the live identity/catalog test at +30 seconds. Matching official-command controls subsequently passed all 27 on +both unmodified main and that candidate. Direct/extended-deadline diagnostics +are recorded separately. Their passing results do not establish the cause of +the original timeout, which remains preserved and unexplained. No test-policy +or product file was changed to obtain a pass. + +## Reproduction and retained evidence + +Use `retained-investigation.mjs` from this directory with an authentic exported +bundle and its original source Evidence ID. Build both selected runtime revisions +with the pinned toolchain, preserving a separate baseline. For each runtime run: + +```sh +python3 experiments/analysis-efficiency/resource-run.py --observe-payload-exit -- \ + node experiments/analysis-efficiency/retained-investigation.mjs \ + --runtime-root /absolute/runtime --revision REVISION \ + --bundle /absolute/history.json --evidence-id ORIGINAL_EVIDENCE_ID \ + --scenario native --delivery focused --output /absolute/new-receipt-directory +``` + +Use `--scenario javascript` for the npm history, and `--verify-mixed` with an +actual mixed investigation. Reuse the same bytes and source identity on both +sides. Do not regenerate parent identities or synthesize bundle records. +The checked-in wrapper defaults to a 4 GiB hard / 3 GiB early stop. This +checkpoint used a private resource-only copy with 8 GiB / 7 GiB thresholds, +a 16 GiB available-memory start floor and 8 GiB host early stop. Its source +and unchanged observer hashes are recorded in the curated receipt. This local +resource policy does not change the public workflow or global wrapper defaults. +Raw histories, transcripts, oracle outputs, and resource logs stay in the private +local audit cache; only curated receipts are committed. diff --git a/src/application/analysisView/AnalysisViewService.test.ts b/src/application/analysisView/AnalysisViewService.test.ts index 302302ee21..958db14b0a 100644 --- a/src/application/analysisView/AnalysisViewService.test.ts +++ b/src/application/analysisView/AnalysisViewService.test.ts @@ -25,6 +25,36 @@ import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js"; import { err, ok } from "../../domain/result.js"; import { EvidenceLedger } from "../investigation/EvidenceLedger.js"; import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js"; +import { isImmutableJsonSnapshot } from "../../domain/immutableJson.js"; + +it.each(["layout", "javascript"])( + "carries the retained %s source snapshot into repeatable shared views", + (kind) => { + const parent = + kind === "layout" + ? analysisViewLayoutEvidence() + : analysisViewJavaScriptEvidence(); + const ledger = new EvidenceLedger(); + const recorded = ledger.record(parent); + if (!recorded.ok) throw recorded.error; + const borrowed = ledger.forAnalysis(parent.evidence_id)!; + const original = JSON.stringify(borrowed); + const input = { + source: { kind: "retained-evidence", evidence_id: parent.evidence_id }, + view: { kind: "summary" }, + } as const; + const first = inspectAnalysisView(input, (id) => ledger.forAnalysis(id)); + if (!first.ok) throw first.error; + const repeated = inspectAnalysisView(input, (id) => ledger.forAnalysis(id)); + if (!repeated.ok) throw repeated.error; + expect(repeated.value.normalized_result).toEqual( + first.value.normalized_result, + ); + expect(isImmutableJsonSnapshot(borrowed.normalized_result)).toBe(true); + expect(JSON.stringify(borrowed)).toBe(original); + expect(ledger.forAnalysis(parent.evidence_id)).toBe(borrowed); + }, +); it.each(["/fixtures/unknown.exe", ""])( "preserves recorded empty native identity fields: path=%j", diff --git a/src/application/analysisView/AnalysisViewService.ts b/src/application/analysisView/AnalysisViewService.ts index 5016b357e3..1bc09eae01 100644 --- a/src/application/analysisView/AnalysisViewService.ts +++ b/src/application/analysisView/AnalysisViewService.ts @@ -21,7 +21,10 @@ import { type InspectAnalysisViewInput, } from "../../domain/analysisView/analysisView.js"; import { jsonValueSchema } from "../../domain/jsonValue.js"; -import { isImmutableJsonSnapshot } from "../../domain/immutableJson.js"; +import { + immutableJsonSnapshotMember, + isImmutableJsonSnapshot, +} from "../../domain/immutableJson.js"; import { err, ok, type Result } from "../../domain/result.js"; import { WebNetworkApiSurfaceAccumulator } from "../../domain/analysisView/WebNetworkApiSurfaceAccumulator.js"; import { admitAnalysisViewData } from "../../domain/analysisView/analysisViewDataAdmission.js"; @@ -70,7 +73,9 @@ export const inspectAnalysisViewValidated = ( { evidenceId: parent.evidence_id, operation: parent.operation, - normalizedResult: parent.normalized_result, + normalizedResult: + immutableJsonSnapshotMember(parent, "normalized_result") ?? + parent.normalized_result, // Sealing an Evidence root deep-freezes it; only the root is registered. sealedResult: isImmutableJsonSnapshot(parent), subject: parent.subject, diff --git a/src/domain/analysisView/analysisView.ts b/src/domain/analysisView/analysisView.ts index c6693ba24d..fce6620604 100644 --- a/src/domain/analysisView/analysisView.ts +++ b/src/domain/analysisView/analysisView.ts @@ -10,10 +10,7 @@ import { } from "./javascriptFactsView.js"; import { javaScriptSourceSelectionSchema } from "../javascript/javascriptSourceSelection.js"; -import { - AnalysisInputError, - AnalysisUnsupportedTargetError, -} from "../analysisErrorCore.js"; +import { AnalysisUnsupportedTargetError } from "../analysisErrorCore.js"; import { INSPECT_ANALYSIS_VIEW_OPERATION, analysisViewInputError, @@ -24,7 +21,7 @@ import { evidenceSchema, type Evidence } from "../evidence.js"; import { jsonObjectSchema, jsonValueSchema } from "../jsonValue.js"; import { err, ok, type Result } from "../result.js"; import type { AnalysisError } from "../analysisErrorBase.js"; -import { binaryLayoutSchema } from "../native/binaryLayout.js"; +import { safeParseOwnedBinaryLayout } from "../native/binaryLayout.js"; import { functionDossierSchema } from "../hopperValues.js"; import { artifactGraphManifestSchema, @@ -741,8 +738,8 @@ const parseJavaScriptParent = ( input: unknown, ): Result => { try { - // Only the existing realm-private graph proofs permit reuse. Imported or - // merely frozen graphs still receive full canonical validation. + // The format owner authenticates imported results completely before reuse; + // mutable or merely frozen inputs still receive canonical validation. const steps = parseOwnedJavaScriptApplicationAnalysisSteps(input); let step = steps.next(); while (!step.done) step = steps.next(); @@ -841,7 +838,7 @@ export const projectAnalysisView = ( return projected.ok ? ok(sealAnalysisView(projected.value)) : projected; } if (parent.operation === "inspect_binary_layout") { - const layout = binaryLayoutSchema.safeParse(parent.normalizedResult); + const layout = safeParseOwnedBinaryLayout(parent.normalizedResult); if (!layout.success) return err( analysisViewInputError([ diff --git a/src/domain/immutableJson.test.ts b/src/domain/immutableJson.test.ts index 16e524a96e..068192f104 100644 --- a/src/domain/immutableJson.test.ts +++ b/src/domain/immutableJson.test.ts @@ -1,12 +1,55 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import type { JsonValue } from "./jsonValue.js"; import { freezeJsonSnapshot, + canBorrowParsedJsonSnapshot, + immutableJsonSnapshotMember, isImmutableJsonSnapshot, } from "./immutableJson.js"; describe("owned immutable JSON snapshots", () => { + it("borrows only the selected own child of a completed sealed root", () => { + const value = freezeJsonSnapshot({ + selected: { values: [1, 2] }, + other: { value: 3 }, + absent: null, + }); + expect(isImmutableJsonSnapshot(value.selected)).toBe(false); + expect(immutableJsonSnapshotMember(value, "selected")).toBe(value.selected); + expect(isImmutableJsonSnapshot(value.selected)).toBe(true); + expect(isImmutableJsonSnapshot(value.other)).toBe(false); + expect(immutableJsonSnapshotMember(value.selected, "values")).toBe( + value.selected.values, + ); + expect(isImmutableJsonSnapshot(value.selected.values)).toBe(true); + expect(immutableJsonSnapshotMember(value, "absent")).toBeNull(); + expect(immutableJsonSnapshotMember(value, "missing")).toBeUndefined(); + expect(immutableJsonSnapshotMember(value, "toString")).toBeUndefined(); + expect(Reflect.set(value.selected.values, 0, 9)).toBe(false); + }); + + it("does not borrow children from unowned roots or invoke their accessors", () => { + let accessed = false; + const child = Object.freeze({ value: 1 }); + const root = Object.freeze({ child }); + expect(immutableJsonSnapshotMember(root, "child")).toBeUndefined(); + expect(isImmutableJsonSnapshot(child)).toBe(false); + expect( + immutableJsonSnapshotMember( + { + get child() { + accessed = true; + return child; + }, + }, + "child", + ), + ).toBeUndefined(); + expect(accessed).toBe(false); + expect(immutableJsonSnapshotMember(null, "child")).toBeUndefined(); + }); + it("seals reachable data and preserves repeated references", () => { const shared = { values: [1, "observed"] }; const value = { left: shared, right: shared }; @@ -61,4 +104,166 @@ describe("owned immutable JSON snapshots", () => { "JSON values", ); }); + + it("rejects proxy snapshots before reflecting on their mutable behavior", () => { + let reflected = false; + const proxy = new Proxy( + { value: 1 }, + { + getPrototypeOf() { + reflected = true; + throw new Error("must not reflect on a proxy"); + }, + }, + ); + const root = { child: proxy }; + for (const input of [proxy, root]) { + expect(() => freezeJsonSnapshot(input)).toThrow("proxies"); + expect(isImmutableJsonSnapshot(input)).toBe(false); + } + expect(Object.isFrozen(root)).toBe(false); + expect(reflected).toBe(false); + }); +}); + +describe("borrowing a parsed JSON snapshot", () => { + it("accepts reordered object fields but preserves arrays and exact scalars", () => { + const source = freezeJsonSnapshot({ second: [true, null], first: -0 }); + expect( + canBorrowParsedJsonSnapshot(source, { first: -0, second: [true, null] }), + ).toBe(true); + expect( + canBorrowParsedJsonSnapshot(source, { first: 0, second: [true, null] }), + ).toBe(false); + expect( + canBorrowParsedJsonSnapshot(source, { first: -0, second: [null, true] }), + ).toBe(false); + expect( + canBorrowParsedJsonSnapshot(Object.freeze({ value: 1 }), { value: 1 }), + ).toBe(false); + }); + + it("preserves parser defaults, hidden-field removal and enumerability changes", () => { + const source = freezeJsonSnapshot({ observed: 1 }); + expect( + canBorrowParsedJsonSnapshot(source, { observed: 1, defaulted: 0 }), + ).toBe(false); + const hidden = { observed: 1 }; + Object.defineProperty(hidden, "unmodeled", { value: 2, enumerable: false }); + freezeJsonSnapshot(hidden); + expect(canBorrowParsedJsonSnapshot(hidden, { observed: 1 })).toBe(false); + const nonenumerable = { observed: 1 }; + Object.defineProperty(nonenumerable, "observed", { enumerable: false }); + freezeJsonSnapshot(nonenumerable); + expect(canBorrowParsedJsonSnapshot(nonenumerable, { observed: 1 })).toBe( + false, + ); + }); + + it("does not evaluate accessors introduced by a parsed representation", () => { + let accessed = false; + const source = freezeJsonSnapshot({ observed: 1 }); + expect( + canBorrowParsedJsonSnapshot(source, { + get observed() { + accessed = true; + return 1; + }, + }), + ).toBe(false); + expect(accessed).toBe(false); + }); + + it("declines parser-introduced proxies without reflecting on them", () => { + let reflected = false; + const parsed = new Proxy( + { value: 1 }, + { + getPrototypeOf() { + reflected = true; + throw new Error("must not reflect on a proxy"); + }, + }, + ); + expect( + canBorrowParsedJsonSnapshot(freezeJsonSnapshot({ value: 1 }), parsed), + ).toBe(false); + expect(reflected).toBe(false); + }); + + it("compares deeply nested snapshots without depending on the call stack", () => { + let source: JsonValue = 1; + let parsed: JsonValue = 1; + for (let depth = 0; depth < 6_000; depth += 1) { + source = { child: source }; + parsed = { child: parsed }; + } + freezeJsonSnapshot(source); + expect(canBorrowParsedJsonSnapshot(source, parsed)).toBe(true); + }); + + it("compares shared snapshot graphs without repeatedly expanding their paths", () => { + let source: JsonValue = { value: 1 }; + for (let depth = 0; depth < 26; depth += 1) + source = { left: source, right: source }; + const parsed = structuredClone(source); + freezeJsonSnapshot(source); + const ownKeys = Reflect.ownKeys; + let inspections = 0; + const observed = vi + .spyOn(Reflect, "ownKeys") + .mockImplementation((value) => { + // Fail deterministically instead of consuming exponential CPU on regressions. + if (++inspections > 10_000) + throw new Error("Shared JSON comparison exceeded inspection budget"); + return ownKeys(value); + }); + let borrowed: boolean; + try { + borrowed = canBorrowParsedJsonSnapshot(source, parsed); + } finally { + observed.mockRestore(); + } + expect(borrowed).toBe(true); + }); + + it("keeps the parsed result when a parser splits shared source nodes", () => { + const shared = { value: 1 }; + const source = freezeJsonSnapshot({ left: shared, right: shared }); + expect( + canBorrowParsedJsonSnapshot(source, { + left: { value: 1 }, + right: { value: 1 }, + }), + ).toBe(false); + }); + + it.each(["direct", "nested"])( + "detects shared-node splits across a %s preserved subtree", + (kind) => { + const shared = { value: 1 }; + const source = freezeJsonSnapshot({ + preserved: kind === "direct" ? shared : { nested: shared }, + cloned: shared, + }); + expect( + canBorrowParsedJsonSnapshot(source, { + preserved: source.preserved, + cloned: { value: 1 }, + }), + ).toBe(false); + expect(canBorrowParsedJsonSnapshot(source, source)).toBe(true); + }, + ); + + it("keeps the parsed result when a parser merges distinct source nodes", () => { + const source = freezeJsonSnapshot({ + left: { value: 1 }, + right: { value: 1 }, + }); + const shared = { value: 1 }; + expect( + canBorrowParsedJsonSnapshot(source, { left: shared, right: shared }), + ).toBe(false); + }); }); diff --git a/src/domain/immutableJson.ts b/src/domain/immutableJson.ts index 0b68b4e2b6..afe8462212 100644 --- a/src/domain/immutableJson.ts +++ b/src/domain/immutableJson.ts @@ -1,3 +1,5 @@ +import { isProxy } from "node:util/types"; + import type { JsonValue } from "./jsonValue.js"; const immutableSnapshots = new WeakSet(); @@ -14,6 +16,87 @@ export const isImmutableJsonSnapshot = (value: unknown): value is JsonValue => (typeof value === "number" && Number.isFinite(value)) || (typeof value === "object" && immutableSnapshots.has(value)); +/** Borrow one own member of a completed snapshot, carrying its sealing proof. */ +export const immutableJsonSnapshotMember = ( + snapshot: unknown, + key: string, +): JsonValue | undefined => { + if ( + typeof snapshot !== "object" || + snapshot === null || + !immutableSnapshots.has(snapshot) + ) + return undefined; + const descriptor = Object.getOwnPropertyDescriptor(snapshot, key); + if (descriptor === undefined || !Object.hasOwn(descriptor, "value")) + return undefined; + // Completed sealing checked and froze every reachable member. Register only + // the selected child; do not traverse it again or add per-node weak entries. + const member = descriptor.value as JsonValue; + if (typeof member === "object" && member !== null) + immutableSnapshots.add(member); + return member; +}; + +/** Match a completed snapshot to its parsed representation without recursion. */ +export const canBorrowParsedJsonSnapshot = ( + source: unknown, + parsed: Parsed, +): source is Parsed => { + if (!isImmutableJsonSnapshot(source)) return false; + if (Object.is(source, parsed)) return true; + const pending: unknown[] = [source, parsed]; + const compared = new Map(); + const sources = new Map(); + while (pending.length > 0) { + const right = pending.pop(); + const left = pending.pop(); + if ( + typeof left !== "object" || + left === null || + typeof right !== "object" || + right === null + ) { + if (!Object.is(left, right)) return false; + continue; + } + if ( + isProxy(right) || + Object.getPrototypeOf(left) !== Object.getPrototypeOf(right) + ) + return false; + const previous = compared.get(left); + if (previous !== undefined) { + if (previous !== right) return false; + continue; + } + if (sources.has(right)) return false; + // Preserve sharing in both directions, including descendants of an + // identical subtree that may also appear in a separately parsed field. + compared.set(left, right); + sources.set(right, left); + const keys = Reflect.ownKeys(left); + if (keys.length !== Reflect.ownKeys(right).length) return false; + for (const key of keys) { + // A parser may drop hidden fields or normalize enumerability. Generic + // deep equality omits these differences and can borrow unparsed data. + if (typeof key !== "string") return false; + const a = Object.getOwnPropertyDescriptor(left, key); + const b = Object.getOwnPropertyDescriptor(right, key); + if ( + a === undefined || + b === undefined || + !Object.hasOwn(a, "value") || + !Object.hasOwn(b, "value") || + a.enumerable !== b.enumerable + ) + return false; + pending.push(a.value as unknown, b.value as unknown); + } + } + return true; +}; + /** Seal an already parsed, owned JSON snapshot for immutable zero-copy reuse. */ export const freezeJsonSnapshot = (value: T): T => { const pending: SnapshotEntry[] = [{ value, exiting: false }]; @@ -104,6 +187,8 @@ export function* freezeOwnedJsonSnapshotSteps( } const pushSnapshotChildren = (item: object, pending: SnapshotEntry[]): void => { + if (isProxy(item)) + throw new TypeError("Immutable JSON snapshots cannot contain proxies"); const prototype: unknown = Object.getPrototypeOf(item); if ( !Array.isArray(item) && diff --git a/src/domain/immutableJsonSteps.test.ts b/src/domain/immutableJsonSteps.test.ts index 0755e75f06..d426525665 100644 --- a/src/domain/immutableJsonSteps.test.ts +++ b/src/domain/immutableJsonSteps.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { freezeOwnedJsonSnapshotSteps, + immutableJsonSnapshotMember, isImmutableJsonSnapshot, } from "./immutableJson.js"; import { jsonValueValidationIssue, type JsonValue } from "./jsonValue.js"; @@ -23,6 +24,7 @@ describe("cooperative owned snapshot sealing", () => { expect(steps.next().done).toBe(false); expect(Object.isFrozen(value)).toBe(true); expect(isImmutableJsonSnapshot(value)).toBe(false); + expect(immutableJsonSnapshotMember(value, "deferred")).toBeUndefined(); const introduced = { value: 2 }; deferred.child = introduced; expect(complete(steps)).toBe(value); @@ -49,6 +51,7 @@ describe("cooperative owned snapshot sealing", () => { expect(interrupted.next().done).toBe(false); interrupted.return(cancelled); expect(isImmutableJsonSnapshot(cancelled)).toBe(false); + expect(immutableJsonSnapshotMember(cancelled, "values")).toBeUndefined(); }); it("preserves aliases and rejects accessors, sparse values and cycles", () => { @@ -80,4 +83,26 @@ describe("cooperative owned snapshot sealing", () => { value.child.value = Number.NaN; expect(jsonValueValidationIssue(value)).toBe("JSON numbers must be finite"); }); + + it("never authenticates roots containing proxies or invokes their traps", () => { + let reflected = false; + const proxy = new Proxy( + { value: 1 }, + { + getPrototypeOf() { + reflected = true; + throw new Error("must not reflect on a proxy"); + }, + }, + ); + const root = { child: proxy }; + for (const input of [proxy, root]) { + expect(() => complete(freezeOwnedJsonSnapshotSteps(input))).toThrow( + "proxies", + ); + expect(isImmutableJsonSnapshot(input)).toBe(false); + expect(immutableJsonSnapshotMember(input, "child")).toBeUndefined(); + } + expect(reflected).toBe(false); + }); }); diff --git a/src/domain/javascript/javascriptApplicationAnalysis.ts b/src/domain/javascript/javascriptApplicationAnalysis.ts index 7b1055b790..adda5c31ba 100644 --- a/src/domain/javascript/javascriptApplicationAnalysis.ts +++ b/src/domain/javascript/javascriptApplicationAnalysis.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { canBorrowParsedJsonSnapshot } from "../immutableJson.js"; import { javascriptApplicationGraphSchema, @@ -172,21 +173,43 @@ const ownedAnalysisResultShape = applicationAnalysisResultShape.extend({ ), }); +// One successful interpretation per exact immutable source. Weak keys retain +// neither a second graph nor a source beyond its Evidence lifetime. +const validatedImmutableResults = new WeakSet(); +const isValidatedImmutableResult = ( + input: unknown, +): input is JavaScriptApplicationAnalysisResult => + typeof input === "object" && + input !== null && + validatedImmutableResults.has(input); + /** Reuse exact owned graphs while validating metadata and cross-graph bindings. */ export function* parseOwnedJavaScriptApplicationAnalysisSteps( input: unknown, ): Generator { + if (isValidatedImmutableResult(input)) return input; const prepared = ownedAnalysisResultShape.safeParse(input); - if (!prepared.success) - return javascriptApplicationAnalysisResultSchema.parse(input); - const issues: BindingIssue[] = []; - yield* checkApplicationBindingsSteps(prepared.data, { - addIssue: (issue) => { - issues.push(issue); - }, - }); - if (issues.length > 0) throw new z.ZodError(issues); - return prepared.data; + if (prepared.success) { + const issues: BindingIssue[] = []; + yield* checkApplicationBindingsSteps(prepared.data, { + addIssue: (issue) => { + issues.push(issue); + }, + }); + if (issues.length > 0) throw new z.ZodError(issues); + // Keep this existing fast path and its metadata normalization. Rewalking + // already owned graphs just to borrow the wrapper would add full-graph work. + return prepared.data; + } + const result = javascriptApplicationAnalysisResultSchema.parse(input); + // Borrow only if parsing preserved the exact representation. A future schema + // default/normalization must still return its parsed value, without caching. + // Admission follows full validation, including completion of yielding checks. + if (canBorrowParsedJsonSnapshot(input, result)) { + validatedImmutableResults.add(input); + return input; + } + return result; } /** Parsed public JavaScript application analysis request. */ diff --git a/src/domain/javascript/javascriptApplicationAnalysisSteps.test.ts b/src/domain/javascript/javascriptApplicationAnalysisSteps.test.ts index 6356f92c0e..d754a9897a 100644 --- a/src/domain/javascript/javascriptApplicationAnalysisSteps.test.ts +++ b/src/domain/javascript/javascriptApplicationAnalysisSteps.test.ts @@ -56,6 +56,101 @@ const issuesFrom = (action: () => unknown) => { throw new Error("Expected complete result validation to reject the input"); }; +describe("immutable JavaScript result reuse", () => { + it("preserves parser copies shared across owned graphs and result metadata", () => { + const owned = ownedExample(); + const input = freezeJsonSnapshot({ + ...owned, + limitations: owned.graph.limitations, + }); + for (let repeat = 0; repeat < 2; repeat += 1) { + const parsed = complete( + parseOwnedJavaScriptApplicationAnalysisSteps(input), + ); + expect(parsed).not.toBe(input); + expect(parsed.graph).toBe(input.graph); + expect(parsed.limitations).toEqual(input.graph.limitations); + expect(parsed.limitations).not.toBe(parsed.graph.limitations); + } + }); + + it.each(["hidden-extra", "changed-enumerability"])( + "preserves parsed results when the source has %s normalization", + (kind) => { + const input = example(); + if (kind === "hidden-extra") + Object.defineProperty(input, "not_in_parsed_result", { + value: { unvalidated: true }, + enumerable: false, + }); + else Object.defineProperty(input, "input_path", { enumerable: false }); + freezeJsonSnapshot(input); + const expected = javascriptApplicationAnalysisResultSchema.parse(input); + for (let repeat = 0; repeat < 2; repeat += 1) { + const parsed = complete( + parseOwnedJavaScriptApplicationAnalysisSteps(input), + ); + expect(parsed).not.toBe(input); + expect(Object.hasOwn(parsed, "not_in_parsed_result")).toBe(false); + expect( + Object.getOwnPropertyDescriptor(parsed, "input_path")?.enumerable, + ).toBe( + Object.getOwnPropertyDescriptor(expected, "input_path")?.enumerable, + ); + } + }, + ); + + it("borrows imported immutable results only after complete format validation", () => { + const input = example(); + delete input.statistics.skipped_javascript_files; + freezeJsonSnapshot(input); + const result = complete( + parseOwnedJavaScriptApplicationAnalysisSteps(input), + ); + expect(result).toBe(input); + expect(result).toEqual( + javascriptApplicationAnalysisResultSchema.parse(input), + ); + expect(result.statistics).not.toHaveProperty("skipped_javascript_files"); + expect(complete(parseOwnedJavaScriptApplicationAnalysisSteps(input))).toBe( + input, + ); + expect(Reflect.set(input.statistics, "findings", -1)).toBe(false); + }); + + it("preserves a normalized representation instead of borrowing changed input", () => { + const input = example(); + Object.setPrototypeOf(input, null); + freezeJsonSnapshot(input); + for (let repeat = 0; repeat < 2; repeat += 1) { + const result = complete( + parseOwnedJavaScriptApplicationAnalysisSteps(input), + ); + expect(result).not.toBe(input); + expect(Object.getPrototypeOf(result)).toBe(Object.prototype); + expect(result).toEqual( + javascriptApplicationAnalysisResultSchema.parse(input), + ); + } + }); + + it.each(["mutable", "shallow-frozen"])( + "rechecks changed %s metadata despite authenticated nested graphs", + (kind) => { + const input = ownedExample(); + if (kind === "shallow-frozen") Object.freeze(input); + expect( + complete(parseOwnedJavaScriptApplicationAnalysisSteps(input)), + ).not.toBe(input); + input.statistics.findings = -1; + expect(() => + complete(parseOwnedJavaScriptApplicationAnalysisSteps(input)), + ).toThrow(z.ZodError); + }, + ); +}); + describe("owned JavaScript graph validation", () => { it("reuses exact sealed graphs and preserves all public result fields", () => { const input = ownedExample(); @@ -239,6 +334,7 @@ describe("owned JavaScript result bindings", () => { input.semantic_graph = complete( createImmutableJavaScriptSemanticGraphSteps(semantic), ); + freezeJsonSnapshot(input); const steps = parseOwnedJavaScriptApplicationAnalysisSteps(input); expect(steps.next().done).toBe(false); steps.return(input); diff --git a/src/domain/native/binaryLayout.test.ts b/src/domain/native/binaryLayout.test.ts index 9f8f970e0e..49b26d4177 100644 --- a/src/domain/native/binaryLayout.test.ts +++ b/src/domain/native/binaryLayout.test.ts @@ -1,5 +1,10 @@ import { expect, it } from "vitest"; -import { binaryLayoutSchema, type BinaryLayout } from "./binaryLayout.js"; +import { + binaryLayoutSchema, + safeParseOwnedBinaryLayout, + type BinaryLayout, +} from "./binaryLayout.js"; +import { freezeJsonSnapshot } from "../immutableJson.js"; const report = () => ({ artifact: { path: "/fixture.elf", sha256: "a".repeat(64), bytes: 128 }, @@ -60,6 +65,55 @@ const report = () => ({ limitations: [], }); +it("reuses an exact validated immutable layout without a second retained copy", () => { + const value = freezeJsonSnapshot(report()); + const parsed = safeParseOwnedBinaryLayout(value); + expect(parsed.success).toBe(true); + if (!parsed.success) throw parsed.error; + expect(parsed.data).toBe(value); + expect(safeParseOwnedBinaryLayout(value)).toEqual(parsed); + expect(parsed.data).toEqual(binaryLayoutSchema.parse(value)); + expect(Reflect.set(value.sections[0]!, "index", 5)).toBe(false); +}); + +it.each(["mutable", "shallow-frozen"])( + "revalidates a changed %s layout instead of caching its former validity", + (kind) => { + const value = report(); + if (kind === "shallow-frozen") Object.freeze(value); + const first = safeParseOwnedBinaryLayout(value); + if (!first.success) throw first.error; + expect(first.data).not.toBe(value); + value.sections[0]!.header_location.offset = "0x80"; + const invalid = safeParseOwnedBinaryLayout(value); + expect(invalid.success).toBe(false); + expect(invalid).toEqual(binaryLayoutSchema.safeParse(value)); + }, +); + +it("never treats sealing as format validity or caches a refused layout", () => { + const value = report(); + value.sections[0]!.index = 10; + freezeJsonSnapshot(value); + expect(safeParseOwnedBinaryLayout(value).success).toBe(false); + expect(safeParseOwnedBinaryLayout(value).success).toBe(false); +}); + +it("does not borrow hidden fields that the format parser removed", () => { + const value = report(); + Object.defineProperty(value, "not_in_parsed_result", { + value: { unvalidated: true }, + enumerable: false, + }); + freezeJsonSnapshot(value); + for (let repeat = 0; repeat < 2; repeat += 1) { + const parsed = safeParseOwnedBinaryLayout(value); + if (!parsed.success) throw parsed.error; + expect(parsed.data).not.toBe(value); + expect(Object.hasOwn(parsed.data, "not_in_parsed_result")).toBe(false); + } +}); + it("retains high linked values and NOBITS without inventing file backing", () => { const value = binaryLayoutSchema.parse(report()); expect(value.entry_point.reported_value).toBe("0x20000000000001"); diff --git a/src/domain/native/binaryLayout.ts b/src/domain/native/binaryLayout.ts index 62ebb34f2f..cc37376dd9 100644 --- a/src/domain/native/binaryLayout.ts +++ b/src/domain/native/binaryLayout.ts @@ -1,4 +1,5 @@ import { z } from "zod"; +import { canBorrowParsedJsonSnapshot } from "../immutableJson.js"; import { localPathStringSchema } from "../localPath.js"; import { CANONICAL_BASE64_PATTERN } from "../stringPatterns.js"; @@ -316,6 +317,28 @@ export type InspectBinaryLayoutInput = z.infer< >; export type BinaryLayout = z.infer; +const validatedImmutableLayouts = new WeakSet(); + +/** Validate once per exact immutable source; mutable inputs always revalidate. */ +export const safeParseOwnedBinaryLayout = ( + input: unknown, +): z.ZodSafeParseResult => { + if ( + typeof input === "object" && + input !== null && + validatedImmutableLayouts.has(input) + ) + return { success: true, data: input as BinaryLayout }; + const parsed = binaryLayoutSchema.safeParse(input); + if (parsed.success && canBorrowParsedJsonSnapshot(input, parsed.data)) { + // Preserve normalization if the schema changes: only unchanged, completely + // validated data can be borrowed without retaining a duplicate layout. + validatedImmutableLayouts.add(input); + return { success: true, data: input }; + } + return parsed; +}; + const validateRelocationReferences = ( value: z.output, context: z.RefinementCtx,