Compare commits

...
Author SHA1 Message Date
github-actions[bot]andGitHub 0f337cb398 Saving release notes 2026-10-13 11:39:49 +00:00
Kaoru0822andGitHub fb28ec1537 test(apple): keep the same-named slice dyld fixture openable on x64 hosts (#2755)
Add a plain x86_64 slice to the dyld fixture while retaining both same-named arm64e slices and their independent environment assertions.

The original fixture was rejected by real x64 CLI admission before tracing. The contributing PR's complete Linux CI run passes all 30 jobs, including the portable suite. Codex and Greptile reviews completed without findings; no unresolved threads remain.

This restores the main test baseline needed for #2635. The change is limited to test data and preserves the original author's contribution.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-13 19:31:18 +08:00
morluto 490a285070 ci: reuse current-run builds across provider lanes 2026-10-13 19:30:50 +08:00
morluto fa0a9b2d32 ci: observe affected-owner test plans against full results 2026-10-13 19:21:09 +08:00
morluto c8d861f976 ci: scope timing helpers to portable verification 2026-10-13 19:12:26 +08:00
morluto bd2c202dd9 fix(ci): reject incomplete verification plans 2026-10-13 19:06:31 +08:00
morluto 9f3f392eb8 ci: bound core job execution time
Limit classification to five minutes, static/build/docs/report jobs to ten minutes, and each portable shard to thirty minutes. The saved successful-job sample peaked at 392 seconds per shard and 61 seconds for other core jobs, leaving generous setup and coverage headroom. Runner queue waiting is unaffected.
2026-10-13 19:00:47 +08:00
morluto f0f09950f3 ci: finish main validation and balance complete test costs 2026-10-13 18:54:05 +08:00
morluto 3af8f847f0 ci: fix PR metadata triggers and defer expensive runner work 2026-10-13 18:38:46 +08:00
morluto 747e729914 test(release): validate only the selected preflight job
Scope the steps schema to the release job whose preflight is executed, allowing unrelated reusable-workflow jobs such as qualification. Preserve all four valid and invalid metadata execution checks.

Reproduced on main ec3db3745: 35 passed, 4 failed. After the fix: 104 release and CI boundary tests pass; check:fast and format:check pass.
2026-10-13 18:24:40 +08:00
记得吃中饭andGitHub 2995ec6910 fix(ghidra): resolve file offsets in decompile (#2101)
* fix(native): expose loaded image bases in overviews

* test(ghidra): retain resources when verifier cleanup fails

* docs(ghidra): spell out file-offset address mapping

* fix(ci): separate and retry NativeAOT dependency restoration

* fix(test): resolve CI script file URLs on Windows

* fix(native): retain conflict observations and actionable CLI guidance

* fix(fixtures): keep NativeAOT retry logs credential-safe

* fix(fixtures): redact authenticated URLs at diagnostic creation

* fix(ghidra): restrict source-coordinate observations to decompilation

* fix(cli): identify conflicting file-offset selectors accurately

* fix(ghidra): bind file offsets to admitted target sources

* fix(ghidra): preserve resolved file-offset failure context

* test(ghidra): verify ELF address and file-offset round trips

* test(nativeaot): align diagnostic regressions with fixture host support
2026-10-13 18:23:17 +08:00
Yijie LuandGitHub ec3db37457 fix(native): preserve literal line breaks in signing identifiers (#2107)
* fix(native): preserve literal line breaks in signing identifiers

* fix(native): infer codesign framing without a matching path
2026-10-13 18:22:34 +08:00
N0zoM1z0 f1f5582ce8 fix(website): clarify translated evidence and runtime observations 2026-10-13 18:21:09 +08:00
N0zoM1z0 3d78a57043 docs(website): publish model essay in all supported languages 2026-10-13 18:21:09 +08:00
N0zoM1z0 b06751b703 docs(website): simplify reconstruction evidence in model essay
Explain each case through the obstacle, useful hypothesis and verified result. Remove byte totals, relocation counts and trial counts from the main narrative; preserve commit references and the constant-value example that explains the oracle defect. Make the experiment table and compiler terminology easier to follow.

Validation: website.py check, formatting, preserved source links, and eight article layout checks across both preview paths and light/dark themes.
2026-10-13 18:21:09 +08:00
N0zoM1z0 806ff57567 docs(website): use straight quotes in model essay
Normalize the three quoted phrases to straight English quotation marks. Website checks and formatting pass; the local preview serves the revised punctuation.
2026-10-13 18:21:09 +08:00
N0zoM1z0 ca07373e44 docs(website): improve model essay transitions and narrative flow
Connect the reconstruction cases to the questions about specialization, retrieval and feedback-driven search. Replace fragmented sentence sequences with explicit causal links and smooth the progression into oracle design and the proposed comparison.

Validation: website.py check, formatting, preserved source links and evidence figures, and eight desktop/mobile light/dark layout checks at both preview paths.
2026-10-13 18:21:09 +08:00
N0zoM1z0 8583a190c0 fix(website): keep omitted language selectors free of whitespace
Indent the optional language selector with its value so a nonindexable draft does not generate a line containing only spaces. Indexed-page markup is unchanged.

Validation: website.py check passes all 12 maintenance regressions; synchronization changes only the draft header blank line.
2026-10-13 18:21:09 +08:00
N0zoM1z0 fe496bd1cf docs(website): add essay on reverse engineering models
Add an English review draft connecting Touhou reconstruction evidence with model specialization, repository knowledge and feedback-driven learning. Preserve model attribution and historical verification scope in supporting notes, and translate the new Blog listing for all existing editions.

Validation: website.py check, focused formatting checks, and 32 Chromium cases covering both preview paths, light/dark themes, responsive layouts, translated indexes and no-JavaScript reading.
2026-10-13 18:21:09 +08:00
Yijie LuandGitHub f8e95799f9 fix(artifacts): preserve NIB field and class identities (#2106) 2026-10-13 18:20:54 +08:00
HarlyandGitHub 6c816af119 fix(managed): preserve negative zero in CIL operands (#2437) 2026-10-13 18:19:44 +08:00
morluto ab02f3c8d6 ci: reduce reporter fan-out and preserve shard history 2026-10-13 18:16:10 +08:00
ChaoqiandGitHub 9928f357fb fix(artifacts): read ASCII control bytes in legacy ZIP names as ASCII (#2365)
* fix(artifacts): read ASCII control bytes in legacy ZIP names as ASCII

ZIP names without the UTF-8 flag were decoded with zip.js's CP437 table,
which maps bytes 0x01-0x1F and 0x7F to IBM PC display glyphs. A folder
zipped on macOS with a custom icon therefore listed and extracted
`Folder/Icon\r` as `Folder/Icon♪`, a name that is not in the archive and
differs from REA's directory inventory of the same folder. Tabs became `○`
and DEL became `⌂`.

Decode legacy names in REA's existing decodeText hook with the Unicode
CP437 mapping that Python's zipfile also uses: bytes below 0x80 as ASCII,
and the upper half with the same table zip.js uses.

Windows file names cannot contain U+0001-U+001F, so a Windows extraction
destination now refuses such a path with a typed path failure before
creating anything, as it already does for ':'.

* test(artifacts): check extracted legacy ZIP control names on disk

The control-name cases checked only the logical path that inventory
reports. Extraction writes through SafeOutputTree, so a change that
renamed the extracted files would still pass those cases.

Extract `Icon\r` and `é\t\u007F.a`, then compare the reported
relative_path, the directory listing, and the file contents. The new
cases skip on Windows, which refuses these names.
safeOutputTree.test.ts covers that refusal.
2026-10-13 18:05:20 +08:00
morluto d13c9f840d ci: reduce redundant compatibility and qualification work
Select SQLite compatibility by explicit source, fixture, lifecycle, and script ownership while retaining every version in full qualification. Share the canonical scope inventory between planning and the required gate.

Reuse recent exact-SHA full qualification only for hourly schedules, with trusted successful-run metadata and receipts. Force a daily refresh, reject stale original runs even after retries, and never renew proof from reused validation.

Report attempt-specific queue, execution, and matched step timing in a separate completion workflow using trusted default-branch code and read-only API access. Preserve raw metadata and unknown timings without delaying validation or publication.

Validation: 177 focused tests; typecheck/lint; knip; actionlint 1.7.7; all 1046 tests discovered; live 37-job API report smoke. Remote workflow scheduling and real-provider matrices remain CI verification.
2026-10-13 17:46:04 +08:00
0cf224e762 fix(javascript): retain distinct chunk registration callback metadata (#2737)
Co-authored-by: LouisChen <3361008+chqcjq@users.noreply.github.com>
2026-10-13 17:42:41 +08:00
52d4ce6ced fix(apple): derive dyld environment findings from each traced slice (#2733)
deriveFindings looked up each traced root's slice again by architecture
name. A universal executable with arm64e and arm64e.x1 slices reports
both as "arm64e", so both roots resolved to the first slice: a later
slice's LC_DYLD_ENVIRONMENT produced no finding, or the first slice's
command was reported twice. The per-slice dyld_environment arrays were
already correct.

traceDylibLoading already has the exact slice when it records a root, so
pass it to deriveFindings instead of re-deriving it from the name. The
public roots and findings shapes are unchanged.

Fixes #2661

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 17:42:09 +08:00
112 changed files with 33231 additions and 591 deletions
@@ -119,6 +119,8 @@ loaded bytes, file mappings, relocations and the entry against the immutable
snapshot. Keep `mismatch` and `unsupported` explicit. Verified import covers the
reported static image, not runtime DOS or PC-98 hardware behavior.
For Ghidra decompilation, `procedure_pseudo_code` also accepts `procedure: "file-offset:0x..."`; CLI `rea decompile <path> --file-offset=0x... --provider ghidra` resolves the source byte offset internally. An unresolved 0x-prefixed numeric procedure selector also falls back to a unique file mapping. Existing symbols and analysis addresses win; multiple mappings fail as ambiguous. Preserve the returned file-offset and address locations and raw selection observations. ELF virtual addresses are distinct from file offsets; never assume an arbitrary offset plus image base is an analysis address.
Use `read_bytes` for initialized analysis memory and `address_to_file_offset` to
anchor one observed address to original file bytes. Loader fixups can change a
word; a source offset does not imply byte equality. Complete function body ranges
@@ -1,14 +1,24 @@
name: Portable build cache
description: Reuse Turbo build outputs without serializing verification jobs.
description: Restore portable Turbo build outputs from the current run or cross-run cache.
inputs:
save:
description: Save newly built outputs after the job succeeds.
default: "false"
build_cache_artifact:
description: Reuse portable Turbo outputs uploaded by the current workflow run.
default: ""
runs:
using: composite
steps:
- name: Download portable build outputs from this workflow run
if: inputs.build_cache_artifact != ''
uses: actions/download-artifact@v4
with:
name: ${{ inputs.build_cache_artifact }}
path: .turbo/cache/
- name: Resolve portable build task hashes
id: plan
if: inputs.build_cache_artifact == ''
shell: bash
env:
BUILD_CACHE_PREFIX: rea-portable-build-v2-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.npmrc', '.nvmrc', 'package-lock.json') }}
@@ -28,14 +38,14 @@ runs:
console.log(`prefix=${process.env.BUILD_CACHE_PREFIX}-`);
' >> "$GITHUB_OUTPUT"
- name: Restore portable build outputs
if: inputs.save != 'true'
if: inputs.build_cache_artifact == '' && inputs.save != 'true'
uses: actions/cache/restore@v4
with:
path: .turbo/cache/
key: ${{ steps.plan.outputs.key }}
restore-keys: ${{ steps.plan.outputs.prefix }}
- name: Restore and save portable build outputs
if: inputs.save == 'true'
if: inputs.build_cache_artifact == '' && inputs.save == 'true'
uses: actions/cache@v4
with:
path: .turbo/cache/
+18
View File
@@ -0,0 +1,18 @@
name: Full PR qualification
on:
pull_request:
branches: [main, "release/*"]
types: [labeled]
permissions:
contents: read
actions: read
jobs:
qualification:
name: Qualify labeled PR revision
if: github.event.label.name == 'ci:full'
uses: ./.github/workflows/ci.yml
with:
source_sha: ${{ github.sha }}
+46
View File
@@ -0,0 +1,46 @@
name: CI timings
on:
schedule:
- cron: "17 * * * *"
workflow_dispatch:
concurrency:
group: ci-timings-${{ github.repository }}
cancel-in-progress: false
permissions:
contents: read
actions: read
jobs:
report:
name: Collect recent completed workflow attempts
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
# Consume only API data and execute batch code from the trusted default branch.
- uses: actions/checkout@v4
with:
ref: ${{ github.event.repository.default_branch }}
persist-credentials: false
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Record queue, execution, and matched step timing
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
CI_REPOSITORY: ${{ github.repository }}
run: node scripts/ci/job-timings-batch.mjs
- name: Upload CI timing artifacts
if: always() && hashFiles('.cache/ci-job-timings/*') != ''
continue-on-error: true
uses: actions/upload-artifact@v4
with:
name: ci-job-timings-batch
path: .cache/ci-job-timings/
include-hidden-files: true
if-no-files-found: error
overwrite: true
retention-days: 14
+105 -62
View File
@@ -6,12 +6,13 @@ on:
pull_request:
branches: [main, "release/*"]
schedule:
- cron: "23 * * * *"
- cron: "23 0-2,4-23 * * *"
- cron: "23 3 * * *"
workflow_dispatch:
workflow_call:
inputs:
source_sha:
description: Exact release source; must match the caller revision.
description: Exact qualification source; must match the caller revision.
required: true
type: string
@@ -20,40 +21,28 @@ permissions:
concurrency:
# Complete qualification finishes even while main receives more pushes.
# Main feedback also finishes, coalescing newer pushes to one pending run.
# Scheduled/manual main runs coalesce to one active and the latest pending;
# release qualification has its own exact-revision group.
# release and label-triggered qualification use exact-revision groups.
group: ci-${{ (github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' || inputs.source_sha != '' || contains(github.event.pull_request.labels.*.name, 'ci:full')) && 'qualification' || 'feedback' }}-${{ inputs.source_sha || github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name != 'schedule' && github.event_name != 'workflow_dispatch' && inputs.source_sha == '' && !contains(github.event.pull_request.labels.*.name, 'ci:full') }}
cancel-in-progress: ${{ github.event_name == 'pull_request' && inputs.source_sha == '' && !contains(github.event.pull_request.labels.*.name, 'ci:full') }}
env:
HUSKY: "0"
jobs:
pr-title:
name: Conventional PR title
if: github.event_name == 'pull_request' && inputs.source_sha == ''
runs-on: ubuntu-latest
steps:
- name: Validate title for Release Please
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: >-
node -e 'const title = process.env.PR_TITLE ?? "";
const conventional = /^(?:feat|fix|docs|refactor|perf|test|build|ci|chore|revert)(?:\([a-z0-9._/-]+\))?!?: .+/u;
if (!conventional.test(title)) {
console.error("PR title must use Conventional Commits, for example: feat: add historical source import");
process.exit(1);
}'
changes:
name: Classify changes
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
actions: read
outputs:
tier: ${{ steps.plan.outputs.tier }}
qualification_run_id: ${{ steps.qualification.outputs.run_id }}
code: ${{ steps.plan.outputs.code }}
sqlite: ${{ steps.plan.outputs.sqlite }}
runtime: ${{ steps.plan.outputs.runtime }}
package: ${{ steps.plan.outputs.package }}
apple: ${{ steps.plan.outputs.apple }}
@@ -72,6 +61,7 @@ jobs:
nativeaot: ${{ steps.plan.outputs.nativeaot }}
website: ${{ steps.plan.outputs.website }}
matrix: ${{ steps.plan.outputs.matrix }}
test_selection: ${{ steps.plan.outputs.test_selection }}
steps:
- uses: actions/checkout@v4
with:
@@ -80,7 +70,7 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version-file: .nvmrc
- name: Bind release qualification to the caller revision
- name: Bind qualification to the caller revision
if: inputs.source_sha != ''
env:
SOURCE_SHA: ${{ inputs.source_sha }}
@@ -96,6 +86,17 @@ jobs:
CI_REPOSITORY: ${{ github.repository }}
CI_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: node scripts/ci/baseline.mjs
- name: Admit scheduled full qualification
id: qualification
if: github.event_name == 'schedule' && inputs.source_sha == ''
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.sha }}
CI_REPOSITORY: ${{ github.repository }}
CI_REPOSITORY_ID: ${{ github.repository_id }}
CI_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
CI_FORCE_FULL: ${{ github.event.schedule == '23 3 * * *' }}
run: node scripts/ci/qualification.mjs
- name: Select verification lanes
id: plan
env:
@@ -103,13 +104,15 @@ jobs:
BASELINE_REASON: ${{ steps.baseline.outputs.reason }}
HEAD_SHA: ${{ inputs.source_sha || github.event.pull_request.head.sha || github.sha }}
CI_FULL: ${{ inputs.source_sha != '' || contains(github.event.pull_request.labels.*.name, 'ci:full') }}
CI_SKIP_FULL: ${{ steps.qualification.outputs.run_full == 'false' }}
run: node scripts/ci/plan.mjs
static:
name: Static checks
needs: changes
if: ${{ !cancelled() }}
if: ${{ !cancelled() && needs.changes.outputs.tier != 'reused' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Require successful change classification
env:
@@ -121,6 +124,9 @@ jobs:
node-version-file: .nvmrc
cache: npm
- run: npm ci
- name: Validate workflow syntax and expressions
if: needs.changes.outputs.workflows == 'true'
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 -shellcheck= -pyflakes=
- if: needs.changes.outputs.code == 'true'
run: npm run check:ci
- if: needs.changes.outputs.code == 'true'
@@ -145,6 +151,7 @@ jobs:
needs: [changes, build]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
@@ -183,8 +190,20 @@ jobs:
build:
name: Build package
needs: changes
if: needs.changes.outputs.code == 'true'
# Reusable provider workflows consume this run's portable build artifact,
# even when a workflow-only edit selects the lane without portable tests.
if: >-
needs.changes.outputs.code == 'true' ||
needs.changes.outputs.browser == 'true' ||
needs.changes.outputs.binary_layout == 'true' ||
needs.changes.outputs.evm == 'true' ||
needs.changes.outputs.javascript_recovery == 'true' ||
needs.changes.outputs.recorded_crash == 'true' ||
needs.changes.outputs.web_network == 'true' ||
needs.changes.outputs.web_runtime == 'true' ||
needs.changes.outputs.web_source_map == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
actions: read
@@ -200,19 +219,15 @@ jobs:
with:
save: ${{ github.event_name != 'pull_request' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) }}
- run: npm run test:prepare
- name: Find successful default-branch timing history
- name: Find available trusted full-suite timing history
id: history
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
CI_REPOSITORY: ${{ github.repository }}
CI_REPOSITORY_ID: ${{ github.repository_id }}
CI_DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
run_id="$(gh api --method GET \
"repos/${CI_REPOSITORY}/actions/workflows/ci.yml/runs" \
-f branch="${CI_DEFAULT_BRANCH}" -f event=schedule -f status=success -f per_page=1 \
--jq '.workflow_runs[0].id // empty')"
echo "run-id=${run_id}" >> "${GITHUB_OUTPUT}"
run: node scripts/ci/test-history.mjs
- name: Download previous timing report
if: steps.history.outputs.run-id != ''
continue-on-error: true
@@ -248,15 +263,18 @@ jobs:
retention-days: 7
sqlite-runtime:
needs: [changes, build]
if: needs.changes.outputs.code == 'true'
# Preserve all selected coverage, but admit expensive host/provider work
# only after static validation. Portable tests still provide early feedback.
needs: [changes, static, build]
if: needs.changes.outputs.sqlite == 'true'
uses: ./.github/workflows/sqlite-runtime.yml
test:
name: Portable tests
needs: [changes, test-shard]
if: ${{ !cancelled() }}
if: ${{ !cancelled() && needs.changes.outputs.tier != 'reused' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Require successful change classification
env:
@@ -289,6 +307,7 @@ jobs:
--reporter=default
--reporter=junit
--reporter=json
--reporter=./scripts/ci/test-timing-reporter.mjs
--outputFile.junit=test-results.xml
--outputFile.json=test-results.json
- name: Merge fast-suite JUnit and timing reports
@@ -299,14 +318,17 @@ jobs:
--reporter=default
--reporter=junit
--reporter=json
--reporter=./scripts/ci/test-timing-reporter.mjs
--outputFile.junit=test-results.xml
--outputFile.json=test-results.json
- name: Summarize slowest test files
if: needs.changes.outputs.code == 'true' && !cancelled() && hashFiles('test-results.json') != ''
if: needs.changes.outputs.code == 'true' && !cancelled() && hashFiles('test-timings.json') != ''
env:
SHARD_RESULT: ${{ needs.test-shard.result }}
MERGE_RESULT: ${{ needs.changes.outputs.tier == 'full' && steps.merge.outcome || steps.merge-fast.outcome }}
CI_VERIFY_TEST_INVENTORY: "true"
CI_TEST_SELECTION: ${{ needs.changes.outputs.test_selection }}
CI_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: node scripts/ci/test-report.mjs
- uses: actions/upload-artifact@v4
if: needs.changes.outputs.code == 'true' && !cancelled() && hashFiles('test-results.json') != ''
@@ -315,7 +337,10 @@ jobs:
path: |
test-results.xml
test-results.json
test-timings.json
coverage/
.cache/ci-test-selection/report.json
include-hidden-files: true
if-no-files-found: error
retention-days: 30
- name: Publish successful timing history
@@ -336,7 +361,7 @@ jobs:
conformance-fixtures:
name: Source-built conformance fixtures
needs: changes
needs: [changes, static]
if: needs.changes.outputs.fixtures == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -347,7 +372,7 @@ jobs:
windows-native-build:
name: Build bundled Windows x64 native controls
needs: changes
needs: [changes, static]
if: needs.changes.outputs.runtime == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
@@ -383,7 +408,7 @@ jobs:
package-e2e:
name: Native package baseline (${{ matrix.platform }} ${{ matrix.architecture }})
needs: [changes, build, windows-native-build]
needs: [changes, static, build, windows-native-build]
if: needs.changes.outputs.runtime == 'true'
strategy:
fail-fast: false
@@ -422,6 +447,8 @@ jobs:
name: windows-native-controls
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- name: Verify NativeAOT diagnostic applicability on this host
run: npm run test:focused -- tests/evaluation/package/nativeAotDiagnostics.test.ts
- name: Resolve npm cache before verifier HOME isolation
id: npm-cache
run: |
@@ -447,7 +474,7 @@ jobs:
apple-artifacts:
name: Real Apple artifact CLI/MCP E2E
needs: [changes, build]
needs: [changes, static, build]
if: needs.changes.outputs.apple == 'true'
runs-on: macos-15
timeout-minutes: 15
@@ -495,7 +522,7 @@ jobs:
reconstruction-readiness:
name: Reconstruction readiness
needs: changes
needs: [changes, static]
if: needs.changes.outputs.readiness == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
@@ -524,7 +551,7 @@ jobs:
windows-curated:
name: Windows capability and package lane
needs: [changes, build, windows-native-build]
needs: [changes, static, build, windows-native-build]
if: needs.changes.outputs.package == 'true'
runs-on: windows-latest
timeout-minutes: 25
@@ -636,6 +663,7 @@ jobs:
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
@@ -655,63 +683,69 @@ jobs:
skills/reverse-engineer-anything
if-no-files-found: error
workflows:
name: Validate workflows
needs: changes
if: needs.changes.outputs.workflows == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate workflow syntax and expressions
run: go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 -shellcheck= -pyflakes=
browser:
needs: changes
needs: [changes, static, build]
if: needs.changes.outputs.browser == 'true'
uses: ./.github/workflows/real-browser.yml
with:
build_cache_artifact: rea-build-cache
binary_layout:
needs: changes
needs: [changes, static, build]
if: needs.changes.outputs.binary_layout == 'true'
uses: ./.github/workflows/real-binary-layout.yml
with:
build_cache_artifact: rea-build-cache
evm:
needs: changes
needs: [changes, static, build]
if: needs.changes.outputs.evm == 'true'
uses: ./.github/workflows/real-evm-interface.yml
with:
build_cache_artifact: rea-build-cache
javascript_recovery:
needs: changes
needs: [changes, static, build]
if: needs.changes.outputs.javascript_recovery == 'true'
uses: ./.github/workflows/real-javascript-recovery.yml
with:
build_cache_artifact: rea-build-cache
recorded_crash:
needs: changes
needs: [changes, static, build]
if: needs.changes.outputs.recorded_crash == 'true'
uses: ./.github/workflows/real-recorded-crash.yml
with:
build_cache_artifact: rea-build-cache
web_network:
needs: changes
needs: [changes, static, build]
if: needs.changes.outputs.web_network == 'true'
uses: ./.github/workflows/real-web-network-captures.yml
with:
build_cache_artifact: rea-build-cache
web_runtime:
needs: changes
needs: [changes, static, build]
if: needs.changes.outputs.web_runtime == 'true'
uses: ./.github/workflows/real-web-runtime.yml
with:
build_cache_artifact: rea-build-cache
web_source_map:
needs: changes
needs: [changes, static, build]
if: needs.changes.outputs.web_source_map == 'true'
uses: ./.github/workflows/real-web-source-map.yml
with:
build_cache_artifact: rea-build-cache
nativeaot:
needs: changes
needs: [changes, static]
if: needs.changes.outputs.nativeaot == 'true'
uses: ./.github/workflows/nativeaot-fixtures.yml
website:
needs: changes
needs: [changes, static]
if: needs.changes.outputs.website == 'true'
uses: ./.github/workflows/website-check.yml
@@ -721,7 +755,6 @@ jobs:
needs:
[
changes,
pr-title,
static,
build,
test-shard,
@@ -734,7 +767,6 @@ jobs:
reconstruction-readiness,
windows-curated,
docs,
workflows,
browser,
binary_layout,
evm,
@@ -754,6 +786,17 @@ jobs:
with:
node-version-file: .nvmrc
- name: Require every selected lane to pass
id: gate
env:
CI_JOBS: ${{ toJSON(needs) }}
CI_QUALIFICATION_DIRECTORY: .cache/ci-qualification
run: node scripts/ci/required.mjs
- name: Upload full qualification receipt artifact
if: needs.changes.outputs.tier == 'full' && success()
uses: actions/upload-artifact@v4
with:
name: ${{ steps.gate.outputs.receipt_name }}
path: ${{ steps.gate.outputs.receipt_path }}
if-no-files-found: error
overwrite: true
retention-days: 7
+2 -1
View File
@@ -12,7 +12,8 @@ jobs:
name: Build Windows x64 NativeAOT fixture
# .NET 8.0.22's bundled linker discovery supports VS 2019/2022.
runs-on: windows-2022
timeout-minutes: 15
# Two bounded restore attempts and one compile can each take five minutes.
timeout-minutes: 20
env:
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
DOTNET_PROCESSOR_COUNT: "1"
+29
View File
@@ -0,0 +1,29 @@
name: PR title
on:
pull_request:
branches: [main, "release/*"]
types: [opened, synchronize, reopened, edited]
permissions: {}
concurrency:
group: pr-title-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
pr-title:
name: Conventional PR title
runs-on: ubuntu-latest
timeout-minutes: 2
steps:
- name: Validate title for Release Please
env:
PR_TITLE: ${{ github.event.pull_request.title }}
run: >-
node -e 'const title = process.env.PR_TITLE ?? "";
const conventional = /^(?:feat|fix|docs|refactor|perf|test|build|ci|chore|revert)(?:\([a-z0-9._/-]+\))?!?: .+/u;
if (!conventional.test(title)) {
console.error("PR title must use Conventional Commits, for example: feat: add historical source import");
process.exit(1);
}'
+8
View File
@@ -8,6 +8,12 @@ on:
type: boolean
default: false
workflow_call:
inputs:
build_cache_artifact:
description: Portable build cache artifact from the calling CI run.
required: false
type: string
default: ""
permissions:
contents: read
@@ -29,6 +35,8 @@ jobs:
cache: npm
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Acquire exact upstream libraries in an isolated runner environment
shell: bash
run: |
+8
View File
@@ -2,6 +2,12 @@ name: Real browser verification
on:
workflow_call:
inputs:
build_cache_artifact:
description: Portable build cache artifact from the calling CI run.
required: false
type: string
default: ""
workflow_dispatch:
env:
@@ -22,6 +28,8 @@ jobs:
cache: npm
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- run: npm run verify:browser
env:
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
+8
View File
@@ -3,6 +3,12 @@ name: Real offline EVM interface
on:
workflow_dispatch:
workflow_call:
inputs:
build_cache_artifact:
description: Portable build cache artifact from the calling CI run.
required: false
type: string
default: ""
permissions:
contents: read
@@ -23,6 +29,8 @@ jobs:
cache: npm
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Acquire unchanged fixture compiler in isolated runner storage
shell: bash
run: |
@@ -2,6 +2,12 @@ name: Real JavaScript recovery
on:
workflow_call:
inputs:
build_cache_artifact:
description: Portable build cache artifact from the calling CI run.
required: false
type: string
default: ""
workflow_dispatch:
permissions:
@@ -24,6 +30,8 @@ jobs:
cache: npm
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Fetch pinned Wakaru release into the job workspace
shell: bash
run: |
@@ -3,6 +3,12 @@ name: Real recorded crash evidence
on:
workflow_dispatch:
workflow_call:
inputs:
build_cache_artifact:
description: Portable build cache artifact from the calling CI run.
required: false
type: string
default: ""
permissions:
contents: read
@@ -26,6 +32,8 @@ jobs:
cache: npm
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Install GDB on the disposable CI runner
shell: bash
run: |
@@ -3,6 +3,12 @@ name: Real historical web network captures
on:
workflow_dispatch:
workflow_call:
inputs:
build_cache_artifact:
description: Portable build cache artifact from the calling CI run.
required: false
type: string
default: ""
permissions:
contents: read
@@ -25,6 +31,8 @@ jobs:
run: npm install --global npm@11.16.0
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Acquire fixed upstream native profile in runner temporary storage
shell: bash
run: |
+8
View File
@@ -1,6 +1,12 @@
name: Real website runtime attribution
on:
workflow_call:
inputs:
build_cache_artifact:
description: Portable build cache artifact from the calling CI run.
required: false
type: string
default: ""
workflow_dispatch:
permissions:
contents: read
@@ -18,6 +24,8 @@ jobs:
cache: npm
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- run: npm run verify:browser:runtime
env:
REA_BROWSER_EXECUTABLE: /usr/bin/google-chrome
@@ -1,6 +1,12 @@
name: Real website source-map verification
on:
workflow_call:
inputs:
build_cache_artifact:
description: Portable build cache artifact from the calling CI run.
required: false
type: string
default: ""
workflow_dispatch:
permissions:
contents: read
@@ -18,6 +24,8 @@ jobs:
cache: npm
- run: npm ci
- uses: ./.github/actions/portable-build-cache
with:
build_cache_artifact: ${{ inputs.build_cache_artifact }}
- name: Install the pinned compiler into an isolated fixture directory
run: npm install --prefix "$RUNNER_TEMP/rea-web-source-map-compiler" --no-audit --no-fund --package-lock=false esbuild@0.25.10
- run: npm run verify:browser:source-maps
+1
View File
@@ -36,6 +36,7 @@ coverage/
.vitest/
.vitest-reports/
test-results.xml
test-timings.json
.turbo/
.cache/
docs/.vitepress/cache/
+112 -15
View File
@@ -106,6 +106,8 @@ public final class ReaGhidraBridge extends HeadlessScript {
private static final int MAX_HIGH_PCODE_INPUTS_PER_OP = 64;
private static final int MAX_HIGH_PCODE_DEF_USE_EDGES = 12000;
private static final Gson GSON = new GsonBuilder().serializeNulls().create();
private final Map<FileBytes, Boolean> targetSourceIdentity = new IdentityHashMap<>();
private String admittedTargetSha256;
private static final Set<String> DESCRIPTOR_KEYS = Set.of(
"transport",
"endpoint_path",
@@ -207,6 +209,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
"Ghidra imported-byte digest does not match the admitted target"
);
}
admittedTargetSha256 = descriptor.targetSha256;
// GhidraScript wraps run() in a transaction. End it before serving so
// each edit owns a real outer transaction and can roll back immediately.
end(true);
@@ -633,7 +636,7 @@ public final class ReaGhidraBridge extends HeadlessScript {
return result;
}
private JsonObject addressToFileOffset(JsonObject params) {
private JsonObject addressToFileOffset(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "address"));
requireDocument(params);
Address address = requireAddress(params, "address");
@@ -643,14 +646,15 @@ public final class ReaGhidraBridge extends HeadlessScript {
Long fileOffset = null;
int mappings = 0;
for (MemoryBlockSourceInfo source : block.getSourceInfos()) {
if (!source.contains(address) || source.getFileBytes().isEmpty()) continue;
if (!source.contains(address) || source.getFileBytes().isEmpty() ||
!isTargetSource(source.getFileBytes().get())) continue;
long offset = source.getFileBytesOffset(address);
if (offset < 0) continue;
fileOffset = offset;
mappings++;
}
if (mappings != 1)
throw new RequestFailure("invalid_request", "Address has " + mappings + " authoritative source mappings: " + canonicalAddress(address));
throw new RequestFailure("invalid_request", "Address has " + mappings + " authoritative target-file mappings: " + canonicalAddress(address));
JsonObject result = new JsonObject();
result.addProperty("address", canonicalAddress(address));
result.addProperty("file_offset", fileOffset);
@@ -673,14 +677,96 @@ public final class ReaGhidraBridge extends HeadlessScript {
private JsonElement procedurePseudocode(JsonObject params) throws Exception {
requireKeys(params, Set.of("document", "procedure"));
requireDocument(params);
Function function = resolveProcedure(requireString(params, "procedure"));
String value = decompiledText(decompile(function));
return functionObservation(
value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value),
noReturnCallLimitations(
currentProgram.getListing().getInstructions(function.getBody(), true)
)
);
String selector = requireString(params, "procedure");
JsonObject selection = new JsonObject();
Function function = resolveDecompileProcedure(selector, selection);
try {
String value = decompiledText(decompile(function));
JsonObject result = functionObservation(
value == null ? JsonNull.INSTANCE : GSON.toJsonTree(value),
noReturnCallLimitations(
currentProgram.getListing().getInstructions(function.getBody(), true)
)
);
if (selection.size() > 0) result.add("selection", selection);
return result;
} catch (RequestFailure failure) {
if (selection.size() == 0) throw failure;
// Preserve completed resolution facts without changing the backend's
// failure code or suggesting that the successful mapping was invalid.
throw new RequestFailure(failure.code, "Selector " + selector +
" mapped to analysis address " + selection.get("mapped_address").getAsString() +
" and procedure " + canonicalAddress(function.getEntryPoint()) +
"; " + failure.getMessage());
}
}
private Function resolveDecompileProcedure(String selector, JsonObject selection) throws Exception {
String prefix = "file-offset:";
boolean explicitOffset = selector.startsWith(prefix);
if (!explicitOffset) {
try {
return resolveProcedure(selector);
} catch (RequestFailure failure) {
// Existing names/addresses win. Only an unresolved numeric input
// can fall back to source coordinates; ambiguity never falls back.
if (!failure.code.equals("not_found") || !selector.matches("0x[0-9a-fA-F]+"))
throw failure;
}
}
String coordinate = explicitOffset ? selector.substring(prefix.length()) : selector;
if (!coordinate.matches("0x[0-9a-fA-F]+"))
throw new RequestFailure("invalid_request", "File offsets require hexadecimal file-offset:0x... syntax");
BigInteger parsed = new BigInteger(coordinate.substring(2), 16);
if (parsed.compareTo(BigInteger.valueOf(9007199254740991L)) > 0)
throw new RequestFailure("invalid_request", "File offset exceeds the exact JSON integer range");
long offset = parsed.longValueExact();
monitor.checkCancelled();
// Ghidra's convenience lookup spans every FileBytes and uses coordinates
// local to each object. Only a complete, digest-matched target source
// establishes that the caller's offset belongs to the admitted file.
Set<Address> mappedAddresses = new java.util.LinkedHashSet<>();
for (MemoryBlock block : currentProgram.getMemory().getBlocks()) {
for (MemoryBlockSourceInfo source : block.getSourceInfos()) {
monitor.checkCancelled();
FileBytes backing = source.getFileBytes().orElse(null);
if (backing == null || !isTargetSource(backing)) continue;
Address mapped = source.locateAddressForFileOffset(offset);
if (mapped != null) mappedAddresses.add(mapped);
}
}
List<Address> candidates = new ArrayList<>(mappedAddresses);
if (candidates.size() > 1)
throw new RequestFailure("ambiguous", "File offset " + coordinate +
" has multiple loaded mappings: " + candidates.stream().map(ReaGhidraBridge::canonicalAddress).toList() +
"; select an explicit analysis address");
if (candidates.isEmpty())
throw new RequestFailure("not_found", "File offset " + coordinate +
" has no loaded source mapping for the admitted target file; Ghidra image base: " + canonicalAddress(currentProgram.getImageBase()) +
". Inspect mappings with rea inspect-native-load-image <path> --provider ghidra --json or MCP inspect_native_load_image.");
Address mapped = candidates.get(0);
Function function = functionAtOrContaining(mapped);
if (function == null)
throw new RequestFailure("not_found", "File offset " + coordinate +
" maps to analysis address " + canonicalAddress(mapped) +
", but no procedure starts at or contains that address in the current Ghidra analysis. " +
"Choose an observed procedure with CLI rea search <path> . --kind procedures --mode regex --provider ghidra --json or MCP list_procedures.");
selection.addProperty("file_offset", offset);
selection.addProperty("mapped_address", canonicalAddress(mapped));
selection.addProperty("procedure_address", canonicalAddress(function.getEntryPoint()));
return function;
}
private boolean isTargetSource(FileBytes source) throws Exception {
Boolean observed = targetSourceIdentity.get(source);
if (observed != null) return observed;
// Original FileBytes are immutable even when analysis memory is edited.
// Partial/auxiliary sources remain inspectable but cannot establish an
// original-target offset from their object-local coordinates alone.
boolean matched = source.getFileOffset() == 0 &&
constantTimeEquals(hashFileBytes(source, false), admittedTargetSha256);
targetSourceIdentity.put(source, matched);
return matched;
}
private JsonElement procedureAssembly(JsonObject params) throws Exception {
@@ -1833,14 +1919,25 @@ public final class ReaGhidraBridge extends HeadlessScript {
Address address = explicitAddress ? parseReaAddress(value) : tryParseAddress(value);
if (address == null)
throw new RequestFailure("not_found", "Unknown Ghidra procedure name or address: " + value);
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
if (function == null && address.isMemoryAddress())
function = currentProgram.getFunctionManager().getFunctionContaining(address);
Function function = functionAtOrContaining(address);
if (function == null)
throw new RequestFailure("not_found", "No procedure exists at the requested address: " + value);
// Other procedure operations retain their analysis-address semantics.
throw new RequestFailure("not_found", "No procedure exists at the requested address: " + value +
"; Ghidra image base: " + canonicalAddress(currentProgram.getImageBase()) +
". Choose an observed analysis address. CLI: rea search <path> . --kind procedures --mode regex --provider ghidra --json; " +
"MCP: list_procedures. Inspect the loaded base and source mappings with CLI rea analyze <path> --provider ghidra --json " +
"and rea inspect-native-load-image <path> --provider ghidra --json, or MCP binary_overview and inspect_native_load_image. " +
"Repeat the original target options. For decompile, select source coordinates explicitly with --file-offset=0x... or MCP procedure file-offset:0x....");
return function;
}
private Function functionAtOrContaining(Address address) {
Function function = currentProgram.getFunctionManager().getFunctionAt(address);
return function == null && address.isMemoryAddress()
? currentProgram.getFunctionManager().getFunctionContaining(address)
: function;
}
private JsonObject nativeApiBoundary(
Function function,
DecompileResults decompilation
+74
View File
@@ -41,6 +41,80 @@ does not prove text is absent. Both list and search Evidence carry this
limitation. When an address is known, use `read_bytes` and `xrefs` to inspect
bytes and references.
## Loaded addresses and file offsets
`rea analyze <path> --provider ghidra --json` and MCP `binary_overview` report
the observed loaded `image_base` in `normalized_result`. They retain each
segment's reported `image_base` and `address_space`. A provider that does not
report a base, or whose segments disagree, omits the overview field, following
the existing contract on main. REA does not substitute zero or the first
segment's address. All reported bases and address spaces remain visible in the
complete segment inventory, including conflicting observations.
`function` and most MCP procedure selectors use procedure names or
provider analysis addresses. Ghidra `decompile` / MCP `procedure_pseudo_code`
also accept source-file byte offsets. Existing symbols and analysis addresses
take precedence; a `0x`-prefixed number that resolves to no procedure falls back
to its observed source mapping. Use `--file-offset=0x...` on the CLI or
`procedure: "file-offset:0x..."` through MCP to select file coordinates explicitly.
Multiple mapping candidates fail as ambiguous rather than selecting a bank or
address space. A successful conversion retains the file offset, mapped address
and procedure entry in raw Evidence and `locations`.
If an offset maps to loaded target bytes but no procedure starts at or contains
the mapped address, the failure preserves both the original file offset and
analysis address. This differs from an unmapped offset and does not recommend
retrying the same offset syntax.
If the function resolves but the backend fails or cancels decompilation, the
failure retains the selector, mapped address and procedure entry without
changing the backend failure code.
Both mapping directions require a complete original `FileBytes` source whose
SHA-256 matches the admitted target. Auxiliary files and partial source objects
remain visible in the load-image inventory but do not establish offsets in the
original target. Ghidra's object-local offsets are not silently treated as
original-file coordinates. Original-source identity is checked once per source
object per session; analysis-memory edits do not change the original bytes.
ELF virtual addresses and file offsets are
different coordinates. Ghidra can rebase a PIE ELF during import: in the
source-owned verification fixture, ELF entry `0x1013` becomes Ghidra procedure
`0x101013` with image base `0x100000`. That example is not a general conversion
rule.
Use `list_procedures` to select an observed procedure address. To relate file
bytes to loaded memory, call MCP `inspect_native_load_image` or:
```sh
rea search <path> . --kind procedures --mode regex --provider ghidra --json
rea inspect-native-load-image <path> --provider ghidra --json
rea decompile <path> <observed-procedure-address> --provider ghidra --json
rea decompile <path> --file-offset=0x1013 --provider ghidra --json
rea address-to-file-offset <path> <observed-procedure-address> --provider ghidra --json
```
The load-image result preserves source-file indexes, file offsets, address
spaces, and inclusive mapping ranges. Follow the mapping for the selected
source file and region; adding `image_base` to an arbitrary file offset is not
valid. For a linear byte-addressed, file-backed mapping, a file offset `F` in
`[mapping.file_offset, mapping.file_offset + mapping.length)` maps to
`mapping.start + (F - mapping.file_offset)` in the reported `address_space`.
If several mappings cover the same file offset, keep those candidates distinct
and select the procedure from the observed inventory. Uninitialized regions may
have no file backing. ELF mappings are observed
without independent format-specific load-image verification, as the result's
status and limitations state.
An unsuccessful numeric procedure lookup reports the current Ghidra image base
and gives the CLI commands above alongside the corresponding MCP operations.
Repeat any target-format, architecture, and entry-point options used to open the
target. Other procedure operations keep their analysis-address semantics;
file-offset fallback applies only to decompilation.
Overview workflow revision 3 binds this output to snapshot identity. Older
serialized overview Evidence remains readable; it is not replayed as a current
overview merely because its source artifact is unchanged.
## Admitted function-analysis semantics
| Concern | Ghidra contract |
+168 -19
View File
@@ -263,18 +263,62 @@ CI owns coverage; aggregate and domain/contract thresholds are maintained in
Coverage does not replace named boundary or real-provider scenarios.
CI runs six native Vitest shards without retries. Each shard emits a blob
report; the merge job produces JUnit and JSON timing reports, plus aggregate
coverage during full qualification. It retains available
report; the merge job produces JUnit and JSON diagnostics, a module timing
report, plus aggregate coverage during full qualification. It retains available
reports even when a shard fails, uploads available diagnostics, and writes the
slowest files to the workflow summary. Partial reports are marked diagnostic
only. Failed, cancelled, or missing selected shards still fail the required gate,
and aggregate coverage thresholds remain enforced in full qualification.
Only successful complete full-tier merges publish the small `test-timings` artifact, with
checkout-relative paths; the build job downloads that artifact for the sequencer
instead of the coverage/JUnit bundle. Missing history falls back to native
instead of the coverage/JUnit bundle. Timing weights come from Vitest module
diagnostics and include tests and hooks, worker preparation, environment,
imports, and setup. Import timings already include transform waits, which are
not added again. The weights describe file costs under the measured worker and
coverage settings; their sum is not workflow elapsed time. The diagnostic
artifact retains the phase breakdown, while shard history keeps only each
file's total. Legacy test-body-only history and missing history fall back to native
sharding. Static checks, documentation, build, package verification, and
real-system lanes remain separate jobs so one kind of evidence cannot stand in
for another.
for another. Workflow syntax and expression validation runs inside the required
static-check job when workflow changes select it, avoiding a separate runner.
Native, provider, fixture, SQLite compatibility, and website lanes wait for
static checks to succeed before allocating their runners. This avoids spending
those runner slots on revisions that already fail static validation. Portable
test shards and documentation checks remain independent of static results, so
they can still provide diagnostic feedback. The required gate rejects skipped
selected lanes when static checks fail; passing revisions retain all selected
coverage. This ordering reduces peak demand on invalid revisions but can add
static-check waiting to otherwise passing runs; it is not a measured reduction
in end-to-end latency.
Core jobs have execution limits of five minutes for classification, ten minutes
for static checks, builds, docs, and report merging, and thirty minutes per
portable test shard. These limits leave headroom for cold setup and full coverage
while preventing a hung job from holding the active main run for hours. They do
not bound runner queue waiting. Timeouts still fail the required gate.
The separate `CI timings` workflow batches recent completed CI and Release runs
hourly or on manual dispatch. It uploads `ci-job-timings-batch` with raw API pages and
a normalized point-in-time report for each collected workflow attempt, plus a
batch manifest describing examined coverage, failures, and limits. Overlapping
lookback windows may collect an attempt again; identify records by run ID and
attempt. Each batch examines at most 100 completed runs per workflow created in
the preceding 24 hours, with a four-minute API collection budget. It collects the
latest attempt of each selected run and does not claim an exhaustive archive.
It runs the reporter from the trusted default branch with read-only permissions,
treating workflow runs as API data. It does not allocate a new reporter job for
every completed, cancelled, or skipped workflow. Reports separate queue
waiting (`created_at` to `started_at`) from completed execution and report
percentiles plus matched `npm ci` and artifact-step durations.
Queue excludes dependency waiting before job creation and does not establish
whether account concurrency or runner availability caused the wait. Queued jobs with
placeholder start times, skipped jobs, and missing or invalid timestamps remain
unknown. The reporter targets each selected run and attempt explicitly.
Summed job elapsed time is parallel work, not workflow wall time, CPU time,
memory usage, or billing. Step names identify samples, not complete transfer
costs. Its queue, execution, and failures are independent of the required gate
and publication. CPU time and peak process-family memory are not measured by this API.
The build job saves portable Turbo outputs across runs. Eight Linux real-system
lanes restore this cache without waiting for the build job. The shared action
@@ -698,6 +742,44 @@ specifications, independent mappings, unchanged bytes, and decompilation of a
known return value. AArch64 remains covered by deterministic tests rather than
this real-provider lane. See [raw binary analysis](raw-binary.md).
`npm run verify:ghidra:address-context` runs on Linux with a caller-installed
Ghidra, full JDK, and an x86-64 ELF GCC toolchain (`CC` selects another compatible
compiler). It builds
`tests/conformance/c/pie-address-context.c` as a PIE ELF without executing it.
The compiled CLI and real stdio MCP workflow must expose the measured image
base, retain segment address spaces, agree on the overview, resolve explicit and
numeric file offsets, and reject unmapped analysis addresses with actionable
guidance. It checks the reverse `address-to-file-offset` mapping, file-coordinate
Evidence locations, and the suggested CLI search and load-image commands.
The verifier compares mapped bytes with the source file and checks source
identity, session preservation after failed lookup, owned runtime cleanup, and
verified empty process lineage. This lane does not establish Windows or macOS
real-provider coverage.
For source-identity boundaries that a normal single-file import cannot create,
`tests/conformance/ghidra/ReaFileOffsetSourceProbe.java` exercises the production
bridge against real Ghidra `FileBytes`. It rejects auxiliary and partial sources,
checks the original target's round trip, and retains ambiguity when duplicate
target bytes have distinct loaded mappings. Injected decompiler failures and
cancellation verify that completed source/function resolution facts and backend
failure codes survive; these injections do not establish natural engine failures.
On Windows, after creating the
source-owned PE fixture, run from the repository root:
```powershell
node scripts/create-ghidra-windows-fixture.mjs
$proofRoot = Join-Path $env:TEMP 'rea-file-offset-source-proof'
New-Item -ItemType Directory -Path $proofRoot -Force | Out-Null
Copy-Item bridge/ghidra/ReaGhidraBridge.java, bridge/ghidra/ReaGhidraExtensions.java, tests/conformance/ghidra/ReaFileOffsetSourceProbe.java -Destination $proofRoot
& "$env:GHIDRA_INSTALL_DIR/support/analyzeHeadless.bat" $proofRoot source-proof -import "$PWD/build/fixtures/rea-ghidra-windows.exe" -scriptPath $proofRoot -postScript ReaFileOffsetSourceProbe.java -deleteProject
```
Require the `REA_FILE_OFFSET_SOURCE_OK` marker and no script errors; Ghidra may
exit successfully even when a post-script fails. The probe edits only its
temporary analysis database and does not execute the fixture. This producer
regression supplements public CLI/MCP coverage and does not establish Linux
host behavior or native process-supervision controls.
## Apple Interface Builder archives
`npm run verify:interface-builder` compiles the source-owned AppKit XIB into a
@@ -723,6 +805,27 @@ remain present and fail if classification fails. Every code change retains the
complete six-shard Linux suite; individual test files are not selected by
imports or filenames.
Ordinary PRs also produce an **observational** test-selection plan. The candidate
leaf-adapter ownership table lives in `scripts/ci/test-selection.mjs`, separately
from provider-job scopes. It proposes keeping affected owners plus every unowned
test, including all CLI/MCP, acceptance, composition, conformance, and process
tests. Shared inputs, unknown paths, missing owner tests, and removed tests fall
back to the complete inventory. Renames account for both paths. Main, scheduled,
manual, and `ci:full` runs always propose the full suite.
All six shards still execute the complete suite. The existing report merge
compares the proposal with actual module outcomes and retains
`.cache/ci-test-selection/report.json` in `test-results`, including selected and
omitted paths, changed input paths, missed failures, unobserved files, scheduling weights, PR head and
executed merge SHA. This adds no test reruns or runner jobs. Missing, duplicate,
skipped, interrupted, or unhandled results cannot establish a complete comparison;
even a complete passing comparison is not proof of ownership completeness.
Before enabling omissions, review representative full-run comparisons and
cross-owner consumers, including failures, and measure completed CI feedback and
runner demand. There is no automatic promotion or switch to skip tests. The
initial proposal measures only leaf producer-test omissions; it does not promise
the savings of a broader affected-test planner.
`scripts/ci/plan.mjs` selects lanes using the ownership table in
`scripts/ci/scopes.mjs`. Provider implementation changes run their real-provider
verifiers and the Linux x64 installed-package/Inspector baseline. The `apple`
@@ -737,9 +840,19 @@ lane. Edits to verifier entrypoints run only by manual, licensed or self-hosted
workflows run static checks and the Linux suite. Website changes run website
checks. Provider workflow changes validate the workflow and run that provider's
verifier. Manual-provider/Android workflow changes run workflow validation only.
The four-version SQLite compatibility matrix has its own `sqlite` scope for
SQLite implementation, contracts, CLI/MCP registration, fixtures, and shared
runtime boundaries. SQLite and server-lifecycle test edits and the SQLite npm
scripts also select it. Unrelated code edits retain the complete portable suite
on the pinned Node version without starting four additional compatibility jobs.
Full qualification retains every SQLite version.
Shared process/contract/CLI boundaries, dependencies, other packaging inputs,
unknown paths, and changes to the planner select the full baseline. Package
script-only edits select their owning verifier when its entrypoint is
unknown paths, and changes to the planner select the full baseline.
CI timing collection, timing history, and test-report helper edits retain static
checks and the complete portable suite without selecting unrelated host/provider
lanes. Classification, qualification, gate, release receipt, and unknown CI
scripts keep the broad fallback.
Package script-only edits select their owning verifier when its entrypoint is
identifiable; unknown or shared script changes select everything. Android
build-script edits retain the Linux suite and workflow validation; real Android
verification stays manual. Renames account for both paths.
@@ -760,23 +873,46 @@ Every code change still runs all six portable shards, without coverage
instrumentation in this fast tier. This deliberately avoids relying on an
import graph for generated files, filesystem inputs, subprocesses, or bridges.
Full qualification runs hourly at minute 23, on manual dispatch, and for the
exact release revision before publication. It includes aggregate coverage,
every portable provider lane, and all native hosts below. A `ci:full` PR label
selects full qualification on the next PR run; manual dispatch can request it
immediately. Fast-tier broad fallbacks retain all owner lanes and the three-host
Scheduled qualification is admitted hourly at minute 23. A forced full run at
03:23 UTC refreshes runner and dependency drift every day. Other hourly runs
can reuse a successful full-qualification receipt for the exact source SHA
from the same repository's trusted CI workflow on its default branch. Both the
receipt and original workflow run must be less than 24 hours old, so retrying an
old gate cannot renew stale proof. Missing, expired, failed, foreign, or uncertain proof runs full
validation; API lookup failure also runs full. Lookups examine at most 100 named
receipts and three candidate runs within a 20-second budget. Reused runs select
no validation lanes, identify the original run, and produce no new receipt, so
reuse cannot indefinitely extend proof freshness. They make no fresh provider
observation. Manual dispatch, `ci:full`, and exact release qualification always
run full validation.
Full qualification includes aggregate coverage,
every portable provider lane, and all native hosts below. Adding `ci:full`
immediately starts the separate `Full PR qualification` workflow against the
label event's merge revision; subsequent PR revisions also select the full tier
in normal CI while that label remains. Unrelated labels do not start validation
jobs or replace the main CI checks. Manual dispatch also requests full validation.
Fast-tier broad fallbacks retain all owner lanes and the three-host
package matrix; Intel macOS and coverage remain full-tier obligations. Each
classification job records its selection and broad-fallback reasons in the run
summary. Representative Git-diff cases live in
`tests/boundary/filesystem/ciChangeScope.test.ts`.
`CI required` always runs and checks classification and every selected lane,
rejecting failed, cancelled, or unexpectedly skipped work. Branch protection
should require this aggregate, `Static checks`, and `Portable tests`. Fast runs
cancel superseded feedback for the same PR or main branch. Scheduled/manual
qualification has a separate concurrency group: active work finishes and only
the latest pending run is retained. Release qualification is grouped by its
exact source SHA and cannot be cancelled by newer main pushes. Provider and
rejecting failed, cancelled, or unexpectedly skipped work. It rejects missing or
unknown tiers and requires every canonical scope to be explicitly `true` or
`false`; incomplete classification cannot silently omit verification lanes.
Branch protection
should require this aggregate, `Static checks`, `Portable tests`, and
`Conventional PR title`. Title validation runs separately on PR opening,
reopening, commits, and edits; correcting a title refreshes only this lightweight
check. It reads title text through an environment variable, executes no PR code,
and has no token permissions. Its separate concurrency group cannot cancel
test runs. Fast PR runs cancel superseded feedback for the same PR. Main push
runs keep one active run and the latest pending run for that branch.
Scheduled/manual qualification has a separate concurrency group: active work
finishes and only the latest pending run is retained. Release qualification uses
its exact source SHA and cannot be cancelled by newer main pushes. Provider and
website lanes inherit their caller's scheduling; reusable
workflows have no independent cancellation group. Direct manual lane dispatches
run independently on disposable hosted runners. Publication
@@ -830,8 +966,11 @@ catalog, and managed-conformance metadata. Each shard still installs the locked
dependencies and runs its assigned tests; full qualification enables coverage. It no longer
regenerates the same metadata independently.
The build job also retrieves the merged timing report from the last successful
default-branch scheduled qualification and includes that same report in each shard's snapshot.
The build job also finds an available `test-timings` artifact from a successful
trusted default-branch scheduled or manual qualification and includes that same
report in each shard's snapshot. The bounded lookup verifies the artifact's
producer; newer schedules that reuse qualification without producing timings do
not hide the last full run's artifact.
CI assigns discovered files by historical duration, with a deterministic
fallback weight for new files, rather than dividing only by file count. Every
discovered file still belongs to exactly one shard; worker limits and project
@@ -843,6 +982,16 @@ timing data is reused; live provider results are never cached. Turbo outputs are
restored by task hash, with trusted default-branch runs as cache writers and PRs
as restore-only consumers. npm caches hold downloads, not `node_modules`.
The eight automatic browser, binary-layout, EVM, JavaScript-recovery,
recorded-crash, and web provider lanes download `rea-build-cache` from their own
run after the build and static jobs succeed. Their existing build commands still
validate task hashes and rebuild mismatched inputs; provider observations always
run. Provider-workflow-only edits also admit the build without enabling source
test shards. Direct provider dispatches retain the cross-run restore-only cache.
This removes repeated compilation on fresh source revisions, but adds the build
as a prerequisite: a delayed build can delay provider admission. Compare queue
and execution times before claiming an end-to-end speedup.
These native baseline checks complement the Linux source-test shards and the
separate Apple-artifact and real-provider lanes. Actual Hopper, Ghidra, IDA,
browser and managed-tool claims require their corresponding verification lanes.
+1
View File
@@ -182,6 +182,7 @@
"verify:ghidra:dos": "npm run build:cached && node scripts/verify-real-ghidra-dos.mjs",
"verify:ghidra:com": "npm run build:cached && node scripts/verify-real-ghidra-com.mjs",
"verify:ghidra:raw": "npm run build:cached && node scripts/verify-real-ghidra-raw.mjs",
"verify:ghidra:address-context": "npm run build:cached && node scripts/verify-real-ghidra-address-context.mjs",
"verify:ghidra:aarch64-jump-table": "npm run build:cached && node scripts/verify-real-ghidra.mjs --aarch64-jump-table",
"verify:interface-builder": "npm run build:cached && node scripts/verify/apple/interface-builder.mjs",
"verify:ghidra:cross-format": "npm run build:cached && node scripts/verify-real-ghidra.mjs --cross-format",
+525
View File
@@ -0,0 +1,525 @@
:robot: I have created a release *beep* *boop*
---
## [6.5.0](https://github.com/morluto/rea/compare/rea-agents-6.4.0...rea-agents-6.5.0) (2026-10-13)
###   BREAKING CHANGES
* **runtime:** attached fields in JavaScript runtime, browser, and Electron target discovery and inspection results may be null when discovery does not report attachment state.
* **sourcemaps:** Browser source-map original and mapping rows expose null for unavailable source names instead of inventing an unknown-source string. New index fields are optional when reading older retained evidence.
* **analysis:** get_call_graph normalized_result now contains { graph, limitations }; traversal-depth groups are under graph.
* **mcp:** binary_session no longer includes server_identity.catalog.tools; tool_availability and provider capabilities use grouped facts documented in docs/mcp-contracts.md.
### Features
* add Frida instrumentation support ([#1359](https://github.com/morluto/rea/issues/1359)) ([4a4ae9b](https://github.com/morluto/rea/commit/4a4ae9b2b9712f7ac16f2d23c71b5c29b0c23d2f))
* **analysis:** add opt-in retained inventory and WASM views ([#2122](https://github.com/morluto/rea/issues/2122)) ([58fb372](https://github.com/morluto/rea/commit/58fb372071e56da9c5df0e37593ee3f0da6b64bc)), closes [#2016](https://github.com/morluto/rea/issues/2016)
* **android:** add opt-in package summaries and retained manifest views ([f254f5a](https://github.com/morluto/rea/commit/f254f5ac2b804b353a711264bb178f43fdf07867))
* **browser:** inspect retained aioquic qlog JSON captures ([bb154d1](https://github.com/morluto/rea/commit/bb154d1067b0fc250452b7d468d0b0523031a16f))
* **captures:** discover installed protocol and field definitions ([#2496](https://github.com/morluto/rea/issues/2496)) ([9c34c09](https://github.com/morluto/rea/commit/9c34c091b3b8c47f8bd18d6f0321efde2dedb92a))
* **captures:** inspect recorded packets with bounded TShark delivery ([#2318](https://github.com/morluto/rea/issues/2318)) ([f611d8c](https://github.com/morluto/rea/commit/f611d8ce722cd36345e9ff83b9669ef2e7fe57a9))
* **clients:** register Qoder CLI during setup ([#1572](https://github.com/morluto/rea/issues/1572)) ([1097693](https://github.com/morluto/rea/commit/109769319a4a3a5f85e8a43bb419d04aefff66a4))
* **clients:** register Trae during setup ([#1573](https://github.com/morluto/rea/issues/1573)) ([bfa805d](https://github.com/morluto/rea/commit/bfa805decf83744bd3ecb0d1402667cf3ed5c3b2))
* **clients:** register ZCode during setup ([#1564](https://github.com/morluto/rea/issues/1564)) ([51d6c87](https://github.com/morluto/rea/commit/51d6c87cdbf5f11eaf3cb1862befe704a81827c5))
* **evm:** add retained interface summary delivery ([#2308](https://github.com/morluto/rea/issues/2308)) ([f76163c](https://github.com/morluto/rea/commit/f76163c2467db623f9de0bda757c9d8610ca9565)), closes [#2016](https://github.com/morluto/rea/issues/2016)
* **fiddler:** inspect native SAZ captures ([a2d8b94](https://github.com/morluto/rea/commit/a2d8b94dd50dab8d966239dd63ab84718aa8b4b4))
* **flutter:** identify Dart snapshot builds and inspect AOT payloads by pure APK parsing ([#1582](https://github.com/morluto/rea/issues/1582)) ([e4d3e13](https://github.com/morluto/rea/commit/e4d3e1399d9cd4932653e7015a1bf8589e56029d))
* **ghidra:** admit bounded PSP ELF through installed Allegrex ([#1599](https://github.com/morluto/rea/issues/1599)) ([e4407db](https://github.com/morluto/rea/commit/e4407dbed5dc24851f47532f5e3d75d5627d3e7d))
* **ghidra:** admit relocatable MIPS32r2 ELF objects ([#1950](https://github.com/morluto/rea/issues/1950)) ([290d301](https://github.com/morluto/rea/commit/290d301ec770955f313c2d48678ed0ae3b9f8a73))
* **ghidra:** admit RISC-V ELF analysis through a pinned generic lane ([#2151](https://github.com/morluto/rea/issues/2151)) ([0baad58](https://github.com/morluto/rea/commit/0baad58e606f12a8dc6cef06253f85350378e5b1))
* **ghidra:** admit soft-float MIPS32r2 ELF with integer-register float storage ([#1949](https://github.com/morluto/rea/issues/1949)) ([5e219de](https://github.com/morluto/rea/commit/5e219de44134d34f5a07a9cd1911fb2295bf1d88))
* **ghidra:** admit verified MIPS16 shared ELF images ([e9fe643](https://github.com/morluto/rea/commit/e9fe6437a7da381f0aa5aa77571dcf03317122e7)), closes [#1851](https://github.com/morluto/rea/issues/1851)
* **ghidra:** apply validated analyzer overrides before analysis ([9878bb7](https://github.com/morluto/rea/commit/9878bb7296c13c2b4838be76f6128e5fc31eda3c))
* **go:** inspect embedded binary build metadata ([#1363](https://github.com/morluto/rea/issues/1363)) ([a1b5d51](https://github.com/morluto/rea/commit/a1b5d51a8dfb7fff6272d91270c16b95555c2643))
* **harmony:** project HarmonyOS package inventory evidence ([#1447](https://github.com/morluto/rea/issues/1447)) ([f2150eb](https://github.com/morluto/rea/commit/f2150eb9a7ed76748238009ca6666efa52de5271))
* **ida:** support optional native IDA Free MCP in attached mode ([#1544](https://github.com/morluto/rea/issues/1544)) ([d1aaeee](https://github.com/morluto/rea/commit/d1aaeee400e0d9911d5c53339ca3ac42dccbbba3))
* **javascript:** analyze selected application sources ([85c1d25](https://github.com/morluto/rea/commit/85c1d2587a7d42c6e2108405c802fe7f7b046c15)), closes [#1848](https://github.com/morluto/rea/issues/1848)
* **javascript:** recover code-split bundles from multiple inputs ([#1686](https://github.com/morluto/rea/issues/1686)) ([12c4160](https://github.com/morluto/rea/commit/12c416005f9d2abf438f4df4447972424d781b65))
* **javascript:** resolve exact package import aliases ([#1862](https://github.com/morluto/rea/issues/1862)) ([d28b41e](https://github.com/morluto/rea/commit/d28b41ea4e93bb4c4d360a3bf2291ca83bb2b378))
* **managed:** add retained metadata overview and pages ([#2413](https://github.com/morluto/rea/issues/2413)) ([82cfcee](https://github.com/morluto/rea/commit/82cfcee19f18b0eeae9a330ba04e0411cdf85225))
* **managed:** select declaring types in inspect_managed_members ([a149d18](https://github.com/morluto/rea/commit/a149d183efd08b0ad7c2a35c99f2394d905621d5)), closes [#1878](https://github.com/morluto/rea/issues/1878)
* **mcp:** add opt-in paging for retained unknown heads ([9a18251](https://github.com/morluto/rea/commit/9a18251426f891e6a24856b715ef8002ab8b9610))
* **mcp:** derive an API surface view from retained web network captures ([#1891](https://github.com/morluto/rea/issues/1891)) ([880a603](https://github.com/morluto/rea/commit/880a603fe89db2132fd05a35b33a8c459673beda))
* **native:** admit raw binaries with caller-supplied architecture and entry ([#1868](https://github.com/morluto/rea/issues/1868)) ([7c207c7](https://github.com/morluto/rea/commit/7c207c79512f6baa70f4ed9d7d5334ab2d62f3ac))
* **native:** expose typed incoming references at exact addresses ([#1715](https://github.com/morluto/rea/issues/1715)) ([cdd9fe7](https://github.com/morluto/rea/commit/cdd9fe716774c9e5abd121b70e9fd5e1eb91d2b8))
* **pcap:** add bounded capture summaries through CLI and MCP ([9581e06](https://github.com/morluto/rea/commit/9581e067ad306aab842024e47f2dd18ac8056859))
* **server:** frame analysis-target content as untrusted data ([#1908](https://github.com/morluto/rea/issues/1908)) ([85e1242](https://github.com/morluto/rea/commit/85e124253ac93dd443be6bbcee0939d525b90d4e))
* **setup:** register DeepSeek Harness through setup, doctor and uninstall ([#1929](https://github.com/morluto/rea/issues/1929)) ([d9d2a17](https://github.com/morluto/rea/commit/d9d2a173819ce8e2e7f09eb584ec6ff7b4ee3480))
* **signals:** decode recordings with bounded sigrok output ([#2339](https://github.com/morluto/rea/issues/2339)) ([91b8f58](https://github.com/morluto/rea/commit/91b8f587cd495c509946e69076525eb19dc1ca65))
* **signals:** discover installed decoders and option help ([#2487](https://github.com/morluto/rea/issues/2487)) ([4276aaf](https://github.com/morluto/rea/commit/4276aaf18bb607cdd0182280f76cf50fcd110c3c))
* **sqlite:** inspect offline snapshots with selective evidence ([#2271](https://github.com/morluto/rea/issues/2271)) ([aeee507](https://github.com/morluto/rea/commit/aeee5074d124414239b6dd440dc79c26bcbd58a4))
* **target:** explain static WASM inspection after native rejection ([1f17ac8](https://github.com/morluto/rea/commit/1f17ac82282dfda1ae2dc87d99e66a8a039c0259)), closes [#1852](https://github.com/morluto/rea/issues/1852)
* **update:** update Bun global installations ([#1735](https://github.com/morluto/rea/issues/1735)) ([e4027e4](https://github.com/morluto/rea/commit/e4027e4afccc6316cd9cfc27c7b0427e66ceb91f))
* **wasm:** add opt-in first-call summary delivery ([#2193](https://github.com/morluto/rea/issues/2193)) ([0b71453](https://github.com/morluto/rea/commit/0b7145361ec02d27260eb1f5bc0be955c9374a96))
* **wasm:** inspect local artifacts with WABT ([#1533](https://github.com/morluto/rea/issues/1533)) ([ffaf2c1](https://github.com/morluto/rea/commit/ffaf2c12f1793c7c451e66dc09c3074544a4c086))
* **website:** add a persistent light and dark theme switch ([4773c15](https://github.com/morluto/rea/commit/4773c152bc5f299fa682575bb8d1efea7587c7bc))
* **website:** add multilingual editions and translation workflow ([a2d3338](https://github.com/morluto/rea/commit/a2d3338e90a3a11385e2f63bedaa9750612f8719))
* **website:** follow the system dark mode and fix small-screen text ([#1894](https://github.com/morluto/rea/issues/1894)) ([12ee785](https://github.com/morluto/rea/commit/12ee785e5e61132898adecf9424ce6706a2e754a))
### Bug Fixes
* **adb:** admit canonical device states and preserve malformed output ([e1cd292](https://github.com/morluto/rea/commit/e1cd292aa6e8f5f13441d408c66126782c8dbb0d))
* **adb:** count requested permissions that the device marks restricted ([#2359](https://github.com/morluto/rea/issues/2359)) ([15463bd](https://github.com/morluto/rea/commit/15463bdbe605b9dd11da5dbba031f99cd1ec9190))
* **adb:** count UTF-8 bytes in logcat total_bytes ([#2212](https://github.com/morluto/rea/issues/2212)) ([ec2d251](https://github.com/morluto/rea/commit/ec2d251c0ad8382e0665546be5ec8f5f1c175962))
* **adb:** evaluate the full activity start report ([c89972c](https://github.com/morluto/rea/commit/c89972c8777d0114566fe76a060849a18bdd2659)), closes [#2225](https://github.com/morluto/rea/issues/2225)
* **adb:** preserve device paths and prevent destination collisions ([55b6148](https://github.com/morluto/rea/commit/55b61485e9f7568b69597eef8696ff6abfe53cfd))
* **adb:** preserve directory identities and share byte capture ([3bdc873](https://github.com/morluto/rea/commit/3bdc8730bd98e073522bcb8ba86527be402c0cbe))
* **adb:** preserve existing bugreport archives ([#2221](https://github.com/morluto/rea/issues/2221)) ([d80b55e](https://github.com/morluto/rea/commit/d80b55e11bc47c6f999dac7dcb8382219d65739a))
* **adb:** reject unsafe host file names when pulling one device file ([#1962](https://github.com/morluto/rea/issues/1962)) ([1d4c4eb](https://github.com/morluto/rea/commit/1d4c4eb0308b4cb5761a91533edeb33fcfb442f2))
* **adb:** share actionable provider configuration refusals ([c42232f](https://github.com/morluto/rea/commit/c42232f566d55fe7191763649755148c768eef9f))
* **adb:** share host configuration refusals with apktool ([#1954](https://github.com/morluto/rea/issues/1954)) ([39418ea](https://github.com/morluto/rea/commit/39418ea173763ca55d4f116992ee903bcaeb1898))
* align MCP archive contracts and documentation ([#2478](https://github.com/morluto/rea/issues/2478)) ([a86deec](https://github.com/morluto/rea/commit/a86deecac5fa8434a4f50482f86b0c6ab486a936)), closes [#2454](https://github.com/morluto/rea/issues/2454)
* **analysis:** expose provider image base ([#2083](https://github.com/morluto/rea/issues/2083)) ([81cca2e](https://github.com/morluto/rea/commit/81cca2e0e3ebd98c21f41d7ad63d6a00e4cbc3f9))
* **analysis:** preserve producer identity and partial evidence ([6fb3a34](https://github.com/morluto/rea/commit/6fb3a345fe952b8e8f59221aec993acc37ccbf31))
* **analysis:** preserve relationship coverage across evidence and replay ([9c85114](https://github.com/morluto/rea/commit/9c8511409c0706f9979c2b68dd56aaaf9f21e191))
* **analysis:** scope call-graph caveats to their observed relationships ([5dfb9d0](https://github.com/morluto/rea/commit/5dfb9d050c2cc44a61b183dae765bef4d22a53b7))
* **android:** bound inline search Evidence with retained inventory provenance ([#1890](https://github.com/morluto/rea/issues/1890)) ([7a0140b](https://github.com/morluto/rea/commit/7a0140b4aa43276368058ab98826b77e040ea14f))
* **android:** preserve decoded values and duplicate class inventories ([0677468](https://github.com/morluto/rea/commit/0677468877465c8dfbe25c72f142a7b5bc8a3212)), closes [#1978](https://github.com/morluto/rea/issues/1978)
* **android:** preserve executable static initializer evidence ([#1876](https://github.com/morluto/rea/issues/1876)) ([efbc25f](https://github.com/morluto/rea/commit/efbc25fbea1b6ade2a284a4916644c791f83bf49)), closes [#1846](https://github.com/morluto/rea/issues/1846)
* **android:** reject malformed UTF-8 at the JADX protocol boundary ([fe8d0bd](https://github.com/morluto/rea/commit/fe8d0bdd36fe12bc0836468d1a702a34fbdc219b))
* **apktool:** accept --no-strings CLI option ([#2707](https://github.com/morluto/rea/issues/2707)) ([11f8b76](https://github.com/morluto/rea/commit/11f8b7625788d747eaadd59af6efdf88587ed058))
* **apktool:** name host requirements in configuration refusals ([#1946](https://github.com/morluto/rea/issues/1946)) ([7cdb798](https://github.com/morluto/rea/commit/7cdb7981c3612bc42bd0153fddd9c33b639918ed))
* **apktool:** run Windows batch launchers through cmd.exe ([#1777](https://github.com/morluto/rea/issues/1777)) ([ae95e68](https://github.com/morluto/rea/commit/ae95e68ba5fa95cf2ec0453b50ffd217cabf5359)), closes [#1774](https://github.com/morluto/rea/issues/1774)
* **apple:** decode UIKit keyed-archive outlet connections ([8d4d631](https://github.com/morluto/rea/commit/8d4d631606c701aac150cf009d3b80ae4a7ae7d5))
* **apple:** derive dyld environment findings from each traced slice ([#2733](https://github.com/morluto/rea/issues/2733)) ([52d4ce6](https://github.com/morluto/rea/commit/52d4ce6ced1c3b6ae7570a5df7f4499cd65a1182)), closes [#2661](https://github.com/morluto/rea/issues/2661)
* **apple:** join recognized keys from NIBArchive field maps ([#2248](https://github.com/morluto/rea/issues/2248)) ([fd985db](https://github.com/morluto/rea/commit/fd985dbc5b9435918732c0cc1825060af2a3a47e))
* **apple:** limit already-loaded reuse to rpath dependencies ([#2292](https://github.com/morluto/rea/issues/2292)) ([60f771a](https://github.com/morluto/rea/commit/60f771a11e9889ce69cce6ad631b6b186ac742dd))
* **apple:** preflight XML plist nesting before keyed archive decoding ([#2161](https://github.com/morluto/rea/issues/2161)) ([7711b00](https://github.com/morluto/rea/commit/7711b0000592aa80c7b0412004150313f78feabc))
* **apple:** preserve archive signatures and metadata byte identity ([dc038d1](https://github.com/morluto/rea/commit/dc038d16bba9046afd28970de9c47f6259a43524))
* **apple:** preserve ARM64E X1 architecture identity ([f598bab](https://github.com/morluto/rea/commit/f598bab488e38c378d69346fa3c106f9e18529ed))
* **apple:** preserve slice identity and macOS boundary diagnostics ([#1772](https://github.com/morluto/rea/issues/1772)) ([5c29565](https://github.com/morluto/rea/commit/5c2956587a1b0d431f72e10df07a1a6363fa066c))
* **apple:** validate build-version tool records within cmdsize ([#1784](https://github.com/morluto/rea/issues/1784)) ([817aef4](https://github.com/morluto/rea/commit/817aef406861c2ce3f87ad3c912ea6c235bcdf12))
* **artifacts:** authenticate file subjects across provider and view boundaries ([3c57e1a](https://github.com/morluto/rea/commit/3c57e1a8b21b379fca55032d177078e1d8426e64))
* **artifacts:** bind directory reads to captured file state ([918713a](https://github.com/morluto/rea/commit/918713af376e307ecf24d75e6a0dd9d1b544545b))
* **artifacts:** bound archive decompression by source size ([#2144](https://github.com/morluto/rea/issues/2144)) ([c59fbf5](https://github.com/morluto/rea/commit/c59fbf5e9c3b856d042f02cb18ad4de077eaeab5))
* **artifacts:** close mounted directory resources before DMG detach ([2260887](https://github.com/morluto/rea/commit/2260887f2092be6a3d9c79509e4ae4a3e52fce7f))
* **artifacts:** compare leaf inventories using explicit coverage ([ed485a3](https://github.com/morluto/rea/commit/ed485a3dc8cc3a4baf8cabe01acb7aa0490b73b3))
* **artifacts:** compare package signatures without ASCII folding ([#2069](https://github.com/morluto/rea/issues/2069)) ([75a4a14](https://github.com/morluto/rea/commit/75a4a14fa05d352762ffaefd3cd2e16515842b03))
* **artifacts:** count a changed partial-inventory root once ([#1854](https://github.com/morluto/rea/issues/1854)) ([ee5275c](https://github.com/morluto/rea/commit/ee5275cd9fae8db64f945a0c8b59e1c19ac402c3)), closes [#1844](https://github.com/morluto/rea/issues/1844)
* **artifacts:** distinguish Java classes from universal Mach-O ([921bb95](https://github.com/morluto/rea/commit/921bb9575505065f6cfd57b14d97d80d0f238546)), closes [#2317](https://github.com/morluto/rea/issues/2317)
* **artifacts:** honor cancellation at the output publication boundary ([e7882b3](https://github.com/morluto/rea/commit/e7882b34f4ac4a26f903c5f49264acc2c19bb562))
* **artifacts:** mark Windows host executables by extension ([#1823](https://github.com/morluto/rea/issues/1823)) ([91f843b](https://github.com/morluto/rea/commit/91f843bf38b0e4ec8576f41f3140488b4ee8ff32)), closes [#1819](https://github.com/morluto/rea/issues/1819)
* **artifacts:** mark Windows launchable files as host executables ([#1824](https://github.com/morluto/rea/issues/1824)) ([e8491e0](https://github.com/morluto/rea/commit/e8491e023a255f790ae9850225084d7495e9ade0)), closes [#1819](https://github.com/morluto/rea/issues/1819)
* **artifacts:** preserve decode coverage and cleanup ownership ([a73a55f](https://github.com/morluto/rea/commit/a73a55f53cc2b2e5b7618effe6ecb5cee6e0cee2))
* **artifacts:** preserve emitted ZIP entry identity ([e36c82b](https://github.com/morluto/rea/commit/e36c82becdd613787bcc59903f8b5fe14eb2f105))
* **artifacts:** preserve inventory uncertainty in application projections ([d8ceb3f](https://github.com/morluto/rea/commit/d8ceb3f276a65c181b146f68c0729ff4b6f9fa61))
* **artifacts:** preserve NIB field and class identities ([#2106](https://github.com/morluto/rea/issues/2106)) ([f8e9579](https://github.com/morluto/rea/commit/f8e95799f941e9e55e0d47841896b401bab67d29))
* **artifacts:** preserve physical ZIP member identities ([#1865](https://github.com/morluto/rea/issues/1865)) ([bcd350c](https://github.com/morluto/rea/commit/bcd350c365030a5d210208c0a8dc238302c43303)), closes [#1860](https://github.com/morluto/rea/issues/1860)
* **artifacts:** preserve ZIP identities and partial managed metadata ([74d7da6](https://github.com/morluto/rea/commit/74d7da6ced0f869936ee0316b00616931b088066))
* **artifacts:** read ASCII control bytes in legacy ZIP names as ASCII ([#2365](https://github.com/morluto/rea/issues/2365)) ([9928f35](https://github.com/morluto/rea/commit/9928f357fb9c420e6384e155d74586dfe37d4533))
* **artifacts:** read every ZIP package family extraction is asked to open ([#2493](https://github.com/morluto/rea/issues/2493)) ([7017c4d](https://github.com/morluto/rea/commit/7017c4d68cab49aed998283e09069d9d791e1b0b))
* **artifacts:** reject malformed serialized keys and integer values ([9435c7a](https://github.com/morluto/rea/commit/9435c7aae279ba184937c0dfeb713fd574ba3f29))
* **artifacts:** retain ASAR resources through cleanup failures ([eb97da3](https://github.com/morluto/rea/commit/eb97da33881e3d1472d97a7d788ee35944fb6b07))
* **artifacts:** retain read cleanup ownership through CLI and MCP shutdown ([b955b82](https://github.com/morluto/rea/commit/b955b825fc002a4bb408f2910b78768c566c2cd1))
* **artifacts:** retry transient DMG verification refusals ([bb54082](https://github.com/morluto/rea/commit/bb540826d65316c164d2de8ba4ef10f81a8ad073))
* **browser:** bound CDP discovery response size and lifetime ([951b095](https://github.com/morluto/rea/commit/951b095e11adf9faa9c4a27e212260060aa6b095))
* **browser:** compare only observed HTTP response facts ([486b567](https://github.com/morluto/rea/commit/486b56724c4eaba50719177bcb9eae84d6907297))
* **browser:** compare PNG IHDR bytes without ASCII folding ([#2099](https://github.com/morluto/rea/issues/2099)) ([fdd8f82](https://github.com/morluto/rea/commit/fdd8f826023c568937d237c2f97d855de283a9b8))
* **browser:** exclude sensitive binary lengths from historical captures ([#2477](https://github.com/morluto/rea/issues/2477)) ([68ecf3d](https://github.com/morluto/rea/commit/68ecf3df8c666fdbe1ced8089c64340af614d1c3))
* **browser:** isolate script URLs with empty query or fragment ([#2074](https://github.com/morluto/rea/issues/2074)) ([9b780c4](https://github.com/morluto/rea/commit/9b780c47bca039939dd5f73b78541f3ea566a7dd))
* **browser:** keep elapsed capture timing independent of wall-clock changes ([6bd2b28](https://github.com/morluto/rea/commit/6bd2b28161411f70ed5dc72e42c2736a1aab758b))
* **browser:** preserve attached service-worker policies and ownership ([221c856](https://github.com/morluto/rea/commit/221c85648108e9846d9920b0964dafa7f108d581))
* **browser:** preserve body API provenance through script export ([966f7cc](https://github.com/morluto/rea/commit/966f7cc84b4a8bb0043641cb64d7a7aaf8715e05))
* **browser:** preserve uncertainty in malformed network observations ([a8d4984](https://github.com/morluto/rea/commit/a8d4984404b611e60b88bc42289c0ba07eee4578))
* **browser:** redact encoded declared secrets consistently ([f09bb8a](https://github.com/morluto/rea/commit/f09bb8a99773f79c7306b8799c86f856fe2c0d4b))
* **browser:** report mismatched event family selection as unknown in capture comparison ([#2239](https://github.com/morluto/rea/issues/2239)) ([0043d9b](https://github.com/morluto/rea/commit/0043d9b134c0a353e8bf85c0bde4492275f1f720))
* **browser:** retain effective source-map response urls ([062723e](https://github.com/morluto/rea/commit/062723e44e937c1e1df865e9ae16184b41e70ae8))
* **browser:** retain module resolver cleanup ownership ([a82e37c](https://github.com/morluto/rea/commit/a82e37c8065cc200a0ca3e14e7c4f85e4e468580))
* **build:** preserve LF text checkouts on Windows ([#1807](https://github.com/morluto/rea/issues/1807)) ([031d778](https://github.com/morluto/rea/commit/031d7781f0f6c99e5f14e67f56cb89a88d6af3eb))
* **build:** preserve resource controls in strict Turbo tasks ([#1809](https://github.com/morluto/rea/issues/1809)) ([2e22330](https://github.com/morluto/rea/commit/2e223300375101558f90b6e78ef9612c55626673))
* **build:** resolve rolldown from the npm registry in package-lock.json ([#1986](https://github.com/morluto/rea/issues/1986)) ([f889a69](https://github.com/morluto/rea/commit/f889a69fb9efe42dd0120c96ff44b4298c1c7c6c))
* **bun:** report the real runtime and enforce worker memory budgets on Windows ([#1730](https://github.com/morluto/rea/issues/1730)) ([66c2157](https://github.com/morluto/rea/commit/66c21572defc97fe277cad49d3173bd4073d1f58))
* **call-path:** preserve relationship coverage from analysis ([#2001](https://github.com/morluto/rea/issues/2001)) ([ed6af7e](https://github.com/morluto/rea/commit/ed6af7eb8cf052c52fec8bfdc5d509377cb30a1a))
* **captures:** name the violated HAR schema constraint ([#2708](https://github.com/morluto/rea/issues/2708)) ([b69d20a](https://github.com/morluto/rea/commit/b69d20a300dba57783a810146f25dacb69f9b8a4))
* **checks:** order artifact-backed lint after compilation ([0d5033a](https://github.com/morluto/rea/commit/0d5033ad12e12abe78fc3c0ba81622b410d22aa1))
* **ci:** align FAT fixtures and consolidate duplicated workflow helpers ([a0317cf](https://github.com/morluto/rea/commit/a0317cf732b3addefad5ac3b234db1e044e3351d))
* **ci:** allow cold Chrome startup in runtime fixtures ([f941968](https://github.com/morluto/rea/commit/f941968eb1860dd41efc2a5201b35cecca42301d))
* **ci:** ignore generated website translation staging ([#2511](https://github.com/morluto/rea/issues/2511)) ([1883149](https://github.com/morluto/rea/commit/18831491cfb5cb98abbffce0cb04dae12a90fe28)), closes [#2508](https://github.com/morluto/rea/issues/2508)
* **ci:** keep cancellation ownership in the calling workflow ([db3c9f0](https://github.com/morluto/rea/commit/db3c9f084bacad34f620ba678924564e20f441f9))
* **ci:** reject incomplete verification plans ([bd2c202](https://github.com/morluto/rea/commit/bd2c202dd930548194cdbc55c27a797f22279db9))
* **ci:** stop aggregate jobs when verification is cancelled ([2e45b4d](https://github.com/morluto/rea/commit/2e45b4d7a5b3228fe8b283def6f047797561bee1))
* **cli:** await browser scenario cleanup on interrupts ([aaeadf2](https://github.com/morluto/rea/commit/aaeadf2d4c4e3f0d0581af2dca33c5bfdc2bcb0e))
* **cli:** normalize empty Windows profile before imports ([#2482](https://github.com/morluto/rea/issues/2482)) ([fa4b828](https://github.com/morluto/rea/commit/fa4b8288655188426222773ead2ccbdaf76334fd))
* **cli:** preserve failure semantics across full-output formats ([570c40c](https://github.com/morluto/rea/commit/570c40cde55d31e455ef9bc484b7b908658ded58))
* **cli:** report an invalid REA_LOG_LEVEL as configuration ([#2140](https://github.com/morluto/rea/issues/2140)) ([3159732](https://github.com/morluto/rea/commit/315973259732544a597b8b17d25c69735910e72e)), closes [#2135](https://github.com/morluto/rea/issues/2135)
* **cli:** retain browser and Inspector input correction details ([#2230](https://github.com/morluto/rea/issues/2230)) ([916ac97](https://github.com/morluto/rea/commit/916ac97bf9f7b976982e438ceb856a9692576cb1))
* **cli:** retain correction details across adjacent browser commands ([#2294](https://github.com/morluto/rea/issues/2294)) ([bb26622](https://github.com/morluto/rea/commit/bb266226707135ad9f181e6c3afd6af0ef62fb22))
* **cli:** retry retained browser scenario cleanup ([4daea94](https://github.com/morluto/rea/commit/4daea941bfeeb426886f2d048aed7f3cb2dbf97b))
* **cli:** reuse firmware and web module input constraints ([48c352b](https://github.com/morluto/rea/commit/48c352b6bfa34c820a530272cd36600158c37b1e))
* **config:** reject NUL bytes in Hopper loader arguments ([#2474](https://github.com/morluto/rea/issues/2474)) ([a07ceae](https://github.com/morluto/rea/commit/a07ceaec36346a7c73e66efe35c8af4654029c84))
* **contracts:** mark foreign execution and device mutation destructive ([#1904](https://github.com/morluto/rea/issues/1904)) ([0b7fc40](https://github.com/morluto/rea/commit/0b7fc40eeef03b06de08d886572c2862331d8947))
* **contracts:** reject OS-invalid local paths in binary and artifact tools ([#1925](https://github.com/morluto/rea/issues/1925)) ([976c2c0](https://github.com/morluto/rea/commit/976c2c083eee1fdd9e89a1a720f768a6ee6df277))
* **dev:** preserve command arguments in exclusive runners ([fd95880](https://github.com/morluto/rea/commit/fd9588089d089e7cb02d12178e34407d6f889f22)), closes [#2319](https://github.com/morluto/rea/issues/2319)
* **docs:** synchronize disclaimer links across translations ([d864ad5](https://github.com/morluto/rea/commit/d864ad54717d92fe4b512ce1170717d9904c1f00))
* **docs:** validate translated README website destinations ([c9136ee](https://github.com/morluto/rea/commit/c9136eef716aa8d32e0301189fbf5a511ff62d5d))
* **domain:** preserve derived byte counts and uncertain identity ([7a2cd1e](https://github.com/morluto/rea/commit/7a2cd1ea0a63e8c44166860b7ee9c7a1aa43492b))
* **domain:** preserve retryable capability failures through redaction ([#2494](https://github.com/morluto/rea/issues/2494)) ([d73ed6f](https://github.com/morluto/rea/commit/d73ed6fce24f6d1372c80319cd41d29d66666159))
* **electron:** attribute unresolved paths to selected inputs ([b7169c8](https://github.com/morluto/rea/commit/b7169c8b747cbc032913b31f008a3b81c77b6219)), closes [#2272](https://github.com/morluto/rea/issues/2272)
* **elf:** centralize fixed header validation across consumers ([34a4bb6](https://github.com/morluto/rea/commit/34a4bb6508006fa113463ef20ed799a2994e9ee8))
* **errors:** stop recommending retries for non-retryable failures ([#1869](https://github.com/morluto/rea/issues/1869)) ([fb94d9d](https://github.com/morluto/rea/commit/fb94d9dde0ccda83ae520ac6fb22b1d283562cf3))
* **evaluation:** authenticate delivered Evidence citations ([844fa52](https://github.com/morluto/rea/commit/844fa52772c1533f7358a71a764da780503775a3))
* **evaluation:** share completed transcript facts across graders ([1447fd2](https://github.com/morluto/rea/commit/1447fd2f990c5d9820337dfd4b8a170f8cad17cd))
* **evidence:** admit bundle imports across session transitions ([#2303](https://github.com/morluto/rea/issues/2303)) ([1230381](https://github.com/morluto/rea/commit/12303812a8d70f198c99c7aeb0df3aaa8166543c))
* **evidence:** authenticate identities and retain extraction provenance ([6bae774](https://github.com/morluto/rea/commit/6bae774328724403255590aa2ddad4d7a2aa727f))
* **evidence:** enforce managed identity and cancel bundle reads ([3bcc68c](https://github.com/morluto/rea/commit/3bcc68c037e9415c78093cb286069c1fcca6a2f8))
* **evidence:** normalize unknown revisions and preserve mutation intent ([#1733](https://github.com/morluto/rea/issues/1733)) ([2165045](https://github.com/morluto/rea/commit/21650455a9c90a26e725c3505a3c17054cc4516c)), closes [#1723](https://github.com/morluto/rea/issues/1723) [#1724](https://github.com/morluto/rea/issues/1724)
* **evidence:** preserve call coverage in function comparisons ([049132a](https://github.com/morluto/rea/commit/049132ab43fa962d31774f2b258c15a71217148b)), closes [#2222](https://github.com/morluto/rea/issues/2222)
* **evidence:** preserve literal keys and response body ownership ([d1dcebc](https://github.com/morluto/rea/commit/d1dcebce2c0820469ed63e33497a14258971b710))
* **evidence:** preserve trace uncertainty and partial provider observations ([e997085](https://github.com/morluto/rea/commit/e9970855ba16ba711816ac30212c5ca17ae085cd))
* **evidence:** reject NUL bytes in subject and snapshot paths ([#1873](https://github.com/morluto/rea/issues/1873)) ([fa9a7e5](https://github.com/morluto/rea/commit/fa9a7e5ec026c5a4e8426f0f36c2c4555be9c9f0))
* **evidence:** reject unsupported metadata before authentication ([acf75e6](https://github.com/morluto/rea/commit/acf75e6700ace28bb1db25e54e56673ac15e0ed3))
* **evidence:** retain unreadable PE and Darwin process facts ([4a4d36f](https://github.com/morluto/rea/commit/4a4d36f881861bbab3c734b0dd459f805766caec))
* **evidence:** validate closure projections and browser body coverage ([aada0ca](https://github.com/morluto/rea/commit/aada0ca8126bf14c60750a4ccd4a79667770a1e4))
* **filesystem:** enforce symlink and decoded-text admission ([ec0b081](https://github.com/morluto/rea/commit/ec0b081371f118e6e5e042fbef3479359c672ddb))
* **firmware:** name missing extractor dependencies in partial extraction ([#1879](https://github.com/morluto/rea/issues/1879)) ([1769417](https://github.com/morluto/rea/commit/176941700118cc33c86e07373cd6950f30548386))
* **formats:** preserve high bits in Windows signature checks ([ac57c88](https://github.com/morluto/rea/commit/ac57c88efe64c3125850b54d3d833227e6b8ec51))
* **frida:** name the argument each Frida input error rejects ([#2217](https://github.com/morluto/rea/issues/2217)) ([86de439](https://github.com/morluto/rea/commit/86de439ba4c9297f721abc60c715fe30fb6ff3df)), closes [#2213](https://github.com/morluto/rea/issues/2213)
* **frida:** reject an unreadable script file before starting a session ([#2211](https://github.com/morluto/rea/issues/2211)) ([c9aecde](https://github.com/morluto/rea/commit/c9aecde580d26d4e7745c2623a4efa327a27367e))
* **ghidra:** derive installation admission from Java property semantics ([2187e16](https://github.com/morluto/rea/commit/2187e16f149a7a7b512a4cb45d5f468cc04091fe))
* **ghidra:** encode Java probe output as UTF-8 ([#2206](https://github.com/morluto/rea/issues/2206)) ([59be866](https://github.com/morluto/rea/commit/59be866719e8fe8ec060515044d427a3b0a57649))
* **ghidra:** keep JVM warnings out of LaunchSupport output ([#1698](https://github.com/morluto/rea/issues/1698)) ([e20994a](https://github.com/morluto/rea/commit/e20994a8ea10609d420e958bd8a1268b0bcd8899))
* **ghidra:** recover stale clients on same-target reopen ([#2014](https://github.com/morluto/rea/issues/2014)) ([4573e8c](https://github.com/morluto/rea/commit/4573e8cf35de822bfc5feabcc89ec3717c247c8c))
* **ghidra:** report progress while startup is pending ([#1877](https://github.com/morluto/rea/issues/1877)) ([29d2624](https://github.com/morluto/rea/commit/29d2624b3475586356c27f518bf6b823030268fb))
* **ghidra:** resolve file offsets in decompile ([#2101](https://github.com/morluto/rea/issues/2101)) ([2995ec6](https://github.com/morluto/rea/commit/2995ec6910ead327829378f5ab1b9172ecce33fc))
* **ghidra:** retain launcher command outcome in launch failure diagnostics ([#2011](https://github.com/morluto/rea/issues/2011)) ([d4e8ef7](https://github.com/morluto/rea/commit/d4e8ef74999666cdb735b49eb8fea367f8f86489))
* **ghidra:** share Java properties semantics with PSP extension admission ([b953f96](https://github.com/morluto/rea/commit/b953f96c0aae27e1876c317a8f7db1d6f292b8ff))
* **go:** accept file-backed unmapped Mach-O debug segments ([7fb097d](https://github.com/morluto/rea/commit/7fb097d72c9931dd5dfae481ad2b9d3bb6029f15))
* honor empty URL query and fragment markers ([#2091](https://github.com/morluto/rea/issues/2091)) ([af29247](https://github.com/morluto/rea/commit/af29247a7873ba8badec204a4a59f9a2b3a880b6))
* **hopper:** let failed one-shot commands exit while retaining a lease ([#1763](https://github.com/morluto/rea/issues/1763)) ([e8528ce](https://github.com/morluto/rea/commit/e8528cedcdfb1ca1d769304afdc353936e8e0d70))
* **hopper:** preserve observed process failures across requests ([#1927](https://github.com/morluto/rea/issues/1927)) ([601814e](https://github.com/morluto/rea/commit/601814ed0e4a0c57d6e045da0e6cf8ee2f29bb7f))
* **hopper:** preserve startup failures and recover denied launches ([069308b](https://github.com/morluto/rea/commit/069308bbe6d73b1d87038b43c9121658325042a9)), closes [#1808](https://github.com/morluto/rea/issues/1808)
* **hopper:** stop Linux demo children when the REA owner exits ([#2499](https://github.com/morluto/rea/issues/2499)) ([c105391](https://github.com/morluto/rea/commit/c105391db53e2e2ab8b8624d5f3870c14aae3637))
* **hopper:** wait for late bridges during cleanup ([#1866](https://github.com/morluto/rea/issues/1866)) ([cf7477e](https://github.com/morluto/rea/commit/cf7477eb4642bc07228fa33ccc4c87656e6e3003))
* **ida:** declare GUI navigation for attached legacy analysis ([#1764](https://github.com/morluto/rea/issues/1764)) ([cf69207](https://github.com/morluto/rea/commit/cf692072b56903b60426e4b301b3c48fd31ee0fd)), closes [#1712](https://github.com/morluto/rea/issues/1712)
* **ida:** preserve native coverage and verify returned values ([#2324](https://github.com/morluto/rea/issues/2324)) ([37b7550](https://github.com/morluto/rea/commit/37b7550ecc916acc8619fa72e5c05af45702733f))
* **ida:** preserve non-x86 call edges and workflow uncertainty ([#1906](https://github.com/morluto/rea/issues/1906)) ([0ae38e1](https://github.com/morluto/rea/commit/0ae38e1e31414c67a3be09231d208f0726c035f8))
* **ida:** recover cleanup after worker sessions are reaped ([12fa8fd](https://github.com/morluto/rea/commit/12fa8fd9f986dbd4ff58165c3ec71a46dc8a8a3b))
* **ida:** report a procedure IDA cannot resolve as invalid input ([#1785](https://github.com/morluto/rea/issues/1785)) ([d3e9ee5](https://github.com/morluto/rea/commit/d3e9ee5505251f760666bb8e1e6c295b6d6aedb2))
* **ida:** stop the private input of a universal Mach-O at the end of the selected slice ([#2291](https://github.com/morluto/rea/issues/2291)) ([c6acd19](https://github.com/morluto/rea/commit/c6acd19b6d842699ab807d9526dc762c743686f4))
* **ida:** thin the selected FAT slice into the private headless input ([#2201](https://github.com/morluto/rea/issues/2201)) ([10400fd](https://github.com/morluto/rea/commit/10400fdb57e4226c5ce15af17968a664f35498ce)), closes [#2139](https://github.com/morluto/rea/issues/2139)
* **ida:** wait out a transient busy worker health in headless sessions ([#1778](https://github.com/morluto/rea/issues/1778)) ([0bdc7a4](https://github.com/morluto/rea/commit/0bdc7a40ac171a05971b287f2aab0376e2f14358))
* **install:** describe automatic setup and guard truncated downloads ([#1902](https://github.com/morluto/rea/issues/1902)) ([d802304](https://github.com/morluto/rea/commit/d802304c542b20bc86e22cc492718e89a8a5579b))
* **install:** honor redirected setup diagnostics ([8b18171](https://github.com/morluto/rea/commit/8b181716aca2cb467be499cba226abfb5ae19141))
* **io:** consolidate managed snapshots and retain cleanup ownership ([98d8470](https://github.com/morluto/rea/commit/98d8470d36cfbf7cd25820a4ed430aebae20c0c5))
* **io:** retain cleanup ownership and honor cancellation ([d8c76b6](https://github.com/morluto/rea/commit/d8c76b629639bda4405afa9ae2f7676b694f2f10))
* **javascript:** apply loader-owned module lookup rules ([c0904b4](https://github.com/morluto/rea/commit/c0904b4c1aabfccf2f0ab12f44960a0a1fbbbfae))
* **javascript:** bound self-alias mutation paths that never prune ([e365f37](https://github.com/morluto/rea/commit/e365f3715f0e9856218f0b2878bfe2144c821d9a))
* **javascript:** bound value evaluation through nested closure aliases ([#1941](https://github.com/morluto/rea/issues/1941)) ([d59ab6d](https://github.com/morluto/rea/commit/d59ab6d977d5b9376af2fdd141b4f65460bb9a9f))
* **javascript:** centralize module syntax and separate erased type dependencies ([1b95302](https://github.com/morluto/rea/commit/1b953021eadf4cac09e20be388d79536903633b3))
* **javascript:** distinguish file URL paths from query identity ([f43fc02](https://github.com/morluto/rea/commit/f43fc0247527aebb8efc7d9db04276fe2d489323))
* **javascript:** follow actual receivers in nested intrinsic calls ([#1912](https://github.com/morluto/rea/issues/1912)) ([9d8e42b](https://github.com/morluto/rea/commit/9d8e42bdd6f1bf8a984da1e06e1d76a1dbb9a7ed))
* **javascript:** follow bundler namespace interop for Electron APIs ([#1827](https://github.com/morluto/rea/issues/1827)) ([6ac5c36](https://github.com/morluto/rea/commit/6ac5c36503492d3b6da4fcf2a5dee87aff60fc8c))
* **javascript:** honor package exports before sibling files ([3ad5782](https://github.com/morluto/rea/commit/3ad5782dfdc8864879a887dc63fef682ffeb5839))
* **javascript:** index repeated graph observations before final sorting ([6d5d71f](https://github.com/morluto/rea/commit/6d5d71fab5e135c687f7aa2f797da19b4c2a1c1e)), closes [#1748](https://github.com/morluto/rea/issues/1748)
* **javascript:** keep TypeScript substitution out of runtime paths ([f8513a4](https://github.com/morluto/rea/commit/f8513a42dc44451a621127f06edde5d38bc93f69))
* **javascript:** normalize package URLs before array selection ([#1916](https://github.com/morluto/rea/issues/1916)) ([369cd3d](https://github.com/morluto/rea/commit/369cd3dfdc47709cc34d01c50f645a885c70ded7))
* **javascript:** parse explicit CommonJS wrapper syntax ([#2475](https://github.com/morluto/rea/issues/2475)) ([6a04f92](https://github.com/morluto/rea/commit/6a04f922e16ad4d171ba0c07e32238efdaf43e2d))
* **javascript:** preserve and resolve complete source-map URLs ([#1788](https://github.com/morluto/rea/issues/1788)) ([ce845ba](https://github.com/morluto/rea/commit/ce845ba428e9b23d6b74eae1e25e17c37532adac))
* **javascript:** preserve cancellation through recovery publication cleanup ([eaa9616](https://github.com/morluto/rea/commit/eaa961658578522cecfbba7c7be1acf1c64da65a))
* **javascript:** preserve execution and invocation provenance ([b8fab74](https://github.com/morluto/rea/commit/b8fab7429771bb17a7ca0297661de3c9935212af))
* **javascript:** preserve graphs within parent capacity ([#1921](https://github.com/morluto/rea/issues/1921)) ([0485cd3](https://github.com/morluto/rea/commit/0485cd3bce29c977bae05a4fdce5436f3e3d7913)), closes [#1911](https://github.com/morluto/rea/issues/1911)
* **javascript:** preserve import-equals declaration and export evidence ([#2062](https://github.com/morluto/rea/issues/2062)) ([86fab11](https://github.com/morluto/rea/commit/86fab11fbe000a04c0810699419e99e50476e424))
* **javascript:** preserve loader-specific package ancestry ([9f67e44](https://github.com/morluto/rea/commit/9f67e44e5d451d705bae9a579ad9802d5c9f097c))
* **javascript:** preserve namespace re-export relationships ([#1833](https://github.com/morluto/rea/issues/1833)) ([37a9eb9](https://github.com/morluto/rea/commit/37a9eb9d65a088209f962e5094f6bc67eacbbaf9))
* **javascript:** preserve package and source-map boundary identity ([#1939](https://github.com/morluto/rea/issues/1939)) ([14935e4](https://github.com/morluto/rea/commit/14935e4d470be318fdee442d279d6acb7d441119))
* **javascript:** preserve POSIX runtime path case on Windows ([#2422](https://github.com/morluto/rea/issues/2422)) ([1a1b67c](https://github.com/morluto/rea/commit/1a1b67c8885e9ff33f7a06ed2e027ebfdb83e084))
* **javascript:** preserve TypeScript loader and dependency semantics ([e4532b5](https://github.com/morluto/rea/commit/e4532b537663026c3e5b784380eecd009c959a36))
* **javascript:** preserve unknown module-alias provenance ([c7a890d](https://github.com/morluto/rea/commit/c7a890d12987d9b2d496119c1fbf1a88c0fc298e))
* **javascript:** prove namespace interop before attributing Electron APIs ([#1857](https://github.com/morluto/rea/issues/1857)) ([9b3e598](https://github.com/morluto/rea/commit/9b3e59804db71072ed023f109342f96063c57b45))
* **javascript:** report available parent result budget in coverage limits ([#1915](https://github.com/morluto/rea/issues/1915)) ([72489a9](https://github.com/morluto/rea/commit/72489a9445299f695d793f004fed88e1bac0a557))
* **javascript:** report JSON status for imported package manifests ([914ba83](https://github.com/morluto/rea/commit/914ba833ded5f8604345de4e4f6d306de0a38c4e))
* **javascript:** resolve bundled exports by binding identity ([5ab0cbb](https://github.com/morluto/rea/commit/5ab0cbb48ea5a4f11542eac44479b9af48f1dabd)), closes [#2171](https://github.com/morluto/rea/issues/2171)
* **javascript:** resolve module-relative Worker URL references ([#2104](https://github.com/morluto/rea/issues/2104)) ([653067d](https://github.com/morluto/rea/commit/653067d799591ce2b8fdc84e99c160ac17e2860f))
* **javascript:** resolve relative ESM URLs as exact files ([783fcb0](https://github.com/morluto/rea/commit/783fcb06a4bd0444be214822fed60a3786932782))
* **javascript:** retain compiler-dependent module resolution facts ([c923fb0](https://github.com/morluto/rea/commit/c923fb0dc9462e302b84a15770131ff899f89455))
* **javascript:** retain completed facts through cancellation ([#1934](https://github.com/morluto/rea/issues/1934)) ([b6651cb](https://github.com/morluto/rea/commit/b6651cb99fba8fec3fd3efcb6794ae32b8a1525d))
* **javascript:** retain distinct chunk registration callback metadata ([#2737](https://github.com/morluto/rea/issues/2737)) ([0cf224e](https://github.com/morluto/rea/commit/0cf224e76214abbcf47274aa936d1b5cad28db2f))
* **javascript:** retain guards without duplicating observed IPC calls ([42b0ed1](https://github.com/morluto/rea/commit/42b0ed126df7d5b908ac369715378d5e519051b7))
* **javascript:** retain unbound CommonJS semantic imports ([3a17882](https://github.com/morluto/rea/commit/3a17882cc469515e562df9ecc7d154f30a5a9205))
* **javascript:** retain wrapped isolated-world bridge exposures ([1c37914](https://github.com/morluto/rea/commit/1c37914306b3ff257d7a0360f11c9bf2a3f2cac7))
* **javascript:** share source-map resolution and preserve path uncertainty ([3ae826c](https://github.com/morluto/rea/commit/3ae826ceba6f25b08bce50bcb51001209bc16b57))
* **javascript:** share TypeScript emitted import resolution ([1dfb933](https://github.com/morluto/rea/commit/1dfb933e47db0156cf1cb61f28715b81e740c57b))
* **javascript:** trace wrapped Electron IPC paths in large bundles ([#1874](https://github.com/morluto/rea/issues/1874)) ([87b9793](https://github.com/morluto/rea/commit/87b9793c53caf70a9f3a516214856899ca2d7cf8))
* **jeb:** confine an invalid REA_JEB_MCP_URL to the JEB tools ([#2145](https://github.com/morluto/rea/issues/2145)) ([df2c08b](https://github.com/morluto/rea/commit/df2c08b546d4f40103e11b8b31dbfc3d41ee687b)), closes [#2143](https://github.com/morluto/rea/issues/2143)
* **managed:** admit ReadyToRun images built for non-Windows hosts ([#1985](https://github.com/morluto/rea/issues/1985)) ([0d11f71](https://github.com/morluto/rea/commit/0d11f71e0dc5cf95d3bea093355f7edc09089a68))
* **managed:** bind row budget charges to metadata identities ([#1897](https://github.com/morluto/rea/issues/1897)) ([23d50ea](https://github.com/morluto/rea/commit/23d50ea3bec186b4459d36c04854a92276accdf1)), closes [#1878](https://github.com/morluto/rea/issues/1878)
* **managed:** bound transient CIL by decoded instruction count ([#1822](https://github.com/morluto/rea/issues/1822)) ([554cb45](https://github.com/morluto/rea/commit/554cb45f7ff967884c9d1fdf706c20803d76a6df))
* **managed:** charge retained member facts instead of decoding bounds ([#1768](https://github.com/morluto/rea/issues/1768)) ([3078729](https://github.com/morluto/rea/commit/30787297d49bf93e67f62a9ec42f896188d57a20))
* **managed:** charge transient CIL by decoded instructions ([554cb45](https://github.com/morluto/rea/commit/554cb45f7ff967884c9d1fdf706c20803d76a6df))
* **managed:** decode generic, array, fnptr, and modifier signatures ([#1835](https://github.com/morluto/rea/issues/1835)) ([1f531fc](https://github.com/morluto/rea/commit/1f531fc257f71731624cbd6b595208adacf59108))
* **managed:** distinguish .NET Standard targets from runtimes ([81dc64e](https://github.com/morluto/rea/commit/81dc64e3cd13dc8bfcac8898386717ece9721da1))
* **managed:** honor metadata ExtraData before table decoding ([#1893](https://github.com/morluto/rea/issues/1893)) ([fb43dd9](https://github.com/morluto/rea/commit/fb43dd9716fe113effa14a03b603b72ca7d9097c))
* **managed:** index attribute constructor owners once per layout ([#1943](https://github.com/morluto/rea/issues/1943)) ([c2cf7dc](https://github.com/morluto/rea/commit/c2cf7dcb86e11350214f82e5c51de7ca6ee3d27a))
* **managed:** preserve negative zero in CIL operands ([#2437](https://github.com/morluto/rea/issues/2437)) ([6c816af](https://github.com/morluto/rea/commit/6c816af11975e8e801adb2924c4e3c116c72ebba))
* **managed:** report ExportedType rows in managed member inspection ([#1828](https://github.com/morluto/rea/issues/1828)) ([ed1519d](https://github.com/morluto/rea/commit/ed1519ddee02739f57136e356786f64c7f74fed0))
* **managed:** report malformed PE layouts as invalid input ([#2194](https://github.com/morluto/rea/issues/2194)) ([90e37f4](https://github.com/morluto/rea/commit/90e37f4152990be721784006a820a3794920213c))
* **managed:** resolve member ownership through pointer tables ([#1886](https://github.com/morluto/rea/issues/1886)) ([a95795c](https://github.com/morluto/rea/commit/a95795c6ed3cb149e03460f4c818ad12e0c670d9))
* **managed:** share first-match member list ownership ([#1955](https://github.com/morluto/rea/issues/1955)) ([7d92d93](https://github.com/morluto/rea/commit/7d92d93a550f3be221d7cf1da07c37ea1770e922))
* **managed:** share first-match member ownership indexes ([52e76c5](https://github.com/morluto/rea/commit/52e76c5d39b58d1685bb5e22bd40d19524dfa2cb))
* **managed:** validate CLI type selections with the member contract ([#2125](https://github.com/morluto/rea/issues/2125)) ([60259a9](https://github.com/morluto/rea/commit/60259a91d9a58cbd313ad7f3d321acc2deb65d2e)), closes [#2121](https://github.com/morluto/rea/issues/2121)
* **mcp:** cancel evidence imports before session mutation ([e5699d2](https://github.com/morluto/rea/commit/e5699d23b173bbd0b68e3ab177d0b7583d6df345)), closes [#1746](https://github.com/morluto/rea/issues/1746)
* **mcp:** express inspect_analysis_view selector constraints in the contract schema ([#1705](https://github.com/morluto/rea/issues/1705)) ([a8a9c4d](https://github.com/morluto/rea/commit/a8a9c4d8173b63fb42d343a8ca043927970ea4ce)), closes [#1695](https://github.com/morluto/rea/issues/1695)
* **mcp:** join owned doctor shutdown without signaling saved PIDs ([7fc0b5e](https://github.com/morluto/rea/commit/7fc0b5ec9c2532bbf789d7783b7a0cab6fc054d8))
* **mcp:** keep prompt completion within ready provider sessions ([07b36c1](https://github.com/morluto/rea/commit/07b36c148946f46b99a4ce04e55726e97d0ee03a))
* **mcp:** keep shared optional providers open across discarded probes ([#2205](https://github.com/morluto/rea/issues/2205)) ([7c9c1eb](https://github.com/morluto/rea/commit/7c9c1ebdb77662f6276ef2f7250e7df414bd03ca)), closes [#2195](https://github.com/morluto/rea/issues/2195)
* **mcp:** preserve target provenance on retained partial errors ([8fc2043](https://github.com/morluto/rea/commit/8fc2043eb8bcedbf34dce064f977e455438635d6))
* **mcp:** provide native evidence paging continuation ([1449e8e](https://github.com/morluto/rea/commit/1449e8e05138bf0c86d5fe6f4c93122b4b93b2b1)), closes [#1849](https://github.com/morluto/rea/issues/1849)
* **native:** accept universal binaries with LIB64 x86_64 slices ([#1982](https://github.com/morluto/rea/issues/1982)) ([4b653fe](https://github.com/morluto/rea/commit/4b653fe6ad1d25a2d9a69d5e93610380c0c09a34))
* **native:** clarify Mach-O offset origins and reject partial ELF tables ([8bd5ba2](https://github.com/morluto/rea/commit/8bd5ba2d234cbd9a5bb9dd5daeaa191c3a1cad24)), closes [#1973](https://github.com/morluto/rea/issues/1973)
* **native:** honor Mach-O chained pointer locations and library ordinals ([#1709](https://github.com/morluto/rea/issues/1709)) ([3b0ea59](https://github.com/morluto/rea/commit/3b0ea590fcc876fa017946f4cec009d653f02b38))
* **native:** name the ownership helper prerequisite in command failures ([#2506](https://github.com/morluto/rea/issues/2506)) ([20f51d7](https://github.com/morluto/rea/commit/20f51d7286b1759e42f99c0512c73230052dda86))
* **native:** name the retained pwntools process group in cleanup resources ([#2281](https://github.com/morluto/rea/issues/2281)) ([4303dcd](https://github.com/morluto/rea/commit/4303dcdcf68af4d71d777ffff75cbd4f17a8877d)), closes [#2274](https://github.com/morluto/rea/issues/2274)
* **native:** preserve literal line breaks in signing identifiers ([#2107](https://github.com/morluto/rea/issues/2107)) ([ec3db37](https://github.com/morluto/rea/commit/ec3db374574060fcc4fa6b215fceaea1bd4ca3f3))
* **native:** preserve universal Mach-O container identity ([b38429c](https://github.com/morluto/rea/commit/b38429cb6a0de8f4bb5ae01633e24abe2d7cde46))
* **native:** raise default PE resource entry budget to 16384 ([#1703](https://github.com/morluto/rea/issues/1703)) ([951c2aa](https://github.com/morluto/rea/commit/951c2aae3be1c357df9f23558d8c09680e64c2cb))
* **native:** rank Mach-O slices by parsed CPU subtype ([c02fb0e](https://github.com/morluto/rea/commit/c02fb0ea8055f98def2b12ad557aed0faefd3639))
* **native:** reject ambiguous otool load-command framing ([108b5f9](https://github.com/morluto/rea/commit/108b5f94254281639badd25b1f448cca35dc89a2))
* **native:** reject required otool fields truncated before their value ([#1995](https://github.com/morluto/rea/issues/1995)) ([adde8a8](https://github.com/morluto/rea/commit/adde8a80921b199ddd7105998f3760d345b468fc))
* **native:** retain failed command cleanup owners ([d9b4ae1](https://github.com/morluto/rea/commit/d9b4ae182d60dfd7fd5ba5bb539716413cbfdca6))
* **native:** retain failed pwntools process and runtime cleanup ([#1708](https://github.com/morluto/rea/issues/1708)) ([11fe5a1](https://github.com/morluto/rea/commit/11fe5a13f8aee87d0c0e036a9f497407e085fe48))
* **native:** retain signature snapshot cleanup ownership ([ebc22d9](https://github.com/morluto/rea/commit/ebc22d99d62225505c0a84f6e39823ba4ef2f829))
* **native:** select one slice when universal Mach-O slices share a CPU type ([#2040](https://github.com/morluto/rea/issues/2040)) ([96fc84f](https://github.com/morluto/rea/commit/96fc84f1fb7c8bc69f66cd899896f4de9061fead))
* order remaining outputs independently of host locale ([#1867](https://github.com/morluto/rea/issues/1867)) ([e4ac7bb](https://github.com/morluto/rea/commit/e4ac7bbfc2f8c631ba1c60f507d85253e122d4c8))
* **pcap:** name the host tool to configure in capability refusals ([#2507](https://github.com/morluto/rea/issues/2507)) ([bb96afd](https://github.com/morluto/rea/commit/bb96afdbe32f5318d57fcb00099191c5a7712f53))
* **pcap:** order evidence ties by code point instead of host locale ([#2509](https://github.com/morluto/rea/issues/2509)) ([f6556ad](https://github.com/morluto/rea/commit/f6556ad2bc102d80223f5b434136f546dbd2bbab))
* **plist:** decode formatted XML dictionary keys ([f1924cf](https://github.com/morluto/rea/commit/f1924cf9c1910d5d29d11e7bf634b51af8d738cf))
* **process:** accept a trace identifier the comparison result can represent ([#2488](https://github.com/morluto/rea/issues/2488)) ([2c6a4fa](https://github.com/morluto/rea/commit/2c6a4fac4ff3477ac14f3d780894c5b390880607))
* **process:** add an opt-in Swift module cache ([#2252](https://github.com/morluto/rea/issues/2252)) ([47a51b0](https://github.com/morluto/rea/commit/47a51b08f4d79f808c1b74de79c3852aa853df53))
* **process:** aggregate incomplete trace-spec diagnostics ([857a1b7](https://github.com/morluto/rea/commit/857a1b7b04da4488db2b224c270ad78998c6b6c7))
* **process:** align capture Evidence provider admission ([713db4d](https://github.com/morluto/rea/commit/713db4dc2665c964707cb0e937e8794cbe73534e))
* **process:** bound retained trace ordering state ([63e796d](https://github.com/morluto/rea/commit/63e796d50ea323ae8dcfef80f86c8154bb106790))
* **process:** cancel run-token reads during sampling ([09b6828](https://github.com/morluto/rea/commit/09b68285166bef2b7633402c8adf7eb25823b47e))
* **process:** observe already-due trailing callbacks before settling the capture journal ([#1700](https://github.com/morluto/rea/issues/1700)) ([ed084b3](https://github.com/morluto/rea/commit/ed084b3dbb13ec9dee1adc8f5b76e784b52c00bb)), closes [#1697](https://github.com/morluto/rea/issues/1697)
* **process:** preserve invalid runtime path selections as input errors ([cecc6ec](https://github.com/morluto/rea/commit/cecc6ecbffe84689a173d1634f342e046b0c8ceb))
* **process:** preserve monotonic capture timing and observed wall dates ([9049dba](https://github.com/morluto/rea/commit/9049dbaf0c3df32357419f9899965df2b611f493))
* **process:** preserve nested path identities during normalization ([#2468](https://github.com/morluto/rea/issues/2468)) ([b9ee921](https://github.com/morluto/rea/commit/b9ee921560dcf4590c2b13a1cc92c49c93e10675))
* **process:** reject deep trace cycles without recursive expansion ([10935b7](https://github.com/morluto/rea/commit/10935b7b1136accab2a43b308ab380749da0f7ae))
* **process:** report single empty settlement observation as unverifiable ([#2232](https://github.com/morluto/rea/issues/2232)) ([7724edb](https://github.com/morluto/rea/commit/7724edbdd3fa0acd94c8210a30dceba501fcdbe8))
* **process:** report the Swift compiler outcome with bounded diagnostics ([#1999](https://github.com/morluto/rea/issues/1999)) ([091cb81](https://github.com/morluto/rea/commit/091cb81fc3ca2b8ec96df7a534934388a6b882cd))
* **process:** retain owned command cleanup diagnostics ([#1907](https://github.com/morluto/rea/issues/1907)) ([38539a2](https://github.com/morluto/rea/commit/38539a2f4a522243d6fc369ae34ef30e2a2f80cc)), closes [#1856](https://github.com/morluto/rea/issues/1856)
* **process:** return private runtime ownership at allocation ([c22111e](https://github.com/morluto/rea/commit/c22111eca2c4bc84a084f12711f405b3e7422a40))
* **process:** use monotonic time for provider execution budgets ([d9e9fb2](https://github.com/morluto/rea/commit/d9e9fb2273543caab4eb21d896c33d462ada6971))
* **progress:** measure delivery intervals with a monotonic clock ([ed881c6](https://github.com/morluto/rea/commit/ed881c6d2ef6a8adba0c2a241ebc9ab4603b7868))
* **providers:** retain native parsing and compiler failure diagnostics ([b5ededd](https://github.com/morluto/rea/commit/b5ededd0f9c28d0f696a205103066928117368ee))
* **readiness:** validate stage evidence references once ([02cedc3](https://github.com/morluto/rea/commit/02cedc37961a5d230b134f330c148322cd04bf37))
* **reconstruction:** resolve latest verifier observations without input-order bias ([1f7f808](https://github.com/morluto/rea/commit/1f7f808c4f3bb5eca6b13bcc71d5cceb2ace6614))
* **reference:** report sloppy scripts, PHP, C# and enclosing Git facts ([#1770](https://github.com/morluto/rea/issues/1770)) ([7b15762](https://github.com/morluto/rea/commit/7b15762d09acf3d4ffb937f55ec1b54e7db5fbfa))
* **reference:** verify symlink identity around target reads ([3d83135](https://github.com/morluto/rea/commit/3d8313598785359c424d7305f503cb69fc8639a6))
* **runtime:** preserve unknown debugger attachment state ([e55ef72](https://github.com/morluto/rea/commit/e55ef7213515ba204c3d88a4487b4ee6a2773b06))
* **screenshot:** reject invalid PNG header dimensions ([#2472](https://github.com/morluto/rea/issues/2472)) ([e74e4e9](https://github.com/morluto/rea/commit/e74e4e951bf59c42b25779b45d1c7b8dde6b618f))
* **scripts, install:** honor test:fast "without build" promise and orient Node.js engine mismatches ([#2026](https://github.com/morluto/rea/issues/2026)) ([69e097a](https://github.com/morluto/rea/commit/69e097a8ca256ab97ee4b2883739447784a5f90e))
* **scripts:** pass MCP callTool timeouts as request options ([#2387](https://github.com/morluto/rea/issues/2387)) ([c462a4e](https://github.com/morluto/rea/commit/c462a4ebd3a10e59253bcf38353a0914b176738b))
* **session:** attribute selected-provider capabilities to the resolved identity ([#2013](https://github.com/morluto/rea/issues/2013)) ([2ff850d](https://github.com/morluto/rea/commit/2ff850de68dc9b05c6faf2bbfd26d56bc2d5d27c)), closes [#2009](https://github.com/morluto/rea/issues/2009)
* **session:** invalidate snapshots before metadata write dispatch ([1c64fe3](https://github.com/morluto/rea/commit/1c64fe3eff48679c6880f42d2f0a4af46686ade7))
* **session:** verify retained client liveness on open fast path ([e59db2f](https://github.com/morluto/rea/commit/e59db2f5deefb1c5e74f01e19631f6d75cfaa25a))
* **setup:** configure an empty Hermes config.yaml ([#1983](https://github.com/morluto/rea/issues/1983)) ([43f9c39](https://github.com/morluto/rea/commit/43f9c39dfbdfaa6e5bc260b037a506cd1f56fe6d))
* **setup:** honor config roots and display terminal dry-run plans ([2397137](https://github.com/morluto/rea/commit/23971376158e7815edcbe6c04a8df7c9504650dc))
* **setup:** ignore empty home directory selections ([c339f9e](https://github.com/morluto/rea/commit/c339f9ebe7bd1f30608d6ccb7175da523dd3c485)), closes [#2325](https://github.com/morluto/rea/issues/2325)
* **setup:** name each skill directory in the uninstall summary ([#2118](https://github.com/morluto/rea/issues/2118)) ([5190843](https://github.com/morluto/rea/commit/5190843f8f09ac17cbf79561bd1f708c509cda78)), closes [#2111](https://github.com/morluto/rea/issues/2111)
* **setup:** name invalid client config shape errors clearly ([#2157](https://github.com/morluto/rea/issues/2157)) ([b1bf4a7](https://github.com/morluto/rea/commit/b1bf4a70d44191348ca20a91a52fb663da4daed0))
* **setup:** name structural problems in client configurations ([#2154](https://github.com/morluto/rea/issues/2154)) ([51367ca](https://github.com/morluto/rea/commit/51367cae6f4e6ef9b9dcb28dc21fe6c92d1dd6f8)), closes [#2152](https://github.com/morluto/rea/issues/2152)
* **setup:** pin Hopper's macOS publisher identity before install ([#2481](https://github.com/morluto/rea/issues/2481)) ([650237e](https://github.com/morluto/rea/commit/650237efb01b3c4a154799dbbedb2af1084f2312))
* **setup:** preserve Cordis ownership across uncertain patch structure ([21eff3b](https://github.com/morluto/rea/commit/21eff3bae10f58711a35ad44f86e9f96f9bb4790))
* **setup:** preserve current recovery snapshots and decoding failures ([f6775a3](https://github.com/morluto/rea/commit/f6775a36694a6a9c620f4248638e8056a6cbcb66))
* **setup:** preserve skill files and report rollback failures ([c240768](https://github.com/morluto/rea/commit/c2407689cb04df082e8db0b6afa8f8d022f6bcd1))
* **setup:** preserve unrelated Hermes YAML line formatting ([58e91e1](https://github.com/morluto/rea/commit/58e91e14bd5659dd57acbb91e56b5fc6a6963c80))
* **setup:** register a stable launcher when run through bunx ([#1731](https://github.com/morluto/rea/issues/1731)) ([d412def](https://github.com/morluto/rea/commit/d412def7225a556e6d2140176dc9ab4bd34af0f3))
* **setup:** remove emptied skill directories on uninstall ([#2182](https://github.com/morluto/rea/issues/2182)) ([d350357](https://github.com/morluto/rea/commit/d35035780a643e38301b8eaea03f01bb506e9df9))
* **setup:** retain regular-file cleanup ownership through workflows ([dcfc5d4](https://github.com/morluto/rea/commit/dcfc5d403ac997c2b08bee4753e57a1a4a8a8538))
* **setup:** retain symlinked data roots during uninstall ([#1821](https://github.com/morluto/rea/issues/1821)) ([7e2f79a](https://github.com/morluto/rea/commit/7e2f79ab45a23ab844f89fb93c9cdf202230c73f))
* **setup:** verify independently selected skill destinations ([bb50813](https://github.com/morluto/rea/commit/bb508134efbba392a404b651d6ec05be8b7eac1e))
* **snapshot:** bind replayed subjects to the selected target path ([c002b89](https://github.com/morluto/rea/commit/c002b89e2d8130a3c07e25e5fd604876be18c703))
* **snapshot:** classify non-object roots as invalid content ([#2685](https://github.com/morluto/rea/issues/2685)) ([9cf49c7](https://github.com/morluto/rea/commit/9cf49c7a784ace9b79e4b4c1022c9fbf2b3f77e1))
* **sourcemap:** retain named originals beside unknown source names ([83a4445](https://github.com/morluto/rea/commit/83a4445f440fab978ad9376b4260acae3f5fb189))
* **sourcemaps:** preserve nullable source declarations ([ee50ed1](https://github.com/morluto/rea/commit/ee50ed121e38cc6e1a2ff0dc8b789f15f71c5402))
* **target:** recognize binary plists by magic under any extension ([#2133](https://github.com/morluto/rea/issues/2133)) ([672bdea](https://github.com/morluto/rea/commit/672bdea21fd943688e081d209a993e234c45507f)), closes [#2129](https://github.com/morluto/rea/issues/2129)
* **targets:** project invalid raw-binary loads as caller input ([#2004](https://github.com/morluto/rea/issues/2004)) ([edf4624](https://github.com/morluto/rea/commit/edf46248f2a44b4e5776518489fee09d0fc4516b))
* **targets:** recognize Windows headers after short DOS stubs ([c9fed50](https://github.com/morluto/rea/commit/c9fed50691ae014890e91a72534773d2fb58ce44))
* **targets:** remove path advice for format refusals ([f28f338](https://github.com/morluto/rea/commit/f28f3382836f8dd8629c25b1bf5455d29b28d41d)), closes [#1979](https://github.com/morluto/rea/issues/1979) [#1852](https://github.com/morluto/rea/issues/1852)
* **test:** audit static Wasm and Harmony tool wording ([#1917](https://github.com/morluto/rea/issues/1917)) ([dfe3117](https://github.com/morluto/rea/commit/dfe31172ca0a090be3f40bc134f54d69c08e31d8))
* **test:** wait for Hopper reconnect listener readiness ([#1885](https://github.com/morluto/rea/issues/1885)) ([d3e83b0](https://github.com/morluto/rea/commit/d3e83b089943e2941dace3546a068a59bd0a335e))
* **uninstall:** label YAML configuration syntax failures correctly ([94153f1](https://github.com/morluto/rea/commit/94153f1c757e07a6d092e00903aa8b125d4a0afa))
* **update:** resolve Bun global directories before changing cwd ([e90acae](https://github.com/morluto/rea/commit/e90acaea674f5f7f9f17ea472104afdc1422ada6))
* **update:** share parsed client activation with maintenance ([b045df1](https://github.com/morluto/rea/commit/b045df100e1489e08a36d6ff04cea7feecf1332c))
* **wasm:** accept unpadded WABT section rows ([#1963](https://github.com/morluto/rea/issues/1963)) ([9eee709](https://github.com/morluto/rea/commit/9eee709f0cc4064808579c225814898a475ca388))
* **wasm:** inspect modules whose custom section names contain NUL ([#1981](https://github.com/morluto/rea/issues/1981)) ([1febc22](https://github.com/morluto/rea/commit/1febc22907f1cc8035dd1450616233121bd7f19d))
* **wasm:** preserve debug module names in WABT output ([#1967](https://github.com/morluto/rea/issues/1967)) ([b777e5d](https://github.com/morluto/rea/commit/b777e5d4d8a7b60edad7d5c0eaa56408a4b3f610))
* **wasm:** preserve leading BOM in custom section names ([#2102](https://github.com/morluto/rea/issues/2102)) ([abc8c6d](https://github.com/morluto/rea/commit/abc8c6dadf4f11b67de1cc2ee96bb55047416d71))
* **wasm:** stop appending WABT remediation to unrelated runtime-root failures ([#2084](https://github.com/morluto/rea/issues/2084)) ([ea2f94d](https://github.com/morluto/rea/commit/ea2f94d4de2241306158fecdd75f96cd34566694))
* **web:** reject NUL bytes in web tool local path inputs ([499ac18](https://github.com/morluto/rea/commit/499ac180115de7c9de3f1adc55159ffa6e9bb40e))
* **website:** clarify translated evidence and runtime observations ([f1f5582](https://github.com/morluto/rea/commit/f1f5582ce84f54e529826fef7af3e201f52248d9))
* **website:** clarify translated terminology and RTL flows ([a956275](https://github.com/morluto/rea/commit/a956275d8d0249c16dad282a3fb5d7da48fb2d6f))
* **website:** incorporate multilingual contributor refinements ([d981a58](https://github.com/morluto/rea/commit/d981a58d7f5344c3ab2117b6384aece4d1c35480))
* **website:** keep omitted language selectors free of whitespace ([8583a19](https://github.com/morluto/rea/commit/8583a190c0178374132c888622d693fed1f88a18))
* **website:** prioritize getting started across language editions ([8cbd57c](https://github.com/morluto/rea/commit/8cbd57cafaf1cfacbe832bae593589cf211442ef))
* **windows:** bring the real Windows Ghidra verifiers up to date ([#1863](https://github.com/morluto/rea/issues/1863)) ([ea40f71](https://github.com/morluto/rea/commit/ea40f717b26392000f989a8b500f6774b5e85091))
* **windows:** preserve caller affinity only within one processor group ([#2202](https://github.com/morluto/rea/issues/2202)) ([ab62ee9](https://github.com/morluto/rea/commit/ab62ee9dcf98e4717d0b3445330f7cfdfe50c9f4))
* **windows:** preserve transient runtime admission failures ([#2467](https://github.com/morluto/rea/issues/2467)) ([1724077](https://github.com/morluto/rea/commit/172407787f1842dedbcd27b5385177595f786ce4))
* **windows:** retain failed native inspection ownership ([#1826](https://github.com/morluto/rea/issues/1826)) ([0b4ce83](https://github.com/morluto/rea/commit/0b4ce83618bb800627264ae6221e2e35140417b5))
* **workflows:** retain staged output ownership and cancellation semantics ([7285e15](https://github.com/morluto/rea/commit/7285e15839a4fecf9940683699b781a60ccc4775)), closes [#1968](https://github.com/morluto/rea/issues/1968)
### Performance Improvements
* **browser:** parse captured module URLs once per lookup ([#1829](https://github.com/morluto/rea/issues/1829)) ([ca11162](https://github.com/morluto/rea/commit/ca11162d027a0d3e3b5a65a54825b00d5689fb80))
* **ci:** share bytecode across fresh Apple artifact subprocesses ([ba7df3f](https://github.com/morluto/rea/commit/ba7df3f0f7ee63e6350d5fbdddc8ed945a6b141d))
* **cli:** bundle runtime entries to reduce Windows startup I/O ([#1996](https://github.com/morluto/rea/issues/1996)) ([d93fbda](https://github.com/morluto/rea/commit/d93fbda390c6b8b2741c552bd194b50c3e08e0c1))
* **cli:** defer command-specific analysis implementations ([a4dd4e8](https://github.com/morluto/rea/commit/a4dd4e8e8700ce350e1a0c612ebd61b91ca5e585))
* **cli:** defer command-specific modules until dispatch ([a82d14e](https://github.com/morluto/rea/commit/a82d14e4db4dca989ffa2e7dbe78fd43551da256))
* **domain:** avoid duplicate readiness bundle validation ([29ff887](https://github.com/morluto/rea/commit/29ff887f6558cf00c0d6392889b890ed8a702a6f))
* **evidence:** authenticate and seal imported bundles in one pass ([b123b2e](https://github.com/morluto/rea/commit/b123b2e51cc7d6d2157525664be2df88fb1ba3bd))
* **evidence:** fuse streamed JSON token handling ([#1694](https://github.com/morluto/rea/issues/1694)) ([13e03e9](https://github.com/morluto/rea/commit/13e03e94363a007353fcb5dfedec764de12fc927))
* **evidence:** reuse authenticated comparisons through aggregation ([26bffb9](https://github.com/morluto/rea/commit/26bffb94bf80355c35e22a98a8fc629c6210307e))
* **evidence:** reuse authenticated file snapshots during session import ([#1721](https://github.com/morluto/rea/issues/1721)) ([966dc3a](https://github.com/morluto/rea/commit/966dc3acde2880a0f94b2658e2a02180d505cafa))
* **evidence:** reuse authenticated unknown mutation snapshots ([042dcaa](https://github.com/morluto/rea/commit/042dcaa1ee66f1b8668f08b193f38ab01bceaacb))
* **flutter:** add opt-in retained AOT summary and string pages ([23de4f7](https://github.com/morluto/rea/commit/23de4f77c74dd299d45cab880c36321d802b21fe))
* **frida:** evict captured messages without copying the whole queue ([#2628](https://github.com/morluto/rea/issues/2628)) ([6a05248](https://github.com/morluto/rea/commit/6a05248d464387c0ce4ec1577b4c1b24d3d355c7))
* **ghidra:** compile bundle activator with bridge scripts ([#1931](https://github.com/morluto/rea/issues/1931)) ([0f4992f](https://github.com/morluto/rea/commit/0f4992fb9e5bfe1fafe75c35b7f20ad411595573))
* **javascript:** index source-map ignore lists per leaf ([#1905](https://github.com/morluto/rea/issues/1905)) ([898d509](https://github.com/morluto/rea/commit/898d5096f94fe1ed3ea7d091455e638cc7d23bfd))
* **javascript:** reuse collected callable identities for returns ([d3088c3](https://github.com/morluto/rea/commit/d3088c3795f6c37cb3b6aec0bb0c371dd0640ee8))
* **javascript:** skip nested callables during fingerprint traversal ([#1871](https://github.com/morluto/rea/issues/1871)) ([097f0d5](https://github.com/morluto/rea/commit/097f0d54afceb3ce61dbaa24dd6736658d1fce06))
* **javascript:** skip the repeated-prefix scan on first capture visits ([#1789](https://github.com/morluto/rea/issues/1789)) ([41544fb](https://github.com/morluto/rea/commit/41544fb952aef84fcf6ff5d9a5284cf2b2985053))
* **mcp:** bundle internal modules to reduce Windows startup I/O ([76e4153](https://github.com/morluto/rea/commit/76e4153d346b14ef194222bbeef240cf8a16dc99))
* **mcp:** compact repeated session discovery facts ([#1988](https://github.com/morluto/rea/issues/1988)) ([6395e8d](https://github.com/morluto/rea/commit/6395e8d3007d5ee453477460e7ee1c51cff76be0))
* **mcp:** keep Playwright and type-only imports off MCP startup ([#1691](https://github.com/morluto/rea/issues/1691)) ([0ecf3f2](https://github.com/morluto/rea/commit/0ecf3f2438ec96a6b543942869808018e8d84452))
* **mcp:** reduce Windows startup module resolution ([#1814](https://github.com/morluto/rea/issues/1814)) ([76e4153](https://github.com/morluto/rea/commit/76e4153d346b14ef194222bbeef240cf8a16dc99))
* **native:** add opt-in Mach-O summary delivery and retained pages ([#2634](https://github.com/morluto/rea/issues/2634)) ([bf34d0d](https://github.com/morluto/rea/commit/bf34d0d2fc3cc22076f49d682e74a0b71aa1db18))
* **native:** run independent Mach-O slice commands concurrently ([#2466](https://github.com/morluto/rea/issues/2466)) ([2c7240c](https://github.com/morluto/rea/commit/2c7240cf9d8231e16ad01de7678af9dbd01bab46))
* **snapshot:** restore large legacy caches within live memory bounds ([092de8f](https://github.com/morluto/rea/commit/092de8f4b336b537579bd48d7d30135b7114371d))
* **test:** batch compatible client profile lifecycles ([0153b4d](https://github.com/morluto/rea/commit/0153b4d95bdf38eddee91d5ce08181030fb955b4))
* **test:** reuse CLI boundary worker module graphs ([f178bf1](https://github.com/morluto/rea/commit/f178bf1c00139e1b7fe2d14fcfe1f558fe08fbe2))
* **test:** reuse the CI scope Git baseline ([ef5f9aa](https://github.com/morluto/rea/commit/ef5f9aa2d7769b32d9f88211812ae11456a88267))
* **tests:** consolidate TypeScript dialect producer checks ([01762b1](https://github.com/morluto/rea/commit/01762b10c6a40370996e6dfa3a96ffcb27148f97))
* **test:** share stdio analysis for comparison fixtures ([43dafb5](https://github.com/morluto/rea/commit/43dafb5abe60ee21be4f6a6b6de482cd7a5bab0a))
* **tests:** remove timed output and synchronize browser fixtures ([693ed33](https://github.com/morluto/rea/commit/693ed332b58d05192f55b8192d2c0c4bc4f4824a))
* **tests:** reuse stateless workflows and immutable fixtures ([5f4c7ff](https://github.com/morluto/rea/commit/5f4c7ff6df0735d5a4461b5d0e6f2e466b888be0))
### Code Refactoring
* **evm:** bind offline analysis through a static descriptor ([ad08510](https://github.com/morluto/rea/commit/ad08510a113d7a9b8d92fb1e385757880e469869))
* **javascript:** reuse validated source-map decoding ([f64790b](https://github.com/morluto/rea/commit/f64790bfc32db330b2be5bdff61af555a9d26067))
* **mcp:** trust validated inputs and tighten contract fixtures ([2646150](https://github.com/morluto/rea/commit/264615059ce6c89badcc7288e8c67bb8dd1ce0dd))
* prune obsolete provider and verification code ([e198793](https://github.com/morluto/rea/commit/e19879328a11fe38b49b6b133a8ebde908ea980e))
### Documentation
* add responsible-use disclaimer ([4a5aec8](https://github.com/morluto/rea/commit/4a5aec8ad6d54444b2a2c2313fbc506b17ff91aa))
* **agents:** require brittleness review before resolving threads ([0365454](https://github.com/morluto/rea/commit/0365454a573eb52c7089320fd74bfd4a673b78ae))
* **agents:** require related brittleness cleanup in bug fixes ([0d8b728](https://github.com/morluto/rea/commit/0d8b728d2470a9b51239d61986944bacaf9a8708))
* celebrate 110,000 GitHub stars <‰ ([#2042](https://github.com/morluto/rea/issues/2042)) ([0d78c99](https://github.com/morluto/rea/commit/0d78c99c11efee66570772efe6eb88dfd0e1c694))
* celebrate 170,000 GitHub stars <‰ ([#2462](https://github.com/morluto/rea/issues/2462)) ([9188698](https://github.com/morluto/rea/commit/91886982fccc50a9f7f31d4f320b25ffbcff35af))
* celebrate 80,000 GitHub stars <‰ ([#1752](https://github.com/morluto/rea/issues/1752)) ([6ff4d4a](https://github.com/morluto/rea/commit/6ff4d4a0500e5f9fb7601e1c5ae5dbbd85796d91))
* celebrate 90,000 GitHub stars <‰ ([#1858](https://github.com/morluto/rea/issues/1858)) ([5876779](https://github.com/morluto/rea/commit/5876779013726964deef748120c48f5fa0155546))
* clarify ownership and interpretation boundaries ([64b71b1](https://github.com/morluto/rea/commit/64b71b180a88a2df1b0b61e803c1c322ee59c5e1))
* clarify responsible use and preserve third-party notices ([#2082](https://github.com/morluto/rea/issues/2082)) ([974f11d](https://github.com/morluto/rea/commit/974f11d010570c0a18423cd1cc93e9a5203df1b6))
* **cli:** add a managed assembly inspection workflow ([#2037](https://github.com/morluto/rea/issues/2037)) ([538f8cc](https://github.com/morluto/rea/commit/538f8cc7224dba44b728cf6c000920f911957fc4))
* correct drift and reorganize English guides ([#2207](https://github.com/morluto/rea/issues/2207)) ([7ae3c50](https://github.com/morluto/rea/commit/7ae3c50a9d49c480035855de9fcc4484befacd92))
* define contribution scope and simplify proposal forms ([ac6ef95](https://github.com/morluto/rea/commit/ac6ef95e12335bdc054ccaaa96c32474cf8789ea))
* define performance priorities for analysis workflows ([#2245](https://github.com/morluto/rea/issues/2245)) ([8e00452](https://github.com/morluto/rea/commit/8e004522f232ec698c77536235853088a851e6b5))
* **ghidra:** verified macOS arm64 native decompiler build recipe ([#2112](https://github.com/morluto/rea/issues/2112)) ([5f79c85](https://github.com/morluto/rea/commit/5f79c852af8e53a56f58266a9dcf84314c197164))
* **ghidra:** verify and document the macOS arm64 native decompiler build ([5f79c85](https://github.com/morluto/rea/commit/5f79c852af8e53a56f58266a9dcf84314c197164))
* **i18n:** celebrate 80,000 GitHub stars in translated READMEs ([#1758](https://github.com/morluto/rea/issues/1758)) ([953a0ce](https://github.com/morluto/rea/commit/953a0ce5bd3a8b44051eb9c16d8c9a20cad29e7c))
* **install:** explain Claude Code upfront tool loading on third-party endpoints ([#2029](https://github.com/morluto/rea/issues/2029)) ([d024928](https://github.com/morluto/rea/commit/d0249282c64882329a6acab8f3b477552da53499)), closes [#2019](https://github.com/morluto/rea/issues/2019)
* preserve the native guide title anchor ([#2224](https://github.com/morluto/rea/issues/2224)) ([ae41265](https://github.com/morluto/rea/commit/ae412654d5947f3841c191e92f3ad6b73d6f894b))
* **readiness:** align required stage identifier and checks with evaluator ([#2200](https://github.com/morluto/rea/issues/2200)) ([85c5477](https://github.com/morluto/rea/commit/85c5477930d504cfd8b72334b48255d8e746a85e))
* **readme:** remove CI badges from translations ([3298219](https://github.com/morluto/rea/commit/3298219d8cd905897ec9a96bf639602a273d4a9b))
* **readme:** remove CI status badge ([b07e54a](https://github.com/morluto/rea/commit/b07e54a10e66319d90815313fae93350257e1655))
* **setup:** confirm published 6.4.0 client support ([57c5aca](https://github.com/morluto/rea/commit/57c5acac2e13fd7f0e3832bc147c7ceda8c314f2))
* shorten responsible-use notices across languages ([fa87dd2](https://github.com/morluto/rea/commit/fa87dd2aac3eaeb49875c54e93ec18c98cc92e4d))
* sync French, Indonesian and Italian READMEs with English ([#1861](https://github.com/morluto/rea/issues/1861)) ([96547d3](https://github.com/morluto/rea/commit/96547d373fcdc672058b29774d6c634f4e9cc1bd))
* **website:** add essay on reverse engineering models ([fe496bd](https://github.com/morluto/rea/commit/fe496bd1cf589938c372646ddef8ca9701417bf0))
* **website:** highlight the setup guide on the homepage ([#1714](https://github.com/morluto/rea/issues/1714)) ([8be7ad4](https://github.com/morluto/rea/commit/8be7ad496c9a52a9986c4d3f6596de279b52df7e))
* **website:** improve model essay transitions and narrative flow ([ca07373](https://github.com/morluto/rea/commit/ca07373e44e19e0c8357ac49e460c90d87fcc22c))
* **website:** publish model essay in all supported languages ([3d78a57](https://github.com/morluto/rea/commit/3d78a57043eb6b92812c71bcb6d83753c3d48036))
* **website:** simplify reconstruction evidence in model essay ([b06751b](https://github.com/morluto/rea/commit/b06751b703181527a320226c4ab7e4cecd5dcea0))
* **website:** use straight quotes in model essay ([806ff57](https://github.com/morluto/rea/commit/806ff575676e447c7ba349b6fd185e6e8e02ee5d))
### Tests
* **analysis:** verify real preset snapshot and cleanup boundaries ([#2349](https://github.com/morluto/rea/issues/2349)) ([1dd20ce](https://github.com/morluto/rea/commit/1dd20ce1ff4ab66c74350929577e753aa17a9006)), closes [#2016](https://github.com/morluto/rea/issues/2016)
* **android:** verify duplicate class inventory behavior on empty discovery queries ([#1984](https://github.com/morluto/rea/issues/1984)) ([6611bcf](https://github.com/morluto/rea/commit/6611bcf6e86630c775c24e6afa00d7239c64e70e))
* **apple:** consolidate keyed archive integer verification ([#1933](https://github.com/morluto/rea/issues/1933)) ([3b490a7](https://github.com/morluto/rea/commit/3b490a743fd56a8a27b1356f7b2d1284db8848ef))
* **apple:** keep the same-named slice dyld fixture openable on x64 hosts ([#2755](https://github.com/morluto/rea/issues/2755)) ([fb28ec1](https://github.com/morluto/rea/commit/fb28ec1537e2d02aaca96325483c849c5c8ade6f))
* **apple:** skip ibtool-compiled NIB tests when Xcode ibtool is unavailable ([#2070](https://github.com/morluto/rea/issues/2070)) ([7bc57e1](https://github.com/morluto/rea/commit/7bc57e1e64915a26c32f89fb53d0321193a37160))
* **architecture:** consolidate import boundary verification ([2976fac](https://github.com/morluto/rea/commit/2976fac30d4088c1f1ba383261eb3eff5e95bce6))
* bound CLI validation subprocess concurrency ([ff57b75](https://github.com/morluto/rea/commit/ff57b75e226028767b86aa3d4271c2c2f56342db))
* **ci:** stabilize JADX recovery and ignore generated reports ([#1668](https://github.com/morluto/rea/issues/1668)) ([0a6b46d](https://github.com/morluto/rea/commit/0a6b46d5b0299f76c0d268f7b70395f9b6fbd68e))
* classify filesystem and process workflows by boundary ([c2b56e3](https://github.com/morluto/rea/commit/c2b56e3e1c9619beab15de8e1f200422473caf30))
* classify filesystem application workflows as boundaries ([804c287](https://github.com/morluto/rea/commit/804c287e441dcaa3876429f026ac89f6482f9699))
* **cli:** align reference import failure envelopes ([8224aeb](https://github.com/morluto/rea/commit/8224aeba9dd41f29ea6f8ef7ad7efa3f45fd4a6e))
* **cli:** cache subprocess compilation within each worker ([6bcfeb0](https://github.com/morluto/rea/commit/6bcfeb0e55a6fcc3ed586e5744f2aa1c270cac31))
* **cli:** consolidate development runner workflow coverage ([16f5ded](https://github.com/morluto/rea/commit/16f5ded4468d7100d3640f874f33ddbaa953a787))
* **cli:** consolidate schema coverage and assert input refusals ([beb80a0](https://github.com/morluto/rea/commit/beb80a07bafc0847600eb635e0323068a386c811))
* **clients:** verify native Claude Code integration on Windows ([#1926](https://github.com/morluto/rea/issues/1926)) ([c32d846](https://github.com/morluto/rea/commit/c32d84622fb3952823c6cfc25c557aec08c1788c)), closes [#1923](https://github.com/morluto/rea/issues/1923)
* **clients:** verify native Gemini CLI integration on Windows ([#1936](https://github.com/morluto/rea/issues/1936)) ([04089d5](https://github.com/morluto/rea/commit/04089d5bec11d6e0411e7c35d0168bbae63d7e7d))
* **cli:** preserve Windows runtime paths and failure diagnostics ([79156e5](https://github.com/morluto/rea/commit/79156e5bed1d72b83ade0483246f457b337b7e59))
* **docs:** type README drift scenarios as fixed pairs ([6e38529](https://github.com/morluto/rea/commit/6e38529333a7a0704690a087c7cb054fb7385d25))
* **evidence:** cover workflow question whitespace normalization ([#1739](https://github.com/morluto/rea/issues/1739)) ([b2da21a](https://github.com/morluto/rea/commit/b2da21abe11ac43a4cc24b233bd52ce10995e4a1))
* **evm:** verify real worker cancellation and cleanup recovery ([#2404](https://github.com/morluto/rea/issues/2404)) ([ae80513](https://github.com/morluto/rea/commit/ae8051302b5834322fa00cfa31c968ae74ab6ed9))
* **evm:** verify static descriptor bindings before use ([#2427](https://github.com/morluto/rea/issues/2427)) ([4f4f6cd](https://github.com/morluto/rea/commit/4f4f6cd7b5d7a400b90772a138d7b9f942c2a556))
* **ghidra:** require pending multi-HI16 coverage in the real MIPS relocatable lane ([#1974](https://github.com/morluto/rea/issues/1974)) ([eaf88ac](https://github.com/morluto/rea/commit/eaf88aca2af2b8ad7a277c09d0b45f6e2de8ac4a))
* **ghidra:** restore complete installation fixtures across CI boundaries ([18d56f5](https://github.com/morluto/rea/commit/18d56f5321c3c5d5be40c19bcb0a0f2b25c2d459))
* **hopper:** align startup fixtures with cleanup and lease contracts ([b3dec00](https://github.com/morluto/rea/commit/b3dec005bcb89b5afe6e0aa41e5251f8a3a31de7))
* **hopper:** repair Linux demo setup and native verification ([#2638](https://github.com/morluto/rea/issues/2638)) ([141d209](https://github.com/morluto/rea/commit/141d2095f5c6faaf2d3c9b81b0f0f992cacec79c))
* **ida:** expect direction in combined call coverage ([4dd44dd](https://github.com/morluto/rea/commit/4dd44ddf16c810c4bcaa16e0bdb3b09bfcb4e33a))
* **javascript:** classify artifact workflows as boundary tests ([dc303a8](https://github.com/morluto/rea/commit/dc303a8ee8a2ceaae37e4da4ac4ed64035793d59))
* **javascript:** consolidate module lookup regressions ([#2031](https://github.com/morluto/rea/issues/2031)) ([783fcb0](https://github.com/morluto/rea/commit/783fcb06a4bd0444be214822fed60a3786932782))
* **javascript:** expect unknown parameter provenance ([e05d339](https://github.com/morluto/rea/commit/e05d3399f19f25fdf04285a02b293418256b26dc))
* **javascript:** verify CommonJS file URL boundaries via CLI and MCP ([#2061](https://github.com/morluto/rea/issues/2061)) ([6ce5903](https://github.com/morluto/rea/commit/6ce5903e7a37252156ec847c4c8874410a4d8f3f))
* **managed:** use the current attribute fixture contract ([6407718](https://github.com/morluto/rea/commit/6407718f3020a34cc087163492e3f9a465e2e841))
* **mcp:** align completion checks with provider readiness ([fe4b94a](https://github.com/morluto/rea/commit/fe4b94a661054cef7603a3f07aae71f1df0a701a))
* **mcp:** fix three MCP boundary suites on native Windows ([#2312](https://github.com/morluto/rea/issues/2312)) ([a97b713](https://github.com/morluto/rea/commit/a97b713f740932339a5f1036ad2213168c421270))
* **mcp:** preserve coverage in unchanged function fixtures ([3ee6ff4](https://github.com/morluto/rea/commit/3ee6ff43c1696a5a5269bd4aa5de72f7e5f40627))
* **mcp:** ready providers before live prompt completion checks ([#2137](https://github.com/morluto/rea/issues/2137)) ([621cee1](https://github.com/morluto/rea/commit/621cee192d582c87ca3d254d4cb22c9d209d3aea)), closes [#2123](https://github.com/morluto/rea/issues/2123)
* **mcp:** reuse advertised schema in inventory check ([#2469](https://github.com/morluto/rea/issues/2469)) ([a6efa4f](https://github.com/morluto/rea/commit/a6efa4fcefeffcf5ff5310b8154988b4be30c349))
* **native:** replay complete dylib-use provider output ([89f5e48](https://github.com/morluto/rea/commit/89f5e489adac69af2937fabc1a5ba283191d630a)), closes [#1970](https://github.com/morluto/rea/issues/1970)
* **native:** skip the DMG EACCES diagnostic as root ([#1584](https://github.com/morluto/rea/issues/1584)) ([262efe6](https://github.com/morluto/rea/commit/262efe6c54d8f179e9f9515d96025871ae45806a))
* **native:** skip the EACCES directory diagnostic as root ([#1394](https://github.com/morluto/rea/issues/1394)) ([40d9c7b](https://github.com/morluto/rea/commit/40d9c7b836a3aca5d589c0a65c7d02b3ea5cd36b))
* **native:** skip the inaccessible-directory diagnostic as root ([#1900](https://github.com/morluto/rea/issues/1900)) ([d0edeec](https://github.com/morluto/rea/commit/d0edeec1009f1149956ee2a95dc5941aff8f78b5))
* **native:** verify real PE and DOS entry views ([1770f38](https://github.com/morluto/rea/commit/1770f38cfc6cbe87491d0de8b8730c305c5cf8ea))
* **pcap:** run the missing-path refusal only where captures are admitted ([#2510](https://github.com/morluto/rea/issues/2510)) ([1991542](https://github.com/morluto/rea/commit/1991542e59f64d46269b3ff942be77a5ff06e88f))
* **perf:** distinguish Codex tool context and token accounting ([#2416](https://github.com/morluto/rea/issues/2416)) ([ee87111](https://github.com/morluto/rea/commit/ee87111009ace2ca4ef82c9aef09c82a37ca55f7))
* **perf:** observe owned counters at payload completion ([ca2d15b](https://github.com/morluto/rea/commit/ca2d15b3ee965bd7ab27204cdf4b96b117d148c1))
* **perf:** verify existing SQLite column selection on QGIS data ([#2489](https://github.com/morluto/rea/issues/2489)) ([45984a4](https://github.com/morluto/rea/commit/45984a4319c8b6dcf2b03e969fbd4330c8432394))
* **reference:** scope cleanup faults to the owned source file ([#2430](https://github.com/morluto/rea/issues/2430)) ([eeba618](https://github.com/morluto/rea/commit/eeba61826c1cd1c7be70f011fda0537baadd4179))
* **release:** validate only the selected preflight job ([747e729](https://github.com/morluto/rea/commit/747e7299141cec0b03774c427f579889d5088d20))
* reuse application analysis across comparison directions ([044c0a9](https://github.com/morluto/rea/commit/044c0a92d5c86faa9965669680935d4f552977b1))
* reuse identical JavaScript analysis fixtures ([42951bb](https://github.com/morluto/rea/commit/42951bbbbe8d98ebde1783450d130fdddb5ba889))
* reuse validators for identical MCP schemas ([85ccef3](https://github.com/morluto/rea/commit/85ccef34de1cddcf71037ccea93acdf709f94965))
* share native keyed archive integer fixture generation ([847a1a3](https://github.com/morluto/rea/commit/847a1a3f25f69e1b3808fb0a53e5f1415a7ed058))
* **sourcemap:** consolidate source map regressions ([#2034](https://github.com/morluto/rea/issues/2034)) ([83a4445](https://github.com/morluto/rea/commit/83a4445f440fab978ad9376b4260acae3f5fb189))
* **sourcemaps:** assert semantic evidence and actual output admission ([bd74c7a](https://github.com/morluto/rea/commit/bd74c7af2bec952c2eea7a17feedd05e6675b719))
* strengthen archive and resource cleanup assertions ([5ba1642](https://github.com/morluto/rea/commit/5ba1642a7d891a60493b2195e55149b0f3197fb4))
* strengthen public input and module lookup controls ([b7f563e](https://github.com/morluto/rea/commit/b7f563e15f854061dc1507896181b7b24ac3b5f0))
* **web:** remove duplicate path schema cases ([b5b7301](https://github.com/morluto/rea/commit/b5b73019f946e5edc0df506c9f8f4bb28afa6590))
### Continuous Integration
* balance test shards and reuse package download caches ([40b37ae](https://github.com/morluto/rea/commit/40b37aeee10e89d85ee8b7747cbdcd3ad1a34db4))
* bound core job execution time ([9f3f392](https://github.com/morluto/rea/commit/9f3f392eb8fe81060b44941ed754521984def3bd))
* finish main validation and balance complete test costs ([f0f0995](https://github.com/morluto/rea/commit/f0f09950f34d96e3b1c62fd04821af5e301f80c7))
* fix PR metadata triggers and defer expensive runner work ([3af8f84](https://github.com/morluto/rea/commit/3af8f847f0e35843f51622a136165dd6ee12cdd0))
* keep validation fresh and preserve failed-shard reports ([#2210](https://github.com/morluto/rea/issues/2210)) ([428eee0](https://github.com/morluto/rea/commit/428eee00fdd0406cdf2b734c9ea14a2bb02da279))
* narrow native-host lane selection and reuse build caches ([#2131](https://github.com/morluto/rea/issues/2131)) ([4f46eac](https://github.com/morluto/rea/commit/4f46eac453965efde3bc782aebfeec198551afc4))
* observe affected-owner test plans against full results ([fa0a9b2](https://github.com/morluto/rea/commit/fa0a9b2d328a07d8ec2924dc224d51c620981dcc))
* parallelize native verification and share test artifacts ([91f6840](https://github.com/morluto/rea/commit/91f6840c35e99e5f23612920d790a30d324ac115))
* raise duplication threshold to 3.5 percent ([11f1d95](https://github.com/morluto/rea/commit/11f1d952e79241b507e47c690eed00e0d5ffda30))
* reduce macOS runner contention ([3f4ea32](https://github.com/morluto/rea/commit/3f4ea3218929baba0c30c2cb7eb7e7a829a1fc21))
* reduce redundant compatibility and qualification work ([d13c9f8](https://github.com/morluto/rea/commit/d13c9f840d36a321a56917e75c48afcb8d72920e))
* reduce reporter fan-out and preserve shard history ([ab02f3c](https://github.com/morluto/rea/commit/ab02f3c8d665ead35391c94db74cd0fb0cfed0ed))
* reuse current-run builds across provider lanes ([490a285](https://github.com/morluto/rea/commit/490a285070268aee0ecd50001b9a9a83c1e313f0))
* reuse portable build cache in native package jobs ([8fa1175](https://github.com/morluto/rea/commit/8fa11751eaab94eaa5b8de8c1ecd143ef0066cd6))
* reuse portable Turbo outputs across verification runs ([a00c34d](https://github.com/morluto/rea/commit/a00c34dcb155e5993bda8ad0f51bf3b13e289760))
* scope timing helpers to portable verification ([c8d861f](https://github.com/morluto/rea/commit/c8d861f9764f8c07f0925dae0c6451de85fffd6a))
* separate fast feedback from full qualification ([0c7f4df](https://github.com/morluto/rea/commit/0c7f4dfbd9c1975eeedfecd96debb2e4263012d0))
* summarize static checks and retain complete reports ([#2242](https://github.com/morluto/rea/issues/2242)) ([d96550f](https://github.com/morluto/rea/commit/d96550f2f61647ffa150597de73363fa90c4e169))
---
This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please).
+268
View File
@@ -0,0 +1,268 @@
import { execFile } from "node:child_process";
import { appendFile, mkdir, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
import { collectJobTimings } from "./job-timings.mjs";
const execFileAsync = promisify(execFile);
const OUTPUT_DIRECTORY = join(".cache", "ci-job-timings");
const MAX_BUFFER = 16 * 1024 * 1024;
const API_TIMEOUT_MS = 30_000;
const WORKFLOWS = [
{ id: "ci.yml", name: "CI" },
{ id: "release.yml", name: "Release" },
];
const LOOKBACK_MS = 24 * 60 * 60 * 1_000;
const MAX_RUNS_PER_WORKFLOW = 100;
const TIME_BUDGET_MS = 240_000;
/** Collect a bounded recent sample of completed CI and Release attempts. */
export async function collectJobTimingsBatch({
repository,
outputDirectory = OUTPUT_DIRECTORY,
observedAt = new Date().toISOString(),
env = process.env,
now = Date.now,
collect = collectJobTimings,
} = {}) {
const identity = validateRepository(repository);
const started = now();
const cutoff = new Date(started - LOOKBACK_MS).toISOString();
const manifest = {
schemaVersion: 1,
observedAt,
repository: identity,
selection: {
workflows: WORKFLOWS,
status: "completed",
createdAfter: cutoff,
lookbackHours: 24,
overlap: "Each hourly run re-reads the preceding 24 hours.",
pagination:
"Run listings use the first 100; jobs are paginated within the remaining time and 16 MiB response limits.",
maxRunsPerWorkflow: MAX_RUNS_PER_WORKFLOW,
timeBudgetMs: TIME_BUDGET_MS,
exhaustive: false,
},
coverage: {
workflowsQueried: [],
runsListed: 0,
selectedRuns: 0,
collectedRuns: 0,
},
truncation: [],
errors: [],
runs: [],
elapsedMs: 0,
};
const budgetExpired = () => now() - started >= TIME_BUDGET_MS;
const selected = [];
for (const workflow of WORKFLOWS) {
if (budgetExpired()) {
manifest.truncation.push("Time budget reached while listing workflows.");
break;
}
try {
const response = await apiJson(
`repos/${identity}/actions/workflows/${workflow.id}/runs?status=completed&created=%3E%3D${encodeURIComponent(cutoff)}&per_page=${String(MAX_RUNS_PER_WORKFLOW)}&page=1`,
env,
started,
now,
);
if (!Array.isArray(response.workflow_runs))
throw new Error("run listing must contain a workflow_runs array");
manifest.coverage.workflowsQueried.push(workflow.name);
manifest.coverage.runsListed += response.workflow_runs.length;
if (response.workflow_runs.length >= MAX_RUNS_PER_WORKFLOW)
manifest.truncation.push(
`${workflow.name} run listing reached the 100-run first-page limit.`,
);
for (const run of response.workflow_runs.slice(
0,
MAX_RUNS_PER_WORKFLOW,
)) {
if (
run.status !== "completed" ||
!Number.isSafeInteger(run.id) ||
!Number.isSafeInteger(run.run_attempt)
) {
manifest.errors.push({
scope: `workflow:${workflow.name}`,
runId: run.id ?? null,
message:
"Run record missing completed status, numeric id, or numeric latest attempt.",
});
continue;
}
if (run.created_at && Date.parse(run.created_at) < Date.parse(cutoff))
continue;
selected.push({
id: run.id,
runAttempt: run.run_attempt,
workflow: workflow.name,
conclusion: run.conclusion ?? null,
createdAt: run.created_at ?? null,
});
}
} catch (cause) {
manifest.errors.push({
scope: `workflow:${workflow.name}`,
message: errorMessage(cause),
});
}
}
selected.sort(
(left, right) =>
(Date.parse(right.createdAt ?? "") || 0) -
(Date.parse(left.createdAt ?? "") || 0),
);
manifest.coverage.selectedRuns = selected.length;
for (const run of selected) {
if (budgetExpired()) {
manifest.truncation.push(
"Time budget reached before all selected runs were collected.",
);
break;
}
const runRecord = {
...run,
status: "pending",
rawPath: null,
reportPath: null,
error: null,
};
manifest.runs.push(runRecord);
const runDirectory = join(
outputDirectory,
"runs",
`${String(run.id)}-${String(run.runAttempt)}`,
);
try {
const result = await collect({
repository: identity,
runId: String(run.id),
runAttempt: String(run.runAttempt),
outputDirectory: runDirectory,
env,
timeoutMs: Math.max(
1,
Math.min(API_TIMEOUT_MS, TIME_BUDGET_MS - (now() - started)),
),
});
runRecord.status = "collected";
runRecord.rawPath = result.rawPath;
runRecord.reportPath = result.reportPath;
manifest.coverage.collectedRuns += 1;
} catch (cause) {
runRecord.status = "error";
runRecord.error = errorMessage(cause);
manifest.errors.push({
scope: "run",
runId: run.id,
runAttempt: run.runAttempt,
message: runRecord.error,
});
}
}
if (budgetExpired() && manifest.runs.length < manifest.coverage.selectedRuns)
manifest.truncation.push(
"Time budget reached; selected runs remain unattempted.",
);
return await writeManifest(outputDirectory, manifest, started, now, env);
}
function validateRepository(repository) {
if (
typeof repository !== "string" ||
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository)
)
throw new Error(
"CI_REPOSITORY must be an owner/name repository identifier",
);
return repository;
}
async function apiJson(endpoint, env, started, now) {
const timeout = Math.max(
1,
Math.min(API_TIMEOUT_MS, TIME_BUDGET_MS - (now() - started)),
);
const { stdout } = await execFileAsync("gh", ["api", endpoint], {
env,
encoding: "utf8",
maxBuffer: MAX_BUFFER,
timeout,
});
try {
return JSON.parse(stdout);
} catch (cause) {
throw new Error("GitHub Actions API returned malformed JSON", { cause });
}
}
async function writeManifest(outputDirectory, manifest, started, now, env) {
manifest.elapsedMs = Math.max(0, now() - started);
if (
manifest.elapsedMs >= TIME_BUDGET_MS &&
!manifest.truncation.some((item) => item.includes("Time budget"))
)
manifest.truncation.push("Time budget reached.");
await mkdir(outputDirectory, { recursive: true });
const manifestPath = join(outputDirectory, "batch-manifest.json");
await writeFile(
manifestPath,
`${JSON.stringify(manifest, null, 2)}\n`,
"utf8",
);
if (env.GITHUB_STEP_SUMMARY)
await appendBatchSummary(env.GITHUB_STEP_SUMMARY, manifest, manifestPath);
return { manifestPath, manifest };
}
async function appendBatchSummary(summaryPath, manifest, manifestPath) {
const collected = manifest.runs.filter(
({ status }) => status === "collected",
).length;
const failed = manifest.runs.filter(
({ status }) => status === "error",
).length;
const lines = [
"### Batched CI timing snapshot",
"",
`Observed ${manifest.observedAt}; collected ${String(collected)} of ${String(manifest.coverage.selectedRuns)} selected recent completed runs (${String(failed)} run errors).`,
`Coverage uses the first ${String(MAX_RUNS_PER_WORKFLOW)} runs per workflow created in the preceding 24 hours and a ${String(TIME_BUDGET_MS / 1_000)} second total budget. This sample is bounded and is not exhaustive.`,
`Manifest: \`${manifestPath}\`.`,
...manifest.truncation.map((message) => `- Truncated: ${message}`),
...manifest.errors.map(
({ scope, message }) => `- Error (${scope}): ${message}`,
),
"",
];
await appendFile(summaryPath, lines.join("\n"), "utf8");
}
function errorMessage(cause) {
return cause instanceof Error ? cause.message : String(cause);
}
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
try {
const result = await collectJobTimingsBatch({
repository: process.env.CI_REPOSITORY,
outputDirectory: process.env.CI_JOB_TIMINGS_DIRECTORY ?? OUTPUT_DIRECTORY,
});
process.stdout.write(
`Wrote batch manifest for ${String(result.manifest.coverage.collectedRuns)} collected runs to ${result.manifestPath}\n`,
);
} catch (cause) {
process.stderr.write(`CI timing batch failed: ${errorMessage(cause)}\n`);
process.exitCode = 1;
}
}
+354
View File
@@ -0,0 +1,354 @@
import { execFile } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { promisify } from "node:util";
import { fileURLToPath } from "node:url";
const execFileAsync = promisify(execFile);
const MAX_BUFFER = 16 * 1024 * 1024;
const TIMEOUT_MS = 30_000;
const OUTPUT_DIRECTORY = join(".cache", "ci-job-timings");
const STEP_COSTS = [
{ key: "npmCi", pattern: /\bnpm ci\b/iu },
{ key: "artifactUpload", pattern: /upload.*artifact/iu },
{ key: "artifactDownload", pattern: /download.*artifact/iu },
];
/** Fetch and summarize one workflow attempt without turning telemetry into a gate. */
export async function collectJobTimings({
repository,
runId,
runAttempt,
outputDirectory = OUTPUT_DIRECTORY,
summaryPath,
observedAt = new Date().toISOString(),
env = process.env,
timeoutMs = TIMEOUT_MS,
}) {
const identity = validateIdentity({ repository, runId, runAttempt });
const endpoint = `repos/${identity.repository}/actions/runs/${identity.runId}/attempts/${identity.runAttempt}/jobs?per_page=100`;
const { stdout } = await execFileAsync(
"gh",
["api", "--paginate", "--slurp", endpoint],
{ env, encoding: "utf8", maxBuffer: MAX_BUFFER, timeout: timeoutMs },
);
const rawPath = join(outputDirectory, "jobs.json");
const reportPath = join(outputDirectory, "report.json");
await mkdir(outputDirectory, { recursive: true });
await writeFile(rawPath, stdout, "utf8");
const pages = parsePages(stdout);
const jobs = pages.flatMap(({ jobs: pageJobs }, index) =>
validateJobs(pageJobs, index),
);
const report = normalizeReport({ identity, jobs, observedAt });
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`, "utf8");
if (summaryPath !== undefined)
await appendSummary(summaryPath, report, rawPath, reportPath);
return { rawPath, reportPath, report };
}
function validateIdentity({ repository, runId, runAttempt }) {
if (
typeof repository !== "string" ||
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/u.test(repository)
)
throw new Error(
"CI_REPOSITORY must be an owner/name repository identifier",
);
return {
repository,
runId: parsePositiveInteger(runId, "CI_TIMING_RUN_ID"),
runAttempt: parsePositiveInteger(runAttempt, "CI_TIMING_RUN_ATTEMPT"),
};
}
function parsePositiveInteger(value, name) {
if (typeof value === "string" && /^[1-9]\d*$/u.test(value)) {
const parsed = Number(value);
if (Number.isSafeInteger(parsed)) return parsed;
}
throw new Error(`${name} must be a positive safe integer`);
}
function parsePages(stdout) {
let pages;
try {
pages = JSON.parse(stdout);
} catch (cause) {
throw new Error("GitHub jobs API returned malformed JSON", { cause });
}
if (!Array.isArray(pages) || pages.length === 0)
throw new Error("GitHub jobs API slurp must contain a nonempty page array");
for (const page of pages)
if (
typeof page !== "object" ||
page === null ||
Array.isArray(page) ||
!Array.isArray(page.jobs)
)
throw new Error("GitHub jobs API page must contain a jobs array");
return pages;
}
function validateJobs(jobs, pageIndex) {
return jobs.map((job, jobIndex) => {
if (
typeof job !== "object" ||
job === null ||
Array.isArray(job) ||
typeof job.name !== "string" ||
job.name.length === 0 ||
typeof job.status !== "string"
)
throw new Error(
`GitHub jobs API page ${String(pageIndex + 1)} job ${String(jobIndex + 1)} is malformed`,
);
return job;
});
}
function normalizeReport({ identity, jobs, observedAt }) {
const normalizedJobs = jobs.map(normalizeJob);
const queues = normalizedJobs
.map(({ queue }) => queue.seconds)
.filter((value) => value !== null);
const completedExecutions = normalizedJobs
.filter(
({ status, conclusion }) =>
status === "completed" && conclusion !== "skipped",
)
.map(({ execution }) => execution.seconds)
.filter((value) => value !== null);
const completedDurationSumSeconds = completedExecutions.reduce(
(sum, seconds) => sum + seconds,
0,
);
const completedExecutionUnknown = normalizedJobs.filter(
({ status, conclusion, execution }) =>
status === "completed" &&
conclusion !== "skipped" &&
execution.seconds === null,
).length;
const skipped = normalizedJobs.filter(
({ conclusion }) => conclusion === "skipped",
).length;
const costs = Object.fromEntries(STEP_COSTS.map(({ key }) => [key, []]));
for (const job of normalizedJobs)
for (const step of job.steps)
if (step.costKey !== null)
costs[step.costKey].push({ job: job.name, ...step });
return {
schemaVersion: 1,
run: {
...identity,
observedAt,
snapshot:
normalizedJobs.length > 0 &&
normalizedJobs.every(({ status }) => status === "completed")
? "completed job status snapshot; unknown timestamps remain unmeasured"
: "partial; job statuses may still be queued or in progress",
},
counts: {
jobs: normalizedJobs.length,
completed: normalizedJobs.filter(({ status }) => status === "completed")
.length,
skipped,
queueUnknown: normalizedJobs.filter(({ queue }) => queue.seconds === null)
.length,
executionUnknown: normalizedJobs.filter(
({ execution }) => execution.seconds === null,
).length,
completedExecutionUnknown,
},
aggregates: {
queueSeconds: percentileSummary(queues),
completedExecutionSeconds: percentileSummary(completedExecutions),
summedCompletedJobElapsedSeconds: {
value:
completedExecutions.length === 0
? null
: round(completedDurationSumSeconds),
knownCount: completedExecutions.length,
unknownCount: completedExecutionUnknown,
label:
"sum of known completed job elapsed durations; incomplete when timestamps are unknown; not CPU time, billing, or workflow wall time",
},
},
jobs: normalizedJobs,
stepCostInterpretation:
"Matched install/artifact steps by step name; these are not total artifact transfer costs.",
stepCosts: costs,
};
}
function normalizeJob(job) {
const skipped = job.conclusion === "skipped";
const hasStarted = ["in_progress", "completed"].includes(job.status);
const queue = skipped
? unknownDuration("job was skipped")
: !hasStarted
? unknownDuration("job start is not established by its status")
: duration(job.created_at, job.started_at);
const execution = skipped
? unknownDuration("job was skipped")
: job.status === "completed"
? duration(job.started_at, job.completed_at)
: unknownDuration("job is not completed");
const steps = Array.isArray(job.steps) ? job.steps.map(normalizeStep) : [];
return {
id: Number.isSafeInteger(job.id) && job.id > 0 ? job.id : null,
url: stringOrNull(job.html_url),
name: job.name,
workflowName: stringOrNull(job.workflow_name),
status: job.status,
conclusion: stringOrNull(job.conclusion),
createdAt: stringOrNull(job.created_at),
startedAt: stringOrNull(job.started_at),
completedAt: stringOrNull(job.completed_at),
queue,
execution,
steps,
};
}
function normalizeStep(step) {
if (
typeof step !== "object" ||
step === null ||
Array.isArray(step) ||
typeof step.name !== "string"
)
throw new Error("GitHub jobs API contains a malformed job step");
const cost = STEP_COSTS.find(({ pattern }) => pattern.test(step.name));
return {
name: step.name,
costKey: cost?.key ?? null,
duration: duration(step.started_at, step.completed_at),
};
}
function duration(startValue, endValue) {
if (typeof startValue !== "string" || typeof endValue !== "string")
return unknownDuration("timestamp unavailable");
const start = Date.parse(startValue);
const end = Date.parse(endValue);
if (!Number.isFinite(start) || !Number.isFinite(end))
return unknownDuration("invalid timestamp");
const milliseconds = end - start;
if (!Number.isFinite(milliseconds) || milliseconds < 0)
return unknownDuration("timestamps out of order");
return { seconds: round(milliseconds / 1_000), reason: null };
}
function unknownDuration(reason) {
return { seconds: null, reason };
}
function percentileSummary(values) {
return {
count: values.length,
p50: nearestRank(values, 0.5),
p95: nearestRank(values, 0.95),
unit: "seconds",
};
}
function nearestRank(values, percentile) {
if (values.length === 0) return null;
const sorted = [...values].sort((left, right) => left - right);
return round(sorted[Math.ceil(percentile * sorted.length) - 1]);
}
function round(value) {
return Math.round(value * 1_000) / 1_000;
}
function stringOrNull(value) {
return typeof value === "string" ? value : null;
}
async function appendSummary(summaryPath, report, rawPath, reportPath) {
await mkdir(dirname(summaryPath), { recursive: true });
const rows = [
["Queue", report.aggregates.queueSeconds],
["Completed execution", report.aggregates.completedExecutionSeconds],
]
.map(
([label, item]) =>
`| ${label} | ${display(item.p50)} | ${display(item.p95)} | ${String(item.count)} |`,
)
.join("\n");
const jobRows = report.jobs
.map(
(job) =>
`| ${escapeCell(job.name)} | ${job.status} | ${job.conclusion ?? "unknown"} | ${display(job.queue.seconds)} | ${job.queue.seconds === null ? job.queue.reason : ""} | ${display(job.execution.seconds)} | ${job.execution.seconds === null ? job.execution.reason : ""} |`,
)
.join("\n");
const costRows = STEP_COSTS.map(({ key }) => {
const samples = report.stepCosts[key];
const values = samples
.map(({ duration: item }) => item.seconds)
.filter((value) => value !== null);
return `| ${key} | ${String(samples.length)} | ${display(nearestRank(values, 0.5))} | ${samples.filter(({ duration: item }) => item.seconds === null).length} |`;
}).join("\n");
const text = [
"### CI job timing snapshot",
"",
`Observed at ${report.run.observedAt} for run ${String(report.run.runId)}, attempt ${String(report.run.runAttempt)}. This is a point-in-time snapshot of fetched jobs. Queue starts at job creation and excludes preceding dependency waiting. Missing, queued, in-progress, invalid, or out-of-order timestamps are reported as unknown. Elapsed durations are not CPU time or billing data.`,
"",
`Fetched ${String(report.counts.jobs)} jobs; ${String(report.counts.completed)} are completed (${String(report.counts.skipped)} skipped); ${String(report.counts.queueUnknown)} queue durations and ${String(report.counts.executionUnknown)} execution durations (${String(report.counts.completedExecutionUnknown)} for non-skipped completed jobs) are unknown.`,
"",
"| Metric | p50 (s) | p95 (s) | Samples |",
"| --- | ---: | ---: | ---: |",
rows,
"",
"| Job | Status | Conclusion | Queue (s) | Queue detail | Execution (s) | Execution detail |",
"| --- | --- | --- | ---: | --- | ---: | --- |",
jobRows,
"",
`Sum of known completed job elapsed durations: **${display(report.aggregates.summedCompletedJobElapsedSeconds.value)} s** across ${String(report.aggregates.summedCompletedJobElapsedSeconds.knownCount)} known and ${String(report.aggregates.summedCompletedJobElapsedSeconds.unknownCount)} unknown jobs; this is not CPU time, billing, or workflow wall time.`,
"",
"Matched install/artifact steps by name; these are not total artifact transfer costs.",
"",
"| Matched step group | Samples | p50 (s) | Unknown |",
"| --- | ---: | ---: | ---: |",
costRows,
"",
`Reports: \`${rawPath}\`, \`${reportPath}\`.`,
"",
].join("\n");
await writeFile(summaryPath, text, { encoding: "utf8", flag: "a" });
}
function display(value) {
return value === null ? "unknown" : String(value);
}
function escapeCell(value) {
return value.replaceAll("|", "\\|").replaceAll("\n", " ");
}
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
try {
const result = await collectJobTimings({
repository: process.env.CI_REPOSITORY,
runId: process.env.CI_TIMING_RUN_ID,
runAttempt: process.env.CI_TIMING_RUN_ATTEMPT,
outputDirectory: process.env.CI_JOB_TIMINGS_DIRECTORY ?? OUTPUT_DIRECTORY,
summaryPath: process.env.GITHUB_STEP_SUMMARY,
});
process.stdout.write(
`Wrote ${result.report.jobs.length} job timing records to ${result.reportPath}\n`,
);
} catch (cause) {
process.stderr.write(
`CI timing telemetry failed: ${cause instanceof Error ? cause.message : String(cause)}\n`,
);
process.exitCode = 1;
}
}
+45 -25
View File
@@ -1,30 +1,20 @@
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { matchesGlob } from "node:path";
import { MANUAL_VERIFIERS, SCOPES } from "./scopes.mjs";
import { CI_FLAGS as flags, MANUAL_VERIFIERS, SCOPES } from "./scopes.mjs";
import { planTestSelection } from "./test-selection.mjs";
const flags = [
...new Set([
"code",
"runtime",
"package",
"apple",
"fixtures",
"readiness",
"docs",
"workflows",
...Object.keys(SCOPES),
]),
];
// Test files select the native-host lanes that run them explicitly. Provider
// scopes remain implementation-owned, so test-only edits stay off those lanes.
const NATIVE_TEST_OWNERS = ["apple", "package"];
// Test files select the host/runtime lanes that execute their focused suites.
const HOST_TEST_OWNERS = ["apple", "package", "sqlite"];
// Actionlint validates workflow syntax only. The semantic suite asserts these
// workflows' release, publication and self-hosted runner safeguards.
const GUARDED_WORKFLOWS = new Set([
".github/workflows/pr-title.yml",
".github/workflows/ci-full-pr.yml",
".github/workflows/release.yml",
".github/workflows/real-ghidra-windows.yml",
".github/workflows/sqlite-runtime.yml",
".github/workflows/ci-timings.yml",
]);
// Sources compiled by build-conformance-fixtures.mjs. The readiness journey
// analyzes the built C fixture as well as its own applications.
@@ -36,6 +26,15 @@ const FIXTURE_SOURCES = [
"tests/conformance/versions/**",
];
const READINESS_SOURCES = ["tests/conformance/readiness/**"];
// These reporters affect CI output only; they do not select a host or provider
// verification lane when changed on their own.
const PORTABLE_CI_REPORTERS = new Set([
"scripts/ci/job-timings.mjs",
"scripts/ci/job-timings-batch.mjs",
"scripts/ci/test-report.mjs",
"scripts/ci/test-timing-reporter.mjs",
"scripts/ci/test-history.mjs",
]);
const matchesAny = (file, patterns) =>
patterns.some((pattern) => matchesGlob(file, pattern));
const selected = Object.fromEntries(flags.map((flag) => [flag, false]));
@@ -63,6 +62,10 @@ function classifyPackage(base, head) {
]),
].filter((key) => beforeScripts?.[key] !== afterScripts?.[key]);
for (const script of changed) {
if (script === "test:sqlite" || script === "test:sqlite:run") {
selected.sqlite = true;
continue;
}
if (
/^(test(?::|$)|check(?::|$)|lint(?::|$)|format(?::|$)|knip$|jscpd$)/.test(
script,
@@ -110,14 +113,24 @@ function classifyPackage(base, head) {
const event = process.env.GITHUB_EVENT_NAME;
if (!["pull_request", "push", "schedule", "workflow_dispatch"].includes(event))
throw new Error(`Unsupported CI event: ${event}`);
const tier =
const requestedFull =
event === "schedule" ||
event === "workflow_dispatch" ||
process.env.CI_FULL === "true"
? "full"
: "fast";
process.env.CI_FULL === "true";
const tier =
event === "schedule" &&
process.env.CI_SKIP_FULL === "true" &&
process.env.CI_FULL !== "true"
? "reused"
: requestedFull
? "full"
: "fast";
let files = [];
if (tier === "full") {
if (tier === "reused") {
reasons.push(
"Recent exact-SHA full qualification reused; no fresh provider observation",
);
} else if (tier === "full") {
full("Full baseline requested by event or ci:full label");
} else if (event === "pull_request" || event === "push") {
const head = process.env.HEAD_SHA;
@@ -162,7 +175,7 @@ if (tier === "full") {
}
if (/^(src|tests)\/.*\.(?:test|spec)\.[cm]?[jt]sx?$/.test(file)) {
selected.code = true;
for (const owner of NATIVE_TEST_OWNERS)
for (const owner of HOST_TEST_OWNERS)
selected[owner] ||= matchesAny(file, SCOPES[owner]);
continue;
}
@@ -197,6 +210,7 @@ if (tier === "full") {
}
if (matchesAny(file, READINESS_SOURCES)) selected.readiness = true;
if (file.startsWith("tests/")) continue;
if (PORTABLE_CI_REPORTERS.has(file)) continue;
if (
/^(src\/process\/|src\/filesystem\/|src\/contracts\/tool|src\/server\/createServer|src\/cli\.ts|scripts\/ci\/)/.test(
file,
@@ -237,11 +251,17 @@ const matrix = {
]
: [linux],
};
const plan = { ...selected, tier, matrix, files, reasons };
const testSelection = planTestSelection(files, event, tier);
const plan = { ...selected, tier, matrix, files, reasons, testSelection };
if (process.env.GITHUB_OUTPUT)
appendFileSync(
process.env.GITHUB_OUTPUT,
Object.entries({ ...selected, tier, matrix: JSON.stringify(matrix) })
Object.entries({
...selected,
tier,
matrix: JSON.stringify(matrix),
test_selection: JSON.stringify(testSelection),
})
.map(([key, value]) => `${key}=${value}\n`)
.join(""),
);
+176
View File
@@ -0,0 +1,176 @@
import { execFileSync } from "node:child_process";
import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const ARTIFACT_NAME = "ci-full-qualification-v1";
const MAX_AGE_MS = 24 * 60 * 60 * 1000;
const positiveId = (value) =>
(typeof value === "string" || typeof value === "number") &&
/^[1-9]\d*$/.test(String(value)) &&
Number.isSafeInteger(Number(value));
const commitSha = (value) =>
typeof value === "string" && /^[0-9a-f]{40}$/.test(value);
/** Record only after the required gate has verified every full-tier obligation. */
export function recordQualification(directory) {
const sourceSha = process.env.GITHUB_SHA;
const runId = process.env.GITHUB_RUN_ID;
const runAttempt = process.env.GITHUB_RUN_ATTEMPT;
const repository = process.env.GITHUB_REPOSITORY;
if (
!commitSha(sourceSha) ||
!positiveId(runId) ||
!positiveId(runAttempt) ||
!repository
)
throw new Error(
"Full qualification receipt requires source and workflow identity",
);
const checkoutSha = execFileSync("git", ["rev-parse", "HEAD"], {
encoding: "utf8",
}).trim();
if (checkoutSha !== sourceSha)
throw new Error("Qualification checkout differs from workflow source");
mkdirSync(directory, { recursive: true });
const path = resolve(directory, "receipt.json");
writeFileSync(
path,
`${JSON.stringify(
{
schemaVersion: 1,
tier: "full",
sourceSha,
repository,
runId: Number(runId),
runAttempt: Number(runAttempt),
qualifiedAt: new Date().toISOString(),
},
null,
2,
)}\n`,
);
if (process.env.GITHUB_OUTPUT)
appendFileSync(
process.env.GITHUB_OUTPUT,
`receipt_name=${ARTIFACT_NAME}\nreceipt_path=${path}\n`,
);
}
function admit() {
if (process.env.CI_FORCE_FULL === "true")
return {
run_full: true,
reason: "Forced daily runner and dependency refresh",
};
const repository = process.env.CI_REPOSITORY;
const repositoryId = Number(process.env.CI_REPOSITORY_ID);
const branch = process.env.CI_DEFAULT_BRANCH;
const head = process.env.HEAD_SHA;
if (!repository || !positiveId(repositoryId) || !branch || !commitSha(head))
return {
run_full: true,
reason: "Qualification lookup identity is incomplete",
};
const now = Date.now();
const deadline = now + 20_000;
const api = (...arguments_) => {
const remaining = deadline - Date.now();
if (remaining <= 0)
throw new Error("Qualification lookup budget exhausted");
return JSON.parse(
execFileSync("gh", ["api", "--method", "GET", ...arguments_], {
encoding: "utf8",
timeout: Math.min(15_000, remaining),
maxBuffer: 8 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
}),
);
};
try {
const { artifacts } = api(
`repos/${repository}/actions/artifacts`,
"-f",
`name=${ARTIFACT_NAME}`,
"-F",
"per_page=100",
);
if (!Array.isArray(artifacts))
throw new Error("Missing qualification artifact inventory");
const candidates = artifacts.filter((artifact) => {
const lineage = artifact?.workflow_run;
const age = now - Date.parse(artifact?.created_at);
return (
artifact?.name === ARTIFACT_NAME &&
artifact?.expired === false &&
typeof artifact?.created_at === "string" &&
Number.isFinite(age) &&
age >= 0 &&
age < MAX_AGE_MS &&
positiveId(lineage?.id) &&
String(lineage.id) !== process.env.GITHUB_RUN_ID &&
lineage?.repository_id === repositoryId &&
lineage?.head_repository_id === repositoryId &&
lineage?.head_branch === branch &&
lineage?.head_sha === head
);
});
// Artifact metadata narrows the search before run lookups. Never scan an
// unbounded history or treat a missing receipt as a successful qualification.
for (const artifact of candidates.slice(0, 3)) {
const run = api(
`repos/${repository}/actions/runs/${artifact.workflow_run.id}`,
);
// An old run can upload a new artifact when only its gate is rerun.
// Bound the original run age as well so retries cannot renew stale proof.
const runAge = Date.now() - Date.parse(run.created_at);
if (
typeof run.created_at === "string" &&
Number.isFinite(runAge) &&
runAge >= 0 &&
runAge < MAX_AGE_MS &&
run.id === artifact.workflow_run.id &&
run.status === "completed" &&
run.conclusion === "success" &&
run.path === ".github/workflows/ci.yml" &&
["schedule", "workflow_dispatch"].includes(run.event) &&
run.head_sha === head &&
run.head_branch === branch &&
run.head_repository?.full_name === repository
) {
return {
run_full: false,
run_id: run.id,
reason: `Reusing full qualification from run ${run.id}; no fresh provider observation`,
};
}
}
return {
run_full: true,
reason: "No trusted full qualification for this SHA within 24 hours",
};
} catch {
return {
run_full: true,
reason: "Qualification lookup unavailable; running full validation",
};
}
}
if (resolve(process.argv[1] ?? "") === fileURLToPath(import.meta.url)) {
const decision = admit();
if (process.env.GITHUB_OUTPUT)
appendFileSync(
process.env.GITHUB_OUTPUT,
Object.entries(decision)
.map(([key, value]) => `${key}=${value}\n`)
.join(""),
);
if (process.env.GITHUB_STEP_SUMMARY)
appendFileSync(
process.env.GITHUB_STEP_SUMMARY,
`### Scheduled qualification\n\n${decision.reason}.\n`,
);
console.log(JSON.stringify(decision));
}
+40 -6
View File
@@ -1,18 +1,46 @@
import { CI_FLAGS } from "./scopes.mjs";
import { recordQualification } from "./qualification.mjs";
const jobs = JSON.parse(process.env.CI_JOBS);
const outputs = jobs.changes.outputs;
const required = ["changes", "static", "test"];
const outputs = jobs?.changes?.outputs;
if (!["fast", "full", "reused"].includes(outputs?.tier))
throw new Error(
"CI classification requires a valid tier: fast, full, or reused",
);
const invalidFlags = CI_FLAGS.filter(
(flag) => outputs[flag] !== "true" && outputs[flag] !== "false",
);
if (invalidFlags.length)
throw new Error(
`CI classification requires explicit true/false scope outputs: ${invalidFlags.join(", ")}`,
);
const selectedScopes = CI_FLAGS.filter((flag) => outputs[flag] === "true");
const reused = outputs.tier === "reused";
if (
reused &&
(process.env.GITHUB_EVENT_NAME !== "schedule" ||
!/^[1-9]\d*$/.test(outputs.qualification_run_id ?? "") ||
!Number.isSafeInteger(Number(outputs.qualification_run_id)) ||
selectedScopes.length !== 0)
)
throw new Error(
"Reused qualification requires a scheduled admission receipt and no selected lanes",
);
if (outputs.tier === "full" && selectedScopes.length !== CI_FLAGS.length)
throw new Error("Full qualification requires every canonical CI scope");
const required = reused ? ["changes"] : ["changes", "static", "test"];
const lanes = {
code: ["build", "test-shard", "sqlite-runtime"],
code: ["build", "test-shard"],
runtime: ["windows-native-build", "package-e2e"],
package: ["windows-curated"],
sqlite: ["sqlite-runtime"],
apple: ["apple-artifacts"],
fixtures: ["conformance-fixtures"],
readiness: ["reconstruction-readiness"],
docs: ["docs"],
workflows: ["static"],
};
for (const [scope, value] of Object.entries(outputs)) {
if (value === "true") required.push(...(lanes[scope] ?? [scope]));
}
for (const scope of selectedScopes) required.push(...(lanes[scope] ?? [scope]));
const failed = [...new Set(required)].filter(
(name) => jobs[name]?.result !== "success",
);
@@ -23,3 +51,9 @@ for (const [name, job] of Object.entries(jobs)) {
if (failed.length)
throw new Error(`CI lanes did not pass: ${failed.join(", ")}`);
console.log("Every selected CI lane passed");
if (reused)
console.log(
`Qualification reused from run ${outputs.qualification_run_id}; no fresh provider observation`,
);
if (outputs.tier === "full" && process.env.CI_QUALIFICATION_DIRECTORY)
recordQualification(process.env.CI_QUALIFICATION_DIRECTORY);
+64
View File
@@ -85,6 +85,55 @@ export const SCOPES = {
"tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts",
"tests/boundary/process/sourceMapUncHosts.test.ts",
],
// Native SQLite runtime compatibility for the producer, public adapters,
// fixtures, shared lifecycle, and the package script that defines its suite.
sqlite: [
"src/sqlite/**",
"src/domain/sqlite/**",
"src/contracts/sqlite/**",
"src/application/sqlite/**",
"src/composition/sqlite.ts",
"src/cli/sqliteCommands.ts",
"src/server/registerSqliteTools.ts",
"src/application/DirectAnalysisStatus*.ts",
"src/application/CapabilityInventory*.ts",
"src/application/AnalysisProvider*.ts",
"src/contracts/toolContracts.ts",
"src/contracts/toolContractTypes.ts",
"src/contracts/toolEffects*.ts",
"src/contracts/toolOutputSchemaPrimitives.ts",
"src/domain/analysisError*.ts",
"src/domain/evidence*.ts",
"src/domain/inputIssueProjection*.ts",
"src/domain/jsonSerialization*.ts",
"src/domain/jsonValue*.ts",
"src/domain/localPath*.ts",
"src/domain/providerAdapterError*.ts",
"src/domain/providerCleanupError*.ts",
"src/domain/result*.ts",
"src/process/OwnedCommand*.ts",
"src/process/ProviderProcess*.ts",
"src/process/PrivateRuntimeRoot*.ts",
"src/artifacts/ArtifactReader*.ts",
"src/artifacts/ArtifactResourceScope*.ts",
"src/artifacts/ArtifactPaths*.ts",
"src/artifacts/readStableArtifact*.ts",
"src/filesystem/OwnedFileHandle*.ts",
"src/server/EvidenceMcpServer*.ts",
"src/server/analysisAdmission*.ts",
"src/server/contractToolHandler*.ts",
"src/server/toolRegistrationOptions*.ts",
"src/server/createServer*.ts",
"src/cli.ts",
"src/cliCommandNames.ts",
"src/cliLogging.ts",
"src/cli/commandCancellation.ts",
"tests/boundary/sqlite/**",
"tests/fixtures/sqlite/**",
"tests/acceptance/sqliteDatabaseCli.test.ts",
"tests/boundary/mcp/serverLifecycleMcp.test.ts",
".github/workflows/sqlite-runtime.yml",
],
browser: [
".github/workflows/real-browser.yml",
"scripts/verify-real-browser.mjs",
@@ -300,6 +349,7 @@ export const SCOPES = {
nativeaot: [
"bridge/nativeaot/**",
"scripts/generate-nativeaot-fixtures.mjs",
"scripts/lib/nativeaot-build.*",
"tests/fixtures/nativeaot/**",
".github/workflows/nativeaot-fixtures.yml",
],
@@ -312,6 +362,20 @@ export const SCOPES = {
],
};
export const CI_FLAGS = [
...new Set([
"code",
"runtime",
"package",
"apple",
"fixtures",
"readiness",
"docs",
"workflows",
...Object.keys(SCOPES),
]),
];
// Verifier entrypoints that only manual, licensed, or self-hosted workflows
// run. No selected CI lane executes them, so their edits need only the static
// checks and the source-test suite. Shared helpers under scripts/lib remain
+114
View File
@@ -0,0 +1,114 @@
import { execFileSync } from "node:child_process";
import { appendFileSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
const ARTIFACT_NAME = "test-timings";
const MAX_AGE_MS = 30 * 24 * 60 * 60 * 1000;
const MAX_CANDIDATES = 5;
const positiveId = (value) =>
(typeof value === "string" || typeof value === "number") &&
/^[1-9]\d*$/.test(String(value)) &&
Number.isSafeInteger(Number(value));
function findTimingRun({ repository, branch, repositoryId, now = Date.now() }) {
if (
!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository ?? "") ||
!branch ||
!positiveId(repositoryId)
)
return "";
const deadline = Date.now() + 20_000;
const api = (...arguments_) => {
const remaining = deadline - Date.now();
if (remaining <= 0)
throw new Error("Timing history lookup budget exhausted");
return JSON.parse(
execFileSync("gh", ["api", "--method", "GET", ...arguments_], {
encoding: "utf8",
timeout: Math.min(10_000, remaining),
maxBuffer: 4 * 1024 * 1024,
stdio: ["ignore", "pipe", "pipe"],
}),
);
};
const { artifacts } = api(
`repos/${repository}/actions/artifacts`,
"-f",
`name=${ARTIFACT_NAME}`,
"-F",
"per_page=100",
);
if (!Array.isArray(artifacts))
throw new Error("Missing timing artifact inventory");
const candidates = artifacts
.filter((artifact) => {
const lineage = artifact?.workflow_run;
const age = now - Date.parse(artifact?.created_at);
return (
artifact?.name === ARTIFACT_NAME &&
artifact?.expired === false &&
typeof artifact?.created_at === "string" &&
Number.isFinite(age) &&
age >= 0 &&
age < MAX_AGE_MS &&
positiveId(lineage?.id) &&
positiveId(lineage?.repository_id) &&
positiveId(lineage?.head_repository_id) &&
lineage.repository_id === lineage.head_repository_id &&
lineage.repository_id === Number(repositoryId) &&
lineage.head_branch === branch
);
})
.sort(
(left, right) =>
Date.parse(right.created_at) - Date.parse(left.created_at),
)
.slice(0, MAX_CANDIDATES);
for (const artifact of candidates) {
const run = api(
`repos/${repository}/actions/runs/${artifact.workflow_run.id}`,
);
const runAge = now - Date.parse(run?.created_at);
if (
typeof run?.created_at === "string" &&
Number.isFinite(runAge) &&
runAge >= 0 &&
runAge < MAX_AGE_MS &&
run.id === artifact.workflow_run.id &&
run.status === "completed" &&
run.conclusion === "success" &&
run.path === ".github/workflows/ci.yml" &&
["schedule", "workflow_dispatch"].includes(run.event) &&
run.head_branch === branch &&
run.head_repository?.full_name === repository
)
return String(run.id);
}
return "";
}
function write(runId) {
if (process.env.GITHUB_OUTPUT)
appendFileSync(process.env.GITHUB_OUTPUT, `run-id=${runId}\n`);
console.log(JSON.stringify({ run_id: runId }));
}
if (
process.argv[1] &&
fileURLToPath(import.meta.url) === resolve(process.argv[1])
) {
let runId = "";
try {
runId = findTimingRun({
repository: process.env.CI_REPOSITORY,
branch: process.env.CI_DEFAULT_BRANCH,
repositoryId: process.env.CI_REPOSITORY_ID,
});
} catch {
// History improves shard ordering but must never block CI.
}
write(runId);
}
+69 -25
View File
@@ -4,26 +4,33 @@ import { isAbsolute, join, relative, resolve, sep } from "node:path";
import { z } from "zod";
import { compareTestSelection } from "./test-selection.mjs";
const root = process.cwd();
const historyDirectory = join(root, ".cache/ci-test-history");
const historyPath = join(historyDirectory, "test-results.json");
const selectionDirectory = join(root, ".cache/ci-test-selection");
const selectionPath = join(selectionDirectory, "report.json");
// Never let a failed or partial rerun leave a previous successful history file.
await rm(historyPath, { force: true });
await rm(selectionPath, { force: true });
const report = z
.object({
version: z.literal(1),
success: z.boolean(),
numFailedTests: z.number().int().nonnegative(),
testResults: z.array(
reason: z.string().optional(),
unhandledErrors: z.number().int().nonnegative().optional(),
files: z.array(
z.object({
name: z.string().min(1),
startTime: z.number().finite(),
endTime: z.number().finite(),
durationMs: z.number().finite().nonnegative(),
state: z.string().optional(),
}),
),
})
.parse(JSON.parse(await readFile("test-results.json", "utf8")));
.parse(JSON.parse(await readFile("test-timings.json", "utf8")));
const files = report.testResults.map(({ name, startTime, endTime }) => {
const files = report.files.map(({ name, durationMs, state }) => {
const path = relative(root, resolve(root, name));
if (
path === "" ||
@@ -32,27 +39,47 @@ const files = report.testResults.map(({ name, startTime, endTime }) => {
isAbsolute(path)
)
throw new Error(`Test report path is outside the checkout: ${name}`);
const duration = endTime - startTime;
if (!Number.isFinite(duration) || duration < 0)
throw new Error(`Test report has an invalid duration: ${name}`);
return { name: path.split(sep).join("/"), startTime: 0, endTime: duration };
return { name: path.split(sep).join("/"), durationMs, state };
});
const complete = process.env.SHARD_RESULT === "success";
const passed =
complete &&
process.env.MERGE_RESULT === "success" &&
report.success &&
report.numFailedTests === 0;
complete && process.env.MERGE_RESULT === "success" && report.success;
// Successful shards and merged reports must account for the complete portable
// inventory. Coverage thresholds alone cannot prove that every file ran.
if (passed && process.env.CI_VERIFY_TEST_INVENTORY === "true") {
const expected = execFileSync(
"git",
["ls-files", "-z", "--", "src", "tests"],
{ encoding: "utf8", maxBuffer: 8 * 1024 * 1024 },
)
.split("\0")
.filter((path) => path.endsWith(".test.ts"));
const verifyInventory = process.env.CI_VERIFY_TEST_INVENTORY === "true";
const expected =
verifyInventory || process.env.CI_TEST_SELECTION
? execFileSync("git", ["ls-files", "-z", "--", "src", "tests"], {
encoding: "utf8",
maxBuffer: 8 * 1024 * 1024,
})
.split("\0")
.filter((path) => path.endsWith(".test.ts"))
: [];
let selection;
if (process.env.CI_TEST_SELECTION) {
const comparison = compareTestSelection(
JSON.parse(process.env.CI_TEST_SELECTION),
expected,
files,
);
selection = {
...comparison,
complete:
comparison.complete &&
report.unhandledErrors === 0 &&
["passed", "failed"].includes(report.reason) &&
["success", "failure"].includes(process.env.SHARD_RESULT) &&
["success", "failure"].includes(process.env.MERGE_RESULT),
executionSha: process.env.GITHUB_SHA ?? null,
prHeadSha: process.env.CI_PR_HEAD_SHA ?? null,
runId: process.env.GITHUB_RUN_ID ?? null,
runAttempt: process.env.GITHUB_RUN_ATTEMPT ?? null,
};
await mkdir(selectionDirectory, { recursive: true });
await writeFile(selectionPath, JSON.stringify(selection));
}
if (passed && verifyInventory) {
const actual = files.map(({ name }) => name);
const expectedSet = new Set(expected);
const actualSet = new Set(actual);
@@ -66,18 +93,31 @@ if (passed && process.env.CI_VERIFY_TEST_INVENTORY === "true") {
const summary = [
complete ? "## Merged test diagnostics" : "## Partial test diagnostics",
"",
...(selection
? [
"### Observational PR test selection",
"",
`Plan: ${selection.plan.mode}. Proposed files: ${selection.selected.length}; omitted: ${selection.omitted.length}.`,
`Full-run comparison: ${selection.complete ? "complete" : "incomplete"}; failures outside proposed selection: ${selection.missedFailures.length}.`,
"The complete suite still ran. This report cannot enable omissions or qualify a revision. Skipped, missing, and interrupted observations do not establish parity.",
"",
]
: []),
`Shard result: ${process.env.SHARD_RESULT ?? "unknown"}; merge result: ${process.env.MERGE_RESULT ?? "unknown"}.`,
passed
? "Successful timing history is available."
: "This report is diagnostic only and is not a successful timing baseline.",
"",
"Durations include tests and hooks, worker preparation, environment, imports, and setup. They are scheduling weights, not summed workflow wall time.",
"",
"| Slowest file | Duration (ms) |",
"| --- | ---: |",
...files
.toSorted((left, right) => right.endTime - left.endTime)
.toSorted((left, right) => right.durationMs - left.durationMs)
.slice(0, 20)
.map(
({ name, endTime }) => `| ${name.replaceAll("|", "\\|")} | ${endTime} |`,
({ name, durationMs }) =>
`| ${name.replaceAll("|", "\\|")} | ${durationMs} |`,
),
"",
].join("\n");
@@ -89,6 +129,10 @@ if (passed) {
await mkdir(historyDirectory, { recursive: true });
await writeFile(
historyPath,
JSON.stringify({ success: true, numFailedTests: 0, testResults: files }),
JSON.stringify({
version: 1,
success: true,
files: files.map(({ name, durationMs }) => ({ name, durationMs })),
}),
);
}
+35
View File
@@ -0,0 +1,35 @@
export interface TestSelectionPlan {
readonly version: 1;
readonly mode: "full" | "candidate";
readonly owners: readonly string[];
readonly changedTests: readonly string[];
readonly changedPaths: readonly string[];
readonly reason: string;
}
export function planTestSelection(
files: readonly string[],
event: string,
tier: string,
): TestSelectionPlan;
export function compareTestSelection(
plan: unknown,
inventory: readonly string[],
results: readonly {
readonly name: string;
readonly durationMs: number;
readonly state?: string;
}[],
): {
readonly version: 1;
readonly observational: true;
readonly plan: TestSelectionPlan;
readonly selected: readonly string[];
readonly omitted: readonly string[];
readonly missing: readonly string[];
readonly unexpected: readonly string[];
readonly duplicateFiles: number;
readonly unobserved: readonly string[];
readonly missedFailures: readonly string[];
readonly complete: boolean;
readonly omittedDurationMs: number;
};
+151
View File
@@ -0,0 +1,151 @@
import { matchesGlob } from "node:path";
// Candidate ownership only: these leaf adapters and their producer tests can be
// compared with full-suite outcomes. This table never changes test execution.
// Shared application/domain code and dynamic CLI/MCP consumers are not leaf owners.
const OWNERS = {
adb: ["src/adb/**", "tests/boundary/providers/adb/**"],
apktool: ["src/apktool/**"],
dotnet: ["src/dotnet/**"],
evm: ["src/evm/**", "tests/boundary/evm/**"],
firmware: ["src/firmware/**", "tests/boundary/firmware/**"],
flutter: ["src/flutter/**"],
ghidra: ["src/ghidra/**", "tests/boundary/providers/ghidra/**"],
hopper: ["src/hopper/**", "tests/boundary/providers/hopper/**"],
ida: ["src/ida/**", "tests/boundary/providers/ida/**"],
jeb: ["src/jeb/**"],
pcap: ["src/pcap/**", "tests/boundary/pcap/**"],
sqlite: ["src/sqlite/**", "tests/boundary/sqlite/**"],
wasm: ["src/wasm/**"],
};
const ownersOf = (path) =>
Object.entries(OWNERS)
.filter(([, patterns]) => patterns.some((glob) => matchesGlob(path, glob)))
.map(([owner]) => owner);
const validPath = (path) =>
typeof path === "string" &&
/^(src|tests)\//.test(path) &&
!/[\\\0\r\n]/.test(path) &&
!path.split("/").some((part) => ["", ".", ".."].includes(part));
const full = (reason, changedPaths = []) => ({
version: 1,
mode: "full",
owners: [],
changedTests: [],
changedPaths,
reason,
});
/** Plan an observational PR subset from the classifier's existing Git diff. */
export function planTestSelection(files, event, tier) {
if (event !== "pull_request" || tier !== "fast")
return full("Only ordinary PRs are candidates", files);
if (!files.length) return full("No changed paths available");
const owners = new Set();
const changedTests = [];
for (const path of files) {
if (!validPath(path))
return full(`Shared or unknown input: ${path}`, files);
const matches = ownersOf(path);
if (!matches.length) return full(`Shared or unknown input: ${path}`, files);
for (const owner of matches) owners.add(owner);
if (path.endsWith(".test.ts")) changedTests.push(path);
}
return {
version: 1,
mode: "candidate",
owners: [...owners].sort(),
changedTests: [...new Set(changedTests)].sort(),
changedPaths: files,
reason:
"Observe affected leaf owners; retain integration and unowned tests",
};
}
/** Compare a proposal with the full run; missing observations never prove parity. */
export function compareTestSelection(plan, inventory, results) {
if (
plan?.version !== 1 ||
!["full", "candidate"].includes(plan.mode) ||
!Array.isArray(plan.owners) ||
plan.owners.some((owner) => !Object.hasOwn(OWNERS, owner)) ||
!Array.isArray(plan.changedTests) ||
!Array.isArray(plan.changedPaths) ||
plan.changedPaths.some((path) => typeof path !== "string") ||
plan.changedTests.some(
(path) => !validPath(path) || !path.endsWith(".test.ts"),
) ||
typeof plan.reason !== "string" ||
(plan.mode === "candidate" && !plan.owners.length) ||
(plan.mode === "full" && (plan.owners.length || plan.changedTests.length))
)
throw new Error("Invalid observational test selection plan");
const expected = new Set(inventory);
if (!expected.size || expected.size !== inventory.length)
throw new Error("Test selection requires a nonempty unique inventory");
const ownersByFile = new Map(
plan.mode === "candidate"
? inventory.map((path) => [path, ownersOf(path)])
: [],
);
// Removed/renamed tests have no current executable identity. Keep all tests.
const removed = plan.changedTests.filter((path) => !expected.has(path));
const missingOwners = plan.owners.filter(
(owner) =>
![...ownersByFile.values()].some((owners) => owners.includes(owner)),
);
const effective = removed.length
? full(
`Changed tests absent from current inventory: ${removed.join(", ")}`,
plan.changedPaths,
)
: missingOwners.length
? full(
`Owners without executable tests: ${missingOwners.join(", ")}`,
plan.changedPaths,
)
: plan;
const selected = [];
const omitted = [];
for (const path of inventory.toSorted()) {
const owners = ownersByFile.get(path) ?? [];
const keep =
effective.mode === "full" ||
!owners.length ||
effective.changedTests.includes(path) ||
owners.some((owner) => effective.owners.includes(owner));
(keep ? selected : omitted).push(path);
}
const actual = new Set(results.map((file) => file.name));
const missing = inventory.filter((path) => !actual.has(path));
const unexpected = [...actual].filter((path) => !expected.has(path));
const duplicateFiles = results.length - actual.size;
const unobserved = results
.filter((file) => !["passed", "failed"].includes(file.state))
.map((file) => file.name);
const omittedSet = new Set(omitted);
const missedFailures = results
.filter((file) => omittedSet.has(file.name) && file.state === "failed")
.map((file) => file.name);
return {
version: 1,
observational: true,
plan: effective,
selected,
omitted,
missing,
unexpected,
duplicateFiles,
unobserved,
missedFailures,
complete:
!missing.length &&
!unexpected.length &&
!duplicateFiles &&
!unobserved.length,
// Phase totals are scheduling weights, not an estimate of wall-time savings.
omittedDurationMs: results
.filter((file) => omittedSet.has(file.name))
.reduce((sum, file) => sum + file.durationMs, 0),
};
}
+43
View File
@@ -0,0 +1,43 @@
import { writeFile } from "node:fs/promises";
/** @implements {import("vitest/node").Reporter} */
export default class TestTimingReporter {
/**
* @param {ReadonlyArray<import("vitest/node").TestModule>} modules
* @param {ReadonlyArray<import("vitest/node").SerializedError>} errors
* @param {import("vitest/node").TestRunEndReason} reason
*/
async onTestRunEnd(modules, errors, reason) {
const files = modules.map((module) => {
const diagnostic = module.diagnostic();
// These disjoint phases match Vitest's per-file wall-time accounting.
// Import timings already include transform waits; do not add them again.
const phases = {
execution: diagnostic.duration,
preparation: diagnostic.prepareDuration,
environment: diagnostic.environmentSetupDuration,
imports: diagnostic.collectDuration,
setup: diagnostic.setupDuration,
};
return {
name: module.moduleId,
state: module.state(),
durationMs: Object.values(phases).reduce(
(total, value) => total + value,
0,
),
phases,
};
});
await writeFile(
"test-timings.json",
JSON.stringify({
version: 1,
success: reason === "passed" && errors.length === 0,
reason,
unhandledErrors: errors.length,
files,
}),
);
}
}
+12 -17
View File
@@ -4,9 +4,14 @@ import { createHash } from "node:crypto";
import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import {
buildNativeAotFixture,
nativeAotCommandError,
supportsNativeAotFixtureHost,
} from "./lib/nativeaot-build.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
if (process.arch !== "x64" || !["linux", "win32"].includes(process.platform))
if (!supportsNativeAotFixtureHost(process.platform, process.arch))
throw new Error(
"NativeAOT fixture lane requires Linux or Windows x64; it does not cross-compile NativeAOT.",
);
@@ -40,9 +45,7 @@ const run = (command, args) => {
maxBuffer: 8 * 1024 * 1024,
});
if (result.error || result.status !== 0)
throw new Error(
`NativeAOT fixture lane requires/action failed ${command}: ${result.error?.message ?? ""}\n${result.stdout ?? ""}${result.stderr ?? ""}`,
);
throw nativeAotCommandError(command, args[0], result);
return result.stdout;
};
const dotnet = process.env.REA_NATIVEAOT_DOTNET ?? "dotnet";
@@ -57,20 +60,12 @@ if (process.platform === "linux") {
run("clang", ["--version"]);
}
const map = join(output, "NativeAotFixture.map");
run(dotnet, [
"publish",
join(root, "tests/fixtures/nativeaot/NativeAotFixture.csproj"),
"-r",
await buildNativeAotFixture(run, {
dotnet,
project: join(root, "tests/fixtures/nativeaot/NativeAotFixture.csproj"),
runtime,
"-c",
"Release",
"-o",
join(output, "symbols"),
"--maxcpucount:1",
`-p:BaseIntermediateOutputPath=${join(output, "obj")}/`,
`-p:BaseOutputPath=${join(output, "bin")}/`,
`-p:NativeAotMapPath=${map}`,
]);
output,
});
const name =
process.platform === "win32" ? "NativeAotFixture.exe" : "NativeAotFixture";
const binary = join(output, "symbols", name);
+25
View File
@@ -0,0 +1,25 @@
export function supportsNativeAotFixtureHost(
platform: string,
architecture: string,
): boolean;
export function buildNativeAotFixture(
run: (command: string, args: string[]) => unknown,
options: {
dotnet: string;
project: string;
runtime: string;
output: string;
},
): Promise<void>;
export function nativeAotCommandError(
command: string,
operation: string,
result: {
error?: { message: string };
status: number | null;
stdout?: string | null;
stderr?: string | null;
},
): Error;
+60
View File
@@ -0,0 +1,60 @@
import { join } from "node:path";
import { setTimeout as delay } from "node:timers/promises";
/** NativeAOT fixtures use native toolchains, not cross-compilation. */
export function supportsNativeAotFixtureHost(platform, architecture) {
return architecture === "x64" && ["linux", "win32"].includes(platform);
}
/** Keep network recovery separate from compilation and preserve restore failures. */
export async function buildNativeAotFixture(run, options) {
const { dotnet, project, runtime, output } = options;
const common = [
project,
"-r",
runtime,
"--maxcpucount:1",
"-p:Configuration=Release",
`-p:BaseIntermediateOutputPath=${join(output, "obj")}/`,
`-p:BaseOutputPath=${join(output, "bin")}/`,
`-p:NativeAotMapPath=${join(output, "NativeAotFixture.map")}`,
];
for (let attempt = 1; attempt <= 2; attempt++) {
console.error(`NativeAOT dependency restore: attempt ${attempt}/2`);
try {
run(dotnet, ["restore", ...common, "--verbosity", "normal"]);
break;
} catch (cause) {
if (attempt === 2)
throw new Error(
"NativeAOT dependency restore failed after two attempts",
{
cause,
},
);
// Child diagnostics can contain authenticated feed URLs; keep retry logs static.
console.error(
"NativeAOT dependency restore failed; retrying in 1 second",
);
await delay(1_000);
}
}
console.error("NativeAOT compilation (dependencies already restored)");
run(dotnet, [
"publish",
...common,
"--no-restore",
"-o",
join(output, "symbols"),
]);
}
/** Sanitize child diagnostics before any retry, cause chain, or uncaught output. */
export function nativeAotCommandError(command, operation, result) {
const diagnostic = `NativeAOT fixture lane requires/action failed ${command} ${operation}: ${result.error?.message ?? `exit ${result.status}`}\n${result.stdout ?? ""}${result.stderr ?? ""}`;
// Only URL authority userinfo is authentication; preserve host, path, query,
// and unrelated diagnostic values. Never retain the raw result as a cause.
return new Error(
diagnostic.replace(/\b(https?:\/\/)[^\\\s/?#]*@/giu, "$1[redacted]@"),
);
}
+264
View File
@@ -0,0 +1,264 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import {
mkdir,
mkdtemp,
readFile,
readdir,
rm,
writeFile,
} from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../../dist/domain/evidence.js";
import {
requireMcpOperationResult,
requireMcpToolError,
mcpTextValue,
} from "./mcp-verifier-results.mjs";
/** Exercise file coordinates through public CLI/MCP using the source-owned PE fixture. */
export async function verifyGhidraFileOffsets(
targetPath,
entrypoint,
procedure,
) {
const original = await readFile(targetPath);
const workspace = await mkdtemp(join(tmpdir(), "rea-file-offsets-"));
const runtime = join(workspace, "runtime");
await mkdir(runtime);
const env = {
...process.env,
TEMP: runtime,
TMP: runtime,
TMPDIR: runtime,
REA_ANALYSIS_PROVIDER: "ghidra",
REA_LOG_LEVEL: "silent",
};
const client = new Client({ name: "rea-file-offsets", version: "1" });
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
let connected = false;
let complete = false;
let primaryFailure;
const call = async (name, arguments_ = {}) => {
const result = await client.callTool(
{ name, arguments: arguments_ },
{ timeout: 240_000 },
);
const projected = requireMcpOperationResult(result, name);
return ["open_binary", "binary_session", "close_binary"].includes(name)
? projected
: (result.structuredContent ?? JSON.parse(mcpTextValue(result)));
};
try {
await client.connect(transport);
connected = true;
await call("open_binary", { path: targetPath, provider_id: "ghidra" });
const session = await call("binary_session");
const mapping = parseEvidence(
await call("address_to_file_offset", { address: procedure }),
);
const offset = mapping.normalized_result.file_offset;
const coordinate = `0x${offset.toString(16)}`;
const expected = parseEvidence(
await call("procedure_pseudo_code", { procedure }),
);
const explicit = parseEvidence(
await call("procedure_pseudo_code", {
procedure: `file-offset:${coordinate}`,
}),
);
const automatic = parseEvidence(
await call("procedure_pseudo_code", { procedure: coordinate }),
);
for (const actual of [explicit, automatic]) {
assert.equal(actual.normalized_result, expected.normalized_result);
assert.equal(actual.raw_result.selection.file_offset, offset);
assert.equal(actual.raw_result.selection.mapped_address, procedure);
assert.ok(
actual.locations.some(
(location) =>
location.kind === "file-offset" && location.offset === offset,
),
);
}
const invalid = await client.callTool({
name: "procedure_pseudo_code",
arguments: { procedure: "file-offset:0x20000000000000" },
});
assert.equal(requireMcpToolError(invalid).code, "invalid_request");
const unmapped = await client.callTool({
name: "procedure_pseudo_code",
arguments: { procedure: "file-offset:0x100000" },
});
assert.ok(
JSON.stringify(requireMcpToolError(unmapped)).includes(
"no loaded source mapping",
),
);
assert.equal(
(await call("binary_session")).analysis_run.run_id,
session.analysis_run.run_id,
);
const image = parseEvidence(await call("inspect_native_load_image"));
const headerMapping = image.normalized_result.observations.mappings.find(
(item) => item.initialized && item.file_offset === 0,
);
assert.ok(
headerMapping,
"Fixture header must have an observed source mapping",
);
const headerOffset = parseEvidence(
await call("address_to_file_offset", { address: headerMapping.start }),
);
assert.equal(headerOffset.normalized_result.file_offset, 0);
for (const selector of ["file-offset:0x0", "0x0"]) {
const missing = requireMcpToolError(
await client.callTool({
name: "procedure_pseudo_code",
arguments: { procedure: selector },
}),
);
assertMissingMappedProcedure(missing, headerMapping.start);
}
assert.equal(
(await call("binary_session")).analysis_run.run_id,
session.analysis_run.run_id,
);
await call("close_binary");
const cli = JSON.parse(
(
await promisify(execFile)(
process.execPath,
[
entrypoint,
"decompile",
targetPath,
`--file-offset=${coordinate}`,
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240_000, maxBuffer: 16 * 1024 * 1024 },
)
).stdout,
);
assert.equal(cli.normalized_result, explicit.normalized_result);
assert.deepEqual(cli.locations, explicit.locations);
const missingCli = await promisify(execFile)(
process.execPath,
[
entrypoint,
"decompile",
targetPath,
"--file-offset=0x0",
"--provider",
"ghidra",
"--json",
],
{ env, timeout: 240_000, maxBuffer: 16 * 1024 * 1024 },
).then(
() => {
throw new Error(
"Mapped header bytes must not decompile as a procedure",
);
},
(cause) => JSON.parse(cause.stdout),
);
assertMissingMappedProcedure(missingCli, headerMapping.start);
const alias = Buffer.from(original);
const pe = alias.readUInt32LE(0x3c);
if (alias.readUInt16LE(pe + 6) >= 2) {
const secondSection = pe + 24 + alias.readUInt16LE(pe + 20) + 40;
// Both sections now carry the same original bytes at distinct virtual addresses.
alias.writeUInt32LE(0x200, secondSection + 20);
const ambiguousPath = join(workspace, "ambiguous.exe");
await writeFile(ambiguousPath, alias);
await call("open_binary", { path: ambiguousPath, provider_id: "ghidra" });
const ambiguous = await client.callTool(
{
name: "procedure_pseudo_code",
arguments: { procedure: `file-offset:${coordinate}` },
},
{ timeout: 240_000 },
);
assert.ok(
JSON.stringify(requireMcpToolError(ambiguous)).includes(
"multiple loaded mappings",
),
);
await call("close_binary");
}
assert.deepEqual(await readFile(targetPath), original);
await client.close();
connected = false;
assert.deepEqual(await readdir(runtime), []);
complete = true;
return {
cli_mcp_parity: true,
explicit_and_numeric_offsets: true,
invalid_and_unmapped_offsets: true,
mapped_offset_without_procedure: true,
ambiguous_mappings:
alias.readUInt16LE(pe + 6) >= 2
? "verified"
: "unavailable-single-section",
source_unchanged: true,
cleanup: "complete",
};
} catch (cause) {
primaryFailure = cause;
throw cause;
} finally {
const cleanupFailures = [];
if (connected) {
try {
await call("close_binary");
} catch (cause) {
cleanupFailures.push(cause);
}
}
try {
await client.close();
} catch (cause) {
cleanupFailures.push(cause);
}
if (cleanupFailures.length > 0) {
process.stderr.write(
`Retained file-offset verifier workspace: ${workspace}\n`,
);
throw new AggregateError(
primaryFailure === undefined
? cleanupFailures
: [primaryFailure, ...cleanupFailures],
"File-offset verifier cleanup failed",
);
}
if (complete) await rm(workspace, { recursive: true, force: true });
else
process.stderr.write(
`Retained file-offset verifier workspace: ${workspace}\n`,
);
}
}
function assertMissingMappedProcedure(error, mappedAddress) {
assert.equal(error.code, "invalid_request");
const message = error.details?.issues?.[0]?.message;
assert.equal(typeof message, "string");
assert.ok(
message.includes("File offset 0x0 maps to analysis address"),
message,
);
assert.ok(message.includes(mappedAddress), message);
assert.ok(message.includes("no procedure starts at or contains"), message);
assert.ok(!message.includes("--file-offset"), message);
}
@@ -0,0 +1,313 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdir, mkdtemp, readFile, readdir, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { parseEvidence } from "../dist/domain/evidence.js";
import { requireMcpOperationResult } from "./lib/mcp-verifier-results.mjs";
import { createVerifierRun, completeVerifierRun } from "./lib/verifier-run.mjs";
if (process.argv.length !== 2 || process.platform !== "linux")
throw new Error("Run this ELF verifier on Linux without arguments.");
if (!process.env.GHIDRA_INSTALL_DIR)
throw new Error("Set GHIDRA_INSTALL_DIR to a caller-installed Ghidra.");
const exec = promisify(execFile);
const run = createVerifierRun();
const root = fileURLToPath(new URL("..", import.meta.url));
const workspace = await mkdtemp(join(tmpdir(), "rea-address-context-"));
const runtime = join(workspace, "runtime");
await mkdir(runtime);
const path = join(workspace, "pie.elf");
const entrypoint = join(root, "scripts/rea.mjs");
const env = {
...process.env,
TMPDIR: runtime,
REA_ANALYSIS_PROVIDER: "ghidra",
REA_LOG_LEVEL: "silent",
};
const transport = new StdioClientTransport({
command: process.execPath,
args: [entrypoint, "mcp"],
env,
stderr: "pipe",
});
const client = new Client({ name: "rea-real-address-context", version: "1" });
let stderr = "";
transport.stderr?.on("data", (chunk) => {
stderr = (stderr + chunk.toString()).slice(-65536);
});
let report;
let connected = false;
let primaryFailure;
let verifierRun;
try {
const compiler = process.env.CC ?? "gcc";
const version = await exec(compiler, ["--version"], { timeout: 10000 });
await exec(
compiler,
[
"-O0",
"-g",
"-fPIE",
"-pie",
"-nostdlib",
"-Wl,-e,rea_pie_entry",
"-Wl,--build-id=none",
join(root, "tests/conformance/c/pie-address-context.c"),
"-o",
path,
],
{ timeout: 30000 },
);
const bytes = await readFile(path);
assert.equal(bytes.subarray(0, 6).toString("hex"), "7f454c460201");
assert.equal(bytes.readUInt16LE(16), 3); // ET_DYN: source-owned x86-64 PIE.
assert.equal(bytes.readUInt16LE(18), 62);
const declaredEntry = `0x${bytes.readBigUInt64LE(24).toString(16)}`;
const sha256 = createHash("sha256").update(bytes).digest("hex");
await client.connect(transport);
connected = true;
await call("open_binary", { path, provider_id: "ghidra" });
const overview = parseEvidence(await call("binary_overview"));
assert.equal(overview.subject.digest.sha256, sha256);
const base = overview.normalized_result.image_base;
assert.match(base, /^0x[0-9a-f]+$/u);
assert.notEqual(base, "0x0");
const active = await call("binary_session");
const image = parseEvidence(await call("inspect_native_load_image"));
assert.equal(base, image.normalized_result.observations.image_base);
assert.ok(overview.normalized_result.segments.length > 0);
assert.ok(
overview.normalized_result.segments.every(
(segment) =>
segment.image_base === base &&
typeof segment.address_space === "string",
),
);
const procedures = parseEvidence(await call("list_procedures"));
const entry = procedures.normalized_result.find(
(item) => item.value === "rea_pie_entry",
);
const addition = procedures.normalized_result.find(
(item) => item.value === "rea_pie_add",
);
assert.ok(entry && addition);
assert.notEqual(entry.address, declaredEntry);
assert.ok(
image.normalized_result.observations.entry_points.includes(entry.address),
);
const mapping = image.normalized_result.observations.mappings.find(
(item) =>
item.source_file_index === 0 &&
item.file_offset !== null &&
item.address_space === "ram" &&
BigInt(item.start) <= BigInt(entry.address) &&
BigInt(item.end) >= BigInt(entry.address),
);
assert.ok(mapping);
const offset =
mapping.file_offset + Number(BigInt(entry.address) - BigInt(mapping.start));
const reverseMapping = parseEvidence(
await call("address_to_file_offset", { address: entry.address }),
);
assert.equal(reverseMapping.normalized_result.file_offset, offset);
const memory = parseEvidence(
await call("read_bytes", { address: entry.address, length: 8 }),
);
assert.equal(
memory.normalized_result.bytes_hex,
bytes.subarray(offset, offset + 8).toString("hex"),
);
const fromOffset = parseEvidence(
await call("procedure_pseudo_code", {
procedure: `file-offset:0x${offset.toString(16)}`,
}),
);
assert.ok(
fromOffset.locations.some(
(location) =>
location.kind === "file-offset" && location.offset === offset,
),
);
assert.equal(fromOffset.raw_result.selection.mapped_address, entry.address);
const fromNumericOffset = parseEvidence(
await call("procedure_pseudo_code", {
procedure: `0x${offset.toString(16)}`,
}),
);
assert.equal(
fromNumericOffset.normalized_result,
fromOffset.normalized_result,
);
const missingAddress = "ram:0xffffffff";
const rejected = await client.callTool(
{ name: "procedure_pseudo_code", arguments: { procedure: missingAddress } },
{ timeout: 240000 },
);
assert.equal(rejected.isError, true);
assert.ok(
JSON.stringify(rejected.content).includes(`Ghidra image base: ${base}`),
);
assert.ok(JSON.stringify(rejected.content).includes("list_procedures"));
assert.ok(
JSON.stringify(rejected.content).includes("inspect_native_load_image"),
);
const pseudo = parseEvidence(
await call("procedure_pseudo_code", { procedure: addition.address }),
);
assert.match(pseudo.normalized_result, /\+\s*(?:7|0x7)\b/u);
const retained = await call("binary_session");
assert.equal(retained.analysis_run.run_id, active.analysis_run.run_id);
await call("close_binary");
const cli = async (args) =>
exec(
process.execPath,
[entrypoint, ...args, "--provider", "ghidra", "--json"],
{ env, timeout: 240000, maxBuffer: 16 * 1024 * 1024 },
);
const cliOverview = parseEvidence(
JSON.parse((await cli(["analyze", path])).stdout),
);
assert.deepEqual(cliOverview.normalized_result, overview.normalized_result);
let cliFailure;
try {
await cli(["decompile", path, missingAddress]);
} catch (cause) {
cliFailure = cause;
}
assert.ok(cliFailure && typeof cliFailure.stdout === "string");
const error = JSON.parse(cliFailure.stdout);
assert.equal(error.code, "invalid_request");
assert.ok(JSON.stringify(error).includes(`Ghidra image base: ${base}`));
for (const command of [
"rea search <path> . --kind procedures --mode regex",
"rea analyze <path>",
"rea inspect-native-load-image <path>",
])
assert.ok(JSON.stringify(error).includes(command));
const cliProcedures = parseEvidence(
JSON.parse(
(
await cli([
"search",
path,
".",
"--kind",
"procedures",
"--mode",
"regex",
])
).stdout,
),
);
const cliEntry = cliProcedures.normalized_result.find(
(item) => item.value === "rea_pie_entry",
);
assert.ok(cliEntry);
assert.equal(cliEntry.address, entry.address);
const cliImage = parseEvidence(
JSON.parse((await cli(["inspect-native-load-image", path])).stdout),
);
assert.equal(cliImage.normalized_result.observations.image_base, base);
const cliPseudo = parseEvidence(
JSON.parse((await cli(["decompile", path, cliEntry.address])).stdout),
);
const cliOffset = parseEvidence(
JSON.parse(
(await cli(["decompile", path, `--file-offset=0x${offset.toString(16)}`]))
.stdout,
),
);
assert.equal(cliOffset.normalized_result, fromOffset.normalized_result);
assert.deepEqual(cliOffset.locations, fromOffset.locations);
assert.match(cliPseudo.normalized_result, /rea_pie_add/u);
assert.deepEqual(await readFile(path), bytes);
assert.deepEqual(await readdir(runtime), []);
report = {
ok: true,
mocked: false,
provider: image.provider,
compiler: version.stdout.split("\n")[0],
sha256,
declared_elf_entry: declaredEntry,
loaded_entry: entry.address,
observed_image_base: base,
cli_mcp_overview_parity: true,
failed_lookup_guidance: true,
cli_recovery_commands: true,
loaded_decompilation: true,
file_offset_decompilation: true,
address_file_offset_round_trip: true,
source_unchanged: true,
runtime_cleanup: "complete",
};
} catch (cause) {
primaryFailure = { cause };
if (stderr) process.stderr.write(stderr);
}
const cleanupFailures = [];
// Await provider-owned cleanup before the SDK can terminate its MCP parent.
if (connected) {
try {
await call("close_binary");
} catch (cause) {
cleanupFailures.push(cause);
}
}
try {
await client.close();
} catch (cause) {
cleanupFailures.push(cause);
}
try {
await transport.close();
} catch (cause) {
cleanupFailures.push(cause);
}
try {
assert.deepEqual(await readdir(runtime), []);
verifierRun = await completeVerifierRun(run);
assert.equal(verifierRun.process_lineage.status, "verified");
assert.deepEqual(verifierRun.process_lineage.descendants, []);
} catch (cause) {
cleanupFailures.push(cause);
}
if (cleanupFailures.length === 0) {
try {
await rm(workspace, { recursive: true, force: true });
} catch (cause) {
cleanupFailures.push(cause);
}
}
if (cleanupFailures.length > 0) {
process.stderr.write(
`Retained verifier workspace after cleanup failure: ${workspace}\n`,
);
throw new AggregateError(
primaryFailure === undefined
? cleanupFailures
: [primaryFailure.cause, ...cleanupFailures],
"Ghidra address-context verifier cleanup failed",
);
}
if (primaryFailure !== undefined) throw primaryFailure.cause;
process.stdout.write(
JSON.stringify({ verifier_run: verifierRun, ...report }) + "\n",
);
async function call(name, args = {}) {
const result = await client.callTool(
{ name, arguments: args },
{ timeout: 240000 },
);
assert.notEqual(result.isError, true, JSON.stringify(result));
return ["open_binary", "binary_session", "close_binary"].includes(name)
? requireMcpOperationResult(result, name)
: result.structuredContent;
}
+6
View File
@@ -28,6 +28,7 @@ import {
import { parseBinaryTarget } from "../dist/application/BinaryTargetResolver.js";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
import { verifyGhidraCompactReferences } from "./lib/real-ghidra-compact-references.mjs";
import { verifyGhidraFileOffsets } from "./lib/real-ghidra-file-offsets.mjs";
const verifierRun = createVerifierRun();
const SESSION_CAPABILITIES = GHIDRA_SESSION_CAPABILITIES.filter(
@@ -300,6 +301,11 @@ try {
}
if (report === undefined)
throw new Error("Windows Ghidra report was not produced");
report.file_offsets = await verifyGhidraFileOffsets(
targetPath,
resolve(root, "scripts", "rea.mjs"),
architecture === "x86" ? "0x401010" : "0x140001010",
);
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), ...report })}\n`,
);
+1 -1
View File
@@ -114,7 +114,7 @@ def shared_blocks(root, path, source):
}
from website_i18n import language_markup
selector, alternates = language_markup(root, path.relative_to(site).as_posix())
values["language_picker"] = selector if not metadata.noindex else ""
values["language_picker"] = indent(selector, " ") if not metadata.noindex else ""
values["alternates"] = alternates if not metadata.noindex else ""
templates = root / "website/templates"
return {
+15 -9
View File
@@ -21,8 +21,8 @@ import {
parseFunctionDossier,
parseListCount,
parseRelatedAddresses,
parseSegmentImageBase,
parseSegments,
parseSegmentImageBase,
} from "../domain/hopperValues.js";
import { err, ok, type Result } from "../domain/result.js";
import {
@@ -46,6 +46,7 @@ import { traceCallPath } from "./CallPathTracing.js";
import { resolveProcedureAddress } from "./ProcedureAddressResolution.js";
import { traceLiteralFeature } from "./EnhancedLiteralTracing.js";
import { projectNativeApiInspection } from "./native/NativeApiInspection.js";
/**
* Composes direct provider operations into bounded reverse-engineering tools.
* Direct callers use `execute`; adapters that own validation use
@@ -528,24 +529,29 @@ export class EnhancedTools {
const segments = parseSegments(segmentsResult.value);
if (!segments.ok) return segments;
const imageBase = parseSegmentImageBase(segmentsResult.value);
const procedureCount = parseListCount(proceduresResult.value, "procedures");
if (!procedureCount.ok) return procedureCount;
const stringCount = parseListCount(stringsResult.value, "strings");
if (!stringCount.ok) return stringCount;
const imageBase = parseSegmentImageBase(segments.value);
return ok({
document: document.value,
segments: segments.value.map(({ name, start, end }) => ({
name,
start,
end,
length: addressDistance(start, end),
})),
...(imageBase === undefined ? {} : { image_base: imageBase }),
segments: segments.value.map(
({ name, start, end, image_base, address_space }) => ({
name,
start,
end,
length: addressDistance(start, end),
...(image_base === undefined ? {} : { image_base }),
...(address_space === undefined ? {} : { address_space }),
}),
),
segment_count: segments.value.length,
procedure_count: procedureCount.value,
string_count: stringCount.value,
...(imageBase === undefined ? {} : { image_base: imageBase }),
});
}
+5 -5
View File
@@ -18,11 +18,11 @@ export const workflowAnalysisProfile = (
): AnalysisProfileCommitment =>
createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
upstream_analysis_profile: upstream,
...(operation === "binary_overview" ||
operation === "batch_decompile" ||
operation === "get_call_graph"
? { workflow_revision: 2 }
: {}),
...(operation === "binary_overview"
? { workflow_revision: 3 }
: operation === "batch_decompile" || operation === "get_call_graph"
? { workflow_revision: 2 }
: {}),
});
/** Provider identity for deterministic artifact inventories. */
+5
View File
@@ -585,6 +585,11 @@ const normalizeDestinationPath = (
"path",
`Windows destination cannot materialize artifact path as a regular file: ${path}; ':' denotes alternate data stream syntax.`,
);
if (platform === "win32" && /[\u0001-\u001f]/u.test(path))
throw new ArtifactReaderFailure(
"path",
`Windows destination cannot materialize artifact path: ${JSON.stringify(path)}; Windows names cannot contain U+0001-U+001F.`,
);
return path;
};
+21 -2
View File
@@ -255,10 +255,29 @@ const decodeZipEntryText = (
encoding: string,
type: "filename" | "comment",
): string | undefined => {
if (type !== "filename" || encoding !== "utf-8") return undefined;
return decodeStrictZipUtf8(bytes, "ZIP filename is not valid UTF-8");
if (type !== "filename") return undefined;
if (encoding === "utf-8")
return decodeStrictZipUtf8(bytes, "ZIP filename is not valid UTF-8");
return encoding === "cp437" ? decodeCp437(bytes) : undefined;
};
/** CP437 characters for bytes 0x80-0xFF. */
const CP437_HIGH_HALF =
"ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■\u00A0";
/**
* Decode a legacy ZIP name with the Unicode CP437 mapping, which Python's
* zipfile also uses: bytes below 0x80 are ASCII. zip.js shows 0x01-0x1F and
* 0x7F as IBM PC display glyphs, which turns the macOS folder icon file
* `Icon\r` into `Icon♪`.
*/
const decodeCp437 = (bytes: Uint8Array): string =>
Array.from(bytes, (byte) =>
byte < 0x80
? String.fromCharCode(byte)
: CP437_HIGH_HALF.charAt(byte - 0x80),
).join("");
const zipEntryPath = (entry: Entry): string => {
const field = entry.extraFieldUnicodePath;
if (field?.valid !== true) return entry.filename;
+10 -2
View File
@@ -134,7 +134,14 @@ export const decodeNibArchive = (
const value = values.get(index);
if (value === undefined) continue;
const key = keys[value.key_index];
if (key !== undefined) fields[key] = value.value;
if (key !== undefined)
// Archive keys are data, including the legacy __proto__ setter name.
Object.defineProperty(fields, key, {
value: value.value,
enumerable: true,
configurable: true,
writable: true,
});
}
return [{ id, class_name: className, values: fields }];
});
@@ -519,7 +526,8 @@ const decodeUtf8 = (
label: string,
): string => {
try {
return new TextDecoder("utf-8", { fatal: true }).decode(
// These are length-prefixed names, not standalone text documents.
return new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
bytes.subarray(start, end),
);
} catch (cause: unknown) {
+18 -2
View File
@@ -137,6 +137,13 @@ const registerDecompileCommand = (
.describe(
"Procedure name or address; use --procedure=<value> for leading dashes",
),
"file-offset": z
.string()
.regex(/^0x[0-9a-fA-F]+$/u)
.optional()
.describe(
"Ghidra source-file byte offset; mapped internally to an analysis address",
),
snapshot: z
.string()
.min(1)
@@ -152,8 +159,17 @@ const registerDecompileCommand = (
{
procedure: resolveCliAnalysisSelector(
args.address,
options.procedure,
"procedure",
options["file-offset"] === undefined
? options.procedure
: resolveCliAnalysisSelector(
options.procedure,
`file-offset:${options["file-offset"]}`,
"file-offset",
"procedure_pseudo_code",
),
options["file-offset"] === undefined
? "procedure"
: "file-offset",
"procedure_pseudo_code",
),
},
+1 -1
View File
@@ -181,7 +181,7 @@ export const OFFICIAL_TOOL_CONTRACTS = [
),
official(
"procedure_pseudo_code",
"Decompile one analyzed procedure by symbol name or provider-normalized address. Returns provider-specific pseudocode, never original source or cross-provider text equivalence, and may return null; request procedure_assembly when instruction precision matters. Ghidra no-return assumptions may cut the analyzed body; Evidence limitations report observed terminal calls and available callee identities.",
"Decompile one analyzed procedure by symbol name or provider-normalized address. Ghidra also accepts file-offset:0x... selectors and falls back to a unique source-file mapping when a 0x-prefixed number resolves to no procedure; existing analysis addresses take precedence and ambiguous mappings fail explicitly. Returns provider-specific pseudocode, never original source or cross-provider text equivalence, and may return null; request procedure_assembly when instruction precision matters. Ghidra no-return assumptions may cut the analyzed body; Evidence limitations report observed terminal calls and available callee identities.",
z.object({ procedure, document }),
),
official(
+3 -1
View File
@@ -357,18 +357,20 @@ export const enhancedOutputSchemas: Readonly<Record<string, z.ZodObject>> = {
binary_overview: evidenceResultOf(
z.object({
document: z.string(),
image_base: z.string().min(1).optional(),
segments: z.array(
z.object({
name: z.string(),
start: z.string(),
end: z.string(),
length: z.number().min(0),
image_base: z.string().exactOptional(),
address_space: z.string().exactOptional(),
}),
),
segment_count: z.number().int().min(0),
procedure_count: z.number().int().min(0),
string_count: z.number().int().min(0),
image_base: z.string().min(1).optional(),
}),
),
analyze_function: functionDossierOutput,
+4 -1
View File
@@ -16,6 +16,7 @@ import {
import {
DYLIB_RESOLUTION_LIMITATIONS,
deriveFindings,
type TracedRootSlice,
} from "./dylibResolutionFindings.js";
/**
@@ -568,6 +569,7 @@ export const traceDylibLoading = async (
for (const path of request.unclassified ?? [])
images.set(path, await view.image(path));
const roots: DylibTrace["roots"][number][] = [];
const rootSlices: TracedRootSlice[] = [];
const edges: Edge[] = [];
const withoutArchitecture: string[] = [];
const environmentRoots = new Set<string>();
@@ -597,6 +599,7 @@ export const traceDylibLoading = async (
)
environmentRoots.add(root);
roots.push({ image: root, architecture: slice.architecture });
rootSlices.push({ image: root, slice });
edges.push(
...(await traceProcess(
{
@@ -641,7 +644,7 @@ export const traceDylibLoading = async (
facts.status === "parsed" ? facts.slices.map(withoutDependencies) : [],
})),
edges,
findings: deriveFindings(edges, roots, images),
findings: deriveFindings(edges, rootSlices),
coverage: {
status:
unparsed.length === 0 &&
+14 -14
View File
@@ -1,14 +1,18 @@
import type {
DylibEdge as Edge,
DylibFinding as Finding,
DylibTrace,
MachoImageFacts,
MachoSlice,
} from "./dylibResolution.js";
/** One traced process root with the exact slice it was traced from. */
export interface TracedRootSlice {
readonly image: string;
readonly slice: MachoSlice;
}
export const deriveFindings = (
edges: readonly Edge[],
roots: DylibTrace["roots"],
images: ReadonlyMap<string, MachoImageFacts>,
roots: readonly TracedRootSlice[],
): Finding[] => {
const findings: Finding[] = [];
edges.forEach((edge, index) => {
@@ -32,15 +36,11 @@ export const deriveFindings = (
explanation: `The modeled dyld search checks ${earlier.join(", ")} before ${edge.resolution.image ?? edge.install_name}. A compatible Mach-O placed at an earlier modeled path could take precedence over that fallback, subject to unmodeled search inputs and code-signing library validation; library validation is not evaluated here (see inspect_signature).${edge.loader_conditional ? " This loader loads only conditionally; if it never loads, this search never occurs." : ""}${edge.resolution.status === "conditional" ? " Unmodeled environment overrides or earlier unknown candidates may take precedence; placing an image at this rpath does not establish which image dyld will load." : ""}`,
});
});
for (const { image, architecture } of roots) {
const facts = images.get(image);
const slice =
facts?.status === "parsed"
? facts.slices.find(
(candidate) => candidate.architecture === architecture,
)
: undefined;
const environment = slice?.dyld_environment ?? [];
// Slices can share an architecture name (arm64e and arm64e.x1), so each
// finding uses the slice its root was traced from, not a name lookup.
for (const { image, slice } of roots) {
const { architecture } = slice;
const environment = slice.dyld_environment;
if (environment.length > 0)
findings.push({
kind: "dyld-environment-present",
@@ -48,7 +48,7 @@ export const deriveFindings = (
image,
basis: "derived",
explanation:
slice?.file_type === "execute"
slice.file_type === "execute"
? `${image} (${architecture}) sets dyld environment variables through LC_DYLD_ENVIRONMENT (${environment.join(", ")}); any image-selection overrides they request are not modeled. Diagnostic settings alone do not alter resolution.`
: `${image} (${architecture}) contains LC_DYLD_ENVIRONMENT (${environment.join(", ")}). Dyld reads these commands only from the main executable; entries in this non-executable root are observations and do not alter resolution.`,
});
-14
View File
@@ -8,7 +8,6 @@ import {
parseNames,
parseProcedures,
parseRelatedAddresses,
parseSegmentImageBase,
parseSegments,
} from "./hopperValues.js";
@@ -66,19 +65,6 @@ describe("Hopper boundary values", () => {
ok: true,
value: ["fixture"],
});
expect(
parseSegmentImageBase([
{ image_base: "0x100000" },
{ image_base: "0x100000" },
]),
).toBe("0x100000");
expect(
parseSegmentImageBase([
{ image_base: "0x100000" },
{ image_base: "0x200000" },
]),
).toBeUndefined();
expect(parseSegmentImageBase([{ name: "__TEXT" }])).toBeUndefined();
});
it.each([
+14 -12
View File
@@ -20,6 +20,8 @@ export interface SegmentSummary {
readonly readable: boolean | null;
readonly writable: boolean | null;
readonly executable: boolean | null;
readonly image_base?: string;
readonly address_space?: string;
}
/** Return a bounded byte distance between canonical addresses in one space. */
@@ -58,10 +60,9 @@ const segmentSchema = z.object({
readable: z.boolean().nullable(),
writable: z.boolean().nullable(),
executable: z.boolean().nullable(),
image_base: z.string().min(1).exactOptional(),
address_space: z.string().min(1).exactOptional(),
});
const segmentImageBaseSchema = z
.object({ image_base: z.string().min(1) })
.passthrough();
const unavailableAnalysisFactSchema = z
.object({ available: z.literal(false), reason: z.string() })
.strict();
@@ -415,7 +416,7 @@ export const parseRelatedAddresses = (
: invalid(`${relation} list`, parsed.error);
};
/** Parse direct or wrapped Hopper segment records. */
/** Preserve reported address context in direct or wrapped provider segments. */
export const parseSegments = (
value: JsonValue,
): Result<readonly SegmentSummary[], HopperProtocolError> => {
@@ -427,14 +428,15 @@ export const parseSegments = (
: invalid("segment list", parsed.error);
};
/** Return a provider-reported image base only when every segment agrees on it. */
export const parseSegmentImageBase = (value: JsonValue): string | undefined => {
const parsed = z
.array(segmentImageBaseSchema)
.safeParse(unwrapProperty(value, "segments"));
if (!parsed.success) return undefined;
const bases = new Set(parsed.data.map(({ image_base }) => image_base));
return bases.size === 1 ? parsed.data[0]?.image_base : undefined;
/** Return a reported overview base only when every parsed segment agrees. */
export const parseSegmentImageBase = (
segments: readonly SegmentSummary[],
): string | undefined => {
const base = segments[0]?.image_base;
return base !== undefined &&
segments.every((segment) => segment.image_base === base)
? base
: undefined;
};
/** Parse direct or wrapped Hopper document names. */
@@ -0,0 +1,78 @@
import { expect, it } from "vitest";
import { analyzeJavaScriptStaticSource } from "./javascriptStaticAnalysis.js";
const registration = (runtime: string, callback: string, chunk = 1) =>
"(globalThis." +
runtime +
" = globalThis." +
runtime +
" || []).push([[" +
chunk +
"], {a: function() {}}, " +
callback +
"]);";
it.each(["webpackChunkFixture", "rspackChunkFixture"])(
"keeps differing callback facts for %s independent of source order",
(runtime) => {
const first = registration(
runtime,
'function(r) { r(7); r.e("chunk-a"); }',
);
const second = registration(
runtime,
'function(r) { r(8); r.e("chunk-b"); }',
);
for (const source of [first + "\n" + second, second + "\n" + first]) {
const result = analyzeJavaScriptStaticSource(source);
expect(result.parse_status).toBe("complete");
expect(result.bundler_registrations).toHaveLength(2);
expect(
result.bundler_registrations.flatMap((r) => r.entry_module_keys).sort(),
).toEqual(["7", "8"]);
expect(
result.bundler_registrations.flatMap((r) => r.async_chunk_keys).sort(),
).toEqual(["chunk-a", "chunk-b"]);
}
expect(
analyzeJavaScriptStaticSource(first + "\n" + first).bundler_registrations,
).toHaveLength(1);
expect(
analyzeJavaScriptStaticSource(first).bundler_registrations[0],
).toMatchObject({
entry_module_keys: ["7"],
async_chunk_keys: ["chunk-a"],
});
expect(
analyzeJavaScriptStaticSource(second).bundler_registrations[0],
).toMatchObject({
entry_module_keys: ["8"],
async_chunk_keys: ["chunk-b"],
});
},
);
it.each([
["function(r) { r(7); }", "function(r) { r(8); }"],
['function(r) { r.e("a"); }', 'function(r) { r.e("b"); }'],
["function(r) {}", "function(s) {}"],
["function(r) {}", "function(r) { r(dynamic); }"],
["function(r) {}", "function(r) { r.e(dynamic); }"],
])("retains callback metadata differences: %s / %s", (left, right) => {
const result = analyzeJavaScriptStaticSource(
registration("webpackChunkFixture", left) +
"\n" +
registration("webpackChunkFixture", right),
);
expect(result.bundler_registrations).toHaveLength(2);
});
it("retains distinct chunk identities with identical callbacks", () => {
const callback = "function(r) { r(7); }";
const result = analyzeJavaScriptStaticSource(
registration("webpackChunkFixture", callback, 1) +
"\n" +
registration("webpackChunkFixture", callback, 2),
);
expect(result.bundler_registrations).toHaveLength(2);
});
@@ -691,6 +691,14 @@ export const registrationKey = (
registration.runtime,
registration.chunk_keys,
String(registration.unknown_chunk_keys),
// Equal factory tables can be registered with different runtime callbacks.
// Both early collection and finalization must retain those recovered facts.
registration.runtime_require_name,
registration.runtime_module_cache_status,
registration.entry_module_keys,
String(registration.unknown_entry_module_keys),
registration.async_chunk_keys,
String(registration.unknown_async_chunk_keys),
registration.modules.map(({ module_key: key, source_sha256: digest }) => [
key,
digest,
@@ -265,6 +265,10 @@ type OperandResult = {
type OperandReader = (il: Buffer, offset: number) => OperandResult;
// String(-0) loses a CIL constant's sign in both anchors and normalized hashes.
const floatingPointOperand = (value: number): string =>
Object.is(value, -0) ? "-0" : String(value);
const FIXED_OPERAND_READERS = new Map<string, OperandReader>([
[
"none",
@@ -315,7 +319,7 @@ const FIXED_OPERAND_READERS = new Map<string, OperandReader>([
(il, offset) => ({
next: offset + 4,
anchorKind: "constant",
value: String(il.readFloatLE(offset)),
value: floatingPointOperand(il.readFloatLE(offset)),
}),
],
[
@@ -323,7 +327,7 @@ const FIXED_OPERAND_READERS = new Map<string, OperandReader>([
(il, offset) => ({
next: offset + 8,
anchorKind: "constant",
value: String(il.readDoubleLE(offset)),
value: floatingPointOperand(il.readDoubleLE(offset)),
}),
],
[
+4
View File
@@ -123,6 +123,8 @@ export const resolveGhidraAnalysisProfile = async (
call_relationship_coverage: "resolved-call-reference-diagnostics-v1",
string_inventory_evidence: "defined-data-coverage-v1",
location_resolution: "explicit-address-exact-entry-symbol-first-v3",
procedure_file_offset_resolution:
"address-first-unique-target-source-mapping-v2",
instruction_flow_evidence: "decoded-return-pcode-v1",
instruction_references: "exact-address-incoming-v1",
function_boundary_observations: "ghidra-terminal-call-limitations-v1",
@@ -212,6 +214,8 @@ export const resolveGhidraAnalysisProfile = async (
call_relationship_coverage: "resolved-call-reference-diagnostics-v1",
string_inventory_evidence: "defined-data-coverage-v1",
location_resolution: "explicit-address-exact-entry-symbol-first-v3",
procedure_file_offset_resolution:
"address-first-unique-target-source-mapping-v2",
instruction_flow_evidence: "decoded-return-pcode-v1",
instruction_references: "exact-address-incoming-v1",
function_boundary_observations: "ghidra-terminal-call-limitations-v1",
+12 -1
View File
@@ -1,9 +1,10 @@
import { describe, expect, it } from "vitest";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { runDoctor, type DoctorHost } from "../application/Doctor.js";
import { createDoctorHostFixture } from "../application/Doctor.fixture.js";
import { providerRegistrationEnvironment } from "../application/SetupRegistrationEnvironment.js";
import { WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON } from "../process/WindowsAuthority.js";
import * as windowsAuthority from "../process/WindowsAuthority.js";
import {
inspectGhidraInstallation,
type GhidraInstallationHost,
@@ -173,6 +174,16 @@ describe("Ghidra doctor integration", () => {
});
describe("Windows Ghidra doctor authority", () => {
beforeEach(() => {
vi.spyOn(windowsAuthority, "hasWindowsNativeAuthority").mockReturnValue(
false,
);
vi.spyOn(
windowsAuthority,
"windowsNativeAuthorityUnavailableReason",
).mockReturnValue(WINDOWS_NATIVE_AUTHORITY_UNAVAILABLE_REASON);
});
afterEach(() => vi.restoreAllMocks());
it("distinguishes a valid Windows installation from unavailable native authority", async () => {
const ghidra = inspection({
installDir: "C:\\tools\\ghidra_12.1.4_PUBLIC",
+24
View File
@@ -42,6 +42,30 @@ const parseFunctionValueFixture = (
};
describe("Ghidra function-analysis result values", () => {
it("preserves file-offset resolution in Evidence locations and rejects inexact offsets", () => {
const selection = {
file_offset: 0x210,
mapped_address: "0x140001010",
procedure_address: "0x140001010",
};
const wire = { value: "return 42;", limitations: [], selection };
expect(parseGhidraFunctionResult("procedure_assembly", wire)).toMatchObject(
{ ok: false, error: { _tag: "AnalysisOutputError" } },
);
const parsed = parseGhidraFunctionResult("procedure_pseudo_code", wire);
if (!parsed.ok) throw parsed.error;
expect(parsed.value.locations).toEqual([
{ kind: "file-offset", offset: 0x210 },
{ kind: "address", address: "0x140001010" },
{ kind: "address", address: "0x140001010" },
]);
expect(
parseGhidraFunctionResult("procedure_pseudo_code", {
...wire,
selection: { ...selection, file_offset: Number.MAX_SAFE_INTEGER + 1 },
}),
).toMatchObject({ ok: false, error: { _tag: "AnalysisOutputError" } });
});
it.each([...observationOperations])(
"requires the function-observation limitations envelope for %s",
(operation) => {
+36 -2
View File
@@ -26,6 +26,7 @@ import {
} from "../domain/jsonValue.js";
import { nativeApiBoundarySchema } from "../domain/native/nativeApiBoundary.js";
import { nativeValueFlowSchema } from "../domain/native/nativeValueFlow.js";
import type { EvidenceLocation } from "../domain/evidence.js";
import { err, ok, type Result } from "../domain/result.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import {
@@ -399,11 +400,19 @@ const functionBoundaryObservationOperations: ReadonlySet<GhidraFunctionOperation
const functionObservationEnvelope = z.strictObject({
value: z.unknown(),
limitations: z.array(z.string()),
selection: z
.strictObject({
file_offset: z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
mapped_address: ghidraCanonicalAddressSchema,
procedure_address: ghidraCanonicalAddressSchema,
})
.exactOptional(),
});
export interface ParsedGhidraFunctionResult {
readonly value: JsonValue;
readonly limitations: readonly string[];
readonly locations?: readonly EvidenceLocation[];
}
/** Require exact, bounded Java-bridge function output before creating Evidence. */
@@ -414,15 +423,22 @@ export const parseGhidraFunctionResult = (
let observation: {
readonly value: unknown;
readonly limitations: readonly string[];
readonly selection?: z.infer<
typeof functionObservationEnvelope
>["selection"];
};
if (functionBoundaryObservationOperations.has(operation)) {
const envelope = functionObservationEnvelope.safeParse(value);
if (!envelope.success)
if (
!envelope.success ||
(operation !== "procedure_pseudo_code" &&
envelope.data.selection !== undefined)
)
return err(
new AnalysisOutputError(
operation,
"Ghidra bridge omitted the function observation envelope",
{ cause: envelope.error },
envelope.success ? undefined : { cause: envelope.error },
),
);
observation = envelope.data;
@@ -434,6 +450,24 @@ export const parseGhidraFunctionResult = (
? ok({
value: jsonValueSchema.parse(parsed.data),
limitations: observation.limitations,
...(observation.selection === undefined
? {}
: {
locations: [
{
kind: "file-offset" as const,
offset: observation.selection.file_offset,
},
{
kind: "address" as const,
address: observation.selection.mapped_address,
},
{
kind: "address" as const,
address: observation.selection.procedure_address,
},
],
}),
})
: err(
new AnalysisOutputError(
+6
View File
@@ -32,6 +32,7 @@ import {
AnalysisTimeoutError,
} from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import type { EvidenceLocation } from "../domain/evidence.js";
import { jsonValueSchema, type JsonValue } from "../domain/jsonValue.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -678,10 +679,12 @@ export const createGhidraProviderClient = (input: {
normalized: JsonValue,
rawResult: JsonValue,
observationLimitations: readonly string[] = [],
locations: readonly EvidenceLocation[] = [],
) => {
const relationshipCoverage = relationshipCoverageFor(operation);
return createAnalysisExecution(normalized, committedProfile.provider, {
rawResult,
locations,
analysisProfile: committedProfile,
...(relationshipCoverage === undefined ? {} : { relationshipCoverage }),
limitations: [
@@ -776,11 +779,13 @@ export const createGhidraProviderClient = (input: {
);
let normalized: JsonValue;
let observationLimitations: readonly string[] = [];
let observationLocations: readonly EvidenceLocation[] = [];
if (isGhidraFunctionOperation(operation)) {
const result = parseGhidraFunctionResult(operation, called.value);
if (!result.ok) return result;
normalized = result.value.value;
observationLimitations = result.value.limitations;
observationLocations = result.value.locations ?? [];
} else {
const result = parseGhidraInventoryResult(operation, called.value);
if (!result.ok) return result;
@@ -875,6 +880,7 @@ export const createGhidraProviderClient = (input: {
normalized,
called.value,
observationLimitations,
observationLocations,
),
);
},
+26 -12
View File
@@ -16,14 +16,11 @@ export const parseCodeSignature = (
unsigned: boolean,
executablePaths: readonly string[] = [],
): Omit<InspectSignature, "provenance"> => {
const { identifier, fields } = splitEchoedValues(
output.replaceAll("\r\n", "\n"),
executablePaths,
);
const { identifier, fields } = splitEchoedValues(output, executablePaths);
const values = new Map<string, string>();
const authorities: string[] = [];
const cdhashes: string[] = [];
for (const line of fields.split("\n")) {
for (const line of fields.split(/\r?\n/u)) {
if (line.startsWith("CodeDirectory ")) {
values.set("CodeDirectory", line.slice("CodeDirectory ".length));
continue;
@@ -65,12 +62,20 @@ const splitEchoedValues = (
text: string,
executablePaths: readonly string[],
): { readonly identifier: string | undefined; readonly fields: string } => {
const executable = executablePaths
.map((path) => `Executable=${path}\n`)
.find((line) => text.startsWith(line));
let rest = text;
if (executable !== undefined) rest = text.slice(executable.length);
else if (text.startsWith("Executable=")) {
// Frame the exact source path before interpreting record separators. A CRLF
// inside that path or the signing identifier is payload, not a line ending.
let newline: string | undefined;
for (const path of executablePaths) {
const prefix = `Executable=${path}`;
if (!text.startsWith(prefix)) continue;
const separator = /^\r?\n/u.exec(text.slice(prefix.length))?.[0];
if (separator === undefined) continue;
newline = separator;
rest = text.slice(prefix.length + separator.length);
break;
}
if (rest === text && text.startsWith("Executable=")) {
const identifierLine = text.indexOf("\nIdentifier=");
rest = identifierLine < 0 ? text : text.slice(identifierLine + 1);
}
@@ -79,8 +84,17 @@ const splitEchoedValues = (
// Later fields come from the signature itself, so the last `Format=` line
// is the one codesign printed after the identifier.
const format = rest.lastIndexOf("\nFormat=");
const lineEnd = rest.indexOf("\n");
const end = format >= 0 ? format : lineEnd >= 0 ? lineEnd : rest.length;
// Without an exact path, use the producer-owned Format record's terminator.
// Looking inside Identifier instead would confuse literal CRLF payload with
// record framing, or strip a literal trailing CR from an LF-delimited value.
if (newline === undefined && format >= 0)
newline = /\r?\n/u.exec(rest.slice(format + 1))?.[0];
const boundary = format >= 0 ? format : rest.indexOf("\n");
const crlf = newline === undefined || newline === "\r\n";
const end =
boundary < 0
? rest.length
: boundary - (crlf && rest[boundary - 1] === "\r" ? 1 : 0);
return {
identifier: rest.slice("Identifier=".length, end),
fields: rest.slice(end),
@@ -0,0 +1,61 @@
import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
import { analyzeJavaScriptDirectory } from "../../support/cli/javascriptApplication.js";
cliTest.for(["CLI", "stdio MCP"] as const)(
"retains both repeated chunk callback observations through %s",
async (surface, { cli }) => {
const root = await createTestTempDirectory("rea-bundler-callbacks-");
const source = [
'(globalThis.webpackChunkFixture = globalThis.webpackChunkFixture || []).push([[1], {a: function() {}}, function(r) { r(7); r.e("chunk-a"); }]);',
'(globalThis.webpackChunkFixture = globalThis.webpackChunkFixture || []).push([[1], {a: function() {}}, function(r) { r(8); r.e("chunk-b"); }]);',
].join("\n");
await writeFile(join(root, "bundle.js"), source);
const { graph } = await analyzeJavaScriptDirectory(surface, cli, root);
const chunk = graph.nodes.find(({ kind }) => kind === "javascript-chunk");
for (const [index, entry, asyncChunk] of [
[0, "7", "chunk-a"],
[1, "8", "chunk-b"],
] as const) {
expect(chunk?.observations).toContainEqual(
expect.objectContaining({
properties: expect.objectContaining({
entry_module_keys: [entry],
async_chunk_keys: [asyncChunk],
}),
evidence: expect.objectContaining({
location: expect.objectContaining({
value: expect.objectContaining({
source: "bundle.js",
start: { line: index + 1, column: 0 },
}),
}),
}),
}),
);
expect(graph.edges).toContainEqual(
expect.objectContaining({
source_node_id: chunk?.node_id,
properties: expect.objectContaining({
kind: "bundler-entry-module",
module_key: entry,
resolution_status: "not-found",
}),
}),
);
expect(graph.edges).toContainEqual(
expect.objectContaining({
source_node_id: chunk?.node_id,
properties: expect.objectContaining({
kind: "bundler-async-chunk",
chunk_key: asyncChunk,
resolution_status: "not-found",
}),
}),
);
}
},
);
@@ -0,0 +1,110 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { createHash } from "node:crypto";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { expect } from "vitest";
import { encodeNibArchiveFixture } from "../../../src/artifacts/apple/NibArchive.fixture.js";
import { toolContract } from "../../../src/contracts/toolContracts.js";
import { interfaceBuilderAnalysisSchema } from "../../../src/domain/apple/interfaceBuilderGraph.js";
import { thinMach } from "../../../src/domain/binaryTarget.fixture.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { cliTest } from "../../support/cli/cliFixture.js";
cliTest(
"preserves literal NIB fields and UTF-8 names through CLI and stdio MCP",
async ({ cli }) => {
const root = await createTestTempDirectory("rea-nib-field-identity-");
const app = join(root, "Fixture.app");
const contents = join(app, "Contents");
await mkdir(join(contents, "Resources"), { recursive: true });
await mkdir(join(contents, "MacOS"));
await writeFile(
join(contents, "MacOS/App"),
thinMach(0xfeedfacf, 0x0100000c),
);
await writeFile(
join(contents, "Info.plist"),
"<plist><dict><key>CFBundleExecutable</key><string>App</string><key>CFBundleIdentifier</key><string>dev.rea.nib-fields</string></dict></plist>",
);
const archive = encodeNibArchiveFixture({
classes: ["UIView", "\uFEFFCustomView"],
objects: [
{
classIndex: 0,
values: {
["__proto__"]: { ref: 1 },
constructor: 7,
enabled: true,
"\uFEFFenabled": false,
},
},
{ classIndex: 1, values: { ["__proto__"]: 23 } },
],
});
const path = join(contents, "Resources/Panel.nib");
await writeFile(path, archive);
const result = await cli.run({
arguments: ["decode-interface-builder", app, "--json"],
environment: { REA_LOG_LEVEL: "silent", REA_ANALYSIS_PROVIDER: "auto" },
});
expect(result.exitCode, result.stdout + result.stderr).toBe(0);
const contract = toolContract("decode_interface_builder");
const evidence = contract.outputSchema.parse(result.json);
const transport = new StdioClientTransport({
command: process.execPath,
args: [resolve("scripts/rea.mjs"), "mcp"],
env: {
PATH: process.env.PATH ?? "",
REA_LOG_LEVEL: "silent",
REA_ANALYSIS_PROVIDER: "auto",
},
stderr: "pipe",
});
const client = new Client({ name: "nib-field-identity", version: "1" });
try {
await client.connect(transport);
const opened = await client.callTool({
name: "open_binary",
arguments: { path: app },
});
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
const response = await client.callTool({
name: contract.name,
arguments: {},
});
expect(response.isError, JSON.stringify(response)).not.toBe(true);
const mcp = contract.outputSchema.parse(response.structuredContent);
expect(mcp.normalized_result).toEqual(evidence.normalized_result);
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
const report = interfaceBuilderAnalysisSchema.parse(
evidence.normalized_result,
);
expect(report.documents[0]?.archive_sha256).toBe(
createHash("sha256").update(archive).digest("hex"),
);
const objects = report.graph.nodes.filter(
({ attributes }) => attributes.interface_builder_object_id !== undefined,
);
expect(objects).toHaveLength(2);
expect(objects[0]?.attributes.nibValues).toEqual({
["__proto__"]: { objectID: "1" },
constructor: 7,
enabled: true,
"\uFEFFenabled": false,
});
expect(objects[1]?.attributes).toMatchObject({
class_name: "\uFEFFCustomView",
nibValues: { ["__proto__"]: 23 },
});
expect(await readFile(path)).toEqual(archive);
},
60_000,
);
@@ -12,6 +12,38 @@ const selectors = [
] as const;
describe("CLI analysis selector boundaries", () => {
cliTest(
"accepts file offsets and rejects mixed coordinate selectors before opening a target",
async ({ cli }) => {
const selected = await cli.run({
arguments: [
"decompile",
"does-not-exist",
"--file-offset=0x210",
"--json",
],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(selected.json).toMatchObject({ code: "target_unavailable" });
const conflicting = await cli.run({
arguments: [
"decompile",
"does-not-exist",
"0x140001010",
"--file-offset=0x210",
"--json",
],
environment: { REA_LOG_LEVEL: "silent" },
});
expect(conflicting.json).toMatchObject({
code: "invalid_request",
details: {
operation: "procedure_pseudo_code",
issues: [{ path: ["file-offset"], reason: "invalid_value" }],
},
});
},
);
cliTest(
"rejects missing and conflicting selectors before opening a target",
async ({ cli }) => {
@@ -4,11 +4,13 @@ import { dirname, join } from "node:path";
import { describe, expect } from "vitest";
import {
CPU,
FILE_TYPE,
LC,
buildVersionCommand,
dylibCommand,
dyldEnvironmentCommand,
fatImage,
machoImage,
rpathCommand,
} from "../../../src/artifacts/apple/MachoImage.fixture.js";
@@ -471,6 +473,64 @@ cliTest(
},
);
cliTest(
"reports each same-named slice's own embedded dyld environment",
async ({ cli }) => {
// arm64e and arm64e.x1 slices both report the architecture name arm64e.
// A plain x86_64 slice keeps the bundle openable on x64 hosts.
for (const [settings, expected] of [
[[null, "DYLD_PRINT_RPATHS=1"], ["DYLD_PRINT_RPATHS=1"]],
[
["DYLD_PRINT_LIBRARIES=1", "DYLD_PRINT_RPATHS=1"],
["DYLD_PRINT_LIBRARIES=1", "DYLD_PRINT_RPATHS=1"],
],
] as const) {
const app = await fixtureApp();
await writeFiles(app, {
"Contents/MacOS/App": fatImage([
...[CPU.arm64e, CPU.arm64eX1].map((cpu, index) => {
const setting = settings[index];
return {
cpu,
bytes: machoImage({
cpu,
commands:
setting === null || setting === undefined
? []
: [dyldEnvironmentCommand(setting)],
}),
};
}),
{ cpu: CPU.x86_64, bytes: machoImage({ cpu: CPU.x86_64 }) },
]),
});
const result = await cli.run({
arguments: [
"trace-dylib-resolution",
app,
"--root",
"Contents/MacOS/App",
"--json",
],
environment: ENVIRONMENT,
});
expect(result.exitCode, JSON.stringify(result.json)).toBe(0);
const trace = dylibResolutionResultSchema.parse(
parseEvidence(result.json).normalized_result,
);
expect(
trace.roots.filter(({ architecture }) => architecture === "arm64e"),
).toHaveLength(2);
const environments = trace.findings
.filter(({ kind }) => kind === "dyld-environment-present")
.map(({ explanation }) =>
expected.filter((setting) => explanation.includes(setting)),
);
expect(environments).toEqual(expected.map((setting) => [setting]));
}
},
);
cliTest(
"retains definitive load findings for an explicitly selected app library root",
async ({ cli }) => {
@@ -21,14 +21,14 @@ const adHocProgram = async (directory: string, name: string) => {
describe.skipIf(process.platform !== "darwin")(
"native signature CLI with echoed path text",
() => {
cliTest(
"does not read signature fields from the file name codesign echoes",
async ({ cli }) => {
cliTest.for(["\n", "\r\n"])(
"does not read signature fields from the file name codesign echoes (%j)",
async (newline, { cli }) => {
const directory = await createTestTempDirectory("rea-signature-");
const forged = "Developer ID Application: Example (ABCDE12345)";
const program = await adHocProgram(
directory,
`tool\nAuthority=${forged}\nTeamIdentifier=ABCDE12345`,
`tool${newline}Authority=${forged}${newline}Identifier=forged${newline}TeamIdentifier=ABCDE12345`,
);
const unsignedName = await adHocProgram(
directory,
@@ -75,7 +75,7 @@ describe.skipIf(process.platform !== "darwin")(
normalized_result: {
identifier: expect.stringMatching(
new RegExp(
`^tool\\nAuthority=${forged.replace(/[()]/gu, "\\$&")}\\nTeamIdentifier=ABCDE12345-`,
`^tool${newline}Authority=${forged.replace(/[()]/gu, "\\$&")}${newline}Identifier=forged${newline}TeamIdentifier=ABCDE12345-`,
"u",
),
),
@@ -38,8 +38,19 @@ it("completes an empty ZIP extraction with an empty identity", async () => {
expect(await readdir(output)).toEqual([]);
});
const legacyControlNames = [
["legacy ASCII control", Buffer.from("Icon\r"), false, "Icon\r"],
[
"legacy CP437 control",
Buffer.from([0x82, 0x09, 0x7f, 0x2e, 0x61]),
false,
"é\t\u007F.a",
],
] as const;
it.each([
["legacy CP437", Buffer.from([0x82, 0x2e, 0x74, 0x78, 0x74]), false, "é.txt"],
...legacyControlNames,
[
"EFS with leading U+FEFF",
Buffer.from([0xef, 0xbb, 0xbf, 0x61, 0x62]),
@@ -68,6 +79,38 @@ it.each([
},
);
it.skipIf(process.platform === "win32").each(legacyControlNames)(
"extracts %s ZIP members under their stored names",
async (_label, rawName, utf8Flag, logicalPath) => {
const root = await createTestTempDirectory("rea-zip-extracted-name-");
const path = join(root, "names.zip");
await writeSingleEntryZip(path, "x.txt");
const archive = await readFile(path);
await writeFile(
path,
rewriteSingleEntryName(archive, "x.txt", rawName, utf8Flag),
);
const target = await parseBinaryTarget(path);
if (!target.ok) throw target.error;
const output = join(root, "output");
const result = await withArtifactClient(target.value, (client) =>
client.execute("extract_artifact", { output_root: output }),
);
if (!result.ok) throw result.error;
const extraction = artifactExtractionResultSchema.parse(
result.value.result,
);
expect(
extraction.artifacts.map(({ relative_path }) => relative_path),
).toEqual([logicalPath]);
expect(await readdir(output)).toEqual([logicalPath]);
expect(await readFile(join(output, logicalPath), "utf8")).toBe(
"member bytes",
);
},
);
it("preserves valid Info-ZIP Unicode Path overrides", async () => {
const root = await createTestTempDirectory("rea-zip-unicode-path-");
const path = join(root, "unicode-path.zip");
+187 -19
View File
@@ -8,6 +8,7 @@ import {
writeFile,
} from "node:fs/promises";
import { dirname, join, matchesGlob } from "node:path";
import { fileURLToPath } from "node:url";
import { afterAll, beforeAll, expect, it } from "vitest";
import { parse } from "yaml";
@@ -79,12 +80,13 @@ async function classify(
head: string,
event = "pull_request",
full = false,
skipFull = false,
) {
const output = join(directory, "scope-output");
await writeFile(output, "");
const result = await execFileAsync(
process.execPath,
[new URL("../../../scripts/ci/plan.mjs", import.meta.url).pathname],
[fileURLToPath(new URL("../../../scripts/ci/plan.mjs", import.meta.url))],
{
cwd: directory,
env: {
@@ -93,6 +95,7 @@ async function classify(
HEAD_SHA: head,
GITHUB_EVENT_NAME: event,
CI_FULL: String(full),
CI_SKIP_FULL: String(skipFull),
GITHUB_OUTPUT: output,
},
},
@@ -100,6 +103,21 @@ async function classify(
return z.record(z.string(), z.unknown()).parse(JSON.parse(result.stdout));
}
it("reuses only scheduled qualification and preserves every explicit full request", async () => {
const { directory, base } = await fixture();
const reused = await classify(directory, base, base, "schedule", false, true);
expect(reused.tier).toBe("reused");
const { CI_FLAGS } = await loadScopes();
for (const flag of CI_FLAGS) expect(reused[flag]).toBe(false);
for (const event of ["workflow_dispatch", "pull_request", "schedule"])
expect(
(await classify(directory, base, base, event, true, true)).tier,
).toBe("full");
expect(
(await classify(directory, base, base, "push", false, true)).tier,
).toBe("fast");
});
it("keeps every translated root README and authored guide on the documentation lane", async () => {
const { directory, base } = await fixture();
const readmes = (await readdir(new URL("../../../", import.meta.url))).filter(
@@ -119,6 +137,64 @@ it("keeps every translated root README and authored guide on the documentation l
});
});
it.each([
"job-timings.mjs",
"job-timings-batch.mjs",
"test-report.mjs",
"test-timing-reporter.mjs",
"test-history.mjs",
])(
"keeps portable CI reporter %s on static checks and portable tests",
async (name) => {
const { directory, base } = await fixture();
const head = await change(directory, [`scripts/ci/${name}`]);
const plan = await classify(directory, base, head);
const { CI_FLAGS } = await loadScopes();
expect(plan.code).toBe(true);
for (const flag of CI_FLAGS)
if (flag !== "code") expect(plan[flag]).toBe(false);
},
);
it("preserves a provider lane when a portable CI reporter changes with provider source", async () => {
const { directory, base } = await fixture();
const head = await change(directory, [
"scripts/ci/test-report.mjs",
"src/evm/Example.ts",
]);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
evm: true,
browser: false,
package: false,
});
});
it("keeps unknown CI scripts on complete qualification", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["scripts/ci/new-qualification.mjs"]);
const plan = await classify(directory, base, head);
const { CI_FLAGS } = await loadScopes();
for (const flag of CI_FLAGS) expect(plan[flag]).toBe(true);
});
it("emits an observational owner proposal without changing ordinary or full portable execution", async () => {
const { directory, base } = await fixture();
const head = await change(directory, ["src/evm/Example.ts"]);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
testSelection: { mode: "candidate", owners: ["evm"] },
});
expect(
await classify(directory, base, head, "pull_request", true),
).toMatchObject({
code: true,
testSelection: { mode: "full", owners: [] },
});
const output = await readFile(join(directory, "scope-output"), "utf8");
expect(output).toContain('test_selection={"version":1,"mode":"full"');
});
it("does not charge a documentation PR for unrelated implementation updates on main", async () => {
const { directory, base } = await fixture();
await git(directory, ["switch", "-c", "docs/change"]);
@@ -205,18 +281,8 @@ it.each(["", "not-a-commit"])(
browser: true,
reasons: expect.arrayContaining([expect.stringContaining("baseline")]),
});
const { SCOPES } = await loadScopes();
for (const flag of [
"code",
"runtime",
"package",
"fixtures",
"readiness",
"docs",
"workflows",
...Object.keys(SCOPES),
])
expect(plan[flag]).toBe(true);
const { CI_FLAGS } = await loadScopes();
for (const flag of CI_FLAGS) expect(plan[flag]).toBe(true);
},
);
@@ -374,7 +440,14 @@ it("retains native Inspector verification on every documented package host", asy
it.each([
[
"src/evm/client.ts",
{ code: true, runtime: true, evm: true, browser: false, package: false },
{
code: true,
runtime: true,
evm: true,
browser: false,
package: false,
sqlite: false,
},
],
[
"src/browser/execution/evaluate.ts",
@@ -471,6 +544,38 @@ it.each([
"src/process/ProcessOwnership.test.ts",
{ code: true, package: true, apple: false, evm: false, browser: false },
],
[
"src/sqlite/SqliteDatabaseProvider.ts",
{ code: true, sqlite: true, runtime: true, package: false },
],
[
"src/application/sqlite/SqliteDatabaseService.test.ts",
{ code: true, sqlite: true, runtime: false },
],
[
"tests/boundary/sqlite/sqliteDatabaseProvider.test.ts",
{ code: true, sqlite: true, runtime: false },
],
[
"tests/fixtures/sqlite/database.ts",
{ code: true, sqlite: true, runtime: false },
],
["tests/acceptance/sqliteDatabaseCli.test.ts", { code: true, sqlite: true }],
[
"tests/boundary/mcp/serverLifecycleMcp.test.ts",
{ code: true, sqlite: true },
],
["src/process/OwnedCommand.ts", { code: true, sqlite: true, runtime: true }],
["src/server/createServer.ts", { code: true, sqlite: true, runtime: true }],
[
"src/contracts/toolContracts.ts",
{ code: true, sqlite: true, runtime: true },
],
[
"src/domain/analysisErrorProjection.ts",
{ code: true, sqlite: true, runtime: true },
],
["src/domain/javascript/example.ts", { code: true, sqlite: false }],
])("selects the appropriate lanes for %s", async (path, expected) => {
const { directory, base } = await fixture();
const head = await change(directory, [path]);
@@ -513,6 +618,52 @@ it("scopes package script edits while retaining broad dependency checks", async
});
});
it.each(["test:sqlite", "test:sqlite:run"])(
"routes %s script changes to the SQLite runtime matrix",
async (script) => {
const { directory } = await fixture();
const base = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { [script]: "vitest run" } }),
);
const head = await change(
directory,
["package.json"],
JSON.stringify({
scripts: { [script]: "vitest run --reporter=default" },
}),
);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
sqlite: true,
evm: false,
package: false,
});
},
);
it("keeps unrelated test script changes off the SQLite runtime matrix", async () => {
const { directory } = await fixture();
const base = await change(
directory,
["package.json"],
JSON.stringify({ scripts: { "test:domain": "vitest run src/domain" } }),
);
const head = await change(
directory,
["package.json"],
JSON.stringify({
scripts: { "test:domain": "vitest run src/domain --reporter=default" },
}),
);
expect(await classify(directory, base, head)).toMatchObject({
code: true,
sqlite: false,
evm: false,
});
});
it.each([
["browser", "real-browser"],
["binary_layout", "real-binary-layout"],
@@ -529,7 +680,9 @@ it.each([
async (scope, name) => {
const workflow = z
.object({
on: z.object({ workflow_call: z.null() }).passthrough(),
on: z
.object({ workflow_call: z.object({}).passthrough().nullable() })
.passthrough(),
})
.parse(
parse(
@@ -566,10 +719,19 @@ it.each([
);
it("rejects skipped selected work while allowing unselected lanes to skip", async () => {
const command = new URL("../../../scripts/ci/required.mjs", import.meta.url)
.pathname;
const command = fileURLToPath(
new URL("../../../scripts/ci/required.mjs", import.meta.url),
);
const { CI_FLAGS } = await loadScopes();
const jobs = {
changes: { result: "success", outputs: { evm: "true" } },
changes: {
result: "success",
outputs: {
...Object.fromEntries(CI_FLAGS.map((flag) => [flag, "false"])),
tier: "fast",
evm: "true",
},
},
static: { result: "success" },
test: { result: "success" },
evm: { result: "success" },
@@ -605,7 +767,10 @@ it.each([
"src/process/capture/example.test.ts",
{ code: true, runtime: false, browser: false, package: false },
],
[".nvmrc", { code: true, package: true, browser: true, evm: true }],
[
".nvmrc",
{ code: true, package: true, browser: true, evm: true, sqlite: true },
],
[
"tsconfig.build.json",
{ code: true, package: true, browser: true, evm: true },
@@ -745,6 +910,7 @@ it.each(["workflow_dispatch", "schedule"])(
tier: "full",
code: true,
package: true,
sqlite: true,
evm: true,
browser: true,
});
@@ -762,6 +928,7 @@ it("honors ci:full for successive PR revisions", async () => {
tier: "full",
code: true,
package: true,
sqlite: true,
evm: true,
browser: true,
});
@@ -829,6 +996,7 @@ const workflowFile = async (name: string): Promise<unknown> =>
);
const scopesSchema = z.object({
CI_FLAGS: z.array(z.string()),
SCOPES: z.record(z.string(), z.array(z.string())),
MANUAL_VERIFIERS: z.array(z.string()),
});
@@ -0,0 +1,243 @@
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { delimiter, join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
executeWorkflowFixture,
workflowGit,
} from "../../support/workflowGit.js";
const reporter = fileURLToPath(
new URL("../../../scripts/ci/job-timings.mjs", import.meta.url),
);
it("merges paginated jobs, reports elapsed metrics, and preserves raw responses", async () => {
const fixture = await createFixture([
{
total_count: 4,
jobs: [
{
name: "Build package",
workflow_name: "CI",
status: "completed",
conclusion: "success",
created_at: "2026-10-13T00:00:00Z",
started_at: "2026-10-13T00:00:10Z",
completed_at: "2026-10-13T00:01:10Z",
steps: [
{
name: "Install dependencies (npm ci)",
started_at: "2026-10-13T00:00:10Z",
completed_at: "2026-10-13T00:00:40Z",
},
{
name: "Upload artifact",
started_at: "2026-10-13T00:00:40Z",
completed_at: "2026-10-13T00:00:41Z",
},
],
},
],
},
{
total_count: 4,
jobs: [
{
name: "Queued lane",
status: "queued",
created_at: "2026-10-13T00:00:00Z",
started_at: "2026-10-13T00:00:00Z",
completed_at: null,
steps: [],
},
{
name: "Running lane",
status: "in_progress",
created_at: "2026-10-13T00:00:00Z",
started_at: "2026-10-13T00:00:15Z",
completed_at: null,
steps: [
{
name: "Download artifacts",
started_at: "2026-10-13T00:00:20Z",
completed_at: "2026-10-13T00:00:22Z",
},
],
},
{
name: "Skipped lane",
status: "completed",
conclusion: "skipped",
created_at: "2026-10-13T00:00:00Z",
started_at: "2026-10-13T00:00:00Z",
completed_at: "2026-10-13T00:00:00Z",
steps: [],
},
],
},
]);
const result = await runReporter(fixture);
const report = JSON.parse(
await readFile(join(fixture.outputDirectory, "report.json"), "utf8"),
);
expect(report.run.snapshot).toContain("partial");
expect(report.counts).toMatchObject({
jobs: 4,
completed: 2,
skipped: 1,
queueUnknown: 2,
executionUnknown: 3,
completedExecutionUnknown: 0,
});
expect(report.aggregates.queueSeconds).toMatchObject({
count: 2,
p50: 10,
p95: 15,
});
expect(report.aggregates.completedExecutionSeconds).toMatchObject({
count: 1,
p50: 60,
p95: 60,
});
expect(report.aggregates.summedCompletedJobElapsedSeconds).toMatchObject({
value: 60,
knownCount: 1,
unknownCount: 0,
label: expect.stringContaining("not CPU time"),
});
expect(report.jobs[1].queue).toEqual({
seconds: null,
reason: "job start is not established by its status",
});
expect(report.jobs[3].queue).toEqual({
seconds: null,
reason: "job was skipped",
});
expect(report.jobs[3].execution).toEqual({
seconds: null,
reason: "job was skipped",
});
expect(report.stepCosts.npmCi[0].duration.seconds).toBe(30);
expect(report.stepCosts.artifactUpload[0].duration.seconds).toBe(1);
expect(report.stepCosts.artifactDownload[0].duration.seconds).toBe(2);
expect(report.stepCostInterpretation).toContain(
"not total artifact transfer costs",
);
expect(report.cpuSeconds).toBeUndefined();
expect(result.stdout).toContain("4 job timing records");
expect(
await readFile(join(fixture.outputDirectory, "jobs.json"), "utf8"),
).toBe(fixture.raw);
const summary = await readFile(fixture.summary, "utf8");
expect(summary).toContain("point-in-time snapshot");
expect(summary).toContain("unknown");
expect(summary).toContain("not CPU time");
expect(summary).toContain("60 s");
expect(summary).toContain("not total artifact transfer costs");
const args = JSON.parse(await readFile(fixture.args, "utf8"));
expect(args).toEqual([
"api",
"--paginate",
"--slurp",
"repos/example/rea/actions/runs/42/attempts/2/jobs?per_page=100",
]);
});
it("marks invalid and out-of-order timestamps unknown instead of emitting negative durations", async () => {
const fixture = await createFixture([
{
jobs: [
{
name: "Invalid dates",
status: "completed",
created_at: "not-a-date",
started_at: "2026-10-13T00:00:10Z",
completed_at: "2026-10-13T00:00:05Z",
steps: [],
},
],
},
]);
await runReporter(fixture);
const report = JSON.parse(
await readFile(join(fixture.outputDirectory, "report.json"), "utf8"),
);
expect(report.jobs[0].queue).toEqual({
seconds: null,
reason: "invalid timestamp",
});
expect(report.jobs[0].execution).toEqual({
seconds: null,
reason: "timestamps out of order",
});
expect(report.counts.completedExecutionUnknown).toBe(1);
expect(report.aggregates.summedCompletedJobElapsedSeconds).toMatchObject({
value: null,
unknownCount: 1,
});
});
it("fails on malformed pages or a failed GitHub API request", async () => {
const malformed = await createFixture([{ jobs: "not-an-array" }]);
await expect(runReporter(malformed)).rejects.toThrow();
await expect(
readFile(join(malformed.outputDirectory, "jobs.json"), "utf8"),
).resolves.toBe(malformed.raw);
const failedApi = await createFixture([], { exitCode: 1 });
await expect(runReporter(failedApi)).rejects.toThrow();
});
async function createFixture(
pages: unknown[],
{ exitCode = 0 }: { exitCode?: number } = {},
) {
const directory = await createTestTempDirectory("rea-ci-job-timings-");
await workflowGit(directory, ["init", "--initial-branch=main"]);
const fakeBin = join(directory, "bin");
await mkdir(fakeBin);
const raw = JSON.stringify(pages);
const rawPath = join(directory, "gh-response.json");
const args = join(directory, "gh-args.json");
await writeFile(rawPath, raw);
const fakeGh = join(fakeBin, "gh");
await writeFile(
fakeGh,
`#!/usr/bin/env node\nconst fs = require("node:fs");\nfs.writeFileSync(process.env.CI_GH_ARGS, JSON.stringify(process.argv.slice(2)));\nprocess.stdout.write(fs.readFileSync(process.env.CI_GH_RESPONSE, "utf8"));\nprocess.exitCode = ${String(exitCode)};\n`,
);
await chmod(fakeGh, 0o755);
const outputDirectory = join(directory, ".cache", "ci-job-timings");
const summary = join(directory, "summary.md");
await writeFile(summary, "");
return {
directory,
fakeBin,
raw,
rawPath,
args,
outputDirectory,
summary,
};
}
function runReporter(fixture: Awaited<ReturnType<typeof createFixture>>) {
return executeWorkflowFixture(process.execPath, [reporter], {
cwd: fixture.directory,
env: {
...process.env,
PATH: `${fixture.fakeBin}${delimiter}${process.env.PATH ?? ""}`,
CI_REPOSITORY: "example/rea",
CI_JOB_TIMINGS_DIRECTORY: fixture.outputDirectory,
CI_GH_ARGS: fixture.args,
CI_GH_RESPONSE: fixture.rawPath,
CI_TIMING_RUN_ID: "42",
CI_TIMING_RUN_ATTEMPT: "2",
GITHUB_STEP_SUMMARY: fixture.summary,
},
});
}
@@ -0,0 +1,288 @@
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { delimiter, join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { executeWorkflowFixture } from "../../support/workflowGit.js";
const batchScript = fileURLToPath(
new URL("../../../scripts/ci/job-timings-batch.mjs", import.meta.url),
);
it("collects the latest attempt for recent completed CI and Release runs into a bounded manifest", async () => {
const recent = new Date().toISOString();
const fixture = await createFixture([
{
workflow_runs: [
{
id: 42,
run_attempt: 3,
status: "completed",
conclusion: "failure",
created_at: new Date(Date.parse(recent) + 1_000).toISOString(),
},
{
id: 41,
run_attempt: 1,
status: "completed",
conclusion: "cancelled",
created_at: recent,
},
],
},
{
workflow_runs: [
{
id: 43,
run_attempt: 2,
status: "completed",
conclusion: "success",
created_at: new Date(Date.parse(recent) + 2_000).toISOString(),
},
],
},
[
{
jobs: [
{
name: "Release lane",
status: "completed",
conclusion: "success",
created_at: recent,
started_at: recent,
completed_at: new Date(Date.parse(recent) + 50_000).toISOString(),
steps: [],
},
],
},
],
[
{
jobs: [
{
name: "Failed lane",
status: "completed",
conclusion: "failure",
created_at: recent,
started_at: recent,
completed_at: new Date(Date.parse(recent) + 55_000).toISOString(),
steps: [],
},
],
},
],
[
{
jobs: [
{
name: "Cancelled lane",
status: "completed",
conclusion: "cancelled",
created_at: recent,
started_at: recent,
completed_at: new Date(Date.parse(recent) + 30_000).toISOString(),
steps: [],
},
],
},
],
]);
const result = await runBatch(fixture);
const manifest = JSON.parse(
await readFile(
join(fixture.outputDirectory, "batch-manifest.json"),
"utf8",
),
);
const calls = JSON.parse(await readFile(fixture.calls, "utf8"));
expect(result.stdout).toContain("3 collected runs");
expect(
await readFile(join(fixture.directory, "summary.md"), "utf8"),
).toContain("collected 3 of 3");
expect(manifest.selection).toMatchObject({
lookbackHours: 24,
maxRunsPerWorkflow: 100,
exhaustive: false,
});
expect(manifest.coverage).toMatchObject({
workflowsQueried: ["CI", "Release"],
selectedRuns: 3,
collectedRuns: 3,
});
expect(manifest.truncation).toEqual([]);
expect(manifest.errors).toEqual([]);
expect(manifest.runs).toMatchObject([
{ id: 43, runAttempt: 2, conclusion: "success" },
{ id: 42, runAttempt: 3, conclusion: "failure" },
{ id: 41, runAttempt: 1, conclusion: "cancelled" },
]);
expect(manifest.runs).toEqual(
expect.arrayContaining([
expect.objectContaining({
status: "collected",
rawPath: expect.any(String),
reportPath: expect.any(String),
}),
]),
);
expect(calls).toContainEqual([
"api",
"--paginate",
"--slurp",
"repos/example/rea/actions/runs/42/attempts/3/jobs?per_page=100",
]);
expect(calls).toContainEqual([
"api",
"--paginate",
"--slurp",
"repos/example/rea/actions/runs/41/attempts/1/jobs?per_page=100",
]);
const failedReport = JSON.parse(
await readFile(
join(fixture.outputDirectory, "runs/42-3/report.json"),
"utf8",
),
);
expect(failedReport.jobs[0].conclusion).toBe("failure");
expect(failedReport.jobs[0].execution.seconds).toBe(55);
});
it("records listing and per-run errors without claiming complete coverage", async () => {
const recent = new Date().toISOString();
const fixture = await createFixture([
{
workflow_runs: [
{
id: 7,
run_attempt: 2,
status: "completed",
conclusion: "success",
created_at: recent,
},
],
},
{ error: true },
{ jobs: "malformed" },
]);
await runBatch(fixture);
const manifest = JSON.parse(
await readFile(
join(fixture.outputDirectory, "batch-manifest.json"),
"utf8",
),
);
expect(manifest.selection.exhaustive).toBe(false);
expect(manifest.errors).toMatchObject([
{ scope: "workflow:Release" },
{ scope: "run" },
]);
expect(manifest.runs[0]).toMatchObject({
id: 7,
runAttempt: 2,
status: "error",
});
});
it("retains a partial manifest when its collection budget is exhausted", async () => {
const fixture = await createFixture([
{
workflow_runs: [8, 9].map((id) => ({
id,
run_attempt: 1,
status: "completed",
created_at: new Date().toISOString(),
})),
},
{ workflow_runs: [] },
]);
await executeWorkflowFixture(
process.execPath,
[
"--input-type=module",
"-e",
`
const { collectJobTimingsBatch } = await import(${JSON.stringify(pathToFileURL(batchScript).href)});
let clock = Date.now();
await collectJobTimingsBatch({
repository: "example/rea",
outputDirectory: process.env.CI_JOB_TIMINGS_DIRECTORY,
now: () => clock,
collect: async () => {
clock += 240001;
throw new Error("fixture request exhausted the remaining budget");
},
});
`,
],
{ cwd: fixture.directory, env: fixtureEnvironment(fixture) },
);
const manifest = JSON.parse(
await readFile(
join(fixture.outputDirectory, "batch-manifest.json"),
"utf8",
),
);
expect(manifest.coverage).toMatchObject({
selectedRuns: 2,
collectedRuns: 0,
});
expect(manifest.runs).toHaveLength(1);
expect(manifest.runs[0]).toMatchObject({ status: "error" });
expect(manifest.truncation.join(" ")).toContain("Time budget");
});
async function createFixture(responses: unknown[]) {
const directory = await createTestTempDirectory("rea-ci-job-timings-batch-");
const fakeBin = join(directory, "bin");
await mkdir(fakeBin);
const responsePath = join(directory, "responses.json");
const calls = join(directory, "calls.json");
await writeFile(responsePath, JSON.stringify(responses));
await writeFile(calls, "[]");
const fakeGh = join(fakeBin, "gh");
await writeFile(
fakeGh,
`#!/usr/bin/env node
const fs = require("node:fs");
const calls = JSON.parse(fs.readFileSync(process.env.CI_GH_CALLS, "utf8"));
calls.push(process.argv.slice(2));
fs.writeFileSync(process.env.CI_GH_CALLS, JSON.stringify(calls));
const responses = JSON.parse(fs.readFileSync(process.env.CI_GH_RESPONSES, "utf8"));
const response = responses[calls.length - 1];
if (response && response.error) { process.stderr.write("fixture API error"); process.exitCode = 1; }
else process.stdout.write(JSON.stringify(response));
`,
);
await chmod(fakeGh, 0o755);
return {
directory,
fakeBin,
responses: responsePath,
calls,
outputDirectory: join(directory, ".cache", "ci-job-timings"),
};
}
function runBatch(fixture: Awaited<ReturnType<typeof createFixture>>) {
return executeWorkflowFixture(process.execPath, [batchScript], {
cwd: fixture.directory,
env: fixtureEnvironment(fixture),
});
}
function fixtureEnvironment(
fixture: Awaited<ReturnType<typeof createFixture>>,
) {
return {
...process.env,
PATH: `${fixture.fakeBin}${delimiter}${process.env.PATH ?? ""}`,
CI_REPOSITORY: "example/rea",
CI_JOB_TIMINGS_DIRECTORY: fixture.outputDirectory,
CI_GH_CALLS: fixture.calls,
CI_GH_RESPONSES: fixture.responses,
GITHUB_STEP_SUMMARY: join(fixture.directory, "summary.md"),
};
}
@@ -0,0 +1,98 @@
import { readFile } from "node:fs/promises";
import { expect, it } from "vitest";
import { parse } from "yaml";
import { z } from "zod";
import { executeWorkflowFixture } from "../../support/workflowGit.js";
async function workflow(name: string) {
return z
.object({
on: z.record(z.string(), z.unknown()),
permissions: z.record(z.string(), z.string()),
concurrency: z.object({ group: z.string() }).optional(),
jobs: z.record(
z.string(),
z.object({
name: z.string().optional(),
if: z.string().optional(),
uses: z.string().optional(),
with: z.record(z.string(), z.string()).optional(),
steps: z
.array(
z.object({
run: z.string().optional(),
uses: z.string().optional(),
env: z.record(z.string(), z.string()).optional(),
}),
)
.optional(),
}),
),
})
.parse(
parse(
await readFile(
new URL(`../../../.github/workflows/${name}`, import.meta.url),
"utf8",
),
),
);
}
it("refreshes title validation on edits without checking out PR code or cancelling CI", async () => {
const title = await workflow("pr-title.yml");
expect(title.on.pull_request).toMatchObject({
types: ["opened", "synchronize", "reopened", "edited"],
});
expect(title.permissions).toEqual({});
expect(title.concurrency?.group).not.toBe(
(await workflow("ci.yml")).concurrency?.group,
);
const jobs = Object.values(title.jobs);
expect(jobs).toHaveLength(1);
expect(jobs[0]?.name).toBe("Conventional PR title");
expect(jobs[0]?.if).toBeUndefined(); // A skipped check must not green-light an invalid title on body edits.
const steps = jobs[0]?.steps ?? [];
expect(steps).toHaveLength(1);
expect(steps[0]?.uses).toBeUndefined();
expect(steps[0]?.env).toEqual({
PR_TITLE: "${{ github.event.pull_request.title }}",
});
const command = z.string().parse(steps[0]?.run);
for (const title of [
"fix(ci): repair workflow",
"feat!: changed contract",
"docs: literal $(exit 91) `exit 92` text",
]) {
await expect(
executeWorkflowFixture("bash", ["-c", command], {
env: { ...process.env, PR_TITLE: title },
}),
).resolves.toMatchObject({ stderr: "" });
}
for (const title of ["", "Fix CI", "feat: "]) {
await expect(
executeWorkflowFixture("bash", ["-c", command], {
env: { ...process.env, PR_TITLE: title },
}),
).rejects.toMatchObject({ code: 1 });
}
expect((await workflow("ci.yml")).jobs["pr-title"]).toBeUndefined();
});
it("qualifies only ci:full label additions at the caller's merge revision", async () => {
const full = await workflow("ci-full-pr.yml");
expect(full.on.pull_request).toMatchObject({ types: ["labeled"] });
expect(full.permissions).toEqual({ contents: "read", actions: "read" });
expect(Object.values(full.jobs)).toEqual([
{
name: "Qualify labeled PR revision",
if: "github.event.label.name == 'ci:full'",
uses: "./.github/workflows/ci.yml",
with: { source_sha: "${{ github.sha }}" },
},
]);
// Metadata events must not create skipped replacements for the main CI checks.
expect(full.jobs.required).toBeUndefined();
expect(full.concurrency).toBeUndefined();
});
@@ -0,0 +1,375 @@
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { parse } from "yaml";
import { z } from "zod";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import {
executeWorkflowFixture,
workflowGit,
} from "../../support/workflowGit.js";
const qualificationScript = fileURLToPath(
new URL("../../../scripts/ci/qualification.mjs", import.meta.url),
);
const requiredScript = fileURLToPath(
new URL("../../../scripts/ci/required.mjs", import.meta.url),
);
const now = Date.now();
const validSha = "a".repeat(40);
const repository = "example/rea";
function artifact(overrides: Record<string, unknown> = {}) {
return {
id: 42,
name: "ci-full-qualification-v1",
expired: false,
created_at: new Date(now - 60_000).toISOString(),
workflow_run: {
id: 7,
repository_id: 1,
head_repository_id: 1,
head_branch: "main",
head_sha: validSha,
},
...overrides,
};
}
function run(overrides: Record<string, unknown> = {}) {
return {
id: 7,
status: "completed",
conclusion: "success",
path: ".github/workflows/ci.yml",
event: "schedule",
created_at: new Date(now - 120_000).toISOString(),
head_sha: validSha,
head_branch: "main",
head_repository: { full_name: repository },
...overrides,
};
}
async function admissionFixture(
candidate: Record<string, unknown> = artifact(),
candidateRun: Record<string, unknown> = run(),
options: { apiError?: boolean; malformed?: boolean; force?: boolean } = {},
) {
const directory = await createTestTempDirectory("rea-ci-qualification-");
const bin = join(directory, "bin");
await mkdir(bin);
const fixturePath = join(directory, "api.json");
await writeFile(
fixturePath,
options.malformed
? "not-json"
: JSON.stringify({ artifacts: [candidate], run: candidateRun }),
);
const gh = join(bin, "gh");
await writeFile(
gh,
[
"#!/bin/sh",
'if [ "$GH_API_ERROR" = "true" ]; then exit 1; fi',
'case "$*" in',
' *actions/artifacts*) cat "$GH_FIXTURE_RESPONSE" ;;',
" *actions/runs/7*) node -e 'const x=require(process.env.GH_FIXTURE_RESPONSE); process.stdout.write(JSON.stringify(x.run))' ;;",
" *) exit 2 ;;",
"esac",
"",
].join("\n"),
);
await chmod(gh, 0o755);
const output = join(directory, "github-output");
await writeFile(output, "");
const env = {
...process.env,
PATH: `${bin}:${process.env.PATH ?? ""}`,
GH_FIXTURE_RESPONSE: fixturePath,
GH_API_ERROR: String(options.apiError ?? false),
CI_REPOSITORY: repository,
CI_REPOSITORY_ID: "1",
CI_DEFAULT_BRANCH: "main",
HEAD_SHA: validSha,
GITHUB_RUN_ID: "100",
GITHUB_OUTPUT: output,
...(options.force ? { CI_FORCE_FULL: "true" } : {}),
};
return { directory, output, env };
}
async function decide(
candidate?: Record<string, unknown>,
candidateRun?: Record<string, unknown>,
options?: { apiError?: boolean; malformed?: boolean; force?: boolean },
) {
const fixture = await admissionFixture(candidate, candidateRun, options);
const result = await executeWorkflowFixture(
process.execPath,
[qualificationScript],
{
cwd: fixture.directory,
env: fixture.env,
},
);
return {
...fixture,
decision: z
.record(z.string(), z.unknown())
.parse(JSON.parse(result.stdout)),
outputText: await readFile(fixture.output, "utf8"),
};
}
it("reuses only a recent trusted full qualification for the exact default-branch SHA", async () => {
const { decision, outputText } = await decide();
expect(decision).toMatchObject({ run_full: false, run_id: 7 });
expect(Object.keys(decision).sort()).toEqual([
"reason",
"run_full",
"run_id",
]);
expect(outputText).toContain("run_full=false\n");
expect(outputText).toContain("run_id=7\n");
});
const rejectedCandidates: Array<
[string, () => Record<string, unknown>, () => Record<string, unknown>]
> = [
[
"old",
() =>
artifact({
created_at: new Date(now - 24 * 60 * 60 * 1000 - 1).toISOString(),
}),
run,
],
[
"future",
() => artifact({ created_at: new Date(now + 60_000).toISOString() }),
run,
],
["expired", () => artifact({ expired: true }), run],
[
"foreign repository",
() =>
artifact({
workflow_run: { ...artifact().workflow_run, repository_id: 2 },
}),
run,
],
[
"foreign branch",
() =>
artifact({
workflow_run: { ...artifact().workflow_run, head_branch: "other" },
}),
run,
],
[
"foreign SHA",
() =>
artifact({
workflow_run: { ...artifact().workflow_run, head_sha: "b".repeat(40) },
}),
run,
],
[
"untrusted workflow",
artifact,
() => run({ path: ".github/workflows/release.yml" }),
],
["failed run", artifact, () => run({ conclusion: "failure" })],
[
"old producer rerun with a fresh artifact",
artifact,
() =>
run({ created_at: new Date(now - 25 * 60 * 60 * 1000).toISOString() }),
],
["wrong workflow event", artifact, () => run({ event: "push" })],
];
it.each(rejectedCandidates)(
"runs full qualification for a %s candidate",
async (_name, makeArtifact, makeRun) => {
const { decision } = await decide(makeArtifact(), makeRun());
expect(decision.run_full).toBe(true);
},
);
it.each([
["API error", { apiError: true }],
["malformed query response", { malformed: true }],
] as const)("fails closed on %s", async (_name, options) => {
const { decision } = await decide(undefined, undefined, options);
expect(decision.run_full).toBe(true);
});
it("lets the daily force-full setting bypass the GitHub API", async () => {
const { decision } = await decide(undefined, undefined, {
force: true,
apiError: true,
});
expect(decision).toMatchObject({ run_full: true });
expect(decision.reason).toContain("Forced");
});
async function workflowContract() {
const workflowPath = new URL(
"../../../.github/workflows/ci.yml",
import.meta.url,
);
const parsed = parse(await readFile(workflowPath, "utf8"));
return z
.object({
jobs: z.record(
z.string(),
z.object({ needs: z.unknown().optional() }).passthrough(),
),
})
.parse(parsed).jobs;
}
async function requiredRun(
changes: Record<string, string>,
overrides: { failed?: string; runAttempt?: string; event?: string } = {},
) {
const directory = await createTestTempDirectory("rea-ci-receipt-");
await workflowGit(directory, ["init", "--initial-branch=main"]);
await writeFile(join(directory, "fixture.txt"), "qualified source\n");
await workflowGit(directory, ["add", "."]);
await workflowGit(directory, ["commit", "-m", "fixture source"]);
const head = await workflowGit(directory, ["rev-parse", "HEAD"]);
const jobs = await workflowContract();
const results: Record<
string,
{ result: string; outputs?: Record<string, string> }
> = {};
for (const name of z.array(z.string()).parse(jobs.required?.needs)) {
results[name] = {
result:
name === overrides.failed
? "failure"
: changes.tier === "reused"
? "skipped"
: "success",
};
}
results.changes = { result: "success", outputs: changes };
const receiptDirectory = join(directory, ".cache", "receipt");
const output = join(directory, "github-output");
await writeFile(output, "");
const env = {
...process.env,
CI_JOBS: JSON.stringify(results),
CI_QUALIFICATION_DIRECTORY: receiptDirectory,
GITHUB_EVENT_NAME: overrides.event ?? "schedule",
GITHUB_SHA: head,
GITHUB_RUN_ID: "100",
GITHUB_RUN_ATTEMPT: overrides.runAttempt ?? "1",
GITHUB_REPOSITORY: repository,
GITHUB_OUTPUT: output,
};
const result = executeWorkflowFixture(process.execPath, [requiredScript], {
cwd: directory,
env,
});
return { directory, head, output, result };
}
it("writes a full receipt only after every canonical flag and workflow need pass", async () => {
const scopesPath = new URL("../../../scripts/ci/scopes.mjs", import.meta.url);
const { CI_FLAGS } = await import(scopesPath.href);
const changes = Object.fromEntries(
CI_FLAGS.map((flag: string) => [flag, "true"]),
);
changes.tier = "full";
const { directory, head, output, result } = await requiredRun(changes);
await expect(result).resolves.toMatchObject({ stderr: "" });
const receiptPath = join(directory, ".cache", "receipt", "receipt.json");
const receipt = z
.object({
sourceSha: z.string(),
runId: z.number(),
runAttempt: z.number(),
})
.parse(JSON.parse(await readFile(receiptPath, "utf8")));
expect(receipt).toMatchObject({ sourceSha: head, runId: 100, runAttempt: 1 });
expect(await readFile(output, "utf8")).toContain(
`receipt_path=${receiptPath}\n`,
);
expect(await readFile(output, "utf8")).toContain(
"receipt_name=ci-full-qualification-v1\n",
);
});
it.each([
["failed selected lane", { failed: "sqlite-runtime" }],
["partial full scope", {}],
] as const)(
"does not issue a receipt after %s",
async (caseName, overrides) => {
const scopesPath = new URL(
"../../../scripts/ci/scopes.mjs",
import.meta.url,
);
const { CI_FLAGS } = await import(scopesPath.href);
const changes = Object.fromEntries(
CI_FLAGS.map((flag: string) => [flag, "true"]),
);
changes.tier = "full";
if (caseName === "partial full scope") changes.sqlite = "false";
const outcome = await requiredRun(changes, overrides);
await expect(outcome.result).rejects.toMatchObject({ code: 1 });
await expect(
readFile(join(outcome.directory, ".cache", "receipt", "receipt.json")),
).rejects.toMatchObject({ code: "ENOENT" });
expect(await readFile(outcome.output, "utf8")).not.toContain("receipt_");
},
);
it("never writes a fresh receipt for a reused scheduled qualification", async () => {
const scopesPath = new URL("../../../scripts/ci/scopes.mjs", import.meta.url);
const { CI_FLAGS } = await import(scopesPath.href);
const changes = Object.fromEntries(
CI_FLAGS.map((flag: string) => [flag, "false"]),
);
changes.tier = "reused";
changes.qualification_run_id = "7";
const outcome = await requiredRun(changes);
const result = await outcome.result;
await expect(
readFile(join(outcome.directory, ".cache", "receipt", "receipt.json")),
).rejects.toMatchObject({ code: "ENOENT" });
expect(result.stdout).toContain("Qualification reused from run 7");
expect(await readFile(outcome.output, "utf8")).not.toContain("receipt_");
});
it.each([
["missing receipt run", { qualification_run_id: "" }, "schedule"],
["selected validation lane", { sqlite: "true" }, "schedule"],
["manual dispatch", {}, "workflow_dispatch"],
] as const)(
"rejects qualification reuse with %s",
async (_name, override, event) => {
const { CI_FLAGS } = await import(
new URL("../../../scripts/ci/scopes.mjs", import.meta.url).href
);
const changes: Record<string, string> = {
...Object.fromEntries(CI_FLAGS.map((flag: string) => [flag, "false"])),
tier: "reused",
qualification_run_id: "7",
...override,
};
const outcome = await requiredRun(changes, { event });
await expect(outcome.result).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining("scheduled admission receipt"),
});
expect(await readFile(outcome.output, "utf8")).not.toContain("receipt_");
},
);
+338 -29
View File
@@ -26,7 +26,19 @@ const workflow = async (filename = "ci.yml") =>
),
);
const scopeFlags = async () =>
z
.object({ CI_FLAGS: z.array(z.string()) })
.parse(
await import(
new URL("../../../scripts/ci/scopes.mjs", import.meta.url).href
),
).CI_FLAGS;
it("binds SQLite compatibility jobs to the build's prepared runtime artifact and root layout", async () => {
expect((await workflow()).jobs["sqlite-runtime"]?.if).toBe(
"needs.changes.outputs.sqlite == 'true'",
);
const steps = z.array(
z.object({
uses: z.string().optional(),
@@ -52,6 +64,77 @@ it("binds SQLite compatibility jobs to the build's prepared runtime artifact and
expect(downloads[0]?.with).toEqual({ name, path: "." });
});
it("exports every canonical scope with read authority for admission and keeps telemetry outside the gate", async () => {
const { jobs } = await workflow();
const CI_FLAGS = await scopeFlags();
const outputs = z.record(z.string(), z.string()).parse(jobs.changes?.outputs);
expect(jobs.changes?.permissions).toEqual({
contents: "read",
actions: "read",
});
expect(jobs.build?.permissions).toEqual({
contents: "read",
actions: "read",
});
for (const flag of CI_FLAGS)
expect(outputs[flag]).toBe(`\${{ steps.plan.outputs.${flag} }}`);
const steps = z
.array(
z.object({
run: z.string().optional(),
if: z.string().optional(),
}),
)
.parse(jobs.required?.steps);
const telemetry = steps.find((step) =>
step.run?.includes("scripts/ci/job-timings.mjs"),
);
expect(telemetry).toBeUndefined();
const timings = z
.object({
on: z.object({
schedule: z.array(z.object({ cron: z.string() })),
workflow_dispatch: z.unknown(),
workflow_run: z.unknown().optional(),
}),
permissions: z.record(z.string(), z.string()),
jobs: z.object({
report: z.object({
steps: z.array(
z.object({
uses: z.string().optional(),
run: z.string().optional(),
with: z.record(z.string(), z.unknown()).optional(),
env: z.record(z.string(), z.unknown()).optional(),
}),
),
}),
}),
})
.parse(
parse(
await readFile(
new URL("../../../.github/workflows/ci-timings.yml", import.meta.url),
"utf8",
),
),
);
expect(timings.on.workflow_run).toBeUndefined();
expect(timings.on.schedule).toHaveLength(1);
expect(timings.permissions).toEqual({ contents: "read", actions: "read" });
const checkout = timings.jobs.report.steps.find((step) =>
step.uses?.startsWith("actions/checkout@"),
);
expect(checkout?.with).toEqual({
ref: "${{ github.event.repository.default_branch }}",
"persist-credentials": false,
});
const report = timings.jobs.report.steps.find((step) =>
step.run?.includes("scripts/ci/job-timings-batch.mjs"),
);
expect(report?.env?.CI_REPOSITORY).toBe("${{ github.repository }}");
});
const requiredNeeds = async () => {
const { jobs } = await workflow();
return { jobs, needs: z.array(z.string()).parse(jobs.required?.needs) };
@@ -64,6 +147,147 @@ it("makes the required gate wait for every CI lane, including future additions",
);
});
it("holds native and provider fan-out behind static validation while portable feedback stays independent", async () => {
const { jobs } = await workflow();
const earlyJobs = new Set([
"changes",
"static",
"build",
"test-shard",
"test",
"docs",
"required",
]);
for (const [name, job] of Object.entries(jobs)) {
if (earlyJobs.has(name)) continue;
expect(job.needs, `${name} must wait for static validation`).toContain(
"static",
);
expect(job.if).not.toMatch(/always\(|cancelled\(/u);
}
expect(jobs["test-shard"]?.needs).not.toContain("static");
});
it("binds provider build reuse to the current run while keeping standalone dispatches independent", async () => {
const { jobs } = await workflow();
const consumers = [
"browser",
"binary_layout",
"evm",
"javascript_recovery",
"recorded_crash",
"web_network",
"web_runtime",
"web_source_map",
];
const buildTerms = z
.string()
.parse(jobs.build?.if)
.split("||")
.map((term) => term.trim());
// Provider workflow edits select their lane without code=true. The build
// producer must still run, without selecting the portable test shards.
expect(new Set(buildTerms)).toEqual(
new Set(
["code", ...consumers].map(
(scope) => `needs.changes.outputs.${scope} == 'true'`,
),
),
);
expect(jobs["test-shard"]?.if).toBe("needs.changes.outputs.code == 'true'");
for (const name of consumers) {
const caller = jobs[name];
expect(caller?.needs).toContain("build");
expect(caller?.with).toEqual({ build_cache_artifact: "rea-build-cache" });
const path = z.string().parse(caller?.uses);
const called = z
.object({
on: z.object({
workflow_call: z.object({
inputs: z.record(z.string(), z.unknown()),
}),
workflow_dispatch: z.unknown(),
}),
jobs: z.record(
z.string(),
z.object({
steps: z.array(
z.object({
uses: z.string().optional(),
with: z.record(z.string(), z.unknown()).optional(),
}),
),
}),
),
})
.parse(
parse(
await readFile(new URL(`../../../${path}`, import.meta.url), "utf8"),
),
);
expect(called.on.workflow_call.inputs.build_cache_artifact).toMatchObject({
type: "string",
default: "",
});
expect(Object.hasOwn(called.on, "workflow_dispatch")).toBe(true);
for (const job of Object.values(called.jobs))
expect(
job.steps.find(
(step) => step.uses === "./.github/actions/portable-build-cache",
)?.with,
).toEqual({ build_cache_artifact: "${{ inputs.build_cache_artifact }}" });
}
const action = z
.object({
runs: z.object({
steps: z.array(
z.object({
id: z.string().optional(),
uses: z.string().optional(),
if: z.string().optional(),
with: z.record(z.string(), z.unknown()).optional(),
"continue-on-error": z.boolean().optional(),
}),
),
}),
})
.parse(
parse(
await readFile(
new URL(
"../../../.github/actions/portable-build-cache/action.yml",
import.meta.url,
),
"utf8",
),
),
);
const download = action.runs.steps.find(
(step) => step.uses === "actions/download-artifact@v4",
);
expect(download?.with).toEqual({
name: "${{ inputs.build_cache_artifact }}",
path: ".turbo/cache/",
});
expect(download?.if).toBe("inputs.build_cache_artifact != ''");
expect(download?.["continue-on-error"]).not.toBe(true);
for (const step of action.runs.steps.filter(
(step) => step.id === "plan" || step.uses?.startsWith("actions/cache"),
))
expect(step.if).toContain("inputs.build_cache_artifact == ''");
});
it("runs workflow validation inside required static checks when selected", async () => {
const { jobs } = await workflow();
const steps = z
.array(z.object({ run: z.string().optional(), if: z.string().optional() }))
.parse(jobs.static?.steps);
const validation = steps.filter((step) => step.run?.includes("actionlint"));
expect(validation).toHaveLength(1);
expect(validation[0]?.if).toBe("needs.changes.outputs.workflows == 'true'");
expect(jobs.workflows).toBeUndefined();
});
it("leaves cancellation ownership with CI so child lanes cannot cancel another tier", async () => {
const { jobs } = await workflow();
const children = Object.values(jobs).flatMap((job) =>
@@ -131,15 +355,15 @@ it("keeps fast feedback separate from complete qualification and release proof",
),
),
);
expect(ci.on.schedule).toEqual([{ cron: "23 * * * *" }]);
expect(ci.on.schedule).toEqual([
{ cron: "23 0-2,4-23 * * *" },
{ cron: "23 3 * * *" },
]);
expect(ci.concurrency.group).toContain("'qualification' || 'feedback'");
expect(ci.concurrency.group).toContain("inputs.source_sha ||");
for (const fullEvent of ["schedule", "workflow_dispatch"])
expect(ci.concurrency["cancel-in-progress"]).toContain(
`github.event_name != '${fullEvent}'`,
);
expect(ci.concurrency["cancel-in-progress"]).toContain(
"inputs.source_sha == ''",
expect(ci.concurrency.group).toContain("github.ref");
expect(ci.concurrency["cancel-in-progress"]).toBe(
"${{ github.event_name == 'pull_request' && inputs.source_sha == '' && !contains(github.event.pull_request.labels.*.name, 'ci:full') }}",
);
expect(ci.jobs.required?.if).toBe("${{ always() }}");
const shards = ci.jobs["test-shard"]?.steps ?? [];
@@ -164,34 +388,119 @@ it.each([
["true", "skipped", false],
["true", "missing", false],
["false", "skipped", true],
["false", "failure", false],
] as const)(
"enforces the SQLite required gate for code=%s and result=%s",
async (code, result, passes) => {
"enforces mapped required scopes for selected=%s and result=%s",
async (selected, result, passes) => {
const workflow = await requiredNeeds();
// GitHub exposes only direct dependencies in the required job's needs context.
const needs: Record<
string,
{ result: string; outputs?: Record<string, string> }
> = Object.fromEntries(
workflow.needs.map((name) => [name, { result: "success" }]),
);
needs.changes = { result: "success", outputs: { code } };
if (result === "missing") delete needs["sqlite-runtime"];
else if ("sqlite-runtime" in needs) needs["sqlite-runtime"] = { result };
const execution = executeWorkflowFixture(
for (const [scope, lane] of [
["sqlite", "sqlite-runtime"],
["workflows", "static"],
] as const) {
// GitHub exposes only direct dependencies in the required job's needs context.
const needs: Record<
string,
{ result: string; outputs?: Record<string, string> }
> = Object.fromEntries(
workflow.needs.map((name) => [name, { result: "success" }]),
);
needs.changes = {
result: "success",
outputs: {
...Object.fromEntries(
(await scopeFlags()).map((flag) => [flag, "false"]),
),
tier: "fast",
code: "true",
[scope]: selected,
},
};
if (result === "missing") delete needs[lane];
else needs[lane] = { result };
const execution = executeWorkflowFixture(
process.execPath,
[
fileURLToPath(
new URL("../../../scripts/ci/required.mjs", import.meta.url),
),
],
{ env: { ...process.env, CI_JOBS: JSON.stringify(needs) } },
);
// Static checks remain required even without workflow edits.
const expected = passes && !(lane === "static" && result !== "success");
if (expected)
await expect(execution).resolves.toMatchObject({ stderr: "" });
else
await expect(execution).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining(lane),
});
}
},
);
it("rejects incomplete or malformed classification even when available jobs succeeded", async () => {
const flags = await scopeFlags();
const valid = {
...Object.fromEntries(flags.map((flag) => [flag, "false"])),
tier: "fast",
};
const run = (outputs: unknown) =>
executeWorkflowFixture(
process.execPath,
[
fileURLToPath(
new URL("../../../scripts/ci/required.mjs", import.meta.url),
),
],
{ env: { ...process.env, CI_JOBS: JSON.stringify(needs) } },
{
env: {
...process.env,
CI_JOBS: JSON.stringify({
changes: { result: "success", outputs },
static: { result: "success" },
test: { result: "success" },
}),
CI_QUALIFICATION_DIRECTORY: "",
},
},
);
if (passes) await expect(execution).resolves.toMatchObject({ stderr: "" });
else
await expect(execution).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining("sqlite-runtime"),
});
},
);
await expect(run(valid)).resolves.toMatchObject({ stderr: "" });
// Metadata is not a scope, even when its string happens to be "true".
await expect(
run({ ...valid, qualification_run_id: "true" }),
).resolves.toMatchObject({ stderr: "" });
for (const outputs of [
undefined,
null,
{},
{ ...valid, tier: undefined },
{ ...valid, tier: "" },
{ ...valid, tier: "unknown" },
]) {
await expect(run(outputs)).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining("valid tier"),
});
}
for (const flag of flags) {
await expect(run({ ...valid, [flag]: undefined })).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining(`scope outputs: ${flag}`),
});
}
for (const value of ["", "TRUE", "yes", true, false, 0, null, [], {}]) {
await expect(run({ ...valid, sqlite: value })).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining("scope outputs: sqlite"),
});
}
for (const tier of ["full", "reused"]) {
await expect(
run({ ...valid, tier, sqlite: undefined }),
).rejects.toMatchObject({
code: 1,
stderr: expect.stringContaining("scope outputs: sqlite"),
});
}
});
@@ -0,0 +1,188 @@
import { chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { delimiter, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { expect, it } from "vitest";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { executeWorkflowFixture } from "../../support/workflowGit.js";
const script = fileURLToPath(
new URL("../../../scripts/ci/test-history.mjs", import.meta.url),
);
const repository = "example/rea";
const repositoryId = 1;
const now = Date.now();
function artifact(id: number, overrides: Record<string, unknown> = {}) {
return {
id: id + 100,
name: "test-timings",
expired: false,
created_at: new Date(now - 60_000).toISOString(),
workflow_run: {
id,
repository_id: repositoryId,
head_repository_id: repositoryId,
head_branch: "main",
},
...overrides,
};
}
function run(id: number, overrides: Record<string, unknown> = {}) {
return {
id,
status: "completed",
conclusion: "success",
path: ".github/workflows/ci.yml",
event: "schedule",
created_at: new Date(now - 120_000).toISOString(),
head_branch: "main",
head_repository: { full_name: repository },
...overrides,
};
}
async function invoke(
artifacts: unknown[],
runs: Record<number, unknown>,
options: {
apiError?: boolean;
repository?: string;
repositoryId?: string;
} = {},
) {
const directory = await createTestTempDirectory("rea-ci-test-history-");
const fakeBin = join(directory, "bin");
await mkdir(fakeBin);
const fixture = join(directory, "api.json");
const calls = join(directory, "api-calls");
const output = join(directory, "github-output");
await writeFile(fixture, JSON.stringify({ artifacts, runs }));
await writeFile(output, "");
await writeFile(calls, "");
const fakeGh = join(fakeBin, "gh");
await writeFile(
fakeGh,
[
"#!/bin/sh",
'printf \'%s\\n\' "$*" >> "$GH_API_CALLS"',
'if [ "$GH_API_ERROR" = "true" ]; then exit 1; fi',
'if [ "$4" = "repos/example/rea/actions/artifacts" ]; then',
' node -e \'const fs=require("node:fs"); const x=JSON.parse(fs.readFileSync(process.env.GH_FIXTURE_RESPONSE,"utf8")); process.stdout.write(JSON.stringify({artifacts:x.artifacts}));\'',
"else",
' run_id="${4##*/}"',
' node -e \'const fs=require("node:fs"); const x=JSON.parse(fs.readFileSync(process.env.GH_FIXTURE_RESPONSE,"utf8")); process.stdout.write(JSON.stringify(x.runs[Number(process.argv[1])] ?? {}));\' "$run_id"',
"fi",
"",
].join("\n"),
);
await chmod(fakeGh, 0o755);
const node24 = dirname(process.execPath);
const result = await executeWorkflowFixture(process.execPath, [script], {
cwd: directory,
env: {
...process.env,
PATH: [fakeBin, node24, process.env.PATH ?? ""].join(delimiter),
GH_FIXTURE_RESPONSE: fixture,
GH_API_CALLS: calls,
GH_API_ERROR: String(options.apiError ?? false),
GH_TOKEN: "fixture-token",
CI_REPOSITORY: options.repository ?? repository,
CI_REPOSITORY_ID: options.repositoryId ?? String(repositoryId),
CI_DEFAULT_BRANCH: "main",
GITHUB_OUTPUT: output,
},
});
return {
result,
output: await readFile(output, "utf8"),
calls: await readFile(calls, "utf8"),
};
}
it("selects the latest trusted full run that actually published timing history", async () => {
const { result, output, calls } = await invoke(
[
artifact(15),
artifact(14),
artifact(13, {
workflow_run: { ...artifact(13).workflow_run, head_repository_id: 2 },
}),
artifact(12, {
workflow_run: { ...artifact(12).workflow_run, head_branch: "feature" },
}),
artifact(11),
],
{
15: run(15, { conclusion: "failure" }),
14: run(14, { path: ".github/workflows/release.yml" }),
11: run(11, { event: "workflow_dispatch" }),
},
);
expect(JSON.parse(result.stdout)).toEqual({ run_id: "11" });
expect(output).toBe("run-id=11\n");
expect(calls).toContain("name=test-timings");
expect(calls).toContain("per_page=100");
expect(calls.match(/actions\/runs\//gu)).toHaveLength(3);
expect(calls).not.toContain("actions/workflows");
});
it("rejects expired, foreign, old, and non-full run candidates", async () => {
const { result, output } = await invoke(
[
artifact(15, { expired: true }),
artifact(14, {
workflow_run: { ...artifact(14).workflow_run, head_repository_id: 2 },
}),
artifact(13, {
created_at: new Date(now - 31 * 24 * 60 * 60 * 1000).toISOString(),
}),
artifact(12),
],
{
12: run(12, { event: "pull_request" }),
},
);
expect(JSON.parse(result.stdout)).toEqual({ run_id: "" });
expect(output).toBe("run-id=\n");
});
it("writes an empty run id when GitHub history is unavailable", async () => {
const { result, output } = await invoke([], {}, { apiError: true });
expect(JSON.parse(result.stdout)).toEqual({ run_id: "" });
expect(output).toBe("run-id=\n");
});
it("bounds run lookups to five artifacts even when inventory has more", async () => {
const ids = [25, 24, 23, 22, 21, 20, 19];
const { result, calls } = await invoke(
ids.map((id) => artifact(id)),
Object.fromEntries(
ids.map((id, index) => [
id,
run(id, index < 5 ? { conclusion: "failure" } : {}),
]),
),
);
expect(JSON.parse(result.stdout)).toEqual({ run_id: "" });
expect(calls.match(/actions\/runs\//gu)).toHaveLength(5);
});
it("returns empty history without querying for an invalid repository identity", async () => {
const { result, output, calls } = await invoke(
[],
{},
{
repository: "owner/repo?per_page=100",
repositoryId: "not-an-id",
},
);
expect(JSON.parse(result.stdout)).toEqual({ run_id: "" });
expect(output).toBe("run-id=\n");
expect(calls).toBe("");
});
+255 -15
View File
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
@@ -8,6 +8,8 @@ import { expect, it } from "vitest";
import { parse } from "yaml";
import { z } from "zod";
import { planTestSelection } from "../../../scripts/ci/test-selection.mjs";
import { parseTestDurations } from "../../ciTestSequencer.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { workflowGit } from "../../support/workflowGit.js";
@@ -54,6 +56,23 @@ it("collects failed-shard diagnostics before enforcing the shard result", async
expect(upload).toBeGreaterThan(merge);
expect(gate).toBeGreaterThan(upload);
expect(steps[merge]?.if).not.toContain("needs.test-shard.result");
const merges = steps.filter((step) => step.run?.includes("--reporter=json"));
expect(merges).toHaveLength(2);
for (const step of merges)
expect(step.run).toContain(
"--reporter=./scripts/ci/test-timing-reporter.mjs",
);
expect(steps[upload]?.with?.path).toContain("test-timings.json");
expect(steps[upload]?.with).toMatchObject({ "include-hidden-files": true });
expect(steps[upload]?.with?.path).toContain(
".cache/ci-test-selection/report.json",
);
const summary = steps.find((step) =>
step.run?.includes("scripts/ci/test-report.mjs"),
);
expect(summary?.env?.CI_TEST_SELECTION).toBe(
"${{ needs.changes.outputs.test_selection }}",
);
const command = steps[gate]?.run;
if (command === undefined) throw new Error("Missing shard result gate");
for (const result of ["failure", "cancelled", "skipped", ""]) {
@@ -80,10 +99,22 @@ it("keeps diagnosis available after merge failure and publishes only complete su
expect(history?.if).toContain("needs.test-shard.result == 'success'");
expect(history?.if).toContain("needs.changes.outputs.tier == 'full'");
expect(history?.with?.path).toBe(".cache/ci-test-history/test-results.json");
const download = (await workflow()).build!.steps.find(
const buildSteps = (await workflow()).build!.steps;
const lookup = buildSteps.find((step) =>
step.run?.includes("test-history.mjs"),
);
expect(lookup?.env).toMatchObject({
CI_REPOSITORY: "${{ github.repository }}",
CI_REPOSITORY_ID: "${{ github.repository_id }}",
CI_DEFAULT_BRANCH: "${{ github.event.repository.default_branch }}",
});
const download = buildSteps.find(
(step) => step.name === "Download previous timing report",
);
expect(download?.with?.name).toBe(history?.with?.name);
expect(download?.with?.["run-id"]).toBe(
"${{ steps.history.outputs.run-id }}",
);
});
it("rejects successful shard reports with missing or duplicate portable files", async () => {
@@ -95,11 +126,11 @@ it("rejects successful shard reports with missing or duplicate portable files",
await workflowGit(root, ["add", "tests"]);
const run = async (names: readonly string[]) => {
await writeFile(
join(root, "test-results.json"),
join(root, "test-timings.json"),
JSON.stringify({
version: 1,
success: true,
numFailedTests: 0,
testResults: names.map((name) => ({ name, startTime: 0, endTime: 1 })),
files: names.map((name) => ({ name, durationMs: 1 })),
}),
);
return execute(process.execPath, [fileURLToPath(script)], {
@@ -161,20 +192,25 @@ it("exports portable timing history and removes it on a failed, partial, or inva
const summary = join(root, "summary.md");
const history = join(root, ".cache/ci-test-history/test-results.json");
const report = {
version: 1,
success: true,
numFailedTests: 0,
testResults: [
files: [
{
name: join(root, "tests/slow.test.ts"),
startTime: 10,
endTime: 110,
assertionResults: [{ large: "not timing data" }],
durationMs: 100,
phases: {
execution: 20,
preparation: 20,
environment: 10,
imports: 40,
setup: 10,
},
},
{ name: join(root, "tests/fast.test.ts"), startTime: 20, endTime: 30 },
{ name: join(root, "tests/fast.test.ts"), durationMs: 10 },
],
};
const run = async (value: unknown, shards = "success", merge = "success") => {
await writeFile(join(root, "test-results.json"), JSON.stringify(value));
await writeFile(join(root, "test-timings.json"), JSON.stringify(value));
await writeFile(summary, "");
return execute(process.execPath, [fileURLToPath(script)], {
cwd: root,
@@ -188,7 +224,8 @@ it("exports portable timing history and removes it on a failed, partial, or inva
};
await run(report);
const contents = await readFile(history, "utf8");
expect(contents).not.toContain("assertionResults");
expect(contents).not.toContain("phases");
expect(contents).not.toContain("startTime");
expect(contents).not.toContain(root);
expect(
parseTestDurations(contents, resolve(root, "different-checkout")),
@@ -199,7 +236,7 @@ it("exports portable timing history and removes it on a failed, partial, or inva
]),
);
for (const [value, shards, merge] of [
[{ ...report, success: false, numFailedTests: 1 }, "failure", "failure"],
[{ ...report, success: false }, "failure", "failure"],
[report, "cancelled", "success"],
[report, "success", "failure"],
] as const) {
@@ -212,8 +249,211 @@ it("exports portable timing history and removes it on a failed, partial, or inva
await expect(
run({
...report,
testResults: [{ name: "../outside.test.ts", startTime: 1, endTime: 3 }],
files: [{ name: "../outside.test.ts", durationMs: 2 }],
}),
).rejects.toMatchObject({ code: 1 });
await expect(readFile(history)).rejects.toMatchObject({ code: "ENOENT" });
await run(report);
await expect(
run({
...report,
files: [{ name: "tests/invalid.test.ts", durationMs: -1 }],
}),
).rejects.toMatchObject({ code: 1 });
await expect(readFile(history)).rejects.toMatchObject({ code: "ENOENT" });
});
it("recovers complete module costs from real blob reports without executing tests again", async () => {
const root = await createTestTempDirectory("rea-ci-module-timing-");
const config = join(root, "vitest.config.mjs");
await writeFile(
config,
'export default { test: { globals: true, include: ["sample.test.js"], maxWorkers: 1 } };',
);
await writeFile(
join(root, "sample.test.js"),
[
'import { writeFileSync } from "node:fs";',
"beforeAll(() => new Promise(resolve => setTimeout(resolve, 80)));",
"afterAll(() => new Promise(resolve => setTimeout(resolve, 80)));",
'it("has cheap assertions and expensive outer hooks", () => { writeFileSync("executed", "yes"); expect(1).toBe(1); });',
].join("\n"),
);
const vitest = fileURLToPath(
new URL("../../../node_modules/vitest/vitest.mjs", import.meta.url),
);
const reporter = fileURLToPath(
new URL("../../../scripts/ci/test-timing-reporter.mjs", import.meta.url),
);
const run = (args: string[]) =>
execute(process.execPath, [vitest, ...args, "--config", config], {
cwd: root,
env: process.env,
timeout: 20_000,
});
await run(["run", "--reporter=blob"]);
expect(await readFile(join(root, "executed"), "utf8")).toBe("yes");
await rm(join(root, "executed"));
await run([
"--merge-reports",
`--reporter=${reporter}`,
"--reporter=json",
"--outputFile.json=body.json",
]);
await expect(readFile(join(root, "executed"))).rejects.toMatchObject({
code: "ENOENT",
});
const report = z
.object({
version: z.literal(1),
success: z.literal(true),
files: z.array(
z.object({
name: z.string(),
durationMs: z.number(),
phases: z.record(z.string(), z.number()),
}),
),
})
.parse(JSON.parse(await readFile(join(root, "test-timings.json"), "utf8")));
expect(
JSON.parse(await readFile(join(root, "test-timings.json"), "utf8")),
).toMatchObject({
reason: "passed",
unhandledErrors: 0,
files: [{ state: "passed" }],
});
expect(report.files).toHaveLength(1);
const file = report.files[0]!;
const json = z
.object({
testResults: z.array(
z.object({ startTime: z.number(), endTime: z.number() }),
),
})
.parse(JSON.parse(await readFile(join(root, "body.json"), "utf8")));
const body = json.testResults[0]!;
expect(
file.phases.execution! - (body.endTime - body.startTime),
).toBeGreaterThan(100);
expect(file.phases.imports).toBeGreaterThan(0);
expect(file.phases.preparation).toBeGreaterThan(0);
expect(file.durationMs).toBeCloseTo(
Object.values(file.phases).reduce((sum, value) => sum + value, 0),
);
await execute(process.execPath, [fileURLToPath(script)], {
cwd: root,
env: { ...process.env, SHARD_RESULT: "success", MERGE_RESULT: "success" },
});
expect(
parseTestDurations(
await readFile(
join(root, ".cache/ci-test-history/test-results.json"),
"utf8",
),
root,
)?.get("sample.test.js"),
).toBe(file.durationMs);
await writeFile(
join(root, "sample.test.js"),
'it("fails", () => { throw new Error("fixture failure"); });',
);
await expect(run(["run", "--reporter=blob"])).rejects.toMatchObject({
code: 1,
});
await expect(
run(["--merge-reports", `--reporter=${reporter}`]),
).rejects.toMatchObject({ code: 1 });
const failed = JSON.parse(
await readFile(join(root, "test-timings.json"), "utf8"),
) as unknown;
expect(failed).toMatchObject({
version: 1,
success: false,
reason: "failed",
unhandledErrors: 0,
files: [{ state: "failed" }],
});
// Even a caller accidentally marking the merge successful cannot publish this report.
await execute(process.execPath, [fileURLToPath(script)], {
cwd: root,
env: { ...process.env, SHARD_RESULT: "success", MERGE_RESULT: "success" },
});
await expect(
readFile(join(root, ".cache/ci-test-history/test-results.json")),
).rejects.toMatchObject({ code: "ENOENT" });
});
it("records failed omissions from full reports without treating partial or unhandled outcomes as parity", async () => {
const root = await createTestTempDirectory("rea-ci-selection-report-");
await workflowGit(root, ["init"]);
const paths = [
"src/sqlite/provider.test.ts",
"src/evm/provider.test.ts",
"tests/boundary/mcp/workflow.test.ts",
];
for (const path of paths) {
await mkdir(join(root, path, ".."), { recursive: true });
await writeFile(join(root, path), "// fixture\n");
}
await workflowGit(root, ["add", "src", "tests"]);
const timing = {
version: 1,
success: false,
reason: "failed",
unhandledErrors: 0,
files: paths.map((name, index) => ({
name,
durationMs: 10,
state: index === 1 ? "failed" : "passed",
})),
};
const run = async (report: unknown) => {
await writeFile(join(root, "test-timings.json"), JSON.stringify(report));
await execute(process.execPath, [fileURLToPath(script)], {
cwd: root,
env: {
...process.env,
SHARD_RESULT: "failure",
MERGE_RESULT: "failure",
CI_VERIFY_TEST_INVENTORY: "true",
CI_TEST_SELECTION: JSON.stringify(
planTestSelection(["src/sqlite/Provider.ts"], "pull_request", "fast"),
),
GITHUB_SHA: "merge-sha",
CI_PR_HEAD_SHA: "pr-head-sha",
},
});
return JSON.parse(
await readFile(
join(root, ".cache/ci-test-selection/report.json"),
"utf8",
),
) as unknown;
};
expect(await run(timing)).toMatchObject({
observational: true,
complete: true,
selected: [paths[0], paths[2]],
omitted: [paths[1]],
missedFailures: [paths[1]],
executionSha: "merge-sha",
prHeadSha: "pr-head-sha",
});
for (const report of [
{ ...timing, files: timing.files.slice(0, 1) },
{ ...timing, unhandledErrors: 1 },
{ ...timing, reason: "interrupted" },
])
expect(await run(report)).toMatchObject({ complete: false });
await expect(run({ ...timing, files: "invalid" })).rejects.toMatchObject({
code: 1,
});
await expect(
readFile(join(root, ".cache/ci-test-selection/report.json")),
).rejects.toMatchObject({ code: "ENOENT" });
await expect(
readFile(join(root, ".cache/ci-test-history/test-results.json")),
).rejects.toMatchObject({ code: "ENOENT" });
});
@@ -17,24 +17,24 @@ describe("CI test history", () => {
const root = resolve("/workspace/rea");
const durations = parseTestDurations(
JSON.stringify({
version: 1,
success: true,
numFailedTests: 0,
testResults: [
files: [
{
name: join(root, "tests/slow.test.ts"),
startTime: 10,
endTime: 110,
durationMs: 100,
},
{
name: join(root, "tests/slow.test.ts"),
startTime: 20,
endTime: 80,
durationMs: 60,
},
{
name: resolve(root, "../outside.test.ts"),
startTime: 10,
endTime: 1000,
durationMs: 990,
},
{ name: "tests/negative.test.ts", durationMs: -1 },
{ name: "tests/zero.test.ts", durationMs: 0 },
{ name: "tests/invalid.test.ts", durationMs: "100" },
null,
],
}),
@@ -43,7 +43,20 @@ describe("CI test history", () => {
expect(durations).toEqual(new Map([["tests/slow.test.ts", 100]]));
expect(
parseTestDurations('{"success": false, "numFailedTests": 1}', root),
parseTestDurations('{"version": 1, "success": false, "files": []}', root),
).toBeUndefined();
// Legacy test-body spans must not be mixed with complete module costs.
expect(
parseTestDurations(
JSON.stringify({
success: true,
numFailedTests: 0,
testResults: [
{ name: "tests/legacy.test.ts", startTime: 0, endTime: 100 },
],
}),
root,
),
).toBeUndefined();
expect(parseTestDurations("not json", root)).toBeUndefined();
expect(parseTestDurations("null", root)).toBeUndefined();
@@ -649,9 +649,8 @@ it.skipIf(process.platform === "win32").each([
const sourceSha = await candidate(f, valid ? "6.0.0" : "5.1.0");
const workflow = z
.object({
jobs: z.record(
z.string(),
z.object({
jobs: z.object({
[job]: z.object({
steps: z.array(
z.object({
name: z.string().optional(),
@@ -659,7 +658,7 @@ it.skipIf(process.platform === "win32").each([
}),
),
}),
),
}),
})
.parse(
parse(
@@ -489,4 +489,28 @@ describe("safe artifact output Windows stream paths", () => {
await expect(access(output)).rejects.toMatchObject({ code: "ENOENT" });
expect(await readdir(parent)).toEqual([]);
});
it.each(["Folder/Icon\r", "Fold\ter/file.txt"])(
"refuses Windows control characters before creating %j",
async (path) => {
const parent = await createTestTempDirectory("rea-safe-output-control-");
const output = join(parent, "published");
const tree = await SafeOutputTree.create(output, "win32");
const bytes = Buffer.from("regular file content");
await expect(
tree.write(path, Readable.from(bytes), {
sha256: createHash("sha256").update(bytes).digest("hex"),
bytes: bytes.byteLength,
}),
).rejects.toMatchObject({
reason: "path",
message: `Windows destination cannot materialize artifact path: ${JSON.stringify(path)}; Windows names cannot contain U+0001-U+001F.`,
});
expect(await readdir(output)).toEqual([]);
expect(await tree.rollback()).toEqual({
status: "complete",
residualPaths: [],
});
},
);
});
@@ -1,6 +1,6 @@
import { execFile } from "node:child_process";
import { readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { cp, readFile, writeFile } from "node:fs/promises";
import { basename, join } from "node:path";
import { promisify } from "node:util";
import { expect, it } from "vitest";
@@ -78,7 +78,7 @@ it.skipIf(process.platform === "android")(
await workflowGit(root, ["init", "--initial-branch=main"]);
await workflowGit(root, ["add", "."]);
await workflowGit(root, ["commit", "-m", "test: seed cache fixture"]);
const run = () =>
const run = (cwd = root) =>
execute(
process.execPath,
[
@@ -87,7 +87,7 @@ it.skipIf(process.platform === "android")(
"test:run",
"--output-logs=errors-only",
],
{ cwd: root, env: { ...process.env, TURBO_DAEMON: "false" } },
{ cwd, env: { ...process.env, TURBO_DAEMON: "false" } },
);
await run();
await writeFile(join(root, "README_zh.md"), "wrong command");
@@ -104,5 +104,32 @@ it.skipIf(process.platform === "android")(
"prepared\n",
);
}
// Provider jobs receive the producer's cache in a fresh checkout, not its
// outputs. Turbo must restore identical inputs and rebuild changed ones.
const consumer = await createTestTempDirectory("rea-cache-consumer-");
await cp(root, consumer, {
recursive: true,
filter: (path) =>
basename(path) !== ".turbo" &&
!/^prepared-\d+\.txt$/u.test(basename(path)),
});
await cp(join(root, ".turbo/cache"), join(consumer, ".turbo/cache"), {
recursive: true,
});
const restored = await run(consumer);
expect(restored.stdout).toContain(`${dependencies.length} cached`);
for (const index of dependencies.keys())
expect(
await readFile(join(consumer, `prepared-${index}.txt`), "utf8"),
).toBe("prepared\n");
await writeFile(
join(consumer, "artifact-input.txt"),
"changed producer input",
);
await run(consumer);
for (const index of dependencies.keys())
expect(
await readFile(join(consumer, `prepared-${index}.txt`), "utf8"),
).toBe("prepared\nprepared\n");
},
);
@@ -144,14 +144,26 @@ describe("enhanced MCP tools", () => {
);
expect(result).toEqual({
document: "fixture",
image_base: "0x1000",
segments: [
{ name: "__TEXT", start: "0x1000", end: "0x1800", length: 2048 },
{ name: "__DATA", start: "0x1800", end: "0x2000", length: 2048 },
{
name: "__TEXT",
start: "0x1000",
end: "0x1800",
length: 2048,
image_base: "0x1000",
},
{
name: "__DATA",
start: "0x1800",
end: "0x2000",
length: 2048,
image_base: "0x1000",
},
],
segment_count: 2,
procedure_count: 2,
string_count: 700,
image_base: "0x1000",
});
});
@@ -0,0 +1,121 @@
import { afterEach, expect, it } from "vitest";
import { Ajv2020 } from "ajv/dist/2020.js";
import type { AnySchemaObject } from "ajv";
import {
closeEnhancedToolResources,
connect,
jsonResult,
} from "./enhancedToolsHarness.js";
import { observed as ok } from "../../fixtures/analysisExecution.js";
import type { SegmentSummary } from "../../../src/domain/hopperValues.js";
afterEach(closeEnhancedToolResources);
const segment = {
name: ".text",
start: "0x101000",
end: "0x101100",
readable: true,
writable: false,
executable: true,
image_base: "0x100000",
address_space: "ram",
};
const overviewClient = (segments: SegmentSummary[]) =>
connect({
execute: (name) => {
switch (name) {
case "current_document":
return Promise.resolve(ok("fixture"));
case "list_documents":
return Promise.resolve(ok(["fixture"]));
case "list_segments":
return Promise.resolve(ok(segments));
case "list_procedures":
return Promise.resolve(ok([{ address: "0x101013", value: "entry" }]));
case "list_strings":
return Promise.resolve(ok([]));
default:
throw new Error(`Unexpected operation: ${name}`);
}
},
});
it("preserves observed coordinates and main's optional-string output contract after SDK conversion", async () => {
const client = await overviewClient([segment]);
const schema = (await client.listTools()).tools.find(
({ name }) => name === "binary_overview",
)?.outputSchema;
if (schema === undefined) throw new Error("Missing overview output schema");
const validate = new Ajv2020({ strict: false }).compile(
schema as AnySchemaObject,
);
const response = await client.callTool({
name: "binary_overview",
arguments: {},
});
expect(validate(response.structuredContent)).toBe(true);
expect(jsonResult(response)).toMatchObject({
image_base: "0x100000",
segments: [
{ start: "0x101000", image_base: "0x100000", address_space: "ram" },
],
});
const absent = JSON.parse(JSON.stringify(response.structuredContent));
delete absent.normalized_result.image_base;
expect(validate(absent)).toBe(true);
absent.normalized_result.image_base = null;
expect(validate(absent)).toBe(false);
});
it("omits unreported bases without inventing zero and preserves explicit zero", async () => {
const { image_base: _base, ...unreported } = segment;
const client = await overviewClient([unreported]);
expect(
jsonResult(
await client.callTool({ name: "binary_overview", arguments: {} }),
),
).not.toHaveProperty("image_base");
const zero = await overviewClient([{ ...segment, image_base: "0x0" }]);
expect(
jsonResult(await zero.callTool({ name: "binary_overview", arguments: {} })),
).toMatchObject({ image_base: "0x0" });
});
it("keeps a partially reported base unknown while retaining the reported segment observation", async () => {
const { image_base: _base, ...unreported } = segment;
const client = await overviewClient([
segment,
{ ...unreported, name: ".data" },
]);
const result = jsonResult(
await client.callTool({ name: "binary_overview", arguments: {} }),
);
expect(result).not.toHaveProperty("image_base");
expect(result).toMatchObject({
segments: [{ image_base: "0x100000" }, { name: ".data" }],
});
});
it("retains all conflicting observations after repeated early bases without selecting one", async () => {
const { address_space: _space, ...unreported } = segment;
const client = await overviewClient([
...Array.from({ length: 40 }, () => unreported),
{
...segment,
name: ".data",
start: "other%20space:0x201000",
end: "other%20space:0x201100",
image_base: "0x200000",
address_space: "other space",
},
]);
const result = jsonResult(
await client.callTool({ name: "binary_overview", arguments: {} }),
);
expect(result).not.toHaveProperty("image_base");
expect(result).toHaveProperty("segments.length", 41);
expect(result).toHaveProperty("segments.40.image_base", "0x200000");
expect(result).toHaveProperty("segments.40.address_space", "other space");
expect(result).not.toHaveProperty("segments.0.address_space");
});
@@ -116,6 +116,66 @@ describe("managed member comparison", () => {
});
});
describe("managed member comparison floating-point constants", () => {
it.each([
{
opcode: "ldc.r4",
positive: "22000000002a",
negative: "22000000802a",
type: 0x0c,
},
{
opcode: "ldc.r8",
positive: "2300000000000000002a",
negative: "2300000000000000802a",
type: 0x0d,
},
])(
"detects a change from positive to negative zero in $opcode",
({ opcode, positive, negative, type }) => {
const observe = (hex: string, path: string) => {
const il = Buffer.from(hex, "hex");
return inspect(
buildManagedPeFixture({
methodSignature: Buffer.from([0x00, 0x00, type]),
ilBody: Buffer.concat([Buffer.from([(il.length << 2) | 2]), il]),
}),
path,
);
};
const left = observe(positive, "/tmp/positive-zero.dll");
const right = observe(negative, "/tmp/negative-zero.dll");
const comparison = compareManagedMembers(left, right);
expect(comparison.methods[0]).toMatchObject({
status: "changed",
match: { status: "matched", basis: "exact-signature" },
dimensions: ["cil"],
});
for (const [observation, operand] of [
[left, "0"],
[right, "-0"],
] as const) {
expect(observation.result.methods[0]?.body).toMatchObject({
status: "present",
anchors: [
{ il_offset: 0, opcode, operand_kind: "constant", operand },
],
});
}
expect(left.result.methods[0]?.body.normalized_il_sha256).not.toBe(
right.result.methods[0]?.body.normalized_il_sha256,
);
const repeated = observe(negative, "/tmp/negative-zero-copy.dll");
expect(compareManagedMembers(right, repeated).methods[0]).toMatchObject({
status: "unchanged",
match: { status: "matched", basis: "exact-il-signature" },
dimensions: [],
});
},
);
});
describe("managed member comparison uncertainty", () => {
it("does not infer a structural method match from undecoded signature fields", () => {
const left = inspect(
@@ -61,7 +61,13 @@ afterEach(async () => {
});
describe("native macOS provider discovery", () => {
it.each(["ordinary.fixture", " spaced.fixture ", " leading", "trailing "])(
it.each([
"ordinary.fixture",
" spaced.fixture ",
" leading",
"trailing ",
"literal\r\nidentifier\r",
])(
"preserves the exact signing identifier %j reported by codesign",
(identifier) => {
const parsed = parseCodeSignature(
@@ -74,31 +80,68 @@ describe("native macOS provider discovery", () => {
},
);
it("separates echoed path and identifier text from signature fields", () => {
const path = "/apps/x\nAuthority=Forged\nIdentifier=forged";
const identifier = "x\nFormat=forged\nAuthority=Forged-5555";
const parsed = parseCodeSignature(
[
`Executable=${path}`,
`Identifier=${identifier}`,
"Format=Mach-O thin (arm64)",
"CodeDirectory v=20400 size=342 flags=0x2(adhoc) hashes=2+2",
"CDHash=6aee90e6",
"Signature=adhoc",
"TeamIdentifier=not set",
"",
].join("\r\n"),
false,
["/elsewhere", path],
);
expect(parsed).toMatchObject({
identifier,
format: "Mach-O thin (arm64)",
authorities: [],
cdhashes: ["6aee90e6"],
team_identifier: null,
});
});
it.each(["\n", "\r\n"])(
"separates echoed path and identifier text from signature fields (%j)",
(newline) => {
const path = "/apps/x\r\nAuthority=Forged\nIdentifier=forged";
const identifier = "x\r\nFormat=forged\nAuthority=Forged-5555\r";
const parsed = parseCodeSignature(
[
`Executable=${path}`,
`Identifier=${identifier}`,
"Format=Mach-O thin (arm64)",
"CodeDirectory v=20400 size=342 flags=0x2(adhoc) hashes=2+2",
"CDHash=6aee90e6",
"Signature=adhoc",
"TeamIdentifier=not set",
"",
].join(newline),
false,
["/elsewhere", path],
);
expect(parsed).toMatchObject({
identifier,
format: "Mach-O thin (arm64)",
authorities: [],
cdhashes: ["6aee90e6"],
team_identifier: null,
});
},
);
it.each([
{ executable: "", paths: [], newline: "\r\n", identifier: "fixture" },
{
executable: "Executable=/apps/fixture\r\n",
paths: [],
newline: "\r\n",
identifier: "fixture\nFormat=payload\r",
},
{
executable: "Executable=/apps/fixture\n",
paths: ["/elsewhere"],
newline: "\n",
identifier: "fixture\r\nFormat=payload\r",
},
{
executable: "Executable=/apps/fixture\r\n",
paths: ["/elsewhere"],
newline: "\r\n",
identifier: "fixture\r",
},
])(
"frames the identifier without a matching executable path: %j",
({ executable, paths, newline, identifier }) => {
const parsed = parseCodeSignature(
`${executable}Identifier=${identifier}${newline}Format=Mach-O thin (arm64)${newline}TeamIdentifier=not set${newline}`,
false,
paths,
);
expect(parsed.identifier).toBe(identifier);
expect(parsed.format).toBe("Mach-O thin (arm64)");
expect(parsed.team_identifier).toBeNull();
},
);
it.each(["timeout", "output-limit"] as const)(
"preserves %s resolution failures with their captured output and cleanup status",
+6 -10
View File
@@ -5,14 +5,13 @@ import { BaseSequencer, type TestSpecification } from "vitest/node";
import { z } from "zod";
const reportSchema = z.object({
version: z.literal(1),
success: z.literal(true),
numFailedTests: z.literal(0),
testResults: z.array(z.unknown()),
files: z.array(z.unknown()),
});
const fileSchema = z.object({
name: z.string(),
startTime: z.number(),
endTime: z.number(),
durationMs: z.number().finite().positive(),
});
export function parseTestDurations(
@@ -30,15 +29,12 @@ export function parseTestDurations(
if (!report.success) return undefined;
const durations = new Map<string, number>();
for (const value of report.data.testResults) {
for (const value of report.data.files) {
const result = fileSchema.safeParse(value);
if (!result.success) continue;
const { name, startTime, endTime } = result.data;
const duration = endTime - startTime;
const { name, durationMs } = result.data;
const path = relative(root, resolve(root, name));
if (
!Number.isFinite(duration) ||
duration <= 0 ||
path === "" ||
path === ".." ||
path.startsWith(".." + sep) ||
@@ -46,7 +42,7 @@ export function parseTestDurations(
)
continue;
const key = path.split(sep).join("/");
durations.set(key, Math.max(durations.get(key) ?? 0, duration));
durations.set(key, Math.max(durations.get(key) ?? 0, durationMs));
}
return durations.size ? durations : undefined;
}
@@ -0,0 +1,7 @@
__attribute__((noinline)) int rea_pie_add(int value) {
return value + 7;
}
int rea_pie_entry(void) {
return rea_pie_add(35);
}
@@ -0,0 +1,118 @@
//@category REA Verification
import java.io.ByteArrayInputStream;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Locale;
import com.google.gson.JsonNull;
import com.google.gson.JsonObject;
import ghidra.app.script.GhidraScriptUtil;
import ghidra.app.util.headless.HeadlessScript;
import ghidra.program.model.address.AddressSet;
import ghidra.program.model.listing.Function;
import ghidra.program.model.symbol.SourceType;
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.app.decompiler.DecompileProcess.DisposeState;
import ghidra.util.task.TaskMonitor;
/** Real source-identity cases unavailable through ordinary single-file CLI imports. */
public final class ReaFileOffsetSourceProbe extends HeadlessScript {
@Override
public void run() throws Exception {
var file = GhidraScriptUtil.findScriptByName("ReaGhidraBridge.java");
var bridge = GhidraScriptUtil.getProvider(file).getScriptInstance(file, new PrintWriter(System.out, true));
bridge.set(state, monitor, new PrintWriter(System.out, true));
var space = bridge.getClass().getDeclaredField("sessionDefaultAddressSpace");
space.setAccessible(true);
space.set(null, currentProgram.getAddressFactory().getDefaultAddressSpace());
var identity = bridge.getClass().getDeclaredField("admittedTargetSha256");
identity.setAccessible(true);
identity.set(bridge, currentProgram.getExecutableSHA256().toLowerCase(Locale.ROOT));
var resolve = bridge.getClass().getDeclaredMethod("resolveDecompileProcedure", String.class, JsonObject.class);
resolve.setAccessible(true);
var reverse = bridge.getClass().getDeclaredMethod("addressToFileOffset", JsonObject.class);
reverse.setAccessible(true);
var memory = currentProgram.getMemory();
var original = memory.getAllFileBytes().get(0);
byte[] bytes = new byte[Math.toIntExact(original.getSize())];
require(original.getOriginalBytes(0, bytes) == bytes.length, "complete fixture source");
byte[] auxiliary = new byte[0x1000];
auxiliary[0x910] = (byte)0xc3;
var foreign = memory.createFileBytes("auxiliary.bin", 0, auxiliary.length,
new ByteArrayInputStream(auxiliary), monitor);
var address = toAddr(0x140100000L);
memory.createInitializedBlock("auxiliary", address, foreign, 0, auxiliary.length, false);
var entry = address.add(0x910);
currentProgram.getFunctionManager().createFunction("foreign_only", entry,
new AddressSet(entry), SourceType.USER_DEFINED);
rejected(resolve, bridge, "no loaded source mapping", "file-offset:0x910", new JsonObject());
rejected(reverse, bridge, "0 authoritative target-file mappings", reverseInput("0x140100910"));
// A source object's declared original-file start does not make its local
// offsets a complete, digest-matched target representation.
var partial = memory.createFileBytes("partial.bin", 0x200, 0x200,
new ByteArrayInputStream(bytes, 0x200, 0x200), monitor);
memory.createInitializedBlock("partial", toAddr(0x140300000L), partial, 0, 0x200, false);
rejected(reverse, bridge, "0 authoritative target-file mappings", reverseInput("0x140300010"));
var selected = (Function)resolve.invoke(bridge, "file-offset:0x210", new JsonObject());
require(selected.getEntryPoint().equals(toAddr(0x140001010L)), "original target selected despite other sources");
var mapped = (JsonObject)reverse.invoke(bridge, reverseInput("0x140001010"));
require(mapped.get("file_offset").getAsLong() == 0x210, "original target round trip");
// Inject backend failures after real source/function resolution. This
// verifies context propagation, not a naturally failing decompiler.
var pseudocode = bridge.getClass().getDeclaredMethod("procedurePseudocode", JsonObject.class);
pseudocode.setAccessible(true);
var decompiler = bridge.getClass().getDeclaredField("decompiler");
decompiler.setAccessible(true);
JsonObject request = new JsonObject();
request.add("document", JsonNull.INSTANCE);
request.addProperty("procedure", "file-offset:0x210");
for (DisposeState disposition : new DisposeState[] { DisposeState.NOT_DISPOSED, DisposeState.DISPOSED_ON_CANCEL }) {
decompiler.set(bridge, new DecompInterface() {
@Override
public DecompileResults decompileFunction(Function function, int timeout, TaskMonitor taskMonitor) {
return new DecompileResults(function, currentProgram.getLanguage(),
currentProgram.getCompilerSpec(), null, "Injected backend failure", null, disposition);
}
});
Throwable failure = rejected(pseudocode, bridge,
"Selector file-offset:0x210 mapped to analysis address 0x140001010 and procedure 0x140001010", request);
var code = failure.getClass().getDeclaredField("code");
code.setAccessible(true);
require(code.get(failure).equals(disposition == DisposeState.DISPOSED_ON_CANCEL
? "decompile_cancelled" : "decompile_failed"), "backend failure code retained");
}
var duplicate = memory.createFileBytes("duplicate.bin", 0, bytes.length,
new ByteArrayInputStream(bytes), monitor);
memory.createInitializedBlock("duplicate", toAddr(0x140201000L), duplicate, 0x200, 0x200, false);
rejected(resolve, bridge, "multiple loaded mappings", "file-offset:0x210", new JsonObject());
println("REA_FILE_OFFSET_SOURCE_OK foreign/partial rejected; original round trip; duplicate ambiguity; injected backend failure context retained");
}
private JsonObject reverseInput(String address) {
JsonObject params = new JsonObject();
params.add("document", JsonNull.INSTANCE);
params.addProperty("address", address);
return params;
}
private Throwable rejected(Method method, Object bridge, String reason, Object... arguments) throws Exception {
try {
method.invoke(bridge, arguments);
} catch (InvocationTargetException rejected) {
require(rejected.getCause().getMessage().contains(reason), "expected mapping failure: " + reason);
return rejected.getCause();
}
throw new AssertionError("Expected mapping failure: " + reason);
}
private void require(boolean condition, String message) {
if (!condition) throw new AssertionError(message);
}
}
+240
View File
@@ -0,0 +1,240 @@
import { expect, it } from "vitest";
import {
compareTestSelection,
planTestSelection,
} from "../../scripts/ci/test-selection.mjs";
const sqliteSource = "src/sqlite/SqliteDatabaseProvider.test.ts";
const sqliteBoundary = "tests/boundary/sqlite/sqliteDatabaseProvider.test.ts";
const adbSource = "src/adb/AdbProvider.test.ts";
const adbBoundary =
"tests/boundary/providers/adb/adbBugreportPublication.test.ts";
const cliBoundary = "tests/boundary/cli/apktoolCli.test.ts";
const mcpBoundary = "tests/boundary/mcp/apktoolMcp.test.ts";
const processBoundary =
"tests/boundary/process/processCaptureLifecycle.test.ts";
const acceptance = "tests/acceptance/tools/localToolContracts.test.ts";
const composition = "tests/composition/directAnalysisCleanup.test.ts";
const evaluation = "tests/evaluation/testDiscovery.test.ts";
const passed = (name: string) => ({ name, durationMs: 10, state: "passed" });
it("unions affected leaf owners, keeps changed tests and retains unowned integration tests", () => {
const plan = planTestSelection(
["src/sqlite/SqliteDatabaseProvider.ts", adbBoundary],
"pull_request",
"fast",
);
const inventory = [
sqliteSource,
sqliteBoundary,
adbSource,
adbBoundary,
cliBoundary,
mcpBoundary,
processBoundary,
acceptance,
composition,
evaluation,
];
const report = compareTestSelection(plan, inventory, inventory.map(passed));
expect(plan).toMatchObject({
mode: "candidate",
owners: ["adb", "sqlite"],
});
expect(report.selected).toEqual(
expect.arrayContaining([
sqliteSource,
sqliteBoundary,
adbSource,
adbBoundary,
cliBoundary,
mcpBoundary,
processBoundary,
acceptance,
composition,
evaluation,
]),
);
expect(report.omitted).toEqual([]);
});
it("omits only unrelated owned tests while retaining dynamic boundary suites", () => {
const plan = planTestSelection(
["src/sqlite/SqliteDatabaseProvider.ts"],
"pull_request",
"fast",
);
const inventory = [
sqliteSource,
sqliteBoundary,
adbSource,
adbBoundary,
cliBoundary,
mcpBoundary,
processBoundary,
acceptance,
composition,
evaluation,
];
const report = compareTestSelection(plan, inventory, inventory.map(passed));
expect(report.selected).toEqual(
expect.arrayContaining([
sqliteSource,
sqliteBoundary,
cliBoundary,
mcpBoundary,
processBoundary,
acceptance,
composition,
evaluation,
]),
);
expect(report.omitted).toEqual([adbSource, adbBoundary]);
expect(report.complete).toBe(true);
});
it.each([
["unowned shared source", "src/application/Workflow.ts"],
["fixture input", "tests/conformance/c/fixture.c"],
["configuration", "vitest.config.ts"],
["documentation", "README.md"],
["CLI implementation", "src/cli/apktoolCommands.ts"],
["MCP implementation", "src/server/registerSqliteTools.ts"],
["process ownership", "src/process/ProcessOwnership.ts"],
["acceptance test", acceptance],
["composition test", composition],
])("uses full fallback for %s changes", (_description, path) => {
expect(planTestSelection([path], "pull_request", "fast")).toMatchObject({
mode: "full",
owners: [],
changedTests: [],
});
});
it("uses full fallback outside ordinary fast pull requests or without diff paths", () => {
expect(
planTestSelection(["src/sqlite/SqliteDatabaseProvider.ts"], "push", "fast")
.mode,
).toBe("full");
expect(
planTestSelection(
["src/sqlite/SqliteDatabaseProvider.ts"],
"pull_request",
"full",
).mode,
).toBe("full");
expect(planTestSelection([], "pull_request", "fast").mode).toBe("full");
});
it("keeps new changed tests and falls back when a changed test was removed", () => {
const addedTest = "src/sqlite/NewDatabaseRule.test.ts";
const candidate = planTestSelection([addedTest], "pull_request", "fast");
expect(candidate.changedTests).toEqual([addedTest]);
const inventory = [addedTest, adbSource];
expect(
compareTestSelection(candidate, inventory, inventory.map(passed)).selected,
).toContain(addedTest);
const removedTest = "src/sqlite/RemovedDatabaseRule.test.ts";
const report = compareTestSelection(
planTestSelection([removedTest], "pull_request", "fast"),
[sqliteSource, adbSource],
[passed(sqliteSource), passed(adbSource)],
);
expect(report.plan.mode).toBe("full");
expect(report.selected).toEqual([adbSource, sqliteSource]);
expect(report.omitted).toEqual([]);
});
it("marks incomplete inventories and nonterminal outcomes as incomplete", () => {
const plan = planTestSelection(
["src/sqlite/SqliteDatabaseProvider.ts"],
"pull_request",
"fast",
);
const inventory = [sqliteSource, adbSource];
const completeResults = inventory.map(passed);
expect(
compareTestSelection(plan, inventory, [passed(sqliteSource)]).complete,
).toBe(false);
expect(
compareTestSelection(plan, inventory, [
...completeResults,
passed("tests/evaluation/unexpected.test.ts"),
]),
).toMatchObject({
complete: false,
unexpected: ["tests/evaluation/unexpected.test.ts"],
});
expect(
compareTestSelection(plan, inventory, [
...completeResults,
passed(adbSource),
]),
).toMatchObject({ complete: false, duplicateFiles: 1 });
for (const state of ["pending", "skipped"])
expect(
compareTestSelection(plan, inventory, [
passed(sqliteSource),
{ name: adbSource, durationMs: 0, state },
]),
).toMatchObject({ complete: false, unobserved: [adbSource] });
});
it("reports a failure from an omitted test, including collection or module failures", () => {
const plan = planTestSelection(
["src/sqlite/SqliteDatabaseProvider.ts"],
"pull_request",
"fast",
);
const report = compareTestSelection(
plan,
[sqliteSource, adbSource],
[passed(sqliteSource), { name: adbSource, durationMs: 1, state: "failed" }],
);
expect(report.omitted).toContain(adbSource);
expect(report.missedFailures).toEqual([adbSource]);
});
it("rejects plans that name unowned owners or malformed changed tests", () => {
const plan = planTestSelection(
["src/sqlite/SqliteDatabaseProvider.ts"],
"pull_request",
"fast",
);
expect(() =>
compareTestSelection(
{ ...plan, owners: ["unknown"] },
[sqliteSource],
[passed(sqliteSource)],
),
).toThrow("Invalid observational test selection plan");
expect(() =>
compareTestSelection(
{ ...plan, changedTests: ["src/sqlite/not-a-test.ts"] },
[sqliteSource],
[passed(sqliteSource)],
),
).toThrow("Invalid observational test selection plan");
});
it("uses full fallback when a changed owner has no executable tests", () => {
const report = compareTestSelection(
planTestSelection(
["src/sqlite/SqliteDatabaseProvider.ts"],
"pull_request",
"fast",
),
[adbSource],
[passed(adbSource)],
);
expect(report.plan.mode).toBe("full");
expect(report.omitted).toEqual([]);
});
@@ -0,0 +1,90 @@
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import { buildNativeAotFixture } from "../../../scripts/lib/nativeaot-build.mjs";
const options = {
dotnet: "dotnet",
project: "fixture.csproj",
runtime: "win-x64",
output: "fixture-output",
};
it("restores the exact publish target before compiling without network restore", async () => {
const run = vi.fn<(command: string, args: string[]) => void>();
await buildNativeAotFixture(run, options);
expect(run).toHaveBeenCalledTimes(2);
const restore = run.mock.calls[0]?.[1] ?? [];
const publish = run.mock.calls[1]?.[1] ?? [];
expect(restore[0]).toBe("restore");
expect(publish[0]).toBe("publish");
expect(publish).toContain("--no-restore");
for (const argument of [
options.project,
options.runtime,
"-p:Configuration=Release",
`-p:BaseIntermediateOutputPath=${join(options.output, "obj")}/`,
]) {
expect(restore).toContain(argument);
expect(publish).toContain(argument);
}
});
it("retries without logging credential-bearing restore diagnostics", async () => {
const logged = vi.spyOn(console, "error").mockImplementation(() => {});
const timeout = Object.assign(
new Error(
"dotnet restore ETIMEDOUT https://fixture-user:fixture-password@feed.invalid/v3/index.json",
),
{
code: "ETIMEDOUT",
},
);
const run = vi
.fn<(command: string, args: string[]) => void>()
.mockImplementationOnce(() => {
throw timeout;
});
try {
await buildNativeAotFixture(run, options);
expect(
logged.mock.calls.flat().every((value) => typeof value === "string"),
).toBe(true);
expect(logged.mock.calls.flat().join("\n")).not.toContain(
"fixture-password",
);
} finally {
logged.mockRestore();
}
expect(run.mock.calls.map((call) => call[1][0])).toEqual([
"restore",
"restore",
"publish",
]);
});
it("stops after two restore failures and preserves the final diagnostic", async () => {
const failure = new Error("NuGet source unavailable");
const run = vi.fn<(command: string, args: string[]) => void>(() => {
throw failure;
});
await expect(buildNativeAotFixture(run, options)).rejects.toMatchObject({
message: "NativeAOT dependency restore failed after two attempts",
cause: failure,
});
expect(run).toHaveBeenCalledTimes(2);
expect(run.mock.calls.every((call) => call[1][0] === "restore")).toBe(true);
});
it("does not retry compilation failures", async () => {
const failure = new Error("Native compiler rejected the fixture");
const run = vi
.fn<(command: string, args: string[]) => void>()
.mockImplementationOnce(() => {})
.mockImplementationOnce(() => {
throw failure;
});
await expect(buildNativeAotFixture(run, options)).rejects.toBe(failure);
expect(run).toHaveBeenCalledTimes(2);
});
@@ -0,0 +1,150 @@
import { execFile } from "node:child_process";
import { readdir, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { expect, it } from "vitest";
import {
nativeAotCommandError,
supportsNativeAotFixtureHost,
} from "../../../scripts/lib/nativeaot-build.mjs";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
const generator = fileURLToPath(
new URL("../../../scripts/generate-nativeaot-fixtures.mjs", import.meta.url),
);
const supportedHost = supportsNativeAotFixtureHost(
process.platform,
process.arch,
);
it.skipIf(!supportedHost).each(["restore", "publish"])(
"redacts authenticated feed URLs from the uncaught %s diagnostic",
async (operation) => {
const directory = await createTestTempDirectory(
"rea-nativeaot-diagnostic-",
);
const hook = join(directory, "child-output.cjs");
await writeFile(
hook,
`
const child = require("node:child_process");
child.spawnSync = (_command, args) => {
if (args[0] === "--version") return { status: 0, stdout: "8.0.416\\n", stderr: "" };
if (args[0] === ${JSON.stringify(operation)}) return {
status: 1,
error: new Error("failed HTTPS://error-user:error-secret@feed.invalid/v3/index.json"),
stdout: "NU1301 https://stdout-user:stdout-secret@feed.invalid/v3/index.json\\n",
stderr: "source https://stderr-user:stderr-secret%40suffix@feed.invalid/v3/index.json\\nkeep https://feed.invalid/path/user@name?label=token-name\\n"
};
return { status: 0, stdout: "", stderr: "" };
};
require("node:module").syncBuiltinESMExports();
`,
"utf8",
);
const failure = await promisify(execFile)(
process.execPath,
["--require", hook, generator],
{
env: {
...process.env,
REA_NATIVEAOT_FIXTURE_ROOT: join(directory, "output"),
},
timeout: 15_000,
},
).then(
() => {
throw new Error("Expected fixture generation to fail");
},
(error: { stderr: string; code: number }) => error,
);
expect(failure.code).toBe(1);
for (const secret of [
"error-user",
"error-secret",
"stdout-user",
"stdout-secret",
"stderr-user",
"stderr-secret",
])
expect(failure.stderr).not.toContain(secret);
expect(failure.stderr).toContain("NU1301");
expect(failure.stderr).toContain("feed.invalid/v3/index.json");
expect(failure.stderr).toContain(
"https://feed.invalid/path/user@name?label=token-name",
);
if (operation === "restore") {
expect(failure.stderr).toContain("attempt 2/2");
expect(failure.stderr).toContain("failed after two attempts");
expect(failure.stderr).toContain("[cause]");
}
},
);
it.skipIf(supportedHost)(
"refuses unsupported fixture hosts before tooling or output creation",
async () => {
const directory = await createTestTempDirectory("rea-nativeaot-host-");
const hook = join(directory, "unexpected-child.cjs");
await writeFile(
hook,
`const child = require("node:child_process");
child.spawnSync = () => { throw new Error("Unexpected fixture tooling invocation"); };
require("node:module").syncBuiltinESMExports();
`,
"utf8",
);
const failure = await promisify(execFile)(
process.execPath,
["--require", hook, generator],
{
env: {
...process.env,
REA_NATIVEAOT_FIXTURE_ROOT: join(directory, "output"),
},
timeout: 15_000,
},
).then(
() => {
throw new Error("Expected unsupported fixture host refusal");
},
(error: { stderr: string; code: number }) => error,
);
expect(failure.code).toBe(1);
expect(failure.stderr).toContain("requires Linux or Windows x64");
expect(failure.stderr).not.toContain(
"Unexpected fixture tooling invocation",
);
expect(await readdir(directory)).toEqual(["unexpected-child.cjs"]);
},
);
it("preserves diagnostics without transport authentication exactly", () => {
const error = nativeAotCommandError(
"C:/token projects/dotnet.exe",
"restore",
{
status: 1,
stdout: "NU1301 https://feed.invalid/user@name?label=password\n",
stderr: "project C:/token projects/fixture.csproj unavailable",
},
);
expect(error.message).toBe(
"NativeAOT fixture lane requires/action failed C:/token projects/dotnet.exe restore: exit 1\nNU1301 https://feed.invalid/user@name?label=password\nproject C:/token projects/fixture.csproj unavailable",
);
expect(error.cause).toBeUndefined();
});
it("treats HTTP backslashes as path separators rather than authentication", () => {
const output = String.raw`https://feed.invalid\path\user@name`;
expect(new URL(output).username).toBe("");
const error = nativeAotCommandError("dotnet", "restore", {
status: 1,
stdout: output,
});
expect(error.message).toContain(output);
});
+1
View File
@@ -131,6 +131,7 @@ describe its recorded investigation; update them only when repeating that work.
- `public/showcase/index.html`: the case-study index.
- `public/blog/index.html`: articles about reconstruction methods, ports, mods and reverse engineering.
- `public/blog/touhou-reconstruction/index.html`: a personal essay about agent autonomy, oracle quality, knowledge carried between TH08 and TH095, PC-98 TH04, and the path from exact reconstruction to readable source and modern ports.
- `public/blog/reverse-engineering-models/index.html`: an essay on general models, compiler specialization and learning useful reconstruction experiments, with historical evidence from TH08, TH095, TH04 and TH09. Model attribution and verification scope are in [evidence/reverse-engineering-models.md](evidence/reverse-engineering-models.md).
- `public/showcase/aegis/index.html`: Aegis's Android login-code calculation, with an adjustable clock and reference checks.
- `public/showcase/dx-ball/index.html`: sound-pan investigation and project status.
- `public/showcase/notion/index.html`: Notion's Electron clipboard bridge and rich clipboard format.
@@ -0,0 +1,166 @@
# General models and specialist models: article evidence
Supporting record for `public/blog/reverse-engineering-models/index.html`.
The article is published from the English source and translated through the
website's gettext catalogs.
## Research scope
Repository history, maintained source and recorded comparison notes were reviewed
on 13 October 2026. This research did not run game targets, compile reconstruction
candidates or repeat historical runtime checks. Public commit availability was
checked through GitHub CLI.
The evidence describes specific historical functions and their recorded checks.
Source presence, compiler exactness, complete-product builds and runtime agreement
remain distinct. The proposed specialist/RL comparisons are future experiments;
no trained specialist was evaluated for this article.
## Model attribution
- The project operator identifies `gpt-dots` as **GPT-6-Astra**.
- `gpt-web` is a changing label for the model used at the time. The operator
describes then-latest models, usually xhigh, commonly GPT-5.6-Sol earlier and
GPT-6-Sol recently. Do not assign all such commits a single model or infer
individual identities from a release date.
- Explicit `gpt-6-sol` and `gpt-6.1-sol` subjects establish the recorded names.
Per-run reasoning effort and full budgets are not uniformly recorded.
- `gpt-6-luna-max` records GPT-6-Luna and max effort. It does not establish
parameter count or specialist training.
Later sessions inherited earlier source, diagnostics and failed experiments.
Historical before/after closures support the essay's questions; they are not
controlled estimates of model-size effects.
## TH09 Type18/24 attack update
Earlier source-context review:
[GPT-6-Sol, 26 September](https://github.com/N0zoM1z0/th09/commit/de63c0f9121963f3a6b1ab5471c4878625fd3425).
Closing reconstruction:
[GPT-6.1-Sol, 10 October](https://github.com/N0zoM1z0/th09/commit/52929ed7c299f1a5f29cc13964e22510145fd616).
The earlier candidate was 1,435 bytes against a 1,436-byte target. Matching the
overall frame size concealed different homes for returned vectors and real
collision storage. Declaration and helper-visibility controls did not close it.
The closing session introduces an ordinary XYZ destination assignment, consumes
real returned vectors directly and groups six actual publication/rotation
operations into one comma-sequenced full expression. Keeping the real collision
point at function scope preserves separate storage. The seventh warm candidate
matches, followed by a maintained-path canonical cold comparison of all 1,436
bytes and 44 encoded fields. Original class/source spelling and later runtime
gates remain separate.
The article's four-row table groups selected lessons from seven candidates. It
does not enumerate every build or reproduce a complete source listing.
## TH09 Front callback
Earlier diagnostic checkpoint:
[GPT-6.1-Sol, 2 October](https://github.com/N0zoM1z0/th09/commit/f8d12d72aaf02212ee1b585a8f3f86d7ed000269).
Full graph closure:
[GPT-6-Astra (`gpt-dots`), 6 October](https://github.com/N0zoM1z0/th09/commit/da33a646221198ceb7f3df2b96e2ca0e9b2f705f).
Accepted replay:
[6 October follow-up](https://github.com/N0zoM1z0/th09/commit/5f825b28a862f8e1a6d4aea2f78a42f04c09a1c0).
The earlier maintained owner was 1,510 bytes versus the 1,491-byte target. Its
prefix and suffix matched only as independently positioned diagnostic regions;
they did not establish a complete-function match.
A common outer continuation places completion and frame dispatch in complementary
arms with one final frame increment/return. Within this graph, the side/stage
decision recovers target block sharing. The record establishes the complete
1,491-byte owner, 107 independently bound fields, 384 instructions and 42 calls.
The follow-up records accepted forced-recompile replay for `da33a646`.
Bounded trace models are static checks, not game-runtime validation.
## TH095 identifier-bucket reuse
[Supervisor::LoadDat, 2 September](https://github.com/N0zoM1z0/th095/commit/26bb64bdb093a13f73abd0ee8ae7ce199ee4658f).
Under observed VC7.1 build 3077, declaration order alone did not recover the real
local homes. Earlier background work established useful identifier buckets.
Backing the real file-size scalar with `averagedPanLocal12` places it at
`EBP-0x04` and the genuine 128-byte buffer at `EBP-0x88`. All 151 bytes and 15
relocations close.
This is compiler-context evidence. It does not establish a universal C++ rule
or uniquely recover the original author's variable name. The `gpt-web` label
does not identify this run's model individually.
## TH09 Luna enemy-spawn closure
[GPT-6-Luna-max, 27 September](https://github.com/N0zoM1z0/th09/commit/1eb843383941d00065622a9e353146c4bb96979e).
An ordinary loop with `continue`/`break`, branch-local RunEcl failure handling
and a shared success tail closes the 373-byte owner. TH08 source supplies the
source-family hypothesis; TH09 establishes the bounds, fields, ABI and callees.
Two independent cold builds and canonical replay are recorded.
The earlier stable 361-byte candidate is `gpt-web`, with an individually
unresolved model identity. This case demonstrates a useful discovery across
model tiers, not a measured specialist or smaller-parameter advantage.
## TH04 compiler diversity
[Player MAIN_0 cohort, 15 September](https://github.com/N0zoM1z0/th04/commit/6dce0a79d5b931f0e2a08475b90e0c33d2321b98).
Natural Turbo C++ 4.0J source produces a 629-byte update/miss contribution and a
334-byte renderer. Expression lifetimes, branch-local stores, declaration order
and a shared firing tail matter. Correct physical linker order avoids an extra
alignment byte. The recorded focused and aggregate replays cover 129 and 208
owners respectively. These are historical owned-extent checks; this checkpoint
does not establish whole-game execution.
TH08 uses VC7, while TH095/TH09 use VC7.1. Avoid treating every game's source
patterns as output from the same compiler. Shared game authorship supplies a
prior; target-local evidence validates an actual transfer.
## TH08 numerical investigation
[Polar motion and x87 behavior, 13 October](https://github.com/N0zoM1z0/th08/commit/af9cb1d3e839b15a78b47ac1701506083cc82847).
A collision divergence at stage 2 frame 6,825 leads back to a one-ULP
displacement difference at frame 5,573. Historical CRT wrappers retain a wider
x87 return despite storing a rounded float. A distinct i386 case involves x87
precision-control state. The record includes field probes and replay checks for
specified scenarios. Several preceding investigations use the same model; this
is breadth-of-investigation evidence, not a model-switch comparison.
## TH08 floating-literal oracle repair
[Repository-wide audit, 2 September](https://github.com/N0zoM1z0/th08/commit/a393f400786e4cc6cbb1000af0dad3276da12c78).
Literal payload checks previously depended on optional explicit `data_hex`
attestation. The fix decodes and target-checks every VC7 `__real@...` relocation.
The follow-up audits 1,548 relocations and corrects 12 stale rows in five already
accepted units. One source change corrects a bullet-spread increment from
`1.0f` to `32.0f`.
Affected-unit replay and a complete 1,106-unit cold replay are recorded after
repair. This defect is separate from the later one-ULP numerical investigation.
The RL consequence in the article is an inference: a reward based on the earlier
check could reinforce wrong source. The record does not describe an actual RL
exploit or trained model doing so.
## TH09 target-sized replay-save candidate
[Replay-save closure, 2 October](https://github.com/N0zoM1z0/th09/commit/ab72fd77728293fc0beacfc656ca701b0e266016).
The earlier candidate already had the target's 1,825-byte length but 467 complete
comparison differences. Shared index identity, loop structure and the real
cancel/overwrite continuation close the complete owner. The example supports
using size as a diagnostic rather than final acceptance.
## Related research
The article links these primary sources:
- [LLM4Decompile, EMNLP 2024](https://aclanthology.org/2024.emnlp-main.203/).
- [AutoDecompiler, June 2026 preprint](https://arxiv.org/html/2606.16162v1).
- [When LLM Decompilers Recompile More and Preserve Less, September 2026 preprint](https://arxiv.org/html/2609.05370v1).
These works concern specialized decompilation, feedback-based refinement and
functional checking. Their published setups do not establish a winner for the
article's proposed historical-compiler reconstruction experiment. Preserve that
objective distinction when expanding the discussion.
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More