Compare commits

..
Author SHA1 Message Date
44457d0990 fix(javascript): integrate Reflect.set receiver mutation coverage
Retain concurrent explicit receiver and opaque argument coverage while
preserving corrected call/apply receiver controls and the public CLI/MCP
fixture. Keep the complete invocation follow-up history without rewriting it.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-11 17:06:34 +00:00
2e860e2823 fix(javascript): integrate intrinsic binding and receiver proofs
Preserve generic binding factories and replaced-intrinsic checks from the
concurrent follow-up. Resolve call/apply by actual callable receiver rather
than nominal property-chain depth. Retain deep invocation, bound receiver,
fresh-target and public CLI/MCP regression coverage.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
2026-10-11 17:01:30 +00:00
morluto c98d326514 fix(javascript): retain Reflect set receiver effects 2026-10-12 01:01:18 +08:00
morluto ad27b8b4ff fix(javascript): prove generic binding and invocation effects 2026-10-12 00:55:02 +08:00
0285701033 fix(javascript): preserve callable receivers through generic invocations
Follow actual callable receivers for Function.prototype.call and apply,
including bound and rebinding forms. Treat call/apply projections as generic
function values instead of stripping arguments by property-chain depth.
Keep opaque arguments conservative and preserve fresh local target identity.
Cover uncurry, nested chains and the public CLI/MCP boundary.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 16:50:52 +00:00
morluto 3e9bce6249 fix(javascript): preserve bound receivers for intrinsic invocation 2026-10-12 00:39:01 +08:00
morluto 1e1a611012 fix(javascript): retain bound intrinsic mutation effects 2026-10-12 00:34:35 +08:00
b60248c462 fix(javascript): integrate current main stability fixes
Include the merged Hopper reconnect-listener readiness fix so local and CI
validation use the same current main recovery boundary.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 16:25:37 +00:00
2a9fdde5db fix(javascript): retain stable declarations and bound intrinsic writes
Keep repeated variable declarations with one unconditional initializer while
rejecting assignments, conditional initialization and multiple values.
Normalize known intrinsic calls, aliases and bound argument prefixes at the
semantic owner, retaining opaque targets without tainting unused bindings or
fresh local objects. Cover both regressions through source and CLI/MCP tests.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 16:25:15 +00:00
morluto 3d1d5b56b2 fix(javascript): preserve proven interop result projections 2026-10-12 00:10:02 +08:00
morluto 6b460683d7 fix(javascript): respect hoisted fresh function declarations
Function declarations provide fresh targets before their textual position. Retain initialization-order checks for variable and class values, including aliases. Cover the reviewed regression with source controls and the public CLI/MCP fixture.
2026-10-11 23:56:06 +08:00
morluto 548a4334c4 fix(javascript): preserve intrinsic alias capture timing
Validate each alias read after initialization and retain the original function capture across copies. Share declaration initializer facts with RPC and Electron analysis, including class declarations used for fresh-target identity. Accept single-statement control blocks while keeping lexical roles and added operations checked. Cover the capture regressions through inert source and public CLI/MCP workflows.
2026-10-11 23:53:55 +08:00
b03df22ce4 fix(javascript): retain fresh function and class target identity
Recognize stable local function, undecorated class and RegExp targets before
widening intrinsic mutations. Keep decorated classes opaque because their
binding value can be replaced, and preserve class-initialization writes.
Cover fresh targets and replacement decorators in inert CLI/MCP fixtures.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 15:41:00 +00:00
5569e96162 fix(javascript): preserve intrinsic projections and fresh target identity
Resolve known mutators through static binding projections and prove stable
identifier chains to fresh literals before widening unknown mutation targets.
Keep reassigned, uninitialized and member-projected targets conservative.
Cover both review regressions through source tests and inert CLI/MCP fixtures.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 15:22:12 +00:00
47f8156efe fix(javascript): integrate current provider timeout recovery
Include the main-branch Hopper startup timeout normalization already merged
in #1866 so combined CI retains launcher diagnostics when either timer fires.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 14:53:16 +00:00
824728d7ee fix(javascript): retain destructured intrinsic writes
Share one pattern-target walker across lexical definitions, member mutation
tracking and Electron identity writes. Keep overwritten globals unresolved
without treating property keys or shadowed locals as global replacements.

Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 14:43:17 +00:00
ced8778094 refactor(javascript): share AST child field metadata
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-11 14:19:51 +00:00
N0zoM1z0andfactory-droid[bot] 6d470fc9cb fix(javascript): integrate current managed and stability fixes
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-10-11 14:07:53 +00:00
N0zoM1z0andfactory-droid[bot] 9788112548 fix(javascript): integrate verified namespace interop with main
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
2026-10-11 14:06:55 +00:00
N0zoM1z0 ebaa639cf8 fix(javascript): prove Electron module namespace conversion 2026-10-11 13:59:19 +00:00
220 changed files with 33442 additions and 39851 deletions

No files matched your search

