Compare commits

...
Author SHA1 Message Date
N0zoM1z0andKaoru0822 43303502e3 chore(ci): verify native Windows ESM fixture loading and full acceptance file
Diagnostic workflow only. Compare the unchanged raw-drive baseline error with the current complete CLI/MCP acceptance file, requiring matching source and fresh exact reports.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 16:00:12 +00:00
N0zoM1z0andKaoru0822 011be9c3d5 test(applications): execute ESM fixtures through encoded file URLs
Refresh the Windows fixture fix on current main and exercise spaces and hash characters in the existing test-owned application directory. Preserve real module execution, CLI/MCP comparisons and the existing timeout.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 15:58:56 +00:00
Guilherme Dias FusariandKaoru0822 5b16f24310 test(applications): pass Evidence requests to the CLI through files (#3049)
Use existing test-owned request files for the remaining full Evidence CLI inputs. Preserve complete inputs, semantic assertions and inline transport coverage. Verified the full Linux file and native Windows baseline failures plus corrected public CLI cases.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 15:51:03 +00:00
YsopwnableandKaoru0822 9f8659e053 fix(javascript): enforce semantic byte bounds for literals
Fixes #3933. Part of #4027.

Normalize exact literals and configuration defaults through the shared primitive owner. Preserve tagged limits through member/destructuring projection and record encountered bounds in per-analysis state independently of later selected values. Remove the retained-value rescan and duplicate default reader. Preserve source Evidence, bounded/null values, aggregate wire capacity and complete endpoint lookup identities for query/trace/comparison.

Validated 66 focused and 932 semantic tests on the restored identical runtime; final public selection 7/7 adds an augmented large-endpoint acceptance (67 distinct focused cases across invocations). Full static/docs and fresh real Obsidian controls passed. Compact measurements and rejected attempts remain recorded.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 23:43:05 +08:00
KenmegeandKaoru0822 40f73f62bc test(rizin): own fake provider cleanup in shared fixtures (#3735)
Retain native fixture ownership through observed bounded cleanup and preserve files and handles when cleanup cannot be confirmed. Verified all seven files on Linux and native macOS, including cleanup failure and exit-wait controls.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 15:24:55 +00:00
Ysopwnable 6ae59f3d19 fix(verifiers): launch npm through Node on Windows (#4153)
* fix(verifiers): launch npm through Node on Windows

* test(verifiers): cover Windows npm fallback
2026-10-15 23:12:29 +08:00
Kaoru0822andClaude Opus 5.5 9d6651e5b5 fix(artifacts): never treat a directory named *.map as a source map (#4131)
Artifact inventory decided source-map edges from the `.map` path suffix,
so an archive directory such as `main.js.map/` received a maps-source
edge whose parent was the sibling `main.js`, although the same
occurrence was classified as a directory container. Edge construction
now uses the occurrence's own artifact_kind, and the duplicate suffix
rule in relationFor is removed; such directories are contained by their
parent, and real source-map files keep maps-source.

Fixes #3909

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:41:04 -07:00
Kaoru0822andClaude Opus 5.5 a90c9eac6a fix(apple): keep Interface Builder objects whose archived class is unresolved (#4118)
* fix(apple): keep Interface Builder objects whose archived class is unresolved

Interface Builder keyed-archive projection skipped every object-table
entry without a resolved class name, so an archived object whose $class
reference dangled vanished from the objects, its count and its coverage,
which still reported complete; as a hierarchy root it lost its label.
Entries that carry their own $class are archived objects: they are now
kept with class_name null and the generic kind, their fields and labels
preserved. Scalars and class descriptors remain excluded.

Fixes #3958

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(apple): keep the dangling class reference of an unknown IB object

Review follow-up. An archived object kept with an unresolved class lost
its original `$class` reference, because class resolution replaces it
with a null class name. The object's attributes now retain that raw
reference when the class cannot be resolved. A graph-level test covers
the reported hierarchy-root case: the unresolved root keeps its label,
unknown kind, containment edge, object count and coverage.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:40:10 -07:00
Kaoru0822andClaude Opus 5.5 cfd3c35a50 fix(kuna): keep the real lane's wide-string marker independent of layout (#4116)
On macOS arm64 the lane's UTF-8 marker ends right before the UTF-16 array,
and its last byte plus NUL decode as the UTF-16LE unit 'r'. Kuna therefore
reports "rREA wide marker", and the lane, which requires the exact text,
failed with "Required string \"REA wide marker\" was not recovered". A
leading NUL unit now ends any run the preceding bytes start, so the marker
is recovered exactly on any layout.

Refs #1957

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:38:33 -07:00
Kaoru0822andClaude Opus 5.5 3776f218e5 fix(apple): bound Interface Builder XML plists by shared value depth (#4114)
* fix(apple): bound Interface Builder XML plists by shared value depth

The Interface Builder preflight rejected XML archives whose raw element
depth exceeded 128, but the shared plist parser bounds zero-based value
depth at 128; the plist envelope and dictionary keys add elements, not
value depth. Archives whose deepest value sat at depth 127 or 128 were
therefore omitted as over budget although keyed-archive inspection
decodes them. The preflight's duplicate depth rule is removed; the
shared parser's depth limit is mapped to the existing budget omission,
so deeper archives stay explicitly omitted.

Fixes #3949

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(apple): return the decode reservation of a depth-rejected archive

Review follow-up. The removed element-depth preflight rejected an archive
before its plist representation was reserved; the shared depth limit now
rejects it during decoding, after the reservation. A large over-depth
archive therefore kept its share of the aggregate budget and could
starve later valid archives. A document omitted by the budget after
reserving now releases that reservation.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:38:07 -07:00
Kaoru0822andClaude Opus 5.5 f7250fe35f fix(kuna): say why Kuna cannot answer instead of a generic message (#4113)
Kuna capability gaps such as "no code at this address" or "no function node
at this address" were built without a user message, so MCP and the CLI
showed only "This analysis is unavailable for the current target. Choose
another analysis or target." and kept the reason in details. One Kuna
helper now presents the reason as the message for every such gap, and the
unsupported-operation reason names call relationships and xrefs, which Kuna
now answers.

Refs #1957

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 07:37:20 -07:00
Ysopwnable cd64e24ff2 fix(verifiers): align WABT operation timeout (#4117)
* fix(verifiers): align WABT MCP request timeout

* fix(verifiers): bound packaged WABT workflow
2026-10-15 22:29:15 +08:00
Abdul Samad SiddiquiandKaoru0822 a40ee741c2 test(ghidra): align MIPS verifier ABI and snapshot expectations
Fixes #3415. Retain independently inspected GNU attribute facts and compare exact snapshot wire fields. Preserve strict metadata comparisons, specific incomplete-ABI refusals and source immutability checks.

Original implementation by @AbdulSamad94.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 22:00:16 +08:00
GuilhermeF 8a695d1585 test(applications): import capture fixtures through file URLs
The capture-lifetime acceptance test executes its authored fixtures with
`import(process.argv[1])` and passed a filesystem path. ESM import()
takes a URL specifier, so on Windows the drive letter parses as a `c:`
scheme and Node rejects the import with ERR_UNSUPPORTED_ESM_URL_SCHEME
before the fixture runs.

Pass the fixture as a file URL from pathToFileURL instead.
2026-10-13 20:52:03 -03:00
57 changed files with 2266 additions and 1325 deletions

No files matched your search

+79 -917
View File
File diff suppressed because it is too large. Load diff
+3
View File
@@ -168,6 +168,9 @@ valid inputs.
Endpoint nodes keep the exact endpoint in each observation's `properties.value`;
semantic request and response nodes keep it in `properties.endpoint`. Endpoint
lookup identities are preserved independently of the primitive-value lattice's
byte budget. An oversized normalized literal can become unknown while the same
string remains an exact endpoint for tracing and comparison. Endpoint
identity keys use `value-sha256:<digest>` over the canonical JSON string when
that is shorter, or when the original value starts with the reserved digest
prefix. Other short endpoint keys remain unchanged. Endpoint labels use the
+12 -1
View File
@@ -437,7 +437,18 @@ retained candidate strings and temporary canonical-key serialization. This is
an in-memory semantic allocation bound independent of CLI or MCP transport
budgets. Source literal bytes remain in the parsed source and artifact evidence;
when a normalized semantic value exceeds the bound, its value is unknown and
coverage is partial. Expression
coverage is partial. Direct string literals, cooked templates without
substitutions, and derived strings all use this normalization boundary. A
single exact primitive is validated without allocating deduplication or sort
state or serializing a canonical key. Configuration defaults use the same
normalizer; limited defaults retain `value_resource_limit` and an anchored
resource-limit unknown. Known null, false, zero, and empty-string defaults stay
exact. This primitive-value bound does not truncate lookup identities such as
source paths, field names or request endpoints. Full endpoint properties remain
available for endpoint queries, tracing and comparison. Each analysis records encountered limit classifications independently of
later projections: a selected healthy sibling can remain exact while whole-file
coverage reports the encountered bound. Projection preserves limiting values
from the shared property-slot owner. Expression
evaluation and provenance walks stop after 256 nested levels. Union products
and string lengths are checked before allocating combinations or concatenated
strings. When a boundary is reached, the value stays unknown, semantic coverage
@@ -0,0 +1,547 @@
{
"base_revision": "e9a71efa31d6c3675b67938b8a79fa4ddf01604c",
"scope": "Semantic per-value string-byte normalization; real static shipped-application control, no hosted-model performance claim",
"runtime": {
"platform": "Linux x64",
"node": "24.18.0",
"npm": "11.16.0",
"tiktoken": "0.12.0"
},
"baseline_source_sha256": {
"src/domain/javascript/javascriptSemanticValues.ts": "df3fc30b5ba91901a85fe56f200355930da32d054bb026a747e3414af233b6b5",
"src/domain/javascript/javascriptSemanticPrimitives.ts": "c12b29f0faf4b6511777cba7ed8ac007c6d6170d657f01e43e194feacb3e0601",
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0203d207e0d6bb8aaa17746090f008876a11653afd660fce3dc44f3fda646a6c",
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1"
},
"final_source_sha256": {
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1",
"src/application/javascript/JavaScriptSemanticGraphDataProjection.ts": "844d4639401a9f050ce42c80192572a6d0d74fe4f81b37e93a2c20aaca9fb9a6",
"src/domain/javascript/javascriptSemanticAnalysis.ts": "a3f802ca3446c2b5376cc58b19d818eff318e6775bd47752793c3f649994f8a2",
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "2e78717d30e637b5fc5bab6d0eff063f7a62e0199d33697c1cdfaf8317baa698",
"src/domain/javascript/javascriptSemanticDataEffectHelpers.ts": "8f9cf1a36f05b03dd816eaf9a6cdba0160d34ad7d17a961206e1bc32444d2ccc",
"src/domain/javascript/javascriptSemanticDataEffects.ts": "e69408f2f5910a0107fd5d0cae0ff60ed706448857bc8e535c37cac1a0cd4d37",
"src/domain/javascript/javascriptSemanticIr.ts": "68dad360d0a9e6bd72108b2717cae756b84ca1efb48455248ded2ff699d2c113",
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0993a397f2a2a48c2b6c35494e940b3fce5c2c9032cfe33c7cf0401128c79dde",
"src/domain/javascript/javascriptSemanticState.ts": "bfcface5dcda22594f47ed390efa15f9140fe41cb2d5995896e52dd0ee31b978",
"src/domain/javascript/javascriptSemanticValues.ts": "a4ba51cc60fff38ef11fd5af35a2e1ad753cdb2dea9d44895f91d89320c5ddeb",
"tests/acceptance/applications/javascriptEndpointPayload.test.ts": "b3cfcde1c5b5e0698f84afd51f4b53e42e56101267707c20c0d038320f5cfbd2",
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "070106115b482269794799e448bd20b8e02df1f511f4ceb2b8fa75612c7f78e4",
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50"
},
"artifact": {
"path": "/tmp/rea-codemode-experiment-20261015/artifacts/obsidian-1.14.4.asar",
"sha256": "146ef8470d8cbb4a6db209bdc19bf2bad13350444186c0fd93c31087b142b92f"
},
"selected_source_paths": ["package.json", "main.js"],
"where": {
"path": "main.js",
"kind": "javascript-module"
},
"identical_parent_evidence_id": "ev_73ef06e3b9b944f54c5257f7960279127d8518d2a73cd8618bd045dbe44e6b5a",
"cli_parity": {
"evidence_id": "ev_b3586dfdf7d4f01157f8f2978326aaa7103987110f2df4fa5bf3331ff2898749",
"facts_sha256": "d3be5e91d2ec3fd9a95851cfbbdef9c168bb4c7153f78810b8b235e005401299",
"exact_cli_mcp_view_parity": true
},
"unchanged_query_shapes": {
"legacy": {
"calls": 24,
"structured_reply_bytes": 325201,
"facts_sha256": "d3be5e91d2ec3fd9a95851cfbbdef9c168bb4c7153f78810b8b235e005401299",
"matching_modules": 23,
"coverage": {
"status": "page",
"examined": 55,
"total": 55,
"next_offset": null,
"exhausted": true
}
},
"references": {
"calls": 1,
"structured_reply_bytes": 22158,
"facts_sha256": "d3be5e91d2ec3fd9a95851cfbbdef9c168bb4c7153f78810b8b235e005401299",
"matching_modules": 23,
"coverage": {
"status": "page",
"examined": 23,
"total": 23,
"next_offset": null,
"exhausted": true
}
}
},
"unchanged_token_costs": {
"tokenizer": "cl100k_base",
"tiktoken_version": "0.12.0",
"costs": {
"legacy": {
"selected_catalog": 7977,
"full_catalog": 195522,
"common_request": 53,
"common_summary": 2675,
"requests": 2836,
"structured_replies": 71761,
"selected_catalog_task_total": 85302,
"full_catalog_task_total": 272847
},
"references": {
"selected_catalog": 7977,
"full_catalog": 195522,
"common_request": 53,
"common_summary": 2675,
"requests": 102,
"structured_replies": 5741,
"selected_catalog_task_total": 16548,
"full_catalog_task_total": 204093
}
}
},
"cohorts": {
"baseline": {
"preparation_ms": 19089.119622,
"query_samples": [
{
"round": 0,
"mode": "legacy",
"elapsed_ms": 383.8260569999984
},
{
"round": 0,
"mode": "references",
"elapsed_ms": 15.778886999996757
},
{
"round": 1,
"mode": "references",
"elapsed_ms": 13.15173700000014
},
{
"round": 1,
"mode": "legacy",
"elapsed_ms": 240.44363500000327
},
{
"round": 2,
"mode": "legacy",
"elapsed_ms": 346.04796499999793
},
{
"round": 2,
"mode": "references",
"elapsed_ms": 13.727306999997381
}
],
"resources": {
"elapsed_seconds": 33.394,
"cpu_seconds": 33.016802,
"peak_cgroup_bytes": 1979863040,
"payload_cleanup_verified": true,
"events_at_payload_exit": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
}
},
"final": {
"preparation_ms": 19061.292544,
"query_samples": [
{
"round": 0,
"mode": "legacy",
"elapsed_ms": 344.6316079999997
},
{
"round": 0,
"mode": "references",
"elapsed_ms": 14.438351000000694
},
{
"round": 1,
"mode": "references",
"elapsed_ms": 11.38763100000142
},
{
"round": 1,
"mode": "legacy",
"elapsed_ms": 298.41579600000114
},
{
"round": 2,
"mode": "legacy",
"elapsed_ms": 211.45682099999976
},
{
"round": 2,
"mode": "references",
"elapsed_ms": 10.275370999999723
}
],
"resources": {
"elapsed_seconds": 32.949,
"cpu_seconds": 32.559187,
"peak_cgroup_bytes": 1880096768,
"payload_cleanup_verified": true,
"events_at_payload_exit": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
}
}
},
"conditions": "First query is after shared artifact analysis; subsequent queries share one MCP session. Legacy item reads are serial; a concurrent legacy baseline is unmeasured. Filesystem/build warmness uncontrolled. Token costs count one post-change catalog, preparation request/summary and each request/structured reply once; client rendering, replay, reasoning, final answer and model latency are unmeasured. Preparation and parity/tokenization overhead are outside per-query elapsed times; external family-resource receipts include the complete verifier and children. Before/after used separate fresh MCP sessions. CPU/memory includes analysis, query arms, export, CLI parity and tokenization; cgroup memory includes file cache, not summed RSS. Counters are at payload exit while observer lives, not final scope counters. One before/after control establishes no speedup or equivalence bound. Catalog BPE projection excludes annotations/output schemas.",
"resource_policy": {
"memory_max_bytes": 8589934592,
"early_stop_bytes": 6442450944,
"cpu_quota_percent": 100,
"swap": false
},
"observer_sources_sha256": {
"resource-run.py": "7728862171415e7dee2c11af2ead2b9a54265fccaf82a7113956d36b29065e33",
"resource_payload.py": "6f089e645459eba80c5d73e39c21606c78173a44daf49fabc3b88c4631ce51c6"
},
"regressions": {
"original_main": "Quoted/template oversized literals failed through public CLI/MCP; addition/small controls passed. Corrected domain boundary reproduced 2 failures/16 passes.",
"review_head": "8 failed/25 passed before fixes for projection tags, encountered-but-discarded limits and bare quoted/template defaults; includes exact null default control.",
"interim_review_fix": "65/66 focused passed; old doubling-chain expectation omitted a genuine expression-depth limit now observed. Adjusted that assertion to preserve the byte-limit requirement while allowing other observed classes.",
"final_focused": "66 passed across five files on the restored 38d263a runtime. Final corrected public selection passed 7/7 in two files: the six literal/default/member cases plus the augmented original large-endpoint acceptance. 67 distinct focused cases across invocations; not a single 67-case invocation.",
"final_semantic_suite": "932 passed in 36 files on 38d263a. Final production src tree (including these domain tests) verified byte-identical to that revision; no repeated semantic run claimed.",
"arithmetic": "Computed 174762*6+2=1048574 and 174763*6+2=1048580. Corrected the erroneous published 1048578 total.",
"final_grouped_domain": "27 passed after grouping observations/defaults/allocation within function-size rules; no scenarios removed.",
"endpoint_contract_correction": "Restored exact endpoint lookup identities rather than shrinking the existing large-endpoint fixture. Added the identical oversized string as an exported primitive in that acceptance workflow: literal normalization must report its bound while full endpoint lookup/feature trace/semantic trace/version comparison continue to work. Production src tree restored byte-identical to 38d263a; 932 semantic tests from that state remain applicable. Final corrected public tests/gates recorded after execution.",
"final_corrected_gates": "Full static gate and docs gate (5 consistency tests) passed after endpoint identity restoration and strengthened unchanged-large-input acceptance. Production src remains byte-identical to 38d263a; final public 7/7 and fresh real control passed."
},
"prior_attempts": [
{
"stage": "Initial literal normalization, incomplete for member/default paths identified in review",
"revision": "40b9d3c5b9cbeae7661d8e351e1b5ef6e5338931",
"source_sha256": {
"src/domain/javascript/javascriptSemanticValues.ts": "2debbb4f32d7d189819f2413f0f7d05b9bdcf497451a1c578475e3f2ec6c8554",
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0203d207e0d6bb8aaa17746090f008876a11653afd660fce3dc44f3fda646a6c",
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "747d893d6ddd91cf24d3b62b7eb50e0af9b1715ab355bbfc2e10ed0dc171ec17",
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "46ceb5a169de3eec3a335b05ef4aa7b1b341a5ac06ad325bd191e862c5b6be70",
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50",
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1"
},
"real_control_cohort": {
"preparation_ms": 18876.764036,
"query_samples": [
{
"round": 0,
"mode": "legacy",
"elapsed_ms": 346.7870870000006
},
{
"round": 0,
"mode": "references",
"elapsed_ms": 18.34765800000241
},
{
"round": 1,
"mode": "references",
"elapsed_ms": 11.56626999999935
},
{
"round": 1,
"mode": "legacy",
"elapsed_ms": 227.17703500000061
},
{
"round": 2,
"mode": "legacy",
"elapsed_ms": 251.5148330000011
},
{
"round": 2,
"mode": "references",
"elapsed_ms": 10.888227999999799
}
],
"resources": {
"elapsed_seconds": 31.347,
"cpu_seconds": 31.05517,
"peak_cgroup_bytes": 1589915648,
"payload_cleanup_verified": true,
"events_at_payload_exit": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
}
},
"validation": {
"baseline_public": "Quoted and cooked-template cases retained 200000-unit normalized literals and failed; addition and small exact controls passed. Aggregate-capacity test passed.",
"baseline_domain_corrected": "2 over-bound failures, 16 passed. Initial combined-run domain assertions had incorrectly required an empty optional resourceLimits array; corrected before using the domain reproduction as evidence.",
"fixed_focused": "55 passed across five files.",
"semantic_suite": "923 passed across 36 files.",
"capacity_after_fixture_cleanup": "1 passed; all 16 bounded values, aggregate size >= original, typed identities and retained tracing preserved.",
"interim_static_failure": "Typecheck rejected resume() on the new test SDK stderr Stream declaration. Switched to its declared data event, matching existing typed tests; analyzer source unchanged.",
"public_boundary_after_stream_type_fix": "4 passed across actual CLI and stdio MCP; analyzer source unchanged."
}
},
{
"stage": "Projection/default correction; final review subsequently found unnormalized request endpoints",
"revision": "38d263a369e7853ccfae1c8e0a3bd387f920c7cf",
"source_sha256": {
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1",
"src/application/javascript/JavaScriptSemanticGraphDataProjection.ts": "844d4639401a9f050ce42c80192572a6d0d74fe4f81b37e93a2c20aaca9fb9a6",
"src/domain/javascript/javascriptSemanticAnalysis.ts": "a3f802ca3446c2b5376cc58b19d818eff318e6775bd47752793c3f649994f8a2",
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "2e78717d30e637b5fc5bab6d0eff063f7a62e0199d33697c1cdfaf8317baa698",
"src/domain/javascript/javascriptSemanticDataEffectHelpers.ts": "8f9cf1a36f05b03dd816eaf9a6cdba0160d34ad7d17a961206e1bc32444d2ccc",
"src/domain/javascript/javascriptSemanticDataEffects.ts": "e69408f2f5910a0107fd5d0cae0ff60ed706448857bc8e535c37cac1a0cd4d37",
"src/domain/javascript/javascriptSemanticIr.ts": "68dad360d0a9e6bd72108b2717cae756b84ca1efb48455248ded2ff699d2c113",
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0993a397f2a2a48c2b6c35494e940b3fce5c2c9032cfe33c7cf0401128c79dde",
"src/domain/javascript/javascriptSemanticState.ts": "bfcface5dcda22594f47ed390efa15f9140fe41cb2d5995896e52dd0ee31b978",
"src/domain/javascript/javascriptSemanticValues.ts": "a4ba51cc60fff38ef11fd5af35a2e1ad753cdb2dea9d44895f91d89320c5ddeb",
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "070106115b482269794799e448bd20b8e02df1f511f4ceb2b8fa75612c7f78e4",
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50"
},
"real_control_cohort": {
"preparation_ms": 19599.643274,
"query_samples": [
{
"round": 0,
"mode": "legacy",
"elapsed_ms": 396.9265450000021
},
{
"round": 0,
"mode": "references",
"elapsed_ms": 15.658120000000054
},
{
"round": 1,
"mode": "references",
"elapsed_ms": 12.24905100000251
},
{
"round": 1,
"mode": "legacy",
"elapsed_ms": 237.03392900000108
},
{
"round": 2,
"mode": "legacy",
"elapsed_ms": 357.78818299999693
},
{
"round": 2,
"mode": "references",
"elapsed_ms": 14.37622100000226
}
],
"resources": {
"elapsed_seconds": 33.969,
"cpu_seconds": 33.499223,
"peak_cgroup_bytes": 1893015552,
"payload_cleanup_verified": true,
"events_at_payload_exit": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
}
},
"validation": {
"original_main": "Quoted/template oversized literals failed through public CLI/MCP; addition/small controls passed. Corrected domain boundary reproduced 2 failures/16 passes.",
"review_head": "8 failed/25 passed before fixes for projection tags, encountered-but-discarded limits and bare quoted/template defaults; includes exact null default control.",
"interim_review_fix": "65/66 focused passed; old doubling-chain expectation omitted a genuine expression-depth limit now observed. Adjusted that assertion to preserve the byte-limit requirement while allowing other observed classes.",
"final_focused": "66 passed in five files.",
"final_semantic_suite": "932 passed in 36 files.",
"arithmetic": "Computed 174762*6+2=1048574 and 174763*6+2=1048580. Corrected the erroneous published 1048578 total.",
"final_grouped_domain": "27 passed after grouping observations/defaults/allocation within function-size rules; no scenarios removed.",
"endpoint_review_baseline": "Production at 38d263a: 7 failed/14 passed across 13 new domain and 8 CLI/MCP cases before endpoint normalization. The later public linked-response case was not in this baseline run. Regression tests were uncommitted; final formatted test hashes are preserved, not claimed as the baseline test bytes.",
"endpoint_selection_attempt": "First six-file selection exited before execution because a signed-zero test filename was mistyped; corrected the exact path. No test result claimed for that attempt."
}
},
{
"stage": "Rejected endpoint normalization: violated existing full-endpoint lookup/trace contract; reverted after CI acceptance failure",
"revision": "580887209c4fceda432eac4083fac82a0dd78f15",
"source_sha256": {
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1",
"src/application/javascript/JavaScriptSemanticGraphDataProjection.ts": "a648f97e631985edcb5259ff4f1fdf0332dca18b8c10b57e050e36a785c1c524",
"src/domain/javascript/javascriptSemanticAnalysis.requestBudget.test.ts": "8e84ffa70d834f5589d04118f0aaf33b0383b15ea97545ff69c66a74473aca73",
"src/domain/javascript/javascriptSemanticAnalysis.ts": "a3f802ca3446c2b5376cc58b19d818eff318e6775bd47752793c3f649994f8a2",
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "2e78717d30e637b5fc5bab6d0eff063f7a62e0199d33697c1cdfaf8317baa698",
"src/domain/javascript/javascriptSemanticDataEffectHelpers.ts": "8f9cf1a36f05b03dd816eaf9a6cdba0160d34ad7d17a961206e1bc32444d2ccc",
"src/domain/javascript/javascriptSemanticDataEffects.ts": "eed961e0b1d8a320e76bbba933408e950b4a4a446110401e8cf470f1e74d8315",
"src/domain/javascript/javascriptSemanticIr.ts": "3b2ede73dbcfc74c4014e4d1affcbf00ecccc29a422ed495a7a3f2f517056c9b",
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0993a397f2a2a48c2b6c35494e940b3fce5c2c9032cfe33c7cf0401128c79dde",
"src/domain/javascript/javascriptSemanticState.ts": "bfcface5dcda22594f47ed390efa15f9140fe41cb2d5995896e52dd0ee31b978",
"src/domain/javascript/javascriptSemanticValues.ts": "a4ba51cc60fff38ef11fd5af35a2e1ad753cdb2dea9d44895f91d89320c5ddeb",
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "61319ea954de7f256841f43ca9355a1706ce8e93d30f2a8ba851d5e717bd19e7",
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50"
},
"real_control_cohort": {
"preparation_ms": 19939.160272,
"query_samples": [
{
"round": 0,
"mode": "legacy",
"elapsed_ms": 348.3974309999976
},
{
"round": 0,
"mode": "references",
"elapsed_ms": 13.97052899999835
},
{
"round": 1,
"mode": "references",
"elapsed_ms": 12.060379000002285
},
{
"round": 1,
"mode": "legacy",
"elapsed_ms": 212.01217800000086
},
{
"round": 2,
"mode": "legacy",
"elapsed_ms": 223.73954800000138
},
{
"round": 2,
"mode": "references",
"elapsed_ms": 10.450989999997546
}
],
"resources": {
"elapsed_seconds": 33.062,
"cpu_seconds": 32.71624,
"peak_cgroup_bytes": 1897406464,
"payload_cleanup_verified": true,
"events_at_payload_exit": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
}
},
"validation": {
"original_main": "Quoted/template oversized literals failed through public CLI/MCP; addition/small controls passed. Corrected domain boundary reproduced 2 failures/16 passes.",
"review_head": "8 failed/25 passed before fixes for projection tags, encountered-but-discarded limits and bare quoted/template defaults; includes exact null default control.",
"interim_review_fix": "65/66 focused passed; old doubling-chain expectation omitted a genuine expression-depth limit now observed. Adjusted that assertion to preserve the byte-limit requirement while allowing other observed classes.",
"final_focused": "81 passed in six files; the same CLI/MCP file then passed 9/9 after adding a linked-response case. That follow-up replaces its earlier 8-case coverage; 82 distinct cases across the final six-file selection, not one 82-case invocation.",
"final_semantic_suite": "945 passed in 37 files, overlapping focused domain coverage.",
"arithmetic": "Computed 174762*6+2=1048574 and 174763*6+2=1048580. Corrected the erroneous published 1048578 total.",
"final_grouped_domain": "27 passed after grouping observations/defaults/allocation within function-size rules; no scenarios removed.",
"endpoint_review_baseline": "Production at 38d263a: 7 failed/14 passed across 13 new domain and 8 CLI/MCP cases before endpoint normalization. The later public linked-response case was not in this baseline run. Regression tests were uncommitted; final formatted test hashes are preserved, not claimed as the baseline test bytes.",
"endpoint_selection_attempt": "First six-file selection exited before execution because a signed-zero test filename was mistyped; corrected the exact path. No test result claimed for that attempt.",
"endpoint_typecheck_attempt": "Full static gate found the inferred optional dataField in the public test computed key. Replaced the in-property check with an explicit bare === true discriminant; scenario inputs and runtime implementation unchanged. Corrected static/public checks recorded separately.",
"final_gates": "Full static gate passed after the explicit test discriminant; final public file passed 9/9 again, then docs gate passed including 5 consistency tests. Runtime implementation unchanged after the 945-test semantic suite and real control."
},
"ci_failure": {
"run_id": 38758262643,
"job_id": 116503076738,
"test": "tests/acceptance/applications/javascriptEndpointPayload.test.ts",
"result": "1 failed/1573 passed/19 skipped; request endpoint no longer matched its complete source value. Other local tests and real Obsidian control passed but did not establish this compatibility contract.",
"aggregate_result": "Merged source/built report: 1 failed/15625 passed/138 skipped. Portable tests and CI required failed as aggregates of the same endpoint assertion; no separate additional failed test."
}
}
],
"review_baseline_revision": "40b9d3c5b9cbeae7661d8e351e1b5ef6e5338931",
"review_baseline_source_sha256": {
"scripts/verify/javascript/module-references.mjs": "9d5f0b8b35ed2ebb4591f9dc3baea0fc116a0c3fd7d8e5ce30ec476c84671ed1",
"src/application/javascript/JavaScriptSemanticGraphDataProjection.ts": "ee541e634264363822bf4ce05c42c72272029c7e8da72bbeed643ad1341d0756",
"src/domain/javascript/javascriptSemanticAnalysis.ts": "a1cf791a61ee4742c351c006b69b64eba6bef3068f7a8450ed8f9366bfe4ff88",
"src/domain/javascript/javascriptSemanticAnalysis.unions.test.ts": "747d893d6ddd91cf24d3b62b7eb50e0af9b1715ab355bbfc2e10ed0dc171ec17",
"src/domain/javascript/javascriptSemanticDataEffectHelpers.ts": "5ee7af18dd847726d8fe1828ea25f386ff9ef317d996a3f7969253bf73973cfa",
"src/domain/javascript/javascriptSemanticDataEffects.ts": "4c62462016246bc7198294a74a60e87598fc009c53f1601abc2d4662d41b5bd1",
"src/domain/javascript/javascriptSemanticIr.ts": "14955cb4b7912ed6a4f26f804a2480f841dcb6084dbddd15856375fa62cdcfa1",
"src/domain/javascript/javascriptSemanticPrimitives.ts": "899eeb6ccdf666f504f8b722327599997c924c8e4fb13a1cbd13edaf08c96640",
"src/domain/javascript/javascriptSemanticResourceLimits.ts": "0203d207e0d6bb8aaa17746090f008876a11653afd660fce3dc44f3fda646a6c",
"src/domain/javascript/javascriptSemanticState.ts": "d94c5a88bb105284fab192406a8c88860d59f80c93394ed3ee7264969df254a6",
"src/domain/javascript/javascriptSemanticValues.ts": "2debbb4f32d7d189819f2413f0f7d05b9bdcf497451a1c578475e3f2ec6c8554",
"tests/boundary/mcp/javascriptLiteralBudget.test.ts": "46ceb5a169de3eec3a335b05ef4aa7b1b341a5ac06ad325bd191e862c5b6be70",
"tests/boundary/mcp/javascriptLiteralPayload.test.ts": "03618272c605f8877ed171ab39e037e4d80eac72b7478421640076f739adfb50"
},
"implementation": "Per-analysis state records encountered resource classifications at expression/default normalization; final coverage consumes those facts. Slot projection preserves tagged unknown values. Removed retained-value recursive coverage scan and duplicate raw default reader. Defaults retain optional valueResourceLimit and graph value_resource_limit plus an anchored resource-limit unknown; known null/false/0/empty remain exact. Endpoint identities intentionally remain exact lookup values, outside the normalized primitive lattice; rejected endpoint normalization and its CI failure are preserved separately.",
"restored_production_revision": "38d263a369e7853ccfae1c8e0a3bd387f920c7cf",
"final_corrected_resource_receipts": {
"public_7": {
"elapsed_seconds": 43.621,
"cpu_seconds": 43.097453,
"peak_cgroup_bytes": 1520324608,
"payload_cleanup_verified": true,
"events_at_payload_exit": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
},
"static_and_docs": {
"elapsed_seconds": 111.403,
"cpu_seconds": 110.830839,
"peak_cgroup_bytes": 4268244992,
"payload_cleanup_verified": true,
"events_at_payload_exit": {
"memory.events": {
"low": 0,
"high": 0,
"max": 0,
"oom": 0,
"oom_kill": 0,
"oom_group_kill": 0
},
"pids.events": {
"max": 0
}
}
}
}
}
@@ -0,0 +1,86 @@
# Semantic literal byte-budget verification
Tracks #3933 under #4027. All normalized semantic literals and configuration
defaults use the primitive owner. Its singleton path validates strings/nonfinite
numbers/negative zero without a Set, sorting or canonical-key serialization.
Each source analysis owns a bounded set of encountered limit classifications;
coverage consumes these observations after evaluation. This replaces a recursive
scan that only saw retained binding/return values. No persistent cache is added.
## Correctness and coverage
Public CLI and actual stdio MCP cases cover quoted strings, cooked templates,
equivalent concatenation, member receivers, a configuration-only expression
statement and a small exact control. Direct/default oversized values become
`resource-limit` unknowns with partial application coverage and
`javascript_semantic_primitive_bytes = 1048576`. Defaults carry
`value_resource_limit` inline and an unknown anchored to their config node.
Known null/false/zero/empty defaults remain exact. Original source/digests and
CLI/MCP identity parity are preserved.
Member and destructuring projections use the shared slot owner's tagged value.
An encountered limit stays visible even when a subsequent projection selects a
healthy sibling: the selected value remains exactly `7`, while file coverage is
partial. This avoids inferring completeness from the final retained values alone.
The duplicate raw default reader and superseded retained-value scan are removed.
Boundary controls admit 174,762 UTF-16 units (1,048,574 estimated JSON bytes)
and reject 174,763 (1,048,580 bytes). Actual arithmetic corrected the earlier
published 1,048,578 total; the unit-count boundary assertions were already correct.
The estimate counts six bytes per unit plus two quotes. It does not establish
actual ASCII JSON size above 1 MiB or an observed OOM.
The initial main baseline reproduced both public literal failures, with addition
and small controls passing. A mistaken optional-limit-array control was corrected
before the separate domain reproduction (2 failed/16 passed). Review head
`40b9d3c5` then reproduced the additional projection/default gaps (8 failed,
25 passed). The first review correction passed 65/66 focused tests; a doubling
chain now truthfully retains an additional encountered expression-depth limit,
so its byte-growth assertion was updated to allow other recorded limits.
Prior validation on the restored runtime: **66 focused tests** in five files and **932 semantic tests**
in 36 files, with overlapping domain coverage. The large-wire fixture retains
16 individually admitted values and at least the former aggregate UTF-16 payload,
plus complete CLI/MCP delivery, typed identities, empty strings and retained
tracing. Inputs regenerate from the tests.
## Endpoint identity compatibility
An attempted endpoint normalization at `5808872` passed its new local cases and real Obsidian control, but failed the existing CI endpoint-payload acceptance workflow (1 failed/1,573 passed/19 skipped in that shard). It violated the established full endpoint lookup/trace contract. This extension was reverted; endpoint values are lookup identities, outside the normalized primitive lattice. The original large endpoint is preserved. Corrected public validation passed **7/7** across two files; this adds one acceptance workflow to the previous 66-case focused selection (**67 distinct cases**, not one 67-case invocation). Its acceptance workflow now also normalizes that identical string as an exported primitive, requiring the typed value bound while endpoint lookup, both trace workflows and version comparison remain usable. Prior measurements and failed attempts remain in the JSON record. Production source is restored byte-identical to `38d263a`; the 932-test semantic run belongs to that state. Final full static and documentation gates (five consistency tests) passed after the compatibility correction.
## Real application control and costs
Official Obsidian 1.14.4 ASAR, selected `package.json` and `main.js`. Follow the
acquisition/digest and existing verifier command in
[module-reference verification](javascript-module-references-2026-10-15.md).
Use three alternating-order query pairs with CLI parity on each revision.
Application code is not executed.
The final correction retains the exact baseline full parent/view Evidence IDs,
23-row fact digest, source coverage and unknowns. Static `cl100k_base` task costs
remain 16,548 with selected name/description/inputSchema contracts, or 204,093
with the full catalog projection, for the reference workflow. Serial legacy
reads also keep their original costs. The query projection improvement belongs
to #4097; these are controls for semantic normalization and coverage.
| Complete real verifier | Before | Final correction |
| ---------------------- | ------------: | ---------------: |
| Elapsed seconds | 33.394 | 32.949 |
| CPU seconds | 33.016802 | 32.559187 |
| Peak cgroup bytes | 1,979,863,040 | 1,880,096,768 |
Includes initial analysis, both query arms, export, CLI parity and tokenizer.
One CPU, 8 GiB hard cap, 6 GiB early stop, no swap; cleanup verified and no OOM.
Memory includes charged file cache, not summed RSS. Counters are at payload exit
while the observer remains alive. Fresh MCP sessions do not establish OS-cold
cache conditions. Shared-host scheduling and filesystem/build warmness were
uncontrolled. This one before/after control establishes neither a speedup nor
an equivalence bound. Static BPE excludes annotations/output schemas, client
rendering/replay, reasoning and final answers; no hosted agent was run. Linux
static ASAR analysis was exercised, not live Electron or other platforms.
[Compact records, samples, source hashes and prior attempts](javascript-literal-byte-budget-2026-10-15.json)
preserve the initial incomplete correction separately from the final measurements.
No downloadable application or complete bundle is archived. Reproduce boundary
outcomes through the listed public tests and reuse the existing real verifier
and preserved 8 GiB observer linked above. Observer hashes and result-affecting
touched source hashes are pinned in the records.
+1
View File
@@ -138,6 +138,7 @@ export const SCOPES = {
"src/domain/javascript/javascriptRuntimeStaticCandidates.ts",
"tests/boundary/filesystem/webScriptManifestPaths.test.ts",
"tests/boundary/process/ProcessEvidence.host.test.ts",
"tests/boundary/process/packageNpmInvocation.test.ts",
"tests/boundary/process/windowsOutputCapture.test.ts",
"tests/acceptance/webModuleCli.test.ts",
"tests/acceptance/webSourceLocationCli.test.ts",
+20
View File
@@ -1,5 +1,6 @@
import { execFile } from "node:child_process";
import { lstat } from "node:fs/promises";
import { dirname, join } from "node:path";
import { promisify } from "node:util";
import { Ajv2020 } from "ajv/dist/2020.js";
@@ -17,6 +18,25 @@ const loadMcpVerifierResults = () => import("./mcp-verifier-results.mjs");
/** Promisified child_process execution helper. */
export const exec = promisify(execFile);
/** Run npm through its JavaScript entry point so Windows command scripts are not required. */
export const execNpm = (arguments_, options = {}) => {
const environment = options.env ?? process.env;
const selectedEntry = environment.npm_execpath;
if (
selectedEntry !== undefined &&
(typeof selectedEntry !== "string" || selectedEntry.length === 0)
)
throw new Error("npm_execpath must name the selected npm CLI entry point");
const entry =
selectedEntry ??
(process.platform === "win32"
? join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js")
: undefined);
return entry === undefined
? exec("npm", arguments_, options)
: exec(process.execPath, [entry, ...arguments_], options);
};
/** Run a command and return its stdout. */
export const run = async (command, args, env) =>
(await exec(command, args, { env })).stdout;
+4
View File
@@ -0,0 +1,4 @@
// A single inspection can include native admission plus several WABT commands.
// The aggregate verifier covers seven complete fixtures and their failure paths.
export const WABT_OPERATION_TIMEOUT_MS = 4 * 60_000;
export const WABT_WORKFLOW_TIMEOUT_MS = 15 * 60_000;
+4 -6
View File
@@ -2,7 +2,7 @@ import { rm } from "node:fs/promises";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { exec, json, run } from "./lib/verify-package-core.mjs";
import { exec, execNpm, json, run } from "./lib/verify-package-core.mjs";
/** Install the packed CLI globally with and without optional dependencies and verify PTY degradation. */
export async function verifyPackageInstall({
@@ -11,8 +11,7 @@ export async function verifyPackageInstall({
workspace,
environment,
}) {
await exec(
"npm",
await execNpm(
[
"install",
"--global",
@@ -53,8 +52,7 @@ export async function verifyPackageInstall({
`packaged PTY backend failed without lifecycle scripts: ${JSON.stringify(processCaptureCapability)}`,
);
const noOptionalPrefix = join(workspace, "prefix-no-optional");
await exec(
"npm",
await execNpm(
[
"install",
"--global",
@@ -108,7 +106,7 @@ export async function verifyPackageInstall({
const verifyInstalledPackageCanRepack = async (prefix, environment) => {
const installedPackageRoot = join(prefix, "lib/node_modules/rea-agents");
const repackedTarball = (
await exec("npm", ["pack", "--silent"], {
await execNpm(["pack", "--silent"], {
cwd: installedPackageRoot,
env: environment,
})
+4 -8
View File
@@ -2,7 +2,7 @@ import { mkdir } from "node:fs/promises";
import { join } from "node:path";
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
import { exec } from "./lib/verify-package-core.mjs";
import { exec, execNpm } from "./lib/verify-package-core.mjs";
import { verifyPackedBridge } from "./verify-packed-bridge.mjs";
/** Create a tarball and assert production packaging constraints. */
@@ -12,13 +12,9 @@ export async function verifyPackagePack({ root, workspace }) {
const tarball = join(
packDirectory,
(
await exec(
"npm",
["pack", "--silent", "--pack-destination", packDirectory],
{
cwd: root,
},
)
await execNpm(["pack", "--silent", "--pack-destination", packDirectory], {
cwd: root,
})
).stdout.trim(),
);
const packedFiles = (await exec("tar", ["-tf", tarball])).stdout.split("\n");
+3 -9
View File
@@ -28,6 +28,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { execNpm } from "./lib/verify-package-core.mjs";
if (process.platform !== "win32" || process.arch !== "x64")
throw new Error(
@@ -82,15 +83,10 @@ let packageRoot = resolve(packageRootArgument ?? ".");
// package roots support an already-installed artifact without a second install.
if (packageRootArgument === undefined) {
try {
const npmEntry =
process.env.npm_execpath ??
join(dirname(process.execPath), "node_modules/npm/bin/npm-cli.js");
const packed = JSON.parse(
(
await exec(
process.execPath,
await execNpm(
[
npmEntry,
"pack",
"--json",
"--ignore-scripts",
@@ -107,10 +103,8 @@ if (packageRootArgument === undefined) {
"The package lane must produce one artifact.",
);
const prefix = join(workspace, "installed");
await exec(
process.execPath,
await execNpm(
[
npmEntry,
"install",
"--prefix",
prefix,
+8 -4
View File
@@ -2,7 +2,8 @@
import { join } from "node:path";
import { mkdir } from "node:fs/promises";
import { PrivateRuntimeRoot } from "../dist/process/PrivateRuntimeRoot.js";
import { exec } from "./lib/verify-package-core.mjs";
import { exec, execNpm } from "./lib/verify-package-core.mjs";
import { WABT_WORKFLOW_TIMEOUT_MS } from "./lib/wabt-verifier-timeouts.mjs";
import { verifyPackagePack } from "./verify-package-pack.mjs";
const root = await PrivateRuntimeRoot.create({
@@ -23,8 +24,7 @@ try {
XDG_CONFIG_HOME: join(home, ".config"),
XDG_CACHE_HOME: join(home, ".cache"),
};
await exec(
"npm",
await execNpm(
[
"install",
"--global",
@@ -49,7 +49,11 @@ try {
const verified = await exec(
process.execPath,
["scripts/verify-wasm-artifact.mjs", entrypoint, "--require-tools"],
{ env: environment, timeout: 180_000, maxBuffer: 4 * 1024 * 1024 },
{
env: environment,
timeout: WABT_WORKFLOW_TIMEOUT_MS,
maxBuffer: 4 * 1024 * 1024,
},
);
process.stdout.write(verified.stdout);
} finally {
+49 -84
View File
@@ -12,6 +12,7 @@ import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import {
inspectMipsReadelf,
expectedMipsElfProfile,
inspectLegacyMipsReadelf,
assertMipsLoadedImage,
assertMipsProbe,
@@ -120,6 +121,17 @@ async function verifySoftFloat(call, symbols, byteOrder) {
);
return { storage: boundaries };
}
function assertIncompleteMipsInspection(error) {
assert.equal(error.code, "capability_unavailable");
assert.equal(error.details.selection_reason, "target_unsupported");
const rejection = error.details.rejections.find(
(candidate) => candidate.provider_id === "ghidra",
);
assert.equal(rejection?.code, "architecture_unsupported");
assert.match(rejection.reason, /ABI declaration inspection is incomplete/u);
assert.match(rejection.reason, /section header table/u);
}
const reports = [];
let primaryFailure;
try {
@@ -475,28 +487,9 @@ try {
JSON.parse(await readFile(snapshotPath, "utf8")),
);
assert.equal(snapshot.target.architecture, "mips");
const abi = independent.mips.abiFlags;
assert.deepEqual(
snapshot.binding.analysis_profile.parameters.mips_elf,
{
elf_class: independent.mips.elfClass,
byte_order: independent.mips.byteOrder,
type: independent.mips.type,
flags: independent.mips.flags,
abi_flags: {
version: abi.version,
isa_level: abi.isaLevel,
isa_revision: abi.isaRevision,
gpr_size: abi.gprSize,
cpr1_size: abi.cpr1Size,
cpr2_size: abi.cpr2Size,
fp_abi: abi.fpAbi,
isa_extension: abi.isaExtension,
ases: abi.ases,
flags1: abi.flags1,
flags2: abi.flags2,
},
},
expectedMipsElfProfile(independent.mips),
"Snapshot profile did not retain the independently inspected ELF/ABI declaration",
);
assert.equal(
@@ -617,7 +610,8 @@ try {
// PSP keeps its independent Allegrex/A-B-A checks; carved PSP is not verified.
let carved;
if (!psp) {
// Carved firmware keeps all program headers/load bytes but loses sections.
// A program-header ABI record survives carving, but GNU declarations in
// uncaptured section headers remain unknown: both adapters must refuse.
const read16 = (offset) =>
byteOrder === "little"
? sourceBytes.readUInt16LE(offset)
@@ -655,79 +649,50 @@ try {
carvedResolved.value.mips.limitations.join(" "),
/section header table/u,
);
const carvedTarget = await call("open_binary", {
path: carvedPath,
provider_id: "ghidra",
});
assert.equal(carvedTarget.sha256, carvedSha256);
const carvedImage = await call("inspect_native_load_image");
assertMipsLoadedImage(
carvedImage.observations,
byteOrder,
carvedSha256,
);
const carvedProcedures = await call("list_procedures");
assert.ok(
carvedProcedures.some(
(p) =>
BigInt(p.address) === BigInt(independent.symbols.rea_mips_entry),
const carvedError = requireMcpToolError(
await client.callTool(
{
name: "open_binary",
arguments: { path: carvedPath, provider_id: "ghidra" },
},
{ timeout: 60000 },
),
);
const carvedDossier = functionDossierSchema.parse(
await call("analyze_function", {
procedure: independent.symbols.rea_mips_entry,
}),
);
assert.ok(carvedDossier.pseudocode.trim().length > 0);
const carvedSnapshotPath = join(
workspace,
`${byteOrder}-${floatAbi}-carved.snapshot.json`,
);
await call("close_binary", { snapshot_path: carvedSnapshotPath });
const carvedSnapshot = parseAnalysisSnapshot(
JSON.parse(await readFile(carvedSnapshotPath, "utf8")),
);
assert.equal(carvedSnapshot.target.sha256, carvedSha256);
assertIncompleteMipsInspection(carvedError);
let carvedCliError;
try {
await execute(
process.execPath,
[
entrypoint,
"function",
carvedPath,
independent.symbols.rea_mips_entry,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 60000, maxBuffer: 1024 * 1024 },
);
} catch (cause) {
carvedCliError = cause;
}
assert.ok(
carvedSnapshot.evidence_bundle.records.some((record) =>
parseEvidence(record).limitations.some((item) =>
item.includes("section header table"),
),
),
);
const carvedCli = await execute(
process.execPath,
[
entrypoint,
"function",
carvedPath,
independent.symbols.rea_mips_entry,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const carvedEvidence = parseEvidence(JSON.parse(carvedCli.stdout));
assert.equal(carvedEvidence.subject?.digest.sha256, carvedSha256);
assert.ok(
carvedEvidence.limitations.some((item) =>
item.includes("section header table"),
),
);
assert.equal(
functionDossierSchema.parse(carvedEvidence.normalized_result)
.procedure.address,
carvedDossier.procedure.address,
carvedCliError &&
Number.isInteger(carvedCliError.code) &&
carvedCliError.code !== 0,
"Expected an incomplete-ABI refusal, not a timeout or success",
);
assertIncompleteMipsInspection(JSON.parse(carvedCliError.stdout));
assert.deepEqual(await readFile(carvedPath), carvedBytes);
carved = {
sha256: carvedSha256,
source_bytes: carvedBytes.length,
section_header_offset: sectionOffset,
effective_language: carvedImage.observations.language_id,
status: "expected-incomplete-abi-refusal",
limitations: carvedResolved.value.mips.limitations,
procedures: carvedProcedures.length,
cli_exit: carvedCliError.code,
mcp_error: carvedError,
};
}
reports.push({
+4 -1
View File
@@ -83,8 +83,11 @@ const FIXTURE_SOURCE = String.raw`
#include <stdio.h>
#include <stdlib.h>
static const char rea_kuna_utf8[] __attribute__((used)) = "_φ( °-°)/ REA unicode marker";
/* The leading NUL unit ends any UTF-16 run that bytes before this array would
start: the UTF-8 marker's last byte and NUL decode as the unit 'r', so on
layouts where it precedes this array Kuna reports "rREA wide marker". */
static const unsigned short rea_kuna_wide[] __attribute__((used)) = {
'R','E','A',' ','w','i','d','e',' ','m','a','r','k','e','r',0
0,'R','E','A',' ','w','i','d','e',' ','m','a','r','k','e','r',0
};
#include <string.h>
__attribute__((noinline)) unsigned rea_checksum(const unsigned char *p, size_t n) {
+2 -3
View File
@@ -1,7 +1,7 @@
import { cp, mkdir, readFile, readdir } from "node:fs/promises";
import { dirname, join, relative } from "node:path";
import { exec, pathExists } from "./lib/verify-package-core.mjs";
import { exec, execNpm, pathExists } from "./lib/verify-package-core.mjs";
/** Exercise Skills' default root discovery without ignored build outputs. */
export async function verifyRepositorySkill({ root, workspace }) {
@@ -34,8 +34,7 @@ export async function verifyRepositorySkill({ root, workspace }) {
await mkdir(consumer, { recursive: true });
await mkdir(home, { recursive: true });
const skillsVersion = "1.7.2";
await exec(
"npm",
await execNpm(
[
"exec",
"--yes",
+8 -5
View File
@@ -24,6 +24,7 @@ import {
requireMcpToolError,
} from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { WABT_OPERATION_TIMEOUT_MS } from "./lib/wabt-verifier-timeouts.mjs";
const directory = process.env.REA_WABT_BIN_DIRECTORY;
if (!isAbsolute(directory ?? "")) {
@@ -76,6 +77,8 @@ try {
throw cause;
}
const client = new Client({ name: "wabt-artifact-verifier", version: "1" });
const callTool = (request) =>
client.callTool(request, { timeout: WABT_OPERATION_TIMEOUT_MS });
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
@@ -128,7 +131,7 @@ try {
TMP: childTempRoot,
TEMP: childTempRoot,
},
timeout: 120_000,
timeout: WABT_OPERATION_TIMEOUT_MS,
maxBuffer: 64 * 1024 * 1024,
},
)
@@ -238,7 +241,7 @@ try {
const cliValue = await inspectCli(input);
assert.equal(cliValue.category, undefined, JSON.stringify(cliValue));
const cli = parseEvidence(cliValue);
const reply = await client.callTool({
const reply = await callTool({
name: "inspect_wasm_artifact",
arguments: input,
});
@@ -332,7 +335,7 @@ try {
},
);
}
const bundleReply = await client.callTool({
const bundleReply = await callTool({
name: "get_evidence_bundle",
arguments: {},
});
@@ -362,7 +365,7 @@ try {
JSON.stringify(cliError),
);
const error = requireMcpToolError(
await client.callTool({
await callTool({
name: "inspect_wasm_artifact",
arguments: invalidInput,
}),
@@ -370,7 +373,7 @@ try {
assert.equal(error.category, "invalid_input");
const partial = error.details.partial_observation;
assert.equal(partial.kind, "retained-evidence");
const failedBundleReply = await client.callTool({
const failedBundleReply = await callTool({
name: "get_evidence_bundle",
arguments: {},
});
+10 -20
View File
@@ -18,7 +18,10 @@ import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { verifyCompleteToolCatalog } from "./lib/verify-package-core.mjs";
import {
execNpm,
verifyCompleteToolCatalog,
} from "./lib/verify-package-core.mjs";
import { verifyPackageUpdate } from "./verify-package-update.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
@@ -32,9 +35,9 @@ let report;
try {
const packed = JSON.parse(
(
await npm(
await execNpm(
["pack", "--json", "--silent", "--pack-destination", workspace],
root,
{ cwd: root, windowsHide: true },
)
).stdout,
);
@@ -63,9 +66,9 @@ try {
const tarball = join(workspace, packageResult.filename);
await access(tarball);
await npm(
await execNpm(
["install", "--no-package-lock", "--no-save", "--prefix", prefix, tarball],
workspace,
{ cwd: workspace, windowsHide: true },
);
const entry = join(
prefix,
@@ -209,9 +212,9 @@ try {
throw new Error(`Packaged uninstall did not remove ${id}`);
if (process.platform !== "win32")
await npm(
await execNpm(
["install", "--global", "--ignore-scripts", "--prefix", prefix, tarball],
workspace,
{ cwd: workspace, windowsHide: true },
);
const update = await verifyPackageUpdate({
prefix,
@@ -241,16 +244,3 @@ try {
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`,
);
function npm(arguments_, cwd) {
const npmExecPath = process.env.npm_execpath;
return npmExecPath === undefined
? exec(process.platform === "win32" ? "npm.cmd" : "npm", arguments_, {
cwd,
windowsHide: true,
})
: exec(process.execPath, [npmExecPath, ...arguments_], {
cwd,
windowsHide: true,
});
}
@@ -12,6 +12,7 @@ import {
} from "./JavaScriptSemanticGraphConstruction.js";
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
import { semanticResourceLimitReason } from "../../domain/javascript/javascriptSemanticResourceLimits.js";
/** Project environment, argv, file, default, and precedence candidates. */
export const projectSemanticConfiguration = (
@@ -56,7 +57,17 @@ export const projectSemanticConfiguration = (
...(operation.kind === "default" ? (["overrides"] as const) : []),
],
location: operation.location,
detail: `Static ${operation.kind} configuration identity is partial.`,
...(operation.valueResourceLimit === undefined
? {
detail:
operation.kind === "default"
? "Static default configuration value is partial."
: `Static ${operation.kind} configuration identity is partial.`,
}
: {
reason: "resource-limit",
detail: semanticResourceLimitReason(operation.valueResourceLimit),
}),
candidateNodeIds: [],
});
}
@@ -80,6 +91,9 @@ const addConfigurationNode = (
key: operation.key,
source_kind: operation.kind,
value: operation.value,
...(operation.valueResourceLimit === undefined
? {}
: { value_resource_limit: operation.valueResourceLimit }),
},
});
@@ -279,6 +293,7 @@ const bindingOrOwner = (
: (context.callableNodes.get(callableId) ?? context.moduleNode));
interface DataUnknownInput {
readonly reason?: "ambiguous-target" | "resource-limit";
readonly context: SemanticFlowProjectionContext;
readonly node: JavaScriptSemanticGraphNode;
readonly family: "boundary" | "configuration" | "request";
@@ -317,7 +332,7 @@ const addDataUnknown = (input: DataUnknownInput): void => {
node_id: node.node_id,
family,
relation_kinds: relationKinds,
reason: "ambiguous-target",
reason: input.reason ?? "ambiguous-target",
detail,
candidate_node_ids: candidateNodeIds,
evidence: unknownSemanticEvidence(context.file, location),
@@ -84,6 +84,8 @@ export const analyzeInterfaceBuilderBundle = async (input: {
recordOmittedArchive(entry.path, "max_documents");
unreportedOmittedArchives += selection.unreportedOmitted;
for (const entry of selection.selected) {
// Plist representation reserved for this document before decoding.
let plistReservation = 0;
try {
const remainingInputBytes =
MAX_AGGREGATE_INPUT_BYTES - aggregateInputBytes;
@@ -132,6 +134,7 @@ export const analyzeInterfaceBuilderBundle = async (input: {
decodedReservation.estimatedBytes,
"plist representation exceeds the aggregate Interface Builder decode budget",
);
plistReservation = decodedReservation.estimatedBytes;
binaryPrototypeKeyObserved =
decodedReservation.binaryPrototypeKeyObserved;
}
@@ -165,6 +168,9 @@ export const analyzeInterfaceBuilderBundle = async (input: {
} catch (cause: unknown) {
if (input.signal?.aborted === true) throw cause;
if (cause instanceof InterfaceBuilderDecodeBudgetExceeded) {
// A document rejected while decoding, e.g. beyond the shared depth
// limit, retains no representation; later documents keep its share.
decodeBudget.release(plistReservation);
omitted += 1;
recordOmittedArchive(entry.path, cause.reason);
continue;
@@ -700,6 +706,24 @@ const readEntry = async (
return Buffer.concat(chunks, length);
};
/**
* The shared parser bounds plist value nesting before its recursive decode and
* reports the limit as a RangeError; that is an omitted archive, not a
* malformed one.
*/
const parseXmlWithinDecodeBudget = (text: string) => {
try {
return parseXmlPropertyList(text);
} catch (cause: unknown) {
if (cause instanceof RangeError)
throw new InterfaceBuilderDecodeBudgetExceeded(
"aggregate_decode_budget_exhausted",
cause.message,
);
throw cause;
}
};
/** Project decoded plist data and dates while retaining omitted-key coverage. */
const decodePlist = (
bytes: Buffer,
@@ -716,7 +740,7 @@ const decodePlist = (
value: parseBinary(bytes),
omittedPrototypeKeys: binaryPrototypeKeyObserved ? null : 0,
}
: parseXmlPropertyList(xmlText ?? decodeXmlPlistText(bytes));
: parseXmlWithinDecodeBudget(xmlText ?? decodeXmlPlistText(bytes));
const projected = projectPlistValue(value);
return {
value: projected.value,
@@ -49,6 +49,14 @@ export class InterfaceBuilderDecodeBudget {
this.#remainingBytes -= bytes;
}
/** Return a reservation whose representation was rejected and not retained. */
release(bytes: number): void {
this.#remainingBytes = Math.min(
this.#initialBytes,
this.#remainingBytes + bytes,
);
}
/** Reserve an upper bound for one JSON-serialized string occurrence. */
reserveJsonStringOccurrence(value: string): void {
// JSON escaping uses at most six ASCII bytes per UTF-16 code unit, plus
@@ -117,7 +125,8 @@ const estimateXmlPlistExpansion = (
const xml = xmlText ?? decodeXmlPlistText(bytes);
const decodedSourceBytes = xml.length * 2;
let elementCount = 0;
let xmlDepth = 0;
// Value nesting is bounded by the shared plist parser, which counts plist
// value depth; raw element depth would also count the envelope and keys.
for (const token of xmlPreflightTokens(xml)) {
if (token.doctype) {
// Only a declaration outside opaque text can introduce an internal
@@ -130,15 +139,6 @@ const estimateXmlPlistExpansion = (
continue;
}
elementCount += 1;
if (token.closing) xmlDepth -= 1;
else if (!token.selfClosing) {
xmlDepth += 1;
if (xmlDepth > 128)
throw new InterfaceBuilderDecodeBudgetExceeded(
"aggregate_decode_budget_exhausted",
"XML plist nesting exceeds the aggregate Interface Builder decode budget",
);
}
if (decodedSourceBytes * 2 + elementCount * 128 > maxBytes)
return maxBytes + 1;
}
@@ -179,13 +179,7 @@ function* xmlPreflightTokens(xml: string) {
}
if (end === xml.length) return;
cursor = end + 1;
if (doctype || element)
yield {
doctype,
internalSubset,
closing: xml[start + 1] === "/",
selfClosing: xml[end - 1] === "/",
};
if (doctype || element) yield { doctype, internalSubset };
}
}
@@ -121,6 +121,43 @@ describe("source-map artifact relationships", () => {
);
},
);
it("keeps a directory named like a source map contained by its parent", () => {
const archive = createOccurrence(entry("app", "directory"), "app", null);
archive.artifact_id = contentId("1");
const script = createOccurrence(
entry("app/main.js", "file"),
"app/main.js",
archive.occurrence_id,
);
script.artifact_id = contentId("2");
const directory = createOccurrence(
entry("app/main.js.map", "directory"),
"app/main.js.map",
archive.occurrence_id,
);
directory.artifact_id = contentId("3");
const { edges, limitations } = createArtifactEdges(contentId("4"), [
archive,
script,
directory,
]);
expect(directory).toMatchObject({
artifact_kind: "container",
artifact_format: "directory",
});
expect(
edges.find(
({ occurrence_id }) => occurrence_id === directory.occurrence_id,
),
).toMatchObject({
relation: "contains",
parent_artifact_id: contentId("1"),
});
expect(limitations).toEqual([]);
});
});
const fat = (wide = false): Buffer =>
@@ -295,7 +295,9 @@ export const createArtifactEdges = (
const edges: ArtifactEdge[] = [];
const limitations: string[] = [];
for (const occurrence of occurrences) {
const isSourceMap = occurrence.logical_path.toLowerCase().endsWith(".map");
// Use the occurrence's own classification: a directory named `x.map` is
// a container, not a source map of its sibling.
const isSourceMap = occurrence.artifact_kind === "source-map";
const sourcePath = isSourceMap
? occurrence.logical_path.slice(0, -".map".length)
: undefined;
@@ -337,8 +339,10 @@ export const createArtifactEdges = (
relation:
occurrence.entry_kind === "slice"
? ("slice-of" as const)
: isSourceMap && mappedSource === undefined
? ("contains" as const)
: isSourceMap
? mappedSource === undefined
? ("contains" as const)
: ("maps-source" as const)
: relationFor(occurrence.logical_path),
occurrence_id: occurrence.occurrence_id,
};
@@ -587,11 +591,8 @@ const classifyFatArtifact = (
return identity.state === "mach-o" ? "mach-o-universal" : "unknown";
};
const relationFor = (path: string): ArtifactEdge["relation"] => {
const lower = path.toLowerCase();
if (lower.endsWith(".map")) return "maps-source";
if (lower.includes(".framework/")) return "embeds";
return "contains";
};
/** Relation of a non-source-map occurrence to its parent. */
const relationFor = (path: string): ArtifactEdge["relation"] =>
path.toLowerCase().includes(".framework/") ? "embeds" : "contains";
const depth = (path: string): number => path.split("/").length;
@@ -156,6 +156,90 @@ describe("compiled Interface Builder graph projection", () => {
});
});
describe("keyed archive objects with unresolved classes", () => {
it("keeps an archived object whose class reference cannot be resolved", () => {
const parsed = parseInterfaceBuilderRecords({
$archiver: "NSKeyedArchiver",
$version: 100000,
$objects: [
"$null",
{ $class: { UID: 99 }, label: "Unresolved object" },
{ $classname: "UIView", $classes: ["UIView", "NSObject"] },
{ $class: { UID: 2 }, "NS.string": "ok" },
"plain scalar",
],
$top: { root: { UID: 3 } },
});
// The class descriptor and the scalar are not archived objects.
expect(parsed.objects).toMatchObject([
{
id: "1",
kind: "other",
class_name: null,
name: "Unresolved object",
attributes: { label: "Unresolved object", $class: { UID: 99 } },
},
{ id: "3", kind: "view", class_name: "UIView" },
]);
expect(parsed.objects[1]?.attributes).not.toHaveProperty("$class");
expect(parsed.objectCount).toBe(2);
});
it("keeps an unresolved hierarchy root's label and coverage in the graph", () => {
const result = buildInterfaceBuilderAnalysis({
targetSha256: hash,
toolVersion: "test",
documents: [
{
relativePath: "Main.nib",
archiveSha256: hash,
documentKind: "nib",
raw: {
$archiver: "NSKeyedArchiver",
$version: 100000,
$objects: [
"$null",
{ $class: { UID: 99 }, label: "Unresolved object" },
{ $classname: "UIView", $classes: ["UIView", "NSObject"] },
{ $class: { UID: 2 }, "NS.string": "ok" },
],
$top: { root: { UID: 1 } },
},
},
],
limits: interfaceBuilderLimitsSchema.parse({}),
});
const objectNodes = result.graph.nodes.filter(
({ attributes }) => attributes.interface_builder_object_id !== undefined,
);
expect(objectNodes).toMatchObject([
{
kind: "unknown",
name: "Unresolved object",
attributes: { interface_builder_object_id: "1", class_name: null },
},
{ kind: "view", attributes: { interface_builder_object_id: "3" } },
]);
expect(result.documents[0]?.object_count).toBe(2);
expect(
result.graph.edges.some(
({ relation, to, resolution }) =>
relation === "contains" &&
to === objectNodes[0]?.id &&
resolution === "observed",
),
).toBe(true);
expect(result.graph.coverage).toContainEqual(
expect.objectContaining({
facet: "objects:Main.nib",
status: "complete",
examined: 2,
omitted: 0,
}),
);
});
});
describe("Interface Builder parser bounds", () => {
it("reports connections discarded by the parser's hard ceiling", () => {
const connections = Array.from({ length: 40_001 }, (_, index) => ({
@@ -327,11 +327,16 @@ const parseKeyedArchive = (
if (index === 0) continue;
const resolved = record(resolve(raw));
const className = firstString(resolved.className);
if (className === null) continue;
// An archived object names its class with its own `$class` reference;
// when that reference cannot be resolved the object is still observed,
// with an unknown class. Scalars and class descriptors are not objects.
if (className === null && !Object.hasOwn(record(raw), "$class")) continue;
objectCount += 1;
const fields = Object.fromEntries(
Object.entries(resolved).filter(([key]) => key !== "className"),
);
// Keep the unresolvable class reference itself, e.g. a dangling UID.
if (className === null) fields.$class = record(raw).$class;
const authoredId = firstString(fields.objectID, fields["object-id"]);
const objectId =
authoredId ??
@@ -357,6 +362,7 @@ const parseKeyedArchive = (
}),
);
if (
className !== null &&
/(?:Outlet|Connection|Segue|ActionConnection|ControlConnector)/iu.test(
className,
)
@@ -62,7 +62,6 @@ import {
} from "./javascriptElectronMemberWrites.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { semanticCoverage } from "./javascriptSemanticCoverage.js";
import { semanticResourceLimitsIn } from "./javascriptSemanticResourceLimits.js";
import {
parseJavaScriptSource,
type ParsedJavaScriptSource,
@@ -197,12 +196,7 @@ export function* analyzeParsedJavaScriptSemanticsSteps(
...derived,
coverage: semanticCoverage(
parserPartial,
semanticResourceLimitsIn([
...bindings.map(({ value }) => value),
...callables.flatMap((callable) =>
callable.returnSites.map(({ value }) => value),
),
]),
[...state.valueResourceLimits].sort(),
),
limitations: [
...(parserPartial
@@ -698,6 +692,7 @@ const createState = (
return {
program,
staticImportResolutionMode,
valueResourceLimits: new Set(),
parentsByNode: new WeakMap(),
scopes: [root],
scopesById: new Map([[root.scopeId, root]]),
@@ -219,7 +219,120 @@ describe("logical primitive unions", () => {
});
});
describe("semantic primitive resource observations", () => {
it.each([
{
name: "member",
source: (large: string) =>
`const answer = (${JSON.stringify(large)}).field;`,
},
{
name: "destructuring",
source: (large: string) =>
`const { nested: { answer } } = { nested: ${JSON.stringify(large)} };`,
},
])("preserves a limiting value through $name projection", ({ source }) => {
const ir = analyzeJavaScriptSemantics(source("x".repeat(200_000)));
expect(topLevelBinding(ir, "answer").value).toMatchObject({
status: "unknown",
resourceLimit: "primitive-bytes",
});
expect(ir.coverage).toMatchObject({
status: "partial",
resourceLimits: ["primitive-bytes"],
});
});
it("records an encountered bound without weakening a selected healthy sibling", () => {
const ir = analyzeJavaScriptSemantics(
`const answer = ({ ignored: ${JSON.stringify("x".repeat(200_000))}, kept: 7 }).kept;`,
);
expect(topLevelBinding(ir, "answer").value).toEqual({
status: "literal",
value: 7,
});
expect(ir.coverage).toMatchObject({
status: "partial",
resourceLimits: ["primitive-bytes"],
});
});
});
describe("configuration default normalization", () => {
it.each(["quoted", "template"])(
"normalizes a bare %s configuration default",
(syntax) => {
const large = "x".repeat(200_000);
const expression =
syntax === "quoted" ? JSON.stringify(large) : `\`${large}\``;
const ir = analyzeJavaScriptSemantics(
`process.env.MODE ?? ${expression};`,
);
const operation = ir.configurationOperations.find(
({ kind }) => kind === "default",
);
expect(
typeof operation?.value === "string" ? operation.value.length : 0,
).toBeLessThanOrEqual(174_762);
expect(operation).toMatchObject({
value: null,
resolution: "partial",
valueResourceLimit: "primitive-bytes",
});
expect(ir.coverage).toMatchObject({
status: "partial",
resourceLimits: ["primitive-bytes"],
});
},
);
it.each([null, false, 0, ""])(
"keeps an exact bounded configuration default %j",
(value) => {
const ir = analyzeJavaScriptSemantics(
`process.env.MODE ?? ${JSON.stringify(value)};`,
);
expect(
ir.configurationOperations.find(({ kind }) => kind === "default"),
).toMatchObject({ value, resolution: "complete" });
expect(ir.coverage.resourceLimits ?? []).toEqual([]);
},
);
});
describe("semantic primitive allocation and binding reuse", () => {
it.each(["quoted", "template"])(
"applies the normalized byte boundary to %s literals",
(syntax) => {
// Six estimated JSON bytes per UTF-16 code unit plus two quotes:
// 174762 code units fit; one more exceeds the 1 MiB bound.
for (const length of [174_762, 174_763]) {
const text = "x".repeat(length);
const expression =
syntax === "quoted" ? JSON.stringify(text) : `\`${text}\``;
const ir = analyzeJavaScriptSemantics(`const answer = ${expression};`);
const value = topLevelBinding(ir, "answer").value;
if (length === 174_762) {
expect(value.status).toBe("literal");
if (value.status !== "literal")
throw new Error("Expected exact value");
expect(typeof value.value === "string" && value.value.length).toBe(
length,
);
expect(ir.coverage.resourceLimits ?? []).toEqual([]);
} else {
expect(value.status).toBe("unknown");
expect(value).toMatchObject({ resourceLimit: "primitive-bytes" });
expect(ir.coverage).toMatchObject({
status: "partial",
omittedCount: null,
resourceLimits: ["primitive-bytes"],
});
}
}
},
);
it("returns an explicit unknown before expanding a large addition product", () => {
const term = '(choice ? "a" : "b")';
const expression = Array.from({ length: 20 }, () => term).join(" + ");
@@ -256,7 +369,7 @@ describe("semantic primitive allocation and binding reuse", () => {
expect(ir.coverage).toMatchObject({
status: "partial",
omittedCount: null,
resourceLimits: ["primitive-bytes"],
resourceLimits: expect.arrayContaining(["primitive-bytes"]),
});
});
@@ -915,15 +915,6 @@ export const dataEffectArgumentBindingId = (
? (resolveSemanticBindingState(state, node, node.name)?.bindingId ?? null)
: null;
/** Read one JSON primitive default without evaluating code. */
export const dataEffectPrimitive = (
node: t.Node,
): string | number | boolean | null => {
if (t.isStringLiteral(node) || t.isNumericLiteral(node)) return node.value;
if (t.isBooleanLiteral(node)) return node.value;
return null;
};
/**
* Read one static string or expression-free template. A template recovered
* with an invalid escape has no cooked value; its raw spelling is not a
@@ -8,11 +8,13 @@ import type {
} from "./javascriptSemanticIr.js";
import { compareUnicodeCodePoints } from "../unicodeCodePointOrder.js";
import {
readExactJavaScriptLiteral,
semanticStaticPropertyKey,
semanticStaticPropertyName,
unwrapJavaScriptExpression,
} from "./javascriptAstValues.js";
import {
recordSemanticValueLimit,
resolveSemanticBindingState,
semanticResolutionBlocked,
type JavaScriptSemanticAnalysisState,
@@ -22,7 +24,6 @@ import {
dataEffectArgumentBindingId as argumentBindingId,
dataEffectLiteralString as literalString,
dataEffectMemberCallee as memberCallee,
dataEffectPrimitive as primitiveValue,
isSemanticProcessObject as isProcessObject,
outerDataEffectBinding as outerAssignedBinding,
assignedSemanticResultBindings,
@@ -31,6 +32,8 @@ import {
type DataEffectTraversalContext as TraversalContext,
} from "./javascriptSemanticDataEffectHelpers.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { semanticPrimitiveSet } from "./javascriptSemanticPrimitives.js";
import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js";
interface DataEffectAnalysis {
readonly configurationOperations: readonly JavaScriptSemanticConfigurationOperation[];
@@ -175,6 +178,11 @@ const collectDefaults = (
const source = unwrapJavaScriptExpression(node.left).node;
const candidate = configurationsByNode.get(source);
if (candidate === undefined) return;
const literal = readExactJavaScriptLiteral(node.right);
const value: JavaScriptSemanticValue = literal.found
? semanticPrimitiveSet([literal.value])
: { status: "unknown", reason: "Default is not an exact primitive." };
recordSemanticValueLimit(context.state, value);
output.push({
configId: `config:default:${String(node.start ?? -1)}:${String(node.end ?? -1)}`,
kind: "default",
@@ -183,8 +191,11 @@ const collectDefaults = (
resultBindingId: outerAssignedBinding(node, context),
key: candidate.operation.key,
sourceConfigId: candidate.operation.configId,
value: primitiveValue(node.right),
resolution: primitiveValue(node.right) === null ? "partial" : "complete",
value: value.status === "literal" ? value.value : null,
...(value.status === "unknown" && value.resourceLimit !== undefined
? { valueResourceLimit: value.resourceLimit }
: {}),
resolution: value.status === "literal" ? "complete" : "partial",
});
});
return output;
@@ -1,5 +1,8 @@
import type { JavaScriptSourceRange } from "./javascriptStaticAnalysisTypes.js";
import type { JavaScriptSemanticValue } from "./javascriptSemanticValueTypes.js";
import type {
JavaScriptSemanticResourceLimit,
JavaScriptSemanticValue,
} from "./javascriptSemanticValueTypes.js";
import type {
JavaScriptSemanticCoverage,
JavaScriptSemanticReturnCoverage,
@@ -302,6 +305,7 @@ export interface JavaScriptSemanticConfigurationOperation {
readonly key: string | null;
readonly sourceConfigId: string | null;
readonly value: string | number | boolean | null;
readonly valueResourceLimit?: JavaScriptSemanticResourceLimit;
readonly resolution: "complete" | "partial";
}
@@ -14,7 +14,7 @@ import {
export const semanticPrimitiveSet = (
values: readonly JavaScriptSemanticPrimitive[],
): JavaScriptSemanticValue => {
const uniqueValues = new Set<JavaScriptSemanticPrimitive>();
let uniqueValues: Set<JavaScriptSemanticPrimitive> | undefined;
for (const value of values) {
if (typeof value === "number" && !Number.isFinite(value))
return {
@@ -26,10 +26,19 @@ export const semanticPrimitiveSet = (
status: "unknown",
reason: "Negative zero cannot be preserved by JSON primitive evidence.",
};
// The common literal path needs validation, but no deduplication, sorting
// or canonical-key allocation. Check strings before retaining the value.
if (values.length === 1)
return typeof value === "string" &&
exceedsSemanticPrimitiveStringByteBudget([value])
? semanticResourceLimitUnknown("primitive-bytes")
: { status: "literal", value };
uniqueValues ??= new Set<JavaScriptSemanticPrimitive>();
uniqueValues.add(value);
if (uniqueValues.size > SEMANTIC_PRIMITIVE_CANDIDATE_LIMIT)
return semanticResourceLimitUnknown("primitive-candidates");
}
if (uniqueValues === undefined) return { status: "union", values: [] };
if (
exceedsSemanticPrimitiveStringByteBudget(
[...uniqueValues].flatMap((value) =>
@@ -77,25 +77,6 @@ export const semanticResourceLimitUnknown = (
resourceLimit,
});
/** Collect resource-limit classifications from nested semantic values. */
export const semanticResourceLimitsIn = (
values: readonly JavaScriptSemanticValue[],
): readonly JavaScriptSemanticResourceLimit[] => {
const found = new Set<JavaScriptSemanticResourceLimit>();
const pending = [...values];
while (pending.length > 0) {
const value = pending.pop();
if (value === undefined) continue;
if (value.status === "unknown" && value.resourceLimit !== undefined)
found.add(value.resourceLimit);
else if (value.status === "object")
for (const property of value.properties) pending.push(property.value);
else if (value.status === "array")
for (const item of value.items) pending.push(item.value);
}
return [...found].sort();
};
/** Display label for one stable resource classification. */
export const semanticResourceLimitReason = (
resourceLimit: JavaScriptSemanticResourceLimit,
@@ -14,6 +14,10 @@ import type {
JavaScriptSemanticReference,
} from "./javascriptSemanticIr.js";
import type { JavaScriptModuleResolutionMode } from "./javascriptModuleResolutionMode.js";
import type {
JavaScriptSemanticResourceLimit,
JavaScriptSemanticValue,
} from "./javascriptSemanticValueTypes.js";
/** Internal initializer plus a destructuring/member projection. */
interface JavaScriptSemanticInitializer {
@@ -75,6 +79,8 @@ export interface JavaScriptSemanticAnalysisState {
readonly program: t.Program;
/** Effective loader for static imports in this source artifact. */
readonly staticImportResolutionMode: JavaScriptModuleResolutionMode;
/** Bounds encountered during this analysis, independent of later projections. */
readonly valueResourceLimits: Set<JavaScriptSemanticResourceLimit>;
readonly parentsByNode: WeakMap<t.Node, t.Node>;
readonly scopes: JavaScriptSemanticScopeState[];
readonly scopesById: Map<string, JavaScriptSemanticScopeState>;
@@ -110,6 +116,15 @@ export interface JavaScriptSemanticAnalysisState {
readonly ambientWriteEffects: JavaScriptSemanticMemberEffect[];
}
/** Record one producer outcome without traversing or retaining its value graph. */
export const recordSemanticValueLimit = (
state: JavaScriptSemanticAnalysisState,
value: JavaScriptSemanticValue,
): void => {
if (value.status === "unknown" && value.resourceLimit !== undefined)
state.valueResourceLimits.add(value.resourceLimit);
};
/** One parsed source owns its optional indexes and their per-source lifetime. */
export interface JavaScriptBindingIndex {
readonly sourceType: t.Program["sourceType"];
@@ -26,6 +26,7 @@ import type {
JavaScriptSemanticValue,
} from "./javascriptSemanticValueTypes.js";
import {
recordSemanticValueLimit,
resolveSemanticBindingState,
type JavaScriptSemanticAnalysisState,
type JavaScriptSemanticBindingState,
@@ -451,6 +452,15 @@ const evaluateBinding = (
const evaluateExpression = (
node: t.Node,
context: EvaluationContext,
): JavaScriptSemanticValue => {
const value = evaluateExpressionValue(node, context);
recordSemanticValueLimit(context.state, value);
return value;
};
const evaluateExpressionValue = (
node: t.Node,
context: EvaluationContext,
): JavaScriptSemanticValue => {
if (context.expressionDepth > SEMANTIC_EXPRESSION_DEPTH_LIMIT) {
context.dependence.lowest = EVERY_CALLER;
@@ -471,10 +481,7 @@ const evaluateExpression = (
});
}
const literal = readExactJavaScriptLiteral(node);
if (literal.found)
return typeof literal.value === "number"
? primitiveSet([literal.value])
: { status: "literal", value: literal.value };
if (literal.found) return primitiveSet([literal.value]);
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(context.state, node, node.name);
return binding === undefined
@@ -757,11 +764,12 @@ const projectValue = (
value: JavaScriptSemanticValue,
projection: readonly (string | number | null)[],
): JavaScriptSemanticValue => {
if (isSemanticResourceLimit(value)) return value;
if (projection.includes(null))
return { status: "unknown", reason: "Cannot project a dynamic property." };
const path = projection.map((key) => String(key));
const slot = semanticSlotAtPath(value, path);
return slot.presence === "present"
return slot.presence === "present" || isSemanticResourceLimit(slot.value)
? slot.value
: {
status: "unknown",
+2 -3
View File
@@ -4,7 +4,6 @@ import { join } from "node:path";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
@@ -19,6 +18,7 @@ import {
} from "./KunaProjectFiles.js";
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
import { KUNA_FUNCTION_TEXT_BYTES } from "./KunaLimits.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
/** The exact producing range for one facet, including a failed C block. */
export interface KunaProjectFileSpan {
@@ -134,8 +134,7 @@ export class KunaProjectReader {
const span = index.value.code.get(address);
if (span === undefined)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
kunaUnavailable(
operation,
`Kuna's export lists no code at ${address}; import slots and data entries have no listing.`,
),
+4 -19
View File
@@ -8,10 +8,7 @@ import type {
ProviderAvailability,
ProviderTargetSupport,
} from "../application/AnalysisProvider.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
} from "../domain/analysisErrorCore.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import {
createAnalysisProfile,
type AnalysisProfileCommitment,
@@ -33,6 +30,7 @@ import {
import { KUNA_COMMAND_REMEDIATION, KUNA_PROVIDER_ID } from "./KunaRelease.js";
import { KunaRunSlots } from "./KunaRunSlots.js";
import { KunaSessionClient } from "./KunaSessionClient.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
/** Architectures REA verified Kuna on: x86-64 and AArch64 ELF and Mach-O, plus ARM Thumb. */
const VERIFIED_ARCHITECTURES: ReadonlySet<string> = new Set([
@@ -169,14 +167,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
...(options?.signal === undefined ? {} : { signal: options.signal }),
});
if (!installation.ok)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
"open_binary",
installation.error.reason,
{ userMessage: installation.error.reason },
),
);
return err(kunaUnavailable("open_binary", installation.error.reason));
const { value } = installation;
return ok({
profile: createAnalysisProfile(
@@ -218,13 +209,7 @@ export class KunaProvider implements AnalysisProviderCandidate {
if (!configuration.ok)
return {
execute: async (operation) =>
err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
operation,
configuration.error.reason,
),
),
err(kunaUnavailable(operation, configuration.error.reason)),
close: async () => ok(null),
};
return new KunaSessionClient(target, profile, {
+2 -3
View File
@@ -1,7 +1,6 @@
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import {
AnalysisCancelledError,
AnalysisCapabilityUnavailableError,
AnalysisResourceConstraintError,
} from "../domain/analysisErrorCore.js";
import type { JsonValue } from "../domain/jsonValue.js";
@@ -21,6 +20,7 @@ import {
} from "./KunaReferenceFiles.js";
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
import type { KunaPendingQuery, KunaQueryWork } from "./KunaQueryWork.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
/** Charge raw JSON bytes conservatively; retained projections are smaller. */
const XREF_CACHE_BYTES = 16 * 1024 * 1024;
@@ -158,8 +158,7 @@ export class KunaReferenceQueries {
const node = graph.nodes.get(address);
if (node === undefined)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
kunaUnavailable(
operation,
`Kuna's graph has no function node at ${address}; its relationships are unknown.`,
),
+4 -17
View File
@@ -69,6 +69,7 @@ import {
KunaProjectReader,
type KunaProjectFileSpan,
} from "./KunaProjectReader.js";
import { kunaUnavailable } from "./KunaUnavailable.js";
/** How often a request waiting on the project export reports that it still waits. */
const WAIT_PROGRESS_INTERVAL_MS = 10_000;
@@ -278,13 +279,7 @@ export class KunaSessionClient implements AnalysisClient {
options: ExecutionOptions = {},
): Promise<Result<AnalysisExecution, AnalysisError>> {
if (this.#closing)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
operation,
"This Kuna session is closed.",
),
);
return err(kunaUnavailable(operation, "This Kuna session is closed."));
return this.#own(
this.#execute(operation, parameters, {
...options,
@@ -312,8 +307,7 @@ export class KunaSessionClient implements AnalysisClient {
return err(new AnalysisCancelledError(operation));
if (operation !== "health" && !isKunaOperation(operation))
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
kunaUnavailable(
operation,
"Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.",
),
@@ -1229,14 +1223,7 @@ export class KunaSessionClient implements AnalysisClient {
this.#installation ??= inspectKunaInstallation(this.#environment).then(
(inspected): Result<KunaInstallation, AnalysisError> => {
if (!inspected.ok)
return err(
new AnalysisCapabilityUnavailableError(
KUNA_PROVIDER_ID,
"health",
inspected.error.reason,
{ userMessage: inspected.error.reason },
),
);
return err(kunaUnavailable("health", inspected.error.reason));
const committed = this.#profile?.parameters["kuna_sha256"];
if (committed !== undefined && committed !== inspected.value.sha256)
return err(
+18
View File
@@ -0,0 +1,18 @@
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
import type { AnalysisErrorOptions } from "../domain/analysisErrorBase.js";
import { KUNA_PROVIDER_ID } from "./KunaRelease.js";
/**
* A Kuna capability gap whose reason is what the caller needs to read. Without
* a user message the projection shows only "choose another analysis or
* target", which hides facts such as "no code at this address".
*/
export const kunaUnavailable = (
operation: string,
reason: string,
options: AnalysisErrorOptions = {},
): AnalysisCapabilityUnavailableError =>
new AnalysisCapabilityUnavailableError(KUNA_PROVIDER_ID, operation, reason, {
...options,
userMessage: reason,
});
@@ -3,6 +3,7 @@ import { createHash } from "node:crypto";
import { copyFile, mkdir, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { join, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
@@ -850,18 +851,18 @@ describe("application workflow CLI parity", () => {
if (seed === undefined)
throw new TypeError("Expected at least one semantic relation");
const traced = await runCli([
const traced = await runCliWithRequestFile(
root,
"trace-javascript-semantics",
JSON.stringify({
{
application: analyzed.value,
query: {
seed: { kind: "semantic-node", node_id: seed },
direction: "forward-influence",
include_ambiguous_dynamic_edges: true,
},
}),
"--json",
]);
},
);
expect(traced).toMatchObject({
operation: "trace_javascript_semantics",
predicate_type: "rea.javascript-semantic-trace",
@@ -1183,7 +1184,9 @@ describe("application workflow CLI copy boundaries", () => {
describe("application workflow CLI and MCP capture lifetimes", () => {
it("compares captured primitives and source values after aliases are replaced", async () => {
const root = await createTestTempDirectory("rea-capture-lifetime-cli-");
const parent = await createTestTempDirectory("rea-capture-lifetime-cli-");
const root = join(parent, "fixtures # capture");
await mkdir(root);
const fields = [
"rebound",
"rest",
@@ -1247,7 +1250,9 @@ describe("application workflow CLI and MCP capture lifetimes", () => {
"--input-type=module",
"--eval",
"const module = await import(process.argv[1]); console.log(JSON.stringify(module.default()));",
join(root, String(index), "parser.mjs"),
// ESM import() takes a URL specifier; a Windows drive path parses as a
// `c:` URL scheme and is rejected.
pathToFileURL(join(root, String(index), "parser.mjs")).href,
]);
expect(JSON.parse(stdout)).toEqual({
kind: "capture-boundary",
@@ -1300,18 +1305,18 @@ describe("application workflow CLI export Evidence", () => {
'export default async function make() { return { kind: "record", extra: 1 }; }',
];
const [left, right] = await analyzeComparisonSources(root, sources);
const compared = await runCli([
const compared = await runCliWithRequestFile(
root,
"compare-javascript-export-shapes",
JSON.stringify({
{
left,
right,
left_module_path: "parser.mjs",
left_export_name: "default",
right_module_path: "parser.mjs",
right_export_name: "default",
}),
"--json",
]);
},
);
expect(compared).toMatchObject({
operation: "compare_javascript_export_shapes",
normalized_result: {
@@ -1640,3 +1645,19 @@ const runCli = async (
throw cause;
}
};
/**
* Pass a request carrying complete Evidence through the CLI's file-backed JSON
* input. Serialized Evidence exceeds the Windows command-line limit when it is
* passed as one argument, so spawn fails with ENAMETOOLONG before the workflow
* starts. Inline JSON transport keeps its own coverage in the parity tests.
*/
const runCliWithRequestFile = async (
root: string,
command: string,
request: unknown,
): Promise<unknown> => {
const requestPath = join(root, "request.json");
await writeFile(requestPath, JSON.stringify(request));
return runCli([command, requestPath, "--json"]);
};
@@ -30,9 +30,12 @@ cliTest(
surrogate,
replacement,
];
const source = values
const requests = values
.map((value) => `fetch(${JSON.stringify(value)});`)
.join("\n");
// The primitive lattice is bounded; exact endpoint lookup identities stay
// available for feature/semantic tracing and version comparison.
const source = `${requests}\nexport const oversized = ${JSON.stringify(endpoint)};`;
const sourcePath = join(root, "main.js");
await writeFile(sourcePath, source);
const cliResponse = await cli.run({
@@ -79,6 +82,17 @@ cliTest(
const result = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
);
expect(result.graph.coverage.limits).toContainEqual(
expect.objectContaining({
name: "javascript_semantic_primitive_bytes",
}),
);
expect(
result.semantic_graph.nodes.some(
({ kind, properties }) =>
kind === "literal" && properties.value === endpoint,
),
).toBe(false);
const endpoints = result.graph.nodes.filter(
(node) => node.kind === "endpoint",
);
@@ -121,18 +135,18 @@ cliTest(
available: true,
value: { kind: "source-range", source: "main.js" },
});
const requests = result.semantic_graph.nodes.filter(
const requestNodes = result.semantic_graph.nodes.filter(
(node) =>
node.kind === "request" && node.properties.endpoint === endpoint,
);
expect(requests).toHaveLength(1);
expect(requestNodes).toHaveLength(1);
await verifyEndpointWorkflows({
client,
evidenceId: evidence.evidence_id,
endpoint,
endpointNodeId: endpointNode.node_id,
requestNodeIds: requests.map(({ node_id }) => node_id),
requestNodeIds: requestNodes.map(({ node_id }) => node_id),
root,
source,
sourcePath,
@@ -4,9 +4,10 @@ import { join } from "node:path";
import { promisify } from "node:util";
import { buildBinary } from "plist";
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { analyzeInterfaceBuilderBundle } from "../../../src/artifacts/apple/InterfaceBuilderAnalysis.js";
import { InterfaceBuilderDecodeBudget } from "../../../src/artifacts/apple/InterfaceBuilderDecodeBudget.js";
import { encodeNibArchiveFixture } from "../../../src/artifacts/apple/NibArchive.fixture.js";
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
import { xcodeIbtoolAvailable } from "../../fixtures/ibtool.js";
@@ -907,6 +908,57 @@ describe("aggregate Interface Builder archive retention budget", () => {
});
});
describe("Interface Builder XML plist depth", () => {
it("bounds XML archives by plist value depth, not element depth", async () => {
const root = await createTestTempDirectory("rea-ib-depth-test-");
const bundle = join(root, "Example.app");
const resources = join(bundle, "Contents", "Resources");
await mkdir(resources, { recursive: true });
// The root dict is value depth 0, so the leaf sits at `nested + 1`. The
// plist envelope and keys add XML elements but not value depth.
const document = (nested: number) =>
`<?xml version="1.0"?><plist version="1.0"><dict>${"<key>child</key><dict>".repeat(nested)}<key>leaf</key><integer>1</integer>${"</dict>".repeat(nested)}</dict></plist>`;
await writeFile(join(resources, "AtLimit.nib"), document(127));
await writeFile(join(resources, "BeyondLimit.nib"), document(128));
const release = vi.spyOn(InterfaceBuilderDecodeBudget.prototype, "release");
const result = await analyzeInterfaceBuilderBundle({
bundlePath: bundle,
targetSha256: "e".repeat(64),
});
const released = release.mock.calls.map(([bytes]) => bytes);
release.mockRestore();
// The rejected archive returns its reservation to later documents.
expect(released).toHaveLength(1);
expect(released[0]).toBeGreaterThan(0);
expect(result.documents.map(({ relative_path }) => relative_path)).toEqual([
"Contents/Resources/AtLimit.nib",
]);
expect(result.graph.coverage).toContainEqual(
expect.objectContaining({
facet: "archive_decode",
status: "partial",
reason: "aggregate_decode_budget_exhausted",
examined: 2,
omitted: 1,
}),
);
expect(result.limitations).toContain(
"Contents/Resources/BeyondLimit.nib: omitted because aggregate decode budget exhausted.",
);
});
it("restores released representation bytes up to the initial budget", () => {
const budget = new InterfaceBuilderDecodeBudget(100);
budget.reserve(60, "reserve");
budget.release(60);
expect(budget.remainingBytes).toBe(100);
budget.release(10);
expect(budget.remainingBytes).toBe(100);
});
});
describe("bounded Interface Builder archive decoding", () => {
it("counts malformed archives against the document limit", async () => {
const root = await createTestTempDirectory("rea-ib-test-");
@@ -0,0 +1,137 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createHash } from "node:crypto";
import { readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { afterAll, beforeAll, expect } from "vitest";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { javascriptApplicationAnalysisResultSchema } from "../../../src/domain/javascript/javascriptApplicationAnalysis.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const client = new Client({ name: "literal-budget-boundary", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
cwd: process.cwd(),
env: { PATH: process.env.PATH ?? "" },
stderr: "pipe",
});
beforeAll(async () => {
await client.connect(transport);
transport.stderr?.on("data", () => undefined);
});
afterAll(async () => {
await Promise.all([client.close(), transport.close()]);
});
const large = "x".repeat(200_000);
for (const scenario of [
{ name: "quoted literal", expression: JSON.stringify(large), limited: true },
{ name: "cooked template", expression: `\`${large}\``, limited: true },
{
name: "limited member receiver",
expression: `(${JSON.stringify(large)}).field`,
limited: true,
},
{
name: "configuration-only default",
expression: `process.env.MODE ?? ${JSON.stringify(large)}`,
bare: true,
limited: true,
},
{
name: "equivalent addition",
expression: `${JSON.stringify(large.slice(0, 100_000))} + ${JSON.stringify(large.slice(100_000))}`,
limited: true,
},
{ name: "small exact control", expression: '"small"', limited: false },
]) {
cliTest(
`preserves normalized string-byte coverage for ${scenario.name}`,
async ({ cli }) => {
const root = await createTestTempDirectory("rea-literal-budget-");
const path = join(root, "main.js");
const source =
"bare" in scenario
? `${scenario.expression};\n`
: `export const payload = ${scenario.expression};\n`;
await writeFile(path, source);
const digest = createHash("sha256").update(source).digest("hex");
const cliResponse = await cli.run({
arguments: ["analyze-javascript-application", root, "--json"],
timeoutMs: 60_000,
});
expect(cliResponse.exitCode).toBe(0);
const cliEvidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(cliResponse.json);
const response = await client.callTool({
name: "analyze_javascript_application",
arguments: { input_path: root },
});
expect(response.isError).not.toBe(true);
const evidence = toolContract(
"analyze_javascript_application",
).outputSchema.parse(response.structuredContent);
expect(evidence.evidence_id).toBe(cliEvidence.evidence_id);
for (const candidate of [cliEvidence, evidence]) {
const analysis = javascriptApplicationAnalysisResultSchema.parse(
candidate.normalized_result,
);
const stringLengths = analysis.semantic_graph.nodes.flatMap(
({ properties }) =>
typeof properties.value === "string"
? [properties.value.length]
: [],
);
expect(stringLengths).not.toContain(large.length);
if (scenario.limited) {
expect(analysis.graph.coverage.status).toBe("partial");
expect(analysis.graph.coverage.limits).toContainEqual(
expect.objectContaining({
name: "javascript_semantic_primitive_bytes",
unit: "bytes",
value: 1_048_576,
}),
);
expect(analysis.semantic_graph.unknowns).toContainEqual(
expect.objectContaining({
reason: "resource-limit",
detail: expect.stringMatching(
/primitive string-byte budget exceeded/i,
),
}),
);
} else {
expect(stringLengths).toContain(5);
expect(analysis.graph.coverage.limits).toEqual([]);
}
if ("bare" in scenario) {
const node = analysis.semantic_graph.nodes.find(
({ kind, properties }) =>
kind === "config-source" && properties.source_kind === "default",
);
expect(node?.properties).toMatchObject({
value: null,
value_resource_limit: "primitive-bytes",
});
expect(analysis.semantic_graph.unknowns).toContainEqual(
expect.objectContaining({
node_id: node?.node_id,
reason: "resource-limit",
}),
);
}
expect(
analysis.graph.nodes.find((node) => node.kind === "javascript-asset")
?.identity,
).toMatchObject({ sha256: digest });
}
expect(await readFile(path, "utf8")).toBe(source);
await client.ping();
},
);
}
@@ -11,13 +11,31 @@ import { javaScriptSemanticTraceResultSchema } from "../../../src/domain/javascr
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
const boundedLargeLiteralPayloads = (): readonly string[] => {
// Preserve the former aggregate payload size while each normalized value
// stays within its independent semantic byte budget.
const values = Array.from({ length: 16 }, (_, index) =>
`rea_literal_payload_ğ\\"\0${String(index)}_`.padEnd(24, "x").repeat(4096),
);
expect(
values.reduce((total, value) => total + value.length, 0),
).toBeGreaterThanOrEqual(24 * 65_536);
expect(values.every((value) => value.length * 6 + 2 <= 1_048_576)).toBe(true);
return values;
};
cliTest(
"delivers complete large literals through CLI and default stdio MCP without metadata copies",
"delivers aggregate large literal payloads through CLI and default stdio MCP without metadata copies",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-literal-payload-");
const value = 'rea_literal_payload_ğ\\"\0'.padEnd(24, "x").repeat(65_536);
const values = boundedLargeLiteralPayloads();
const value = values[0];
if (value === undefined) throw new Error("Missing literal payload");
const source = [
`export const payload = ${JSON.stringify(value)};`,
...values.map(
(payload, index) =>
`export const payload${String(index)} = ${JSON.stringify(payload)};`,
),
'export const choice = flag ? 1 : "1";',
'export const empty = "";',
].join("\n");
@@ -52,7 +70,7 @@ cliTest(
name: "analyze_javascript_application",
arguments: { input_path: root, format: "directory" },
});
// The old payload-bearing role key/label exceed the default wire budget.
// Default wire delivery must retain every admitted value in the aggregate.
expect(response.isError).not.toBe(true);
const evidence = toolContract(
"analyze_javascript_application",
@@ -67,6 +85,15 @@ cliTest(
const graph = javascriptApplicationAnalysisResultSchema.parse(
evidence.normalized_result,
).semantic_graph;
const retainedPayloads = graph.nodes
.filter(
(node) =>
node.kind === "literal" &&
typeof node.properties.value === "string" &&
values.includes(node.properties.value),
)
.map((node) => node.properties.value);
expect(retainedPayloads.toSorted()).toEqual(values.toSorted());
const literal = graph.nodes.find(
(node) => node.kind === "literal" && node.properties.value === value,
);
@@ -0,0 +1,90 @@
import { execFile } from "node:child_process";
import { copyFile, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
const execute = promisify(execFile);
it("runs the caller-selected npm entry through Node without rewriting its inputs", async () => {
const root = await mkdtemp(join(tmpdir(), "rea-npm-invocation-"));
try {
const npmEntry = join(root, "selected npm entry.mjs");
await writeFile(
npmEntry,
`process.stdout.write(JSON.stringify({ arguments: process.argv.slice(2), cwd: process.cwd(), marker: process.env.REA_NPM_TEST_MARKER }));\n`,
);
const helper = pathToFileURL(
resolve("scripts/lib/verify-package-core.mjs"),
).href;
const program = `
const { execNpm } = await import(${JSON.stringify(helper)});
const result = await execNpm(["pack", "value with spaces"], {
cwd: ${JSON.stringify(root)},
env: { ...process.env, npm_execpath: ${JSON.stringify(npmEntry)}, REA_NPM_TEST_MARKER: "caller-selected" },
});
process.stdout.write(result.stdout);
`;
const result = await execute(
process.execPath,
["--input-type=module", "--eval", program],
{ windowsHide: true },
);
expect(JSON.parse(result.stdout)).toEqual({
arguments: ["pack", "value with spaces"],
cwd: root,
marker: "caller-selected",
});
} finally {
await rm(root, { recursive: true, force: true });
}
});
const windowsIt = process.platform === "win32" ? it : it.skip;
windowsIt(
"uses the npm entry adjacent to Node when none is selected",
async () => {
const root = await mkdtemp(join(tmpdir(), "rea-adjacent-npm-"));
try {
const node = join(root, "node.exe");
const npmEntry = join(root, "node_modules/npm/bin/npm-cli.js");
await mkdir(dirname(npmEntry), { recursive: true });
await Promise.all([
copyFile(process.execPath, node),
writeFile(
npmEntry,
`process.stdout.write(JSON.stringify({ arguments: process.argv.slice(2), cwd: process.cwd() }));\n`,
),
]);
const helper = pathToFileURL(
resolve("scripts/lib/verify-package-core.mjs"),
).href;
const program = `
const { execNpm } = await import(${JSON.stringify(helper)});
const environment = { ...process.env };
delete environment.npm_execpath;
const result = await execNpm(["pack", "value with spaces"], {
cwd: ${JSON.stringify(root)},
env: environment,
});
process.stdout.write(result.stdout);
`;
const environment = { ...process.env };
delete environment.npm_execpath;
const result = await execute(
node,
["--input-type=module", "--eval", program],
{ env: environment, windowsHide: true },
);
expect(JSON.parse(result.stdout)).toEqual({
arguments: ["pack", "value with spaces"],
cwd: root,
});
} finally {
await rm(root, { recursive: true, force: true });
}
},
);
@@ -10,6 +10,10 @@ import {
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { stopOwnedDebuggerProcess } from "../../../src/process/ownedDebuggerSession.js";
import {
ProviderProcessSupervisor,
spawnOwnedProviderProcess,
} from "../../../src/process/ProviderProcess.js";
import type {
ProviderProcessHandle,
SpawnedOwnedProviderProcess,
@@ -24,10 +28,11 @@ interface InjectedProcessControl {
exit(signal?: NodeJS.Signals): void;
}
type RizinTestManagerOptions = NonNullable<
ConstructorParameters<typeof RizinDebugSessionManager>[1]
>;
type InjectedSpawnProcess = NonNullable<
NonNullable<
ConstructorParameters<typeof RizinDebugSessionManager>[1]
>["spawnProcess"]
RizinTestManagerOptions["spawnProcess"]
>;
export const incompleteStop: typeof stopOwnedDebuggerProcess = async (
@@ -176,17 +181,47 @@ export const createInjectedProcess = (
return { spawnProcess, control };
};
export const createRizinCoverageHarness = () => {
let directory: string | undefined;
export const createRizinCoverageHarness = (
controls: {
readonly stopOwnedProcess?: (
supervisor: ProviderProcessSupervisor,
) => ReturnType<ProviderProcessSupervisor["stop"]>;
} = {},
) => {
let latestDirectory: string | undefined;
const directories = new Set<string>();
const managers: RizinDebugSessionManager[] = [];
const track = (manager: RizinDebugSessionManager): void => {
const ownedProcesses = new Set<ProviderProcessSupervisor>();
const spawnOwnedProcess: InjectedSpawnProcess = async (options) => {
const launched = await spawnOwnedProviderProcess(options);
// The fixture owns these fake children independently of the manager's
// intentionally uncertain debugger/inferior lifecycle.
ownedProcesses.add(
new ProviderProcessSupervisor(
{ ...launched, ownsProcessLifetime: true },
{ captureStdout: false, maxDiagnosticBytes: 0 },
),
);
return launched;
};
const createManager: typeof createRizinTestManager = (
environment = process.env,
options = {},
) => {
const manager = createRizinTestManager(environment, {
...options,
spawnProcess: options.spawnProcess ?? spawnOwnedProcess,
});
managers.push(manager);
return manager;
};
const makeProvider = async (
body: string,
shebang = "#!/usr/bin/env node",
): Promise<string> => {
directory = await mkdtemp(join(tmpdir(), "rea-rizin-coverage-"));
const directory = await mkdtemp(join(tmpdir(), "rea-rizin-coverage-"));
directories.add(directory);
latestDirectory = directory;
const provider = join(directory, "fake-rizin.mjs");
await writeFile(provider, `${shebang}\n${body}\n`);
await chmod(provider, 0o700);
@@ -194,13 +229,9 @@ export const createRizinCoverageHarness = () => {
};
const start = async (
provider: string,
options: ConstructorParameters<typeof RizinDebugSessionManager>[1] = {},
options: RizinTestManagerOptions = {},
): Promise<{ manager: RizinDebugSessionManager; sessionId: string }> => {
const manager = createRizinTestManager(
{ REA_RIZIN_COMMAND: provider },
options,
);
track(manager);
const manager = createManager({ REA_RIZIN_COMMAND: provider }, options);
const result = await manager.start({
path: "/tmp/coverage-target",
inferior_tty: await rizinTestInferiorTty(),
@@ -210,13 +241,12 @@ export const createRizinCoverageHarness = () => {
};
const startInjected = async (
spawnProcess: InjectedSpawnProcess,
options: ConstructorParameters<typeof RizinDebugSessionManager>[1] = {},
options: RizinTestManagerOptions = {},
) => {
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ ...options, spawnProcess },
);
track(manager);
const started = await manager.start({
path: "/tmp/injected",
inferior_tty: await rizinTestInferiorTty(),
@@ -229,17 +259,42 @@ export const createRizinCoverageHarness = () => {
.splice(0)
.map((manager) => manager.closeAll().catch(() => undefined)),
);
if (directory !== undefined)
const failures: Error[] = [];
for (const supervisor of ownedProcesses) {
const stopped =
controls.stopOwnedProcess === undefined
? await supervisor.stop()
: await controls.stopOwnedProcess(supervisor);
if (stopped.status === "incomplete") {
failures.push(
new Error(`Fake Rizin cleanup incomplete: ${stopped.reason}`),
);
} else if (supervisor.observedExit() === undefined) {
failures.push(
new Error("Fake Rizin cleanup did not observe child exit"),
);
} else {
ownedProcesses.delete(supervisor);
}
}
// Keep ownership and fixture files available when cleanup needs a retry.
if (failures.length > 0)
throw new AggregateError(failures, "Fake Rizin fixture cleanup failed");
for (const directory of directories) {
await rm(directory, { recursive: true, force: true });
directory = undefined;
directories.delete(directory);
}
latestDirectory = undefined;
};
const missingProviderPath = (): string => join(directory!, "does-not-exist");
const missingProviderPath = (): string =>
join(latestDirectory!, "does-not-exist");
return {
cleanup,
makeProvider,
missingProviderPath,
start,
startInjected,
track,
createManager,
spawnOwnedProcess,
};
};
@@ -1,12 +1,13 @@
import { randomUUID } from "node:crypto";
import { access } from "node:fs/promises";
import { afterEach, expect, it, vi } from "vitest";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import {
createRizinTestManager,
stopFakeNoInferior,
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { rizinCommand } from "../../../src/rizin/RizinCommand.js";
import { spawnOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import * as providerProcess from "../../../src/process/ProviderProcess.js";
import type { SpawnedOwnedProviderProcess } from "../../../src/process/ProviderProcess.js";
import {
createRizinCoverageHarness,
createInjectedProcess,
@@ -20,7 +21,8 @@ const {
missingProviderPath,
start,
startInjected,
track,
createManager,
spawnOwnedProcess,
} = createRizinCoverageHarness();
afterEach(cleanup);
@@ -40,7 +42,7 @@ it.skipIf(process.platform === "win32")(
it.skipIf(process.platform === "win32")(
"rejects unsupported, pre-cancelled, and failed startup requests",
async () => {
const windows = createRizinTestManager({}, { platform: "win32" });
const windows = createManager({}, { platform: "win32" });
expect(
(
await windows.start({
@@ -61,20 +63,20 @@ it.skipIf(process.platform === "win32")(
).toBe(false);
const failed = await makeProvider("process.exit(9);");
const manager = createRizinTestManager({
const manager = createManager({
REA_RIZIN_COMMAND: failed,
});
track(manager);
const result = await manager.start({
path: "/tmp/a",
inferior_tty: await rizinTestInferiorTty(),
});
expect(result.ok).toBe(false);
const missing = createRizinTestManager({
const missing = createManager({
REA_RIZIN_COMMAND: missingProviderPath(),
});
track(missing);
const spawnFailure = await missing.start({
path: "/tmp/a",
inferior_tty: await rizinTestInferiorTty(),
@@ -87,10 +89,10 @@ it.skipIf(process.platform === "win32")(
"returns a startup timeout when Rizin exits before its initial frame",
async () => {
const provider = await makeProvider("process.exit(0);");
const manager = createRizinTestManager({
const manager = createManager({
REA_RIZIN_COMMAND: provider,
});
track(manager);
const result = await manager.start({
path: "/tmp/early-exit",
inferior_tty: await rizinTestInferiorTty(),
@@ -110,14 +112,14 @@ it.skipIf(process.platform === "win32")(
startup: () => undefined,
beforeSpawnReturns: resolveSpawned,
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{
spawnProcess: injected.spawnProcess,
stopProcess: async () => ({ status: "stopped" }),
},
);
track(manager);
vi.useFakeTimers({ toFake: ["setTimeout", "clearTimeout"] });
try {
const starting = manager.start({
@@ -139,7 +141,7 @@ it.skipIf(process.platform === "win32")(
"does not spawn Rizin when cancellation arrives during TTY validation",
async () => {
let spawned = false;
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{
spawnProcess: async () => {
@@ -164,7 +166,7 @@ it.skipIf(process.platform === "win32")(
"rejects a non-TTY path before spawning Rizin",
async () => {
let spawned = false;
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{
spawnProcess: async () => {
@@ -195,7 +197,7 @@ it.skipIf(process.platform === "win32")(
releaseSpawns = resolve;
});
const injected = createInjectedProcess();
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{
spawnProcess: async (options) => {
@@ -240,10 +242,9 @@ it.skipIf(process.platform === "win32")(
const provider = await makeProvider(
'process.stdout.write("x".repeat(16 * 1024 * 1024 + 1) + "\\0"); process.stdin.resume();',
);
const manager = createRizinTestManager({
const manager = createManager({
REA_RIZIN_COMMAND: provider,
});
track(manager);
const result = await manager.start({
path: "/tmp/oversized-startup",
@@ -264,19 +265,19 @@ it.skipIf(process.platform === "win32")(
const provider = await makeProvider(
'setTimeout(() => process.stdout.write("\\0"), 500); process.stdin.resume();',
);
const manager = createRizinTestManager(
const manager = createManager(
{
REA_RIZIN_COMMAND: provider,
},
{
spawnProcess: async (options) => {
const launched = await spawnOwnedProviderProcess(options);
const launched = await spawnOwnedProcess(options);
resolveSpawned();
return launched;
},
},
);
track(manager);
const abort = new AbortController();
const starting = manager.start(
{ path: "/tmp/cancel-start", inferior_tty: await rizinTestInferiorTty() },
@@ -288,39 +289,138 @@ it.skipIf(process.platform === "win32")(
},
);
it.skipIf(process.platform === "win32")(
"preserves startup cleanup uncertainty after cancellation",
async () => {
it
.skipIf(process.platform === "win32")
.each(["is uncertain", "reports stopped before exit"] as const)(
"cleans startup fixtures after cancellation when manager stop %s",
async (stopMode) => {
const provider = await makeProvider(
"process.stdin.resume(); setInterval(() => {}, 1000);",
);
let resolveSpawned!: () => void;
const spawned = new Promise<void>((resolve) => {
resolveSpawned = resolve;
});
const provider = await makeProvider(
let ownedLaunch: SpawnedOwnedProviderProcess | undefined;
const nativeSpawn = providerProcess.spawnOwnedProviderProcess;
const spy = vi
.spyOn(providerProcess, "spawnOwnedProviderProcess")
.mockImplementation(async (options) => {
const launched = await nativeSpawn(options);
ownedLaunch = launched;
resolveSpawned();
return launched;
});
try {
const stopProcess: typeof incompleteStop =
stopMode === "is uncertain"
? incompleteStop
: async () => ({ status: "stopped" });
const manager = createManager(
{ REA_RIZIN_COMMAND: provider },
{ stopProcess },
);
const abort = new AbortController();
const starting = manager.start(
{
path: "/tmp/cancel-start",
inferior_tty: await rizinTestInferiorTty(),
},
abort.signal,
);
await spawned;
abort.abort();
const result = await starting;
expect(result.ok).toBe(false);
if (!result.ok && stopMode === "is uncertain")
expect(result.error.message).toContain("injected stop uncertainty");
await cleanup();
if (ownedLaunch === undefined)
throw new Error("Fixture launch was not observed");
expect(
ownedLaunch.process.exitCode !== null ||
ownedLaunch.process.signalCode !== null,
"harness returned before fixture child exit",
).toBe(true);
} finally {
spy.mockRestore();
// A failing regression must also reap its observed, directly owned child.
if (
ownedLaunch !== undefined &&
ownedLaunch.process.exitCode === null &&
ownedLaunch.process.signalCode === null
) {
const safety = new providerProcess.ProviderProcessSupervisor(
{ ...ownedLaunch, ownsProcessLifetime: true },
{ captureStdout: false, maxDiagnosticBytes: 0 },
);
expect((await safety.stop()).status).not.toBe("incomplete");
expect(safety.observedExit()).toBeDefined();
}
}
},
);
it.skipIf(process.platform === "win32")(
"retains owned fixture processes and files until a failed cleanup can complete",
async () => {
let failCleanup = true;
const fixture = createRizinCoverageHarness({
stopOwnedProcess: async (supervisor) =>
failCleanup
? { status: "incomplete", reason: "injected fixture stop failure" }
: supervisor.stop(),
});
const provider = await fixture.makeProvider(
"process.stdin.resume(); setInterval(() => {}, 1000);",
);
const manager = createRizinTestManager(
{ REA_RIZIN_COMMAND: provider },
{
stopProcess: incompleteStop,
spawnProcess: async (options) => {
const launched = await spawnOwnedProviderProcess(options);
resolveSpawned();
return launched;
},
},
);
track(manager);
const abort = new AbortController();
const starting = manager.start(
{ path: "/tmp/cancel-start", inferior_tty: await rizinTestInferiorTty() },
abort.signal,
);
await spawned;
abort.abort();
const result = await starting;
expect(result.ok).toBe(false);
if (!result.ok)
expect(result.error.message).toContain("injected stop uncertainty");
const launched = await fixture.spawnOwnedProcess({
command: provider,
arguments: [],
runId: randomUUID(),
stdin: "pipe",
});
try {
await expect(fixture.cleanup()).rejects.toMatchObject({
message: "Fake Rizin fixture cleanup failed",
errors: [
expect.objectContaining({
message: expect.stringContaining("injected fixture stop failure"),
}),
],
});
expect(launched.process.exitCode).toBeNull();
expect(launched.process.signalCode).toBeNull();
await access(provider);
const nextProvider = await fixture.makeProvider("process.exit(0);");
failCleanup = false;
await fixture.cleanup();
expect(
launched.process.exitCode !== null ||
launched.process.signalCode !== null,
).toBe(true);
await expect(access(provider)).rejects.toMatchObject({ code: "ENOENT" });
await expect(access(nextProvider)).rejects.toMatchObject({
code: "ENOENT",
});
} finally {
failCleanup = false;
try {
await fixture.cleanup();
} finally {
if (
launched.process.exitCode === null &&
launched.process.signalCode === null
) {
const safety = new providerProcess.ProviderProcessSupervisor(
{ ...launched, ownsProcessLifetime: true },
{ captureStdout: false, maxDiagnosticBytes: 0 },
);
expect((await safety.stop()).status).not.toBe("incomplete");
expect(safety.observedExit()).toBeDefined();
}
}
}
},
);
@@ -1,9 +1,5 @@
import { afterEach, expect, it } from "vitest";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import {
createRizinTestManager,
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
import {
createRizinCoverageHarness,
abortOnCheck,
@@ -11,7 +7,7 @@ import {
incompleteStop,
} from "./RizinDebugSessionCoverage.fixtures.js";
const { cleanup, makeProvider, start, startInjected, track } =
const { cleanup, makeProvider, start, startInjected, createManager } =
createRizinCoverageHarness();
afterEach(cleanup);
@@ -88,7 +84,7 @@ it.skipIf(process.platform === "win32")(
async () => {
for (const spawnError of [new Error("injected spawn failure"), "opaque"]) {
const injected = createInjectedProcess({ spawnError });
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -102,7 +98,7 @@ it.skipIf(process.platform === "win32")(
const injected = createInjectedProcess({
startup: (control) => control.closeStdin(),
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -124,7 +120,7 @@ it.skipIf(process.platform === "win32")(
beforeSpawnReturns: () => abort.abort(),
spawnError: new Error("spawn interrupted"),
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -147,7 +143,7 @@ it.skipIf(process.platform === "win32")(
abort.abort();
},
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -166,7 +162,7 @@ it.skipIf(process.platform === "win32")(
const injected = createInjectedProcess({
beforeSpawnReturns: () => abort.abort(),
});
const manager = createRizinTestManager(
const manager = createManager(
{ REA_RIZIN_COMMAND: "injected-rizin" },
{ spawnProcess: injected.spawnProcess },
);
@@ -1,17 +1,13 @@
import { afterEach, expect, it, vi } from "vitest";
import { AnalysisInputError } from "../../../src/domain/analysisErrorCore.js";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import {
createRizinTestManager,
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
import {
createRizinCoverageHarness,
abortOnCheck,
createInjectedProcess,
} from "./RizinDebugSessionCoverage.fixtures.js";
const { cleanup, startInjected, track } = createRizinCoverageHarness();
const { cleanup, startInjected, createManager } = createRizinCoverageHarness();
afterEach(cleanup);
it.skipIf(process.platform === "win32")(
@@ -1,17 +1,13 @@
import { afterEach, expect, it } from "vitest";
import { evidenceSchema } from "../../../src/domain/evidence.js";
import { RizinDebugSessionManager } from "../../../src/rizin/RizinDebugSessionManager.js";
import {
createRizinTestManager,
rizinTestInferiorTty,
} from "./RizinTestInferiorTty.js";
import { rizinTestInferiorTty } from "./RizinTestInferiorTty.js";
import {
createRizinCoverageHarness,
createInjectedProcess,
incompleteStop,
} from "./RizinDebugSessionCoverage.fixtures.js";
const { cleanup, makeProvider, start, startInjected, track } =
const { cleanup, makeProvider, start, startInjected, createManager } =
createRizinCoverageHarness();
afterEach(cleanup);
@@ -80,3 +80,41 @@ it("reports a graph beyond REA's buffer once per session instead of rebuilding i
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
it("says why Kuna cannot answer instead of a generic unavailable message", async () => {
const { open } = await session("rea-kuna-unavailable-reasons-");
const client = open();
try {
const cases = [
{
operation: "procedure_assembly",
parameters: { procedure: "0x2000" },
message:
"Kuna's export lists no code at 0x2000; import slots and data entries have no listing.",
},
{
operation: "procedure_callers",
parameters: { procedure: "0x1004" },
message:
"Kuna's graph has no function node at 0x1004; its relationships are unknown.",
},
{
operation: "list_segments",
parameters: {},
message:
"Kuna answers procedure and string queries, pseudocode, assembly and references; this operation needs another provider.",
},
] as const;
for (const { operation, parameters, message } of cases) {
const result = await client.execute(operation, parameters);
if (result.ok) throw new Error(`${operation} unexpectedly succeeded`);
expect(projectAnalysisError(result.error)).toMatchObject({
code: "capability_unavailable",
message,
retryable: false,
});
}
} finally {
expect(await client.close()).toEqual({ ok: true, value: null });
}
});
+17 -2
View File
@@ -1,10 +1,25 @@
import type { MipsElfMetadata } from "../../../src/domain/binaryTargetTypes.js";
import type {
MipsAbiFlags,
MipsElfMetadata,
MipsGnuAttributes,
} from "../../../src/domain/binaryTargetTypes.js";
import type { JsonValue } from "../../../src/domain/jsonValue.js";
export type MipsReadelfMetadata = MipsElfMetadata & {
readonly abiFlags: MipsAbiFlags;
readonly gnuAttributes: MipsGnuAttributes | null;
};
export function expectedMipsElfProfile(
mips: MipsReadelfMetadata,
): Readonly<Record<string, JsonValue>>;
export type MipsFloatAbi = "hard" | "soft";
export interface MipsReadelfFacts {
mips: MipsElfMetadata;
mips: MipsReadelfMetadata & { readonly gnuAttributes: null };
symbols: Record<string, string>;
}
export function parseMipsReadelfSectionTypes(text: string): readonly string[];
export function parseMipsReadelf(
text: string,
byteOrder: string,
+65 -1
View File
@@ -23,6 +23,65 @@ const SOFT_FLOAT_SYMBOLS = [
"rea_mips_soft_caller",
];
/** Complete indexed ELF32 section types shared by the independent fixture readers. */
export function parseMipsReadelfSectionTypes(text) {
const counts = [
...text.matchAll(/^\s*Number of section headers:\s*(\d+)\s*$/gmu),
];
assert.equal(counts.length, 1, "Missing or ambiguous readelf section count");
const sectionCount = Number(counts[0][1]);
assert.ok(sectionCount > 0, "Fixture must have a section header table");
const sections = [...text.matchAll(/^[ \t]*\[[ \t]*(\d+)\][ \t]+(.+)$/gmu)];
assert.equal(
sections.length,
sectionCount,
"Incomplete readelf section table",
);
return sections.map((section, index) => {
assert.equal(Number(section[1]), index, "Ambiguous readelf section index");
const columns =
/^(?:\S+[ \t]+)?(\S+)[ \t]+[\da-f]{8}[ \t]+[\da-f]+[ \t]+[\da-f]+[ \t]+[\da-f]{2}[ \t]+(?:[A-Za-z]+[ \t]+)?\d+[ \t]+\d+[ \t]+\d+[ \t]*$/u.exec(
section[2],
);
assert.ok(columns !== null, "Malformed readelf section header");
return columns[1];
});
}
/** Independent wire-format expectation shared by generic and PSP snapshots. */
export function expectedMipsElfProfile(mips) {
const abi = mips.abiFlags;
assert.ok(abi != null, "Fixture ABI declarations must be inspected");
assert.notEqual(
mips.gnuAttributes,
undefined,
"Fixture GNU attributes must be inspected",
);
return {
elf_class: mips.elfClass,
byte_order: mips.byteOrder,
type: mips.type,
flags: mips.flags,
gnu_attributes:
mips.gnuAttributes === null
? null
: { fp_abi: mips.gnuAttributes.fpAbi, msa: mips.gnuAttributes.msa },
abi_flags: {
version: abi.version,
isa_level: abi.isaLevel,
isa_revision: abi.isaRevision,
gpr_size: abi.gprSize,
cpr1_size: abi.cpr1Size,
cpr2_size: abi.cpr2Size,
fp_abi: abi.fpAbi,
isa_extension: abi.isaExtension,
ases: abi.ases,
flags1: abi.flags1,
flags2: abi.flags2,
},
};
}
/**
* Parse only the documented GNU readelf fields needed by our tiny fixtures.
* `rel` objects carry their own symbol checks in mips-relocatable-checks.mjs.
@@ -51,6 +110,10 @@ export function parseMipsReadelf(
objectType === "rel" ? /^REL\b/u : /^EXEC\b/u,
);
assert.match(field(/^\s*Machine:\s*(.+)$/gmu, "machine"), /^MIPS\b/u);
assert.ok(
!parseMipsReadelfSectionTypes(text).includes("GNU_ATTRIBUTES"),
"Fixture unexpectedly declares GNU attributes",
);
const flags = Number(field(/^\s*Flags:\s*(0x[\da-f]+),.*$/gmu, "flags"));
assert.equal(flags, 0x70001001, "Fixture ELF flags differ from build intent");
assert.equal(field(/^ISA:\s*(.+)$/gmu, "ISA"), "MIPS32r2");
@@ -123,6 +186,7 @@ export function parseMipsReadelf(
type: objectType === "rel" ? 1 : 2,
flags,
abiFlags,
gnuAttributes: null,
},
symbols,
};
@@ -144,7 +208,7 @@ export async function inspectMipsReadelf(path, byteOrder, floatAbi = "hard") {
);
const result = await execute(
command,
["-h", "-A", "-s", "-W", path],
["-h", "-A", "-S", "-s", "-W", path],
options,
);
assert.equal(
+106 -2
View File
@@ -7,7 +7,7 @@ import {
parseMipsReadelf,
} from "./mips-checks.mjs";
// GNU readelf 2.40 output for the source-owned Clang 17 / LLD fixture.
// GNU readelf 2.44 output for the source-owned Clang 19 / LLD 19 fixture.
// This tests the reader boundary, not Ghidra; the real lane rebuilds both orders.
const readelf = `ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
@@ -21,7 +21,7 @@ const readelf = `ELF Header:
Version: 0x1
Entry point address: 0x201b0
Start of program headers: 52 (bytes into file)
Start of section headers: 2996 (bytes into file)
Start of section headers: 2624 (bytes into file)
Flags: 0x70001001, noreorder, o32, mips32r2
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
@@ -30,6 +30,38 @@ const readelf = `ELF Header:
Number of section headers: 24
Section header string table index: 22
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .MIPS.abiflags MIPS_ABIFLAGS 00010118 000118 000018 18 A 0 0 8
[ 2] .reginfo MIPS_REGINFO 00010130 000130 000018 18 A 0 0 4
[ 3] .rodata PROGBITS 00010148 000148 00001e 00 A 0 0 1
[ 4] .text PROGBITS 00020170 000170 0000f0 00 AX 0 0 16
[ 5] .data PROGBITS 00030260 000260 000000 00 WA 0 0 16
[ 6] .got PROGBITS 00030260 000260 000008 00 WAp 0 0 16
[ 7] .sdata PROGBITS 00030268 000268 000004 00 WAp 0 0 4
[ 8] .bss NOBITS 00030270 00026c 000000 00 WA 0 0 16
[ 9] .mdebug.abi32 PROGBITS 00000000 00026c 000000 00 0 0 1
[10] .pdr PROGBITS 00000000 00026c 000060 00 0 0 4
[11] .debug_abbrev MIPS_DWARF 00000000 0002cc 0000b3 00 0 0 1
[12] .debug_info MIPS_DWARF 00000000 00037f 00015e 00 0 0 1
[13] .debug_str_offsets MIPS_DWARF 00000000 0004dd 000040 00 0 0 1
[14] .debug_str MIPS_DWARF 00000000 00051d 0000ef 01 MS 0 0 1
[15] .debug_addr MIPS_DWARF 00000000 00060c 000018 00 0 0 1
[16] .comment PROGBITS 00000000 000624 00003e 01 MS 0 0 1
[17] .debug_frame MIPS_DWARF 00000000 000664 00004c 00 0 0 4
[18] .debug_line MIPS_DWARF 00000000 0006b0 0000f8 00 0 0 1
[19] .debug_line_str MIPS_DWARF 00000000 0007a8 000098 01 MS 0 0 1
[20] .debug_aranges MIPS_DWARF 00000000 000840 000020 00 0 0 1
[21] .symtab SYMTAB 00000000 000860 000090 10 23 4 4
[22] .shstrtab STRTAB 00000000 0008f0 0000f0 00 0 0 1
[23] .strtab STRTAB 00000000 0009e0 00005f 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
D (mbind), p (processor specific)
Symbol table '.symtab' contains 9 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 00000000 0 NOTYPE LOCAL DEFAULT UND
@@ -54,6 +86,15 @@ ASEs:
None
FLAGS 1: 00000000
FLAGS 2: 00000000
Static GOT:
Canonical gp value: 00038250
Reserved entries:
Address Access Value
00030260 -32752(gp) 00000000
00030264 -32748(gp) 80000000
`;
const sha256 = "a".repeat(64);
const image = {
@@ -94,6 +135,7 @@ describe("MIPS real-verifier independent expectations", () => {
elfClass: 32,
byteOrder: "little",
flags: 0x70001001,
gnuAttributes: null,
abiFlags: {
isaLevel: 32,
isaRevision: 2,
@@ -229,6 +271,67 @@ describe("MIPS real-verifier independent expectations", () => {
});
});
describe("MIPS real-verifier GNU attribute coverage", () => {
it.each(["exec", "rel"] as const)(
"reports verified GNU attribute absence for both byte orders of %s fixtures",
(objectType) => {
for (const byteOrder of ["little", "big"]) {
const text = readelf
.replace("little endian", `${byteOrder} endian`)
.replace(
"EXEC (Executable file)",
objectType === "rel"
? "REL (Relocatable file)"
: "EXEC (Executable file)",
);
expect(
parseMipsReadelf(text, byteOrder, "hard", objectType).mips
.gnuAttributes,
).toBeNull();
}
},
);
it("rejects GNU attribute sections even when their name is unrelated", () => {
expect(() =>
parseMipsReadelf(
readelf.replace(
".comment PROGBITS",
".comment GNU_ATTRIBUTES",
),
"little",
),
).toThrow(/GNU attributes/u);
});
it("uses the section type rather than its name to establish absence", () => {
expect(
parseMipsReadelf(
readelf.replace(
".comment PROGBITS",
".gnu.attributes PROGBITS",
),
"little",
).mips.gnuAttributes,
).toBeNull();
});
it.each([
[
"missing table",
readelf.replace(
/Section Headers:[\s\S]*?Key to Flags:/u,
"Key to Flags:",
),
],
["missing entry", readelf.replace(/^.*\[ 5\].*\n/gmu, "")],
["duplicate index", readelf.replace("[ 5]", "[ 4]")],
["malformed entry", readelf.replace(/^.*\[ 5\].*$/gmu, " [ 5] malformed")],
])("refuses GNU attribute absence from a %s", (_name, text) => {
expect(() => parseMipsReadelf(text, "little")).toThrow(/section/u);
});
});
describe("MIPS soft-float real-verifier expectations", () => {
const softReadelf = readelf
.replace("CPR1 size: 32", "CPR1 size: 0")
@@ -249,6 +352,7 @@ describe("MIPS soft-float real-verifier expectations", () => {
it("reads the soft-float ABI record and its fixture symbols", () => {
const parsed = parseMipsReadelf(softReadelf, "little", "soft");
expect(parsed.mips.abiFlags).toMatchObject({ cpr1Size: 0, fpAbi: 3 });
expect(parsed.mips.gnuAttributes).toBeNull();
expect(parsed.symbols.rea_mips_soft_mixed).toBe("0x20380");
});
+142 -14
View File
@@ -1,6 +1,9 @@
import assert from "node:assert/strict";
import { it } from "vitest";
import { parsePspReadelfAbi } from "./psp-fixture.mjs";
import { expectedMipsElfProfile } from "./mips-checks.mjs";
import { ghidraMipsProfileParameters } from "../../../src/ghidra/GhidraMipsProfile.js";
import type { BinaryTarget } from "../../../src/domain/binaryTargetTypes.js";
import { parsePspReadelfMips } from "./psp-fixture.mjs";
// GNU readelf 2.44 output from the maintained PSPDEV v20261001 fixture.
const observed = `MIPS ABI Flags Version: 0
@@ -16,19 +19,42 @@ FLAGS 1: 00000001
FLAGS 2: 00000000
`;
// Constructed section-table controls combined with the captured ABI declaration;
// this does not establish a real PSPSDK or Allegrex-provider run.
const metadata = `ELF Header:
Class: ELF32
Data: 2's complement, little endian
Type: EXEC (Executable file)
Machine: MIPS R3000
Flags: 0x10a23001, noreorder, allegrex, eabi32, mips2
Number of section headers: 3
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .MIPS.abiflags MIPS_ABIFLAGS 08800118 000118 000018 18 A 0 0 8
[ 2] .text PROGBITS 08800130 000130 000010 00 AX 0 0 16
${observed}`;
it("reads the independent PSP producer's single-float declaration", () => {
assert.deepEqual(parsePspReadelfAbi(observed), {
version: 0,
isaLevel: 2,
isaRevision: 0,
gprSize: 1,
cpr1Size: 1,
cpr2Size: 0,
fpAbi: 2,
isaExtension: 0,
ases: 0,
flags1: 1,
flags2: 0,
assert.deepEqual(parsePspReadelfMips(metadata), {
elfClass: 32,
byteOrder: "little",
type: 2,
flags: 0x10a23001,
gnuAttributes: null,
abiFlags: {
version: 0,
isaLevel: 2,
isaRevision: 0,
gprSize: 1,
cpr1Size: 1,
cpr2Size: 0,
fpAbi: 2,
isaExtension: 0,
ases: 0,
flags1: 1,
flags2: 0,
},
});
});
@@ -46,6 +72,108 @@ it.each([
])(
"rejects unsupported or ambiguous independent ABI output: %s",
(before, after) => {
assert.throws(() => parsePspReadelfAbi(observed.replace(before, after)));
assert.throws(() => parsePspReadelfMips(metadata.replace(before, after)));
},
);
it.each([
[
"missing table",
metadata.replace(
/Section Headers:[\s\S]*?MIPS ABI Flags/u,
"MIPS ABI Flags",
),
],
["incomplete table", metadata.replace(/^.*\[ 2\].*\n/gmu, "")],
["duplicate index", metadata.replace("[ 2]", "[ 1]")],
[
"GNU section under another name",
metadata.replace(
".text PROGBITS",
".text GNU_ATTRIBUTES",
),
],
])("refuses PSP GNU attribute absence from a %s", (_name, text) => {
assert.throws(() => parsePspReadelfMips(text), /section|GNU attributes/u);
});
it.each([
["Class: ELF32", "Class: ELF64"],
["little endian", "big endian"],
["Type: EXEC", "Type: REL"],
["Flags: 0x10a23001", "Flags: 0x70001001"],
["Class: ELF32", "Class: ELF32\n Class: ELF32"],
])("refuses contradictory or ambiguous PSP headers: %s", (before, after) => {
assert.throws(() => parsePspReadelfMips(metadata.replace(before, after)));
});
// GNU readelf 2.40 independently decoded this declaration from a constructed
// ELF32/PSP-shaped control, not a PSPSDK build or a real Allegrex import.
const gnuDeclaration = `Attribute Section: gnu
File Attributes
Tag_GNU_MIPS_ABI_FP: Hard float (single precision)
`;
const gnuMetadata = metadata.replace(
".text PROGBITS",
".text GNU_ATTRIBUTES",
);
it.each(["before", "after"])(
"retains the independent single-float GNU declaration %s the ABI dump",
(position) => {
const text =
position === "before"
? gnuMetadata.replace(observed, `${gnuDeclaration}\n${observed}`)
: `${gnuMetadata}\n${gnuDeclaration}`;
assert.deepEqual(parsePspReadelfMips(text), {
...parsePspReadelfMips(metadata),
gnuAttributes: { fpAbi: 2, msa: null },
});
},
);
it.each([
["unknown vendor", gnuDeclaration.replace("gnu", "aeabi")],
["wrong float ABI", gnuDeclaration.replace("single", "double")],
["unknown tag", `${gnuDeclaration} Tag_GNU_Unknown: 1\n`],
[
"unsupported scope",
gnuDeclaration.replace("File Attributes", "Section Attributes: 1"),
],
["duplicate declaration", `${gnuDeclaration}${gnuDeclaration}`],
])("refuses %s rather than inventing PSP GNU facts", (_name, declaration) => {
assert.throws(
() => parsePspReadelfMips(`${gnuMetadata}\n${declaration}`),
/GNU attributes/u,
);
});
it("refuses undeclared GNU attributes rather than reporting absence", () => {
assert.throws(
() => parsePspReadelfMips(`${metadata}\n${gnuDeclaration}`),
/GNU attributes/u,
);
});
it.each([
["absent", metadata],
["declared", `${gnuMetadata}\n${gnuDeclaration}`],
])(
"matches production snapshot serialization for %s GNU facts",
(_name, text) => {
const mips = parsePspReadelfMips(text);
const target = {
path: "/fixture/psp.elf",
sha256: "a".repeat(64),
kind: "executable",
format: "elf",
architecture: "mips",
availableArchitectures: ["mips"],
mips,
} satisfies BinaryTarget;
assert.deepEqual(
expectedMipsElfProfile(mips),
ghidraMipsProfileParameters(target).mips_elf,
);
},
);
+2 -2
View File
@@ -1,3 +1,3 @@
import type { MipsAbiFlags } from "../../../src/domain/binaryTargetTypes.js";
import type { MipsReadelfMetadata } from "./mips-checks.mjs";
export declare function parsePspReadelfAbi(text: string): MipsAbiFlags;
export declare function parsePspReadelfMips(text: string): MipsReadelfMetadata;
+56 -16
View File
@@ -4,6 +4,7 @@ import { createHash } from "node:crypto";
import { copyFile, readFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { parseMipsReadelfSectionTypes } from "./mips-checks.mjs";
const execute = promisify(execFile);
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
@@ -42,15 +43,11 @@ export async function buildPspFixture(directory, globalValue = 7) {
assert.equal(bytes.readUInt32LE(36), 0x10a23001);
const readelf = await execute(
"psp-readelf",
["-h", "-A", "-sW", path],
["-h", "-A", "-S", "-sW", path],
options,
);
assert.equal(readelf.stderr.trim(), "");
assert.match(readelf.stdout, /Class:\s+ELF32/u);
assert.match(readelf.stdout, /Data:\s+2's complement, little endian/u);
assert.match(readelf.stdout, /Type:\s+EXEC\b/u);
assert.match(readelf.stdout, /Flags:\s+0x10a23001\b/u);
const abiFlags = parsePspReadelfAbi(readelf.stdout);
const mips = parsePspReadelfMips(readelf.stdout);
const abiDump = await execute(
"psp-readelf",
["-x", ".MIPS.abiflags", path],
@@ -99,13 +96,7 @@ export async function buildPspFixture(directory, globalValue = 7) {
source_sha256: sourceDigests,
symbols,
flags: "0x10a23001",
mips: {
elfClass: 32,
byteOrder: "little",
type: 2,
flags: 0x10a23001,
abiFlags,
},
mips,
abi_record_hex_dump: abiDump.stdout,
global_value: globalValue,
versions,
@@ -115,8 +106,8 @@ export async function buildPspFixture(directory, globalValue = 7) {
};
}
/** Independent GNU fields for the pinned PSPSDK fixture, not REA's parser. */
export function parsePspReadelfAbi(text) {
/** Complete independent fields for the pinned PSPSDK fixture, not REA's parser. */
export function parsePspReadelfMips(text) {
const field = (expression, label) => {
const matches = [...text.matchAll(expression)];
assert.equal(
@@ -126,6 +117,13 @@ export function parsePspReadelfAbi(text) {
);
return matches[0][1].trim();
};
assert.equal(field(/^\s*Class:\s*(.+)$/gmu, "class"), "ELF32");
assert.equal(
field(/^\s*Data:.*?,\s*(little|big) endian\s*$/gmu, "byte order"),
"little",
);
assert.match(field(/^\s*Type:\s*(.+)$/gmu, "type"), /^EXEC\b/u);
assert.match(field(/^\s*Flags:\s*(.+)$/gmu, "flags"), /^0x10a23001\b/u);
const width = (name) => {
const bits = Number(
field(new RegExp(`^${name} size:\\s*(\\d+)\\s*$`, "gmu"), name),
@@ -178,7 +176,49 @@ export function parsePspReadelfAbi(text) {
},
"PSPSDK ABI declaration differs from independently inspected build intent",
);
return abi;
return {
elfClass: 32,
byteOrder: "little",
type: 2,
flags: 0x10a23001,
abiFlags: abi,
gnuAttributes: parsePspGnuAttributes(text),
};
}
// The maintained PSP fixture is single-float. Retain that independently decoded
// GNU declaration when present, and refuse other tags/vendors/scopes rather
// than confusing an uninspected or unsupported declaration with absence.
function parsePspGnuAttributes(text) {
const count = parseMipsReadelfSectionTypes(text).filter(
(type) => type === "GNU_ATTRIBUTES",
).length;
const headers = [...text.matchAll(/^Attribute Section: (.+)$/gmu)];
if (count === 0) {
assert.equal(
headers.length,
0,
"GNU attributes without a declared section",
);
return null;
}
assert.equal(count, 1, "Ambiguous GNU attributes sections");
assert.equal(headers.length, 1, "Missing or ambiguous GNU attributes output");
const header = headers[0];
assert.equal(header[1].trim(), "gnu", "Unsupported GNU attributes vendor");
const tail = text.slice(header.index + header[0].length);
// GNU attributes may precede or follow the architecture-specific ABI dump.
const end = tail.search(/^MIPS ABI Flags Version:/mu);
const lines = (end < 0 ? tail : tail.slice(0, end))
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(Boolean);
assert.deepEqual(
lines,
["File Attributes", "Tag_GNU_MIPS_ABI_FP: Hard float (single precision)"],
"Unsupported or incomplete PSP GNU attributes declaration",
);
return { fpAbi: 2, msa: null };
}
/** Observations from Ghidra, not an echo of the requested profile. */
@@ -69,6 +69,12 @@ describe("curated Windows test lane", () => {
"tests/boundary/process/windowsOutputCapture.test.ts",
);
});
it("keeps the adjacent npm fallback regression in the lane", async () => {
expect(await explicitPaths("windows")).toContain(
"tests/boundary/process/packageNpmInvocation.test.ts",
);
});
});
it("keeps every explicit focused CI test path executable after file moves", async () => {