Compare commits
49
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a64851f259 | ||
|
|
c267e99227 | ||
|
|
4bb4c6fe6d | ||
|
|
411498d45f | ||
|
|
c3fc6c3aed | ||
|
|
1f099a6fc0 | ||
|
|
701d90aba4 | ||
|
|
a8701eaf48 | ||
|
|
ad59531a96 | ||
|
|
69c3b32a21 | ||
|
|
5f749c2096 | ||
|
|
76b214e45f | ||
|
|
614e0c2e74 | ||
|
|
73e98b2516 | ||
|
|
99f257bbf0 | ||
|
|
b9e29c52d4 | ||
|
|
1c3766a8bc | ||
|
|
098d76900e | ||
|
|
2ddd9456cd | ||
|
|
43405f9ffe | ||
|
|
6020a0b368 | ||
|
|
fe34ec2ef3 | ||
|
|
ad813e473b | ||
|
|
1e341973cf | ||
|
|
69833fb0d0 | ||
|
|
184ed970b9 | ||
|
|
8c9c5550d0 | ||
|
|
fb57c8643f | ||
|
|
48de11ce2e | ||
|
|
b80bd2db96 | ||
|
|
d601b8016e | ||
|
|
39eb0587c9 | ||
|
|
db98a51dd1 | ||
|
|
59fbaa2d33 | ||
|
|
7b51121f7c | ||
|
|
33bf585185 | ||
|
|
f61673fa99 | ||
|
|
7e96f25f8b | ||
|
|
502528c58d | ||
|
|
bb8ff2cfba | ||
|
|
b8851cb977 | ||
|
|
2060a6d7f3 | ||
|
|
09058ebc4a | ||
|
|
1e83d270c2 | ||
|
|
a8b8288396 | ||
|
|
5486860673 | ||
|
|
fd46cff017 | ||
|
|
7bf63ed7f8 | ||
|
|
da606ff777 |
@@ -5,8 +5,7 @@ on:
|
||||
branches: [main, "release/*"]
|
||||
paths:
|
||||
- "website/**"
|
||||
- "scripts/verify-website.py"
|
||||
- "scripts/prepare-website.py"
|
||||
- "scripts/*website*.py"
|
||||
- ".github/workflows/**"
|
||||
workflow_dispatch:
|
||||
|
||||
@@ -21,11 +20,24 @@ jobs:
|
||||
check:
|
||||
name: Website checks
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare the example download
|
||||
- name: Set up Python for website assets
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
- name: Prepare website assets
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links and publishing configuration
|
||||
- name: Check website links, metadata and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Check sitemap discovery and metadata regressions
|
||||
run: python3 scripts/test-website.py
|
||||
|
||||
@@ -23,7 +23,7 @@ jobs:
|
||||
prepare:
|
||||
if: github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 5
|
||||
timeout-minutes: 10
|
||||
permissions:
|
||||
contents: read
|
||||
pages: read
|
||||
@@ -48,9 +48,20 @@ jobs:
|
||||
fi
|
||||
- name: Check out the repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Prepare the example download
|
||||
- name: Set up Python for website assets
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.12"
|
||||
cache: pip
|
||||
cache-dependency-path: website/requirements.txt
|
||||
- name: Install sharing-image dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --no-install-recommends -y libcairo2 fonts-dejavu-core
|
||||
python3 -m pip install -r website/requirements.txt
|
||||
- name: Prepare website assets
|
||||
run: python3 scripts/prepare-website.py
|
||||
- name: Check website links and publishing configuration
|
||||
- name: Check website links, metadata and publishing configuration
|
||||
run: python3 scripts/verify-website.py
|
||||
- name: Record the published commit
|
||||
run: |
|
||||
|
||||
@@ -50,6 +50,9 @@ Thumbs.db
|
||||
# Generated website download
|
||||
/website/public/examples/notes-example.zip
|
||||
/website/public/deployment.json
|
||||
/website/public/sitemap.xml
|
||||
/website/public/assets/social-card.png
|
||||
/website/.venv/
|
||||
|
||||
# Local Cloudflare development state
|
||||
/website/.wrangler/
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "6.1.0"
|
||||
".": "6.2.0"
|
||||
}
|
||||
|
||||
+243
@@ -1,5 +1,248 @@
|
||||
# Changelog
|
||||
|
||||
## [6.2.0](https://github.com/morluto/rea/compare/rea-agents-6.1.0...rea-agents-6.2.0) (2026-10-09)
|
||||
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
Read the [6.1 to 6.2 migration guide](docs/migration-6.2.md) before upgrading. REA's `always-bump-minor` policy includes breaking changes; 6.2 is not a backward-compatibility promise. Preserve original saved files and regenerate missing observations rather than fabricating identities or facts.
|
||||
|
||||
* **MCP / Evidence / snapshots / artifacts:** Success results are direct canonical Evidence; read `normalized_result` rather than the old `result`/`evidence` wrapper. Evidence requires nullable `analysis_profile`, session provider aliases are removed, snapshots require complete profile-bound query records, and artifact roles/permissions belong to occurrences. Recreate incompatible saved data; adding fields without recomputing their semantic identity is invalid. ([63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999))
|
||||
* **Process captures:** Legacy v3 conversion is removed. Explicit executable identity and `event_journal` are required; retain old captures and recapture when current comparisons are needed instead of inventing launch identity or event order. ([2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f))
|
||||
* **Cleanup and evaluation:** A one-shot analysis reports cleanup failure while preserving the completed observation or primary diagnostic. Handle `cleanup_incomplete` and retained observations/resources. Configured agent-evaluation fixtures require Evidence-backed correctness, not routing or answer wording alone. ([#1211](https://github.com/morluto/rea/pull/1211), [27a9cea](https://github.com/morluto/rea/commit/27a9ceade7232c1c9a24dc7d454ae060948183a1))
|
||||
* **JavaScript:** Return-field projections require explicit presence; export-shape changes carry both sides' presence and property inventory coverage. Semantic object operations retain complete `property_path` segments. Regenerate old analysis and distinguish unknown values from unknown presence. ([475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec))
|
||||
* **Function comparisons:** Remove the unreachable `truncated` dimension state and `summary.truncated` counter; update enum and display consumers while retaining explicit `unknown` facts. ([5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712))
|
||||
* **Historical sources:** Unreadable/unknown symlink targets require `target: null`; external targets retain the observed absolute target. Reimport graphs containing display placeholders such as `<outside-root>` and handle nullable targets. ([b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da))
|
||||
|
||||
### Features
|
||||
|
||||
* **javascript:** report observed export property presence ([#1160](https://github.com/morluto/rea/issues/1160)) ([45e3edc](https://github.com/morluto/rea/commit/45e3edc17f48954c89d8db86f7313c479bd39344))
|
||||
* **mcp:** select views of retained analysis results ([#1180](https://github.com/morluto/rea/issues/1180)) ([db675f1](https://github.com/morluto/rea/commit/db675f1d52acda62b6f57b5a8773532f49b259e7))
|
||||
* **website:** add an Aegis APK code-generation showcase ([0270dd5](https://github.com/morluto/rea/commit/0270dd5739480acced62e045b5890ec0ebde2631))
|
||||
* **website:** add an Android showcase and clearer project prompts ([6cc817e](https://github.com/morluto/rea/commit/6cc817e8bd377b064b3568ef1eacb27eaefcb4c1))
|
||||
* **website:** add beginner examples and synchronized publishing ([0fec65d](https://github.com/morluto/rea/commit/0fec65d06b2f5431e704c472e53015d4b3cc967c))
|
||||
* **website:** add beginner examples and synchronized publishing ([#1203](https://github.com/morluto/rea/issues/1203)) ([0fec65d](https://github.com/morluto/rea/commit/0fec65d06b2f5431e704c472e53015d4b3cc967c))
|
||||
* **website:** add Discord link beside GitHub badge ([cc39fc5](https://github.com/morluto/rea/commit/cc39fc53b829a349ae199823472990a53014c507))
|
||||
* **website:** add homepage navigation and faster reading paths ([863674b](https://github.com/morluto/rea/commit/863674b2e92dd7c699768db82d521ccc88b7ebe4))
|
||||
* **website:** add homepage navigation and faster reading paths ([c94f06d](https://github.com/morluto/rea/commit/c94f06d98f895eb8b6c5ffa5e999ef1d4bb5eea2))
|
||||
* **website:** add homepage navigation and faster reading paths ([#1213](https://github.com/morluto/rea/issues/1213)) ([863674b](https://github.com/morluto/rea/commit/863674b2e92dd7c699768db82d521ccc88b7ebe4))
|
||||
* **website:** add search metadata and automatic sitemap ([99f257b](https://github.com/morluto/rea/commit/99f257bbf03c27327e5e0a232fefd3c792acd5bc))
|
||||
* **website:** add search metadata and automatic sitemap ([b8851cb](https://github.com/morluto/rea/commit/b8851cb977cfdd608798d32bf5877f645b960831)), closes [#1283](https://github.com/morluto/rea/issues/1283)
|
||||
* **website:** clarify reverse engineering and showcase goals ([e750c26](https://github.com/morluto/rea/commit/e750c262fbf37100369ab25bf8659daa1a2c4cc7))
|
||||
* **website:** explain REA through a first investigation ([5160d9b](https://github.com/morluto/rea/commit/5160d9b717d0207a357c754bdf72e215e59d19d1))
|
||||
* **website:** explain REA with Calculator and a playable dinosaur lab ([11b9f03](https://github.com/morluto/rea/commit/11b9f0346cf5413045b41ff552a024e11ba46ccc))
|
||||
* **website:** lead with the playable dinosaur example ([19c52f5](https://github.com/morluto/rea/commit/19c52f5e3f42fb754fc154bbd16552c5111df0c5))
|
||||
* **website:** show manual analysis beside a one-prompt workflow ([291bc6f](https://github.com/morluto/rea/commit/291bc6fb70f47d28d23440f03a8a7fa49eb9a391))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **dev:** accept formatter-ignored staged documentation without failing the commit hook; malformed JSON remains rejected. ([bd2db2f](https://github.com/morluto/rea/commit/bd2db2f0fa495fb08dea443cd16d7683d4b86e6a))
|
||||
|
||||
* **release:** publish reviewed release-PR merges from their exact reviewed source. ([c12bad7](https://github.com/morluto/rea/commit/c12bad7f139431fc0db7a8b7c9db1eab95f96dc2))
|
||||
|
||||
* **android:** support owned JADX stdio on Windows ([#1276](https://github.com/morluto/rea/issues/1276)) ([636b245](https://github.com/morluto/rea/commit/636b245f1badf2a7ecfa63c6e2378ac2f7673cc3)), closes [#1262](https://github.com/morluto/rea/issues/1262)
|
||||
* **android:** validate engine archive prerequisites ([#1145](https://github.com/morluto/rea/issues/1145)) ([e9ebfb9](https://github.com/morluto/rea/commit/e9ebfb94eff0699b1ea84bba000c4467fa1cb998))
|
||||
* **apple:** report unusable dylib-resolution roots as invalid input ([6a1e558](https://github.com/morluto/rea/commit/6a1e558db3ec2b69561e91ea817f1c2065dfc676))
|
||||
* **apple:** report unusable dylib-resolution roots as invalid input ([24a30fd](https://github.com/morluto/rea/commit/24a30fd91c144b7f2533e5f5de8c91268773519c))
|
||||
* **artifacts:** keep cancellation and the caller path in extraction refusals ([37025cc](https://github.com/morluto/rea/commit/37025cc09f12d49f46e61eedc3564c710089146b))
|
||||
* **artifacts:** keep cancellation and the caller path in extraction refusals ([eacf846](https://github.com/morluto/rea/commit/eacf8461dad6bab11a3d5438e6db001a10f33a9b))
|
||||
* **artifacts:** match lipo arm64e variant names to their FAT slices ([de5bfec](https://github.com/morluto/rea/commit/de5bfec79fe862816350b2deeb42a12c0734d855))
|
||||
* **artifacts:** match lipo arm64e variant names to their FAT slices ([0b32577](https://github.com/morluto/rea/commit/0b32577a90aef97e36adf2a7af4f3eccd5d82357)), closes [#1168](https://github.com/morluto/rea/issues/1168)
|
||||
* **artifacts:** name only host-available workflows for non-archive plists ([f4c99ab](https://github.com/morluto/rea/commit/f4c99ab8a53806dd57dc24b3d5a600905bcb4686))
|
||||
* **artifacts:** name only host-available workflows for non-archive plists ([2295855](https://github.com/morluto/rea/commit/22958558eafdfccf71a7d5bbbcf360574a6b2e3c))
|
||||
* **artifacts:** recheck failed DMG detaches before reporting cleanup failure ([#1155](https://github.com/morluto/rea/issues/1155)) ([58a91f0](https://github.com/morluto/rea/commit/58a91f03d72162849991caf537ab058ccc918594))
|
||||
* **artifacts:** refuse unsupported extraction formats before scanning ([8e74c9c](https://github.com/morluto/rea/commit/8e74c9c62fb44556b90f20bb6c12771ac05a18f2))
|
||||
* **artifacts:** report encrypted archive entries as unsupported extraction ([de43da7](https://github.com/morluto/rea/commit/de43da71ae982dd0f3d7b5ae49c914729db3a6ec))
|
||||
* **artifacts:** report non-archive keyed-archive selections as unsupported targets ([fef38e4](https://github.com/morluto/rea/commit/fef38e4d22ee859ec43fb591d53e8b9973c69958))
|
||||
* **artifacts:** report non-archive keyed-archive selections as unsupported targets ([8c5088c](https://github.com/morluto/rea/commit/8c5088c8cdf32a9f96f6c479c6e70a71fa360168))
|
||||
* **artifacts:** report wrong target kinds as unsupported targets ([b34bdcd](https://github.com/morluto/rea/commit/b34bdcdb2877be394c59c9cbf70269dc1841ff78))
|
||||
* **artifacts:** report wrong target kinds as unsupported targets ([637bcb5](https://github.com/morluto/rea/commit/637bcb5e4182b6c60e922b18688642d8e1008e90))
|
||||
* **browser:** classify export-web-scripts capture selection failures by cause ([ddaa183](https://github.com/morluto/rea/commit/ddaa18301ab9c67e7a6f7ce66076075e8a4d5017))
|
||||
* **browser:** classify export-web-scripts capture selection failures by cause ([8c5e34c](https://github.com/morluto/rea/commit/8c5e34cd379392ca40f651ca5eeb6a947b44aa33))
|
||||
* **browser:** report Windows HAR heap exhaustion ([#1147](https://github.com/morluto/rea/issues/1147)) ([1393507](https://github.com/morluto/rea/commit/13935077ba039f6e342f9c11ab5eea935a335ae6))
|
||||
* **browser:** retain scenario observations across failures ([614e0c2](https://github.com/morluto/rea/commit/614e0c2e74819bb5e3ef3d2e16ff31ee6cab76ae))
|
||||
* **browser:** retain scenario resources until cleanup succeeds ([88640f6](https://github.com/morluto/rea/commit/88640f60fe566edbe2320688df62c7ef46c4c61c))
|
||||
* **ci:** pass selected environment to Windows Ghidra fixture ([#1281](https://github.com/morluto/rea/issues/1281)) ([f085589](https://github.com/morluto/rea/commit/f085589957922eaf421db814399e88e415712050))
|
||||
* **cli:** advertise defaulted inputs as optional in --schema and help ([#1175](https://github.com/morluto/rea/issues/1175)) ([b4927a0](https://github.com/morluto/rea/commit/b4927a0a4fa7e8935b101d214b396bfa78208281)), closes [#1169](https://github.com/morluto/rea/issues/1169)
|
||||
* **cli:** cancel routed JavaScript analysis in rea analyze ([#1156](https://github.com/morluto/rea/issues/1156)) ([089bbf5](https://github.com/morluto/rea/commit/089bbf58f3561e3f465c544bbcf8c307287b1256))
|
||||
* **cli:** complete cleanup after relayed interrupts ([#1202](https://github.com/morluto/rea/issues/1202)) ([911152a](https://github.com/morluto/rea/commit/911152a04d3aeb0d361141a8d54959009b269e49))
|
||||
* **cli:** list the read failure cause for an unreadable JSON input file ([db0afce](https://github.com/morluto/rea/commit/db0afce5f344268271c59acee04e96f986c5619a))
|
||||
* **cli:** list the read failure cause for an unreadable JSON input file ([6020df5](https://github.com/morluto/rea/commit/6020df53d9cd44845cf3b6b416db2d7802925578))
|
||||
* **cli:** resolve xrefs symbol selectors after address validation ([#1209](https://github.com/morluto/rea/issues/1209)) ([7e9c911](https://github.com/morluto/rea/commit/7e9c91199ecd133d26a51cb33b6e63e1f942df7e))
|
||||
* **config:** name the rejected setting and its constraint in configuration errors ([c3acc34](https://github.com/morluto/rea/commit/c3acc34475ca22387ea674839acfa1891b0b9f66))
|
||||
* **config:** name the rejected setting and its constraint in configuration errors ([0a0244d](https://github.com/morluto/rea/commit/0a0244db4212bc1f59dec4b925fa90b3b473136a)), closes [#1184](https://github.com/morluto/rea/issues/1184)
|
||||
* **doctor:** find the Homebrew Hopper cask without writing Homebrew caches ([98b07d0](https://github.com/morluto/rea/commit/98b07d07ddd20bea2c3bdf8d55ac2776a263bcf4))
|
||||
* **doctor:** find the Homebrew Hopper cask without writing Homebrew caches ([a503376](https://github.com/morluto/rea/commit/a503376b2f9d9ba14d1602e58e0e93059e9d9181)), closes [#1165](https://github.com/morluto/rea/issues/1165)
|
||||
* **domain:** report target read-permission denials as access_denied ([f36c10f](https://github.com/morluto/rea/commit/f36c10f9e8dc45a298d668e608f8e945bde487cf))
|
||||
* **domain:** report target read-permission denials as access_denied ([d0ff57a](https://github.com/morluto/rea/commit/d0ff57a45c8da8938e7ae16d00bfd5ec63310af3)), closes [#1188](https://github.com/morluto/rea/issues/1188)
|
||||
* **electron:** resolve effective preload after option overrides ([#1286](https://github.com/morluto/rea/issues/1286)) ([411498d](https://github.com/morluto/rea/commit/411498d45f860eba2fbc9d7051af20237b7a4cb1))
|
||||
* emit v-mode-compatible patterns for tool schemas ([#1220](https://github.com/morluto/rea/issues/1220)) ([f0da05f](https://github.com/morluto/rea/commit/f0da05f67fff1411cf2f7a48a752ee7b348c71ff))
|
||||
* **errors:** report caller-selected evidence path failures as invalid input ([d4a598e](https://github.com/morluto/rea/commit/d4a598e93649607dd293a9c81bbf79c211bc8dd4))
|
||||
* **errors:** report caller-selected evidence path failures as invalid input ([db177c9](https://github.com/morluto/rea/commit/db177c91b83adb0c3edc4f9f6f06e31ac7523d53))
|
||||
* **evidence:** cancel exports before atomic publication ([16624f1](https://github.com/morluto/rea/commit/16624f1324e156d14025b2c346596c9781c07488))
|
||||
* **ghidra:** classify modern Swift manglings in swift type analysis ([#1208](https://github.com/morluto/rea/issues/1208)) ([6935dc7](https://github.com/morluto/rea/commit/6935dc7fe1258b340bb9cdfb8cecae66ef7bc0d4))
|
||||
* **ghidra:** do not run Java from a relative JAVA_HOME in doctor and setup ([04a516b](https://github.com/morluto/rea/commit/04a516b1e369f7e57e08d0653cc08292ac497e07))
|
||||
* **ghidra:** do not run Java from a relative JAVA_HOME in doctor and setup ([c3f1698](https://github.com/morluto/rea/commit/c3f169823f690371ecfdd2d1b3b07a5749438cf1))
|
||||
* **ghidra:** keep and name a runtime directory that close could not remove ([5b769d3](https://github.com/morluto/rea/commit/5b769d3a781c645ed8885e4fabf01a00f0bbc89b))
|
||||
* **ghidra:** keep and name a runtime directory that close could not remove ([b79d379](https://github.com/morluto/rea/commit/b79d379c25bcc8ed61571c41a94b912b5704a067)), closes [#1190](https://github.com/morluto/rea/issues/1190)
|
||||
* **ghidra:** keep the NativeAOT target refusal's own recovery advice ([f198993](https://github.com/morluto/rea/commit/f198993f418e6aaa9755e7e6413abcb27c6cd684))
|
||||
* **ghidra:** preserve decoded return flow evidence ([b189c3c](https://github.com/morluto/rea/commit/b189c3ceb175dd9fa089eada9e9ca54e76307cb3))
|
||||
* **ghidra:** preserve decoded return flow evidence ([bcc8a77](https://github.com/morluto/rea/commit/bcc8a773e2a714e4899755b786a4fef1539bbace))
|
||||
* **ghidra:** reject relative installation and Java paths in doctor and setup ([8998438](https://github.com/morluto/rea/commit/8998438c23aa19e68f4ffa7574c5d341aa419165))
|
||||
* **ghidra:** reject relative installation and Java paths in doctor and setup ([5356856](https://github.com/morluto/rea/commit/53568568bf32b36274ba3a532f1e2cfb2daed271)), closes [#1183](https://github.com/morluto/rea/issues/1183)
|
||||
* **ghidra:** report NativeAOT host and target refusals as unsupported, not failures ([cfb49eb](https://github.com/morluto/rea/commit/cfb49eb8a88fc51f460907024c51e05a2f272502))
|
||||
* **ghidra:** report NativeAOT host and target refusals as unsupported, not failures ([42c276a](https://github.com/morluto/rea/commit/42c276af1ec02473a7b6700a31292aae9e53d53e)), closes [#1185](https://github.com/morluto/rea/issues/1185)
|
||||
* **ghidra:** resolve secondary symbols at function entries ([#1195](https://github.com/morluto/rea/issues/1195)) ([6fbdbd6](https://github.com/morluto/rea/commit/6fbdbd682038d012b04f0fe92c470cf1789fb101))
|
||||
* **ghidra:** stop dereferencing the removed error payload in address verification ([b80bd2d](https://github.com/morluto/rea/commit/b80bd2db966731fd3cfddfff7e92704053dbe955))
|
||||
* **ghidra:** stop dereferencing the removed error payload in address verification ([59fbaa2](https://github.com/morluto/rea/commit/59fbaa2d33359216534407f99f49705ac04a3426))
|
||||
* **ghidra:** use dot-free private session paths ([c3fc6c3](https://github.com/morluto/rea/commit/c3fc6c3aedc202e8ade015c4039a63e9f91cac6d))
|
||||
* **hopper:** keep the OS error when the FAT64 source cannot be reopened ([9f5dc52](https://github.com/morluto/rea/commit/9f5dc52be8420fb5e874595d63312bf80d07a3a2))
|
||||
* **hopper:** reserve Linux demo application before launch ([#1158](https://github.com/morluto/rea/issues/1158)) ([33d5ae0](https://github.com/morluto/rea/commit/33d5ae0b52b8684634ecf14d4e9469968c69d462))
|
||||
* **javascript:** allow cancellation during semantic graph commitment ([#1218](https://github.com/morluto/rea/issues/1218)) ([9405bd6](https://github.com/morluto/rea/commit/9405bd660def9946afaa5fcc1d99921a816911bf))
|
||||
* **javascript:** classify application input selection failures by cause ([fe5291e](https://github.com/morluto/rea/commit/fe5291e9166977a6ebbfa6ffcf7850af57192b34))
|
||||
* **javascript:** classify application input selection failures by cause ([0a14bbb](https://github.com/morluto/rea/commit/0a14bbbf05e0e61c98e06326fe7136e52fa4fb3c))
|
||||
* **javascript:** keep Wakaru version stderr out of the refusal message ([6489d58](https://github.com/morluto/rea/commit/6489d58851d055fd7ce621000d960bb42a0e4ce0))
|
||||
* **javascript:** match source-map originals that name parent directories ([#1215](https://github.com/morluto/rea/issues/1215)) ([889fb37](https://github.com/morluto/rea/commit/889fb374df0f8986f49e9d61b0fefd536327f781))
|
||||
* **javascript:** omit self-referential static references with disclosure ([#1157](https://github.com/morluto/rea/issues/1157)) ([b620e2a](https://github.com/morluto/rea/commit/b620e2a2bdcf3683b494d137bc409813735d5572))
|
||||
* **javascript:** preserve alias and container uncertainty ([73e98b2](https://github.com/morluto/rea/commit/73e98b2516dd792ea7b510aeed6b71f384bea21d))
|
||||
* **javascript:** preserve graphs with async chunk self-references ([#1152](https://github.com/morluto/rea/issues/1152)) ([7220166](https://github.com/morluto/rea/commit/72201668ed88c2cc822a07c08cf35a1e5ba88f35))
|
||||
* **javascript:** preserve property paths and uncertain slot presence ([475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec))
|
||||
* **javascript:** reject encoded separators joined by HTML URL whitespace ([2ddd945](https://github.com/morluto/rea/commit/2ddd9456cd405f081784a4b3cb02cb77cd90fbd6))
|
||||
* **javascript:** reject encoded separators joined by HTML URL whitespace ([fb57c86](https://github.com/morluto/rea/commit/fb57c8643fb8b4733cbac9090ef58fd13a3b5347))
|
||||
* **javascript:** report located sources with differing digests as modified ([#1216](https://github.com/morluto/rea/issues/1216)) ([01174c4](https://github.com/morluto/rea/commit/01174c4a81fea109ae712ea1a746a83996f928ac))
|
||||
* **javascript:** resolve HTML script URLs after URL whitespace and percent decoding ([d601b80](https://github.com/morluto/rea/commit/d601b8016eaeb2955cea4e4c0aca10d17e7d12a7))
|
||||
* **javascript:** resolve HTML script URLs after URL whitespace and percent decoding ([502528c](https://github.com/morluto/rea/commit/502528c58df54c7b34eefdb5837f36e58f473405)), closes [#1236](https://github.com/morluto/rea/issues/1236)
|
||||
* **javascript:** resolve package exports targets only to the exact file ([#1181](https://github.com/morluto/rea/issues/1181)) ([2a620c0](https://github.com/morluto/rea/commit/2a620c06cdcaa32844c4a80aeb3df1793f1ce09e)), closes [#1174](https://github.com/morluto/rea/issues/1174)
|
||||
* keep the Ghidra session root free of dot-prefixed path elements ([a8b8288](https://github.com/morluto/rea/commit/a8b8288396cf2e053db768ef03f4db756aa8f71e))
|
||||
* **managed:** preserve literal library names during native matching ([81b70bc](https://github.com/morluto/rea/commit/81b70bc358031686c9d7afd049f21719c67a9525))
|
||||
* **managed:** separate metadata and signature completeness ([2f02eff](https://github.com/morluto/rea/commit/2f02eff9887fc715c33f5d8770a6e6191ba70ca9))
|
||||
* **mcp:** advertise input patterns without look-around ([1f099a6](https://github.com/morluto/rea/commit/1f099a6fc00b08fb0283dcabc5a238025d4c4c8b))
|
||||
* **mcp:** advertise input patterns without look-around ([a8701ea](https://github.com/morluto/rea/commit/a8701eaf489692449739d5b2490502b1a14951c5)), closes [#1282](https://github.com/morluto/rea/issues/1282)
|
||||
* **mcp:** deliver tool errors within advertised schema contracts ([da606ff](https://github.com/morluto/rea/commit/da606ff77718d9a3b10426fcf68c098138e6dcfa)), closes [#1261](https://github.com/morluto/rea/issues/1261)
|
||||
* **mcp:** deliver typed errors without invalid structured output ([#1277](https://github.com/morluto/rea/issues/1277)) ([7e96f25](https://github.com/morluto/rea/commit/7e96f25f8b1380a13a086a68f8d5a70380468151))
|
||||
* **mcp:** emit portable Unicode NUL patterns ([#1230](https://github.com/morluto/rea/issues/1230)) ([aa5750c](https://github.com/morluto/rea/commit/aa5750c263632d98d626c5ca3b985bb1519d2395))
|
||||
* **mcp:** measure tool errors against their text-only delivery ([fe34ec2](https://github.com/morluto/rea/commit/fe34ec2ef3e2968eeb06f64a1a0fdb8b825f56c4))
|
||||
* **mcp:** measure tool errors against their text-only delivery ([7b51121](https://github.com/morluto/rea/commit/7b51121f7cee21a5938f10ac3b5762e0bb41a58d))
|
||||
* **mcp:** only recommend exporting retained evidence ([5f749c2](https://github.com/morluto/rea/commit/5f749c2096af2617d4b6b387ff19c13001af8e54))
|
||||
* name host and tool requirements in optional adapter refusals ([68cb02f](https://github.com/morluto/rea/commit/68cb02fe8b123b9a0778c0065c13a15bcf0e577d))
|
||||
* name host and tool requirements in optional adapter refusals ([170d0bf](https://github.com/morluto/rea/commit/170d0bf9e2d45b2512ad556c02f31bdc81ea03ca))
|
||||
* **native:** distinguish batch selectors from resolved entries ([13c3b68](https://github.com/morluto/rea/commit/13c3b68730df80330fd867d921dea38770d5f880))
|
||||
* **native:** distinguish batch selectors from resolved entries ([bfaa5a6](https://github.com/morluto/rea/commit/bfaa5a6c2c0ddc9e6fcf16cd11ad06f24db174a2))
|
||||
* **native:** name the CLI flag that selects another plist ([9b69503](https://github.com/morluto/rea/commit/9b6950373cfaa1a2c0d28287bfadd4a421afb9e9))
|
||||
* **native:** name the plist selection that works when Info.plist is missing ([5612496](https://github.com/morluto/rea/commit/5612496f6d34a462dcd0fb69fa38337592e2f72d))
|
||||
* **native:** name the plist selection that works when Info.plist is missing ([a6fdf42](https://github.com/morluto/rea/commit/a6fdf426bd98c8cf7b7a7f589208d3193784062f))
|
||||
* **native:** refuse truncated executable headers at target admission ([d39a450](https://github.com/morluto/rea/commit/d39a450bdfac425962c3a6a6f47b45600057a230))
|
||||
* **native:** refuse truncated executable headers at target admission ([0b04d38](https://github.com/morluto/rea/commit/0b04d384cca4f22f271d60bcfe67f79cab89c8de)), closes [#1189](https://github.com/morluto/rea/issues/1189)
|
||||
* **process:** apply one timeout rule to both execFileOutput modes ([3d32819](https://github.com/morluto/rea/commit/3d328193d69a63a355644c51bfb70f524d99bca9))
|
||||
* **process:** await cleanup on CLI and MCP cancellation ([f6fe575](https://github.com/morluto/rea/commit/f6fe575094658e99ced78d45f385633fbfaaae32))
|
||||
* **process:** keep ports before a chunk-final period ([184ed97](https://github.com/morluto/rea/commit/184ed970b9021552a5454ced20d202d10cabcbb8))
|
||||
* **process:** localize capture truncation diagnostics ([#1284](https://github.com/morluto/rea/issues/1284)) ([33bf585](https://github.com/morluto/rea/commit/33bf585185ffcaea63d658e2d3145c900a680485))
|
||||
* **process:** normalize ports that end a sentence ([098d769](https://github.com/morluto/rea/commit/098d76900effdbc682b0f97bcde7f8711380ecea))
|
||||
* **process:** normalize ports that end a sentence ([db98a51](https://github.com/morluto/rea/commit/db98a51dd1750b09b3905e18efac526f7ebf90f7))
|
||||
* **process:** preserve numeric output and original PTY evidence ([c5c75c3](https://github.com/morluto/rea/commit/c5c75c38c3a4ff0153513b8f8ad600d8a5bacc09))
|
||||
* **process:** preserve numeric output and original PTY evidence ([00d8e44](https://github.com/morluto/rea/commit/00d8e44cbf82caa51709c2e0857fbaa69ea329b7))
|
||||
* **process:** preserve unknown filesystem absence in partial captures ([c8544b1](https://github.com/morluto/rea/commit/c8544b1574a32838da6d4ed40f2b72582edb60dd))
|
||||
* **process:** refuse malformed timeouts for stoppable commands ([136d4aa](https://github.com/morluto/rea/commit/136d4aab18d822f0f908096b8acfac556ef1873b))
|
||||
* **process:** refuse malformed timeouts for stoppable commands ([59c91c8](https://github.com/morluto/rea/commit/59c91c88fe88210b4b22b9f1365a16567513da4a))
|
||||
* **process:** require current captures and preserve cleanup failures ([2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f))
|
||||
* **process:** retain known events from incomplete captures ([5eb0049](https://github.com/morluto/rea/commit/5eb0049a9f502b404f30e48c3fe3430a942bb0b5))
|
||||
* **process:** stop cancelled Swift helper builds before removing their files ([cf41561](https://github.com/morluto/rea/commit/cf415619cadcfcb18cda59a54b26c9d164f750a4))
|
||||
* **process:** stop cancelled Swift helper builds before removing their files ([c717204](https://github.com/morluto/rea/commit/c7172047951e7dbb9df58b57ff1b43ac9bd47909)), closes [#1173](https://github.com/morluto/rea/issues/1173)
|
||||
* **process:** stop timed-out commands with the caller's stop signal ([4b38d18](https://github.com/morluto/rea/commit/4b38d1899566089d19b9fd137202f7d86b62fc6f))
|
||||
* **process:** stop timed-out commands with the caller's stop signal ([063de50](https://github.com/morluto/rea/commit/063de5014f19e85c38b6bec5984de4f38e71adaa))
|
||||
* **process:** treat non-ASCII letters as port continuations ([ad813e4](https://github.com/morluto/rea/commit/ad813e473b094ab0d3ab30798b7d252e2f4beb94))
|
||||
* **process:** validate terminal byte totals against retained frames ([69833fb](https://github.com/morluto/rea/commit/69833fb0d0fe9b320b5745a992822342f2c54a92))
|
||||
* **process:** validate terminal byte totals against retained frames ([39eb058](https://github.com/morluto/rea/commit/39eb0587c95ad028bb961c539ad1f326a1e8d81c))
|
||||
* **providers:** preserve observations and primary cleanup failures ([0696bde](https://github.com/morluto/rea/commit/0696bde2660bb5d6538d8c01da15aa023312aa9f))
|
||||
* **reference:** represent unreadable symlink targets explicitly ([b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da))
|
||||
* restore caching, preserve failures, and verify fixture claims ([#1211](https://github.com/morluto/rea/issues/1211)) ([27a9cea](https://github.com/morluto/rea/commit/27a9ceade7232c1c9a24dc7d454ae060948183a1))
|
||||
* **server:** report 2025-era client identity and features in binary_session ([e6abaef](https://github.com/morluto/rea/commit/e6abaefbbc2bd81a6be34cbe71b87be801158c16))
|
||||
* **server:** report 2025-era client identity and features in binary_session ([b8312fa](https://github.com/morluto/rea/commit/b8312fa8d184ba9f422d1f5592fb41e49811544a)), closes [#1167](https://github.com/morluto/rea/issues/1167)
|
||||
* **session:** require an open target for session-bound native and artifact tools ([d8e314c](https://github.com/morluto/rea/commit/d8e314cb24e7a4a494347e45474234e0aad74ad1))
|
||||
* **session:** require an open target for session-bound native and artifact tools ([cf77d07](https://github.com/morluto/rea/commit/cf77d073fc06ab95ce4181c30d68b1346e990b64)), closes [#1166](https://github.com/morluto/rea/issues/1166)
|
||||
* **session:** retain failed provider cleanup for lifecycle retries ([#1260](https://github.com/morluto/rea/issues/1260)) ([24a3aa0](https://github.com/morluto/rea/commit/24a3aa08d1f69e25b441f086300ba3082d860a35))
|
||||
* **setup:** keep Codex TOML comments when adding or removing REA ([f5539cf](https://github.com/morluto/rea/commit/f5539cfc2f6c0daf6c275fc97f3351e2cf89b2c3))
|
||||
* **setup:** keep Codex TOML comments when adding or removing REA ([e9c48a6](https://github.com/morluto/rea/commit/e9c48a61e4c2c05c5b77ef4a815c5b2a65a9337e)), closes [#1163](https://github.com/morluto/rea/issues/1163)
|
||||
* **setup:** preserve initial backups and Codex comments ([#1204](https://github.com/morluto/rea/issues/1204)) ([6051cb2](https://github.com/morluto/rea/commit/6051cb2c0c723d88dc43ee5ef8a96971bf84d49e))
|
||||
* **setup:** stop remaining uninstall removals after a client fails ([fd975c7](https://github.com/morluto/rea/commit/fd975c75fa353840e6e887232ca68496cf8705ea))
|
||||
* **setup:** stop uninstall before removals when a client config is unsafe ([4fb2f3e](https://github.com/morluto/rea/commit/4fb2f3e6ed0233505eada3cddeeff0ea0bfc6fe5))
|
||||
* **setup:** stop uninstall before removals when a client config is unsafe ([5cc841d](https://github.com/morluto/rea/commit/5cc841d5c154f692bdc257e106cc4b212dc19dc0)), closes [#1164](https://github.com/morluto/rea/issues/1164)
|
||||
* **snapshot:** report query workflow and Evidence counts separately ([04aef6d](https://github.com/morluto/rea/commit/04aef6df331634238cfc61f320ba4a4e720750a4))
|
||||
* **snapshot:** report query workflow and Evidence counts separately ([0e77953](https://github.com/morluto/rea/commit/0e779533a9108a550b4d10dcb3920f91c7d48989))
|
||||
* **website:** clarify Android host and JDK requirements ([5691606](https://github.com/morluto/rea/commit/5691606aa948bddc641e3ecbccf5375a12b65924))
|
||||
* **website:** identify publication verifier requests ([e6905c1](https://github.com/morluto/rea/commit/e6905c18e09c41e04525fb1a84575483df789204))
|
||||
* **windows:** retain runtime authority after cleanup failure ([af7a439](https://github.com/morluto/rea/commit/af7a43963351c655eb86197bf52ae798a04e9c22))
|
||||
* **windows:** retain unsettled jobs for cleanup retry ([76b214e](https://github.com/morluto/rea/commit/76b214e45fa05fe723e216894327185b9b4e6f53))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **mcp:** share repeated output schema definitions. ([441813c](https://github.com/morluto/rea/commit/441813c0708dbee82ef554c91e973ae02964bf2e))
|
||||
|
||||
* **hopper:** resolve string search records once per query ([#1229](https://github.com/morluto/rea/issues/1229)) ([9426c3f](https://github.com/morluto/rea/commit/9426c3f1bbb94711d8b33097e604b9b8ed853b5e))
|
||||
* **mcp:** reuse input schemas without losing caller guidance ([#1162](https://github.com/morluto/rea/issues/1162)) ([e1afbc1](https://github.com/morluto/rea/commit/e1afbc1914cc7ff90265313e942f34ea4a58a4fc))
|
||||
* **snapshot:** avoid redundant result copies and serialization ([552634d](https://github.com/morluto/rea/commit/552634d634655a722b2204cfbc4b29f6d442c294))
|
||||
* **snapshot:** avoid redundant result copies and serialization ([569d2bc](https://github.com/morluto/rea/commit/569d2bc6b9f600114267a4c69f8400393ea1bc2f))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **domain:** remove obsolete comparison representations ([5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712))
|
||||
* enforce canonical evidence and provider boundaries ([63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999))
|
||||
* remove forwarding exports and redundant aliases ([ad59531](https://github.com/morluto/rea/commit/ad59531a960a0c51ef6857fa1391270dda0fa7a4))
|
||||
* **schemas:** share portable identifier and base64 patterns ([#1227](https://github.com/morluto/rea/issues/1227)) ([954badb](https://github.com/morluto/rea/commit/954badbaaadd24a210190ce3015e40c23d327567))
|
||||
|
||||
|
||||
### Documentation
|
||||
|
||||
* add French README translation ([84094af](https://github.com/morluto/rea/commit/84094af54a4e0a6247445e59abff27bc4eca361b))
|
||||
* add French README translation ([9664b2b](https://github.com/morluto/rea/commit/9664b2b101fe4ac334b2911f9210b50d58d90a89))
|
||||
* celebrate 30,000 GitHub stars 🎉 ([6987cb0](https://github.com/morluto/rea/commit/6987cb066a5aa5e51480bb06a93a758e6f67cd1e))
|
||||
* celebrate 30,000 GitHub stars 🎉 ([beca997](https://github.com/morluto/rea/commit/beca9977eb2b18fc7b9844cff0ca70004b6ff21a))
|
||||
* clarify REA has no affiliated cryptocurrency or token ([#1214](https://github.com/morluto/rea/issues/1214)) ([c2d8146](https://github.com/morluto/rea/commit/c2d8146f94c7533eb87681c2405e93bdfef21b59))
|
||||
* explain 6.2 contract and saved-data migration ([#1304](https://github.com/morluto/rea/issues/1304)) ([c267e99](https://github.com/morluto/rea/commit/c267e9922751c03df312655ebd623de1b480215c))
|
||||
* **ghidra:** explain supported heap and CPU controls ([#1278](https://github.com/morluto/rea/issues/1278)) ([0c6852b](https://github.com/morluto/rea/commit/0c6852b392b34319a6fd2c6dffcac10cee51ab94)), closes [#1255](https://github.com/morluto/rea/issues/1255)
|
||||
* keep the 30k milestone in Star History ([#1217](https://github.com/morluto/rea/issues/1217)) ([c399670](https://github.com/morluto/rea/commit/c399670653884673158f314ed38260aef74da593))
|
||||
* require focused brittleness cleanup and executable examples ([d6832c3](https://github.com/morluto/rea/commit/d6832c3632847cf3e62d4082b4b8b8d9af554633))
|
||||
* thank feature request contributors ([9db1fd0](https://github.com/morluto/rea/commit/9db1fd0bd7b17ed1dd21b9c72dabfa4ec6c23a3f))
|
||||
* **website:** continue the Calculator example with reconstruction ([4578647](https://github.com/morluto/rea/commit/4578647c59a7487bce597a65be6eab8be0412e84))
|
||||
* **website:** continue the Calculator example with reconstruction ([18a1e8c](https://github.com/morluto/rea/commit/18a1e8cd0efe14c06fd14aabe0c318d2ae390f78))
|
||||
* **website:** explain SEO maintenance and indexing follow-up ([bb8ff2c](https://github.com/morluto/rea/commit/bb8ff2cfba638ee3a77f951c01e98340c757f525))
|
||||
* **website:** introduce the worked homepage examples ([f6f5c16](https://github.com/morluto/rea/commit/f6f5c160979624cdcb33928f1a77cf000de403d4))
|
||||
* **website:** invite deeper projects and reader feedback ([3fb9ffa](https://github.com/morluto/rea/commit/3fb9ffaa4d67685412873cbd3101c64fabbeaff3))
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
* **apple:** verify compiled Mach-O root occurrence facts ([#1300](https://github.com/morluto/rea/issues/1300)) ([4bb4c6f](https://github.com/morluto/rea/commit/4bb4c6fe6d34525c7665a9d38b584b026dd5cdd9))
|
||||
* **artifacts:** assert asset-catalog target refusal only on macOS ([44d5b4b](https://github.com/morluto/rea/commit/44d5b4b25e0831769c1bd4e5426ef498d0bd3c74))
|
||||
* **artifacts:** pin per-occurrence roles for direct Mach-O roots and shared bytes ([701d90a](https://github.com/morluto/rea/commit/701d90aba48aaf4f23e1a73e46ebf9968fb85293))
|
||||
* **artifacts:** pin per-occurrence roles for direct Mach-O roots and shared bytes ([b9e29c5](https://github.com/morluto/rea/commit/b9e29c52d483704c64cd88f87eaa117066288602)), closes [#1234](https://github.com/morluto/rea/issues/1234) [#1235](https://github.com/morluto/rea/issues/1235)
|
||||
* **artifacts:** read /bin/ls slice layout from its FAT table ([#1223](https://github.com/morluto/rea/issues/1223)) ([c89b3a0](https://github.com/morluto/rea/commit/c89b3a00dce8df36ddb696b59c80e1adc6aceac1))
|
||||
* **artifacts:** verify compiled Mach-O roles and permissions ([4bb4c6f](https://github.com/morluto/rea/commit/4bb4c6fe6d34525c7665a9d38b584b026dd5cdd9))
|
||||
* consolidate redundant assertions into boundary workflows ([b71a591](https://github.com/morluto/rea/commit/b71a591a60e3fa5333889e3cf97e05f4ae03c686))
|
||||
* **ghidra:** preserve selected verification resource limits ([1c3766a](https://github.com/morluto/rea/commit/1c3766a8bcc8a2b6af679cdaa92a73fec42b3372))
|
||||
* **ghidra:** size oversized annotations for canonical delivery ([1e34197](https://github.com/morluto/rea/commit/1e341973cfad1dbf06755f3a086ffc23d42a723c))
|
||||
* **ghidra:** use host-native absolute session root fixtures ([1e83d27](https://github.com/morluto/rea/commit/1e83d270c2098b1bcb59591698a257a05d2296fc))
|
||||
* **javascript:** expect the invalid input_path code in the streamed CLI failure test ([d186ca7](https://github.com/morluto/rea/commit/d186ca7e41928fc2748de2631f5cb43115776571))
|
||||
* **mcp:** read bundle failures from public error content ([69c3b32](https://github.com/morluto/rea/commit/69c3b32a21c90ea96194fb695f7b58992e8b3714))
|
||||
* **mcp:** read typed errors in canonical Evidence regressions ([fd46cff](https://github.com/morluto/rea/commit/fd46cff017f0110aaab1216764cf01a71b341766))
|
||||
* **mcp:** validate public error diagnostics in acceptance workflows ([09058eb](https://github.com/morluto/rea/commit/09058ebc4a68c76d865a92dbddb65811ba8344f7))
|
||||
* **native:** assert batch identity in production registration ([933fcb3](https://github.com/morluto/rea/commit/933fcb33c1fa1eae109228b89c1200abb7745eb8))
|
||||
* **native:** build the .app fixture executable with a complete Mach-O header ([facaae8](https://github.com/morluto/rea/commit/facaae865af243656ab8f557aeb42f5909748f2b))
|
||||
* **process:** check malformed capture choices concurrently ([6020a0b](https://github.com/morluto/rea/commit/6020a0b3683ce16e1441963473605f870c247e29))
|
||||
* **process:** check malformed capture choices concurrently ([48de11c](https://github.com/morluto/rea/commit/48de11ce2e5af9810fc97d1c2f89df56bf3a3d4d))
|
||||
* remove duplicate workflow examples and self assertions ([d6d1cd2](https://github.com/morluto/rea/commit/d6d1cd248506015b59a2abdbbb84e3dbd02388fc))
|
||||
|
||||
|
||||
### Continuous Integration
|
||||
|
||||
* **website:** allow explicitly requested Pages-only publication ([796b65e](https://github.com/morluto/rea/commit/796b65e1cd15f3cf819396716320db007655ee3f))
|
||||
* **website:** allow explicitly requested Pages-only publication ([864160d](https://github.com/morluto/rea/commit/864160d8858fad3c0afebff81c31b090a1ebf823))
|
||||
* **website:** allow explicitly requested Pages-only publication ([#1205](https://github.com/morluto/rea/issues/1205)) ([796b65e](https://github.com/morluto/rea/commit/796b65e1cd15f3cf819396716320db007655ee3f))
|
||||
* **website:** publish Cloudflare and Pages from one artifact ([761881c](https://github.com/morluto/rea/commit/761881cec33b18d7228bae5e2093e5d44b96ceb1))
|
||||
|
||||
## [6.1.0](https://github.com/morluto/rea/compare/rea-agents-6.0.0...rea-agents-6.1.0) (2026-10-09)
|
||||
|
||||
|
||||
|
||||
@@ -659,7 +659,7 @@ For a client that requires manual configuration, use:
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@6.1.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@6.2.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,8 +294,11 @@ 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
|
||||
document base and query/fragment rules. CommonJS module lookups retain extension
|
||||
and directory resolution.
|
||||
document base and query/fragment rules. As in a browser, the script URL and base
|
||||
href are read without surrounding whitespace or embedded tabs and newlines, and
|
||||
their percent-encoded path bytes are decoded; encoded dot and separator bytes
|
||||
are still rejected. CommonJS module lookups retain extension and directory
|
||||
resolution.
|
||||
Unresolved HTML references retain their declaration, source range, and resolution
|
||||
reason in the renderer observations.
|
||||
HTML script source ranges follow the HTML parser across LF, CRLF, and bare CR
|
||||
|
||||
+14
-1
@@ -77,6 +77,15 @@ inventories, and do not claim that no short-lived descendant existed.
|
||||
|
||||
## Progress and cancellation
|
||||
|
||||
Tool execution failures return `isError: true` and the complete canonical
|
||||
`{ "error": ... }` diagnostic as JSON in text `content`. Read that diagnostic
|
||||
for the error code, target details, partial observations, and remediation.
|
||||
Failures omit `structuredContent`, since each advertised `outputSchema`
|
||||
describes successful results. Successful replies retain their schema-validated
|
||||
structured data and matching text projection. Oversized errors are retained
|
||||
as Evidence before delivery of a bounded diagnostic with an export reference;
|
||||
if retention is unavailable or fails, the diagnostic reports that reason.
|
||||
|
||||
REA accepts ordinary `tools/call` progress tokens. Updates are monotonic,
|
||||
rate-bounded to at most one intermediate update per 100 ms, and always allow a
|
||||
terminal update. Unknown totals are omitted; REA does not fabricate percentages.
|
||||
@@ -179,6 +188,9 @@ increasing a client deadline alone does not fix those failures.
|
||||
|
||||
## Tool results
|
||||
|
||||
Custom clients upgrading from 6.1 should follow the
|
||||
[6.2 migration guide](migration-6.2.md#mcp-results-and-evidence).
|
||||
|
||||
Evidence-producing tools return the complete canonical Evidence record in both
|
||||
text and structured content. Read `structuredContent.normalized_result` for the
|
||||
operation result and `structuredContent.evidence_id` for its identity. The same
|
||||
@@ -192,7 +204,8 @@ retained session history or an explicit bundle for transfer.
|
||||
|
||||
REA prepares complete MCP results within the pinned stdio client's 10 MiB
|
||||
receive-buffer budget, including both text and structured representations and
|
||||
room for the JSON-RPC envelope. Response budget settings are captured at startup;
|
||||
room for the JSON-RPC envelope. Tool errors carry only their text
|
||||
representation, so only that text counts against the budget. Response budget settings are captured at startup;
|
||||
restart or recreate the server to apply changes. If a result cannot fit, REA returns
|
||||
`resource_constraint` with `details.resource: "transport"` before constructing
|
||||
a document-sized string. Analysis Evidence remains complete in the current
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
# Upgrading from REA 6.1 to 6.2
|
||||
|
||||
REA 6.2 includes stability fixes and changes to public result and saved-data
|
||||
contracts. The repository uses `always-bump-minor`, including breaking releases;
|
||||
the minor version increment does not imply backward compatibility. Refresh cached
|
||||
MCP tool definitions after upgrading and adapt custom consumers using this guide.
|
||||
|
||||
## MCP results and Evidence
|
||||
|
||||
Evidence-producing tools now return the complete Evidence record directly in
|
||||
`structuredContent` and JSON text. The old `{ result, evidence_id, evidence }`
|
||||
wrapper is gone.
|
||||
For a successful Evidence-producing call, change:
|
||||
|
||||
```javascript
|
||||
// 6.1
|
||||
const value = reply.structuredContent.result;
|
||||
const evidence = reply.structuredContent.evidence;
|
||||
|
||||
// 6.2
|
||||
const evidence = reply.structuredContent;
|
||||
const value = evidence.normalized_result;
|
||||
const evidenceId = evidence.evidence_id;
|
||||
```
|
||||
|
||||
Check `isError` before reading success fields. Error results have `isError: true`
|
||||
and carry their typed JSON diagnostic in a text content block; they omit
|
||||
`structuredContent` so they cannot violate the advertised success output schema.
|
||||
|
||||
```javascript
|
||||
if (reply.isError === true) {
|
||||
const text = reply.content.find((part) => part.type === "text");
|
||||
if (text === undefined) throw new Error("REA error has no text diagnostic");
|
||||
const diagnostic = JSON.parse(text.text);
|
||||
// Handle diagnostic.code, diagnostic.message and diagnostic.details.
|
||||
}
|
||||
```
|
||||
|
||||
Lifecycle and other non-Evidence tools still use their own advertised output
|
||||
contracts. Do not assume every successful tool returns Evidence. Consult
|
||||
[`tools/list` and the result guide](mcp-contracts.md#tool-results).
|
||||
|
||||
A transport `resource_constraint` error can include a retained
|
||||
`details.reported_limits.evidence_reference`. Use `inspect_analysis_view` or
|
||||
`export_evidence_bundle` on the same connection to consume the complete retained
|
||||
Evidence. Response-budget settings are captured when the server starts; recreate
|
||||
the server to change them, and keep the client's receive budget aligned. Raising
|
||||
the server budget does not change the client's limit or analysis coverage.
|
||||
|
||||
Every Evidence record now requires `analysis_profile`, either its observed
|
||||
profile object or `null`. The field participates in Evidence identity. Records
|
||||
that omitted it cannot be migrated by inserting `null` while retaining the old
|
||||
`evidence_id`. Keep the original files and regenerate affected observations with
|
||||
6.2; use the new returned identities when linking or comparing them. Existing
|
||||
records that already satisfy the complete canonical contract remain usable.
|
||||
|
||||
The `binary_session` result removes the redundant top-level `provider` and
|
||||
`providers` aliases. Read `analysis_provider_binding`,
|
||||
`analysis_provider_candidates` and per-operation `capabilities` for selection
|
||||
and availability. Select a deep provider through `open_binary.provider_id`
|
||||
or CLI `--provider`, using the advertised values.
|
||||
|
||||
These changes establish one complete result representation and explicit
|
||||
provider/profile ownership rather than accepting incomplete producer records.
|
||||
See [63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999)
|
||||
and [#1277](https://github.com/morluto/rea/pull/1277).
|
||||
|
||||
## Saved snapshots and artifact inventories
|
||||
|
||||
Analysis snapshots require `workflow_entries` and a corresponding profile-bound
|
||||
Evidence record for every cached query. Import rejects incomplete bindings
|
||||
instead of silently discarding them. Preserve an incompatible snapshot and
|
||||
choose a new output path when rerunning analysis, for example:
|
||||
|
||||
```bash
|
||||
rea analyze /absolute/path/to/program --provider ghidra --snapshot /absolute/path/to/analysis/program-6.2.json
|
||||
```
|
||||
|
||||
In MCP, open the target without the incompatible `snapshot_path`, run the
|
||||
required queries, then save to a new path through `close_binary.snapshot_path`.
|
||||
Do not fabricate missing provider profiles or query bindings. See
|
||||
[snapshot usage](cli.md#save-and-reuse-analysis-snapshots).
|
||||
|
||||
Artifact inventory nodes identify content bytes. Path-specific role and
|
||||
permissions now belong to each occurrence:
|
||||
|
||||
| Previously read | Read in 6.2 |
|
||||
| -------------------------------- | ----------------------------------------- |
|
||||
| `nodes[].kind` | `occurrences[].artifact_kind` |
|
||||
| Path-specific `nodes[].format` | `occurrences[].artifact_format` |
|
||||
| `nodes[].executable` | `occurrences[].executable` |
|
||||
| Content identity and byte format | `nodes[].artifact_id`, `sha256`, `format` |
|
||||
|
||||
Join an occurrence to its content node using `artifact_id`; inspect the selected
|
||||
occurrence's role rather than choosing one role for all paths with equal bytes.
|
||||
An executable binary format can have `artifact_kind: "executable"` while its
|
||||
current filesystem permission produces `executable: false`. A `.js` and `.txt`
|
||||
file with identical bytes can share a node and retain different roles.
|
||||
Regenerate old inventories: a merged content node alone cannot recover the
|
||||
per-path facts that were lost. These changes are part of
|
||||
[63bf914](https://github.com/morluto/rea/commit/63bf91405cf97b114a84f337fb1272d98a267999).
|
||||
|
||||
## Process captures and cleanup failures
|
||||
|
||||
REA no longer converts legacy v3 process captures on import. Captures require
|
||||
explicit selected/launch executable identity and `event_journal`; missing data
|
||||
is rejected instead of being filled during parsing. Preserve old captures for
|
||||
historical reference and recapture with 6.2 when current comparisons are needed.
|
||||
Do not infer the original launched binary from a later file digest, or invent
|
||||
event ordering from independently recorded collections. An explicitly empty
|
||||
journal is valid under its advertised contract; the field cannot be omitted.
|
||||
See [2a77469](https://github.com/morluto/rea/commit/2a7746998d47446c4db6d4f524e0f183d6803f3f).
|
||||
|
||||
Filesystem effects can now report `status: "unknown"` with a reason when a
|
||||
partial snapshot cannot establish creation or deletion. Update exhaustive
|
||||
status readers and retain observed facts separately from absence assumptions.
|
||||
|
||||
A completed one-shot analysis can report `cleanup_incomplete` rather than
|
||||
silently returning success when provider cleanup fails. Preserve its
|
||||
`details.partial_observation` or primary execution diagnostics, inspect the
|
||||
reported owned resources, and address cleanup before retrying. A successful
|
||||
analysis observation does not prove successful cleanup. This behavior is covered
|
||||
by [#1211](https://github.com/morluto/rea/pull/1211). That change also makes
|
||||
configured agent-evaluation fixture checks grade cited Evidence; routing or
|
||||
answer wording alone no longer passes an assessed fixture.
|
||||
|
||||
## JavaScript and function comparisons
|
||||
|
||||
JavaScript return-field projections require `presence` with `present`, `absent`
|
||||
or `unknown-coverage`. Export-shape change rows retain both sides' presence,
|
||||
and `property_inventories` report examined properties and coverage. Unknown
|
||||
values and unknown presence are distinct; callers must not treat an unknown
|
||||
value as a missing property. Semantic object-operation graph observations use
|
||||
`properties.property_path`, an array of complete path segments or `null`,
|
||||
rather than a flattened property name. Regenerate old application analysis
|
||||
before depending on nested mutation or presence facts. See
|
||||
[475a0f8](https://github.com/morluto/rea/commit/475a0f84dac63d710e032bc7f731c1a3bda624ec).
|
||||
|
||||
Function comparison dimension statuses are `unchanged`, `changed` or `unknown`.
|
||||
The unreachable `truncated` status and `summary.truncated` counter have been
|
||||
removed. Update exhaustive enum handling and display logic; unavailable facts
|
||||
remain `unknown`. See
|
||||
[5510b27](https://github.com/morluto/rea/commit/5510b27d7f863a3df26212fba4a59f8df0da3712).
|
||||
|
||||
## Historical-source symlinks
|
||||
|
||||
Source graph symlinks with `target_state: "unreadable"` or `"unknown"` require
|
||||
`target: null`. Internal, external and missing targets retain their actual
|
||||
recorded target strings. External targets use the observed absolute target;
|
||||
display placeholders such as `<outside-root>` cannot serve as lookup data.
|
||||
Reimport the source tree when the old graph retained only a placeholder, and
|
||||
handle nullable targets in custom readers. See
|
||||
[b7b4333](https://github.com/morluto/rea/commit/b7b433370310c3ffe33d5027d8393151d5e862da).
|
||||
|
||||
Keep original saved artifacts until their replacements and consuming workflows
|
||||
have been checked. Fresh analysis may establish different facts if the target,
|
||||
source tree or runtime changed; it does not rewrite what an older capture proved.
|
||||
+35
-3
@@ -112,14 +112,17 @@ same admitted PTY chunks rather than a second unbounded stream.
|
||||
|
||||
Port normalization recognizes explicit `port`, `tcp_port`, `udp_port`, and
|
||||
`listen` fields, URL authorities, IP endpoints, and `localhost` endpoints, with
|
||||
ports from 0 through 65535. It preserves ordinary counters, dimensions, file
|
||||
line numbers, and ambiguous bare host labels. Use explicit literal `patterns`
|
||||
ports from 0 through 65535. A port may end a sentence, as in `port: 8080.`;
|
||||
a period followed by a letter or digit continues a decimal, version, or host
|
||||
name instead. Each PTY chunk is normalized on its own, so a port whose period
|
||||
ends the chunk is left unchanged because its continuation is not yet known. It preserves ordinary counters, dimensions, file line numbers,
|
||||
and ambiguous bare host labels. Use explicit literal `patterns`
|
||||
for a different application-specific spelling. Set `ports: false` to preserve
|
||||
all endpoint numbers in comparison text. Captures commit the port-normalization
|
||||
version so older broad numeric normalization cannot silently compare as the
|
||||
same contract.
|
||||
|
||||
Output, file count, file size, process sampling, filesystem depth, total
|
||||
Output retention, file count, aggregate file-hashing bytes, process sampling, filesystem depth, total
|
||||
runtime, idle time, and post-exit settlement are bounded by the scenario's
|
||||
limits. The result marks truncated observations and residual unknowns rather
|
||||
than treating missing data as proof of equivalence. Cancellation and timeout
|
||||
@@ -127,6 +130,35 @@ run the same owned-process cleanup path. Settlement reports whether the
|
||||
sampled process group quiesced or whether cleanup was needed or unverifiable;
|
||||
sampling cannot prove that every short-lived or detached descendant was seen.
|
||||
|
||||
New captures include `truncation_details`, with separate accounting for:
|
||||
|
||||
- `raw_terminal`: original UTF-8 PTY chunk bytes and observed/retained chunk
|
||||
counts. A chunk that does not fit `limits.output_bytes` is omitted whole;
|
||||
a later smaller chunk can still fit.
|
||||
- `rendered_terminal`: cumulative UTF-8 serialized-state and visible-line
|
||||
bytes, plus observed/retained frame counts. This is independent of the raw
|
||||
chunk budget and does not count JSON encoding. Repeated screen snapshots can
|
||||
exhaust it even when all raw output fits. Rendered states use retained PTY
|
||||
input, so raw omissions also limit rendered coverage.
|
||||
- `filesystem_before` and `filesystem_after`: file-count/depth limits,
|
||||
enumeration failures, whole-file hash budget and bytes successfully hashed.
|
||||
Each retained regular file without a digest has an aliased path, size,
|
||||
remaining budget and reason: `file_bytes_budget` or
|
||||
`file_changed_or_short_read`. A file too large for the remaining budget is
|
||||
skipped; a later smaller file can still be hashed. Hash omissions do not
|
||||
imply incomplete path enumeration.
|
||||
- `process`: sampling limit and whether sampling ended partially. Coverage
|
||||
remains `sampled`, including when that limit was not exhausted.
|
||||
|
||||
The legacy `truncated` flag summarizes these observations. Comparisons retain
|
||||
results for unaffected dimensions and mark affected dimensions unknown; an
|
||||
incomplete capture cannot locate the first divergence across all dimensions.
|
||||
Trace assertions use coverage for the sources they select, so an assertion
|
||||
about complete raw terminal output need not fail because rendered snapshots
|
||||
were omitted. Older truncated captures without these details retain the
|
||||
conservative comparison behavior. These diagnostics use the existing scenario
|
||||
budgets; they do not introduce a separate file-hashing budget.
|
||||
|
||||
When the host withholds an unrelated process’s ownership token, REA leaves that
|
||||
process untouched and records its PID and reason in `cleanup.unverified_processes`
|
||||
and process residual unknowns. Successful cleanup verifies the owned group;
|
||||
|
||||
@@ -35,6 +35,9 @@ registry metadata or changelog disagrees with that version before creating a tag
|
||||
|
||||
### Compatibility and version selection
|
||||
|
||||
For the 6.2 candidate, review the [6.1 to 6.2 migration guide](migration-6.2.md)
|
||||
alongside the changelog before publication.
|
||||
|
||||
REA uses Release Please's `always-bump-minor` strategy: every release increments
|
||||
minor and resets patch, including releases with breaking changes. For example,
|
||||
the next release after 6.0.0 is 6.1.0. Version numbers use the SemVer format,
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "6.1.0",
|
||||
"version": "6.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-agents",
|
||||
"version": "6.1.0",
|
||||
"version": "6.2.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"mcpName": "io.github.morluto/rea",
|
||||
"version": "6.1.0",
|
||||
"version": "6.2.0",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
@@ -232,7 +232,7 @@
|
||||
"oxfmt",
|
||||
"oxlint --fix"
|
||||
],
|
||||
"*.{json,md,yml,yaml}": "oxfmt"
|
||||
"*.{json,md,yml,yaml}": "oxfmt --no-error-on-unmatched-pattern"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
|
||||
@@ -7,6 +7,10 @@ export async function startBrowserVerifierSite() {
|
||||
let port = 0;
|
||||
let sessionGeneration = 0;
|
||||
const server = createServer((request, response) => {
|
||||
if (request.url === "/failed-navigation") {
|
||||
response.destroy();
|
||||
return;
|
||||
}
|
||||
if (request.url === "/slow-json") {
|
||||
response.setHeader("content-type", "application/json");
|
||||
response.write('{"pending":');
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { readdir } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { promisify } from "node:util";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Client } from "@modelcontextprotocol/client";
|
||||
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
||||
|
||||
import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
|
||||
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
|
||||
|
||||
const execute = promisify(execFile);
|
||||
|
||||
@@ -142,6 +147,73 @@ export async function runScenarioCli(scenario) {
|
||||
return JSON.parse(stdout);
|
||||
}
|
||||
|
||||
/** Verify failed real navigation retains observations through both public adapters. */
|
||||
export async function verifyScenarioFailureEvidence(executable, origin) {
|
||||
const scenario = browserScenarioSchema.parse({
|
||||
browser: { mode: "launch", executable_path: executable },
|
||||
start_url: { url: `${origin}/failed-navigation` },
|
||||
actions: [
|
||||
{ step_id: "unused", action: "wait_for_timeout", duration_ms: 1 },
|
||||
],
|
||||
capture: { events: ["network"] },
|
||||
});
|
||||
const profilesBefore = await scenarioProfiles();
|
||||
let cliFailure;
|
||||
try {
|
||||
await runScenarioCli(scenario);
|
||||
assert.fail("The fixture must fail before initial navigation completes");
|
||||
} catch (cause) {
|
||||
assert.equal(typeof cause.stdout, "string");
|
||||
cliFailure = JSON.parse(cause.stdout);
|
||||
}
|
||||
assertFailedNavigation(cliFailure, scenario.start_url.url);
|
||||
const transport = new StdioClientTransport({
|
||||
command: process.execPath,
|
||||
args: [fileURLToPath(new URL("../rea.mjs", import.meta.url)), "mcp"],
|
||||
env: process.env,
|
||||
stderr: "pipe",
|
||||
});
|
||||
const client = new Client({
|
||||
name: "browser-scenario-failure-e2e",
|
||||
version: "1",
|
||||
});
|
||||
try {
|
||||
await client.connect(transport);
|
||||
const result = await client.callTool({
|
||||
name: "capture_browser_scenario",
|
||||
arguments: scenario,
|
||||
});
|
||||
assert.equal(result.isError, true);
|
||||
assertFailedNavigation(requireMcpToolError(result), scenario.start_url.url);
|
||||
} finally {
|
||||
await client.close();
|
||||
await transport.close();
|
||||
}
|
||||
const profilesAfter = await scenarioProfiles();
|
||||
assert.ok([...profilesAfter].every((profile) => profilesBefore.has(profile)));
|
||||
return {
|
||||
mocked: false,
|
||||
cli: true,
|
||||
stdio_mcp: true,
|
||||
failed_navigation_events_retained: true,
|
||||
profile_cleanup: true,
|
||||
};
|
||||
}
|
||||
|
||||
const assertFailedNavigation = (error, requestedUrl) => {
|
||||
assert.equal(error?.code, "execution_failure");
|
||||
const observation = error.details?.partial_observation;
|
||||
assert.equal(observation?.kind, "browser-scenario-observation");
|
||||
assert.equal(observation.capture.browser.cleanup, "terminated-owned-process");
|
||||
assert.deepEqual(observation.capture.steps, []);
|
||||
assert.ok(
|
||||
observation.capture.events.items.some(
|
||||
(event) =>
|
||||
event.kind === "request-failed" && event.url.url === requestedUrl,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
/** Snapshot provider-owned profile names for cleanup comparison. */
|
||||
export async function scenarioProfiles() {
|
||||
return new Set(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
@@ -158,11 +159,11 @@ const verifyDefaultScreenshotDelivery = async ({
|
||||
);
|
||||
assert.equal(constrained.isError, true);
|
||||
assert.equal(
|
||||
constrained.structuredContent?.error?.details?.resource,
|
||||
requireMcpToolError(constrained)?.details?.resource,
|
||||
"transport",
|
||||
);
|
||||
const reference =
|
||||
constrained.structuredContent?.error?.details?.reported_limits
|
||||
requireMcpToolError(constrained)?.details?.reported_limits
|
||||
?.evidence_reference;
|
||||
assert.equal(reference?.kind, "retained-evidence");
|
||||
await client.ping();
|
||||
|
||||
@@ -35,6 +35,7 @@ export const assertSameNames = (label, actual, expected) => {
|
||||
const SOURCE_PATHS = {
|
||||
packageMetadata: "dist/generatedPackageMetadata.js",
|
||||
catalogIdentity: "dist/catalogIdentity.js",
|
||||
cliCommandNames: "dist/cliCommandNames.js",
|
||||
cli: "dist/cli.js",
|
||||
toolContracts: "dist/contracts/toolContracts.js",
|
||||
officialContracts: "dist/contracts/officialToolContracts.js",
|
||||
@@ -76,7 +77,7 @@ const SOURCE_PATHS = {
|
||||
nativeProvider: "dist/native/NativeMacOSProviderMetadata.js",
|
||||
artifactProviders: "dist/application/InvestigationProviders.js",
|
||||
browserIdentities: "dist/browser/providerIdentities.js",
|
||||
v8InspectorProvider: "dist/inspector/V8InspectorProvider.js",
|
||||
v8InspectorProvider: "dist/inspector/providerIdentity.js",
|
||||
evidence: "dist/domain/evidence.js",
|
||||
evidenceBundle: "dist/domain/evidenceBundle.js",
|
||||
evidenceCompletion: "dist/domain/evidenceCompletionLedger.js",
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
requireMcpToolError,
|
||||
requireMcpLifecycleResult,
|
||||
requireMcpEvidenceResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -15,10 +20,6 @@ import {
|
||||
} from "../../dist/domain/analysisSnapshot.js";
|
||||
import { createEvidence } from "../../dist/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
|
||||
import {
|
||||
requireMcpLifecycleResult,
|
||||
requireMcpEvidenceResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Reject a valid older snapshot whose instruction-only collector omitted a real edge. */
|
||||
export async function verifyLegacyGhidraReferenceSnapshot(
|
||||
@@ -124,7 +125,7 @@ export async function verifyLegacyGhidraReferenceSnapshot(
|
||||
},
|
||||
});
|
||||
assert.equal(reply.isError, true);
|
||||
const mcpRejection = reply.structuredContent.error;
|
||||
const mcpRejection = requireMcpToolError(reply);
|
||||
assert.equal(mcpRejection.code, "evidence_integrity_mismatch");
|
||||
assert.match(mcpRejection.details.reason, /profile_mismatch/u);
|
||||
assert.match(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
|
||||
import { toolContract } from "../../dist/contracts/toolContracts.js";
|
||||
|
||||
/** Read the textual JSON projection from one MCP tool call. */
|
||||
@@ -9,6 +10,12 @@ export const mcpTextValue = (result) => {
|
||||
|
||||
const jsonValue = (result) => JSON.parse(mcpTextValue(result));
|
||||
|
||||
/** Read the complete canonical diagnostic from an MCP tool failure. */
|
||||
export const requireMcpToolError = (result) => {
|
||||
if (result.isError !== true) throw new Error("Expected an MCP tool failure");
|
||||
return analysisErrorProjectionSchema.parse(jsonValue(result).error);
|
||||
};
|
||||
|
||||
const requireOutput = (result, operation, contract) => {
|
||||
if (result.isError === true)
|
||||
throw new Error(`${operation} failed: ${mcpTextValue(result)}`);
|
||||
|
||||
@@ -25,6 +25,15 @@ const environment = () => ({
|
||||
...(process.env.JAVA_HOME === undefined
|
||||
? {}
|
||||
: { JAVA_HOME: process.env.JAVA_HOME }),
|
||||
...(process.env.GHIDRA_HEADLESS_MAXMEM === undefined
|
||||
? {}
|
||||
: { GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM }),
|
||||
...(process.env.GHIDRA_MAXMEM === undefined
|
||||
? {}
|
||||
: { GHIDRA_MAXMEM: process.env.GHIDRA_MAXMEM }),
|
||||
...(process.env._JAVA_OPTIONS === undefined
|
||||
? {}
|
||||
: { _JAVA_OPTIONS: process.env._JAVA_OPTIONS }),
|
||||
});
|
||||
|
||||
const verifyGraph = (input, target, procedure, globalAddress) => {
|
||||
|
||||
@@ -24,7 +24,7 @@ export const createProductCatalog = async (root) => {
|
||||
assertSameNames(
|
||||
"Primary CLI inventory",
|
||||
cli.primary,
|
||||
sources.catalogIdentity.CLI_COMMAND_NAMES,
|
||||
sources.cliCommandNames.CLI_COMMAND_NAMES,
|
||||
);
|
||||
const tools = toolFamilyCatalog(sources);
|
||||
const providers = providerCatalog(sources);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
/** Exercise segmented-address truncation through real CLI/MCP reads and edits. */
|
||||
@@ -40,13 +41,14 @@ export async function verifyGhidraAddressBoundaries(client, entry, cli) {
|
||||
{ timeout: 240000 },
|
||||
);
|
||||
assert.equal(reply.isError, true, `${name} accepted ${address}`);
|
||||
const error = reply.structuredContent?.error;
|
||||
const error = requireMcpToolError(reply);
|
||||
assert.equal(error?.code, "invalid_request", JSON.stringify(reply));
|
||||
assert.equal(error.details.operation, name);
|
||||
const issues = JSON.stringify(error.details.issues);
|
||||
assert.match(issues, /cannot be represented without truncation/u);
|
||||
assert.ok(issues.includes(address), issues);
|
||||
assert.equal(reply.structuredContent.evidence, undefined);
|
||||
// A rejected request produces no Evidence; errors carry text only.
|
||||
assert.equal(reply.structuredContent, undefined);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
requireMcpToolError,
|
||||
mcpTextValue,
|
||||
requireMcpOperationResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { createHash, randomUUID } from "node:crypto";
|
||||
@@ -7,11 +12,9 @@ import { setTimeout as delay } from "node:timers/promises";
|
||||
import { promisify } from "node:util";
|
||||
import Ajv from "ajv/dist/2020.js";
|
||||
import addFormats from "ajv-formats";
|
||||
|
||||
import { analysisErrorProjectionSchema } from "../../dist/contracts/errorSchemas.js";
|
||||
import {
|
||||
mcpTextValue,
|
||||
requireMcpOperationResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
|
||||
import { verifyLegacyGhidraReferenceSnapshot } from "./ghidra-reference-snapshot-e2e.mjs";
|
||||
import { verifyGhidraSnapshotLifecycle } from "./real-ghidra-snapshot-lifecycle.mjs";
|
||||
import { verifyGhidraTargetAdmission } from "./real-ghidra-target-admission.mjs";
|
||||
@@ -61,23 +64,19 @@ export async function verifyGhidraBoundaries(
|
||||
true,
|
||||
`${name} accepted ${JSON.stringify(args)}`,
|
||||
);
|
||||
// SDK input-schema rejections are text-only; application errors use the
|
||||
// canonical structured error projection, separately from success schemas.
|
||||
if (reply.structuredContent !== undefined) {
|
||||
analysisErrorProjectionSchema.parse(reply.structuredContent.error);
|
||||
assert.deepEqual(
|
||||
reply.structuredContent,
|
||||
JSON.parse(mcpTextValue(reply)),
|
||||
);
|
||||
}
|
||||
// SDK input rejections are prose; application errors carry canonical JSON
|
||||
// text without publishing data outside the advertised success schema.
|
||||
const error = mcpTextValue(reply).startsWith("{")
|
||||
? requireMcpToolError(reply)
|
||||
: undefined;
|
||||
assert.equal(reply.structuredContent, undefined);
|
||||
if (diagnostic !== undefined) {
|
||||
const error = reply.structuredContent?.error;
|
||||
assert.equal(error?.code, "invalid_request");
|
||||
assert.equal(error?.details?.operation, name);
|
||||
assert.match(JSON.stringify(error.details.issues), diagnostic);
|
||||
}
|
||||
rejectedCalls++;
|
||||
return reply.structuredContent?.error;
|
||||
return error;
|
||||
};
|
||||
const cli = async (command, value, flags = []) => {
|
||||
const { stdout } = await promisify(execFile)(
|
||||
@@ -481,7 +480,7 @@ export async function verifyGhidraBoundaries(
|
||||
options,
|
||||
);
|
||||
assert.equal(regexFailure.isError, true);
|
||||
const regexError = regexFailure.structuredContent.error;
|
||||
const regexError = requireMcpToolError(regexFailure);
|
||||
assert.equal(regexError.code, "resource_constraint");
|
||||
assert.equal(regexError.details.operation, "search_strings");
|
||||
assert.equal(regexError.details.resource, "memory");
|
||||
@@ -503,7 +502,7 @@ export async function verifyGhidraBoundaries(
|
||||
options,
|
||||
);
|
||||
assert.equal(compileFailure.isError, true);
|
||||
const compileError = compileFailure.structuredContent.error;
|
||||
const compileError = requireMcpToolError(compileFailure);
|
||||
assert.equal(compileError.code, "resource_constraint");
|
||||
assert.equal(compileError.details.operation, "search_procedures");
|
||||
assert.match(compileError.details.reason, /stack while compiling pattern/u);
|
||||
|
||||
@@ -64,11 +64,13 @@ export async function verifyGhidraLargeResults({
|
||||
assert.equal(expected[0], 0x5a);
|
||||
assert.equal(expected.at(-1), 0xa5);
|
||||
const before = await call("binary_session");
|
||||
// This request fits the SDK receive limit, but repeating the producer's
|
||||
// diagnostic in text and structured content cannot fit the response.
|
||||
// An error reaches the wire once, as escaped JSON text. Each quote costs
|
||||
// two request bytes but four response bytes once the echoed diagnostic is
|
||||
// escaped again as text, so this request fits the SDK receive limit while
|
||||
// the error response cannot.
|
||||
const missingSelector =
|
||||
"REA_MISSING_" +
|
||||
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 2) + 64 * 1024);
|
||||
'"'.repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4) + 64 * 1024);
|
||||
const lookupError = await reject("procedure_address", {
|
||||
procedure: missingSelector,
|
||||
});
|
||||
@@ -87,9 +89,12 @@ export async function verifyGhidraLargeResults({
|
||||
length,
|
||||
});
|
||||
const readId = retainedReference(readError, "read_bytes");
|
||||
// Two annotation strings repeated in text and structured content must
|
||||
// exceed the receive budget even without obsolete Evidence envelopes.
|
||||
// The request contains only the two strings and remains below that budget.
|
||||
const comment =
|
||||
"REA_BIG_COMMENT:" +
|
||||
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 10));
|
||||
"x".repeat(Math.ceil(STDIO_DEFAULT_MAX_BUFFER_SIZE / 4));
|
||||
const annotationError = await reject("annotate_native_function", {
|
||||
procedure: entry.address,
|
||||
name: "rea_frame_probe",
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
requireMcpToolError,
|
||||
requireMcpOperationResult,
|
||||
} from "./mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { access, mkdtemp, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { parseAnalysisSnapshot } from "../../dist/domain/analysisSnapshot.js";
|
||||
import { parseEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
|
||||
import { requireMcpOperationResult } from "./mcp-verifier-results.mjs";
|
||||
|
||||
/** Keep mutable Ghidra observations out of immutable snapshots across target opens. */
|
||||
export async function verifyGhidraSnapshotLifecycle(
|
||||
@@ -31,7 +34,7 @@ export async function verifyGhidraSnapshotLifecycle(
|
||||
true,
|
||||
`${name} accepted a mutated snapshot binding`,
|
||||
);
|
||||
const error = reply.structuredContent?.error;
|
||||
const error = requireMcpToolError(reply);
|
||||
assert.equal(
|
||||
error?.code,
|
||||
"evidence_integrity_mismatch",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
|
||||
import {
|
||||
requireMcpEvidenceResult,
|
||||
requireEvidenceProvider,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProvider.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../../dist/hopper/HopperProviderCapabilities.js";
|
||||
|
||||
/** Verify the selected Hopper candidate and concrete provider binding. */
|
||||
export const requireHopperSelection = (status, expected) => {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
"""Package the public Notes example for the static website."""
|
||||
"""Prepare downloads, the sitemap and the sharing image for the static website."""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from io import BytesIO
|
||||
from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
from urllib.parse import quote
|
||||
import xml.etree.ElementTree as ET
|
||||
from zipfile import ZIP_DEFLATED, ZipFile, ZipInfo
|
||||
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
EXAMPLE_FILES = (
|
||||
"package.json",
|
||||
"main.js",
|
||||
@@ -15,6 +21,70 @@ EXAMPLE_FILES = (
|
||||
)
|
||||
|
||||
|
||||
class IndexPolicy(HTMLParser):
|
||||
"""Read indexing directives from the authored HTML head."""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.in_head = False
|
||||
self.noindex = False
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
if tag == "head":
|
||||
self.in_head = True
|
||||
if tag == "meta" and self.in_head:
|
||||
attributes = dict(attrs)
|
||||
if (attributes.get("name") or "").lower() in ("robots", "googlebot"):
|
||||
directives = re.split(
|
||||
r"[\s,]+", (attributes.get("content") or "").lower()
|
||||
)
|
||||
self.noindex |= bool(set(directives) & {"noindex", "none"})
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
self.in_head = False
|
||||
|
||||
|
||||
def prepare_sitemap(site):
|
||||
"""Discover HTML routes automatically, excluding pages marked noindex."""
|
||||
namespace = "http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||
ET.register_namespace("", namespace)
|
||||
sitemap = ET.Element(f"{{{namespace}}}urlset")
|
||||
for path in sorted(site.rglob("*.html")):
|
||||
policy = IndexPolicy()
|
||||
policy.feed(path.read_text(encoding="utf-8"))
|
||||
if policy.noindex:
|
||||
continue
|
||||
route = path.relative_to(site).as_posix()
|
||||
if path.name == "index.html":
|
||||
route = route.removesuffix("index.html")
|
||||
entry = ET.SubElement(sitemap, f"{{{namespace}}}url")
|
||||
ET.SubElement(
|
||||
entry, f"{{{namespace}}}loc"
|
||||
).text = f"{SITE_ORIGIN}/{quote(route, safe='/')}"
|
||||
ET.indent(sitemap, space=" ")
|
||||
ET.ElementTree(sitemap).write(
|
||||
site / "sitemap.xml", encoding="utf-8", xml_declaration=True
|
||||
)
|
||||
print(f"Prepared sitemap.xml ({len(sitemap)} indexable pages).")
|
||||
|
||||
|
||||
def prepare_sharing_image(site):
|
||||
"""Rasterize the maintained SVG for clients that need a PNG preview."""
|
||||
try:
|
||||
import cairosvg
|
||||
except (ImportError, OSError) as error:
|
||||
sys.exit(
|
||||
f"Cannot prepare sharing image: {error}. Install website/requirements.txt and native Cairo; see website/README.md."
|
||||
)
|
||||
source = site / "assets/social-card.svg"
|
||||
destination = site / "assets/social-card.png"
|
||||
cairosvg.svg2png(
|
||||
url=str(source), write_to=str(destination), output_width=1200, output_height=630
|
||||
)
|
||||
print("Prepared assets/social-card.png (1200 × 630).")
|
||||
|
||||
|
||||
def prepare_notes_example(site):
|
||||
"""Create a ZIP with public source only and reproducible metadata."""
|
||||
source = site / "examples/notes-electron"
|
||||
@@ -37,4 +107,7 @@ def prepare_notes_example(site):
|
||||
|
||||
if __name__ == "__main__":
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
prepare_notes_example(root / "website/public")
|
||||
site = root / "website/public"
|
||||
prepare_sharing_image(site)
|
||||
prepare_sitemap(site)
|
||||
prepare_notes_example(site)
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Exercise automatic route discovery and mistakes that break search metadata."""
|
||||
|
||||
import contextlib
|
||||
import importlib.util
|
||||
import io
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import tempfile
|
||||
import unittest
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def load_script(name):
|
||||
"""Load a repository script without executing its command-line entrypoint."""
|
||||
spec = importlib.util.spec_from_file_location(name, ROOT / "scripts" / f"{name}.py")
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
PREPARE = load_script("prepare-website")
|
||||
VERIFY = load_script("verify-website")
|
||||
|
||||
|
||||
class WebsiteSearchChecks(unittest.TestCase):
|
||||
"""Use authored pages and temporary fixtures to check the publishing boundary."""
|
||||
|
||||
def test_sitemap_tracks_added_moved_removed_and_nonindexable_pages(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory)
|
||||
|
||||
def page(name, head="", body=""):
|
||||
path = site / name
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_text(f"<html><head>{head}</head><body>{body}</body></html>")
|
||||
return path
|
||||
|
||||
page("index.html")
|
||||
nested = page("guides/new/index.html")
|
||||
page(
|
||||
"examples/fixture/index.html",
|
||||
'<meta name="robots" content="NOINDEX, follow">',
|
||||
)
|
||||
page("draft.html", '<meta name="googlebot" content="none">')
|
||||
page("code & notes.html")
|
||||
page("visible/index.html", body='<meta name="robots" content="noindex">')
|
||||
(site / "asset.json").write_text("{}")
|
||||
|
||||
def urls():
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
PREPARE.prepare_sitemap(site)
|
||||
sitemap = ET.parse(site / "sitemap.xml")
|
||||
return {node.text for node in sitemap.findall(".//{*}loc")}
|
||||
|
||||
self.assertEqual(
|
||||
urls(),
|
||||
{
|
||||
"https://rea.tools/",
|
||||
"https://rea.tools/guides/new/",
|
||||
"https://rea.tools/code%20%26%20notes.html",
|
||||
"https://rea.tools/visible/",
|
||||
},
|
||||
)
|
||||
nested.rename(site / "moved.html")
|
||||
self.assertIn("https://rea.tools/moved.html", urls())
|
||||
self.assertNotIn("https://rea.tools/guides/new/", urls())
|
||||
(site / "moved.html").unlink()
|
||||
self.assertNotIn("https://rea.tools/moved.html", urls())
|
||||
|
||||
def test_verifier_rejects_wrong_canonicals_stale_sitemaps_and_missing_previews(
|
||||
self,
|
||||
):
|
||||
cases = (
|
||||
(
|
||||
"index.html",
|
||||
'rel="canonical" href="https://rea.tools/"',
|
||||
'rel="canonical" href="https://morluto.github.io/rea/"',
|
||||
"head canonical",
|
||||
),
|
||||
(
|
||||
"showcase/aegis/index.html",
|
||||
'rel="canonical" href="https://rea.tools/showcase/aegis/"',
|
||||
'rel="canonical" href="https://rea.tools/"',
|
||||
"head canonical",
|
||||
),
|
||||
(
|
||||
"examples/notes-electron/index.html",
|
||||
'content="noindex"',
|
||||
'content="index"',
|
||||
"must have a head noindex",
|
||||
),
|
||||
(
|
||||
"sitemap.xml",
|
||||
"https://rea.tools/faq/",
|
||||
"https://rea.tools/deleted/",
|
||||
"each indexable page once",
|
||||
),
|
||||
(
|
||||
"faq/index.html",
|
||||
'name="twitter:card" content="summary_large_image"',
|
||||
'name="twitter:card" content="summary"',
|
||||
"twitter:card",
|
||||
),
|
||||
)
|
||||
for name, old, new, message in cases:
|
||||
with self.subTest(name=name), tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory) / "public"
|
||||
shutil.copytree(ROOT / "website/public", site)
|
||||
path = site / name
|
||||
source = path.read_text()
|
||||
self.assertIn(old, source)
|
||||
path.write_text(source.replace(old, new, 1))
|
||||
self.assertTrue(
|
||||
any(message in error for error in VERIFY.check_site(site)[3])
|
||||
)
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
site = Path(directory) / "public"
|
||||
shutil.copytree(ROOT / "website/public", site)
|
||||
(site / "assets/social-card.png").unlink()
|
||||
self.assertIn(
|
||||
"Social preview PNG is missing; run scripts/prepare-website.py",
|
||||
VERIFY.check_site(site)[3],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
@@ -232,12 +233,12 @@ const checkHardenedRuntime = async (directory, binary) => {
|
||||
{ timeout: CLI_TIMEOUT_MS },
|
||||
);
|
||||
assert.equal(deniedMcp.isError, true, JSON.stringify(deniedMcp));
|
||||
assert.equal(deniedMcp.structuredContent.error.code, denied.code);
|
||||
assert.equal(requireMcpToolError(deniedMcp).code, denied.code);
|
||||
assert.match(
|
||||
deniedMcp.structuredContent.error.details.reason,
|
||||
requireMcpToolError(deniedMcp).details.reason,
|
||||
/^debugger-attach-denied: /u,
|
||||
);
|
||||
assert.ok(deniedMcp.structuredContent.error.details.partial_observation);
|
||||
assert.ok(requireMcpToolError(deniedMcp).details.partial_observation);
|
||||
});
|
||||
const allowed = join(directory, "debuggable");
|
||||
const entitlements = join(directory, "get-task-allow.plist");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { join } from "node:path";
|
||||
import { execFile } from "node:child_process";
|
||||
@@ -50,7 +51,7 @@ const verifyMcpUnknownProvider = async (client, mcpOptions) => {
|
||||
);
|
||||
if (
|
||||
unknownProvider.isError !== true ||
|
||||
unknownProvider.structuredContent?.error?.details?.selection_reason !==
|
||||
requireMcpToolError(unknownProvider)?.details?.selection_reason !==
|
||||
"unknown_provider"
|
||||
)
|
||||
throw new Error("packaged MCP accepted an unknown analysis provider");
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
listBrowserTargetsInputSchema,
|
||||
} from "../dist/domain/browserObservation.js";
|
||||
import { observeWebSessionInputSchema } from "../dist/domain/browserSession.js";
|
||||
import { compareWebCapturesInputSchema } from "../dist/domain/webCaptureDiff.js";
|
||||
import { compareWebCapturesInputSchema } from "../dist/domain/webCaptureDiffSchemas.js";
|
||||
import {
|
||||
captureWebScreenshotInputSchema,
|
||||
compareWebScreenshotsInputSchema,
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
browserScenario,
|
||||
runScenarioCli,
|
||||
scenarioProfiles,
|
||||
verifyScenarioFailureEvidence,
|
||||
} from "./lib/browser-scenario-verifier.mjs";
|
||||
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
|
||||
import { verifyLargeScreenshotE2e } from "./lib/browser-screenshot-e2e.mjs";
|
||||
@@ -294,6 +295,10 @@ try {
|
||||
if ([...profilesAfter].some((entry) => !profilesBefore.has(entry)))
|
||||
throw new Error("Scenario launch retained a temporary browser profile");
|
||||
assertScenarioCapture(launchedScenario.value);
|
||||
const scenarioFailure = await verifyScenarioFailureEvidence(
|
||||
executable,
|
||||
site.origin,
|
||||
);
|
||||
const scenarioResults = JSON.stringify([
|
||||
attachedScenario,
|
||||
launchedScenario.value,
|
||||
@@ -360,6 +365,7 @@ try {
|
||||
browserScenarioCli: true,
|
||||
browserScenarioAttachCleanup: "disconnected-external",
|
||||
browserScenarioLaunchCleanup: "terminated-owned-process",
|
||||
scenarioFailure,
|
||||
verified: true,
|
||||
};
|
||||
} finally {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { hasTypedSwitchEvidence } from "./verify-real-ghidra-switch-assertions.m
|
||||
import { access } from "node:fs/promises";
|
||||
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||
|
||||
export function assertLocalProcedureInfo(info, expectedAddress) {
|
||||
|
||||
@@ -16,12 +16,10 @@ import {
|
||||
parseGhidraInventoryInput,
|
||||
parseGhidraInventoryResult,
|
||||
} from "../dist/ghidra/GhidraInventoryValues.js";
|
||||
import {
|
||||
inspectGhidraInstallation,
|
||||
SUPPORTED_GHIDRA_VERSION,
|
||||
} from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
|
||||
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
|
||||
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
|
||||
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import { windowsP0Capabilities } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
|
||||
@@ -10,12 +10,10 @@ import { promisify } from "node:util";
|
||||
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { resolveGhidraAnalysisProfile } from "../dist/ghidra/GhidraAnalysisProfile.js";
|
||||
import { GhidraClient } from "../dist/ghidra/GhidraClient.js";
|
||||
import {
|
||||
inspectGhidraInstallation,
|
||||
SUPPORTED_GHIDRA_VERSION,
|
||||
} from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { inspectGhidraInstallation } from "../dist/ghidra/GhidraInstallation.js";
|
||||
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallationPolicy.js";
|
||||
import { GhidraHeadlessLauncher } from "../dist/ghidra/GhidraLauncher.js";
|
||||
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProvider.js";
|
||||
import { GHIDRA_PROVIDER_IDENTITY } from "../dist/ghidra/GhidraProviderCapabilities.js";
|
||||
|
||||
import {
|
||||
assertCleanup,
|
||||
|
||||
@@ -9,7 +9,7 @@ import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
|
||||
import { resolveHopperAnalysisProfile } from "../dist/hopper/HopperAnalysisProfile.js";
|
||||
import { HopperApplicationLauncher } from "../dist/hopper/BridgeLauncher.js";
|
||||
import { HopperClient } from "../dist/hopper/HopperClient.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProvider.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";
|
||||
import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs";
|
||||
|
||||
// These options belong to the native client, below the CLI/MCP tool surface.
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
} from "./lib/mcp-verifier-results.mjs";
|
||||
import { snapshotHopperRuntime } from "./lib/real-hopper-cleanup.mjs";
|
||||
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/hopper/HopperTargetLease.js";
|
||||
import { parseConfig } from "../dist/config.js";
|
||||
import { parseConfig } from "../dist/config/parseConfig.js";
|
||||
import {
|
||||
verifyHopperSourceBinding,
|
||||
relocateSingleFatSlice,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
requireFunctionDossier,
|
||||
requirePseudocode,
|
||||
} from "../dist/application/RealHopperAssertions.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProvider.js";
|
||||
import { HOPPER_PROVIDER_IDENTITY } from "../dist/hopper/HopperProviderCapabilities.js";
|
||||
import { HOPPER_TARGET_LEASE_DIRECTORY } from "../dist/hopper/HopperTargetLease.js";
|
||||
import { REA_WORKFLOW_PROVIDER } from "../dist/application/InvestigationProviders.js";
|
||||
import { loadRealHopperFixtureTargets } from "./lib/real-hopper-fixture.mjs";
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
#!/usr/bin/env node
|
||||
import { requireMcpToolError } from "./lib/mcp-verifier-results.mjs";
|
||||
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile } from "node:child_process";
|
||||
@@ -172,7 +173,7 @@ const mcp = async (cliDossier) => {
|
||||
arguments: { address: "not-an-address" },
|
||||
});
|
||||
assert.equal(invalid.isError, true);
|
||||
assert.equal(invalid.structuredContent.error.code, "invalid_request");
|
||||
assert.equal(requireMcpToolError(invalid).code, "invalid_request");
|
||||
await call("close_binary", {});
|
||||
opened = false;
|
||||
report.checks.push(
|
||||
|
||||
+219
-10
@@ -1,14 +1,19 @@
|
||||
"""Check local website references and the sole GitHub Pages publisher."""
|
||||
"""Check website references, search metadata and the sole production publisher."""
|
||||
|
||||
from html.parser import HTMLParser
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
from urllib.parse import unquote, urlsplit
|
||||
from urllib.parse import quote, unquote, urlsplit
|
||||
import xml.etree.ElementTree as ET
|
||||
from zipfile import BadZipFile, ZipFile
|
||||
|
||||
|
||||
SITE_ORIGIN = "https://rea.tools"
|
||||
NOTES_FIXTURES = {"examples/notes-electron/index.html", "examples/notes-web/index.html"}
|
||||
|
||||
|
||||
class HtmlReferences(HTMLParser):
|
||||
"""Collect document IDs and references without fetching external URLs."""
|
||||
|
||||
@@ -17,9 +22,42 @@ class HtmlReferences(HTMLParser):
|
||||
self.ids = set()
|
||||
self.references = []
|
||||
self.duplicate_ids = []
|
||||
self.in_head = False
|
||||
self.in_title = False
|
||||
self.titles = []
|
||||
self.meta = {}
|
||||
self.properties = {}
|
||||
self.canonicals = []
|
||||
self.lang = None
|
||||
self.h1_count = 0
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
attributes = dict(attrs)
|
||||
if tag == "html":
|
||||
self.lang = attributes.get("lang")
|
||||
if tag == "h1":
|
||||
self.h1_count += 1
|
||||
if tag == "head":
|
||||
self.in_head = True
|
||||
if self.in_head:
|
||||
if tag == "title":
|
||||
self.titles.append("")
|
||||
self.in_title = True
|
||||
if (
|
||||
tag == "link"
|
||||
and "canonical" in (attributes.get("rel") or "").lower().split()
|
||||
):
|
||||
self.canonicals.append(attributes.get("href") or "")
|
||||
if tag == "meta":
|
||||
for attribute, collection in (
|
||||
("name", self.meta),
|
||||
("property", self.properties),
|
||||
):
|
||||
key = attributes.get(attribute)
|
||||
if key:
|
||||
collection.setdefault(key.lower(), []).append(
|
||||
attributes.get("content") or ""
|
||||
)
|
||||
identifier = attributes.get("id")
|
||||
if identifier is not None:
|
||||
if identifier in self.ids:
|
||||
@@ -30,6 +68,154 @@ class HtmlReferences(HTMLParser):
|
||||
if reference is not None:
|
||||
self.references.append(reference)
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag == "head":
|
||||
self.in_head = False
|
||||
if tag == "title":
|
||||
self.in_title = False
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.in_title:
|
||||
self.titles[-1] += data
|
||||
|
||||
def noindex(self):
|
||||
"""Recognize directives that exclude this page from Google's index."""
|
||||
directives = ",".join(
|
||||
self.meta.get("robots", []) + self.meta.get("googlebot", [])
|
||||
).lower()
|
||||
return bool(set(re.split(r"[\s,]+", directives)) & {"noindex", "none"})
|
||||
|
||||
|
||||
def check_search_metadata(site, pages):
|
||||
"""Verify route-specific canonicals, sharing assets and the generated sitemap."""
|
||||
errors = []
|
||||
indexable = set()
|
||||
titles = set()
|
||||
for path, page in pages.items():
|
||||
label = path.relative_to(site).as_posix()
|
||||
title = page.titles[0].strip() if len(page.titles) == 1 else ""
|
||||
if not title:
|
||||
errors.append(f"{label}: needs exactly one nonempty head title")
|
||||
if page.lang != "en" or page.h1_count != 1:
|
||||
errors.append(f"{label}: needs lang=en and exactly one h1")
|
||||
if label in NOTES_FIXTURES and not page.noindex():
|
||||
errors.append(
|
||||
f"{label}: example application must have a head noindex directive"
|
||||
)
|
||||
if page.noindex():
|
||||
continue
|
||||
route = (
|
||||
path.relative_to(site).parent.as_posix()
|
||||
if path.name == "index.html"
|
||||
else label
|
||||
)
|
||||
route = "" if route == "." else route
|
||||
url = f"{SITE_ORIGIN}/{quote(route, safe='/')}" + (
|
||||
"/" if route and path.name == "index.html" else ""
|
||||
)
|
||||
indexable.add(url)
|
||||
if page.canonicals != [url]:
|
||||
errors.append(f"{label}: needs exactly one head canonical for {url}")
|
||||
if title in titles:
|
||||
errors.append(f"{label}: duplicate search title {title!r}")
|
||||
titles.add(title)
|
||||
descriptions = page.meta.get("description", [])
|
||||
description = descriptions[0].strip() if len(descriptions) == 1 else ""
|
||||
if not description:
|
||||
errors.append(f"{label}: needs exactly one nonempty head description")
|
||||
expected_og = {
|
||||
"og:type": "website",
|
||||
"og:site_name": "REA",
|
||||
"og:title": title,
|
||||
"og:description": description,
|
||||
"og:url": url,
|
||||
"og:image": f"{SITE_ORIGIN}/assets/social-card.png",
|
||||
"og:image:type": "image/png",
|
||||
"og:image:width": "1200",
|
||||
"og:image:height": "630",
|
||||
}
|
||||
expected_twitter = {
|
||||
"twitter:card": "summary_large_image",
|
||||
"twitter:title": title,
|
||||
"twitter:description": description,
|
||||
"twitter:image": expected_og["og:image"],
|
||||
}
|
||||
for collection, expected in (
|
||||
(page.properties, expected_og),
|
||||
(page.meta, expected_twitter),
|
||||
):
|
||||
for key, value in expected.items():
|
||||
if collection.get(key) != [value]:
|
||||
errors.append(
|
||||
f"{label}: {key} must appear once and match {value!r}"
|
||||
)
|
||||
for collection, key in (
|
||||
(page.properties, "og:image:alt"),
|
||||
(page.meta, "twitter:image:alt"),
|
||||
):
|
||||
values = collection.get(key, [])
|
||||
if len(values) != 1 or not values[0].strip():
|
||||
errors.append(f"{label}: needs exactly one nonempty {key}")
|
||||
|
||||
sitemap_path = site / "sitemap.xml"
|
||||
try:
|
||||
sitemap = ET.parse(sitemap_path).getroot()
|
||||
namespace = "{http://www.sitemaps.org/schemas/sitemap/0.9}"
|
||||
urls = [entry.findtext(f"{namespace}loc") for entry in sitemap]
|
||||
if sitemap.tag != f"{namespace}urlset" or any(
|
||||
entry.tag != f"{namespace}url" for entry in sitemap
|
||||
):
|
||||
errors.append("sitemap.xml: needs a sitemap-protocol urlset")
|
||||
if len(urls) != len(set(urls)) or set(urls) != indexable:
|
||||
errors.append(
|
||||
"sitemap.xml: must contain each indexable page once, using its rea.tools URL"
|
||||
)
|
||||
if any(
|
||||
len(entry) != 1 or entry[0].tag != f"{namespace}loc" for entry in sitemap
|
||||
):
|
||||
errors.append(
|
||||
"sitemap.xml: keep entries to loc; do not invent lastmod or add priority/changefreq"
|
||||
)
|
||||
except (OSError, ET.ParseError) as error:
|
||||
errors.append(
|
||||
f"sitemap.xml: cannot read sitemap ({error}); run scripts/prepare-website.py"
|
||||
)
|
||||
|
||||
robots_path = site / "robots.txt"
|
||||
if not robots_path.is_file():
|
||||
errors.append("robots.txt: authored crawl rules are missing")
|
||||
else:
|
||||
directives = []
|
||||
for line in robots_path.read_text(encoding="utf-8").splitlines():
|
||||
value = line.split("#", 1)[0].strip()
|
||||
if value:
|
||||
key, separator, content = value.partition(":")
|
||||
directives.append(
|
||||
(key.lower(), content.strip()) if separator else (value, "")
|
||||
)
|
||||
expected = [
|
||||
("user-agent", "*"),
|
||||
("allow", "/"),
|
||||
("sitemap", f"{SITE_ORIGIN}/sitemap.xml"),
|
||||
]
|
||||
if directives != expected:
|
||||
errors.append(
|
||||
"robots.txt: expected crawl access and one absolute rea.tools sitemap directive"
|
||||
)
|
||||
|
||||
image = site / "assets/social-card.png"
|
||||
if not image.is_file():
|
||||
errors.append("Social preview PNG is missing; run scripts/prepare-website.py")
|
||||
else:
|
||||
header = image.read_bytes()[:24]
|
||||
if (
|
||||
len(header) != 24
|
||||
or header[:16] != b"\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR"
|
||||
or struct.unpack(">II", header[16:24]) != (1200, 630)
|
||||
):
|
||||
errors.append("Social preview must be a 1200 × 630 PNG")
|
||||
return len(indexable), errors
|
||||
|
||||
|
||||
def check_site(site):
|
||||
"""Check assets, HTML fragment destinations and SVG syntax."""
|
||||
@@ -63,7 +249,11 @@ def check_site(site):
|
||||
target /= "index.html"
|
||||
if not target.is_file():
|
||||
errors.append(f"{label}: missing local target: {reference}")
|
||||
elif target in pages and url.fragment and unquote(url.fragment) not in pages[target].ids:
|
||||
elif (
|
||||
target in pages
|
||||
and url.fragment
|
||||
and unquote(url.fragment) not in pages[target].ids
|
||||
):
|
||||
errors.append(f"{label}: missing HTML fragment: {reference}")
|
||||
|
||||
for path in sorted(site.rglob("*.svg")):
|
||||
@@ -71,18 +261,23 @@ def check_site(site):
|
||||
ET.parse(path)
|
||||
except ET.ParseError as error:
|
||||
errors.append(f"{path.relative_to(site)}: invalid SVG XML: {error}")
|
||||
return len(pages), references, errors
|
||||
indexable, metadata_errors = check_search_metadata(site, pages)
|
||||
errors.extend(metadata_errors)
|
||||
return len(pages), references, indexable, errors
|
||||
|
||||
|
||||
def check_publisher(root):
|
||||
"""Detect a second Pages action that could replace the website."""
|
||||
expected = root / ".github/workflows/website-pages.yml"
|
||||
workflows = root / ".github/workflows"
|
||||
deploy_action = re.compile(r"^\s*-?\s*uses:\s*['\"]?actions/deploy-pages@", re.MULTILINE)
|
||||
deploy_action = re.compile(
|
||||
r"^\s*-?\s*uses:\s*['\"]?actions/deploy-pages@", re.MULTILINE
|
||||
)
|
||||
publishers = [
|
||||
path
|
||||
for path in sorted(workflows.iterdir())
|
||||
if path.suffix in (".yml", ".yaml") and deploy_action.search(path.read_text(encoding="utf-8"))
|
||||
if path.suffix in (".yml", ".yaml")
|
||||
and deploy_action.search(path.read_text(encoding="utf-8"))
|
||||
]
|
||||
if publishers != [expected]:
|
||||
actual = ", ".join(str(path.relative_to(root)) for path in publishers) or "none"
|
||||
@@ -95,7 +290,14 @@ def check_example_archive(site):
|
||||
source = site / "examples/notes-electron"
|
||||
expected = {
|
||||
f"notes-example/{name}": source / name
|
||||
for name in ("package.json", "main.js", "preload.js", "renderer.js", "csv.js", "index.html")
|
||||
for name in (
|
||||
"package.json",
|
||||
"main.js",
|
||||
"preload.js",
|
||||
"renderer.js",
|
||||
"csv.js",
|
||||
"index.html",
|
||||
)
|
||||
}
|
||||
path = site / "examples/notes-example.zip"
|
||||
if not path.is_file():
|
||||
@@ -103,7 +305,9 @@ def check_example_archive(site):
|
||||
try:
|
||||
with ZipFile(path) as archive:
|
||||
if sorted(archive.namelist()) != sorted(expected):
|
||||
return ["Notes example ZIP must contain exactly the six files in notes-example/."]
|
||||
return [
|
||||
"Notes example ZIP must contain exactly the six files in notes-example/."
|
||||
]
|
||||
return [
|
||||
f"Notes example ZIP has stale source: {name}"
|
||||
for name, original in expected.items()
|
||||
@@ -116,13 +320,18 @@ def check_example_archive(site):
|
||||
def main():
|
||||
"""Run the same checks locally, on website PRs and before publishing."""
|
||||
root = Path(__file__).resolve().parent.parent
|
||||
pages, references, errors = check_site(root / "website/public")
|
||||
pages, references, indexable, errors = check_site(root / "website/public")
|
||||
errors.extend(check_example_archive(root / "website/public"))
|
||||
errors.extend(check_publisher(root))
|
||||
if errors:
|
||||
print("Website checks failed:\n" + "\n".join(errors), file=sys.stderr)
|
||||
return 1
|
||||
print(f"Verified {pages} HTML pages, {references} local references, SVG XML and the example ZIP.")
|
||||
print(
|
||||
f"Verified {pages} HTML pages, {references} local references, SVG XML and the example ZIP."
|
||||
)
|
||||
print(
|
||||
f"Verified search/sharing metadata, robots.txt and the sitemap for {indexable} indexable pages."
|
||||
)
|
||||
print("website-pages.yml is the sole GitHub Pages publisher.")
|
||||
return 0
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
access,
|
||||
@@ -208,13 +209,12 @@ export async function verifyAndroidFailureRecovery({
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
assert.equal(response.isError, true);
|
||||
assert.equal(response.structuredContent.error.code, "invalid_request");
|
||||
assert.deepEqual(
|
||||
response.structuredContent.error.details.issues[0].path,
|
||||
["class_name"],
|
||||
);
|
||||
assert.equal(requireMcpToolError(response).code, "invalid_request");
|
||||
assert.deepEqual(requireMcpToolError(response).details.issues[0].path, [
|
||||
"class_name",
|
||||
]);
|
||||
assert.match(
|
||||
response.structuredContent.error.details.issues[0].message,
|
||||
requireMcpToolError(response).details.issues[0].message,
|
||||
/search_android_classes/u,
|
||||
);
|
||||
assert.doesNotMatch(JSON.stringify(response), /rea doctor/u);
|
||||
@@ -231,8 +231,8 @@ export async function verifyAndroidFailureRecovery({
|
||||
},
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
assert.equal(method.structuredContent.error.code, "invalid_request");
|
||||
assert.deepEqual(method.structuredContent.error.details.issues[0].path, [
|
||||
assert.equal(requireMcpToolError(method).code, "invalid_request");
|
||||
assert.deepEqual(requireMcpToolError(method).details.issues[0].path, [
|
||||
"method_name",
|
||||
]);
|
||||
await client.ping();
|
||||
|
||||
@@ -0,0 +1,86 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { chmod, readFile, stat } from "node:fs/promises";
|
||||
import { relative, sep } from "node:path";
|
||||
|
||||
import { parseArtifactInventoryEvidence } from "../../../dist/domain/artifactInventoryEvidence.js";
|
||||
import {
|
||||
artifactCliEvidence,
|
||||
withArtifactMcp,
|
||||
} from "../../lib/artifact-e2e.mjs";
|
||||
|
||||
/** Verify native file roles and permissions on the real compiled bundle fixture. */
|
||||
export async function verifyMachOOccurrences(app, executable) {
|
||||
const originalMode = (await stat(executable)).mode & 0o7777;
|
||||
const originalBytes = await readFile(executable);
|
||||
const logicalPath = relative(app, executable).split(sep).join("/");
|
||||
const observations = [];
|
||||
try {
|
||||
for (const mode of [0o755, 0o644]) {
|
||||
await chmod(executable, mode);
|
||||
const directEvidence = await artifactCliEvidence(
|
||||
"inspect-artifact",
|
||||
executable,
|
||||
);
|
||||
const direct = parseArtifactInventoryEvidence(directEvidence).inventory;
|
||||
const containedEvidence = await artifactCliEvidence(
|
||||
"inspect-artifact",
|
||||
app,
|
||||
);
|
||||
const contained =
|
||||
parseArtifactInventoryEvidence(containedEvidence).inventory;
|
||||
assert.equal(direct.complete, true);
|
||||
assert.equal(contained.complete, true);
|
||||
const root = direct.occurrences.find(
|
||||
({ logical_path }) => logical_path === ".",
|
||||
);
|
||||
const nested = contained.occurrences.find(
|
||||
({ logical_path }) => logical_path === logicalPath,
|
||||
);
|
||||
assert.ok(root);
|
||||
assert.ok(nested);
|
||||
assert.ok(["mach-o", "mach-o-universal"].includes(root.artifact_format));
|
||||
for (const occurrence of [root, nested]) {
|
||||
assert.equal(occurrence.artifact_kind, "executable");
|
||||
assert.equal(occurrence.artifact_format, direct.manifest.root_format);
|
||||
assert.equal(occurrence.executable, mode === 0o755);
|
||||
}
|
||||
assert.equal(root.artifact_id, nested.artifact_id);
|
||||
assert.equal(root.artifact_id, direct.manifest.root_artifact_id);
|
||||
const node = contained.nodes.find(
|
||||
({ artifact_id }) => artifact_id === nested.artifact_id,
|
||||
);
|
||||
assert.ok(node);
|
||||
assert.equal(node.sha256, direct.manifest.root_sha256);
|
||||
assert.equal(node.format, root.artifact_format);
|
||||
await withArtifactMcp(app, async (client) => {
|
||||
const reply = await client.callTool({
|
||||
name: "inspect_artifact",
|
||||
arguments: {},
|
||||
});
|
||||
assert.notEqual(reply.isError, true, JSON.stringify(reply));
|
||||
assert.deepEqual(
|
||||
parseArtifactInventoryEvidence(reply.structuredContent).inventory,
|
||||
contained,
|
||||
);
|
||||
});
|
||||
observations.push({
|
||||
mode: mode.toString(8),
|
||||
artifact_id: root.artifact_id,
|
||||
artifact_kind: root.artifact_kind,
|
||||
artifact_format: root.artifact_format,
|
||||
executable: root.executable,
|
||||
contained_cli_mcp_parity: true,
|
||||
});
|
||||
}
|
||||
assert.equal(observations[0].artifact_id, observations[1].artifact_id);
|
||||
assert.deepEqual(await readFile(executable), originalBytes);
|
||||
return {
|
||||
direct_cli: true,
|
||||
contained_cli_mcp: true,
|
||||
bytes_unchanged: true,
|
||||
observations,
|
||||
};
|
||||
} finally {
|
||||
await chmod(executable, originalMode);
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
} from "../../fixtures/apple/macos-bundle.mjs";
|
||||
import { verifyDyldEnvironment } from "./dyld-environment.mjs";
|
||||
import { verifyDylibResolution } from "./macos-dylib-e2e.mjs";
|
||||
import { verifyMachOOccurrences } from "./macho-occurrences.mjs";
|
||||
|
||||
const exec = promisify(execFile);
|
||||
|
||||
@@ -67,7 +68,8 @@ const EXPECTED_BUNDLES = [
|
||||
|
||||
let report;
|
||||
try {
|
||||
const { app } = await buildMacosBundleFixture(root);
|
||||
const { app, executable } = await buildMacosBundleFixture(root);
|
||||
const machoOccurrences = await verifyMachOOccurrences(app, executable);
|
||||
const zip = join(root, "MacFixture.zip");
|
||||
await exec("/usr/bin/ditto", ["-c", "-k", "--keepParent", app, zip]);
|
||||
const staging = join(root, "dmg-source");
|
||||
@@ -164,6 +166,7 @@ try {
|
||||
containers: containers.map(({ format }) => format),
|
||||
bundles: EXPECTED_BUNDLES.length,
|
||||
dmg_detached: true,
|
||||
macho_occurrences: machoOccurrences,
|
||||
dylib_resolution: dylibs,
|
||||
dyld_environment: dyldEnvironment,
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { requireMcpToolError } from "../../lib/mcp-verifier-results.mjs";
|
||||
import assert from "node:assert/strict";
|
||||
import { execFile, spawn } from "node:child_process";
|
||||
import { randomUUID } from "node:crypto";
|
||||
@@ -172,11 +173,11 @@ window.loadFile("renderer.html");require("node:fs").writeFileSync(${JSON.stringi
|
||||
{ timeout: 150_000 },
|
||||
);
|
||||
const capture = active.isError
|
||||
? active.structuredContent.error.details.partial_observation.capture
|
||||
? requireMcpToolError(active).details.partial_observation.capture
|
||||
: active.structuredContent.normalized_result;
|
||||
if (active.isError) {
|
||||
assert.equal(
|
||||
active.structuredContent.error.code,
|
||||
requireMcpToolError(active).code,
|
||||
"cleanup_incomplete",
|
||||
JSON.stringify(active),
|
||||
);
|
||||
|
||||
+2
-2
@@ -8,13 +8,13 @@
|
||||
"url": "https://github.com/morluto/rea",
|
||||
"source": "github"
|
||||
},
|
||||
"version": "6.1.0",
|
||||
"version": "6.2.0",
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "npm",
|
||||
"registryBaseUrl": "https://registry.npmjs.org",
|
||||
"identifier": "rea-agents",
|
||||
"version": "6.1.0",
|
||||
"version": "6.2.0",
|
||||
"runtimeHint": "npx",
|
||||
"transport": {
|
||||
"type": "stdio"
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { AndroidRequest } from "../domain/android/androidAnalysis.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { PrivateRuntimeRoot } from "../process/PrivateRuntimeRoot.js";
|
||||
import {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
AnalysisInputError,
|
||||
AnalysisOutputError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
|
||||
import type { OwnedProviderProcessSpawnOptions } from "../process/ProviderProcess.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import type { OfficialToolName } from "../contracts/officialToolContracts.js";
|
||||
import type { EnhancedToolName } from "../contracts/enhancedInputs.js";
|
||||
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
|
||||
|
||||
@@ -7,11 +7,13 @@ import {
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
|
||||
import type {
|
||||
BrowserTargetList,
|
||||
InspectWebPageInput,
|
||||
ListBrowserTargetsInput,
|
||||
WebPageInspection,
|
||||
} from "../domain/browserObservation.js";
|
||||
import type {
|
||||
BrowserTargetList,
|
||||
WebPageInspection,
|
||||
} from "../domain/browserObservationSchemas.js";
|
||||
import type {
|
||||
AnalyzeWebBundleInput,
|
||||
WebBundleAnalysis,
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import type {
|
||||
BrowserTargetList,
|
||||
InspectWebPageInput,
|
||||
ListBrowserTargetsInput,
|
||||
WebPageInspection,
|
||||
} from "../domain/browserObservation.js";
|
||||
import type {
|
||||
BrowserTargetList,
|
||||
WebPageInspection,
|
||||
} from "../domain/browserObservationSchemas.js";
|
||||
import type { Result } from "../domain/result.js";
|
||||
import type {
|
||||
AnalyzeWebBundleInput,
|
||||
|
||||
@@ -6,7 +6,8 @@ import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js
|
||||
import type { BinarySession } from "./binary/BinarySession.js";
|
||||
import type { SessionProviderRoute } from "./binary/SessionProviderRouter.js";
|
||||
import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js";
|
||||
import { silentLogger, type Logger } from "../logger.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import type { Logger } from "pino";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import type { Evidence } from "../domain/evidence.js";
|
||||
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
|
||||
@@ -24,7 +25,7 @@ import {
|
||||
writeAnalysisSnapshot,
|
||||
} from "./binary/AnalysisSnapshotFiles.js";
|
||||
import { parseBinaryTarget } from "./BinaryTargetResolver.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
snapshotEvidenceForQuery,
|
||||
snapshotMatchesTarget,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { AppConfig, parseConfig } from "../config.js";
|
||||
import type { Logger } from "../logger.js";
|
||||
import type { AppConfig } from "../config/types.js";
|
||||
import type { parseConfig } from "../config/parseConfig.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { BinarySession } from "./binary/BinarySession.js";
|
||||
|
||||
/** Configuration and one-shot session factories supplied by the production boundary. */
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
import { createServerIdentity } from "../serverIdentity.js";
|
||||
import { silentLogger, type Logger } from "../logger.js";
|
||||
import { silentLogger } from "../logger.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { DirectAnalysisDependencies } from "./DirectAnalysisDependencies.js";
|
||||
|
||||
const runSessionStatus = async (
|
||||
|
||||
@@ -42,8 +42,6 @@ import { traceCallPath } from "./CallPathTracing.js";
|
||||
import { resolveProcedureAddress } from "./ProcedureAddressResolution.js";
|
||||
import { traceLiteralFeature } from "./EnhancedLiteralTracing.js";
|
||||
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
|
||||
export type { ValidatedEnhancedCall } from "./EnhancedToolTypes.js";
|
||||
|
||||
/**
|
||||
* Composes direct provider operations into bounded reverse-engineering tools.
|
||||
* Direct callers use `execute`; adapters that own validation use
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
} from "../domain/evidenceErrors.js";
|
||||
import { err, ok, type Result } from "../domain/result.js";
|
||||
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCapture.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
|
||||
import {
|
||||
bufferedJsonParts,
|
||||
canonicalJsonParts,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ExecutionOptions,
|
||||
} from "./AnalysisProvider.js";
|
||||
import { toolContract } from "../contracts/toolContracts.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { parseFunctionDossier } from "../domain/hopperValues.js";
|
||||
import { jsonObjectSchema, type JsonValue } from "../domain/jsonValue.js";
|
||||
|
||||
@@ -3,7 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { createEvidence } from "../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { emptyUnverifiedProcessCapture } from "../domain/process/processCapture.fixture.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCapture.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
|
||||
import {
|
||||
digestProcessCommitment,
|
||||
parseProcessScenario,
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
parseEvidenceBundle,
|
||||
type EvidenceBundle,
|
||||
} from "../domain/evidenceBundle.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCapture.js";
|
||||
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
|
||||
import type {
|
||||
ReconstructionObligation,
|
||||
ReviewedReconstructionObligation,
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import { PROCESS_PROVIDER } from "./process/ProcessEvidence.js";
|
||||
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
|
||||
import {
|
||||
buildReconstructionObligationLedgerEvidenceValidated,
|
||||
resolveReconstructionObligationLedgerRequest,
|
||||
} from "./ReconstructionObligationLedgerService.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCapture.fixture.js";
|
||||
import { EMPTY_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCaptureExample.js";
|
||||
import { createEvidence, type Evidence } from "../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
|
||||
import { jsonValueSchema } from "../domain/jsonValue.js";
|
||||
import { processCaptureSchema } from "../domain/process/processCapture.js";
|
||||
import {
|
||||
reconstructionObligationLedgerSchema,
|
||||
type ReconstructionObligationLedgerInput,
|
||||
@@ -15,6 +14,7 @@ import {
|
||||
type ReviewedReconstructionObligation,
|
||||
} from "../domain/reconstructionObligationLedgerSchemas.js";
|
||||
|
||||
import { processCaptureSchema } from "../domain/process/processCapture.js";
|
||||
/** Build controlled fixture Evidence used by reconstruction-ledger scenarios. */
|
||||
export const proofEvidence = (id: string) =>
|
||||
createEvidence(
|
||||
|
||||
@@ -17,11 +17,6 @@ import {
|
||||
} from "./ReferenceSourceImportTypes.js";
|
||||
import { prepareReferenceSourceImport } from "./ReferenceSourceImportPolicy.js";
|
||||
|
||||
export type {
|
||||
ReferenceSourceImportError,
|
||||
ReferenceSourceImportOptions,
|
||||
} from "./ReferenceSourceImportTypes.js";
|
||||
|
||||
const failure = (
|
||||
code: ReferenceSourceImportError["code"],
|
||||
message: string,
|
||||
|
||||
@@ -10,7 +10,7 @@ import { parseReferenceSourceImports } from "../domain/referenceSourceImportPars
|
||||
import type {
|
||||
ReferenceSourceEntry,
|
||||
ReferenceSourceRead,
|
||||
} from "../reference/ReferenceSourceReader.js";
|
||||
} from "../reference/ReferenceSourceReaderTypes.js";
|
||||
import { PARSEABLE_REFERENCE_SOURCE_LANGUAGES } from "./ReferenceSourceImportTypes.js";
|
||||
|
||||
export interface ParsedReferenceSourceEntries {
|
||||
|
||||
@@ -40,7 +40,6 @@ describe("workflow residual question retention", () => {
|
||||
required_authority: "shipped-artifact",
|
||||
},
|
||||
]);
|
||||
expect(records.listUnknowns()).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("preserves registry failures rather than presenting a completed workflow", () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
analysisProfileSchema,
|
||||
committedProviderSchema,
|
||||
} from "../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
createEvidence,
|
||||
type Evidence,
|
||||
|
||||
@@ -3,7 +3,7 @@ import type {
|
||||
ExecutionOptions,
|
||||
} from "../AnalysisProvider.js";
|
||||
import type { AndroidRequest } from "../../domain/android/androidAnalysis.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import type { Result } from "../../domain/result.js";
|
||||
import type { ProviderAvailability } from "../AnalysisProvider.js";
|
||||
|
||||
@@ -2,7 +2,7 @@ import {
|
||||
analysisProfileSchema,
|
||||
type AnalysisProfileCommitment,
|
||||
} from "../../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
AnalysisCancelledError,
|
||||
AnalysisUnsupportedTargetError,
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
type ProviderTargetSupport,
|
||||
} from "../AnalysisProvider.js";
|
||||
import { createAnalysisProfile } from "../../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
|
||||
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
type AnalysisProviderSelector,
|
||||
} from "../../contracts/providerSelection.js";
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
analysisProfilesEqual,
|
||||
type AnalysisProfileCommitment,
|
||||
@@ -48,7 +48,6 @@ import {
|
||||
analysisErrorWithCleanupFailure,
|
||||
closeAnalysisClient,
|
||||
} from "./AnalysisClientCleanup.js";
|
||||
export type { BinarySessionPort } from "./BinarySessionPort.js";
|
||||
const OFFICIAL_OPERATIONS: ReadonlySet<string> = new Set(
|
||||
OFFICIAL_TOOL_CONTRACTS.map(({ name }) => name),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
||||
import { NoBinaryOpenError } from "../../domain/configurationErrors.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../../domain/analysisSnapshot.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import { parseBinaryTarget } from "../BinaryTargetResolver.js";
|
||||
import { type BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import { type BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import {
|
||||
EvidenceIntegrityError,
|
||||
AnalysisSnapshotMismatchError,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InvestigationRecordPort } from "../investigation/InvestigationRecordPort.js";
|
||||
import type { ExecutableFormatHint } from "../../domain/dosCom.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import type { AnalysisSnapshot } from "../../domain/analysisSnapshot.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { Evidence } from "../../domain/evidence.js";
|
||||
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { JsonValue } from "../../domain/jsonValue.js";
|
||||
import type { ProviderOperationHealth } from "../../domain/providerOperationHealth.js";
|
||||
import type {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
||||
import { err, ok } from "../../domain/result.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import type {
|
||||
AnalysisClient,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
|
||||
import { AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
import { ProviderAdapterError } from "../../domain/providerAdapterError.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { AnalysisProviderSelector } from "../../contracts/providerSelection.js";
|
||||
import type { AnalysisProfileCommitment } from "../../domain/analysisProfile.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { AnalysisCapabilityUnavailableError } from "../../domain/analysisErrorCore.js";
|
||||
import { ProviderSelectionError } from "../../domain/providerSelectionError.js";
|
||||
import { type AnalysisError } from "../../domain/analysisErrorBase.js";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { createEvidence } from "../../domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
import { recordUnknownInputSchema } from "../../domain/residualUnknown.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import type { EvidenceBundle } from "../../domain/evidenceBundle.js";
|
||||
import type { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { BinaryTarget } from "../../domain/binaryTarget.js";
|
||||
import type { BinaryTarget } from "../../domain/binaryTargetTypes.js";
|
||||
import { createEvidence, type Evidence } from "../../domain/evidence.js";
|
||||
import type {
|
||||
RecordUnknownInput,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import { sha256Text } from "../../domain/javascript/javascriptStaticAnalysisHelpers.js";
|
||||
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import type { ElectronSenderValidationFinding } from "../../domain/javascript/electronStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import type {
|
||||
ElectronBrowserWindowFinding,
|
||||
ElectronContextBridgeFinding,
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import {
|
||||
createJavaScriptApplicationEdge,
|
||||
createJavaScriptApplicationNode,
|
||||
type ApplicationEdge,
|
||||
type ApplicationNode,
|
||||
} from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type {
|
||||
ApplicationEdge,
|
||||
ApplicationNode,
|
||||
} from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
|
||||
/** Deduplicate graph entities while retaining distinct evidence observations. */
|
||||
export class JavaScriptArtifactGraphAccumulator {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
|
||||
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
||||
import {
|
||||
createJavaScriptApplicationGraph,
|
||||
createImmutableJavaScriptApplicationGraphSteps,
|
||||
@@ -36,10 +36,10 @@ import {
|
||||
import {
|
||||
addJavaScriptArtifactContainers,
|
||||
addJavaScriptArtifactFiles,
|
||||
addJavaScriptBundlerNodes,
|
||||
addJavaScriptPackageNodes,
|
||||
createJavaScriptArtifactRootNode,
|
||||
} from "./JavaScriptArtifactGraphStructure.js";
|
||||
import { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
|
||||
import { addElectronBoundaries } from "./ElectronBoundaryGraph.js";
|
||||
import {
|
||||
classifyElectronIpcPairings,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import type { JavaScriptBundlerRegistration } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
|
||||
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
||||
import {
|
||||
completeApplicationCoverage,
|
||||
partialApplicationCoverage,
|
||||
type ApplicationGraphEvidence,
|
||||
} from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import type {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import type { JavaScriptStaticAnalysis } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
import {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ArtifactInventorySnapshot } from "../../artifacts/inventory/ArtifactInventory.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { ArtifactInventorySnapshot } from "../../domain/artifactInventorySnapshot.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import { completeApplicationCoverage } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
import type { JavaScriptModuleArtifactAnalysis } from "./JavaScriptArtifactAnalysisTypes.js";
|
||||
import type {
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
staticInferenceEvidence,
|
||||
} from "./JavaScriptArtifactGraphEvidence.js";
|
||||
import { resolveArtifactPathByContext } from "./JavaScriptArtifactPathResolution.js";
|
||||
export { addJavaScriptBundlerNodes } from "./JavaScriptArtifactGraphBundlers.js";
|
||||
|
||||
interface PackageRoleInput {
|
||||
readonly packageNode: ApplicationNode;
|
||||
|
||||
@@ -5,7 +5,9 @@ import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptA
|
||||
import {
|
||||
admitsCanonicalPathSyntax,
|
||||
hasScheme,
|
||||
htmlUrlText,
|
||||
looksExternal,
|
||||
percentDecodeUrlPath,
|
||||
stripQueryAndFragment,
|
||||
} from "../../domain/artifactPathSyntax.js";
|
||||
|
||||
@@ -248,7 +250,7 @@ const hasContainerCandidate = (
|
||||
const htmlCandidate = (
|
||||
input: ResolveArtifactPathInput,
|
||||
): string | ArtifactPathResolution => {
|
||||
const declared = stripQueryAndFragment(input.declaredPath);
|
||||
const declared = stripQueryAndFragment(htmlUrlText(input.declaredPath));
|
||||
if (looksExternal(declared))
|
||||
return unresolvedOutcome(input, "external", [
|
||||
"External HTML references are not mapped to local artifact assets.",
|
||||
@@ -257,14 +259,25 @@ const htmlCandidate = (
|
||||
const base =
|
||||
rawBase === undefined || rawBase === null
|
||||
? rawBase
|
||||
: stripQueryAndFragment(rawBase);
|
||||
: stripQueryAndFragment(htmlUrlText(rawBase));
|
||||
if (base !== undefined && base !== null && looksExternal(base))
|
||||
return unresolvedOutcome(input, "external", [
|
||||
"The document base href is external, so its script reference is not a local artifact path.",
|
||||
]);
|
||||
if (declared.startsWith("/")) return declared.slice(1);
|
||||
// Removing URL tabs and newlines can join an encoded dot or separator that
|
||||
// the raw declaration split, so admit the parsed URL text again.
|
||||
if (!admitsCanonicalPathSyntax(declared))
|
||||
return unresolvedOutcome(input, "rejected", [
|
||||
"Encoded dot or separator bytes are rejected before artifact path resolution.",
|
||||
]);
|
||||
const declaredPath = percentDecodeUrlPath(declared);
|
||||
if (declaredPath === null)
|
||||
return unresolvedOutcome(input, "rejected", [
|
||||
"The HTML reference path percent-decodes to NUL or to bytes that are not UTF-8.",
|
||||
]);
|
||||
if (declaredPath.startsWith("/")) return declaredPath.slice(1);
|
||||
if (base === undefined || base === null || base === "")
|
||||
return posix.join(posix.dirname(input.sourcePath), declared);
|
||||
return posix.join(posix.dirname(input.sourcePath), declaredPath);
|
||||
// A local base href is a second untrusted path input; apply the same
|
||||
// admission rules a declared path gets so it cannot smuggle traversal or
|
||||
// separator syntax past canonicalization.
|
||||
@@ -272,10 +285,15 @@ const htmlCandidate = (
|
||||
return unresolvedOutcome(input, "rejected", [
|
||||
"The document base href uses NUL, backslash, or encoded dot and separator bytes that are not admitted for canonical artifact paths.",
|
||||
]);
|
||||
const basePath = base.startsWith("/")
|
||||
? base.slice(1)
|
||||
: posix.join(posix.dirname(input.sourcePath), base);
|
||||
return posix.join(htmlBaseDirectory(base, basePath), declared);
|
||||
const decodedBase = percentDecodeUrlPath(base);
|
||||
if (decodedBase === null)
|
||||
return unresolvedOutcome(input, "rejected", [
|
||||
"The document base href path percent-decodes to NUL or to bytes that are not UTF-8.",
|
||||
]);
|
||||
const basePath = decodedBase.startsWith("/")
|
||||
? decodedBase.slice(1)
|
||||
: posix.join(posix.dirname(input.sourcePath), decodedBase);
|
||||
return posix.join(htmlBaseDirectory(decodedBase, basePath), declaredPath);
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
javascriptArtifactReconstructionInputSchema,
|
||||
type JavaScriptArtifactReconstructionInput,
|
||||
} from "./JavaScriptArtifactReconstructionInput.js";
|
||||
import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/ArtifactInventory.js";
|
||||
import { scanCanonicalArtifactInventory } from "../../artifacts/inventory/scanCanonical.js";
|
||||
import { summarizeElectronBoundaries } from "./ElectronBoundaryAnalysis.js";
|
||||
import { createJavaScriptSemanticGraphProjection } from "./JavaScriptSemanticGraphBuilder.js";
|
||||
import type { ProgressReporter } from "../ProgressReporter.js";
|
||||
|
||||
@@ -109,3 +109,73 @@ describe("HTML base href path syntax", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
describe("HTML reference URL text", () => {
|
||||
const urlPath = (declared: string, base: string) =>
|
||||
decodeURIComponent(
|
||||
new URL(declared.trim(), new URL(base.trim(), documentUrl)).pathname,
|
||||
).replace(/^\/+/, "");
|
||||
it.each([
|
||||
["%61pp.js", "/assets/", "assets/app.js"],
|
||||
[" app.js ", "/assets/", "assets/app.js"],
|
||||
["\napp.js\t", "/assets/", "assets/app.js"],
|
||||
["a\npp.js", "/assets/", "assets/app.js"],
|
||||
["app.js", "%61/", "renderer/a/app.js"],
|
||||
["app.js", " /%61ssets/ ", "assets/app.js"],
|
||||
["/%61ssets/app.js", "", "assets/app.js"],
|
||||
])(
|
||||
"resolves %j against base %j like the URL parser",
|
||||
(declared, base, expected) => {
|
||||
if (base !== "") expect(urlPath(declared, base)).toBe(expected);
|
||||
expect(resolve(declared, base)).toMatchObject({
|
||||
declared_path: declared,
|
||||
resolution_status: "resolved",
|
||||
resolved_path: expected,
|
||||
});
|
||||
},
|
||||
);
|
||||
it("keeps a literal percent sign that starts no escape", () => {
|
||||
expect(resolve("100%.js", "")).toMatchObject({
|
||||
resolution_status: "not-found",
|
||||
limitations: [
|
||||
"The exact HTML resource renderer/100%.js was not found among the selected application files.",
|
||||
],
|
||||
});
|
||||
});
|
||||
it.each(["%00app.js", "%ff.js"])(
|
||||
"rejects %j, which decodes to no canonical artifact path",
|
||||
(declared) => {
|
||||
expect(resolve(declared, "")).toMatchObject({
|
||||
resolution_status: "rejected",
|
||||
resolved_path: null,
|
||||
});
|
||||
},
|
||||
);
|
||||
it("rejects a base href whose URL whitespace splits an encoded separator", () => {
|
||||
expect(resolve("app.js", "a%2\nf/")).toMatchObject({
|
||||
resolution_status: "rejected",
|
||||
resolved_path: null,
|
||||
});
|
||||
});
|
||||
it("rejects a base href that decodes to NUL", () => {
|
||||
expect(resolve("app.js", "/a%00/")).toMatchObject({
|
||||
resolution_status: "rejected",
|
||||
resolved_path: null,
|
||||
});
|
||||
});
|
||||
it.each([" https://external.test/app.js", "\t//external.test/app.js"])(
|
||||
"reports %j as external after removing URL whitespace",
|
||||
(declared) => {
|
||||
expect(resolve(declared, "")).toMatchObject({
|
||||
resolution_status: "external",
|
||||
});
|
||||
},
|
||||
);
|
||||
it.each(["%2e%2e/app.js", "a%2fapp.js", "%2\nfapp.js", "%2\ne%2\te/app.js"])(
|
||||
"still rejects encoded dot and separator bytes in %j",
|
||||
(declared) => {
|
||||
expect(resolve(declared, "")).toMatchObject({
|
||||
resolution_status: "rejected",
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { posix } from "node:path";
|
||||
|
||||
import { compareCodePoints } from "../../domain/canonicalOrdering.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraph.js";
|
||||
import type { ApplicationNode } from "../../domain/javascript/javascriptApplicationGraphSchemas.js";
|
||||
import {
|
||||
completeApplicationCoverage,
|
||||
partialApplicationCoverage,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import type {
|
||||
JavaScriptSemanticEventOperation,
|
||||
JavaScriptSemanticTimerOperation,
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
addSemanticGraphUnknown,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
semanticCallSiteAt,
|
||||
type SemanticFlowProjectionContext,
|
||||
} from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
|
||||
/** Project literal EventEmitter registrations, removals, and dispatches. */
|
||||
export const projectSemanticEvents = (
|
||||
@@ -175,7 +172,7 @@ const projectTimerSchedule = (
|
||||
timerNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
): void => {
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: semanticCallSiteAt(context, operation.location),
|
||||
source: context.callSiteAt(operation.location),
|
||||
target: timerNodes.get(operation.timerId),
|
||||
relation: "schedules-timer",
|
||||
resolution: "resolved",
|
||||
@@ -199,7 +196,7 @@ const projectTimerCancellation = (
|
||||
operation: JavaScriptSemanticTimerOperation,
|
||||
timerNodes: ReadonlyMap<string, JavaScriptSemanticGraphNode>,
|
||||
): void => {
|
||||
const callSite = semanticCallSiteAt(context, operation.location);
|
||||
const callSite = context.callSiteAt(operation.location);
|
||||
const target =
|
||||
operation.linkedTimerId === null
|
||||
? undefined
|
||||
|
||||
@@ -838,6 +838,85 @@ it("retains array length presence without inventing an exact length", () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
["object spread", 'const root = { known: "value", ...dynamic };'],
|
||||
["array spread", 'const root = ["value", ...dynamic];'],
|
||||
])(
|
||||
"keeps omitted initializer coverage on the owning binding: %s",
|
||||
(_label, source) => {
|
||||
const graph = graphFor(source);
|
||||
const root = graph.nodes.find(
|
||||
({ kind, label }) => kind === "binding" && label === "root",
|
||||
);
|
||||
if (root === undefined) throw new Error("Expected root binding");
|
||||
const result = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "semantic-node", node_id: root.node_id },
|
||||
direction: "forward-influence",
|
||||
});
|
||||
|
||||
expect(result.coverage.status).toBe("partial");
|
||||
expect(result.unknowns).toContainEqual(
|
||||
expect.objectContaining({
|
||||
node_id: root.node_id,
|
||||
family: "object-flow",
|
||||
reason: "ambiguous-target",
|
||||
detail: expect.stringContaining("Initializer container has unknown"),
|
||||
}),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
["bare if", 'if (flag) alias = { mode: "other" }; alias.mode = "updated";'],
|
||||
[
|
||||
"logical expression",
|
||||
'flag && (alias = { mode: "other" }); alias.mode = "updated";',
|
||||
],
|
||||
[
|
||||
"logical assignment",
|
||||
'flag ||= (alias = { mode: "other" }); alias.mode = "updated";',
|
||||
],
|
||||
["or assignment", 'alias ||= { mode: "other" }; alias.mode = "updated";'],
|
||||
[
|
||||
"nullish assignment",
|
||||
'alias ??= { mode: "other" }; alias.mode = "updated";',
|
||||
],
|
||||
[
|
||||
"conditional mutation",
|
||||
'alias = { mode: "other" }; if (flag) alias.mode = "updated";',
|
||||
],
|
||||
[
|
||||
"short-circuit mutation",
|
||||
'alias = { mode: "other" }; flag && (alias.mode = "updated");',
|
||||
],
|
||||
])(
|
||||
"preserves possible alias mutation uncertainty in queries: %s",
|
||||
(_label, body) => {
|
||||
const graph = graphFor(`
|
||||
const shared = { mode: "initial" };
|
||||
let alias = shared;
|
||||
${body}
|
||||
`);
|
||||
const sharedMode = graph.nodes.find(
|
||||
({ kind, identity, properties }) =>
|
||||
kind === "property-slot" &&
|
||||
properties.property_pointer === "/mode" &&
|
||||
identity.role_key.includes("binding:shared"),
|
||||
);
|
||||
if (sharedMode === undefined) throw new Error("Expected shared mode slot");
|
||||
const result = queryJavaScriptSemanticGraph(graph, {
|
||||
seed: { kind: "semantic-node", node_id: sharedMode.node_id },
|
||||
direction: "backward-provenance",
|
||||
});
|
||||
|
||||
expect(sharedMode.properties.presence).toBe("unknown-coverage");
|
||||
expect(result.nodes).toContainEqual(
|
||||
expect.objectContaining({ node_id: sharedMode.node_id }),
|
||||
);
|
||||
expect(result.coverage.status).toBe("partial");
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["{}", "unknownRoot", "{ a: null }"])(
|
||||
"preserves the requested deep leaf name after a blocked prefix: %s",
|
||||
(initializer) => {
|
||||
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
createJavaScriptSemanticGraph,
|
||||
createImmutableJavaScriptSemanticGraphSteps,
|
||||
type JavaScriptSemanticGraph,
|
||||
type JavaScriptSemanticGraphNode,
|
||||
} from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import {
|
||||
JAVASCRIPT_SEMANTIC_RELATION_FAMILIES,
|
||||
JAVASCRIPT_SEMANTIC_RELATION_FAMILY,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import type {
|
||||
JavaScriptSemanticChildProcessInteraction,
|
||||
JavaScriptSemanticChildProcessSpawn,
|
||||
@@ -10,10 +10,7 @@ import {
|
||||
addSemanticGraphUnknown,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
semanticCallSiteAt,
|
||||
type SemanticFlowProjectionContext,
|
||||
} from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
|
||||
interface ChildUnknownInput {
|
||||
readonly context: SemanticFlowProjectionContext;
|
||||
@@ -75,7 +72,7 @@ const projectSpawn = (
|
||||
spawn: JavaScriptSemanticChildProcessSpawn,
|
||||
child: JavaScriptSemanticGraphNode,
|
||||
): void => {
|
||||
const callSite = semanticCallSiteAt(context, spawn.location);
|
||||
const callSite = context.callSiteAt(spawn.location);
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: callSite,
|
||||
target: child,
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
createJavaScriptSemanticGraphNode,
|
||||
createJavaScriptSemanticGraphRelation,
|
||||
javaScriptSemanticNodeId,
|
||||
type JavaScriptSemanticGraphNode,
|
||||
} from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
import type {
|
||||
JavaScriptSemanticGraphRelation,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createJavaScriptSemanticGraphUnknown } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import type {
|
||||
JavaScriptSemanticBoundaryOperation,
|
||||
JavaScriptSemanticConfigurationOperation,
|
||||
@@ -11,10 +11,7 @@ import {
|
||||
addSemanticGraphUnknown,
|
||||
} from "./JavaScriptSemanticGraphConstruction.js";
|
||||
import { unknownSemanticEvidence } from "./JavaScriptSemanticGraphEvidence.js";
|
||||
import {
|
||||
semanticCallSiteAt,
|
||||
type SemanticFlowProjectionContext,
|
||||
} from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
import type { SemanticFlowProjectionContext } from "./JavaScriptSemanticGraphFlowProjection.js";
|
||||
|
||||
/** Project environment, argv, file, default, and precedence candidates. */
|
||||
export const projectSemanticConfiguration = (
|
||||
@@ -158,7 +155,7 @@ const projectRequestConstruction = (
|
||||
operation: JavaScriptSemanticRequestOperation,
|
||||
request: JavaScriptSemanticGraphNode,
|
||||
): void => {
|
||||
const callSite = semanticCallSiteAt(context, operation.location);
|
||||
const callSite = context.callSiteAt(operation.location);
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: callSite,
|
||||
target: request,
|
||||
@@ -197,7 +194,7 @@ const projectResponseConsumer = (
|
||||
): void => {
|
||||
addSemanticGraphRelation(context.state, {
|
||||
source: response,
|
||||
target: semanticCallSiteAt(context, operation.location),
|
||||
target: context.callSiteAt(operation.location),
|
||||
relation: "consumed-by",
|
||||
resolution: operation.resolution === "complete" ? "resolved" : "candidate",
|
||||
properties: { method: operation.method },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ApplicationGraphEvidence } from "../../domain/javascript/javascriptApplicationEvidenceSchemas.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraph.js";
|
||||
import type { JavaScriptSemanticGraphNode } from "../../domain/javascript/javascriptSemanticGraphSchemas.js";
|
||||
import type { JavaScriptSourceRange } from "../../domain/javascript/javascriptStaticAnalysisTypes.js";
|
||||
import type { JavaScriptArtifactFile } from "../../domain/javascript/javascriptArtifactFiles.js";
|
||||
|
||||
@@ -101,7 +101,7 @@ export const unavailableSemanticRootEvidence = (
|
||||
/** Explicit unknown evidence at one unresolved dynamic syntax location. */
|
||||
export const unknownSemanticEvidence = (
|
||||
file: JavaScriptArtifactFile,
|
||||
location: JavaScriptSourceRange,
|
||||
location: JavaScriptSourceRange | null,
|
||||
): ApplicationGraphEvidence => ({
|
||||
...observedSemanticEvidence(file, location),
|
||||
authority: "unknown",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user