@@ -9,10 +9,7 @@ provenance; inputs and recovered code are never executed. Recovery requires a
caller-supplied Wakaru on Linux x64; see [the recovery guide](https://github.com/morluto/rea/blob/main/docs/javascript-recovery.md).
Use `analyze_javascript_application` directly on the operator-supplied ASAR or
extracted tree. Exact `#alias` specifiers resolve through the importer's
enclosing package `imports` map to package-local files; nested `node_modules`
packages use their own map. Wildcard, recursive, and external-package import
targets stay unresolved with that limitation. When size is unknown or large and the connected schema advertises
extracted tree. When size is unknown or large and the connected schema advertises
`detail`, request `detail: "summary"`. This returns an `inspect_analysis_view`
Evidence record with a distinct ID and a `normalized_result.parent_evidence_id`
for the complete retained application analysis. Source-map contents are part of
+2 -3
View File
@@ -9,6 +9,7 @@
**See a feature you like. Understand how it works, down to the binary level.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#what-you-can-analyze)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
@@ -244,9 +245,7 @@ Ghidra and IDA use your existing installations. See [provider setup](docs/instal
<summary><strong>Do I need to start Hopper first?</strong></summary>
REA starts Hopper when an operation needs it. On macOS, a first-run dialog may
ask you to choose demo mode or activate your license. Complete that prompt in the
same user session before your first CLI/MCP operation or unattended job. Open
Hopper manually if needed. See
ask you to choose demo mode or activate your license. See
[Hopper startup and troubleshooting](docs/installation.md#launcher-paths-and-troubleshooting).
</details>
+1
View File
@@ -9,6 +9,7 @@
**وجدت ميزة تعجبك؟ افهم طريقة عملها حتى مستوى الشيفرة الثنائية.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Eine Funktion gefällt dir? Verstehe, wie sie funktioniert – bis hinunter auf die Binärebene.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**¿Ves una función que te gusta? Entiende cómo funciona, hasta el nivel binario.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**قابلیتی دیده‌اید که دوستش دارید؟ سازوکارش را تا سطح باینری درک کنید.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Repérez une fonctionnalité qui vous plaît. Comprenez son fonctionnement, jusqu'au niveau du binaire.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Melihat fitur yang Anda sukai. Pahami cara kerjanya hingga ke tingkat biner.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Vedi una funzionalità che ti piace. Capisci come funziona, fino al livello binario.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#cosa-puoi-analizzare)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**気に入った機能を見つけたら、その仕組みをバイナリのレベルまで理解する。**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**마음에 드는 기능을 발견했다면, 바이너리 수준까지 작동 원리를 이해하세요.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Widzisz funkcję, która Ci się podoba? Dowiedz się, jak działa, aż do poziomu kodu binarnego.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Viu uma funcionalidade de que gostou? Entenda como ela funciona, até o nível binário.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Нашли интересную функцию? Разберитесь, как она работает, вплоть до двоичного кода.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**เห็นฟีเจอร์ที่ชอบแล้วอยากรู้ว่าทำงานอย่างไร? เข้าใจได้ลึกถึงระดับไบนารี**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Beğendiğiniz bir özellik mi gördünüz? İkili kod düzeyine kadar nasıl çalıştığını anlayın.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Побачили цікаву функцію? Зрозумійте, як вона працює, аж до рівня двійкового коду.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**Thấy một tính năng bạn thích? Hiểu cách nó hoạt động, đến tận mức mã nhị phân.**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**看到喜歡的功能,弄清它的工作原理,深入到二進位層面。**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
+1
View File
@@ -9,6 +9,7 @@
**看到喜欢的功能,弄清它的工作原理,深入到二进制层面。**
[![npm version](https://img.shields.io/npm/v/rea-agents?style=flat-square&color=cb3837)](https://www.npmjs.com/package/rea-agents)
[![CI](https://img.shields.io/github/actions/workflow/status/morluto/rea/ci.yml?branch=main&style=flat-square&label=CI)](https://github.com/morluto/rea/actions/workflows/ci.yml)
[![MCP tool catalog](https://img.shields.io/badge/MCP-tool_catalog-5c4ee5?style=flat-square)](docs/mcp-contracts.md#generated-catalog)
[![Node.js requirements](https://img.shields.io/badge/Node.js-requirements-339933?style=flat-square&logo=nodedotjs&logoColor=white)](#current-status)
[![skills.sh](https://skills.sh/b/morluto/rea?style=flat-square)](https://skills.sh/morluto/rea/reverse-engineer-anything)
@@ -1,14 +0,0 @@
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
// Compile the no-op activator with REA's scripts. Otherwise Ghidra generates
// one afterward and opens a second Java compiler and file manager.
public final class ReaGhidraBundleActivator implements BundleActivator {
@Override
public void start(BundleContext context) {
}
@Override
public void stop(BundleContext context) {
}
}
+2 -18
View File
@@ -352,12 +352,6 @@ Other integrations, including Qwen Code, use `~/.agents/skills`.
A mixed selection plans each distinct destination. Selecting the skill without a client uses the shared
directory. Setup leaves existing skill copies in other locations untouched.
`rea setup --skill-client claude_code --yes --json` refreshes and verifies the
selected skill destination independently of MCP registration selection. The
final `doctor.scope.skill_clients` records which skill destinations were
checked; `doctor.scope.clients` continues to describe required registrations.
An unselected shared skill does not affect a client-only refresh.
`doctor --skill --json` verifies the selected copies against the bundled
instructions and references. Consumers should use `identity.skill.state`,
`installed_version`, and `installed_tool_count` for skill readiness. The
@@ -567,13 +561,8 @@ need Xvfb, Python 3, X11 and XTEST; approved setup installs those dependencies
on supported distributions. Add the curl installer's reported executable
directory, commonly `~/.local/bin`, to your shell `PATH` if needed.
REA starts Hopper when needed. On macOS, complete its demo or license prompt in
the same user session before the first CLI/MCP operation or unattended job. Open
Hopper manually if needed; its launcher can also bring the dialog forward.
If startup reports `Cannot launch Hopper! Connection is invalid.` or waits for
bridge readiness, check Hopper for a pending demo, license, or loader dialog.
Complete the dialog and retry. If REA reports `cleanup_incomplete`, follow the
retained-resource guidance below before reopening the target.
REA starts Hopper when needed. On macOS, its launcher can bring a window or
first-run dialog forward; choose demo mode or activate your existing license.
Hopper serializes analysis requests. Cancelling a wait can leave provider work
running, which the session reports. Successful decompilation is cached until
a relevant rename or comment changes it.
@@ -602,11 +591,6 @@ if document closure is unconfirmed, REA retains the backing image and reports
MCP-exit behavior.
Startup deadlines report missing bridge readiness and preserve the launcher
outcome; a successful helper exit does not prove that a loader dialog completed.
If an owned provider process exits, request failures preserve its observed exit
code or signal and bounded output in `details.launcher`. An exit can precede
output-stream closure; `output_closed: false` keeps that uncertainty explicit.
Later requests retain the known exit instead of reducing it to socket loss.
An external GUI's helper exit does not establish that the GUI itself exited.
Cursor navigation returns the observed object start when Hopper snaps an interior
address; adjacent-object navigation rejects unmapped inputs and document ends.
Native API text rejects NUL characters and unpaired Unicode surrogates before
+5 -36
View File
@@ -159,11 +159,9 @@ completed partial Evidence. MCP failure details return
after recording succeeds, so `inspect_analysis_view` can open that analysis
directly without serializing the entire partial graph into the error response.
CLI failure output retains the complete partial Evidence.
Cancellation after source analysis begins likewise retains completed facts,
including cancellation during analyzer cleanup, graph construction, or Evidence
sealing. REA finishes projecting those owned facts cooperatively to return partial
Evidence. An SDK client may reject its cancelled request before receiving that
reply. On the same MCP connection, inspect the Evidence bundle
Cancellation after source analysis begins likewise
retains completed facts; an SDK client may reject its cancelled request before
receiving that reply. On the same MCP connection, inspect the Evidence bundle
and use the retained analysis ID with `inspect_analysis_view`. Owned analyzer
cleanup is verified before its private files are released. Cleanup uncertainty
is reported separately and retains ownership for retry.
@@ -233,11 +231,6 @@ unknown instead of silently treating them as absent or verified.
Plain `.ts` artifact sources use TypeScript syntax without JSX, including
angle-bracket type assertions. `.tsx` and `.jsx` retain JSX parsing.
Relative TypeScript imports can name their emitted `.js`, `.mjs`, or `.cjs`
counterparts while resolving to inventoried source files. This substitution
requires import or require semantics. Literal Electron preload, renderer, and
utility-process runtime paths retain the runtime filename selected by the caller.
Selected bounded text is then parsed as inert data to recover:
- `package.json` metadata and declared main or renderer entrypoints;
@@ -461,40 +454,16 @@ such as `./web3:app.js` still names a local artifact.
Package `exports` fallback arrays are supported both at the top level and under
the root `"."` entry. The resolver selects targets in declared order using the
same conditional and invalid-entry handling as nested exports arrays.
Nonempty arrays containing only inactive conditions remain unmatched, allowing
the enclosing condition object to continue to a later active condition.
Empty arrays and explicit null targets keep their blocking outcome.
Refusal evidence distinguishes those source values and retains condition names
from unmatched nested targets rather than reporting array indices.
For imports and requires, the active Node conditions include `module-sync`;
earlier active conditions retain precedence over later ones.
Exact package-private `#alias` specifiers resolve against the importer's
enclosing `package.json` `imports` map — the nearest same-container package
scope, stopping at a `node_modules` directory so a nested package cannot inherit
an ancestor's aliases. Selected targets are package-local `./` files with no
extension or index fallback, using the same condition, array, and null selection
as exports subpaths. A missing key, explicit null, or out-of-scope alias keeps
an unresolved edge with that reason. Wildcard keys (`#internal/*`), recursive
`#` targets, and external-package import targets remain unresolved.
Exact keys such as `#/x` are literal lookup identities. `#` and names ending
in `/` are invalid. Imports arrays skip invalid parent-relative, absolute, and
URL targets before selecting a later package-local file; an unsupported bare
package or recursive alias keeps its unresolved outcome rather than selecting
a later fallback.
ESM relative module paths and selected package exports targets use URL suffix handling
and one percent-decoding pass. CommonJS relative paths and legacy package main
fields retain literal filename punctuation. Exports targets must start with
`./` and exclude `.`, `..`, and `node_modules` path segments, including their
encoded forms, before URL normalization. Arrays skip invalid exports targets
in declared order, including URLs that escape the package root after normalization.
Selected package targets use the normalized URL path for lookup; tabs and
newlines in the URL text are removed by the URL parser. CommonJS package targets
reject encoded separators anywhere in the selected URL; ESM checks its pathname.
URL decoding and file lookup failures for a selected target do not select a
later entry. Rejected references retain the package metadata
in declared order; URL decoding and file lookup failures for a selected target
do not select a later entry. Rejected references retain the package metadata
path, original target, failed constraint, and importing source location.
HTML script references resolve to exact inventoried files after applying the
-18
View File
@@ -126,24 +126,6 @@ Member inspection binds build-local tokens, signatures, row offsets, method
bodies, and decoded relationships to that artifact and MVID. Missing or malformed
facts remain explicit in coverage and per-member status.
Metadata table streams may carry the CLR-compatible `ExtraData` extension
(`HeapSizes` flag `0x40`), which Microsoft readers honor although it is not
specified by ECMA-335. REA skips its fixed four-byte word after the row counts
before decoding table rows; the word's value is not a length. An incomplete
word is reported as a metadata coverage failure at its file offset, without
reading beyond the declared table stream.
For unoptimized `#-` metadata, nonempty `FieldPtr` and `MethodPtr` tables
translate TypeDef list positions to physical member definitions. REA resolves
these indirections for member ownership and local attribute-constructor types;
field and method tokens still identify the original definition rows.
`field_list` and `method_list` retain the metadata's list coordinates, which
refer to the pointer table when present rather than necessarily contiguous
definition rows. Malformed or ambiguous pointers leave the owner unknown with
partial coverage while preserving readable member and attribute facts.
The retained inverse indexes are admitted against the existing representation
budget before allocation.
`exported_types` reports ExportedType rows as declared. Facade assemblies such
as .NET `System.Runtime.dll` provide their types this way: `forwarder: true`
marks a type forwarder, and `implementation_token` names the AssemblyRef, File,
+1 -7
View File
@@ -450,13 +450,7 @@ Tool annotations describe effects and are hints, not authorization controls
([MCP ToolAnnotations](https://modelcontextprotocol.io/specification/2025-06-18/schema#toolannotations)).
`readOnlyHint` includes session state: an analysis call that records additive
Evidence is marked non-read-only even when it leaves the target unchanged.
`destructiveHint` is true when a tool may discard data (overwriting or clearing
analyst metadata, replacing files, dropping session state), runs or drives code
REA does not own (`executesForeignCode`: process, LLDB, Electron, browser, and
native UI scenarios), or changes an attached Android device (`mutatesDevice`:
push, install, uninstall, start, and stop). Ordinary Evidence recording is not
destructive. The effect record in `binary_session` and the generated catalog
carries each flag separately.
`destructiveHint` describes possible data loss, not ordinary Evidence recording.
Effect metadata covers possible behavior across supported inputs: DMG inventory
can launch `hdiutil` and create an owned temporary mount directory; extraction
creates a fresh output directory on every call. Comparing supplied web captures
+8 -13
View File
@@ -139,11 +139,8 @@ directory, so default OS-home discovery remains unverified. Live model APIs and
native Windows are also unverified.
`verify:claude-client` requires installed Claude Code (verified with
`@anthropic-ai/claude-code@2.1.296` on POSIX and native Windows); select it
with `REA_VERIFY_CLAUDE_COMMAND`. On Windows, select
`claude.exe`: npm installs put only a `claude.cmd` shim on `PATH`, and the real
executable is under `node_modules/@anthropic-ai/claude-code/bin`.
The optional lane uses native `CLAUDE_CONFIG_DIR` discovery, an independent
`@anthropic-ai/claude-code@2.1.296`); select it with `REA_VERIFY_CLAUDE_COMMAND`.
The optional POSIX lane uses native `CLAUDE_CONFIG_DIR` discovery, an independent
Git workspace, user settings and default built-in tools. It preserves caller
preferences and backups, checks idempotent setup, waits through the native
`WaitForMcpServers` tool when discovery is pending, and validates the complete
@@ -152,8 +149,8 @@ native `Skill`, checks its full body and verifies named-schema JavaScript Eviden
for a Unicode path. Use `-- chat` for ordinary chat with REA enabled, or
`REA_VERIFY_RUNTIME_ROOT` for a production-only installed package. The local
Anthropic Messages/SSE fixture preserves native resource-hint envelopes in its
request artifacts. Usage is synthetic; live Anthropic API and bare-mode
personal-skill activation remain unverified by this lane.
request artifacts. Usage is synthetic; live Anthropic API, native Windows and
bare-mode personal-skill activation remain unverified by this lane.
`verify:copilot-client` requires installed GitHub Copilot CLI (verified with
`@github/copilot@1.0.95`); select it with `REA_VERIFY_COPILOT_COMMAND`.
@@ -282,12 +279,10 @@ establish live DeepSeek API or native Windows coverage. Receipts include
request artifacts, result digests and owned-process lineage.
Full E2E tests invoke the production command dispatcher and real providers,
without fake launchers, runners or responses. `verify:keyed-archive` writes
Foundation binary, XML, and integer archives using one run-owned Swift module
cache. It checks CLI and separate stdio MCP parity, pagination, escaping-path
rejection, an XML graph golden, and numeric meaning for safe controls, unsafe
integers, boxed numbers, and integral-valued reals. `verify:asset-catalog`
compiles source-owned colors with
without fake launchers, runners or responses. `verify:keyed-archive` writes an
actual Foundation binary archive, runs the CLI and a separate stdio MCP
subprocess, checks parity and pagination, rejects an escaping path and checks
an XML graph golden. `verify:asset-catalog` compiles source-owned colors with
`actool`, invokes real `assetutil`, then checks CLI/MCP results, exact catalog
digest, every raw metadata field, pagination and malformed input rejection.
Neither artifact workflow requires Hopper or launches it. Both run in macOS CI.
+88 -104
View File
@@ -1,12 +1,4 @@
#!/bin/bash
{
# Truncation guard: this brace group opens on line 2 and closes on the last
# line, so bash must read the entire file before it runs any command. On the
# `curl ... | bash` path, a download cut off anywhere before the final `}` is
# an unterminated compound command, which bash reports as a syntax error at
# EOF without running anything. Without the guard, a cut right after `set`
# would run the bare builtin and print every shell variable, and a cut right
# after `main` would run the installer without the caller's flags.
set -euo pipefail
PACKAGE="rea-agents"
@@ -29,112 +21,104 @@ usage() {
cat <<'EOF'
Usage: install.sh [--version VERSION] [--dry-run] [--no-setup] [--no-prompt] [--verbose]
Installs the REA CLI, then starts `rea setup` interactively to configure Hopper
and agents when a terminal is available. Pass --no-setup (or --no-prompt) to
skip setup and run `rea setup` later.
Installs only the REA CLI. Run `rea setup` separately to configure Hopper and agents.
EOF
}
main() {
while (($# > 0)); do
case "$1" in
--version)
(($# >= 2)) || fail "--version requires a value."
version="$2"
shift 2
;;
--dry-run)
dry_run=true
shift
;;
--no-setup|--no-prompt)
start_setup=false
shift
;;
--verbose)
verbose=true
shift
;;
--help|-h)
usage
exit 0
;;
*) fail "unknown option: $1" ;;
esac
done
while (($# > 0)); do
case "$1" in
--version)
(($# >= 2)) || fail "--version requires a value."
version="$2"
shift 2
;;
--dry-run)
dry_run=true
shift
;;
--no-setup|--no-prompt)
start_setup=false
shift
;;
--verbose)
verbose=true
shift
;;
--help|-h)
usage
exit 0
;;
*) fail "unknown option: $1" ;;
esac
done
if [[ "$verbose" == true ]]; then
set -x
fi
if [[ "$verbose" == true ]]; then
set -x
fi
platform="$(uname -s)"
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
platform="$(uname -s)"
[[ "$platform" == "Darwin" || "$platform" == "Linux" ]] || fail "REA supports macOS and selected 64-bit Linux distributions."
command -v curl >/dev/null 2>&1 || fail "curl is required. Install curl, then rerun this installer."
command -v node >/dev/null 2>&1 || fail "Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+ is required; install it from https://nodejs.org and retry."
command -v npm >/dev/null 2>&1 || fail "npm is required; install it with Node.js and retry."
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
fi
node_version="$(node -p 'process.versions.node' 2>/dev/null || true)"
IFS=. read -r node_major node_minor _node_patch <<<"$node_version"
[[ "$node_major" =~ ^[0-9]+$ && "$node_minor" =~ ^[0-9]+$ ]] || fail "the active Node.js version could not be read. Check that node works and is on PATH, then retry."
if ! [[ "$_node_patch" =~ ^[0-9]+(\+[0-9A-Za-z.-]+)?$ ]] ||
! ((node_major == 22 && node_minor >= 19 || node_major == 24 && node_minor >= 11 || node_major >= 26)); then
fail "Node.js $node_version is unsupported; use Node.js 22.x (>=22.19), 24.x (>=24.11), or 26+."
fi
if [[ -n "$version" ]]; then
[[ "$version" =~ ^${version_pattern}$ ]] || fail "version must be an exact semantic version."
else
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION."
[[ "$tag" =~ ^rea-agents-(${version_pattern})$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
version="${BASH_REMATCH[1]}"
fi
if [[ -n "$version" ]]; then
[[ "$version" =~ ^${version_pattern}$ ]] || fail "version must be an exact semantic version."
else
release_json="$(curl -fsSL --proto '=https' --tlsv1.2 --retry 3 -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/$REPOSITORY/releases/latest")" || fail "the latest REA release could not be resolved. Check network access or pass --version VERSION, then retry."
tag="$(printf '%s' "$release_json" | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s).tag_name;if(typeof v!=="string")process.exit(1);process.stdout.write(v)})' 2>/dev/null)" || fail "the release response was invalid. Retry later or pass --version VERSION."
[[ "$tag" =~ ^rea-agents-(${version_pattern})$ ]] || fail "the latest release tag was invalid. Retry later or pass --version VERSION."
version="${BASH_REMATCH[1]}"
fi
# npm's exact-version resolver cleans build metadata from the requested version,
# so it may install the base release and make the post-install identity check fail.
if [[ "$version" =~ \+ ]]; then
fail "npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry."
fi
# npm's exact-version resolver cleans build metadata from the requested version,
# so it may install the base release and make the post-install identity check fail.
if [[ "$version" =~ \+ ]]; then
fail "npm cannot install an exact version with build metadata. Pass a version without build metadata, then retry."
fi
install_args=(install --global)
if [[ "$platform" == "Linux" ]]; then
install_args+=(--prefix "$HOME/.local")
install_bin="$HOME/.local/bin/rea"
else
npm_prefix="$(npm prefix --global)" || fail "the npm global prefix could not be read. Repair the npm configuration, then retry."
install_bin="$npm_prefix/bin/rea"
fi
install_args+=("$PACKAGE@$version")
install_args=(install --global)
if [[ "$platform" == "Linux" ]]; then
install_args+=(--prefix "$HOME/.local")
install_bin="$HOME/.local/bin/rea"
else
npm_prefix="$(npm prefix --global)" || fail "the npm global prefix could not be read. Repair the npm configuration, then retry."
install_bin="$npm_prefix/bin/rea"
fi
install_args+=("$PACKAGE@$version")
printf 'REA install plan\n'
printf ' Version: %s\n' "$version"
printf ' Runtime: Node.js %s\n' "$node_version"
printf ' Command:'
printf ' %q' npm "${install_args[@]}"
printf '\n'
printf ' Binary: %s\n' "$install_bin"
printf ' Setup: %s\n' "$([[ "$start_setup" == true ]] && printf 'start when a terminal is available' || printf 'skipped')"
printf 'REA install plan\n'
printf ' Version: %s\n' "$version"
printf ' Runtime: Node.js %s\n' "$node_version"
printf ' Command:'
printf ' %q' npm "${install_args[@]}"
printf '\n'
printf ' Binary: %s\n' "$install_bin"
printf ' Setup: %s\n' "$([[ "$start_setup" == true ]] && printf 'start when a terminal is available' || printf 'skipped')"
if [[ "$dry_run" == true ]]; then
printf 'Dry run complete; no changes made.\n'
exit 0
fi
if [[ "$dry_run" == true ]]; then
printf 'Dry run complete; no changes made.\n'
exit 0
fi
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
npm "${install_args[@]}" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
printf 'Installing %s@%s...\n' "$PACKAGE" "$version"
npm "${install_args[@]}" || fail "npm could not install REA. Check registry access and npm permissions, then retry."
[[ -x "$install_bin" ]] || fail "npm completed without installing the rea command. Check the npm global bin directory and PATH, then retry."
installed_version="$("$install_bin" --version 2>/dev/null | tr -d '[:space:]')" || fail "the installed REA version could not be read. Reinstall the requested version, then retry."
[[ "$installed_version" == "$version" ]] || fail "installed version $installed_version does not match $version. Reinstall the requested version, then retry."
printf 'REA %s is installed.\n' "$version"
if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
printf 'Starting REA setup...\n'
"$install_bin" setup </dev/tty >/dev/tty
else
printf 'Run %s setup to configure Hopper and agents.\n' "$install_bin"
fi
}
main "$@"
exit $?
}
printf 'REA %s is installed.\n' "$version"
if [[ "$start_setup" == true && -r /dev/tty && -w /dev/tty && -t 1 ]]; then
printf 'Starting REA setup...\n'
"$install_bin" setup </dev/tty >/dev/tty
else
printf 'Run %s setup to configure Hopper and agents.\n' "$install_bin"
fi
-1
View File
@@ -51,7 +51,6 @@
"bridge/process/ProcessRunTokenReader.swift",
"bridge/process/ProcessRunTokenReaderMain.swift",
"bridge/ghidra/ReaGhidraBridge.java",
"bridge/ghidra/ReaGhidraBundleActivator.java",
"bridge/ghidra/ReaGhidraExtensions.java",
"bridge/ghidra/ReaGhidraPrepareCom.java",
"bridge/ghidra/ReaGhidraApplySeeds.java",
-16
View File
@@ -401,22 +401,6 @@ class WebsiteTranslationChecks(unittest.TestCase):
server.shutdown()
worker.join()
def test_every_reading_page_offers_light_and_dark_theme_colors(self):
site = ROOT / "website/_site"
english_pages = reading_pages(ROOT / "website/public")
self.assertTrue(english_pages)
for language in languages(ROOT):
code = language["code"]
for original in english_pages:
relative = original.relative_to(ROOT / "website/public")
for path in {original, site / relative if code == "en" else site / code / relative}:
with self.subTest(language=code, page=str(path.relative_to(ROOT))):
doc = Document(path.read_text())
media = sorted(node.attrs.get("media") for node in doc.nodes if node.tag == "meta" and node.attrs.get("name") == "theme-color")
self.assertEqual(media, ["(prefers-color-scheme: dark)", "(prefers-color-scheme: light)"])
styles = (ROOT / "website/public/assets/styles.css").read_text()
self.assertTrue("@media (prefers-color-scheme: dark)" in styles, "styles.css has no dark color scheme block")
if __name__ == "__main__":
unittest.main()
+5 -6
View File
@@ -13,6 +13,10 @@ import { exec } from "./lib/verify-package-core.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
import { createAnthropicModelFixture } from "./verify/clients/anthropic-model-fixture.mjs";
assert(
process.platform !== "win32",
"Native Claude Code verifier requires POSIX; Windows is unverified",
);
const repo = fileURLToPath(new URL("..", import.meta.url));
const runtimeRoot = process.env.REA_VERIFY_RUNTIME_ROOT ?? repo;
const command = process.env.REA_VERIFY_CLAUDE_COMMAND ?? "claude";
@@ -42,13 +46,8 @@ try {
await exec(command, ["--version"], { env: environment, timeout: 30_000 })
).stdout.trim();
} catch (cause) {
// npm installs put only a claude.cmd shim on PATH, which Node does not spawn without a shell.
const windowsHint =
process.platform === "win32"
? " On Windows, select claude.exe; npm installs provide it under node_modules/@anthropic-ai/claude-code/bin."
: "";
throw new Error(
`Claude Code preflight failed for ${command}; install Claude Code or set REA_VERIFY_CLAUDE_COMMAND.${windowsHint} ${String(cause)}`,
`Claude Code preflight failed for ${command}; install Claude Code or set REA_VERIFY_CLAUDE_COMMAND. ${String(cause)}`,
);
}
const target = join(lab, "fixture α测试");
+1 -18
View File
@@ -14,7 +14,7 @@ import {
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { basename, dirname, join, resolve } from "node:path";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath, pathToFileURL } from "node:url";
@@ -291,23 +291,6 @@ try {
await client.connect(transport);
await call("open_binary", { path: target });
await call("binary_overview", {});
const activators = (
await readdir(runtimeParent, { recursive: true })
).filter((path) => basename(path) === "ReaGhidraBundleActivator.class");
assert.equal(activators.length, 1);
const [activator] = activators;
assert.ok(activator !== undefined);
const bundleDirectory = dirname(join(runtimeParent, activator));
const manifest = await readFile(
join(bundleDirectory, "META-INF", "MANIFEST.MF"),
"utf8",
);
assert.match(manifest, /^Bundle-Activator: ReaGhidraBundleActivator\r?$/mu);
await assert.rejects(
readFile(join(bundleDirectory, "GeneratedActivator.class")),
{ code: "ENOENT" },
);
report.bundleActivator = "ReaGhidraBundleActivator";
assert.equal((await call("list_documents", {})).length, 1);
await call("list_names", {});
const procedures = await call("list_procedures", {});
+1 -7
View File
@@ -5,16 +5,10 @@ import { promisify } from "node:util";
const exec = promisify(execFile);
const packedHopperBridge = "package/bridge/hopper_bridge.py";
const packedGhidraBridge = "package/bridge/ghidra/ReaGhidraBridge.java";
const packedGhidraActivator =
"package/bridge/ghidra/ReaGhidraBundleActivator.java";
/** Check shipped bridge files; real-provider lanes verify runtime behavior. */
export async function verifyPackedBridge({ workspace, tarball, packedFiles }) {
for (const path of [
packedHopperBridge,
packedGhidraBridge,
packedGhidraActivator,
]) {
for (const path of [packedHopperBridge, packedGhidraBridge]) {
if (!packedFiles.includes(path)) throw new Error(`package omitted ${path}`);
}
await exec("tar", ["-xf", tarball, "-C", workspace]);
@@ -1,147 +0,0 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { z } from "zod";
import { keyedArchiveResultSchema } from "../../../dist/domain/apple/keyedArchive.js";
const rowSchema = z.object({
container: z.enum(["roots", "objects"]),
root: z.string(),
objectId: z.number().int().nonnegative().optional(),
path: z.array(z.union([z.string(), z.number().int().nonnegative()])),
kind: z.enum(["integer", "real"]),
literal: z.string(),
});
const archiveSchema = z.object({
filename: z.string(),
sha256: z.string().regex(/^[a-f0-9]{64}$/u),
rows: z.array(rowSchema),
});
const oracleSchema = z.object({ xml: archiveSchema, binary: archiveSchema });
const formats = ["xml", "binary"];
const requiredRows = [
{ root: "unsafeSigned", kind: "integer", literal: "9007199254740993" },
{
root: "unsafeNegative",
kind: "integer",
literal: "-9223372036854775808",
},
{ root: "safeInteger", kind: "integer", literal: "9007199254740991" },
{ root: "safeNegative", kind: "integer", literal: "-42" },
{ root: "integralReal", kind: "real", literal: "9007199254740992" },
{
root: "boxedUnsigned",
kind: "integer",
literal: "9007199254740993",
container: "objects",
},
{
root: "boxedReal",
kind: "real",
literal: "9007199254740992",
container: "objects",
},
];
const valueAt = (value, path) => {
for (const key of path) {
if (value === null || typeof value !== "object") return undefined;
value = value[key];
}
return value;
};
const isUnsafeInteger = (row) =>
row.kind === "integer" &&
(BigInt(row.literal) > BigInt(Number.MAX_SAFE_INTEGER) ||
BigInt(row.literal) < BigInt(Number.MIN_SAFE_INTEGER));
/** Verify the actual Foundation output and the production CLI/MCP result. */
export async function verifyKeyedArchiveIntegerArchives(root) {
const oracle = oracleSchema.parse(
JSON.parse(await readFile(join(root, "oracle.json"), "utf8")),
);
const archives = [];
for (const format of formats) {
const archive = oracle[format];
for (const required of requiredRows) {
assert.ok(
archive.rows.some(
(row) =>
row.root === required.root &&
row.kind === required.kind &&
row.literal === required.literal &&
(required.container === undefined ||
row.container === required.container),
),
`${format} Foundation oracle omitted ${required.root} ${required.literal}`,
);
}
const archivePath = join(root, archive.filename);
const digest = createHash("sha256")
.update(await readFile(archivePath))
.digest("hex");
assert.equal(digest, archive.sha256, `${format} archive digest`);
archives.push({ format, archive, archivePath, digest });
}
return archives;
}
/** Check every oracle numeric slot, preserving object-table identity lookups. */
export function assertKeyedArchiveIntegerResult(
result,
format,
digest,
oracle,
) {
const parsed = keyedArchiveResultSchema.parse(result);
assert.equal(parsed.archive_sha256, digest);
assert.equal(parsed.target_sha256, digest);
assert.equal(
parsed.archive_format,
format === "xml" ? "xml-plist" : "binary-plist",
);
for (const row of oracle.rows) {
const unsafe = isUnsafeInteger(row);
const source =
row.container === "roots"
? parsed.roots
: parsed.objects.find((object) => object.id === row.objectId)?.value;
const value = valueAt(source, row.path);
assert.notEqual(
value,
undefined,
`${row.root} numeric slot remains present`,
);
if (unsafe) {
const exact =
typeof value === "object" &&
value !== null &&
value.$plist_type === "integer" &&
value.decimal === BigInt(row.literal).toString();
const disclosed = parsed.limitations.some(
(reason) =>
/integer|number/iu.test(reason) &&
/precision|exact range|rounded/iu.test(reason),
);
assert.ok(
exact || disclosed,
`${row.root} ${row.literal} must be exact or disclose numeric loss`,
);
} else {
assert.equal(
typeof value,
"number",
`${row.root} retains its numeric type`,
);
assert.equal(value, Number(row.literal), `${row.root} numeric value`);
}
}
return parsed;
}
+22 -40
View File
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises";
import { execFile, spawnSync } from "node:child_process";
import { mkdtemp, readFile, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
@@ -11,10 +11,6 @@ import {
artifactMcpResult,
withArtifactMcp,
} from "../../lib/artifact-e2e.mjs";
import {
assertKeyedArchiveIntegerResult,
verifyKeyedArchiveIntegerArchives,
} from "./keyed-archive-integers.mjs";
if (process.platform !== "darwin")
throw new Error(
@@ -174,37 +170,26 @@ try {
);
});
}
const integers = join(root, "integers");
await mkdir(integers);
await promisify(execFile)("/usr/bin/xcrun", [
"swift",
"-module-cache-path",
join(root, "modules"),
fileURLToPath(
new URL(
"../../../tests/conformance/native/keyed-archive-integers.swift",
import.meta.url,
),
),
integers,
]);
for (const fixture of await verifyKeyedArchiveIntegerArchives(integers)) {
const cliResult = assertKeyedArchiveIntegerResult(
await artifactCli("inspect-keyed-archive", fixture.archivePath),
fixture.format,
fixture.digest,
fixture.archive,
);
await withArtifactMcp(fixture.archivePath, async (client) => {
const mcpResult = assertKeyedArchiveIntegerResult(
await artifactMcpResult(client, "inspect_keyed_archive"),
fixture.format,
fixture.digest,
fixture.archive,
);
assert.deepEqual(mcpResult, cliResult);
});
}
const numericTests = spawnSync(
"npm",
[
"run",
"test:focused",
"--",
"tests/boundary/filesystem/keyedArchiveNativeIntegers.test.ts",
],
{
cwd: fileURLToPath(new URL("../../../", import.meta.url)),
stdio: "inherit",
timeout: 240_000,
},
);
if (numericTests.error) throw numericTests.error;
assert.equal(
numericTests.status,
0,
"Native Foundation integer cases failed",
);
report = {
ok: true,
mocked: false,
@@ -212,9 +197,6 @@ try {
stdio_mcp: true,
xml_golden: true,
uid_named_root: true,
numeric_formats: ["xml", "binary"],
numeric_cli: true,
numeric_stdio_mcp: true,
format: graph.archive_format,
objects: graph.total_objects,
references: graph.total_references,
+8 -6
View File
@@ -10,12 +10,12 @@ import type {
} from "../application/AnalysisProvider.js";
import {
createAnalysisExecution,
providerConfigurationRefusal,
type AnalysisExecution,
} from "../application/AnalysisProvider.js";
import type { AdbRequest } from "../domain/adb/adbDeviceAnalysis.js";
import {
AnalysisArtifactChangedError,
AnalysisCapabilityUnavailableError,
AnalysisCancelledError,
AnalysisInputError,
AnalysisOutputError,
@@ -417,8 +417,8 @@ export class AdbProvider {
async inspectAvailability(
signal?: AbortSignal,
): Promise<ProviderAvailability> {
const selection = this.selection();
try {
const selection = this.selection();
await inspectAdbClient(selection, signal);
return {
status: "available",
@@ -555,19 +555,21 @@ export class AdbProvider {
const signal = options?.signal;
if (signal?.aborted === true)
return err(new AnalysisCancelledError(request.operation));
let selection: AdbBinarySelection;
const selection = this.selection();
let identity: AdbClientIdentity;
try {
selection = this.selection();
identity = await inspectAdbClient(selection, signal);
} catch (cause) {
if (cause instanceof AnalysisCancelledError) return err(cause);
if (cause instanceof AdbConfigurationFailure)
return err(
providerConfigurationRefusal(
new AnalysisCapabilityUnavailableError(
ADB_PROVIDER_IDENTITY.id,
request.operation,
cause,
typeof cause.diagnostics.remediation === "string"
? `${cause.message} ${cause.diagnostics.remediation}`
: cause.message,
{ cause },
),
);
return err(
+9 -9
View File
@@ -22,7 +22,6 @@ import type {
} from "../application/AnalysisProvider.js";
import {
createAnalysisExecution,
providerConfigurationRefusal,
type AnalysisExecution,
} from "../application/AnalysisProvider.js";
import type { ApktoolRequest } from "../domain/apktool/apktoolResourceAnalysis.js";
@@ -154,13 +153,12 @@ const runApktool = async (
invocation = apktoolInvocation(selection, arguments_, hostEnvironment);
} catch (cause) {
if (!(cause instanceof WindowsBatchCommandError)) throw cause;
const reason = `The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`;
return err(
new AnalysisCapabilityUnavailableError(
APKTOOL_PROVIDER_IDENTITY.id,
operation,
reason,
{ cause, userMessage: reason },
`The apktool batch launcher cannot receive REA's decode paths through cmd.exe: ${cause.message}`,
{ cause },
),
);
}
@@ -347,8 +345,8 @@ export class ApktoolProvider {
async #inspectAvailability(
signal: AbortSignal,
): Promise<ProviderAvailability> {
const selection = this.selection();
try {
const selection = this.selection();
await inspectApktoolClient(selection, signal, this.environment);
throwIfCancelled("inspect_apktool_client", signal);
return {
@@ -399,10 +397,9 @@ export class ApktoolProvider {
): Promise<Result<AnalysisExecution, AnalysisError>> {
if (signal?.aborted === true)
return err(new AnalysisCancelledError(request.operation));
let selection: ApktoolCommandSelection;
const selection = this.selection();
let identity: ApktoolClientIdentity;
try {
selection = this.selection();
identity = await inspectApktoolClient(
selection,
signal,
@@ -412,10 +409,13 @@ export class ApktoolProvider {
if (cause instanceof AnalysisCancelledError) return err(cause);
if (cause instanceof ApktoolConfigurationFailure)
return err(
providerConfigurationRefusal(
new AnalysisCapabilityUnavailableError(
APKTOOL_PROVIDER_IDENTITY.id,
request.operation,
cause,
typeof cause.diagnostics.remediation === "string"
? `${cause.message} ${cause.diagnostics.remediation}`
: cause.message,
{ cause },
),
);
return err(
-26
View File
@@ -5,7 +5,6 @@ import type { NativeToolName } from "../contracts/native/nativeToolContracts.js"
import type { ArtifactAnalysisOperation } from "../contracts/artifactToolContracts.js";
import type { ManagedToolName } from "../contracts/managed/managedToolContracts.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { AnalysisCapabilityUnavailableError } from "../domain/analysisErrorCore.js";
import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
@@ -211,31 +210,6 @@ export type ProviderAvailability =
readonly diagnostics: Readonly<Record<string, JsonValue>>;
};
/** A host tool configuration failure observed before any target work. */
export interface ProviderConfigurationFailure extends Error {
readonly diagnostics: Readonly<Record<string, JsonValue>>;
}
/**
* Refuse an operation because the host tool is misconfigured. The reason,
* not the generic target advice, tells the caller what to change.
*/
export const providerConfigurationRefusal = (
providerId: string,
operation: string,
failure: ProviderConfigurationFailure,
): AnalysisCapabilityUnavailableError => {
const remediation = failure.diagnostics.remediation;
const reason =
typeof remediation === "string"
? `${failure.message}. ${remediation}.`
: `${failure.message}.`;
return new AnalysisCapabilityUnavailableError(providerId, operation, reason, {
cause: failure,
userMessage: reason,
});
};
/** Provider-owned target support observation before profile resolution. */
export type ProviderTargetSupport =
| {
+1 -2
View File
@@ -20,7 +20,6 @@ import {
clientRequirementsFor,
NO_CLIENT_FEATURES,
} from "./CapabilityClientRequirements.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
const statusSchema = z.object({
open: z.boolean(),
@@ -211,7 +210,7 @@ export const buildCapabilityInventory = (
reason: availability.reason,
remediation: availability.remediation,
};
}).sort((left, right) => compareUnicodeCodePoints(left.name, right.name));
}).sort((left, right) => left.name.localeCompare(right.name));
};
const availabilityFor = (context: AvailabilityContext): Availability => {
@@ -18,7 +18,6 @@ import { z } from "zod";
import { PRODUCT_IDENTITY } from "../identity.js";
import type { SetupClient } from "./SupportedClients.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
export type ClientConfigurationFormat = NonNullable<SetupClient["format"]>;
export type ClientServersKey = "mcp_servers" | "mcpServers" | "mcp" | "servers";
@@ -64,7 +63,7 @@ const normalizeConfigurationValue = (value: unknown): unknown => {
return value;
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, nested]) => [key, normalizeConfigurationValue(nested)]),
);
};
@@ -8,7 +8,6 @@ import {
type ClientConfigurationDocument,
} from "./ClientConfigurationDocument.js";
import { PRODUCT_IDENTITY } from "../identity.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/** Read explicit REA overrides without dropping peers when one value is invalid. */
export const clientRegistrationEnvironment = (
@@ -48,7 +47,7 @@ export const clientRegistrationEnvironment = (
return ok(
Object.fromEntries(
Object.entries(environment).sort(([left], [right]) =>
compareUnicodeCodePoints(left, right),
left.localeCompare(right),
),
),
);
+1 -2
View File
@@ -25,7 +25,6 @@ import {
supportedClients,
} from "./SupportedClients.js";
import type { SetupClient } from "./SupportedClients.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
interface ClientRegistrationStatusBase {
readonly client: string;
@@ -185,7 +184,7 @@ export const readClientRegistrationStatuses = async (
}
}
return statuses.sort((left, right) =>
compareUnicodeCodePoints(left.client, right.client),
left.client.localeCompare(right.client),
);
};
-36
View File
@@ -281,42 +281,6 @@ describe("doctor installation identity", () => {
});
describe("doctor scoped readiness", () => {
it("checks explicitly selected skill destinations independently of registrations", async () => {
const selected: (readonly string[] | undefined)[] = [];
const result = await runDoctor(
undefined,
host({
clientRegistrations: () => Promise.resolve([]),
installedSkillIdentity: (clients) => {
selected.push(clients);
return Promise.resolve({
version: "stale",
toolCount: 0,
canonical: false,
});
},
}),
{
clients: [],
skillClients: ["claude_code", "claude_code"],
skill: true,
},
);
expect(selected).toEqual([["claude_code"]]);
expect(result.scope).toMatchObject({
mode: "explicit",
clients: [],
skill_clients: ["claude_code"],
skill: true,
});
expect(result.healthy).toBe(false);
expect(result.scope_checks.map(({ name }) => name)).toEqual([
"node",
"skill:identity",
]);
});
it("scopes effective readiness to selected clients without hiding drift", async () => {
const registrations: readonly ClientRegistrationStatus[] = [
{
+2 -4
View File
@@ -120,7 +120,6 @@ export interface DoctorReport {
/** Caller-selected readiness boundary; omitted values retain audit semantics. */
export interface DoctorScope {
readonly clients?: readonly string[];
readonly skillClients?: readonly string[];
readonly providers?: readonly string[];
readonly skill?: boolean;
}
@@ -207,10 +206,9 @@ const collectDoctorIdentity = async (
const installationPaths = (await host.installationPaths?.()) ?? [];
const registrations = (await host.clientRegistrations?.()) ?? [];
const skillClientIds =
scope.skill_clients ??
(scope.mode === "audit-wide"
scope.mode === "audit-wide"
? registrations.map(({ client }) => client)
: scope.clients);
: scope.clients;
const installedSkillIdentity =
await host.installedSkillIdentity?.(skillClientIds);
const skillAligned = skillIdentityAligned(installedSkillIdentity);
-4
View File
@@ -14,7 +14,6 @@ import type {
export interface DoctorScopeReport {
readonly mode: "audit-wide" | "explicit";
readonly clients: readonly string[];
readonly skill_clients?: readonly string[];
readonly providers: readonly string[];
readonly skill: boolean;
readonly target: string | null;
@@ -41,9 +40,6 @@ export const normalizeDoctorScope = (
): DoctorScopeReport => ({
mode: requested === undefined ? "audit-wide" : "explicit",
clients: uniqueSorted(requested?.clients ?? []),
...(requested?.skillClients === undefined
? {}
: { skill_clients: uniqueSorted(requested.skillClients) }),
providers: uniqueSorted(requested?.providers ?? []),
skill: requested?.skill === true,
target: target ?? null,
@@ -18,7 +18,6 @@ import {
semanticObligationPolicy,
type ReconstructionObligationCandidatePolicy,
} from "./ReconstructionObligationPolicies.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
type CandidateFields = Omit<
ReconstructionObligation,
@@ -55,7 +54,7 @@ export const deriveReconstructionObligationCandidates = (
addCandidate(candidates, reviewedCandidate(obligation, records));
return {
candidates: [...candidates.values()].sort((left, right) =>
compareUnicodeCodePoints(left.obligation_id, right.obligation_id),
left.obligation_id.localeCompare(right.obligation_id),
),
limitations: [...limitations].sort(),
};
@@ -431,7 +430,7 @@ const uniqueBy = <Value>(
key: (value: Value) => string,
): Value[] =>
[...new Map(values.map((value) => [key(value), value])).values()].sort(
(left, right) => compareUnicodeCodePoints(key(left), key(right)),
(left, right) => key(left).localeCompare(key(right)),
);
const obligationId = (key: string): string =>
@@ -3,7 +3,6 @@ import type {
ReconstructionObligationManifest,
} from "../domain/reconstructionObligationLedgerSchemas.js";
import type { Evidence } from "../domain/evidence.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
type Diagnostic = ReconstructionObligation["diagnostics"][number];
type Binding = NonNullable<ReconstructionObligation["binding"]>;
@@ -51,8 +50,7 @@ export const uniqueObligationDiagnostics = (
]),
).values(),
].sort((left, right) =>
compareUnicodeCodePoints(
`${left.code}:${left.detail}`,
`${left.code}:${left.detail}`.localeCompare(
`${right.code}:${right.detail}`,
),
);
@@ -36,7 +36,6 @@ import {
obligationReports,
summarizeObligations,
} from "./ReconstructionObligationLedgerProjection.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
type Diagnostic = ReconstructionObligation["diagnostics"][number];
type Binding = NonNullable<ReconstructionObligation["binding"]>;
@@ -61,7 +60,7 @@ export const evaluateReconstructionObligationLedger = ({
}): ReconstructionObligationLedger => {
const bundle = parseEvidenceBundle(bundleInput);
const allCandidates = [...candidatesInput].sort((left, right) =>
compareUnicodeCodePoints(left.obligation_id, right.obligation_id),
left.obligation_id.localeCompare(right.obligation_id),
);
const orphanManifestIds = findOrphanManifestIds(allCandidates, manifest);
const candidates = allCandidates;
@@ -2,7 +2,6 @@ import type { Evidence } from "../domain/evidence.js";
import type { ReconstructionObligation } from "../domain/reconstructionObligationLedgerSchemas.js";
import { evidenceSupportsOriginalAuthority } from "./ReconstructionObligationAuthority.js";
import type { ReconstructionObligationCandidate } from "./ReconstructionObligationCandidates.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
type Binding = NonNullable<ReconstructionObligation["binding"]>;
type Diagnostic = ReconstructionObligation["diagnostics"][number];
@@ -22,8 +21,7 @@ export const originalCasesFor = (
),
).values(),
].sort((left, right) =>
compareUnicodeCodePoints(
`${left.kind}:${left.evidence_id}:${left.location}`,
`${left.kind}:${left.evidence_id}:${left.location}`.localeCompare(
`${right.kind}:${right.evidence_id}:${right.location}`,
),
);
@@ -18,7 +18,6 @@ import type {
ReferenceSourceRead,
} from "../reference/ReferenceSourceReaderTypes.js";
import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
import { typescriptEmittedSourceCandidates } from "./TypeScriptEmittedSourceCandidates.js";
export interface ParsedReferenceSourceEntries {
readonly entries: HistoricalSourceGraphInput["entries"];
@@ -100,9 +99,22 @@ const resolveInternalSpecifier = (
`${normalized}${suffix}`,
`${normalized}/index${suffix}`,
]);
// TypeScript source commonly names the extension emitted for NodeNext.
// Retain present runtime files first; substitute only a missing counterpart.
// Counterparts cover explicit filenames, not directory/index resolution.
candidates.push(...typescriptEmittedSourceCandidates(fromPath, normalized));
if (/\.(?:ts|tsx|mts|cts)$/u.test(fromPath)) {
const sourceSuffixes = normalized.endsWith(".js")
? [".ts", ".tsx", ".d.ts"]
: normalized.endsWith(".mjs")
? [".mts", ".d.mts"]
: normalized.endsWith(".cjs")
? [".cts", ".d.cts"]
: normalized.endsWith(".jsx")
? [".tsx", ".d.ts"]
: [];
const stem = normalized.slice(0, -posix.extname(normalized).length);
candidates.push(...sourceSuffixes.map((suffix) => `${stem}${suffix}`));
}
const match = lookupPath.resolveCandidates(candidates);
return match === undefined
? { to: normalized, resolution: "unresolved" }
-4
View File
@@ -159,7 +159,6 @@ export class FakeSetupHost implements SetupHost {
scope === undefined
? environmentHealthy
: (scope.clients?.length ?? 0) === 0 &&
(scope.skillClients?.length ?? 0) === 0 &&
(scope.providers?.length ?? 0) === 0 &&
scope.skill !== true
? true
@@ -170,9 +169,6 @@ export class FakeSetupHost implements SetupHost {
scope: {
mode: scope === undefined ? "audit-wide" : "explicit",
clients: scope?.clients ?? [],
...(scope?.skillClients === undefined
? {}
: { skill_clients: scope.skillClients }),
providers: scope?.providers ?? [],
skill: scope?.skill === true,
target: null,
-1
View File
@@ -232,7 +232,6 @@ describe("setup scoped readiness", () => {
clients: ["codex"],
providers: [],
skill: true,
skillClients: ["codex"],
});
});
+1 -9
View File
@@ -125,10 +125,7 @@ const runSetupWorkflow = async (
: (options.clientIds ??
options.readinessScope?.clients ??
discovery.defaultClientIds);
let selectedSkillClientIds =
options.skillClientIds ??
options.readinessScope?.skillClients ??
selectedClientIds;
let selectedSkillClientIds = options.skillClientIds ?? selectedClientIds;
let skillNeedsInstall = await host.skillNeedsInstall(selectedSkillClientIds);
let skillSelected =
options.installSkill === true ||
@@ -223,7 +220,6 @@ const runSetupWorkflow = async (
installHopper: planDiscovery.installHopper,
installSkill,
skillSelected,
selectedSkillClientIds,
selectedClients: planned.selectedClients,
clientsToConfigure: planned.clientsToConfigure,
};
@@ -523,7 +519,6 @@ const resolvedReadinessScope = (
readonly selectedClients: readonly SetupClient[];
readonly installHopper: boolean;
readonly skillSelected?: boolean;
readonly selectedSkillClientIds: readonly string[];
},
): DoctorScope => ({
clients: selection.selectedClients.map(({ name }) => name),
@@ -531,9 +526,6 @@ const resolvedReadinessScope = (
? ["hopper"]
: (requested?.providers ?? []),
skill: selection.skillSelected === true || requested?.skill === true,
...(selection.skillSelected === true || requested?.skill === true
? { skillClients: selection.selectedSkillClientIds }
: {}),
});
const summarizeDoctor = (report: DoctorReport): SetupDoctorSummary => ({
+1 -2
View File
@@ -15,7 +15,6 @@ import type {
} from "./SetupTypes.js";
import type { SetupClient } from "./SupportedClients.js";
import type { DoctorReport } from "./Doctor.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/** Read-only setup facts shared by client selection and preflight. */
export interface SetupDiscovery {
@@ -309,7 +308,7 @@ const clientConfigurationDetail = (
installHopper: boolean,
): string => {
const entries = Object.entries(environment)
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, value]) => `${name}=${value}`);
const environmentDetail =
entries.length === 0
@@ -1,5 +1,4 @@
import type { DoctorProviderInspection } from "./Doctor.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/** Preserve adapter-published settings independently of runtime readiness. */
export const providerRegistrationEnvironment = (
@@ -10,5 +9,5 @@ export const providerRegistrationEnvironment = (
.flatMap(({ registrationEnvironment }) =>
Object.entries(registrationEnvironment),
)
.sort(([left], [right]) => compareUnicodeCodePoints(left, right)),
.sort(([left], [right]) => left.localeCompare(right)),
);
+1 -2
View File
@@ -6,7 +6,6 @@ import {
HermesClientPathError,
resolveHermesClientHome,
} from "./HermesClientHome.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/** One supported client configuration location. */
export interface SetupClient {
@@ -643,6 +642,6 @@ export const clientSkillDirectories = (
});
}
return [...destinations.values()].sort((left, right) =>
compareUnicodeCodePoints(left.directory, right.directory),
left.directory.localeCompare(right.directory),
);
};
@@ -1,29 +0,0 @@
/** Emitted NodeNext suffixes and the TypeScript source counterparts they name. */
const EMITTED_SUFFIXES: ReadonlyArray<
readonly [suffix: string, sourceSuffixes: readonly string[]]
> = [
[".js", [".ts", ".tsx", ".d.ts"]],
[".mjs", [".mts", ".d.mts"]],
[".cjs", [".cts", ".d.cts"]],
[".jsx", [".tsx", ".d.ts"]],
];
/**
* Return TypeScript source counterparts for an explicit emitted filename.
* Callers retain exact runtime-file precedence and must not use these for
* directory/index, URL, HTML, or package target resolution.
*/
export const typescriptEmittedSourceCandidates = (
sourcePath: string,
candidate: string,
): readonly string[] => {
if (!/\.(?:ts|tsx|mts|cts)$/u.test(sourcePath)) return [];
const emitted = EMITTED_SUFFIXES.find(([suffix]) =>
candidate.endsWith(suffix),
);
if (emitted === undefined) return [];
// Strip the matched suffix by length. posix.extname would miss dotfile
// names such as ".js", whose whole basename is the emitted suffix itself.
const stem = candidate.slice(0, candidate.length - emitted[0].length);
return emitted[1].map((suffix) => `${stem}${suffix}`);
};
@@ -1,120 +0,0 @@
import type { EvidenceWriter } from "../investigation/InvestigationRecordPort.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { ok, type Result } from "../../domain/result.js";
/**
* Complete class-inventory pages dominate raw-call provenance for focused
* searches: one traversal embeds every examined class name regardless of the
* match count. The summary keeps the pagination audit inline (totals, page
* offsets and sizes, elided item counts) and links the retained record that
* still carries the complete inventory.
*/
const elideInventoryText = (text: string): string | undefined => {
let page: JsonValue;
try {
page = JSON.parse(text) as JsonValue;
} catch {
return undefined;
}
if (
typeof page !== "object" ||
page === null ||
Array.isArray(page) ||
!Array.isArray(page.items)
)
return undefined;
const { items, ...audited } = page;
return JSON.stringify({ ...audited, items_omitted: items.length });
};
/** Replace inventoried page bodies with their pagination audit. */
const summarizeInventoryCall = (call: JsonValue): JsonValue => {
if (
typeof call !== "object" ||
call === null ||
Array.isArray(call) ||
call.operation !== "list_classes"
)
return call;
const response = call.response;
if (
typeof response !== "object" ||
response === null ||
Array.isArray(response) ||
!Array.isArray(response.content)
)
return call;
let elided = false;
const content = response.content.map((entry: JsonValue) => {
if (
typeof entry !== "object" ||
entry === null ||
Array.isArray(entry) ||
typeof entry.text !== "string"
)
return entry;
const summarized = elideInventoryText(entry.text);
if (summarized === undefined) return entry;
elided = true;
return { ...entry, text: summarized };
});
return elided
? { ...call, inventory_elided: true, response: { ...response, content } }
: call;
};
/**
* Retain the complete search Evidence — full raw provider calls included —
* in the session record, then deliver a bounded summary Evidence inline.
* Callers without a retaining session keep the complete inline delivery, so
* complete CLI JSON output is unchanged. Coverage metadata stays truthful in
* both copies: the examined inventory is never rewritten to matched-only.
*/
export const summarizeRetainedAndroidSearch = (
evidence: Evidence,
recordEvidence: EvidenceWriter["recordEvidence"] | undefined,
): Result<Evidence, AnalysisError> => {
if (recordEvidence === undefined) return ok(evidence);
const recorded = recordEvidence(evidence);
if (!recorded.ok) return recorded;
const subject = evidence.subject;
const raw = evidence.raw_result;
const summarized: JsonValue =
raw === null ||
typeof raw !== "object" ||
Array.isArray(raw) ||
!Array.isArray(raw.calls)
? raw
: {
...raw,
complete_evidence_id: evidence.evidence_id,
calls: raw.calls.map(summarizeInventoryCall),
};
return ok(
createEvidence(
subject === null
? undefined
: {
path: subject.local_path,
sha256: subject.digest.sha256,
format: subject.format,
...(subject.architecture === null
? {}
: { architecture: subject.architecture }),
},
evidence.provider,
{
operation: evidence.operation,
parameters: evidence.parameters,
result: evidence.normalized_result,
rawResult: summarized,
confidence: evidence.confidence,
limitations: evidence.limitations,
locations: evidence.locations,
evidenceLinks: [evidence.evidence_id],
},
),
);
};
@@ -25,7 +25,6 @@ import {
evaluateAnalysisProviderCandidate,
type AnalysisProviderCandidateEvaluation,
} from "./AnalysisProviderEvaluation.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
/** Deterministic discovery truth for one configured deep provider. */
export interface AnalysisProviderCandidateStatus {
@@ -110,14 +109,12 @@ export class AnalysisProviderRegistry {
}
this.#providers = Object.freeze(
[...providers].sort((left, right) =>
compareUnicodeCodePoints(left.identity().id, right.identity().id),
left.identity().id.localeCompare(right.identity().id),
),
);
this.#byId = byId;
this.#declaredOperations = Object.freeze(
[...declaredOperations].sort((left, right) =>
compareUnicodeCodePoints(left, right),
),
[...declaredOperations].sort((left, right) => left.localeCompare(right)),
);
}
+6 -62
View File
@@ -98,30 +98,6 @@ export class BinarySession
this.#providerRouter = providerRouter;
}
/**
* Fast-path liveness gate (#1719): short-circuit only when the retained
* client is genuinely alive. The probe reads the child's operation health
* rather than the composite's unconditionally-ok `health`, so a dead child
* falls through and a real client is started; an aborted caller leaves the
* session untouched. Returns the fast-path result, or undefined to fall
* through to the slow path.
*/
async #tryRetainedFastPath(
retained: SessionBinding,
target: BinaryTarget,
options: Pick<BinarySessionOpenOptions, "signal" | "snapshot">,
): Promise<Result<BinaryTarget, AnalysisError> | undefined> {
const live = await this.#retainedClientLive(retained.client, options);
if (isAborted(options.signal))
return err(new AnalysisCancelledError("open_binary"));
if (!live) return undefined;
if (options.snapshot !== undefined) {
const imported = this.importAnalysisSnapshot(options.snapshot);
if (!imported.ok) return imported;
}
return ok(target);
}
/** Identify the provider producing evidence for this session. */
providerIdentity(operation?: AnalysisOperation): ProviderIdentity {
const route = this.#currentRoute();
@@ -271,14 +247,12 @@ export class BinarySession
if (!pendingClosed.ok) return pendingClosed;
if (isAborted(options.signal))
return err(new AnalysisCancelledError("open_binary"));
const retained = sameTarget && sameProfile ? this.#active : undefined;
if (retained !== undefined) {
const fastPath = await this.#tryRetainedFastPath(
retained,
target,
options,
);
if (fastPath !== undefined) return fastPath;
if (sameTarget && sameProfile) {
if (options.snapshot !== undefined) {
const imported = this.importAnalysisSnapshot(options.snapshot);
if (!imported.ok) return imported;
}
return ok(target);
}
const previous = this.#active;
this.#active = undefined;
@@ -729,36 +703,6 @@ export class BinarySession
);
}
/**
* Probe whether a retained provider client is still live.
*
* On production routes the client is a CompositeProvider whose `health`
* answers unconditionally ok without touching a child, so `health` alone
* cannot see a dead child. `operationHealthSnapshot` reaches the
* instantiated children instead. Only the known-live states allow the
* fast path: `exited`/`unreachable` are dead, `unknown` is never
* evidence of health (it is what a recorded but uncharacterized failure
* reports), and `not_started` is a startup that never completed — in the
* Hopper timeout case the client holds retained resources that block the
* next `start()` until `close()` retries cleanup, so treating it as live
* would skip the retire path and hand the wedged client to subsequent
* analysis. The gate fails closed: without a snapshot signal, fall back
* to the `health` admission probe.
*/
async #retainedClientLive(
client: AnalysisClient,
options: Pick<BinarySessionOpenOptions, "signal">,
): Promise<boolean> {
const snapshot = client.operationHealthSnapshot?.();
if (snapshot !== undefined)
return snapshot.state === "idle" || snapshot.state === "busy";
try {
return (await client.execute("health", {}, options)).ok;
} catch {
return false;
}
}
async #restore(
previous: SessionBinding | undefined,
): Promise<Result<null, AnalysisError>> {
@@ -1,338 +0,0 @@
import { describe, expect, it } from "vitest";
import {
createAnalysisExecution,
type AnalysisClient,
type AnalysisOperation,
type AnalysisProvider,
type AnalysisProviderCandidate,
type CapabilityDescriptor,
type ProviderAvailability,
type ProviderIdentity,
type ProviderTargetSupport,
} from "../AnalysisProvider.js";
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
import type { ProviderOperationHealth } from "../../domain/providerOperationHealth.js";
import { err, ok } from "../../domain/result.js";
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
import { AnalysisProviderRegistry } from "./AnalysisProviderRegistry.js";
import { BinarySession } from "./BinarySession.js";
import { SessionProviderRouter } from "./SessionProviderRouter.js";
/**
* Regression coverage for https://github.com/morluto/rea/issues/1719:
* `BinarySession#open` must not short-circuit on the `sameTarget &&
* sameProfile` fast path when the retained provider client is dead. It must
* start a fresh client instead.
*
* The fixtures are production-composed: a deep provider plus an auxiliary
* provider, so `SessionProviderRouter` wraps them in a real
* `CompositeProvider` exactly like `src/composition/binary.ts` does for the
* CLI/MCP runtime. The composite's `health` answers unconditionally ok
* without touching a child, so a probe that only calls `health` cannot see a
* dead child — the liveness signal must reach the actual child through
* `operationHealthSnapshot`, the way `HopperClient.operationHealth` reports a
* recorded process exit.
*/
describe("BinarySession open liveness", () => {
it("reuses the retained client while the child stays live", async () => {
const { session, deep } = openSession();
expect((await session.open(process.execPath)).ok).toBe(true);
// Instantiate the deep child inside the composite, like a real query would.
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(1);
const second = await session.open(process.execPath);
expect(second.ok).toBe(true);
// Still the same composite and child: nothing was retired or recreated.
expect(deep.created).toHaveLength(1);
expect(deep.created[0]?.closeCalled).toBe(false);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(1);
});
it("starts a fresh client when the retained child died", async () => {
const { session, deep } = openSession();
expect((await session.open(process.execPath)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(1);
// Kill the provider process out from under the session, then re-open
// the same target: the session must not short-circuit on the dead
// child — it must retire it and start a real one.
const dead = deep.created[0];
dead?.kill();
const second = await session.open(process.execPath);
expect(second.ok).toBe(true);
// The dead binding was retired, not retained.
expect(dead?.closeCalled).toBe(true);
// The next query runs against a freshly created child, not the dead one.
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(2);
});
it("treats an uncharacterized operation failure as dead", async () => {
// A recorded failure the provider could not characterize ("unknown",
// e.g. a Hopper spawn failure with no observed exit code) is never
// evidence of health: the gate fails closed.
const { session, deep } = openSession();
expect((await session.open(process.execPath)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(1);
const dead = deep.created[0];
dead?.kill("unknown");
const second = await session.open(process.execPath);
expect(second.ok).toBe(true);
expect(dead?.closeCalled).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(2);
});
it("retires a retained client whose startup never completed", async () => {
// A Hopper startup timeout leaves the operation-health snapshot in
// "not_started" while the retained resources block the next start()
// until close() retries cleanup. Classifying that state as live would return success
// without entering the retire path, so subsequent analysis still hits
// the wedged client.
const { session, deep } = openSession();
expect((await session.open(process.execPath)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(1);
const wedged = deep.created[0];
wedged?.kill("not_started");
const second = await session.open(process.execPath);
expect(second.ok).toBe(true);
// The wedged binding was retired (close ran, so cleanup is retried),
// not reused.
expect(wedged?.closeCalled).toBe(true);
// The next query runs against a freshly created child, not the wedged one.
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(2);
});
it("falls back to the health probe when the child reports no operation health", async () => {
// Without an operation-health hook there is no child-liveness signal,
// so the probe keeps the previous `health` admission behavior.
const { session, deep } = openSession({ operationHealth: false });
expect((await session.open(process.execPath)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
expect(deep.created).toHaveLength(1);
const second = await session.open(process.execPath);
expect(second.ok).toBe(true);
// The composite's `health` answers ok, so the live client is reused.
expect(deep.created).toHaveLength(1);
expect(deep.created[0]?.closeCalled).toBe(false);
});
it("reports cancellation without touching the session when aborted mid-probe", async () => {
const { session, deep, onProbe } = openSession();
expect((await session.open(process.execPath)).ok).toBe(true);
expect((await session.execute("address_name", {})).ok).toBe(true);
const controller = new AbortController();
// Fire the abort while the fast-path liveness probe is in flight: the
// hook runs synchronously inside the snapshot read, so the abort lands
// after the probe started but before the fast-path decision completes.
let fired = false;
onProbe(() => {
if (!fired) {
fired = true;
controller.abort();
}
});
const cancelled = await session.open(process.execPath, {
signal: controller.signal,
});
expect(cancelled).toMatchObject({
ok: false,
error: { _tag: "AnalysisCancelledError" },
});
// The dead-or-live question is moot: the session must be untouched.
expect(deep.created).toHaveLength(1);
expect(deep.created[0]?.closeCalled).toBe(false);
const reopened = await session.open(process.execPath);
expect(reopened.ok).toBe(true);
expect(deep.created).toHaveLength(1);
});
});
interface KillableChild extends AnalysisClient {
kill(state?: "exited" | "unreachable" | "unknown" | "not_started"): void;
closeCalled: boolean;
}
const deepIdentity: ProviderIdentity = {
id: "liveness-deep",
name: "Liveness deep provider",
version: "1",
};
const auxIdentity: ProviderIdentity = {
id: "liveness-aux",
name: "Liveness auxiliary provider",
version: "1",
};
const capability = (
identity: ProviderIdentity,
operation: Exclude<AnalysisOperation, "health">,
): CapabilityDescriptor => ({
provider: identity,
operation,
available: true,
reason: null,
effects: {
mutatesArtifact: false,
launchesProcess: false,
mayShowUi: false,
mayAccessNetwork: false,
mayWriteFilesystem: false,
changesPermissions: false,
requiresRoot: false,
},
limitations: [],
});
const openSession = (
options: { operationHealth?: boolean } = {},
): {
session: BinarySession;
deep: { created: KillableChild[] };
onProbe: (hook: () => void) => void;
} => {
const { provider, created, onProbe } = killableDeepProvider(options);
// Mirror the production composition: a deep provider plus auxiliary
// providers, so the route wraps them in a real CompositeProvider (whose
// trivially-ok `health` must not mask a dead child).
const aux: AnalysisProvider = {
identity: () => auxIdentity,
capabilities: () => [],
createClient: (): AnalysisClient => ({
execute: (operation) =>
Promise.resolve(
ok(createAnalysisExecution(`aux:${operation}`, auxIdentity)),
),
close: () => Promise.resolve(ok(null)),
}),
};
const session = new BinarySession(
SessionProviderRouter.selectable(new AnalysisProviderRegistry([provider]), [
aux,
]),
new InvestigationRecords(),
);
return { session, deep: { created }, onProbe };
};
const killableDeepProvider = (options: {
operationHealth?: boolean;
}): {
provider: AnalysisProviderCandidate;
created: KillableChild[];
onProbe: (hook: () => void) => void;
} => {
const created: KillableChild[] = [];
let probeHook: (() => void) | undefined;
const withOperationHealth = options.operationHealth !== false;
const provider: AnalysisProviderCandidate = {
identity: () => deepIdentity,
capabilities: () => [capability(deepIdentity, "address_name")],
inspectAvailability: (): ProviderAvailability => ({
status: "available",
code: null,
reason: null,
diagnostics: {},
}),
inspectTargetSupport: (): ProviderTargetSupport => ({
status: "supported",
code: null,
reason: null,
diagnostics: {},
}),
resolveAnalysisProfile: () =>
Promise.resolve(
ok({
profile: createAnalysisProfile(
{ id: deepIdentity.id, name: deepIdentity.name, version: "1" },
{ fixture: "liveness" },
),
}),
),
createClient: (): KillableChild => {
let alive = true;
let closeCalled = false;
let deadState: "exited" | "unreachable" | "unknown" | "not_started" =
"exited";
const client: KillableChild = {
execute: (operation) => {
return alive
? Promise.resolve(
ok(
createAnalysisExecution(
operation === "health" ? null : `liveness:${operation}`,
deepIdentity,
),
),
)
: Promise.resolve(
err(
new ProviderAdapterError(deepIdentity.id, operation, {
diagnostics: { reason: "process_killed" },
}),
),
);
},
// Genuine child-liveness signal, like HopperClient.operationHealth:
// a recorded process exit stays visible after the kill.
...(withOperationHealth
? {
operationHealthSnapshot: (): ProviderOperationHealth => {
probeHook?.();
return {
state: alive ? "idle" : deadState,
stage: alive ? null : "connection",
retryAction: alive ? null : "restart_provider",
exitCode: alive ? null : 1,
requests: [],
};
},
}
: {}),
close: () => {
closeCalled = true;
return Promise.resolve(ok(null));
},
get closeCalled() {
return closeCalled;
},
kill: (
state:
| "exited"
| "unreachable"
| "unknown"
| "not_started" = "exited",
) => {
alive = false;
deadState = state;
},
};
created.push(client);
return client;
},
};
return {
provider,
created,
onProbe: (hook: () => void) => {
probeHook = hook;
},
};
};
+4 -11
View File
@@ -11,7 +11,6 @@ import type {
SessionProviderRoute,
SessionProviderRouter,
} from "./SessionProviderRouter.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
interface BinarySessionStatusInput {
readonly target: BinaryTarget | undefined;
@@ -40,9 +39,7 @@ export const binarySessionStatus = ({
}: BinarySessionStatusInput): JsonValue => {
const capabilities = [...route.capabilities.values()]
.filter(({ operation }) => operation !== "inventory_artifact")
.sort((left, right) =>
compareUnicodeCodePoints(left.operation, right.operation),
)
.sort((left, right) => left.operation.localeCompare(right.operation))
.map((descriptor) => {
const unavailable = runtimeUnavailability.get(descriptor.operation);
return unavailable === undefined
@@ -72,9 +69,7 @@ export const binarySessionStatus = ({
capabilities: candidate.capabilities
.slice()
.filter(({ operation }) => operation !== "inventory_artifact")
.sort((left, right) =>
compareUnicodeCodePoints(left.operation, right.operation),
)
.sort((left, right) => left.operation.localeCompare(right.operation))
.map(capabilityStatus),
}));
const common = {
@@ -107,9 +102,7 @@ const requestActivityStatus = (
) => {
const providers = snapshots
.slice()
.sort((left, right) =>
compareUnicodeCodePoints(left.provider.id, right.provider.id),
)
.sort((left, right) => left.provider.id.localeCompare(right.provider.id))
.map(({ provider, active, queuedRequests }) => ({
provider: providerSummary(provider),
active:
@@ -157,7 +150,7 @@ const processLineageStatus = (
snapshots: snapshots
.slice()
.sort((left, right) =>
compareUnicodeCodePoints(left.provider.id, right.provider.id),
left.provider.id.localeCompare(right.provider.id),
)
.map(({ provider, observation }) => ({
provider: providerSummary(provider),
+4 -5
View File
@@ -10,7 +10,6 @@ import type {
ProviderIdentity,
} from "../AnalysisProvider.js";
import { closeAnalysisClient } from "./AnalysisClientCleanup.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
/** Synthetic identity for a deterministic provider set. */
const compositeProviderIdentity = (
@@ -54,8 +53,8 @@ export class CompositeProvider implements AnalysisProvider {
this.#capabilities = Object.freeze(
capabilities.sort(
(left, right) =>
compareUnicodeCodePoints(left.operation, right.operation) ||
compareUnicodeCodePoints(left.provider.id, right.provider.id),
left.operation.localeCompare(right.operation) ||
left.provider.id.localeCompare(right.provider.id),
),
);
}
@@ -123,13 +122,13 @@ export class CompositeProvider implements AnalysisProvider {
[...clients.values()]
.flatMap((client) => client.runtimeLineageSnapshots?.() ?? [])
.sort((left, right) =>
compareUnicodeCodePoints(left.provider.id, right.provider.id),
left.provider.id.localeCompare(right.provider.id),
),
requestActivitySnapshots: () =>
[...clients.values()]
.flatMap((client) => client.requestActivitySnapshots?.() ?? [])
.sort((left, right) =>
compareUnicodeCodePoints(left.provider.id, right.provider.id),
left.provider.id.localeCompare(right.provider.id),
),
operationHealthSnapshot: () => {
for (const client of clients.values()) {
@@ -24,7 +24,6 @@ import {
type UpdateUnknownInput,
} from "../../domain/residualUnknown.js";
import { err, ok, type Result } from "../../domain/result.js";
import { compareUnicodeCodePoints } from "../../domain/unicodeCodePointOrder.js";
type EvidenceLedgerFailure = EvidenceIntegrityError;
type RecordResult = Result<"added" | "duplicate", EvidenceLedgerFailure>;
@@ -322,9 +321,7 @@ export class EvidenceLedger {
unknown.severity === filters.severity) &&
(filters.domain === undefined || unknown.domain === filters.domain),
)
.sort((left, right) =>
compareUnicodeCodePoints(left.unknown_id, right.unknown_id),
)
.sort((left, right) => left.unknown_id.localeCompare(right.unknown_id))
.map((unknown) => structuredClone(unknown));
}
@@ -4,7 +4,6 @@ import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
import {
analyzeJavaScriptApplicationInputSchema,
type JavaScriptApplicationAnalysisResult,
parseOwnedJavaScriptApplicationAnalysisSteps,
} from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { AnalysisOutputError } from "../../domain/analysisErrorCore.js";
@@ -21,10 +20,7 @@ import {
checkpointJavaScriptAnalysis,
completeJavaScriptAnalysisSteps,
} from "./JavaScriptAnalysisControl.js";
import {
reconstructJavaScriptArtifact,
type JavaScriptArtifactReconstructionResult,
} from "./JavaScriptArtifactReconstruction.js";
import { reconstructJavaScriptArtifact } from "./JavaScriptArtifactReconstruction.js";
import { JAVASCRIPT_APPLICATION_PROVIDER } from "../InvestigationProviders.js";
const OPERATION = "analyze_javascript_application" as const;
@@ -49,9 +45,6 @@ export const analyzeJavaScriptApplicationValidated = async (
scope: ArtifactResourceScope,
options: ExecutionOptions = {},
): Promise<Result<Evidence, AnalysisError>> => {
let reconstructed: JavaScriptArtifactReconstructionResult | undefined;
let result: JavaScriptApplicationAnalysisResult | undefined;
let evidence: Evidence | undefined;
try {
assertJavaScriptAnalysisActive(options.signal);
await options.progress?.report({
@@ -61,7 +54,7 @@ export const analyzeJavaScriptApplicationValidated = async (
message: "Inventorying and parsing application artifacts",
});
await checkpointJavaScriptAnalysis(options.signal);
reconstructed = await reconstructJavaScriptArtifact(
const reconstructed = await reconstructJavaScriptArtifact(
{
input_path: input.input_path,
format: input.format,
@@ -85,7 +78,7 @@ export const analyzeJavaScriptApplicationValidated = async (
message: "Validating result metadata and cross-graph bindings",
});
await checkpointJavaScriptAnalysis(options.signal);
result = await completeJavaScriptAnalysisSteps(
const result = await completeJavaScriptAnalysisSteps(
parseOwnedJavaScriptApplicationAnalysisSteps({
...application,
summary,
@@ -100,12 +93,13 @@ export const analyzeJavaScriptApplicationValidated = async (
message: "Creating and hashing application analysis Evidence",
});
await checkpointJavaScriptAnalysis(options.signal);
evidence = await createOwnedJavaScriptApplicationEvidenceCooperatively(
input,
result,
options.signal,
options.progress,
);
const evidence =
await createOwnedJavaScriptApplicationEvidenceCooperatively(
input,
result,
options.signal,
options.progress,
);
await checkpointJavaScriptAnalysis(options.signal);
await options.progress?.report({
phase: "analyze_javascript_application",
@@ -117,38 +111,6 @@ export const analyzeJavaScriptApplicationValidated = async (
assertJavaScriptAnalysisActive(options.signal);
return ok(evidence);
} catch (cause: unknown) {
if (
cause instanceof ArtifactReaderFailure &&
cause.reason === "cancelled" &&
reconstructed !== undefined
) {
try {
if (result === undefined) {
const { electron_summary: summary, ...application } = reconstructed;
result = await completeJavaScriptAnalysisSteps(
parseOwnedJavaScriptApplicationAnalysisSteps({
...application,
summary,
limitations: reconstructed.graph.limitations,
}),
);
}
evidence ??=
await createOwnedJavaScriptApplicationEvidenceCooperatively(
input,
result,
);
return err(
artifactFailureToOperationError(cause).retainPartialObservation(
evidence,
),
);
} catch (completionCause: unknown) {
// A validation or Evidence failure while finishing the partial result
// remains the primary failure instead of being hidden by cancellation.
cause = completionCause;
}
}
// Preserve execution constraints and their completed partial Evidence.
if (cause instanceof AnalysisError) return err(cause);
if (cause instanceof ArtifactReaderFailure)
@@ -868,365 +868,6 @@ describe("package exports exact targets", () => {
});
});
describe("package import alias resolution", () => {
it.each(["import", "require"] as const)(
"resolves an exact #%s alias from the enclosing package imports map",
(moduleKind) => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": "./config.cjs" },
}),
"main.js": "",
"config.cjs": "",
});
expect(
resolve({
declaredPath: "#config",
sourcePath: "main.js",
context: "module-specifier",
moduleKind,
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "config.cjs",
declared_path: "#config",
});
},
);
it("resolves an alias from a nested file through the enclosing package root", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": "./config.cjs" },
}),
"src/main.js": "",
"config.cjs": "",
});
expect(
resolve({
declaredPath: "#config",
sourcePath: "src/main.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "config.cjs",
});
});
it("uses a nested package imports map rather than the application root", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": "./root.cjs" },
}),
"root.cjs": "",
"node_modules/fixture/package.json": JSON.stringify({
imports: { "#config": "./local.cjs" },
}),
"node_modules/fixture/index.js": "",
"node_modules/fixture/local.cjs": "",
});
expect(
resolve({
declaredPath: "#config",
sourcePath: "node_modules/fixture/index.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "node_modules/fixture/local.cjs",
});
});
it("does not inherit an ancestor imports map across a nested package.json", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": "./root.cjs" },
}),
"root.cjs": "",
"node_modules/fixture/package.json": "{}",
"node_modules/fixture/index.js": "",
});
const outcome = resolve({
declaredPath: "#config",
sourcePath: "node_modules/fixture/index.js",
context: "module-specifier",
moduleKind: "require",
files,
});
expect(outcome).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
expect(outcome.limitations.join(" ")).toContain(
"node_modules/fixture/package.json",
);
expect(outcome.limitations.join(" ")).not.toContain("root.cjs");
});
it("does not apply a nested package map to the application root importer", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": "./root.cjs" },
}),
"main.js": "",
"root.cjs": "",
"node_modules/fixture/package.json": JSON.stringify({
imports: { "#config": "./local.cjs" },
}),
"node_modules/fixture/local.cjs": "",
});
expect(
resolve({
declaredPath: "#config",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "root.cjs",
});
});
});
describe("package import alias target selection", () => {
it("selects import and require conditions from an imports target", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: {
"#config": {
import: "./config.mjs",
require: "./config.cjs",
default: "./fallback.js",
},
},
}),
"main.js": "",
"config.mjs": "",
"config.cjs": "",
"fallback.js": "",
});
expect(
resolve({
declaredPath: "#config",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "import",
files,
}).resolved_path,
).toBe("config.mjs");
expect(
resolve({
declaredPath: "#config",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
}).resolved_path,
).toBe("config.cjs");
});
it("resolves array import targets at their first string entry", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": [{ import: "./missing.mjs" }, "./config.cjs"] },
}),
"main.js": "",
"config.cjs": "",
});
expect(
resolve({
declaredPath: "#config",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "config.cjs",
});
});
it("does not add an extension or index to a selected import target", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": "./config" },
}),
"main.js": "",
"config.cjs": "",
"config/index.js": "",
});
const outcome = resolve({
declaredPath: "#config",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
});
expect(outcome).toMatchObject({
resolution_status: "not-found",
resolved_path: null,
});
expect(outcome.limitations.join(" ")).toContain(
"import targets are exact files",
);
});
});
describe("package import alias refusals", () => {
it("reports a missing exact alias without looking in another package", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": "./config.cjs" },
}),
"main.js": "",
"config.cjs": "",
});
const outcome = resolve({
declaredPath: "#other",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
});
expect(outcome).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
expect(outcome.limitations.join(" ")).toContain("#other");
expect(outcome.limitations.join(" ")).toContain("#config");
});
it("reports an explicit null import target as blocked", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": null },
}),
"main.js": "",
"config.cjs": "",
});
const outcome = resolve({
declaredPath: "#config",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
});
expect(outcome).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
expect(outcome.limitations.join(" ")).toContain("explicit null");
});
it("leaves wildcard import aliases unresolved", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#internal/*": "./src/*.js" },
}),
"main.js": "",
"src/util.js": "",
});
const outcome = resolve({
declaredPath: "#internal/util",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "import",
files,
});
expect(outcome).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
expect(outcome.limitations.join(" ")).toMatch(/wildcard/i);
});
it("leaves recursive and external-package import targets unresolved", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#loop": "#config", "#dep": "fixture" },
}),
"main.js": "",
"node_modules/fixture/index.js": "",
});
const recursive = resolve({
declaredPath: "#loop",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
});
const external = resolve({
declaredPath: "#dep",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
});
expect(recursive).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
expect(recursive.limitations.join(" ")).toMatch(/recursive/i);
expect(external).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
expect(external.limitations.join(" ")).toMatch(/external-package/i);
});
it("rejects reserved hash specifiers", () => {
const files = aliasFiles({
"package.json": JSON.stringify({
imports: { "#config": "./config.cjs" },
}),
"main.js": "",
"config.cjs": "",
});
expect(
resolve({
declaredPath: "#",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({ resolution_status: "rejected", resolved_path: null });
});
it("does not use a foreign-container enclosing package.json", () => {
const files = fileMap([
file(
"package.json",
"other",
JSON.stringify({ imports: { "#config": "./config.cjs" } }),
),
file("main.js", "root"),
file("config.cjs", "root"),
]);
const outcome = resolve({
declaredPath: "#config",
sourcePath: "main.js",
context: "module-specifier",
moduleKind: "require",
files,
});
expect(outcome).toMatchObject({
resolution_status: "external",
resolved_path: null,
});
expect(outcome.limitations.join(" ")).toContain("package scopes");
});
});
describe("directory package entrypoint precedence", () => {
it.each([
["require", '{"main":"actual.cjs"}', "actual.cjs"],
@@ -1464,159 +1105,6 @@ describe("contextual JavaScript module identity", () => {
});
});
describe("TypeScript NodeNext emitted-extension resolution", () => {
const container = "container";
it.each([
["./dep.js", "src/dep.ts"],
["./ui/Button.js", "src/ui/Button.tsx"],
["./cfg.mjs", "src/cfg.mts"],
["./legacy.cjs", "src/legacy.cts"],
] as const)(
"resolves %s to its TypeScript source counterpart",
(specifier, target) => {
const files = fileMap([
file("src/main.ts", container),
file(target, container, "export const value = 1;"),
]);
expect(
resolve({
declaredPath: specifier,
sourcePath: "src/main.ts",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({ resolution_status: "resolved", resolved_path: target });
},
);
it.each([
["./dep.js", "src/dep.d.ts"],
["./cfg.mjs", "src/cfg.d.mts"],
["./legacy.cjs", "src/legacy.d.cts"],
] as const)(
"resolves %s to a declaration counterpart",
(specifier, target) => {
const files = fileMap([
file("src/main.ts", container),
file(target, container, "export const value = 1;"),
]);
expect(
resolve({
declaredPath: specifier,
sourcePath: "src/main.ts",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({ resolution_status: "resolved", resolved_path: target });
},
);
it("prefers a present runtime file and keeps the fallback off for JS callers", () => {
const files = fileMap([
file("src/main.ts", container),
file("src/dep.js", container),
file("src/dep.ts", container),
file("src/js-main.js", container),
]);
expect(
resolve({
declaredPath: "./dep.js",
sourcePath: "src/main.ts",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "src/dep.js",
});
expect(
resolve({
declaredPath: "./dep.js",
sourcePath: "src/js-main.js",
context: "module-specifier",
moduleKind: "import",
files: fileMap([
file("src/js-main.js", container),
file("src/dep.ts", container),
]),
}),
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
});
it("does not apply the mapping to URL resources or package export targets", () => {
const urlFiles = fileMap([
file("src/main.ts", container),
file("src/asset.ts", container),
]);
expect(
resolve({
declaredPath: "asset.js",
sourcePath: "src/main.ts",
context: "url-reference",
files: urlFiles,
}),
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
const packageFiles = fileMap([
file("src/main.ts", container),
file(
"src/node_modules/fixture/package.json",
container,
'{"exports":"./dep.js"}',
),
file("src/node_modules/fixture/dep.ts", container),
]);
expect(
resolve({
declaredPath: "fixture",
sourcePath: "src/main.ts",
context: "module-specifier",
moduleKind: "import",
files: packageFiles,
}),
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
});
it("does not substitute source counterparts for directory or index resolution", () => {
const files = fileMap([
file("src/main.ts", container),
file("src/lib/index.ts", container),
]);
expect(
resolve({
declaredPath: "./lib.js",
sourcePath: "src/main.ts",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({ resolution_status: "not-found", resolved_path: null });
});
it("resolves an emitted dotfile name within its own directory", () => {
const files = fileMap([
file("src/main.ts", container),
file("src/.ts", container),
]);
expect(
resolve({
declaredPath: "./.js",
sourcePath: "src/main.ts",
context: "module-specifier",
moduleKind: "import",
files,
}),
).toMatchObject({
resolution_status: "resolved",
resolved_path: "src/.ts",
});
});
});
describe("url-reference JavaScript artifact path resolution", () => {
it.each([
"app%00.js.map",
@@ -1723,15 +1211,6 @@ const file = (
: { included: true, value: text },
});
const aliasFiles = (
entries: Readonly<Record<string, string | null>>,
): ReturnType<typeof fileMap> =>
fileMap(
Object.entries(entries).map(([path, text]) =>
file(path, "root", text ?? ""),
),
);
describe("exact HTML resource resolution", () => {
it.each([
["extension", ["app.js"]],
@@ -14,12 +14,6 @@ import {
percentDecodeUrlPath,
stripQueryAndFragment,
} from "../../domain/artifactPathSyntax.js";
import {
classifyPackageImportSpecifier,
classifyPackageImportTargetString,
lookupExactPackageImport,
} from "../../domain/javascript/javascriptPackageImportAliases.js";
import { typescriptEmittedSourceCandidates } from "../TypeScriptEmittedSourceCandidates.js";
type ArtifactPathResolutionContext =
| "package-entrypoint"
@@ -60,7 +54,6 @@ export interface ResolveArtifactPathInput {
readonly context: ArtifactPathResolutionContext;
readonly files: ReadonlyMap<string, JavaScriptArtifactFile>;
readonly htmlBaseHref?: string | null;
/** Import/require semantics; runtime resource paths omit this provenance. */
readonly moduleKind?: "import" | "require";
}
@@ -75,21 +68,6 @@ const EXTENSIONS = [
".node",
];
// A private URL namespace for logical artifact paths, including root packages.
// Encode inventory segments so filename punctuation remains literal in the base.
const PACKAGE_ARTIFACT_ROOT_URL = new URL("file:///rea-artifact/");
const packageMetadataUrl = (path: string): URL =>
new URL(
path.split("/").map(encodeURIComponent).join("/"),
PACKAGE_ARTIFACT_ROOT_URL,
);
interface PackageFileTarget {
readonly value: string;
/** Normalized URL retained until loader-specific validation and file lookup. */
readonly url: URL;
}
type CandidateResolution =
| {
readonly resolvedPath: string;
@@ -125,13 +103,6 @@ export const resolveArtifactPathByContext = (
};
const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
// Package import keys are literal lookup identities, not artifact paths.
// The selected target is admitted separately by resolveExportTarget.
if (
input.context === "module-specifier" &&
input.declaredPath.startsWith("#")
)
return resolvePackageImportAlias(input, input.declaredPath);
const rejected = rejectDeclaration(input.declaredPath);
if (rejected !== null) return rejected;
const candidate = contextualCandidate(input);
@@ -145,16 +116,7 @@ const resolvePath = (input: ResolveArtifactPathInput): CandidateResolution => {
`The exact HTML resource ${confined} was not found among the selected application files.`,
])
);
return resolveCandidate(
input,
confined,
new Set(),
input.context === "module-specifier" &&
input.moduleKind !== undefined &&
(input.declaredPath.startsWith(".") || input.declaredPath.startsWith("/"))
? typescriptEmittedSourceCandidates(input.sourcePath, confined)
: [],
);
return resolveCandidate(input, confined, new Set());
};
const rejectDeclaration = (declared: string): CandidateResolution | null => {
@@ -240,9 +202,9 @@ const bareModuleCandidate = (
declared: string,
): CandidateResolution => {
const packageName = barePackageName(declared);
if (packageName === null || isBuiltin(declared))
if (packageName === null || isBuiltin(declared) || declared.startsWith("#"))
return unresolvedCandidate("external", [
"The bare specifier is a Node builtin or invalid package name.",
"The bare specifier is a Node builtin, package import map, or invalid package name.",
]);
if (input.moduleKind === "import" && packageName.includes("%"))
return unresolvedCandidate("rejected", [
@@ -273,125 +235,6 @@ const bareModuleCandidate = (
]);
};
/**
* Nearest same-container package.json, stopping at a `node_modules` directory
* so a nested package cannot inherit an ancestor's map.
*/
const enclosingPackageMetadata = (
input: ResolveArtifactPathInput,
):
| Extract<PackageMetadata, { readonly kind: "available" | "unavailable" }>
| { readonly kind: "none" } => {
let directory = posix.dirname(input.sourcePath);
while (posix.basename(directory) !== "node_modules") {
const metadata = readPackageMetadata(input, directory);
if (metadata.kind !== "absent") return metadata;
if (directory === "." || directory === "") break;
directory = posix.dirname(directory);
}
return { kind: "none" };
};
/**
* Resolve an exact `#alias` against the importer's enclosing `imports` map.
* Selected relative targets are exact files; patterns, recursive aliases, and
* external-package targets stay unresolved without falling through.
*/
const resolvePackageImportAlias = (
input: ResolveArtifactPathInput,
specifier: string,
): CandidateResolution => {
const classified = classifyPackageImportSpecifier(specifier);
if (classified.kind === "invalid")
return unresolvedCandidate("rejected", [
`The package import specifier ${JSON.stringify(specifier)} is invalid; "#" and specifiers that end with "/" are not valid import aliases.`,
]);
if (classified.kind === "pattern")
return unresolvedCandidate("external", [
`Wildcard and directory package import aliases remain unresolved; ${JSON.stringify(specifier)} is not an exact imports key.`,
]);
const metadata = enclosingPackageMetadata(input);
if (metadata.kind === "none")
return unresolvedCandidate("external", [
`No enclosing package.json defines package import alias ${specifier}; package import maps do not cross package scopes.`,
]);
if (metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
const lookup = lookupExactPackageImport(metadata.imports, specifier);
if (lookup.kind === "undefined-map")
return unresolvedCandidate("external", [
`Package metadata ${metadata.path} does not define package import alias ${specifier}.`,
]);
if (lookup.kind === "missing-key")
return unresolvedCandidate("external", [
`Package metadata ${metadata.path} does not define package import alias ${specifier}; it declares ${lookup.declared.join(", ") || "no imports keys"}.`,
]);
if (lookup.kind === "pattern-unresolved")
return unresolvedCandidate("external", [
`Wildcard package import aliases remain unresolved; package metadata ${metadata.path} has no exact imports key ${specifier}.`,
]);
const selected = selectPackageImportTarget(
lookup.target,
input.moduleKind,
metadata.path,
);
if (selected.status === "rejected")
return unresolvedCandidate("rejected", [
`Package metadata ${metadata.path}: ${selected.limitation}`,
]);
if (selected.status === "unmatched")
return unresolvedCandidate("external", [
`Package metadata ${metadata.path} declares no imports target for alias ${specifier} under the active conditions; it declares ${selected.declared.join(", ") || "no conditions"}.`,
]);
if (selected.status === "blocked")
return unresolvedCandidate("external", [
`Package metadata ${metadata.path} blocks import alias ${specifier} with ${selected.cause === "null" ? "an explicit null imports target" : "an empty imports target array"}.`,
]);
if (selected.status === "non-relative") {
const target = classifyPackageImportTargetString(selected.value);
return unresolvedCandidate("external", [
target.kind === "recursive"
? `Recursive package import aliases remain unresolved; package metadata ${metadata.path} maps ${specifier} to ${JSON.stringify(target.target)}.`
: `External-package import targets remain unresolved; package metadata ${metadata.path} maps ${specifier} to ${JSON.stringify(target.target)}.`,
]);
}
return resolveExportTarget(input, metadata.path, selected, "imports");
};
const selectPackageImportTarget = (
value: unknown,
moduleKind: ResolveArtifactPathInput["moduleKind"],
packagePath: string,
):
| ({ readonly status: "value" } & PackageFileTarget)
| { readonly status: "non-relative"; readonly value: string }
| { readonly status: "blocked"; readonly cause: PackageTargetBlockCause }
| { readonly status: "unmatched"; readonly declared: readonly string[] }
| { readonly status: "rejected"; readonly limitation: string } => {
const selected = selectExportTarget(
value,
packageExportConditions(moduleKind),
packageMetadataUrl(packagePath),
"imports",
);
if (selected.kind === "invalid" || selected.kind === "invalid-config")
return { status: "rejected", limitation: selected.limitation };
if (selected.kind === "unmatched")
return {
status: "unmatched",
declared: selected.declared,
};
if (selected.kind === "blocked")
return { status: "blocked", cause: selected.cause };
if (selected.kind === "non-relative")
return { status: "non-relative", value: selected.value };
return {
status: "value",
value: selected.value,
url: selected.url,
};
};
/** Self references use the nearest package scope before dependency lookup. */
const commonJsSelfCandidate = (
input: ResolveArtifactPathInput,
@@ -399,22 +242,35 @@ const commonJsSelfCandidate = (
): CandidateResolution | null => {
if (input.moduleKind !== "require" || !input.files.has(input.sourcePath))
return null;
const metadata = enclosingPackageMetadata(input);
if (metadata.kind === "none") return null;
if (metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
if (
metadata.name === undefined ||
metadata.exports === undefined ||
(declared !== metadata.name && !declared.startsWith(`${metadata.name}/`))
)
return null;
const subpath =
declared === metadata.name
? "."
: `.${declared.slice(metadata.name.length)}`;
// Self resolution does not apply the CommonJS dependency-name matcher.
return resolvePackageExports(input, metadata.path, subpath, metadata.exports);
let directory = posix.dirname(input.sourcePath);
while (posix.basename(directory) !== "node_modules") {
const metadata = readPackageMetadata(input, directory);
if (metadata.kind === "unavailable")
return unresolvedCandidate("unavailable", [metadata.limitation]);
if (metadata.kind === "available") {
if (
metadata.name === undefined ||
metadata.exports === undefined ||
(declared !== metadata.name &&
!declared.startsWith(`${metadata.name}/`))
)
return null;
const subpath =
declared === metadata.name
? "."
: `.${declared.slice(metadata.name.length)}`;
// Self resolution does not apply the CommonJS dependency-name matcher.
return resolvePackageExports(
input,
metadata.path,
subpath,
metadata.exports,
);
}
if (directory === "." || directory === "") break;
directory = posix.dirname(directory);
}
return null;
};
/** Package roots and exact subpaths share metadata and exports selection. */
@@ -457,12 +313,7 @@ const resolvePackageExports = (
subpath: string,
rawExports: string | object,
): CandidateResolution => {
const exported = selectPackageExport(
rawExports,
subpath,
input.moduleKind,
packagePath,
);
const exported = selectPackageExport(rawExports, subpath, input.moduleKind);
if (exported.status === "rejected")
return unresolvedCandidate("rejected", [
`Package metadata ${packagePath}: ${exported.limitation}`,
@@ -473,47 +324,26 @@ const resolvePackageExports = (
]);
if (exported.status === "blocked")
return unresolvedCandidate("external", [
`Directory package metadata ${packagePath} blocks subpath ${subpath} with ${exported.cause === "null" ? "an explicit null exports target" : "an empty exports target array"}.`,
`Directory package metadata ${packagePath} blocks subpath ${subpath} with an explicit null exports target.`,
]);
return resolveExportTarget(input, packagePath, exported);
return resolveExportTarget(input, packagePath, exported.value);
};
/** An exports or import target is a URL naming one exact file for both Node loaders. */
/** An exports target is a URL naming one exact file for both Node loaders. */
const resolveExportTarget = (
input: ResolveArtifactPathInput,
packagePath: string,
target: PackageFileTarget,
mapKind: "exports" | "imports" = "exports",
target: string,
): CandidateResolution => {
const withTargetSource = (
outcome: CandidateResolution,
): CandidateResolution => ({
...outcome,
limitations: outcome.limitations.map(
(reason) =>
`Package metadata ${packagePath}, ${mapKind} target ${JSON.stringify(target.value)}: ${reason}`,
),
});
// Node's CommonJS finalizer checks the serialized URL, including suffixes;
// its ESM finalizer checks only the pathname. Neither error retries arrays.
if (input.moduleKind === "require" && /%(?:2f|5c)/iu.test(target.url.href))
return withTargetSource(
unresolvedCandidate("rejected", [
"The selected package URL contains encoded separator bytes.",
]),
);
const encodedPath = target.url.pathname.slice(
PACKAGE_ARTIFACT_ROOT_URL.pathname.length,
);
const rejected = rejectDeclaration(encodedPath);
if (rejected !== null) return withTargetSource(rejected);
const decoded = decodeModuleUrlPath(encodedPath);
if (typeof decoded !== "string") return withTargetSource(decoded);
const candidate = confineCandidate(decoded);
if (typeof candidate !== "string") return withTargetSource(candidate);
const rejected = rejectDeclaration(target);
if (rejected !== null) return rejected;
const decoded = decodeModuleUrlPath(stripQueryAndFragment(target));
if (typeof decoded !== "string") return decoded;
const candidate = confineCandidate(relativeCandidate(packagePath, decoded));
if (typeof candidate !== "string") return candidate;
return (
resolveFileCandidates(input, [candidate]) ??
withTargetSource(exactPackageTargetNotFound(candidate, mapKind))
exactExportsTargetNotFound(candidate)
);
};
@@ -662,7 +492,6 @@ type PackageMetadata =
readonly path: string;
readonly name: string | undefined;
readonly exports: string | object | undefined;
readonly imports: object | undefined;
readonly main: unknown;
readonly module: unknown;
}
@@ -703,7 +532,6 @@ const readPackageMetadata = (
};
const name = Reflect.get(value, "name");
const exports = Reflect.get(value, "exports");
const imports = Reflect.get(value, "imports");
return {
kind: "available",
path,
@@ -715,11 +543,6 @@ const readPackageMetadata = (
(typeof exports === "object" && exports !== null)
? exports
: undefined,
// Import maps must be non-array objects; other shapes are not a map.
imports:
typeof imports === "object" && imports !== null && !Array.isArray(imports)
? imports
: undefined,
main: Reflect.get(value, "main"),
module: Reflect.get(value, "module"),
};
@@ -730,12 +553,8 @@ const resolveCandidate = (
input: ResolveArtifactPathInput,
candidate: string,
packageChain: ReadonlySet<string>,
emittedSourceCandidates: readonly string[] = [],
): CandidateResolution =>
resolveFileCandidates(input, [
...fileCandidates(candidate),
...emittedSourceCandidates,
]) ??
resolveFileCandidates(input, fileCandidates(candidate)) ??
resolveDirectory(
input,
candidate,
@@ -825,12 +644,10 @@ const fileCandidates = (candidate: string): readonly string[] => [
const indexCandidates = (candidate: string): readonly string[] =>
EXTENSIONS.map((extension) => posix.join(candidate, `index${extension}`));
type PackageTargetBlockCause = "null" | "empty-array";
type PackageExportOutcome =
| ({ readonly status: "value" } & PackageFileTarget)
| { readonly status: "value"; readonly value: string }
| { readonly status: "rejected"; readonly limitation: string }
| { readonly status: "blocked"; readonly cause: PackageTargetBlockCause }
| { readonly status: "blocked" }
| { readonly status: "unmatched"; readonly declared: readonly string[] };
/** Only exact non-directory keys participate; wildcard expansion is unsupported. */
@@ -838,7 +655,6 @@ const selectPackageExport = (
value: unknown,
subpath: string,
moduleKind: ResolveArtifactPathInput["moduleKind"],
packagePath: string,
): PackageExportOutcome => {
let target = value;
let declared: string[] = [];
@@ -865,44 +681,35 @@ const selectPackageExport = (
const selected = selectExportTarget(
target,
packageExportConditions(moduleKind),
packageMetadataUrl(packagePath),
);
if (selected.kind === "invalid" || selected.kind === "invalid-config")
return { status: "rejected", limitation: selected.limitation };
if (selected.kind === "unmatched")
return {
status: "unmatched",
declared: selected.declared,
declared:
typeof target === "object" && target !== null
? Object.keys(target)
: declared,
};
if (selected.kind === "blocked")
return { status: "blocked", cause: selected.cause };
if (selected.kind === "non-relative")
return {
status: "rejected",
limitation: `The package exports target ${JSON.stringify(selected.value)} must start with "./".`,
};
return {
status: "value",
value: selected.value,
url: selected.url,
};
return selected.kind === "blocked"
? { status: "blocked" }
: { status: "value", value: selected.value };
};
type ExportTargetOutcome =
| ({ readonly kind: "value" } & PackageFileTarget)
| { readonly kind: "non-relative"; readonly value: string }
| { readonly kind: "blocked"; readonly cause: PackageTargetBlockCause }
| { readonly kind: "unmatched"; readonly declared: readonly string[] }
| { readonly kind: "value"; readonly value: string }
| { readonly kind: "blocked" }
| { readonly kind: "unmatched" }
| { readonly kind: "invalid"; readonly limitation: string }
| { readonly kind: "invalid-config"; readonly limitation: string };
const invalidExportTarget = (
value: unknown,
reason: string,
mapKind: "exports" | "imports" = "exports",
): ExportTargetOutcome => ({
kind: "invalid",
limitation: `The package ${mapKind === "imports" ? "import" : "exports"} target ${JSON.stringify(value) ?? String(value)} ${reason}.`,
limitation: `The package exports target ${JSON.stringify(value) ?? String(value)} ${reason}.`,
});
// Node checks raw target segments before URL normalization. Compare encoded
@@ -922,97 +729,50 @@ const forbiddenExportSegment = (value: string): string | undefined =>
);
});
const selectStringPackageTarget = (
value: string,
packageUrl: URL,
mapKind: "exports" | "imports",
): ExportTargetOutcome => {
if (mapKind === "imports") {
const target = classifyPackageImportTargetString(value);
if (target.kind === "invalid")
return invalidExportTarget(
value,
"cannot be parent-relative, absolute, or a URL",
mapKind,
);
if (target.kind !== "relative") return { kind: "non-relative", value };
} else if (!value.startsWith("./"))
return invalidExportTarget(value, 'must start with "./"', mapKind);
const forbidden = forbiddenExportSegment(value);
if (forbidden !== undefined)
return invalidExportTarget(
value,
`contains forbidden path segment ${JSON.stringify(forbidden)} before URL normalization`,
mapKind,
);
// Node constructs the URL before deciding whether this array entry is an
// invalid package target. Tabs/newlines and dot segments normalize here;
// percent decoding and existence checks belong after target selection.
const url = new URL(value, packageUrl);
if (!url.pathname.startsWith(new URL(".", packageUrl).pathname))
return invalidExportTarget(
value,
"escapes the package root after URL normalization",
mapKind,
);
return {
kind: "value",
value,
url,
};
};
const selectArrayPackageTarget = (
value: readonly unknown[],
conditions: ReadonlySet<string>,
packageUrl: URL,
mapKind: "exports" | "imports",
): ExportTargetOutcome => {
if (value.length === 0) return { kind: "blocked", cause: "empty-array" };
let lastRefusal:
| Extract<ExportTargetOutcome, { readonly kind: "invalid" | "blocked" }>
| undefined;
const declared = new Set<string>();
for (const entry of value) {
const nested = selectExportTarget(entry, conditions, packageUrl, mapKind);
// Node's array fallback catches invalid targets, not invalid configuration.
if (nested.kind === "invalid-config") return nested;
if (nested.kind === "unmatched") {
for (const condition of nested.declared) declared.add(condition);
continue;
}
if (nested.kind === "invalid" || nested.kind === "blocked") {
lastRefusal = nested;
continue;
}
return nested;
}
return lastRefusal ?? { kind: "unmatched", declared: [...declared] };
};
/**
* Resolve a target in Node's declared order. Explicit null blocks an active
* condition, whereas an unmatched nested object permits the next condition.
* An array skips unmatched and invalid entries, stops at its first string, and
* preserves the last invalid/null outcome if no string is selected. A nonempty
* array of unmatched conditions stays unmatched; an empty array blocks.
* preserves the final invalid-versus-null refusal if no string is selected.
*/
const selectExportTarget = (
value: unknown,
conditions: ReadonlySet<string>,
packageUrl: URL,
mapKind: "exports" | "imports" = "exports",
): ExportTargetOutcome => {
if (typeof value === "string")
return selectStringPackageTarget(value, packageUrl, mapKind);
if (value === null) return { kind: "blocked", cause: "null" };
if (Array.isArray(value))
return selectArrayPackageTarget(value, conditions, packageUrl, mapKind);
if (typeof value === "string") {
if (!value.startsWith("./"))
return invalidExportTarget(value, 'must start with "./"');
const forbidden = forbiddenExportSegment(value);
return forbidden === undefined
? { kind: "value", value }
: invalidExportTarget(
value,
`contains forbidden path segment ${JSON.stringify(forbidden)} before URL normalization`,
);
}
if (value === null) return { kind: "blocked" };
if (Array.isArray(value)) {
let invalid: string | null = null;
for (const entry of value) {
const nested = selectExportTarget(entry, conditions);
// Node's array fallback catches invalid targets, not invalid configuration.
if (nested.kind === "invalid-config") return nested;
if (nested.kind === "invalid") {
invalid = nested.limitation;
continue;
}
if (nested.kind === "unmatched") continue;
invalid = null;
if (nested.kind === "value") return nested;
}
return invalid === null
? { kind: "blocked" }
: { kind: "invalid", limitation: invalid };
}
if (typeof value !== "object")
return invalidExportTarget(
value,
"must be a relative string, object, array, or null",
mapKind,
);
for (const key of Object.keys(value)) {
const numeric = Number(key);
@@ -1022,15 +782,12 @@ const selectExportTarget = (
limitation: `The package exports configuration contains numeric condition key ${JSON.stringify(key)}.`,
};
}
const declared = new Set<string>();
for (const [condition, target] of Object.entries(value)) {
declared.add(condition);
if (!conditions.has(condition)) continue;
const nested = selectExportTarget(target, conditions, packageUrl, mapKind);
const nested = selectExportTarget(target, conditions);
if (nested.kind !== "unmatched") return nested;
for (const name of nested.declared) declared.add(name);
}
return { kind: "unmatched", declared: [...declared] };
return { kind: "unmatched" };
};
/**
@@ -1059,14 +816,11 @@ const notFoundCandidate = (): CandidateResolution => ({
],
});
const exactPackageTargetNotFound = (
path: string,
mapKind: "exports" | "imports",
): CandidateResolution => ({
const exactExportsTargetNotFound = (path: string): CandidateResolution => ({
resolvedPath: null,
status: "not-found",
limitations: [
`The selected package ${mapKind === "imports" ? "import" : "exports"} target ${path} does not exist in the inventoried artifact container; ${mapKind === "imports" ? "import" : "exports"} targets are exact files, so no extension or directory index was tried.`,
`The selected package exports target ${path} does not exist in the inventoried artifact container; exports targets are exact files, so no extension or directory index was tried.`,
],
});
@@ -1,6 +1,5 @@
import { constants } from "node:fs";
import { access, lstat, realpath } from "node:fs/promises";
import { setImmediate } from "node:timers/promises";
import { ArtifactReaderFailure } from "../../artifacts/ArtifactReader.js";
import {
@@ -14,7 +13,6 @@ import type { JavaScriptSemanticGraph } from "../../domain/javascript/javascript
import type { ElectronBoundarySummary } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { parseOwnedJavaScriptApplicationAnalysisSteps } from "../../domain/javascript/javascriptApplicationAnalysis.js";
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { ArtifactOperationError } from "../../domain/artifactOperationError.js";
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
import type { ArtifactInventoryPartialObservation } from "../../domain/artifactPartialObservation.js";
import type { ArtifactResourceScope } from "../../artifacts/ArtifactResourceScope.js";
@@ -70,17 +68,19 @@ export const reconstructJavaScriptArtifact = async (
progress?: ProgressReporter,
): Promise<JavaScriptArtifactReconstructionResult> =>
scope.run(async () => {
let effectiveSignal = signal;
let effectiveProgress = progress;
const reportPhase = async (
phase: string,
message: string,
): Promise<void> => {
await progress?.report({
await effectiveProgress?.report({
phase,
completed: 0,
total: 1,
message,
});
await checkpointJavaScriptAnalysis(signal);
await checkpointJavaScriptAnalysis(effectiveSignal);
};
const input = javascriptArtifactReconstructionInputSchema.parse(rawInput);
abortIfNeeded(signal);
@@ -182,24 +182,14 @@ export const reconstructJavaScriptArtifact = async (
}
}
const analysis = outcome.analysis;
let interruption = cleanupFailure ?? outcome.interruption;
// Source facts are owned now. Finish their projection cooperatively so a
// later cancellation can retain Evidence instead of discarding those facts.
const checkpointCompletedAnalysis = async (): Promise<void> => {
await setImmediate();
if (interruption == null && signal?.aborted === true)
interruption = new ArtifactOperationError(OPERATION, "cancelled");
};
const reportCompletedPhase = async (
phase: string,
message: string,
): Promise<void> => {
await checkpointCompletedAnalysis();
if (interruption == null)
await progress?.report({ phase, completed: 0, total: 1, message });
await checkpointCompletedAnalysis();
};
await reportCompletedPhase(
const interruption = cleanupFailure ?? outcome.interruption;
if (interruption !== null && interruption !== undefined) {
effectiveSignal = undefined;
effectiveProgress = undefined;
}
await checkpointJavaScriptAnalysis(effectiveSignal);
abortIfNeeded(effectiveSignal);
await reportPhase(
"build_javascript_application_graph",
"Constructing application and Electron boundary relationships",
);
@@ -208,14 +198,15 @@ export const reconstructJavaScriptArtifact = async (
files,
analysis,
);
await reportCompletedPhase(
await reportPhase(
"seal_javascript_application_graph",
"Sealing the validated application graph",
);
const graph = await completeJavaScriptAnalysisSteps(
applicationGraphSteps,
effectiveSignal,
);
await reportCompletedPhase(
await reportPhase(
"build_javascript_semantic_graph",
"Binding and validating static semantic relationships",
);
@@ -223,13 +214,15 @@ export const reconstructJavaScriptArtifact = async (
snapshot.manifest.root_sha256,
graph,
);
await reportCompletedPhase(
await reportPhase(
"seal_javascript_semantic_graph",
"Validating and sealing the semantic graph",
);
const semanticGraph =
await completeJavaScriptAnalysisSteps(semanticGraphSteps);
await checkpointCompletedAnalysis();
const semanticGraph = await completeJavaScriptAnalysisSteps(
semanticGraphSteps,
effectiveSignal,
);
await checkpointJavaScriptAnalysis(effectiveSignal);
const completed = {
input_path: path,
format,
-25
View File
@@ -1,25 +0,0 @@
import { describe, expect, it } from "vitest";
import { orderAsarListing } from "./AsarArtifactReader.js";
describe("orderAsarListing", () => {
it("orders host-spelled listings by their portable archive path", () => {
const posix = orderAsarListing(["/a0", "/a", "/a/b"], "/");
const windows = orderAsarListing(["\\a0", "\\a", "\\a\\b"], "\\");
// "/" (0x2F) precedes "0" (0x30); "\" (0x5C) would follow it.
expect(posix.map(({ path }) => path)).toEqual(["a", "a/b", "a0"]);
expect(windows.map(({ path }) => path)).toEqual(["a", "a/b", "a0"]);
expect(windows.map(({ listed }) => listed)).toEqual([
"\\a",
"\\a\\b",
"\\a0",
]);
});
it("keeps POSIX backslashes as literal filename characters", () => {
expect(orderAsarListing(["/a\\b"], "/")).toEqual([
{ listed: "/a\\b", path: "a\\b" },
]);
});
});
+12 -28
View File
@@ -32,7 +32,6 @@ import { closeAsarHandle, readValidatedAsarEntry } from "./AsarEntryStream.js";
import { admitAsarHeader } from "./AsarHeader.js";
import { OwnedFileHandle } from "../filesystem/OwnedFileHandle.js";
import { readFileHandleChunks } from "../filesystem/readFileHandleChunks.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/**
* Official Electron ASAR adapter with range-streamed member reads.
@@ -65,7 +64,7 @@ export class AsarArtifactReader implements ArtifactReader {
) {}
async *entries(signal?: AbortSignal): AsyncIterable<ArtifactEntry> {
let paths: readonly AsarListedPath[];
let paths: string[];
try {
await this.prepareContainer(undefined, signal);
const snapshotPath = this.#snapshotPath();
@@ -83,7 +82,9 @@ export class AsarArtifactReader implements ArtifactReader {
await headerHandle.close();
}
abortIfNeeded(signal);
paths = orderAsarListing(listPackage(snapshotPath, { isPack: false }));
paths = listPackage(snapshotPath, { isPack: false }).sort((left, right) =>
left.localeCompare(right, "en"),
);
const archiveMetadata = await lstat(snapshotPath);
if (!archiveMetadata.isFile() || archiveMetadata.isSymbolicLink())
throw new ArtifactReaderFailure(
@@ -94,8 +95,13 @@ export class AsarArtifactReader implements ArtifactReader {
} catch (cause: unknown) {
throw asarFailure(this.path, "inventory", cause);
}
for (const { listed, path } of paths) {
for (const listed of paths) {
abortIfNeeded(signal);
// @electron/asar returns paths assembled with the host path module.
// Keep that spelling for its API calls, but expose archive paths with
// portable separators. On POSIX, backslashes can be literal filename
// characters and must remain untouched.
const path = toArtifactPath(listed);
if (path.length === 0) continue;
const providerPath = listed.startsWith(sep)
? listed.slice(sep.length)
@@ -571,30 +577,8 @@ const waitForOperation = async (
});
};
interface AsarListedPath {
readonly listed: string;
readonly path: string;
}
/**
* Pair each host-spelled listing with its portable archive path, ordered by
* the portable path so Windows and POSIX hosts agree.
*
* @electron/asar returns paths assembled with the host path module. Keep that
* spelling for its API calls, but expose archive paths with portable
* separators. On POSIX, backslashes can be literal filename characters and
* must remain untouched.
*/
export const orderAsarListing = (
listed: readonly string[],
separator: string = sep,
): AsarListedPath[] =>
listed
.map((entry) => ({ listed: entry, path: toArtifactPath(entry, separator) }))
.sort((left, right) => compareUnicodeCodePoints(left.path, right.path));
const toArtifactPath = (listed: string, separator: string): string => {
const portable = separator === "\\" ? listed.replaceAll("\\", "/") : listed;
const toArtifactPath = (listed: string): string => {
const portable = sep === "\\" ? listed.replaceAll("\\", "/") : listed;
return portable.replace(/^\/+|\/+$/gu, "");
};
@@ -526,16 +526,12 @@ const CPU_TYPE_X86 = 7;
const CPU_TYPE_ARM = 12;
const CPU_SUBTYPE_MASK = 0xff000000;
const CPU_SUBTYPE_ARM64E = 2;
const CPU_SUBTYPE_ARM64E_X1 = 12;
const CPU_SUBTYPE_X86_64_H = 8;
const architectureName = (cpuType: number, cpuSubtype: number): string => {
const subtype = cpuSubtype & ~CPU_SUBTYPE_MASK;
if (cpuType === (CPU_TYPE_ARM | CPU_ARCH_ABI64))
// Both pointer-authentication subtypes belong to the arm64e family.
return subtype === CPU_SUBTYPE_ARM64E || subtype === CPU_SUBTYPE_ARM64E_X1
? "arm64e"
: "arm64";
return subtype === CPU_SUBTYPE_ARM64E ? "arm64e" : "arm64";
if (cpuType === (CPU_TYPE_ARM | CPU_ARCH_ABI64_32)) return "arm64_32";
if (cpuType === (CPU_TYPE_X86 | CPU_ARCH_ABI64))
return subtype === CPU_SUBTYPE_X86_64_H ? "x86_64h" : "x86_64";
@@ -13,16 +13,6 @@ describe("hostExecutableMarking", () => {
expect(
hostExecutableMarking("C:\\scratch\\SETUP.CMD", file(0o444), "win32"),
).toBe(true);
for (const path of [
"tools/SETUP.EXE",
"run.Cmd",
"build.bat",
"legacy.com",
])
expect(hostExecutableMarking(path, file(0o666), "win32")).toBe(true);
expect(hostExecutableMarking("readonly.exe", file(0o444), "win32")).toBe(
true,
);
expect(
hostExecutableMarking("C:\\scratch\\library.dll", file(0o666), "win32"),
).toBe(false);
@@ -35,11 +25,6 @@ describe("hostExecutableMarking", () => {
).toBe(false);
});
it("does not treat other Windows filenames as executable even with execute bits", () => {
for (const path of ["notes.txt", "README", "archive.exe.bak"])
expect(hostExecutableMarking(path, file(0o777), "win32")).toBe(false);
});
it("keeps POSIX permission bits authoritative regardless of extension", () => {
expect(hostExecutableMarking("/scratch/tool", file(0o755), "linux")).toBe(
true,
+1 -4
View File
@@ -68,7 +68,6 @@ import { captureCdpScreenshot } from "./CdpScreenshot.js";
import { comparePngScreenshots } from "./PngVisualDiff.js";
import { CDP_BROWSER_PROVIDER_IDENTITY } from "./providerIdentities.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
const CLEANUP_DOMAINS = ["Network", "Debugger", "Runtime", "Page"] as const;
/** Passive browser observation through a selected user-owned CDP endpoint. */
@@ -409,9 +408,7 @@ const selectTargets = (
attached: target.attached,
});
}
allowed.sort((left, right) =>
compareUnicodeCodePoints(left.target_id, right.target_id),
);
allowed.sort((left, right) => left.target_id.localeCompare(right.target_id));
return {
allowed,
excluded: {
+1 -2
View File
@@ -12,7 +12,6 @@ import {
import { exclusionReasonForUrl } from "./CdpCaptureEventHelpers.js";
import type { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { createDocumentSvgAncestry, domSvgLink } from "./CdpCaptureDomSvg.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
export type CapturedResource = Omit<
WebPageInspection["resources"][number],
@@ -347,7 +346,7 @@ const accessibilityStates = (
if (state !== undefined) states.set(name, state);
}
return [...states]
.sort(([left], [right]) => compareUnicodeCodePoints(left, right))
.sort(([left], [right]) => left.localeCompare(right))
.map(([name, state]) => ({ name, value: state }));
};
+1 -2
View File
@@ -24,7 +24,6 @@ import {
import { captureElectronScripts } from "./CdpElectronScripts.js";
import { captureElectronWorkers } from "./CdpElectronWorkers.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
interface ElectronContext {
readonly connection: CdpConnection;
@@ -301,7 +300,7 @@ const captureResources = async (
pending.push(...recordsValue(tree.childFrames));
}
return resources.sort((left, right) =>
compareUnicodeCodePoints(left.resource_key, right.resource_key),
left.resource_key.localeCompare(right.resource_key),
);
};
+1 -2
View File
@@ -30,7 +30,6 @@ import { inspectCdpElectronPage } from "./CdpElectronInspection.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { CDP_ELECTRON_PROVIDER_IDENTITY } from "./providerIdentities.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/** Passive Electron provider for local file pages exposed by loopback CDP. */
export class CdpElectronProvider implements ElectronObservationPort {
identity(): ProviderIdentity {
@@ -74,7 +73,7 @@ export class CdpElectronProvider implements ElectronObservationPort {
});
}
allowed.sort((left, right) =>
compareUnicodeCodePoints(left.target_id, right.target_id),
left.target_id.localeCompare(right.target_id),
);
return ok(
electronTargetListSchema.parse({
+1 -2
View File
@@ -9,7 +9,6 @@ import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
import { requiredScriptSource } from "./CdpCaptureValues.js";
import type { ElectronScriptDraft } from "./CdpElectronScriptEvents.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
interface ElectronScriptCaptureInput {
readonly connection: CdpConnection;
@@ -66,7 +65,7 @@ export const captureElectronScripts = async (
return {
total,
items: items.sort((left, right) =>
compareUnicodeCodePoints(left.script_key, right.script_key),
left.script_key.localeCompare(right.script_key),
),
};
};
+1 -2
View File
@@ -12,7 +12,6 @@ import {
import type { CdpEndpointTarget } from "./CdpEndpoint.js";
import { optionalCdpCommand } from "./CdpOptionalCommand.js";
import { authorizedElectronFile } from "./ElectronFileScope.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
/** Inventory local worker targets without attaching to them. */
export const captureElectronWorkers = async (input: {
@@ -71,7 +70,7 @@ export const captureElectronWorkers = async (input: {
});
}
return workers.sort((left, right) =>
compareUnicodeCodePoints(left.target_id, right.target_id),
left.target_id.localeCompare(right.target_id),
);
};
+3 -8
View File
@@ -13,7 +13,6 @@ import {
} from "./CdpCaptureValues.js";
import type { CaptureContext } from "./CdpPageCapture.js";
import type { WebSourceMapRequest } from "./WebSourceMapFetcher.js";
import { compareUnicodeCodePoints } from "../domain/unicodeCodePointOrder.js";
export interface CapturedScripts {
readonly inventory: WebPageInspection["scripts"];
@@ -132,15 +131,11 @@ const keyAndBuildInventory = (
}));
keyed.sort(
(left, right) =>
compareUnicodeCodePoints(left.base, right.base) ||
compareUnicodeCodePoints(
sourceDigest(left.item.source),
left.base.localeCompare(right.base) ||
sourceDigest(left.item.source).localeCompare(
sourceDigest(right.item.source),
) ||
compareUnicodeCodePoints(
left.item.frame_id ?? "",
right.item.frame_id ?? "",
),
(left.item.frame_id ?? "").localeCompare(right.item.frame_id ?? ""),
);
const totals = new Map<string, number>();
for (const { base } of keyed) totals.set(base, (totals.get(base) ?? 0) + 1);
+2 -5
View File
@@ -7,10 +7,7 @@ import type {
} from "../domain/webMcpDiscovery.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { digestCanonicalValue } from "../domain/canonicalDigest.js";
import {
compareUnicodeCodePoints,
compositeKey,
} from "../domain/unicodeCodePointOrder.js";
import { compositeKey } from "../domain/unicodeCodePointOrder.js";
import type { CdpEndpointDiscovery, CdpEndpointTarget } from "./CdpEndpoint.js";
import type { CdpConnection, CdpEvent } from "./CdpConnection.js";
import { CdpCaptureCompleteness } from "./CdpCaptureCompleteness.js";
@@ -524,7 +521,7 @@ const buildWebMcpResult = (options: WebMcpResultOptions): WebMcpDiscovery => {
tools: {
total: tools.size + completeness.droppedTotal,
items: [...tools.values()].sort((left, right) =>
compareUnicodeCodePoints(left.tool_key, right.tool_key),
left.tool_key.localeCompare(right.tool_key),
),
},
completeness: completeness.snapshot(),
+2 -3
View File
@@ -6,10 +6,9 @@ import { z } from "zod";
import { PROMPT_CONTRACTS } from "./contracts/promptContracts.js";
import { TOOL_CONTRACTS } from "./contracts/toolContracts.js";
import { CLI_COMMAND_NAMES } from "./cliCommandNames.js";
import { compareUnicodeCodePoints } from "./domain/unicodeCodePointOrder.js";
const sortedToolContracts = [...TOOL_CONTRACTS].sort((left, right) =>
compareUnicodeCodePoints(left.name, right.name),
left.name.localeCompare(right.name),
);
const createToolCatalog = () =>
@@ -40,7 +39,7 @@ const promptCatalog = PROMPT_CONTRACTS.map((contract) => ({
description: contract.description,
arguments: contract.arguments,
steps: contract.steps,
})).sort((left, right) => compareUnicodeCodePoints(left.name, right.name));
})).sort((left, right) => left.name.localeCompare(right.name));
const digest = (value: unknown): string => {
const encoded = canonicalize(value);
-3
View File
@@ -263,9 +263,6 @@ const setupRunOptions = (
? {
readinessScope: {
clients: options.client,
...(options.skillClient.length > 0
? { skillClients: options.skillClient }
: {}),
providers: options.installHopper ? ["hopper"] : [],
skill: options.skill ?? agentIntegrationSelected,
},
-159
View File
@@ -1,159 +0,0 @@
import { describe, expect, it } from "vitest";
import { TOOL_CONTRACTS } from "./toolContracts.js";
import { TOOL_EFFECTS, annotationsFromEffects } from "./toolEffects.js";
/**
* Descriptions that name running, launching, executing, starting, installing,
* or uninstalling something. A matching tool must declare foreign execution or
* device mutation unless it is listed below with the reason the wording does
* not describe that effect.
*/
const EXECUTION_WORDING =
/\b(?:run|runs|running|launch|launches|launching|execute|executes|executing|execution|start|starts|install|installs|uninstall|uninstalls)\b/iu;
const EXECUTION_WORDING_EXCEPTIONS: Readonly<Record<string, string>> = {
inspect_native_load_image: "states that it does not execute the target",
resolve_native_call_targets: "states that it does not execute code",
inspect_pe_resources: "states that it does not execute the image",
inspect_binary_layout:
"parses the binary offline with operator-configured pwntools",
inspect_recorded_crash: "inspects a core file without launching its target",
inspect_macho: "runs host Mach-O inspection tools, not the target",
inspect_evm_interface:
"decodes bytecode in REA's bundled worker without executing it",
inspect_wasm_artifact:
"WABT validates and decompiles the module without executing it",
inspect_managed_artifact: "reads metadata without executing target code",
inspect_managed_members: "reads metadata without executing target code",
inspect_managed_native_boundaries:
"reads metadata without executing target code",
verify_managed_native_boundaries: "states it does not execute managed code",
project_managed_application_graph: "states it does not execute managed code",
inspect_android_package: "states it does not execute the APK",
search_android_classes: "states it does not execute the APK",
inspect_apktool_client:
"inspects the operator-selected apktool launcher; never installs it",
inspect_jeb_client: "talks to a caller-started JEB; never launches it",
open_jeb_project: "the engine analyzes the file; REA does not execute it",
inspect_adb_client:
"inspects the operator-selected adb client; never starts its server",
list_adb_devices: "may start the local adb server, which REA does not own",
inspect_firmware_regions: "states there is no target execution",
extract_firmware: "states that extracted code is never executed",
analyze_javascript_application: "reconstructs the graph without executing",
reconcile_javascript_runtime: "never invents execution claims",
observe_javascript_runtime: "never evaluates or resumes the target",
trace_application_feature: "never executes the application",
trace_javascript_semantics: "static reachability never claims execution",
compare_javascript_export_shapes: "states it does not execute JavaScript",
project_android_application_graph: "execution-free inventory",
project_apple_application_graph: "execution-free inventory",
project_harmony_application_graph: "execution-free inventory",
trace_web_source_location: "states there is no application execution",
trace_web_module_imports:
"runs only REA's resolver stub in operator-configured Chromium",
export_web_scripts: "states there is no refetch or execution",
observe_web_execution:
"instruments an externally driven page; it does not run code itself",
recover_javascript_sources: "states there is no application code execution",
goto_address: "start names an object boundary, not a launch",
prev_address: "start names an object boundary, not a launch",
trace_call_path: "start names the traversal origin",
observe_native_ui: "observes an already-running app without launching it",
inspect_adb_package: "reports install times without changing the device",
list_adb_processes: "lists running processes without changing the device",
inspect_web_event_listeners: "states it does not evaluate JavaScript",
};
const declaresForeignAuthority = (name: string): boolean => {
const effects = TOOL_EFFECTS[name];
return (
effects !== undefined &&
(effects.executesForeignCode || effects.mutatesDevice)
);
};
describe("tool effects", () => {
it("audits every public tool exactly once", () => {
expect(Object.keys(TOOL_EFFECTS).sort()).toEqual(
TOOL_CONTRACTS.map(({ name }) => name).sort(),
);
});
it("keeps execution and device wording aligned with declared effects", () => {
const undeclared = TOOL_CONTRACTS.filter(
({ name, description }) =>
EXECUTION_WORDING.test(description) &&
!declaresForeignAuthority(name) &&
EXECUTION_WORDING_EXCEPTIONS[name] === undefined,
).map(({ name }) => name);
expect(undeclared).toEqual([]);
const staleExceptions = Object.keys(EXECUTION_WORDING_EXCEPTIONS).filter(
(name) => {
const contract = TOOL_CONTRACTS.find(
(candidate) => candidate.name === name,
);
return (
contract === undefined ||
!EXECUTION_WORDING.test(contract.description) ||
declaresForeignAuthority(name)
);
},
);
expect(staleExceptions).toEqual([]);
});
it("marks foreign execution and device mutation as destructive", () => {
const destructive = TOOL_CONTRACTS.filter(
({ effects }) => effects.executesForeignCode || effects.mutatesDevice,
);
expect(destructive.map(({ name }) => name).sort()).toEqual([
"capture_browser_scenario",
"capture_electron_scenario",
"capture_native_ui_scenario",
"capture_process_scenario",
"install_adb_package",
"observe_native_calls",
"push_adb_file",
"start_adb_activity",
"start_adb_app",
"stop_adb_app",
"uninstall_adb_package",
]);
for (const contract of destructive)
expect(contract.annotations, contract.name).toMatchObject({
readOnlyHint: false,
destructiveHint: true,
});
});
it("derives destructiveHint from each destructive effect alone", () => {
const base = TOOL_EFFECTS["binary_session"];
if (base === undefined) throw new Error("binary_session is unaudited");
expect(annotationsFromEffects(base)).toEqual({
readOnlyHint: true,
destructiveHint: false,
idempotentHint: true,
openWorldHint: false,
});
for (const flag of [
"mayDiscardData",
"executesForeignCode",
"mutatesDevice",
] as const)
expect(annotationsFromEffects({ ...base, [flag]: true }), flag).toEqual({
readOnlyHint: false,
destructiveHint: true,
idempotentHint: true,
openWorldHint: false,
});
});
it("publishes the advertised annotations derived from canonical effects", () => {
for (const contract of TOOL_CONTRACTS)
expect(contract.annotations, contract.name).toEqual(
annotationsFromEffects(contract.effects),
);
});
});
+7 -57
View File
@@ -7,17 +7,7 @@ export interface ToolEffects {
readonly launchesProcess: boolean;
readonly accessesNetwork: boolean;
readonly changesUiState: boolean;
/** Can overwrite, clear, or drop state that the caller cannot recover from REA. */
readonly mayDiscardData: boolean;
/**
* Runs or drives code REA does not own on the host: the analysis target, a
* caller-selected executable, or the application loaded into one. Running
* REA's own pinned helpers or operator-configured analysis engines does not
* count.
*/
readonly executesForeignCode: boolean;
/** Changes an attached Android device: packages, files, or running apps. */
readonly mutatesDevice: boolean;
readonly idempotent: boolean;
}
@@ -37,8 +27,6 @@ const effects = (overrides: Partial<ToolEffects> = {}): ToolEffects => ({
accessesNetwork: false,
changesUiState: false,
mayDiscardData: false,
executesForeignCode: false,
mutatesDevice: false,
idempotent: true,
...overrides,
});
@@ -190,7 +178,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
launchesProcess: true,
writesFilesystem: true,
accessesNetwork: true,
mutatesDevice: true,
idempotent: false,
}),
capture_adb_screen: effects({
@@ -252,7 +239,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
mutatesSession: true,
launchesProcess: true,
accessesNetwork: true,
mutatesDevice: true,
idempotent: false,
}),
uninstall_adb_package: effects({
@@ -260,7 +246,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
mutatesSession: true,
launchesProcess: true,
accessesNetwork: true,
mutatesDevice: true,
idempotent: false,
}),
start_adb_app: effects({
@@ -268,7 +253,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
mutatesSession: true,
launchesProcess: true,
accessesNetwork: true,
mutatesDevice: true,
idempotent: false,
}),
start_adb_activity: effects({
@@ -276,7 +260,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
mutatesSession: true,
launchesProcess: true,
accessesNetwork: true,
mutatesDevice: true,
idempotent: false,
}),
stop_adb_app: effects({
@@ -284,7 +267,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
mutatesSession: true,
launchesProcess: true,
accessesNetwork: true,
mutatesDevice: true,
idempotent: false,
}),
search_android_classes: effects({
@@ -312,7 +294,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
mutatesSession: true,
launchesProcess: true,
writesFilesystem: true,
mayDiscardData: true,
}),
inspect_native_load_image: evidence,
inspect_native_dispatch_metadata: evidence,
@@ -349,31 +330,11 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
resolve_containing_procedure: evidence,
search_procedures: evidence,
search_strings: evidence,
set_address_name: effects({
mutatesTarget: true,
mutatesSession: true,
mayDiscardData: true,
}),
set_addresses_names: effects({
mutatesTarget: true,
mutatesSession: true,
mayDiscardData: true,
}),
set_bookmark: effects({
mutatesTarget: true,
mutatesSession: true,
mayDiscardData: true,
}),
set_comment: effects({
mutatesTarget: true,
mutatesSession: true,
mayDiscardData: true,
}),
set_inline_comment: effects({
mutatesTarget: true,
mutatesSession: true,
mayDiscardData: true,
}),
set_address_name: effects({ mutatesTarget: true, mutatesSession: true }),
set_addresses_names: effects({ mutatesTarget: true, mutatesSession: true }),
set_bookmark: effects({ mutatesTarget: true, mutatesSession: true }),
set_comment: effects({ mutatesTarget: true, mutatesSession: true }),
set_inline_comment: effects({ mutatesTarget: true, mutatesSession: true }),
unset_bookmark: effects({
mutatesTarget: true,
mutatesSession: true,
@@ -404,7 +365,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
accessesNetwork: true,
changesUiState: true,
mayDiscardData: true,
executesForeignCode: true,
idempotent: false,
}),
// The owned process can change files, show UI and use the network.
@@ -414,7 +374,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
launchesProcess: true,
accessesNetwork: true,
changesUiState: true,
executesForeignCode: true,
idempotent: false,
}),
inspect_macho: nativeEvidence,
@@ -460,7 +419,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
accessesNetwork: true,
changesUiState: true,
mayDiscardData: true,
executesForeignCode: true,
idempotent: false,
}),
list_electron_targets: browserEvidence,
@@ -473,7 +431,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
accessesNetwork: true,
changesUiState: true,
mayDiscardData: true,
executesForeignCode: true,
idempotent: false,
}),
list_javascript_runtime_targets: browserEvidence,
@@ -522,7 +479,6 @@ export const TOOL_EFFECTS: Readonly<Record<string, ToolEffects>> = {
writesFilesystem: true,
launchesProcess: true,
accessesNetwork: true,
executesForeignCode: true,
idempotent: false,
}),
compare_process_captures: sessionEvidence,
@@ -550,15 +506,9 @@ export const annotationsFromEffects = (
value.launchesProcess ||
value.accessesNetwork ||
value.changesUiState ||
value.mayDiscardData ||
value.executesForeignCode ||
value.mutatesDevice
value.mayDiscardData
),
// MCP defines destructive as possibly non-additive changes to the
// environment. Running foreign code or changing a device cannot be bounded
// to additive effects, so either one is destructive even without data loss.
destructiveHint:
value.mayDiscardData || value.executesForeignCode || value.mutatesDevice,
destructiveHint: value.mayDiscardData,
idempotentHint: value.idempotent,
openWorldHint: value.launchesProcess || value.accessesNetwork,
});
@@ -293,8 +293,6 @@ const toolAvailabilityFacts = {
accessesNetwork: z.boolean(),
changesUiState: z.boolean(),
mayDiscardData: z.boolean(),
executesForeignCode: z.boolean(),
mutatesDevice: z.boolean(),
idempotent: z.boolean(),
}),
annotations: z.object({
+3 -3
View File
@@ -51,9 +51,9 @@ export interface AnalysisCleanupObservation {
/** Optional provider-neutral context that must survive typed error projection. */
export interface AnalysisErrorOptions extends ErrorOptions {
readonly capturedOutput?: AnalysisCapturedOutput | undefined;
readonly cleanup?: AnalysisCleanupObservation | undefined;
readonly partialObservation?: AnalysisPartialObservation | undefined;
readonly capturedOutput?: AnalysisCapturedOutput;
readonly cleanup?: AnalysisCleanupObservation;
readonly partialObservation?: AnalysisPartialObservation;
}
/** Base class for expected analysis, provider, and session failures. */
+1 -4
View File
@@ -265,12 +265,9 @@ const hopperProcessMessage = (error: HopperProcessError): string => {
const startup =
error.userMessage === undefined ? "" : ` ${error.userMessage}`;
if (error.providerState === "exited") {
const signal = error.launcherFailure?.signal;
const exit =
error.exitCode === null
? signal === undefined || signal === null
? "no exit code was observed"
: `signal ${signal}`
? "no exit code was observed"
: `exit code ${String(error.exitCode)}`;
const recovery =
error.failureCode === undefined && stage !== "launch"
+2 -5
View File
@@ -572,7 +572,7 @@ describe("analysis error projection: caller contract", () => {
describe("analysis error projection: operational diagnostics", () => {
it("distinguishes an observed Hopper exit from an undetermined provider failure", () => {
const exited = projectAnalysisError(
new HopperProcessError(7, { operation: "search_strings", requestId: 4 }),
new HopperProcessError(7, undefined, "search_strings", 4),
);
expect(exited).toMatchObject({
code: "provider_unavailable",
@@ -590,10 +590,7 @@ describe("analysis error projection: operational diagnostics", () => {
expect(exited.remediation.action).toContain("Restart the owned provider");
const unknown = projectAnalysisError(
new HopperProcessError(null, {
operation: "search_strings",
requestId: 5,
}),
new HopperProcessError(null, undefined, "search_strings", 5),
);
expect(unknown.details).toMatchObject({
stage: "analysis",
+16 -34
View File
@@ -1,7 +1,4 @@
import {
AnalysisError,
type AnalysisErrorOptions,
} from "./analysisErrorBase.js";
import { AnalysisError } from "./analysisErrorBase.js";
import type {
HopperProcessProviderState,
ProviderFailureStage,
@@ -76,10 +73,10 @@ export type HopperDiagnosticType =
| "bridge_exception";
/** Hopper's JSON-RPC endpoint returned an expected remote error response. */
export interface HopperRemoteErrorContext extends AnalysisErrorOptions {
readonly diagnosticType?: HopperDiagnosticType | undefined;
readonly operation?: string | undefined;
readonly requestId?: number | undefined;
export interface HopperRemoteErrorContext {
readonly diagnosticType?: HopperDiagnosticType;
readonly operation?: string;
readonly requestId?: number;
}
export class HopperRemoteError extends HopperError {
@@ -93,7 +90,7 @@ export class HopperRemoteError extends HopperError {
readonly safeMessage: string,
context: HopperRemoteErrorContext = {},
) {
super(`Hopper request failed (${String(code)}): ${safeMessage}`, context);
super(`Hopper request failed (${String(code)}): ${safeMessage}`);
this.diagnosticType = context.diagnosticType ?? "remote";
this.operation = context.operation;
this.requestId = context.requestId;
@@ -101,50 +98,35 @@ export class HopperRemoteError extends HopperError {
}
/** The owned Hopper bridge stopped before its client was closed. */
export interface HopperProcessErrorContext extends AnalysisErrorOptions {
readonly diagnostic?: HopperStartupFailureDiagnostic | undefined;
readonly operation?: string | undefined;
readonly requestId?: number | undefined;
readonly providerState?: HopperProcessProviderState | undefined;
readonly stage?: ProviderFailureStage | undefined;
readonly launcherFailure?: HopperLauncherOutcome | undefined;
}
export class HopperProcessError extends HopperError {
readonly _tag = "HopperProcessError";
readonly failureCode: HopperStartupFailureCode | undefined;
override readonly userMessage: string | undefined;
readonly providerState: HopperProcessProviderState;
readonly stage: ProviderFailureStage;
readonly diagnostic: HopperStartupFailureDiagnostic | undefined;
readonly operation: string | undefined;
readonly requestId: number | undefined;
readonly launcherFailure: HopperLauncherOutcome | undefined;
constructor(
readonly exitCode: number | null,
context: HopperProcessErrorContext = {},
readonly diagnostic?: HopperStartupFailureDiagnostic,
readonly operation?: string,
readonly requestId?: number,
providerState?: HopperProcessProviderState,
stage?: ProviderFailureStage,
readonly launcherFailure?: HopperLauncherOutcome,
) {
super(
`Hopper bridge stopped unexpectedly with code ${String(exitCode)}`,
context,
);
this.diagnostic = context.diagnostic;
this.operation = context.operation;
this.requestId = context.requestId;
this.launcherFailure = context.launcherFailure;
super(`Hopper bridge stopped unexpectedly with code ${String(exitCode)}`);
const failure = hopperStartupFailure(exitCode);
this.stage =
context.stage ??
stage ??
(failure === undefined
? context.operation === undefined
? operation === undefined
? "connection"
: "analysis"
: "launch");
this.failureCode = failure?.code;
this.userMessage = failure?.message;
this.providerState =
context.providerState ?? (exitCode === null ? "unknown" : "exited");
providerState ?? (exitCode === null ? "unknown" : "exited");
}
}
@@ -539,9 +539,11 @@ describe("Electron module interop generic invocations", () => {
'const change = Function.prototype.call.bind(Object.defineProperty); change(null,Object,"create",{value:customCreate});',
'const call = Function.prototype.call; const change = call.bind(Object.defineProperty); change(null,Object,"create",{value:customCreate});',
'const change = Function.prototype.call.bind(Object.defineProperty,null,Object,"create"); change({value:customCreate});',
'Function.prototype.call.call(Object.defineProperty,null,Object,"create",{value:customCreate});',
'Function.prototype.call.apply(Object.defineProperty,[null,Object,"create",{value:customCreate}]);',
'Reflect.apply(Function.prototype.call,Object.defineProperty,[null,Object,"create",{value:customCreate}]);',
'const change = Function.prototype.apply.bind(Object.defineProperty); change(null,[Object,"create",{value:customCreate}]);',
'Function.prototype.apply.call(Object.defineProperty,null,[Object,"create",{value:customCreate}]);',
'Reflect.apply(Function.prototype.apply,Object.defineProperty,[null,[Object,"create",{value:customCreate}]]);',
'const first = Function.prototype.call.bind(Object.defineProperty,null,Object,"create"); const change = first.bind(fake); change({value:customCreate});',
"const change = Function.prototype.call.bind(Object.defineProperty); change(null,...args);",
@@ -18,7 +18,6 @@ export const javaScriptAnalysisCoverageLimits = (
"worker_process_memory_limit_bytes",
"worker_timeout_ms",
"parent_result_budget_bytes",
"parent_available_result_budget_bytes",
"semantic_result_budget_bytes",
"transfer_record_bytes",
"transfer_file_bytes",
@@ -1,147 +0,0 @@
import { describe, expect, it } from "vitest";
import {
classifyPackageImportSpecifier,
classifyPackageImportTargetString,
lookupExactPackageImport,
} from "./javascriptPackageImportAliases.js";
describe("package import specifier classification", () => {
it("treats an exact hash alias as an exact key", () => {
expect(classifyPackageImportSpecifier("#config")).toEqual({
kind: "exact",
key: "#config",
});
});
it.each(["#internal/env", "#/hidden"])(
"treats %j as an exact key",
(specifier) => {
expect(classifyPackageImportSpecifier(specifier)).toEqual({
kind: "exact",
key: specifier,
});
},
);
it.each(["#", "#/", "#utils/"])(
"rejects reserved specifier %j",
(specifier) => {
expect(classifyPackageImportSpecifier(specifier)).toEqual({
kind: "invalid",
});
},
);
it.each(["#internal/*", "#*"])(
"leaves pattern specifier %j unresolved",
(specifier) => {
expect(classifyPackageImportSpecifier(specifier)).toEqual({
kind: "pattern",
});
},
);
});
describe("exact package import lookup", () => {
it("returns the exact target without falling through to another key", () => {
expect(
lookupExactPackageImport(
{ "#config": "./config.cjs", "#other": "./other.cjs" },
"#config",
),
).toEqual({
kind: "found",
target: "./config.cjs",
declared: ["#config", "#other"],
});
});
it("names declared exact keys when the requested alias is absent", () => {
expect(
lookupExactPackageImport({ "#config": "./config.cjs" }, "#missing"),
).toEqual({
kind: "missing-key",
declared: ["#config"],
});
});
it("does not claim a miss when the map only has pattern keys", () => {
expect(
lookupExactPackageImport({ "#internal/*": "./src/*.js" }, "#internal/a"),
).toEqual({
kind: "pattern-unresolved",
declared: ["#internal/*"],
});
});
it("still selects an exact key when the same map also has patterns", () => {
expect(
lookupExactPackageImport(
{ "#config": "./config.cjs", "#internal/*": "./src/*.js" },
"#config",
),
).toEqual({
kind: "found",
target: "./config.cjs",
declared: ["#config", "#internal/*"],
});
});
it.each([undefined, null, "./config.cjs", ["#config"], 7] as const)(
"treats non-object imports value %j as an undefined map",
(imports) => {
expect(lookupExactPackageImport(imports, "#config")).toEqual({
kind: "undefined-map",
});
},
);
it("does not treat a pattern key as an exact match for itself", () => {
expect(
lookupExactPackageImport({ "#internal/*": "./src/*.js" }, "#internal/*"),
).toEqual({
kind: "pattern-unresolved",
declared: ["#internal/*"],
});
});
});
describe("package import target string classification", () => {
it("keeps package-local relative targets", () => {
expect(classifyPackageImportTargetString("./config.cjs")).toEqual({
kind: "relative",
target: "./config.cjs",
});
});
it("marks recursive hash aliases as recursive", () => {
expect(classifyPackageImportTargetString("#other")).toEqual({
kind: "recursive",
target: "#other",
});
});
it.each(["lodash", "pkg/subpath"])(
"marks %j as an external-package target",
(target) => {
expect(classifyPackageImportTargetString(target)).toEqual({
kind: "external-package",
target,
});
},
);
it.each([
"../outside.js",
"/outside.js",
"https://example.invalid/mod.js",
"file:///outside.js",
"node:fs",
])("marks %j as an invalid import target", (target) => {
expect(classifyPackageImportTargetString(target)).toEqual({
kind: "invalid",
target,
});
});
});
@@ -1,98 +0,0 @@
/**
* Exact package-private import-alias lookup. Node resolves `#` specifiers
* against the importer's enclosing package `imports` map; this module owns the
* specifier and exact-key rules. Condition, array, and null target selection
* stay with the shared package-target machinery. Wildcard keys, recursive
* `#` targets, and external-package targets remain explicit unknowns.
*/
/** How one `#` specifier participates in exact import-alias lookup. */
export type PackageImportSpecifierClassification =
| { readonly kind: "invalid" }
| { readonly kind: "pattern" }
| { readonly kind: "exact"; readonly key: string };
/** Outcome of looking up one exact `#` key in a package `imports` object. */
export type PackageImportExactLookup =
| { readonly kind: "undefined-map" }
| {
readonly kind: "missing-key";
readonly declared: readonly string[];
}
| {
readonly kind: "pattern-unresolved";
readonly declared: readonly string[];
}
| {
readonly kind: "found";
readonly target: unknown;
readonly declared: readonly string[];
};
/** How a selected string target is interpreted for package-private imports. */
export type PackageImportTargetClassification =
| { readonly kind: "relative"; readonly target: string }
| { readonly kind: "invalid"; readonly target: string }
| { readonly kind: "recursive"; readonly target: string }
| { readonly kind: "external-package"; readonly target: string };
/**
* Classify a `#` module specifier before any package-scope walk.
* `#` and names ending in `/` are invalid. Other literal keys, including
* `#/…`, participate in exact lookup; wildcard expansion remains unsupported.
*/
export const classifyPackageImportSpecifier = (
specifier: string,
): PackageImportSpecifierClassification => {
if (specifier === "#" || specifier.endsWith("/")) return { kind: "invalid" };
if (specifier.includes("*")) return { kind: "pattern" };
return { kind: "exact", key: specifier };
};
/** True when an `imports` key is a pattern rather than an exact alias. */
export const isPackageImportPatternKey = (key: string): boolean =>
key.includes("*") || key.endsWith("/");
/**
* Look up one exact `#` key in a package `imports` value.
* A missing exact key is reported as missing when every declared key is also
* exact; if the map contains pattern keys, the miss stays pattern-unresolved
* rather than claiming the specifier is undefined.
*/
export const lookupExactPackageImport = (
imports: unknown,
key: string,
): PackageImportExactLookup => {
if (typeof imports !== "object" || imports === null || Array.isArray(imports))
return { kind: "undefined-map" };
const declared = Object.keys(imports);
if (Object.hasOwn(imports, key) && !isPackageImportPatternKey(key))
return {
kind: "found",
target: Reflect.get(imports, key),
declared,
};
return declared.some(isPackageImportPatternKey)
? { kind: "pattern-unresolved", declared }
: { kind: "missing-key", declared };
};
/**
* Classify a selected string import target. Only package-local `./` paths are
* resolved as files; recursive `#` aliases and bare packages stay unknown.
* Parent-relative, absolute, and URL targets are invalid, allowing array
* selection to skip them before choosing a package-local fallback.
*/
export const classifyPackageImportTargetString = (
target: string,
): PackageImportTargetClassification => {
if (target.startsWith("./")) return { kind: "relative", target };
if (
target.startsWith("../") ||
target.startsWith("/") ||
URL.canParse(target)
)
return { kind: "invalid", target };
if (target.startsWith("#")) return { kind: "recursive", target };
return { kind: "external-package", target };
};
@@ -179,34 +179,6 @@ describe("JavaScript semantic analysis: calls 2", () => {
).not.toBe(left.functionFingerprints[0]?.components.literalSetSha256);
});
it("keeps nested callable syntax out of its owner's fingerprint", () => {
const analyze = (nested: number, owned: number) =>
analyzeJavaScriptSemantics(`
function outer() {
function inner() { return ${nested}; }
const object = { method() { return ${nested}; } };
class Nested { method() { return ${nested}; } }
return ${owned};
}
`);
const first = analyze(1, 2);
const changedNested = analyze(3, 2);
const changedOwner = analyze(1, 4);
const syntax = (ir: typeof first) =>
ir.functionFingerprints.map(({ components }) => ({
ast: components.normalizedAstSha256,
control: components.controlFlowSha256,
literals: components.literalSetSha256,
}));
const original = syntax(first);
expect(original).toHaveLength(4);
expect(syntax(changedNested)[0]).toEqual(original[0]);
for (const index of [1, 2, 3])
expect(syntax(changedNested)[index]).not.toEqual(original[index]);
expect(syntax(changedOwner)[0]).not.toEqual(original[0]);
expect(syntax(changedOwner).slice(1)).toEqual(original.slice(1));
});
it("keeps fingerprint relations ordered and effects scoped to their owner", () => {
const ordered = analyzeJavaScriptSemantics(`
function first() {}
@@ -63,35 +63,6 @@ it("traverses a large tree in steps with the synchronous visit order (#1462)", (
expect(stepped).toEqual(synchronous);
});
it("skips child subtrees without changing sibling ancestry or exit callbacks", () => {
const parsed = parseJavaScriptSource("[1, [2], 3]", "selection.js");
if (parsed === null) throw new Error("Expected parsed source");
const entered: number[] = [];
const exited: number[] = [];
const parents: string[][] = [];
const steps = traverseJavaScriptAstSteps(parsed.program, {
skip: (node) => node.type === "ArrayExpression" && node.start === 4,
enter: (node, parent, readAncestors) => {
if (node.type !== "NumericLiteral") return;
entered.push(node.value);
expect(readAncestors().at(-1)).toBe(parent);
parents.push(readAncestors().map(({ type }) => type));
},
exit: (node) => {
if (node.type === "NumericLiteral") exited.push(node.value);
},
});
while (steps.next().done !== true) {
// Complete the same cooperative traversal used by fingerprint collection.
}
expect(entered).toEqual([1, 3]);
expect(exited).toEqual([1, 3]);
expect(parents).toEqual([
["Program", "ExpressionStatement", "ArrayExpression"],
["Program", "ExpressionStatement", "ArrayExpression"],
]);
});
it("preserves receiver and Electron alias scope across traversal pauses (#1462)", () => {
const padding = Array.from(
{ length: 400 },
@@ -73,7 +73,7 @@ export function* collectJavaScriptSemanticFingerprintsSteps(
const node = state.callableNodesById.get(callable.callableId);
if (node === undefined) continue;
yield;
const syntax = yield* fingerprintSyntaxSteps(node);
const syntax = yield* fingerprintSyntaxSteps(node, callable.callableId);
const promiseEffects = promiseEffectsByOwner.get(callable.callableId) ?? [];
const eventEffects = eventEffectsByOwner.get(callable.callableId) ?? [];
const timerEffects = timerEffectsByOwner.get(callable.callableId) ?? [];
@@ -164,19 +164,28 @@ interface FingerprintSyntax {
function* fingerprintSyntaxSteps(
root: t.Node,
targetCallableId: string,
): Generator<void, FingerprintSyntax> {
const normalizedTokens: string[] = [];
const controlFlowTokens: string[] = [];
const literals: string[] = [];
const callableStack: string[] = [];
yield* traverseJavaScriptAstSteps(root, {
skip: (node) => semanticCallableIdForNode(node) !== null,
enter: (node, parent) => {
const callableId = semanticCallableIdForNode(node);
if (callableId !== null) callableStack.push(callableId);
if (callableStack.at(-1) !== targetCallableId) return;
normalizedTokens.push(normalizedNodeToken(node, parent));
const control = controlFlowToken(node);
if (control !== null) controlFlowTokens.push(control);
const literal = semanticLiteral(node);
if (literal !== null) literals.push(literal);
},
exit: (node) => {
const callableId = semanticCallableIdForNode(node);
if (callableId !== null && callableStack.at(-1) === callableId)
callableStack.pop();
},
});
return { normalizedTokens, controlFlowTokens, literals };
}
@@ -1,7 +1,7 @@
import * as t from "@babel/types";
/**
* Visit Babel AST nodes in deterministic source-tree order, excluding skipped subtrees.
* Visit every Babel AST node in deterministic source-tree order.
*
* `readAncestors` returns a snapshot during `enter`, outermost-first and
* ending with the direct parent. Callers
@@ -17,8 +17,6 @@ export const traverseJavaScriptAst = (
/** Callbacks for one deterministic AST traversal. */
export interface JavaScriptAstVisitor {
/** Omit a child and its subtree without entering or exiting it. */
readonly skip?: (node: t.Node) => boolean;
readonly enter: (
node: t.Node,
parent: t.Node | null,
@@ -52,7 +50,6 @@ export function* traverseJavaScriptAstSteps(
current.nextIndex += 1;
if (visited % 1024 === 0) yield;
visited += 1;
if (visitor.skip?.(child) === true) continue;
pending.push(enter(child, current.node));
} else {
visitor.exit?.(current.node, current.parent);
+6 -43
View File
@@ -249,37 +249,10 @@ const networkIdentity = (
): string =>
`net_${digestCanonicalValue({ method: request.method, url: request.url, resource_type: request.resource_type })}`;
const networkResponseFields = [
"status",
"mime_type",
"encoded_data_length",
] as const;
type NetworkResponseField = (typeof networkResponseFields)[number];
const sharedNetworkResponseFields = (
before: WebPageInspection,
after: WebPageInspection,
): ReadonlyMap<string, ReadonlySet<NetworkResponseField>> => {
const shared = new Map<string, Set<NetworkResponseField>>();
for (const inspection of [before, after])
for (const request of inspection.network.requests) {
const identity = networkIdentity(request);
const fields = shared.get(identity) ?? new Set(networkResponseFields);
for (const field of networkResponseFields)
if (request[field] === null) fields.delete(field);
shared.set(identity, fields);
}
return shared;
};
const networkDimension = (
before: WebPageInspection,
after: WebPageInspection,
): Dimension => {
const responseFields = sharedNetworkResponseFields(before, after);
const responseFieldsComplete = [...responseFields.values()].every(
(fields) => fields.size === networkResponseFields.length,
);
const selected = bodyShapesSelected(before) && bodyShapesSelected(after);
const webSocketShapes =
webSocketShapesSelected(before) && webSocketShapesSelected(after);
@@ -340,14 +313,14 @@ const networkDimension = (
afterWebSockets,
webSocketShapes,
);
const compared = compareDimension(
return compareDimension(
new Map([
...networkMap(before, sources, responseFields),
...networkMap(before, sources),
...webSocketMetadataMap(beforeWebSockets),
...beforeWebSocketPayload,
]),
new Map([
...networkMap(after, sources, responseFields),
...networkMap(after, sources),
...webSocketMetadataMap(afterWebSockets),
...afterWebSocketPayload,
]),
@@ -363,31 +336,21 @@ const networkDimension = (
webSocketShapesComplete,
"Network capture or selected HTTP/WebSocket payload-shape coverage is attach-window limited, unavailable, or incomplete.",
);
return compared.status === "unchanged" && !responseFieldsComplete
? {
...compared,
status: "unknown",
reason:
"HTTP response status, MIME type, or byte-count evidence is incomplete.",
}
: compared;
};
const networkMap = (
inspection: WebPageInspection,
sources: ReadonlyMap<string, BodyShapeSources>,
responseFields: ReadonlyMap<string, ReadonlySet<NetworkResponseField>>,
): ReadonlyMap<string, string> => {
const grouped = new Map<string, unknown[]>();
for (const request of inspection.network.requests) {
const identity = networkIdentity(request);
const shapes = sources.get(identity);
const values = grouped.get(identity) ?? [];
const response: Record<string, unknown> = {};
for (const field of responseFields.get(identity) ?? [])
response[field] = request[field];
values.push({
...response,
status: request.status,
mime_type: request.mime_type,
encoded_data_length: request.encoded_data_length,
redirects: (request.redirects ?? []).map(
({
request_timestamp: _requestTimestamp,
+8 -7
View File
@@ -1,7 +1,6 @@
import { z } from "zod";
import { digestSchema } from "./digests.js";
import { localPathStringSchema } from "./localPath.js";
import {
exportedWebScriptSchema,
webScriptExportManifestSchema,
@@ -9,9 +8,10 @@ import {
/** Inspect one captured script's native ES module relationships. */
export const webModuleTraceInputSchema = z.strictObject({
manifest_path: localPathStringSchema.describe(
"Absolute path to an export_web_scripts manifest",
),
manifest_path: z
.string()
.min(1)
.describe("Absolute path to an export_web_scripts manifest"),
script_index: z
.number()
.int()
@@ -26,9 +26,10 @@ export const webModuleTraceInputSchema = z.strictObject({
),
import_map: z
.strictObject({
path: localPathStringSchema.describe(
"Absolute local import-map JSON path; no fetch",
),
path: z
.string()
.min(1)
.describe("Absolute local import-map JSON path; no fetch"),
base_url: z
.string()
.min(1)
+4 -4
View File
@@ -1,6 +1,5 @@
import { z } from "zod";
import { jsonValueSchema } from "./jsonValue.js";
import { localPathStringSchema } from "./localPath.js";
/** Complete historical evidence budgets; exceeding a budget returns no partial capture. */
export const WEB_NETWORK_CAPTURE_LIMITS = {
@@ -12,9 +11,10 @@ export const WEB_NETWORK_CAPTURE_LIMITS = {
/** Inspect an explicit retained capture without contacting its recorded destinations. */
export const inspectWebNetworkCaptureInputSchema = z.strictObject({
capture_path: localPathStringSchema.describe(
"Absolute local HAR or mitmproxy capture path.",
),
capture_path: z
.string()
.min(1)
.describe("Absolute local HAR or mitmproxy capture path."),
format: z.enum(["har", "mitmproxy"]),
record_ordinals: z
.array(z.number().int().nonnegative())
+4 -4
View File
@@ -1,13 +1,13 @@
import { z } from "zod";
import { digestSchema } from "./digests.js";
import { localPathStringSchema } from "./localPath.js";
import { webScriptExportManifestSchema } from "./webScriptExport.js";
/** Select one source from an existing captured-script export. */
export const selectedWebScriptInputSchema = z.strictObject({
manifest_path: localPathStringSchema.describe(
"Absolute export_web_scripts manifest path",
),
manifest_path: z
.string()
.min(1)
.describe("Absolute export_web_scripts manifest path"),
script_index: z
.number()
.int()
Loaded 100 of 220 files, more files were not shown because too many files have changed in this diff. Show more