Compare commits

..
221 changed files with 38903 additions and 85555 deletions

No files matched your search

+3 -3
View File
@@ -68,9 +68,9 @@ closing-issue reporters. Grouping preserves all those credits in one line.
When Highlights, detailed entries or migrations reference the same work,
put its thanks in one of those entries, not all of them. Include issue links.
Direct-commit bullets carry the commit link and known human credit.
Exclude GitHub bots and known agent accounts. REA's observed Anthropic, Cursor
and Codex co-author trailers resolve to `@claude`, `@cursoragent` and `@codex`
with GitHub type `User`; the credit policy excludes them while retaining their metadata.
Exclude GitHub bots and known agent accounts. REA's observed Anthropic and
Cursor co-author trailers resolve to `@claude` and `@cursoragent` with GitHub
type `User`; the credit policy excludes them while retaining their metadata.
At the REA maintainer's request, omit `@morluto` self-thanks while preserving
authorship in the inventory. Other human contributors remain eligible.
Use explicit GitHub links for cross-repository references to avoid ambiguous
@@ -1,26 +0,0 @@
name: Prepare qualified release inputs
description: Restore and validate the exact qualified tarball and its verifier runtime
outputs:
version:
description: Package version validated against the source and qualified manifest
value: ${{ steps.package.outputs.version }}
tarball:
description: Absolute path to the validated qualified npm tarball
value: ${{ steps.package.outputs.tarball }}
runs:
using: composite
steps:
- name: Download the exact qualified package
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: verified-release-package
path: .cache/verified-package/
- name: Verify qualified package provenance and digest
id: package
shell: bash
run: node scripts/ci/verified-package.mjs validate .cache/verified-package
- name: Restore qualified verifier runtime
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rea-test-runtime
path: .
-41
View File
@@ -1,41 +0,0 @@
name: Publish MCP Registry metadata
description: Validate and publish one released server.json, preserving an identical existing version
inputs:
directory:
description: Directory containing the released server.json
default: .
runs:
using: composite
steps:
- name: Install MCP Registry publisher
shell: bash
working-directory: ${{ inputs.directory }}
env:
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256_LINUX_AMD64: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
run: |
archive="mcp-publisher_linux_amd64.tar.gz"
curl -fsSLo "${archive}" \
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/${archive}"
echo "${MCP_PUBLISHER_SHA256_LINUX_AMD64} ${archive}" | sha256sum --check --strict
tar xzf "${archive}" mcp-publisher
chmod +x mcp-publisher
- name: Validate MCP Registry metadata
shell: bash
working-directory: ${{ inputs.directory }}
run: ./mcp-publisher validate
- name: Check existing registry version
id: registry
shell: bash
working-directory: ${{ inputs.directory }}
run: node "${GITHUB_ACTION_PATH}/registry-state.mjs"
- name: Authenticate to MCP Registry
if: steps.registry.outputs.exists != 'true'
shell: bash
working-directory: ${{ inputs.directory }}
run: ./mcp-publisher login github-oidc
- name: Publish MCP Registry metadata
if: steps.registry.outputs.exists != 'true'
shell: bash
working-directory: ${{ inputs.directory }}
run: ./mcp-publisher publish
@@ -1,4 +0,0 @@
export function registryPublicationState(
server: Readonly<{ name: string; version: string } & Record<string, unknown>>,
request?: (url: URL, init: RequestInit) => Promise<Response>,
): Promise<"present" | "absent">;
@@ -1,89 +0,0 @@
import assert from "node:assert/strict";
import { appendFile, readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
/** Exact-version routes currently return false 404s; enumerate the filtered listing. */
export async function registryPublicationState(server, request = fetch) {
assert.equal(typeof server?.name, "string");
assert.equal(typeof server?.version, "string");
const cursors = new Set();
let cursor;
const matches = [];
do {
const url = new URL(
"https://registry.modelcontextprotocol.io/v0.1/servers",
);
url.search = new URLSearchParams({
search: server.name,
version: server.version,
include_deleted: "true",
limit: "100",
}).toString();
if (cursor !== undefined) url.searchParams.set("cursor", cursor);
const response = await request(url, {
signal: AbortSignal.timeout(30_000),
});
if (!response.ok)
throw new Error(`Registry lookup failed: HTTP ${response.status}`);
const page = await response.json();
assert(Array.isArray(page?.servers), "Registry response omitted servers");
assert(
page.metadata && typeof page.metadata === "object",
"Registry response omitted metadata",
);
for (const entry of page.servers) {
assert(
typeof entry?.server?.name === "string" &&
typeof entry.server.version === "string",
"Malformed registry server record",
);
if (
entry.server.name === server.name &&
entry.server.version === server.version
)
matches.push(entry);
}
cursor = page.metadata.nextCursor;
if (cursor !== undefined) {
assert(
typeof cursor === "string" && cursor.length > 0 && !cursors.has(cursor),
"Registry returned a non-advancing cursor",
);
cursors.add(cursor);
}
} while (cursor !== undefined);
assert(
matches.length <= 1,
"Registry returned ambiguous exact-version records",
);
if (matches.length === 0) return "absent";
assert.equal(
matches[0]._meta?.["io.modelcontextprotocol.registry/official"]?.status,
"active",
"Existing registry version is not active",
);
assert.deepEqual(
matches[0].server,
server,
"Existing registry version differs from released metadata",
);
return "present";
}
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
) {
const state = await registryPublicationState(
JSON.parse(await readFile("server.json", "utf8")),
);
assert(process.env.GITHUB_OUTPUT, "GITHUB_OUTPUT is required");
await appendFile(
process.env.GITHUB_OUTPUT,
`exists=${state === "present"}\n`,
);
process.stdout.write(
`Registry version is ${state}; ${state === "present" ? "preserving identical published metadata" : "publication may proceed"}.\n`,
);
}
+893 -23
View File
@@ -1,15 +1,599 @@
name: CI
run-name: "Temporary issue 3543 diagnostic; never merge this branch"
on:
push:
branches: [main]
pull_request:
branches: [main, "release/*"]
schedule:
- cron: "23 0-2,4-23 * * *"
- cron: "23 3 * * *"
workflow_dispatch:
workflow_call:
inputs:
source_sha:
description: Exact qualification source; must match the caller revision.
required: true
type: string
permissions:
contents: read
concurrency:
group: diagnose-3543-${{ github.ref }}
cancel-in-progress: false
# 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 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 == 'pull_request' && inputs.source_sha == '' && !contains(github.event.pull_request.labels.*.name, 'ci:full') }}
env:
HUSKY: "0"
jobs:
diagnose-macos:
name: Diagnose original macOS CLI deadline
changes:
name: Classify changes
runs-on: ubuntu-24.04
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 }}
reverse_tools: ${{ steps.plan.outputs.reverse_tools }}
sqlite: ${{ steps.plan.outputs.sqlite }}
runtime: ${{ steps.plan.outputs.runtime }}
package: ${{ steps.plan.outputs.package }}
apple: ${{ steps.plan.outputs.apple }}
fixtures: ${{ steps.plan.outputs.fixtures }}
readiness: ${{ steps.plan.outputs.readiness }}
docs: ${{ steps.plan.outputs.docs }}
workflows: ${{ steps.plan.outputs.workflows }}
browser: ${{ steps.plan.outputs.browser }}
binary_layout: ${{ steps.plan.outputs.binary_layout }}
evm: ${{ steps.plan.outputs.evm }}
javascript_recovery: ${{ steps.plan.outputs.javascript_recovery }}
recorded_crash: ${{ steps.plan.outputs.recorded_crash }}
web_network: ${{ steps.plan.outputs.web_network }}
web_runtime: ${{ steps.plan.outputs.web_runtime }}
web_source_map: ${{ steps.plan.outputs.web_source_map }}
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@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
ref: ${{ inputs.source_sha || github.sha }}
fetch-depth: 0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Bind qualification to the caller revision
if: inputs.source_sha != ''
env:
SOURCE_SHA: ${{ inputs.source_sha }}
run: |
test "${SOURCE_SHA}" = "${GITHUB_SHA}"
test "${SOURCE_SHA}" = "$(git rev-parse HEAD)"
- name: Find a verified ancestor for push selection
id: baseline
if: github.event_name == 'push' && inputs.source_sha == ''
env:
GH_TOKEN: ${{ github.token }}
HEAD_SHA: ${{ github.sha }}
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:
BASE_SHA: ${{ steps.baseline.outputs.base || github.event.pull_request.base.sha }}
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() && needs.changes.outputs.tier != 'reused' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Require successful change classification
env:
CHANGES_RESULT: ${{ needs.changes.result }}
run: test "${CHANGES_RESULT}" = "success"
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- name: Restore static task cache
if: needs.changes.outputs.code == 'true'
id: static-cache
uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .turbo/cache/
key: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-${{ github.sha }}
# Turbo validates each task's inputs after restoring this snapshot.
restore-keys: rea-static-v1-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.nvmrc', '.npmrc', 'package-lock.json') }}-
- 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=
- name: Record static task source and execution identity
continue-on-error: true
if: needs.changes.outputs.code == 'true'
env:
CI_PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: npm run ci:static-context
- if: needs.changes.outputs.code == 'true'
run: npm run check:ci
- if: needs.changes.outputs.code == 'true'
run: npm run verify:test-discovery
- name: Check LLDB producer retention and launch contracts
if: needs.changes.outputs.code == 'true'
run: npm run test:python
- if: needs.changes.outputs.code != 'true'
run: npm run format:check
- name: Save validated static task cache
if: >-
success() && needs.changes.outputs.code == 'true' &&
steps.npm-cache.outputs.can-save == 'true' &&
steps.static-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
with:
path: .turbo/cache/
key: ${{ steps.static-cache.outputs.cache-primary-key }}
- name: Upload complete static diagnostics
if: always() && needs.changes.outputs.code == 'true'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: static-check-reports
path: .cache/static-checks/
include-hidden-files: true
if-no-files-found: ignore
retention-days: 7
test-source:
name: Source tests
needs: changes
if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'fast'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- name: Run source tests without waiting for build
run: npm run test:fast -- --reporter=default --reporter=blob
env:
VITEST_BLOB_LABEL: source
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ !cancelled() }}
with:
name: vitest-report-source
path: .vitest/blob/*
include-hidden-files: true
if-no-files-found: error
retention-days: 7
test-shard:
name: Test shard (${{ matrix.shard }})
needs: [changes, build]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
# Failed PRs release the remaining matrix slots; qualification retains
# every shard's diagnostics and coverage. Passing PRs run the full suite.
fail-fast: ${{ github.event_name == 'pull_request' && needs.changes.outputs.tier == 'fast' }}
matrix:
# Fast feedback spends the same six test jobs on one source group and
# five built groups; full qualification retains six complete shards.
shard: ${{ fromJSON(needs.changes.outputs.tier == 'fast' && '["1/5","2/5","3/5","4/5","5/5"]' || '["1/6","2/6","3/6","4/6","5/6","6/6"]') }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rea-test-runtime
path: .
- name: Run complete portable suite with coverage
if: needs.changes.outputs.tier == 'full'
run: npm run test:ci:shard:run -- --shard=${{ matrix.shard }}
env:
REA_CI_TEST_HISTORY: .cache/ci-test-history/test-results.json
- name: Run built-dependent tests for fast feedback
if: needs.changes.outputs.tier == 'fast'
run: npm run test:built -- --reporter=default --reporter=blob --shard=${{ matrix.shard }}
env:
VITEST_BLOB_LABEL: built
REA_CI_TEST_HISTORY: .cache/ci-test-history/test-results.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ !cancelled() }}
with:
name: vitest-report-built-${{ strategy.job-index }}
path: .vitest/blob/*
include-hidden-files: true
if-no-files-found: error
overwrite: true
retention-days: 7
reverse-tools-quality:
name: Reverse tools coverage and mutation
needs: [changes, static]
if: needs.changes.outputs.reverse_tools == 'true'
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- name: Run reverse tools tests with coverage
run: npm run test:coverage:reverse-tools
- name: Report reverse tools mutation results
run: npm run test:mutation:reverse-tools
build:
name: Build package
needs: changes
# 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
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- name: Reuse portable build outputs across runs
uses: ./.github/actions/portable-build-cache
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 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: node scripts/ci/test-history.mjs
- name: Download previous timing report
if: steps.history.outputs.run-id != ''
continue-on-error: true
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: test-timings
run-id: ${{ steps.history.outputs.run-id }}
github-token: ${{ github.token }}
path: .cache/ci-test-history
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: rea-test-runtime
path: |
dist/
skills/
.cache/mcp-tool-catalog.json
.cache/ci-test-history/test-results.json
docs/public/product-catalog.json
docs/verification/managed-conformance-*.json
include-hidden-files: true
if-no-files-found: error
retention-days: 7
# test:prepare caches only portable JS, skill, and catalog outputs.
# Native package jobs still run their build and prepack checks; Turbo
# restores matching outputs or rebuilds when their inputs differ.
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: rea-build-cache
path: .turbo/cache/
include-hidden-files: true
if-no-files-found: error
retention-days: 7
sqlite-runtime:
# 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-source, test-shard]
if: ${{ !cancelled() && needs.changes.outputs.tier != 'reused' }}
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Require successful change classification
env:
CHANGES_RESULT: ${{ needs.changes.result }}
run: test "${CHANGES_RESULT}" = "success"
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
if: needs.changes.outputs.code == 'true' && success()
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
if: needs.changes.outputs.code == 'true' && success()
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
if: needs.changes.outputs.code == 'true' && success()
id: npm-cache
uses: ./.github/actions/npm-cache
- if: needs.changes.outputs.code == 'true' && success()
run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
if: >-
!cancelled() && needs.changes.outputs.code == 'true' &&
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure' ||
(needs.changes.outputs.tier == 'fast' &&
(needs.test-source.result == 'success' || needs.test-source.result == 'failure')))
with:
path: .vitest/blob
pattern: vitest-report-*
merge-multiple: true
- name: Merge coverage, JUnit, and timing reports
id: merge
if: >-
!cancelled() && needs.changes.outputs.code == 'true' &&
needs.changes.outputs.tier == 'full' &&
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure')
run: >-
npm run test:ci:merge --
--coverage.reportsDirectory=coverage
--coverage.reporter=text
--coverage.reporter=json-summary
--coverage.reporter=lcov
--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
id: merge-fast
if: >-
!cancelled() && needs.changes.outputs.code == 'true' &&
needs.changes.outputs.tier == 'fast' &&
(needs.test-shard.result == 'success' || needs.test-shard.result == 'failure' ||
needs.test-source.result == 'success' || needs.test-source.result == 'failure')
run: >-
npm exec -- vitest --merge-reports
--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-timings.json') != ''
env:
SHARD_RESULT: ${{ needs.test-shard.result }}
SOURCE_REQUIRED: ${{ needs.changes.outputs.tier == 'fast' }}
SOURCE_RESULT: ${{ needs.test-source.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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: needs.changes.outputs.code == 'true' && !cancelled() && hashFiles('test-results.json') != ''
with:
name: test-results
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
if: needs.changes.outputs.code == 'true' && needs.changes.outputs.tier == 'full' && success() && needs.test-shard.result == 'success'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: test-timings
path: .cache/ci-test-history/test-results.json
include-hidden-files: true
if-no-files-found: error
retention-days: 30
- name: Require successful test shards
if: ${{ !cancelled() }}
env:
CODE_REQUIRED: ${{ needs.changes.outputs.code }}
SHARD_RESULT: ${{ needs.test-shard.result }}
SOURCE_REQUIRED: ${{ needs.changes.outputs.tier == 'fast' }}
SOURCE_RESULT: ${{ needs.test-source.result }}
run: |
if [ "${CODE_REQUIRED}" = "true" ]; then
test "${SHARD_RESULT}" = "success" || exit 1
test "${SOURCE_REQUIRED}" != "true" || test "${SOURCE_RESULT}" = "success"
fi
conformance-fixtures:
name: Source-built conformance fixtures
needs: [changes, static]
if: needs.changes.outputs.fixtures == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- name: Verify fixtures from a clean checkout
run: npm run verify:fixtures
windows-native-build:
name: Build bundled Windows x64 native controls
needs: [changes, static]
if: needs.changes.outputs.runtime == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Prepare cross compiler
run: |
# Slow Azure archive transfers can exhaust the job before compilation.
# Keep the same signed packages and avoid slow HTTP mirror fallback.
for source in /etc/apt/apt-mirrors.txt /etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources; do
if [ -f "$source" ]; then
sudo sed -i 's|azure.archive.ubuntu.com|archive.ubuntu.com|g; s|http://archive.ubuntu.com|https://archive.ubuntu.com|g' "$source"
fi
done
sudo apt-get update
sudo apt-get install --no-install-recommends -y g++-mingw-w64-x86-64
- run: npm run build:windows-native
- run: npm run verify:windows-native:artifact
- run: node scripts/build-windows-native.mjs --inspection-fixture
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: windows-native-inspection-fixture
path: .cache/windows-native-inspection-fixture/
if-no-files-found: error
# The process boundary observations are independent of the addon: they
# report the caller's and child's own token, environment and breakaway
# results from a separate executable. Cross-build it in the lane that
# already has MinGW so the Windows job can observe them too.
- name: Build Windows process boundary fixture
run: |
mkdir -p .cache/windows-process-boundary
x86_64-w64-mingw32-g++ -municode -std=c++17 -O2 -static -Wall -Wextra \
-o .cache/windows-process-boundary/process-boundary.exe \
tests/fixtures/windows/processBoundary.cc -ladvapi32
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: windows-process-boundary-fixture
path: .cache/windows-process-boundary/
if-no-files-found: error
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: windows-native-controls
path: native/windows/build/
if-no-files-found: error
package-e2e:
name: Native package baseline (${{ matrix.platform }} ${{ matrix.architecture }})
needs: [changes, static, build, windows-native-build]
if: needs.changes.outputs.runtime == 'true'
strategy:
fail-fast: false
# The four platform jobs use independent hosted runners; keep each
# runner's existing heap and threadpool limits while avoiding a queued
# second platform wave.
max-parallel: 4
matrix: ${{ fromJSON(needs.changes.outputs.matrix) }}
runs-on: ${{ matrix.os }}
timeout-minutes: 20
env:
NODE_OPTIONS: "--max-old-space-size=1024"
UV_THREADPOOL_SIZE: "2"
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- name: Verify native runtime identity
env:
EXPECTED_CI_PLATFORM: ${{ matrix.platform }}
EXPECTED_CI_ARCHITECTURE: ${{ matrix.architecture }}
run: >-
node -e "const assert = require('node:assert/strict');
assert.equal(process.platform, process.env.EXPECTED_CI_PLATFORM);
assert.equal(process.arch, process.env.EXPECTED_CI_ARCHITECTURE);
console.log(JSON.stringify({ platform: process.platform, architecture: process.arch, node: process.version }));"
- run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rea-build-cache
path: .turbo/cache/
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
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
- run: npm run verify:package
env:
npm_config_cache: ${{ steps.npm-cache.outputs.path }}
REA_VERIFIED_PACKAGE_DIRECTORY: ${{ needs.changes.outputs.tier == 'full' && matrix.platform == 'linux' && matrix.architecture == 'x64' && '.cache/verified-package' || '' }}
- name: Verify real Node Inspector discovery and observation
timeout-minutes: 10
run: npm run verify:inspector
- name: Retain the exact qualified release package
if: needs.changes.outputs.tier == 'full' && matrix.platform == 'linux' && matrix.architecture == 'x64'
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: verified-release-package
path: .cache/verified-package/
include-hidden-files: true
if-no-files-found: error
overwrite: true
retention-days: 7
apple-artifacts:
name: Real Apple artifact CLI/MCP E2E
needs: [changes, static, build]
if: needs.changes.outputs.apple == 'true'
runs-on: macos-15
timeout-minutes: 15
steps:
@@ -21,12 +605,10 @@ jobs:
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- name: Record toolchain and create owned diagnostic directories
run: |
node --version
npm --version
mkdir -p diagnostics/full diagnostics/isolated
git rev-parse HEAD
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rea-build-cache
path: .turbo/cache/
- name: Verify Apple artifact formats and metadata
env:
REA_ARTIFACT_NODE_COMPILE_CACHE: ${{ runner.temp }}/rea-artifact-node-cache
@@ -37,9 +619,7 @@ jobs:
npm run verify:legacy-simulator
npm run verify:apple-dispatch
npm run verify:interface-builder
- name: Original complete Apple boundary family, unchanged deadlines
env:
REA_CI_PROCESS_DIAGNOSTICS: ${{ github.workspace }}/diagnostics/full
- name: Run native Apple and process boundary regressions
run: >-
npm run test:focused --
tests/boundary/filesystem/installScript.test.ts
@@ -61,15 +641,305 @@ jobs:
tests/boundary/process/processCli.test.ts
tests/boundary/mcp/sessionMcp.test.ts
- name: Isolated demangle family to distinguish cross-family load
if: ${{ !cancelled() }}
env:
REA_CI_PROCESS_DIAGNOSTICS: ${{ github.workspace }}/diagnostics/isolated
run: npm run test:focused -- tests/boundary/cli/nativeDemangleCli.test.ts
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ always() }}
reconstruction-readiness:
name: Reconstruction readiness
needs: [changes, static]
if: needs.changes.outputs.readiness == 'true'
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
name: macos-deadline-diagnostics
path: diagnostics/
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- name: Run the public CLI/MCP reconstruction journey
env:
REA_READINESS_REPORT_PATH: readiness-report.json
run: |
set -o pipefail
npm run verify:readiness | tee readiness-summary.json
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: ${{ !cancelled() }}
with:
name: reconstruction-readiness
path: |
readiness-summary.json
readiness-report.json
if-no-files-found: error
retention-days: 30
windows-curated:
name: Windows capability and package lane
needs: [changes, static, build, windows-native-build]
if: needs.changes.outputs.package == 'true'
runs-on: windows-latest
timeout-minutes: 25
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rea-build-cache
path: .turbo/cache/
- run: npm run build:cached
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: windows-native-controls
path: native/windows/build/
- run: npm run verify:windows-native:artifact
- name: Record named Windows host capabilities
run: node scripts/windows-capabilities.mjs | Out-File -Encoding utf8NoBOM windows-capabilities.json
- name: Validate Windows host capability report
run: >-
node -e "const report = JSON.parse(require('node:fs').readFileSync('windows-capabilities.json', 'utf8'));
if (report.platform !== 'win32' || report.architecture !== 'x64')
throw new Error('expected a Windows x64 capability report');"
# Keep this list aligned with tests/process-global/curatedWindowsLane.test.ts,
# which fails when any path below is renamed or removed. Vitest silently
# ignores unmatched paths, so a stale entry would otherwise report green
# while skipping coverage.
#
# The matching native artifact establishes real Job Object and private
# runtime boundaries for these fixtures. Ghidra and Java are not needed;
# real engine claims remain in the separate controlled-provider workflow.
- name: Run curated Windows capability tests
run: >-
npx vitest run --coverage.enabled=false
src/process/capture/FilesystemSnapshot.test.ts
tests/process-global/filesystemSnapshotFallbackCancellation.test.ts
tests/boundary/filesystem/binaryTarget.test.ts
tests/boundary/filesystem/safeOutputTree.test.ts
tests/boundary/providers/native/artifactProvider.archiveSafety.test.ts
tests/boundary/providers/native/artifactProvider.extractionCompleteness.test.ts
tests/boundary/filesystem/setupUnavailableProvider.test.ts
tests/boundary/filesystem/windowsNpxRegistration.test.ts
tests/boundary/android/workspaceGuardianRetry.test.ts
tests/acceptance/setup/homeDirectoryFallback.test.ts
tests/boundary/filesystem/javascriptApplicationWorkflows.test.ts
tests/boundary/filesystem/webScriptManifestPaths.test.ts
tests/boundary/process/ProcessEvidence.host.test.ts
tests/acceptance/webModuleCli.test.ts
tests/acceptance/webSourceLocationCli.test.ts
tests/boundary/cli/referenceSourceImportStatus.test.ts
tests/boundary/filesystem/asarInventoryFreshness.test.ts
tests/boundary/filesystem/javascript-applications/javascriptArtifactReconstruction.test.ts
tests/conformance/providers/ghidra/windowsCapabilities.test.ts
src/ghidra/GhidraInstallation.test.ts
tests/boundary/filesystem/ghidraJavaProbe.test.ts
tests/boundary/providers/ghidra/ghidraTransport.test.ts
tests/boundary/providers/ghidra/ghidraTargetSnapshot.test.ts
tests/boundary/providers/ghidra/ghidraLauncher.test.ts
tests/boundary/providers/hopper/hopperClient.cleanupRetry.test.ts
src/windows/WindowsNativeLoader.test.ts
src/windows/WindowsOwnedProcess.test.ts
src/ghidra/GhidraLauncherEnvironment.test.ts
src/process/ProcessOwnership.test.ts
src/process/ProcessOwnership.part2.test.ts
src/process/ProcessOwnership.part3.test.ts
src/process/ProcessOwnership.validation.test.ts
src/application/Update.test.ts
tests/process-global/updateMaintenance.test.ts
tests/conformance/providers/ghidra/realGhidraWindowsWorkflow.test.ts
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: windows-process-boundary-fixture
path: .cache/windows-process-boundary/
# Passing the fixture adds the breakaway, caller/child token, environment
# semantics and ancestor reparse observations, which the lane skips
# entirely when it is absent.
- run: >-
npm run verify:windows-native -- .
"${{ github.workspace }}\.cache\windows-process-boundary\process-boundary.exe"
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: windows-native-inspection-fixture
path: .cache/windows-native-inspection-fixture/
- name: Verify native inspection failure ownership and recovery
run: node scripts/verify-windows-native-inspection.mjs
- name: Verify real Node Inspector discovery and observation
timeout-minutes: 10
env:
NODE_OPTIONS: "--max-old-space-size=1024"
UV_THREADPOOL_SIZE: "2"
run: npm run verify:inspector
- run: npm run verify:package:windows
- name: Verify native directory identity on Node 24
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22.19.0
- name: Verify native path boundaries on Node 22
run: >-
npx vitest run --coverage.enabled=false
tests/boundary/filesystem/directoryArtifactReaderOpen.test.ts
tests/boundary/filesystem/javascriptPackagePrecedence.test.ts
tests/boundary/filesystem/javascriptArtifactContentIdentity.test.ts
tests/boundary/process/sourceMapUncHosts.test.ts
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
if: always()
with:
name: windows-capabilities
path: windows-capabilities.json
retention-days: 30
docs:
name: Docs Generation
needs: changes
if: needs.changes.outputs.docs == 'true'
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
id: npm-cache
uses: ./.github/actions/npm-cache
- run: npm ci
- run: npm run docs:check
- name: Generation leaves authored files unchanged
run: git diff --exit-code
- uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: generated-docs
path: |
docs/public/product-catalog.json
docs/verification/managed-conformance-*.json
skills/reverse-engineer-anything
if-no-files-found: error
browser:
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, 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, 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, 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, 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, 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, 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, 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, static]
if: needs.changes.outputs.nativeaot == 'true'
uses: ./.github/workflows/nativeaot-fixtures.yml
website:
needs: [changes, static]
if: needs.changes.outputs.website == 'true'
uses: ./.github/workflows/website-check.yml
required:
name: CI required
# Report failed dependencies, but release superseded PR concurrency slots.
if: ${{ !cancelled() }}
needs:
[
changes,
static,
build,
reverse-tools-quality,
test-shard,
test-source,
sqlite-runtime,
test,
conformance-fixtures,
windows-native-build,
package-e2e,
apple-artifacts,
reconstruction-readiness,
windows-curated,
docs,
browser,
binary_layout,
evm,
javascript_recovery,
recorded_crash,
web_network,
web_runtime,
web_source_map,
nativeaot,
website,
]
runs-on: ubuntu-24.04
timeout-minutes: 5
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: ${{ steps.gate.outputs.receipt_name }}
path: ${{ steps.gate.outputs.receipt_path }}
if-no-files-found: error
overwrite: true
retention-days: 7
-62
View File
@@ -1,62 +0,0 @@
name: Mutation sweep
on:
schedule:
- cron: "47 4 * * *"
workflow_dispatch:
permissions:
contents: read
concurrency:
group: mutation-${{ github.ref }}
cancel-in-progress: false
env:
HUSKY: "0"
jobs:
mutation:
name: Mutation sweep (${{ matrix.suite }})
runs-on: ubuntu-latest
timeout-minutes: 60
strategy:
fail-fast: false
max-parallel: 2
matrix:
include:
- suite: reverse-tools
config: stryker.config.mjs
- suite: reverse-tools-semantics
config: stryker.semantics.config.mjs
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version-file: .nvmrc
- name: Cache npm package downloads
uses: ./.github/actions/npm-cache
- run: npm ci
- run: npm run test:prepare
- name: Run the complete configuration
env:
MUTATION_CONFIG: ${{ matrix.config }}
run: |
node scripts/prepare-reverse-tools-mutation.mjs
npx --no-install stryker run "${MUTATION_CONFIG}"
- name: Require both reports from the completed sweep
env:
REPORT_BASE: reports/mutation/${{ matrix.suite }}
run: |
test -s "${REPORT_BASE}.json"
test -s "${REPORT_BASE}.html"
- name: Retain available mutation diagnostics
if: ${{ !cancelled() }}
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
with:
name: mutation-${{ matrix.suite }}-${{ github.run_attempt }}
path: |
reports/mutation/${{ matrix.suite }}.json
reports/mutation/${{ matrix.suite }}.html
if-no-files-found: warn
retention-days: 7
-109
View File
@@ -1,109 +0,0 @@
name: Recover published release
on:
workflow_dispatch:
inputs:
version:
description: Existing npm and GitHub release version
required: true
type: string
release_run_id:
description: Original Release run with successful exact-source qualification and npm publication
required: true
type: string
concurrency:
group: release-recovery-${{ inputs.version }}
cancel-in-progress: false
jobs:
verify:
name: Verify existing published package
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: read
actions: read
outputs:
source_sha: ${{ steps.release.outputs.source_sha }}
version: ${{ steps.release.outputs.version }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version-file: .nvmrc
package-manager-cache: false
- run: npm ci
- name: Validate original release authority
id: release
env:
GH_TOKEN: ${{ github.token }}
RECOVERY_VERSION: ${{ inputs.version }}
RECOVERY_RUN_ID: ${{ inputs.release_run_id }}
run: node scripts/ci/release-recovery.mjs resolve
- name: Check out released metadata
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ref: ${{ steps.release.outputs.source_sha }}
path: .cache/release-source
persist-credentials: false
- name: Download original qualified package
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: verified-release-package
run-id: ${{ steps.release.outputs.run_id }}
github-token: ${{ github.token }}
path: .cache/verified-package
- name: Restore original verifier runtime
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: rea-test-runtime
run-id: ${{ steps.release.outputs.run_id }}
github-token: ${{ github.token }}
path: .
- name: Match published npm bytes to the qualified package
env:
RECOVERY_VERSION: ${{ steps.release.outputs.version }}
RECOVERY_RUN_ID: ${{ steps.release.outputs.run_id }}
RECOVERY_RUN_ATTEMPT: ${{ steps.release.outputs.run_attempt }}
RECOVERY_SOURCE_SHA: ${{ steps.release.outputs.source_sha }}
RECOVERY_SOURCE_DIRECTORY: .cache/release-source
RECOVERY_ARTIFACT_DIRECTORY: .cache/verified-package
run: node scripts/ci/release-recovery.mjs verify
- name: Rehearse the original qualified tarball
env:
RECOVERY_VERSION: ${{ steps.release.outputs.version }}
run: node scripts/verify-release-artifact.mjs .cache/verified-package/package.tgz "$RECOVERY_VERSION"
- name: Verify published CLI, MCP and update
env:
RECOVERY_VERSION: ${{ steps.release.outputs.version }}
run: node scripts/verify-published-package.mjs "$RECOVERY_VERSION"
- name: Validate released registry metadata
working-directory: .cache/release-source
run: npm run mcp-registry:check
publish-mcp:
name: Finish MCP Registry publication
needs: verify
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
- name: Check out verified released metadata
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ref: ${{ needs.verify.outputs.source_sha }}
path: .cache/release-source
persist-credentials: false
- uses: ./.github/actions/publish-mcp
with:
directory: .cache/release-source
+65 -53
View File
@@ -304,41 +304,12 @@ jobs:
with:
source_sha: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
rehearsal:
name: Rehearse qualified release on a clean runner
publish:
name: Publish to npm
needs: [merged-release, release-please, qualification]
if: >-
always() && !cancelled() && needs.qualification.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
actions: read
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
ref: ${{ needs.merged-release.outputs.sha || needs.release-please.outputs.sha }}
persist-credentials: false
- uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6.5.0
with:
node-version-file: .nvmrc
package-manager-cache: false
- run: npm ci
- uses: ./.github/actions/prepare-release-package
id: package
- name: Exercise the exact qualified tarball
env:
VERIFIED_PACKAGE_TARBALL: ${{ steps.package.outputs.tarball }}
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: node scripts/verify-release-artifact.mjs "$VERIFIED_PACKAGE_TARBALL" "$VERIFIED_PACKAGE_VERSION"
publish:
name: Publish to npm
needs: [merged-release, release-please, qualification, rehearsal]
if: >-
always() && !cancelled() &&
needs.qualification.result == 'success' && needs.rehearsal.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
@@ -356,36 +327,58 @@ jobs:
- run: npm ci
- uses: ./.github/actions/prepare-release-package
id: package
- name: Download the exact qualified package
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
with:
name: verified-release-package
path: .cache/verified-package/
- name: Inspect existing npm version
id: registry
env:
VERIFIED_PACKAGE_TARBALL: ${{ steps.package.outputs.tarball }}
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: node scripts/ci/npm-release.mjs inspect "$VERIFIED_PACKAGE_TARBALL" "$VERIFIED_PACKAGE_VERSION"
- name: Verify qualified package provenance and digest
id: package
run: node scripts/ci/verified-package.mjs validate .cache/verified-package
- name: Publish
if: steps.registry.outputs.published == 'false'
shell: bash
env:
VERIFIED_PACKAGE_TARBALL: ${{ steps.package.outputs.tarball }}
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: |
tag="$(node scripts/release-npm-tag.mjs "$VERIFIED_PACKAGE_VERSION")"
npm publish "${VERIFIED_PACKAGE_TARBALL}" --access public --tag "${tag}"
- name: Verify published npm bytes and dist-tag
env:
VERIFIED_PACKAGE_TARBALL: ${{ steps.package.outputs.tarball }}
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: node scripts/ci/npm-release.mjs verify "$VERIFIED_PACKAGE_TARBALL" "$VERIFIED_PACKAGE_VERSION"
version="$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')"
published_version="$(npm view "rea-agents@${version}" version --json 2>/dev/null || true)"
if [ "${published_version}" = "\"${version}\"" ]; then
echo "rea-agents@${version} is already published; verifying the existing package."
else
tag="$(node scripts/release-npm-tag.mjs "${version}")"
npm publish "${VERIFIED_PACKAGE_TARBALL}" --access public --tag "${tag}"
fi
- name: Verify published CLI from npm
env:
VERIFIED_PACKAGE_VERSION: ${{ steps.package.outputs.version }}
run: node scripts/verify-published-package.mjs "$VERIFIED_PACKAGE_VERSION"
shell: bash
run: |
version="$(node --input-type=module -e 'import packageJson from "./package.json" with { type: "json" }; process.stdout.write(packageJson.version)')"
verification_root="$(mktemp -d)"
trap 'rm -rf -- "${verification_root}"' EXIT
cd "${verification_root}"
max_attempts=120
sleep_seconds=15
total_window_minutes=$((max_attempts * sleep_seconds / 60))
for attempt in $(seq 1 "${max_attempts}"); do
published_version="$(npm view --prefer-online "rea-agents@${version}" version --json 2>/dev/null || true)"
if [ "${published_version}" = "\"${version}\"" ]; then
echo "rea-agents@${version} is visible on npm after attempt ${attempt}/${max_attempts}; verifying the published CLI."
npm exec --yes --package="rea-agents@${version}" -- rea --help
npm exec --yes --package="rea-agents@${version}" -- rea --llms
node "${GITHUB_WORKSPACE}/scripts/verify-published-package.mjs" "${version}"
exit 0
else
echo "rea-agents@${version} is not visible on npm yet (attempt ${attempt}/${max_attempts}; up to ${total_window_minutes} minutes)" >&2
fi
if [ "${attempt}" -lt "${max_attempts}" ]; then
sleep "${sleep_seconds}"
fi
done
echo "rea-agents@${version} did not become visible on npm within ${total_window_minutes} minutes (${max_attempts} attempts × ${sleep_seconds}s)" >&2
echo "This indicates npm registry propagation delay, not a CLI verification failure." >&2
exit 1
publish-mcp:
name: Publish to MCP Registry
@@ -407,4 +400,23 @@ jobs:
- name: Verify package and registry metadata versions
run: npm run mcp-registry:check
- uses: ./.github/actions/publish-mcp
- name: Install MCP Registry publisher
env:
MCP_PUBLISHER_VERSION: v1.8.0
MCP_PUBLISHER_SHA256_LINUX_AMD64: 1370446bbe74d562608e8005a6ccce02d146a661fbd78674e11cc70b9618d6cf
run: |
archive="mcp-publisher_linux_amd64.tar.gz"
curl -fsSLo "${archive}" \
"https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}/${archive}"
echo "${MCP_PUBLISHER_SHA256_LINUX_AMD64} ${archive}" | sha256sum --check --strict
tar xzf "${archive}" mcp-publisher
chmod +x mcp-publisher
- name: Validate MCP Registry metadata
run: ./mcp-publisher validate
- name: Authenticate to MCP Registry
run: ./mcp-publisher login github-oidc
- name: Publish MCP Registry metadata
run: ./mcp-publisher publish
+1 -1
View File
@@ -1,3 +1,3 @@
{
".": "6.5.0"
".": "6.4.0"
}
-113
View File
@@ -1,118 +1,5 @@
# Changelog
## [6.5.0](https://github.com/morluto/rea/compare/rea-agents-6.4.0...rea-agents-6.5.0) (2026-10-13)
Provider prerequisites and recorded verification scope are listed in [the testing guide](docs/testing.md).
### Highlights
- Analyze selected JavaScript files and recover cross-bundle relationships without expanding the selected source scope.
- Read compact summaries and page retained Evidence across binary, managed, Android, Dart, WASM and network workflows.
- Inspect SQLite snapshots, packet and signal recordings, WebAssembly, Rust dependency metadata and Electron fuses offline.
- Keep completed Evidence and partial observations available when analysis or cleanup fails.
### Changes
- **JavaScript source selection:** analyze exact application paths without expanding imports into the selected scope. Unselected files retain identity and explicit unknown coverage; summary delivery remains separate from source selection. ([#1944](https://github.com/morluto/rea/pull/1944), [issue morluto/rea#1848](https://github.com/morluto/rea/issues/1848)). Thanks @kaoru0822-kitauji, @N0zoM1z0.
- **Code-split recovery:** recover related bundles from multiple selected inputs, retaining per-file provenance and downstream cross-chunk imports. Recorded CLI/MCP coverage is Linux x64 with Wakaru 1.14.0 and source-owned esbuild/webpack fixtures; other hosts, releases and arbitrary application equivalence remain unverified. ([#1686](https://github.com/morluto/rea/pull/1686), [issue morluto/rea#1605](https://github.com/morluto/rea/issues/1605)). Thanks @JingHao-Leon, @pionxzh.
- **Package imports:** resolve exact package import aliases using the selected loader and package ancestry, rather than guessing sibling files. ([#1862](https://github.com/morluto/rea/pull/1862), [issue morluto/rea#1838](https://github.com/morluto/rea/issues/1838)). Thanks @AWUQAQ, @mvanhorn.
- **Retained analysis views:** opt into compact inventory/WASM, managed metadata, Android manifest, Dart AOT, Mach-O and EVM summaries, then page retained Evidence without rerunning analysis. Summary delivery preserves the complete recorded result and its original coverage. ([#2122](https://github.com/morluto/rea/pull/2122), [#2193](https://github.com/morluto/rea/pull/2193), [#2413](https://github.com/morluto/rea/pull/2413), [#2438](https://github.com/morluto/rea/pull/2438), [#2455](https://github.com/morluto/rea/pull/2455), [#2634](https://github.com/morluto/rea/pull/2634), [#2308](https://github.com/morluto/rea/pull/2308), [9a18251](https://github.com/morluto/rea/commit/9a18251426f891e6a24856b715ef8002ab8b9610)). Thanks @kaoru0822-kitauji, @N0zoM1z0.
- **Managed assemblies:** Select declaring types when inspecting members, decode inspected `ldstr` literals with independent coverage, and page retained member summaries. Call and field targets expose `target_declaring_type` and `field_declaring_type`, including supported generic method targets; unresolved metadata remains unknown. ([#1706](https://github.com/morluto/rea/pull/1706), [#2125](https://github.com/morluto/rea/pull/2125), [issue morluto/rea#2121](https://github.com/morluto/rea/issues/2121), [#2162](https://github.com/morluto/rea/pull/2162), [issue morluto/rea#2158](https://github.com/morluto/rea/issues/2158), [#3061](https://github.com/morluto/rea/pull/3061), [issue morluto/rea#3010](https://github.com/morluto/rea/issues/3010), [#2163](https://github.com/morluto/rea/pull/2163), [issue morluto/rea#2160](https://github.com/morluto/rea/issues/2160)). Thanks @kaoru0822-kitauji, @N0zoM1z0, @Nelglor, @skoodster.
- **Packet captures:** inspect saved packets with bounded TShark output, compact capture summaries and retained pages; discover installed protocol/field definitions. Recorded real-engine coverage uses Linux and Wireshark 4.6.6; native Windows/macOS and Bun behavior remain unverified. ([#2318](https://github.com/morluto/rea/pull/2318), [#2340](https://github.com/morluto/rea/pull/2340), [#2496](https://github.com/morluto/rea/pull/2496)). Thanks @3QL, @N0zoM1z0, @Olorinm.
- **Recorded signals:** decode existing recordings and discover installed decoders/options through caller-supplied sigrok-cli 0.7.2. This is Linux-only and requires util-linux `prlimit`; it acquires no hardware samples. ([#2339](https://github.com/morluto/rea/pull/2339), [#2487](https://github.com/morluto/rea/pull/2487)). Thanks @N0zoM1z0, @Olorinm.
- **Web captures:** derive an inferred method/origin/path-template API surface from retained HAR data including observed query-parameter names while retaining unknowns; query values, headers and bodies remain excluded. ([#1891](https://github.com/morluto/rea/pull/1891), [#2982](https://github.com/morluto/rea/pull/2982), [issue morluto/rea#2913](https://github.com/morluto/rea/issues/2913)). Thanks @mvanhorn, @wunianze666-netizen, @zhangguiping-xydt.
- **QUIC captures:** inspect retained aioquic 1.5.0 JSON qlog 0.3 objects and page traces/streams/observations. Other qlog layouts remain unsupported; overlap and loss declarations do not prove retransmission or network loss. ([#2277](https://github.com/morluto/rea/pull/2277)). Thanks @LuisTaffelli, @N0zoM1z0.
- **Fiddler captures:** inspect native SAZ archives with retained HTTP provenance and bounded decoding. Recorded producer coverage is the pinned public encoding corpus; current Fiddler Everywhere variants and Bun-native execution remain unverified. ([#2409](https://github.com/morluto/rea/pull/2409)). Thanks @N0zoM1z0, @zahhak.
- **SQLite snapshots:** inspect an explicitly selected offline database with table/column selection and bounded Evidence. This operation requires Node 24.x >=24.15 or 26+ with `DatabaseSync.limits`; supported Node 22 and earlier 24 runtimes cannot use it. A live database/WAL copy does not establish an atomic snapshot. ([#2271](https://github.com/morluto/rea/pull/2271)). Thanks @N0zoM1z0, @Y0UW1SH.
- **WASM artifacts:** Inspect local modules with bring-your-own WABT 1.0.42 without executing them. Compare imports/exports from two complete core-module inspection Evidence records without requiring an active WABT installation; unsupported signatures and duplicate identities remain unknown, and comparisons do not establish runtime compatibility. Recorded real-tool inspection coverage is macOS arm64. ([#1533](https://github.com/morluto/rea/pull/1533), [issue morluto/rea#884](https://github.com/morluto/rea/issues/884), [#2464](https://github.com/morluto/rea/pull/2464), [issue morluto/rea#2356](https://github.com/morluto/rea/issues/2356)). Thanks @matbrgz, @N0zoM1z0, @thangldw.
- **Go binaries:** inspect embedded build metadata, including file-backed unmapped Mach-O debug segments. ([#1363](https://github.com/morluto/rea/pull/1363), [7fb097d](https://github.com/morluto/rea/commit/7fb097d72c9931dd5dfae481ad2b9d3bb6029f15)). Thanks @Y0UW1SH.
- **Flutter APKs:** identify snapshot/engine builds and inspect AOT structure without an external engine. Recorded real APK coverage uses the documented Flutter Gallery fixture; macOS/Windows and complete pool semantics remain unverified, and pool coverage stays partial. ([#1582](https://github.com/morluto/rea/pull/1582)). Thanks @3QL.
- **HarmonyOS packages:** inventory HAP/HSP/App Pack structures without executing code. The recorded real workflow covers one pinned HAP through repository-built CLI/MCP; installed-package and real HSP/App Pack coverage remain unverified. ([#1447](https://github.com/morluto/rea/pull/1447)). Thanks @BaaRai.
- **Ghidra raw inputs and references:** compatible Linux/macOS Ghidra profiles accept caller-selected raw architecture/entry information and expose typed incoming references at exact addresses. Windows Ghidra P0 admits the raw NMOS 6502 profile described below; other raw targets and the incoming-reference facet remain outside Windows P0. ([#1868](https://github.com/morluto/rea/pull/1868), [issue morluto/rea#1836](https://github.com/morluto/rea/issues/1836), [#1715](https://github.com/morluto/rea/pull/1715), [issue morluto/rea#1245](https://github.com/morluto/rea/issues/1245)). Thanks @bugstan, @casesolved-co-uk, @mvanhorn, @N0zoM1z0.
- **Ghidra MIPS:** Add bounded MIPS32r2/o32 relocatable objects, soft-float integer-register storage, admitted MIPS16 shared ELF, and a narrowly admitted recordless legacy MIPS32 executable profile. The latter pins an endian-matched 32-bit language and records inferred o32 as an analysis assumption. Admission follows each documented ABI/flag/relocation boundary. ([#1949](https://github.com/morluto/rea/pull/1949), [issue morluto/rea#1937](https://github.com/morluto/rea/issues/1937), [#1950](https://github.com/morluto/rea/pull/1950), [issue morluto/rea#1938](https://github.com/morluto/rea/issues/1938), [#1991](https://github.com/morluto/rea/pull/1991), [issue morluto/rea#1851](https://github.com/morluto/rea/issues/1851), [#1898](https://github.com/morluto/rea/pull/1898), [issue morluto/rea#1620](https://github.com/morluto/rea/issues/1620)). Thanks @AurelioB, @BaaRai, @bqw371, @kaoru0822-kitauji, @N0zoM1z0.
- **Ghidra RISC-V:** admit little-endian RV32/RV64 `ET_EXEC` ELF declaring the supported double-float ABI. Relocatable/shared, big-endian and other ABI profiles remain refused; see [the profile and recorded lane](docs/ghidra-riscv.md). ([#2151](https://github.com/morluto/rea/pull/2151)). Thanks @NIU-123370.
- **PSP static ELF profile:** the Ghidra profile is Linux x64, bounded little-endian ELF32 `ET_EXEC` with the documented PSP/ABI declarations and a compatible caller-installed Allegrex extension. PRX, shared/relocatable images, PBP/ISO/CSO and runtime emulation are outside scope. Historical extension experiments are separate from public-provider verification; real public Ghidra PSP coverage remains unverified. ([#1599](https://github.com/morluto/rea/pull/1599)). Thanks @jaelliot.
- **Ghidra analysis settings:** apply validated analyzer overrides before analysis while retaining the selected analysis profile. ([#1913](https://github.com/morluto/rea/pull/1913)). Thanks @akushonkamen, @kaoru0822-kitauji, @N0zoM1z0.
- **Attached native IDA MCP:** add an optional native bridge alongside existing legacy/headless profiles. Recorded native runs use Linux IDA Free 9.4 with a navigation correction and Windows IDA Pro 9.3 with a loopback-listener change; stock native switching, native macOS and Windows IDA Free remain unverified. Attached databases remain read-only and are never saved or closed. ([#1544](https://github.com/morluto/rea/pull/1544)). Thanks @imissapixel, @N0zoM1z0.
- **Frida instrumentation:** add explicit local/remote target selection, caller-supplied scripts and retained messages, with script-read/input diagnostics and bounded message eviction. Recorded real coverage is the Linux local Node marker workflow; macOS, Windows and remote devices/servers remain unverified. Instrumentation executes foreign code and is destructive, not read-only. ([#1359](https://github.com/morluto/rea/pull/1359), [#2211](https://github.com/morluto/rea/pull/2211), [issue morluto/rea#2209](https://github.com/morluto/rea/issues/2209), [#2217](https://github.com/morluto/rea/pull/2217), [issue morluto/rea#2213](https://github.com/morluto/rea/issues/2213), [#2628](https://github.com/morluto/rea/pull/2628)). Thanks @dandgabr, @kaoru0822-kitauji, @N0zoM1z0.
- **Client setup:** register Qoder, Trae, ZCode, DeepSeek Harness, Kimi Code, Factory Droid and WorkBuddy through the existing scoped setup flow. DeepSeek is included from 6.5.0; the historical npm 6.4 check did not cover it. Live Trae and DeepSeek API integration remain unverified. ([#1564](https://github.com/morluto/rea/pull/1564), [#1572](https://github.com/morluto/rea/pull/1572), [#1573](https://github.com/morluto/rea/pull/1573), [#1929](https://github.com/morluto/rea/pull/1929), [issue morluto/rea#1839](https://github.com/morluto/rea/issues/1839), [#2744](https://github.com/morluto/rea/pull/2744), [#2309](https://github.com/morluto/rea/pull/2309), [#3041](https://github.com/morluto/rea/pull/3041), [issue morluto/rea#3040](https://github.com/morluto/rea/issues/3040)). Thanks @0xRafie, @BaaRai, @cainiao1992, @IvenKooLab, @whoishaha, @ysntony.
- **Website:** Add maintained language editions, persistent/system-aware themes, keyboard styling and nested Evidence indentation, plus the reverse-engineering models essay. A simpler home page and beginner journey connect platform prerequisites, client/provider setup and a supplied first investigation. ([#1779](https://github.com/morluto/rea/pull/1779), [issue morluto/rea#1022](https://github.com/morluto/rea/issues/1022), [#1699](https://github.com/morluto/rea/pull/1699), [#1714](https://github.com/morluto/rea/pull/1714), [#1894](https://github.com/morluto/rea/pull/1894), [#2250](https://github.com/morluto/rea/pull/2250), [#2432](https://github.com/morluto/rea/pull/2432), [#2732](https://github.com/morluto/rea/pull/2732), [#3274](https://github.com/morluto/rea/pull/3274)). Thanks @CassieFox23, @kaoru0822-kitauji, @N0zoM1z0.
- **Rust dependency metadata:** Inspect embedded cargo-auditable package/dependency records in ELF, PE and thin Mach-O without executing the file. Missing metadata and inferred compiler hashes retain explicit limitations; universal Mach-O requires a selected thin slice. ([#2456](https://github.com/morluto/rea/pull/2456)). Thanks @mvanhorn.
- **Electron fuses:** Read packaged fuse bytes and per-slice runtime implications without launching or modifying the app. Missing, truncated and unknown wires remain explicit; fuse observations do not prove other Inspector-enablement paths. ([#2399](https://github.com/morluto/rea/pull/2399)). Thanks @kaoru0822-kitauji, @mvanhorn, @N0zoM1z0.
- **Ghidra PowerPC:** Admit bounded ELF32 big-endian `ET_EXEC` PowerPC with an explicit language profile on supported Linux/macOS hosts. PPC64, DOL and other variants remain outside this lane; see [the profile and verification prerequisites](docs/ghidra-ppc.md). ([#1747](https://github.com/morluto/rea/pull/1747)). Thanks @arielfikru.
- **Reference-source summaries:** Use CLI `import-reference-source --detail summary` for compact manifest, count, state and path-outline output. The complete graph remains available and is still the default; hidden outline entries retain separate coverage counts. ([#2768](https://github.com/morluto/rea/pull/2768), [issue morluto/rea#2442](https://github.com/morluto/rea/issues/2442)). Thanks @matbrgz, @To3akaRin.
- **Recorded AArch64 crashes:** Decode 34 ABI register values per thread from supported Linux ELF64 little-endian core notes using the documented caller-provided Python dependencies. These are recorded facts, not proof of current process or library identity. ([#2505](https://github.com/morluto/rea/pull/2505), [issue morluto/rea#2353](https://github.com/morluto/rea/issues/2353)). Thanks @matbrgz, @NIU-123370.
- **Ghidra byte search:** Search initialized analyzed memory using wildcards, bit masks, address bounds and paged results. Unloaded, overlay and uninitialized blocks retain skipped coverage; this is not a raw-file search. ([#2341](https://github.com/morluto/rea/pull/2341)). Thanks @mvanhorn.
- **Raw NMOS 6502 images:** Analyze explicitly selected headerless images with Ghidra, including Windows P0, using a caller-selected entry and source mapping within the 16-bit address space. Cartridge parsing, reset-vector inference and 65C02/65816 are outside this profile. ([#2079](https://github.com/morluto/rea/pull/2079)). Thanks @Zhong-fan.
- **Swift String observations:** Ghidra function dossiers derive supported AArch64 Swift String literals from constant-register pairs at recognized use sites, retaining instruction/register provenance and explicit omissions. This is a bounded derivation, not a complete string inventory. ([#2049](https://github.com/morluto/rea/pull/2049), [issue morluto/rea#2030](https://github.com/morluto/rea/issues/2030)). Thanks @thefolahan.
- **Headless IDA procedure lookup:** Resolve addresses to canonical containing-procedure entries and identify the session’s admitted program document in the modern headless profile. Incomplete surveys remain errors; attached profiles do not advertise this containment capability. ([#2374](https://github.com/morluto/rea/pull/2374), [issue morluto/rea#2335](https://github.com/morluto/rea/issues/2335)). Thanks @nikolay-dementiev.
- **Passive browser attachment:** Six existing passive browser tools accept an explicit-port loopback browser WebSocket endpoint when HTTP target discovery is unavailable. Existing consent and target/origin checks remain required; this does not launch a browser or enable runtime instrumentation. ([#2346](https://github.com/morluto/rea/pull/2346)). Thanks @all-black-493.
- **Android DEX references:** Retain exact method descriptors and individual direct-invoke observations with source method, DEX entry, opcode and code-unit offset. These are static references; unavailable instruction facts remain separate from method-level usage totals. ([#2439](https://github.com/morluto/rea/pull/2439)). Thanks @LcokHcak.
- **Offline EVM metadata:** Retain bounded, lossless facts from plausible terminal Solidity CBOR root-map candidates. Candidates do not prove Solidity origin or source/compiler identity, and linked metadata is not fetched. ([#2790](https://github.com/morluto/rea/pull/2790)). Thanks @matbrgz.
### Fixes
- **JavaScript loader and export identity:** Respect import/require conditions and unavailable package metadata, keep export links separate from writes, and resolve default identifier exports to the value captured at the export statement. ([7e5cffa](https://github.com/morluto/rea/commit/7e5cffa2ae9dfc9afaeca0715110d4dd41c0dcdd), [322506d](https://github.com/morluto/rea/commit/322506d0c0d39d3cd1e2e1c285c2cc23cfa73ee7), [81f7638](https://github.com/morluto/rea/commit/81f763820319a762f003cf95651ed14ce9326a4c), [dae1e55](https://github.com/morluto/rea/commit/dae1e55381f0d6cfee7f2082d5d27b938e55704c)). Thanks @paoValle, @VarunGore36, @xu91102.
- **JavaScript traces:** Retain nested bridge calls and surviving early-return predicates. Honor node-ID match options, match route seeds against source values, and keep ambiguous runtime candidates unknown. ([0351328](https://github.com/morluto/rea/commit/0351328e54ce70ea3f2814482e76b132774aff48), [5031099](https://github.com/morluto/rea/commit/503109995f9dcf8a955585d39a344fc846d2f59e), [44c9d29](https://github.com/morluto/rea/commit/44c9d2924d5b95d7a86595e8d7b7cf4fd4f0f6ac), [5e79d46](https://github.com/morluto/rea/commit/5e79d468e5864204cbe79a7ca2898541cee5e012)). Thanks @jpdefo.
- **Browser comparison coverage:** Compare only known redirect response fields and retain network-selection recovery guidance alongside independent event changes. Missing response facts do not prove equality or a change. ([2b60e09](https://github.com/morluto/rea/commit/2b60e097f281f7708c69f1f7ba9263694547bcc0)).
- **Evidence comparison and ledger input:** Report display-only bundle differences as unchanged while retaining digests, and classify noncanonical bundle records as invalid caller input. ([030c477](https://github.com/morluto/rea/commit/030c47732833c9cedcc31db1309eb3a1ca2c62e8), [3a34ad3](https://github.com/morluto/rea/commit/3a34ad37bdb7b8debba0f6515ba85b80141f2716)).
- **Capture and admission cleanup:** Keep deleted-file observations and original bytes when a parent becomes a file, preserve unrelated I/O failures during cancellation, and require explicit pre-launch proof before releasing an unknown LLDB target owner. LLDB failure controls used simulated boundaries. ([e67c364](https://github.com/morluto/rea/commit/e67c3642c8920a2a9c0e231ee2f16f50b245df31), [2daeff4](https://github.com/morluto/rea/commit/2daeff4b243e1cd137c591ca394e18a1c05c8bbc), [45b6a7f](https://github.com/morluto/rea/commit/45b6a7fe8f51a89951aaa823ffa627f6f5453a33)). Thanks @SII-YDD, @xu91102.
- **Client configuration:** Validate MCP output-schema profiles before setup writes, ignore untrusted Gemini workspace settings before parsing them, and identify installed skills independently of marker-only client roots. ([0930e94](https://github.com/morluto/rea/commit/0930e94e077ab7fb4d695f62785919a8a25c2e21), [aade9bb](https://github.com/morluto/rea/commit/aade9bbcb79aa8504ab6921b25f8ab082aba78e7), [ba33c70](https://github.com/morluto/rea/commit/ba33c706d26b63e43308be50c0249c72cff62cc6)). Thanks @CmsChase, @paoValle, @sx4im.
- **Host tool discovery:** Share macOS Hopper launcher discovery across setup and runtime, preserve explicit paths, and report validated ADB and Apktool availability. ([7582f0d](https://github.com/morluto/rea/commit/7582f0dff29ae0e80c97b885ddd386ac61a92570), [e5deb83](https://github.com/morluto/rea/commit/e5deb832cb09bb524877d0df40f49d5b5bb7dea4)). Thanks @Chessing234, @kaoru0822-kitauji, @viniciusdsandrade.
- **SAZ encoding validation:** Reject invalid XML bytes, unsupported encodings and declarations that contradict the archive bytes, including records outside the selected request. ([1250aae](https://github.com/morluto/rea/commit/1250aaedd8b4d1e471d8a382f76418bde65c05d2)).
- **Managed member matching:** Keep malformed TypeDef list ownership unknown, distinguish constants during structural method matching, and accept legacy small exception-section sizes. ([3f6b575](https://github.com/morluto/rea/commit/3f6b5755b3e699bd9373641acffe0f2c5d2ab094), [ee5cfbe](https://github.com/morluto/rea/commit/ee5cfbe05cd309a88003c711721e67a3bee36554)).
- **Bounded diagnostics:** Bound SDK validation messages and terminal normalization before allocation while retaining diagnostics and explicit unknown final output sizes. ([7fb0a0c](https://github.com/morluto/rea/commit/7fb0a0cc577f7c9033a2674d877d8242b5864556), [1d826f3](https://github.com/morluto/rea/commit/1d826f304f032acfe69dad3ff824b300aacaeb43)). Thanks @CRYPTONIKAV.
- **Windows artifact classification:** identify `.exe`, `.com`, `.bat` and `.cmd` artifacts as launchable host executables. ([#1823](https://github.com/morluto/rea/pull/1823), [#1824](https://github.com/morluto/rea/pull/1824), [issue morluto/rea#1819](https://github.com/morluto/rea/issues/1819)). Thanks @dollarsmoney, @JasonBuildAI, @kaoru0822-kitauji.
- **CLI/MCP delivery:** defer command-specific startup work, bundle runtime entries, retain field/input correction details across output formats and preserve shared optional-provider lifetime. MCP selectors, paging continuation and retained partial-error references remain explicit; session liveness/provider-attribution corrections retain their controlled-provider verification limits. ([#1691](https://github.com/morluto/rea/pull/1691), [issue morluto/rea#1685](https://github.com/morluto/rea/issues/1685), [#1705](https://github.com/morluto/rea/pull/1705), [issue morluto/rea#1695](https://github.com/morluto/rea/issues/1695), [#1755](https://github.com/morluto/rea/pull/1755), [issue morluto/rea#1746](https://github.com/morluto/rea/issues/1746), [#1773](https://github.com/morluto/rea/pull/1773), [issue morluto/rea#1771](https://github.com/morluto/rea/issues/1771), [#1780](https://github.com/morluto/rea/pull/1780), [#1814](https://github.com/morluto/rea/pull/1814), [#1910](https://github.com/morluto/rea/pull/1910), [issue morluto/rea#1849](https://github.com/morluto/rea/issues/1849), [#1996](https://github.com/morluto/rea/pull/1996), [#2140](https://github.com/morluto/rea/pull/2140), [issue morluto/rea#2135](https://github.com/morluto/rea/issues/2135), [#2205](https://github.com/morluto/rea/pull/2205), [issue morluto/rea#2195](https://github.com/morluto/rea/issues/2195), [#2230](https://github.com/morluto/rea/pull/2230), [issue morluto/rea#2180](https://github.com/morluto/rea/issues/2180), [#2294](https://github.com/morluto/rea/pull/2294), [#2482](https://github.com/morluto/rea/pull/2482), [a82d14e](https://github.com/morluto/rea/commit/a82d14e4db4dca989ffa2e7dbe78fd43551da256), [a4dd4e8](https://github.com/morluto/rea/commit/a4dd4e8e8700ce350e1a0c612ebd61b91ca5e585), [48c352b](https://github.com/morluto/rea/commit/48c352b6bfa34c820a530272cd36600158c37b1e), [07b36c1](https://github.com/morluto/rea/commit/07b36c148946f46b99a4ce04e55726e97d0ee03a), [570c40c](https://github.com/morluto/rea/commit/570c40cde55d31e455ef9bc484b7b908658ded58), [8fc2043](https://github.com/morluto/rea/commit/8fc2043eb8bcedbf34dce064f977e455438635d6), [7fc0b5e](https://github.com/morluto/rea/commit/7fc0b5ec9c2532bbf789d7783b7a0cab6fc054d8)). Thanks @369Apps, @chqcjq, @cold-atom, @coygeek, @JackOdegard, @kaoru0822-kitauji, @N0zoM1z0, @sx4im.
- **Evidence import/replay:** import bundles across session transitions while authenticating selected target and file identities, preserve literal keys, metadata/mutation intent and relationship coverage, and honor cancellation before session mutation. Reuse authenticated snapshots and comparisons within their owned lifetime; reject malformed/non-object snapshots and unsupported metadata without disguising uncertainty. ([#1694](https://github.com/morluto/rea/pull/1694), [#1716](https://github.com/morluto/rea/pull/1716), [issue morluto/rea#1680](https://github.com/morluto/rea/issues/1680), [#1721](https://github.com/morluto/rea/pull/1721), [issue morluto/rea#1718](https://github.com/morluto/rea/issues/1718), [#1733](https://github.com/morluto/rea/pull/1733), [issue morluto/rea#1723](https://github.com/morluto/rea/issues/1723), [issue morluto/rea#1724](https://github.com/morluto/rea/issues/1724), [#1776](https://github.com/morluto/rea/pull/1776), [#1873](https://github.com/morluto/rea/pull/1873), [#2001](https://github.com/morluto/rea/pull/2001), [#2013](https://github.com/morluto/rea/pull/2013), [issue morluto/rea#2009](https://github.com/morluto/rea/issues/2009), [#2083](https://github.com/morluto/rea/pull/2083), [#2303](https://github.com/morluto/rea/pull/2303), [#2685](https://github.com/morluto/rea/pull/2685), [issue morluto/rea#2675](https://github.com/morluto/rea/issues/2675), [c002b89](https://github.com/morluto/rea/commit/c002b89e2d8130a3c07e25e5fd604876be18c703), [9c85114](https://github.com/morluto/rea/commit/9c8511409c0706f9979c2b68dd56aaaf9f21e191), [aada0ca](https://github.com/morluto/rea/commit/aada0ca8126bf14c60750a4ccd4a79667770a1e4), [4a4d36f](https://github.com/morluto/rea/commit/4a4d36f881861bbab3c734b0dd459f805766caec), [1c64fe3](https://github.com/morluto/rea/commit/1c64fe3eff48679c6880f42d2f0a4af46686ade7), [6fb3a34](https://github.com/morluto/rea/commit/6fb3a345fe952b8e8f59221aec993acc37ccbf31), [3bcc68c](https://github.com/morluto/rea/commit/3bcc68c037e9415c78093cb286069c1fcca6a2f8), [6bae774](https://github.com/morluto/rea/commit/6bae774328724403255590aa2ddad4d7a2aa727f), [e997085](https://github.com/morluto/rea/commit/e9970855ba16ba711816ac30212c5ca17ae085cd), [d1dcebc](https://github.com/morluto/rea/commit/d1dcebce2c0820469ed63e33497a14258971b710), [b123b2e](https://github.com/morluto/rea/commit/b123b2e51cc7d6d2157525664be2df88fb1ba3bd), [049132a](https://github.com/morluto/rea/commit/049132ab43fa962d31774f2b258c15a71217148b), [042dcaa](https://github.com/morluto/rea/commit/042dcaa1ee66f1b8668f08b193f38ab01bceaacb), [acf75e6](https://github.com/morluto/rea/commit/acf75e6700ace28bb1db25e54e56673ac15e0ed3), [1f7f808](https://github.com/morluto/rea/commit/1f7f808c4f3bb5eca6b13bcc71d5cceb2ace6614), [26bffb9](https://github.com/morluto/rea/commit/26bffb94bf80355c35e22a98a8fc629c6210307e)). Thanks @coygeek, @DeryFerd, @jiangjiang248, @kaoru0822-kitauji, @khanukov, @N0zoM1z0, @NIU-123370, @rohitg00, @sx4im.
- **Ghidra setup and analysis:** preserve launcher/JVM diagnostics, Java-property and extension identity, stale-client recovery, UTF-8 bridge output and pending-startup progress. File-offset decompilation is Ghidra-specific; recorded coverage is Linux x64 with Ghidra 12.1.4. Startup/progress/compiler failure paths that use simulations remain unverified with a real engine. ([#1698](https://github.com/morluto/rea/pull/1698), [#1877](https://github.com/morluto/rea/pull/1877), [#1931](https://github.com/morluto/rea/pull/1931), [#2011](https://github.com/morluto/rea/pull/2011), [issue morluto/rea#1992](https://github.com/morluto/rea/issues/1992), [#2014](https://github.com/morluto/rea/pull/2014), [#2101](https://github.com/morluto/rea/pull/2101), [issue morluto/rea#2078](https://github.com/morluto/rea/issues/2078), [#2206](https://github.com/morluto/rea/pull/2206), [issue morluto/rea#2204](https://github.com/morluto/rea/issues/2204), [2187e16](https://github.com/morluto/rea/commit/2187e16f149a7a7b512a4cb45d5f468cc04091fe), [b953f96](https://github.com/morluto/rea/commit/b953f96c0aae27e1876c317a8f7db1d6f292b8ff)). Thanks @EDMND-SRC, @Ha1baraA11, @kaoru0822-kitauji, @lovecontroing, @lyly-bonbon, @N0zoM1z0, @nosmile99, @Petru101, @sx4im, @Zhong-fan.
- **Process and cleanup ownership:** retain private runtime/file owners and detailed incomplete-cleanup observations, bound trace cycles/order state, preserve caller paths, and use monotonic time for budgets while retaining wall dates. Windows affinity/admission ([#2202](https://github.com/morluto/rea/pull/2202), [issue morluto/rea#2181](https://github.com/morluto/rea/issues/2181)) and Bun memory-enforcement paths retain their recorded native-versus-controlled-fixture limits; these fixes are not a general process-ownership guarantee. ([#1700](https://github.com/morluto/rea/pull/1700), [issue morluto/rea#1697](https://github.com/morluto/rea/issues/1697), [#1730](https://github.com/morluto/rea/pull/1730), [#1756](https://github.com/morluto/rea/pull/1756), [issue morluto/rea#1753](https://github.com/morluto/rea/issues/1753), [#1760](https://github.com/morluto/rea/pull/1760), [issue morluto/rea#1757](https://github.com/morluto/rea/issues/1757), [#1826](https://github.com/morluto/rea/pull/1826), [#1907](https://github.com/morluto/rea/pull/1907), [issue morluto/rea#1856](https://github.com/morluto/rea/issues/1856), [#1999](https://github.com/morluto/rea/pull/1999), [issue morluto/rea#1993](https://github.com/morluto/rea/issues/1993), [#2232](https://github.com/morluto/rea/pull/2232), [issue morluto/rea#2170](https://github.com/morluto/rea/issues/2170), [#2252](https://github.com/morluto/rea/pull/2252), [issue morluto/rea#2218](https://github.com/morluto/rea/issues/2218), [#2467](https://github.com/morluto/rea/pull/2467), [issue morluto/rea#2257](https://github.com/morluto/rea/issues/2257), [#2468](https://github.com/morluto/rea/pull/2468), [#2488](https://github.com/morluto/rea/pull/2488), [09b6828](https://github.com/morluto/rea/commit/09b68285166bef2b7633402c8adf7eb25823b47e), [7285e15](https://github.com/morluto/rea/commit/7285e15839a4fecf9940683699b781a60ccc4775), [b5ededd](https://github.com/morluto/rea/commit/b5ededd0f9c28d0f696a205103066928117368ee), [d8c76b6](https://github.com/morluto/rea/commit/d8c76b629639bda4405afa9ae2f7676b694f2f10), [98d8470](https://github.com/morluto/rea/commit/98d8470d36cfbf7cd25820a4ed430aebae20c0c5), [c22111e](https://github.com/morluto/rea/commit/c22111eca2c4bc84a084f12711f405b3e7422a40), [cecc6ec](https://github.com/morluto/rea/commit/cecc6ecbffe84689a173d1634f342e046b0c8ceb), [10935b7](https://github.com/morluto/rea/commit/10935b7b1136accab2a43b308ab380749da0f7ae), [63e796d](https://github.com/morluto/rea/commit/63e796d50ea323ae8dcfef80f86c8154bb106790), [d9e9fb2](https://github.com/morluto/rea/commit/d9e9fb2273543caab4eb21d896c33d462ada6971), [9049dba](https://github.com/morluto/rea/commit/9049dbaf0c3df32357419f9899965df2b611f493), [ed881c6](https://github.com/morluto/rea/commit/ed881c6d2ef6a8adba0c2a241ebc9ab4603b7868)). Thanks @Ashen00001, @charan-rathore, @coygeek, @EDMND-SRC, @JasonBuildAI, @jimpablo, @kaoru0822-kitauji, @N0zoM1z0, @NIU-123370, @Rinkia, @rudycelekli, @sx4im, @xeropresence, @XiaoBinGan, @xthunder0, @hzh666kevin-hue.
- **Native image identity:** preserve universal Mach-O containers and selected slice/subtype identity, chained fixup locations and library ordinals, exact signing text, PE resource capacity and ELF/header framing. Mach-O summary selection and independent slice commands avoid unnecessary delivery/work. Controlled routing/parser fixtures do not establish every installed-provider path. ([#1703](https://github.com/morluto/rea/pull/1703), [issue morluto/rea#1702](https://github.com/morluto/rea/issues/1702), [#1708](https://github.com/morluto/rea/pull/1708), [#1709](https://github.com/morluto/rea/pull/1709), [#1713](https://github.com/morluto/rea/pull/1713), [#1772](https://github.com/morluto/rea/pull/1772), [issue morluto/rea#1765](https://github.com/morluto/rea/issues/1765), [#1784](https://github.com/morluto/rea/pull/1784), [issue morluto/rea#1775](https://github.com/morluto/rea/issues/1775), [#1982](https://github.com/morluto/rea/pull/1982), [#1995](https://github.com/morluto/rea/pull/1995), [issue morluto/rea#1990](https://github.com/morluto/rea/issues/1990), [#2040](https://github.com/morluto/rea/pull/2040), [#2107](https://github.com/morluto/rea/pull/2107), [#2161](https://github.com/morluto/rea/pull/2161), [issue morluto/rea#2105](https://github.com/morluto/rea/issues/2105), [#2248](https://github.com/morluto/rea/pull/2248), [issue morluto/rea#2176](https://github.com/morluto/rea/issues/2176), [#2281](https://github.com/morluto/rea/pull/2281), [issue morluto/rea#2274](https://github.com/morluto/rea/issues/2274), [#2292](https://github.com/morluto/rea/pull/2292), [#2466](https://github.com/morluto/rea/pull/2466), [#2506](https://github.com/morluto/rea/pull/2506), [#2733](https://github.com/morluto/rea/pull/2733), [issue morluto/rea#2661](https://github.com/morluto/rea/issues/2661), [f598bab](https://github.com/morluto/rea/commit/f598bab488e38c378d69346fa3c106f9e18529ed), [108b5f9](https://github.com/morluto/rea/commit/108b5f94254281639badd25b1f448cca35dc89a2), [f1924cf](https://github.com/morluto/rea/commit/f1924cf9c1910d5d29d11e7bf634b51af8d738cf), [8bd5ba2](https://github.com/morluto/rea/commit/8bd5ba2d234cbd9a5bb9dd5daeaa191c3a1cad24), [b38429c](https://github.com/morluto/rea/commit/b38429cb6a0de8f4bb5ae01633e24abe2d7cde46), [8d4d631](https://github.com/morluto/rea/commit/8d4d631606c701aac150cf009d3b80ae4a7ae7d5), [ac57c88](https://github.com/morluto/rea/commit/ac57c88efe64c3125850b54d3d833227e6b8ec51), [dc038d1](https://github.com/morluto/rea/commit/dc038d16bba9046afd28970de9c47f6259a43524), [34a4bb6](https://github.com/morluto/rea/commit/34a4bb6508006fa113463ef20ed799a2994e9ee8), [ebc22d9](https://github.com/morluto/rea/commit/ebc22d99d62225505c0a84f6e39823ba4ef2f829), [c02fb0e](https://github.com/morluto/rea/commit/c02fb0ea8055f98def2b12ad557aed0faefd3639)). Thanks @1NormalGuy, @Aliciawque, @baron, @coygeek, @dhl84, @hemanthrayuduu, @JasonBuildAI, @kaoru0822-kitauji, @N0zoM1z0, @thangldw, @viniciusdsandrade, @XiaoBinGan, @Zburgers.
- **Archive and artifact provenance:** keep physical ZIP occurrence identity, exact signatures/names and partial inventory coverage; bound decompression and reject malformed keys/integers. Directory/ASAR/DMG reads retain ownership through cleanup and publication cancellation. Duplicate ZIP names remain explicitly ambiguous where the public projection cannot represent them; synthetic archive/cleanup cases do not prove every real Unblob or DMG failure path. ([#1726](https://github.com/morluto/rea/pull/1726), [issue morluto/rea#1725](https://github.com/morluto/rea/issues/1725), [#1854](https://github.com/morluto/rea/pull/1854), [issue morluto/rea#1844](https://github.com/morluto/rea/issues/1844), [#1865](https://github.com/morluto/rea/pull/1865), [issue morluto/rea#1860](https://github.com/morluto/rea/issues/1860), [#2069](https://github.com/morluto/rea/pull/2069), [#2106](https://github.com/morluto/rea/pull/2106), [#2144](https://github.com/morluto/rea/pull/2144), [#2365](https://github.com/morluto/rea/pull/2365), [#2493](https://github.com/morluto/rea/pull/2493), [918713a](https://github.com/morluto/rea/commit/918713af376e307ecf24d75e6a0dd9d1b544545b), [eb97da3](https://github.com/morluto/rea/commit/eb97da33881e3d1472d97a7d788ee35944fb6b07), [2260887](https://github.com/morluto/rea/commit/2260887f2092be6a3d9c79509e4ae4a3e52fce7f), [74d7da6](https://github.com/morluto/rea/commit/74d7da6ced0f869936ee0316b00616931b088066), [ed485a3](https://github.com/morluto/rea/commit/ed485a3dc8cc3a4baf8cabe01acb7aa0490b73b3), [921bb95](https://github.com/morluto/rea/commit/921bb9575505065f6cfd57b14d97d80d0f238546), [d8ceb3f](https://github.com/morluto/rea/commit/d8ceb3f276a65c181b146f68c0729ff4b6f9fa61), [9435c7a](https://github.com/morluto/rea/commit/9435c7aae279ba184937c0dfeb713fd574ba3f29), [bb54082](https://github.com/morluto/rea/commit/bb540826d65316c164d2de8ba4ef10f81a8ad073), [a73a55f](https://github.com/morluto/rea/commit/a73a55f53cc2b2e5b7618effe6ecb5cee6e0cee2), [3c57e1a](https://github.com/morluto/rea/commit/3c57e1a8b21b379fca55032d177078e1d8426e64), [ec0b081](https://github.com/morluto/rea/commit/ec0b081371f118e6e5e042fbef3479359c672ddb), [e7882b3](https://github.com/morluto/rea/commit/e7882b34f4ac4a26f903c5f49264acc2c19bb562), [b955b82](https://github.com/morluto/rea/commit/b955b825fc002a4bb408f2910b78768c566c2cd1)). Thanks @1052326311, @1NormalGuy, @baron, @Bortlesboat, @Chaoqi31, @DeryFerd, @kaoru0822-kitauji, @N0zoM1z0, @XiaoBinGan.
- **Setup/uninstall recovery:** preserve unrelated YAML text, caller-selected roots, symlinked data and existing recovery snapshots; explain malformed configuration shapes and retain failed rollback/cleanup owners. Show terminal dry-run plans and each selected skill location. Bunx registration and fault-injected recovery cases remain simulated rather than live-client verification. ([#1731](https://github.com/morluto/rea/pull/1731), [#1754](https://github.com/morluto/rea/pull/1754), [#1821](https://github.com/morluto/rea/pull/1821), [issue morluto/rea#1816](https://github.com/morluto/rea/issues/1816), [#1983](https://github.com/morluto/rea/pull/1983), [#2118](https://github.com/morluto/rea/pull/2118), [issue morluto/rea#2111](https://github.com/morluto/rea/issues/2111), [#2154](https://github.com/morluto/rea/pull/2154), [issue morluto/rea#2152](https://github.com/morluto/rea/issues/2152), [#2157](https://github.com/morluto/rea/pull/2157), [#2182](https://github.com/morluto/rea/pull/2182), [issue morluto/rea#2159](https://github.com/morluto/rea/issues/2159), [#2481](https://github.com/morluto/rea/pull/2481), [bb50813](https://github.com/morluto/rea/commit/bb508134efbba392a404b651d6ec05be8b7eac1e), [c240768](https://github.com/morluto/rea/commit/c2407689cb04df082e8db0b6afa8f8d022f6bcd1), [dcfc5d4](https://github.com/morluto/rea/commit/dcfc5d403ac997c2b08bee4753e57a1a4a8a8538), [21eff3b](https://github.com/morluto/rea/commit/21eff3bae10f58711a35ad44f86e9f96f9bb4790), [94153f1](https://github.com/morluto/rea/commit/94153f1c757e07a6d092e00903aa8b125d4a0afa), [2397137](https://github.com/morluto/rea/commit/23971376158e7815edcbe6c04a8df7c9504650dc), [c339f9e](https://github.com/morluto/rea/commit/c339f9ebe7bd1f30608d6ccb7175da523dd3c485), [f6775a3](https://github.com/morluto/rea/commit/f6775a36694a6a9c620f4248638e8056a6cbcb66)). Thanks @BeqaKs, @Jax367, @kaoru0822-kitauji, @NIU-123370, @shoyann, @viniciusdsandrade, @xeropresence, @zaydiscold.
- **Bun updates:** locate global installations before changing directories and reuse parsed client activation during maintenance. The update flow is covered with package-manager/filesystem simulations; a real Bun global installation update remains unverified. ([#1735](https://github.com/morluto/rea/pull/1735), [e90acae](https://github.com/morluto/rea/commit/e90acaea674f5f7f9f17ea472104afdc1422ada6), [b045df1](https://github.com/morluto/rea/commit/b045df100e1489e08a36d6ff04cea7feecf1332c)). Thanks @xeropresence.
- **JavaScript relationships:** preserve loader-specific package/TypeScript/CommonJS boundaries, exact file-URL and source-map identity, Electron intrinsic receivers/IPC guards and chunk metadata. Index repeated lookups and retain completed facts through resource limits and cancellation. Real Electron integration, Bun worker integration and failure-path cleanup remain unverified where the recorded lanes use fixtures. ([#1740](https://github.com/morluto/rea/pull/1740), [#1759](https://github.com/morluto/rea/pull/1759), [issue morluto/rea#1748](https://github.com/morluto/rea/issues/1748), [#1762](https://github.com/morluto/rea/pull/1762), [issue morluto/rea#1761](https://github.com/morluto/rea/issues/1761), [#1767](https://github.com/morluto/rea/pull/1767), [#1788](https://github.com/morluto/rea/pull/1788), [issue morluto/rea#1781](https://github.com/morluto/rea/issues/1781), [issue morluto/rea#1782](https://github.com/morluto/rea/issues/1782), [#1789](https://github.com/morluto/rea/pull/1789), [#1799](https://github.com/morluto/rea/pull/1799), [issue morluto/rea#1793](https://github.com/morluto/rea/issues/1793), [#1827](https://github.com/morluto/rea/pull/1827), [issue morluto/rea#1802](https://github.com/morluto/rea/issues/1802), [#1833](https://github.com/morluto/rea/pull/1833), [issue morluto/rea#1831](https://github.com/morluto/rea/issues/1831), [#1857](https://github.com/morluto/rea/pull/1857), [issue morluto/rea#1842](https://github.com/morluto/rea/issues/1842), [#1871](https://github.com/morluto/rea/pull/1871), [#1874](https://github.com/morluto/rea/pull/1874), [#1905](https://github.com/morluto/rea/pull/1905), [#1912](https://github.com/morluto/rea/pull/1912), [issue morluto/rea#1909](https://github.com/morluto/rea/issues/1909), [#1915](https://github.com/morluto/rea/pull/1915), [issue morluto/rea#1911](https://github.com/morluto/rea/issues/1911), [#1916](https://github.com/morluto/rea/pull/1916), [#1921](https://github.com/morluto/rea/pull/1921), [#1934](https://github.com/morluto/rea/pull/1934), [#1939](https://github.com/morluto/rea/pull/1939), [issue morluto/rea#1881](https://github.com/morluto/rea/issues/1881), [issue morluto/rea#1882](https://github.com/morluto/rea/issues/1882), [issue morluto/rea#1883](https://github.com/morluto/rea/issues/1883), [#1941](https://github.com/morluto/rea/pull/1941), [#2062](https://github.com/morluto/rea/pull/2062), [issue morluto/rea#2058](https://github.com/morluto/rea/issues/2058), [#2104](https://github.com/morluto/rea/pull/2104), [#2422](https://github.com/morluto/rea/pull/2422), [#2475](https://github.com/morluto/rea/pull/2475), [#2737](https://github.com/morluto/rea/pull/2737), [issue morluto/rea#2663](https://github.com/morluto/rea/issues/2663), [d3088c3](https://github.com/morluto/rea/commit/d3088c3795f6c37cb3b6aec0bb0c371dd0640ee8), [1dfb933](https://github.com/morluto/rea/commit/1dfb933e47db0156cf1cb61f28715b81e740c57b), [f8513a4](https://github.com/morluto/rea/commit/f8513a42dc44451a621127f06edde5d38bc93f69), [b8fab74](https://github.com/morluto/rea/commit/b8fab7429771bb17a7ca0297661de3c9935212af), [42b0ed1](https://github.com/morluto/rea/commit/42b0ed126df7d5b908ac369715378d5e519051b7), [c0904b4](https://github.com/morluto/rea/commit/c0904b4c1aabfccf2f0ab12f44960a0a1fbbbfae), [9f67e44](https://github.com/morluto/rea/commit/9f67e44e5d451d705bae9a579ad9802d5c9f097c), [e4532b5](https://github.com/morluto/rea/commit/e4532b537663026c3e5b784380eecd009c959a36), [1c37914](https://github.com/morluto/rea/commit/1c37914306b3ff257d7a0360f11c9bf2a3f2cac7), [5ab0cbb](https://github.com/morluto/rea/commit/5ab0cbb48ea5a4f11542eac44479b9af48f1dabd), [b7169c8](https://github.com/morluto/rea/commit/b7169c8b747cbc032913b31f008a3b81c77b6219), [f43fc02](https://github.com/morluto/rea/commit/f43fc0247527aebb8efc7d9db04276fe2d489323), [3ae826c](https://github.com/morluto/rea/commit/3ae826ceba6f25b08bce50bcb51001209bc16b57), [1b95302](https://github.com/morluto/rea/commit/1b953021eadf4cac09e20be388d79536903633b3), [eaa9616](https://github.com/morluto/rea/commit/eaa961658578522cecfbba7c7be1acf1c64da65a), [c923fb0](https://github.com/morluto/rea/commit/c923fb0dc9462e302b84a15770131ff899f89455), [c7a890d](https://github.com/morluto/rea/commit/c7a890d12987d9b2d496119c1fbf1a88c0fc298e)). Thanks @Aurantium-47, @baron, @chqcjq, @coygeek, @cyanseek, @fengdwx, @flitzrrr, @immanuwell, @kaoru0822-kitauji, @N0zoM1z0, @Nelglor, @ranmori, @rudycelekli, @shoyann, @xu91102.
- **Browser observations:** preserve effective source-map URLs, empty query/fragment identity, response-body provenance, service-worker policies and explicit uncertainty for malformed events. Bound discovery lifetimes and retain cleanup ownership. Interrupt/retry/failure cases validated with controlled CDP/session fixtures do not establish those paths in real Chromium. ([#1742](https://github.com/morluto/rea/pull/1742), [issue morluto/rea#1729](https://github.com/morluto/rea/issues/1729), [#1829](https://github.com/morluto/rea/pull/1829), [#2074](https://github.com/morluto/rea/pull/2074), [#2099](https://github.com/morluto/rea/pull/2099), [#2239](https://github.com/morluto/rea/pull/2239), [issue morluto/rea#2169](https://github.com/morluto/rea/issues/2169), [#2477](https://github.com/morluto/rea/pull/2477), [486b567](https://github.com/morluto/rea/commit/486b56724c4eaba50719177bcb9eae84d6907297), [a82e37c](https://github.com/morluto/rea/commit/a82e37c8065cc200a0ca3e14e7c4f85e4e468580), [a8d4984](https://github.com/morluto/rea/commit/a8d4984404b611e60b88bc42289c0ba07eee4578), [966f7cc](https://github.com/morluto/rea/commit/966f7cc84b4a8bb0043641cb64d7a7aaf8715e05), [221c856](https://github.com/morluto/rea/commit/221c85648108e9846d9920b0964dafa7f108d581), [951b095](https://github.com/morluto/rea/commit/951b095e11adf9faa9c4a27e212260060aa6b095), [f09bb8a](https://github.com/morluto/rea/commit/f09bb8a99773f79c7306b8799c86f856fe2c0d4b), [6bd2b28](https://github.com/morluto/rea/commit/6bd2b28161411f70ed5dc72e42c2736a1aab758b)). Thanks @1052326311, @chAng-L19, @chqcjq, @coygeek, @evenafterallisaid, @fisforfaheem, @itsF4LCON, @N0zoM1z0, @shayanxcyber.
- **Managed metadata/CIL:** preserve ExportedType and pointer-table ownership, generic/array/function-pointer/modifier signatures, ReadyToRun host identity, metadata ExtraData and signed zero. Charge retained facts and decoded instruction counts instead of broad decoding bounds; malformed layouts remain caller input errors. ([#1743](https://github.com/morluto/rea/pull/1743), [issue morluto/rea#1736](https://github.com/morluto/rea/issues/1736), [#1768](https://github.com/morluto/rea/pull/1768), [#1822](https://github.com/morluto/rea/pull/1822), [issue morluto/rea#1801](https://github.com/morluto/rea/issues/1801), [#1828](https://github.com/morluto/rea/pull/1828), [issue morluto/rea#1812](https://github.com/morluto/rea/issues/1812), [#1835](https://github.com/morluto/rea/pull/1835), [issue morluto/rea#1832](https://github.com/morluto/rea/issues/1832), [#1886](https://github.com/morluto/rea/pull/1886), [#1893](https://github.com/morluto/rea/pull/1893), [#1897](https://github.com/morluto/rea/pull/1897), [issue morluto/rea#1878](https://github.com/morluto/rea/issues/1878), [#1943](https://github.com/morluto/rea/pull/1943), [issue morluto/rea#1940](https://github.com/morluto/rea/issues/1940), [#1985](https://github.com/morluto/rea/pull/1985), [#2194](https://github.com/morluto/rea/pull/2194), [issue morluto/rea#2191](https://github.com/morluto/rea/issues/2191), [#2437](https://github.com/morluto/rea/pull/2437), [52e76c5](https://github.com/morluto/rea/commit/52e76c5d39b58d1685bb5e22bd40d19524dfa2cb)). Thanks @ac1982, @chqcjq, @fisHarly0, @flitzrrr, @JasonBuildAI, @JayOfTheKeyboard, @jhuang-tw, @kaoru0822-kitauji, @N0zoM1z0, @Nelglor.
- **Hopper failure ownership:** preserve startup/process outcomes, retain cleanup leases across one-shot failures, wait for late bridges, and stop owned Linux demo children when their REA owner exits. Denied/crashed/late-bridge/queued-request failure paths use simulated process or bridge fixtures; they are not fresh installed-Hopper verification. ([#1763](https://github.com/morluto/rea/pull/1763), [issue morluto/rea#1751](https://github.com/morluto/rea/issues/1751), [#1817](https://github.com/morluto/rea/pull/1817), [#1866](https://github.com/morluto/rea/pull/1866), [#1927](https://github.com/morluto/rea/pull/1927), [#2474](https://github.com/morluto/rea/pull/2474), [#2499](https://github.com/morluto/rea/pull/2499), [issue morluto/rea#2459](https://github.com/morluto/rea/issues/2459)). Thanks @coygeek, @Joshwong1908, @kaoru0822-kitauji, @N0zoM1z0, @rudycelekli.
- **IDA session boundaries:** retain non-x86 call coverage and unresolved-procedure uncertainty, and bound private universal-Mach-O inputs to the exact selected slice. Transient headless busy recovery was exercised on macOS arm64 IDA Pro 9.1; Linux headless, attached GUI navigation and worker-reaped cleanup paths retain their documented verification gaps. ([#1764](https://github.com/morluto/rea/pull/1764), [issue morluto/rea#1712](https://github.com/morluto/rea/issues/1712), [#1778](https://github.com/morluto/rea/pull/1778), [issue morluto/rea#1766](https://github.com/morluto/rea/issues/1766), [#1785](https://github.com/morluto/rea/pull/1785), [issue morluto/rea#1769](https://github.com/morluto/rea/issues/1769), [#1906](https://github.com/morluto/rea/pull/1906), [issue morluto/rea#1794](https://github.com/morluto/rea/issues/1794), [#2201](https://github.com/morluto/rea/pull/2201), [issue morluto/rea#2139](https://github.com/morluto/rea/issues/2139), [#2291](https://github.com/morluto/rea/pull/2291), [issue morluto/rea#2285](https://github.com/morluto/rea/issues/2285), [#2324](https://github.com/morluto/rea/pull/2324), [12fa8fd](https://github.com/morluto/rea/commit/12fa8fd9f986dbd4ff58165c3ec71a46dc8a8a3b)). Thanks @akushonkamen, @coygeek, @kaoru0822-kitauji, @N0zoM1z0, @nikolay-dementiev, @zecaro.
- **Input and capability diagnostics:** identify invalid paths, format refusals, missing host tools and non-retryable errors without losing caller selections or retryable capability facts. Foreign execution/device mutation annotations remain destructive, analysis-target text is untrusted, and output ordering follows code points rather than host locale. ([#1770](https://github.com/morluto/rea/pull/1770), [#1867](https://github.com/morluto/rea/pull/1867), [#1869](https://github.com/morluto/rea/pull/1869), [issue morluto/rea#1855](https://github.com/morluto/rea/issues/1855), [#1872](https://github.com/morluto/rea/pull/1872), [#1879](https://github.com/morluto/rea/pull/1879), [issue morluto/rea#1853](https://github.com/morluto/rea/issues/1853), [#1899](https://github.com/morluto/rea/pull/1899), [#1902](https://github.com/morluto/rea/pull/1902), [#1904](https://github.com/morluto/rea/pull/1904), [#1908](https://github.com/morluto/rea/pull/1908), [#1925](https://github.com/morluto/rea/pull/1925), [#1987](https://github.com/morluto/rea/pull/1987), [issue morluto/rea#1979](https://github.com/morluto/rea/issues/1979), [#2004](https://github.com/morluto/rea/pull/2004), [issue morluto/rea#2002](https://github.com/morluto/rea/issues/2002), [#2026](https://github.com/morluto/rea/pull/2026), [#2091](https://github.com/morluto/rea/pull/2091), [#2133](https://github.com/morluto/rea/pull/2133), [issue morluto/rea#2129](https://github.com/morluto/rea/issues/2129), [#2145](https://github.com/morluto/rea/pull/2145), [issue morluto/rea#2143](https://github.com/morluto/rea/issues/2143), [#2472](https://github.com/morluto/rea/pull/2472), [#2478](https://github.com/morluto/rea/pull/2478), [issue morluto/rea#2454](https://github.com/morluto/rea/issues/2454), [#2494](https://github.com/morluto/rea/pull/2494), [#2507](https://github.com/morluto/rea/pull/2507), [#2509](https://github.com/morluto/rea/pull/2509), [#2708](https://github.com/morluto/rea/pull/2708), [3d83135](https://github.com/morluto/rea/commit/3d8313598785359c424d7305f503cb69fc8639a6), [c9fed50](https://github.com/morluto/rea/commit/c9fed50691ae014890e91a72534773d2fb58ce44), [8b18171](https://github.com/morluto/rea/commit/8b181716aca2cb467be499cba226abfb5ae19141), [7a2cd1e](https://github.com/morluto/rea/commit/7a2cd1ea0a63e8c44166860b7ee9c7a1aa43492b)). Thanks @astralyx0129, @CassieFox23, @DeryFerd, @dollarsmoney, @fisforfaheem, @flitzrrr, @Ha1baraA11, @jimpablo, @kaoru0822-kitauji, @larinyo, @N0zoM1z0, @rudycelekli, @SII-YDD, @viniciusdsandrade, @XiaoBinGan.
- **Android analysis:** retain decoded values, duplicate-class inventories and executable static initializers, reject malformed JADX UTF-8, and keep inline search provenance. Apktool accepts `--no-strings` and explains host requirements. Windows batch-launcher selection also resolves PATH/PATHEXT before choosing its launch mode. Recorded Apktool coverage is Linux/OpenJDK, and changed JADX/batch failure paths do not establish native Windows behavior. ([#1777](https://github.com/morluto/rea/pull/1777), [issue morluto/rea#1774](https://github.com/morluto/rea/issues/1774), [#1876](https://github.com/morluto/rea/pull/1876), [issue morluto/rea#1846](https://github.com/morluto/rea/issues/1846), [#1890](https://github.com/morluto/rea/pull/1890), [issue morluto/rea#1850](https://github.com/morluto/rea/issues/1850), [#1946](https://github.com/morluto/rea/pull/1946), [issue morluto/rea#1945](https://github.com/morluto/rea/issues/1945), [#2707](https://github.com/morluto/rea/pull/2707), [issue morluto/rea#2672](https://github.com/morluto/rea/issues/2672), [0677468](https://github.com/morluto/rea/commit/0677468877465c8dfbe25c72f142a7b5bc8a3212), [fe8d0bd](https://github.com/morluto/rea/commit/fe8d0bdd36fe12bc0836468d1a702a34fbdc219b), [#2802](https://github.com/morluto/rea/pull/2802)). Thanks @akushonkamen, @coygeek, @HeadmasterEggy, @kaoru0822-kitauji, @N0zoM1z0, @psipher.
- **ADB acquisition:** preserve device paths and directory identity, prevent host destination collisions/overwrites, count UTF-8 logcat bytes and restricted permissions, and evaluate the complete Activity Manager start report. Launch attempts, status, activity and raw failure output remain observations; real-device timeout/mixed-report and Android 8 directory-read paths remain unverified beyond the documented fixtures. ([#1962](https://github.com/morluto/rea/pull/1962), [#2212](https://github.com/morluto/rea/pull/2212), [issue morluto/rea#2175](https://github.com/morluto/rea/issues/2175), [#2221](https://github.com/morluto/rea/pull/2221), [issue morluto/rea#2220](https://github.com/morluto/rea/issues/2220), [#2359](https://github.com/morluto/rea/pull/2359), [c42232f](https://github.com/morluto/rea/commit/c42232f566d55fe7191763649755148c768eef9f), [55b6148](https://github.com/morluto/rea/commit/55b61485e9f7568b69597eef8696ff6abfe53cfd), [3bdc873](https://github.com/morluto/rea/commit/3bdc8730bd98e073522bcb8ba86527be402c0cbe), [c89972c](https://github.com/morluto/rea/commit/c89972c8777d0114566fe76a060849a18bdd2659), [e1cd292](https://github.com/morluto/rea/commit/e1cd292aa6e8f5f13441d408c66126782c8dbb0d)). Thanks @coygeek, @dakjdakd, @DeryFerd, @huiq777, @sx4im.
- **WASM names and diagnostics:** preserve debug/custom-section names including NUL and leading BOM, accept unpadded WABT section rows, and avoid irrelevant tool-remediation advice on unrelated runtime-root failures. ([#1963](https://github.com/morluto/rea/pull/1963), [issue morluto/rea#1956](https://github.com/morluto/rea/issues/1956), [#1967](https://github.com/morluto/rea/pull/1967), [#1981](https://github.com/morluto/rea/pull/1981), [#2084](https://github.com/morluto/rea/pull/2084), [#2102](https://github.com/morluto/rea/pull/2102)). Thanks @1052326311, @bugstan, @N0zoM1z0, @sathariels, @STiFLeR7, @viniciusdsandrade.
- **Source-map and path handling:** retain source-map entries when embedded-content counts differ or an optional `sourceMappingURL` is used, and keep `file:` URLs out of operator-relative path resolution. ([#2873](https://github.com/morluto/rea/pull/2873), [#2886](https://github.com/morluto/rea/pull/2886), [#2882](https://github.com/morluto/rea/pull/2882), [issue morluto/rea#2821](https://github.com/morluto/rea/issues/2821), [issue morluto/rea#2883](https://github.com/morluto/rea/issues/2883), [issue morluto/rea#2577](https://github.com/morluto/rea/issues/2577)). Thanks @JFSAS, @rileygramlich, @shoyann, @shubhamahuja9999, @coygeek.
- **Native metadata:** accept declared facts in native dispatch inventories and share native verification input diagnostics. ([#2906](https://github.com/morluto/rea/pull/2906), [#2901](https://github.com/morluto/rea/pull/2901), [issue morluto/rea#2564](https://github.com/morluto/rea/issues/2564), [issue morluto/rea#2621](https://github.com/morluto/rea/issues/2621)). Thanks @ShitK, @coygeek.
### ⚠ BREAKING CHANGES
- **Runtime attachment state:** `attached` may be `null` in JavaScript, browser and Electron discovery/inspection results. Treat null as unknown; do not coerce it to false. ([e55ef72](https://github.com/morluto/rea/commit/e55ef7213515ba204c3d88a4487b4ee6a2773b06)).
- **Source maps:** original-source and mapping rows preserve a null source name when unavailable. Handle null explicitly instead of expecting an invented unknown-source string; new index fields remain optional when reading older Evidence. ([ee50ed1](https://github.com/morluto/rea/commit/ee50ed121e38cc6e1a2ff0dc8b789f15f71c5402)).
- **Streamed filtered JSON:** `--filter-output` now emits JSON `null` instead of `{}` when the selected value is null; consumers must distinguish a present null from a missing selection. Use `--full-output` when an object envelope is required. ([#2333](https://github.com/morluto/rea/pull/2333)). Thanks @Jackmeson1.
- **Call graphs:** `get_call_graph.normalized_result` is now `{ graph, limitations }`; read traversal-depth groups under `graph` and retain relationship-specific caveats. Contract/fixture validation does not establish new Ghidra or IDA engine coverage. ([5dfb9d0](https://github.com/morluto/rea/commit/5dfb9d050c2cc44a61b183dae765bef4d22a53b7)).
- **MCP discovery:** `binary_session` no longer repeats `server_identity.catalog.tools`. Use the connected server tool list, grouped `tool_availability` and provider capabilities; see [MCP contracts](docs/mcp-contracts.md). Origin: [6395e8d](https://github.com/morluto/rea/commit/6395e8d3007d5ee453477460e7ee1c51cff76be0). ([#1988](https://github.com/morluto/rea/pull/1988)). Thanks @kaoru0822-kitauji, @N0zoM1z0.
- **Provider admission:** MIPS targets with unresolved GNU ABI attributes are refused. Supply supported ABI declarations. Saved Hopper databases on macOS are unavailable because the launcher cannot run the analysis bridge when opening a database; select an executable target instead. Database support requires a script-capable database launch route. ([d4bd65c](https://github.com/morluto/rea/commit/d4bd65c53c0254d0195cc7ab8da163dcadfd3b9b)).
- **Segment extents:** `binary_overview` segment length may be null when Hopper coordinates cannot establish a finite extent in one address space. Preserve unknown rather than converting it to zero or a saturated value. ([36db19c](https://github.com/morluto/rea/commit/36db19c59)).
- **Invocation identity:** `server.command_path` may be null, and registration/setup statuses or doctor classifications may be unknown. Preserve these states and establish the actual invocation before treating a registration as aligned. ([187f113](https://github.com/morluto/rea/commit/187f113f7)). Thanks @hemv-857, @qorvr.
- **ADB installation:** the optional `replace` input is unchanged. Results now require `replace_requested` to record the requested `adb install -r` mode; `replaced` is nullable because prior device package state was not observed. Update result readers to accept null instead of treating reinstall intent as proof of replacement. ([864314a](https://github.com/morluto/rea/commit/864314ac857924a7e6e65bfb37e8dc720fcc21e7)).
- **Managed/native verification:** unresolved P/Invoke results may retain `matched_native` and candidate facts when module identity is unknown. Interpret `status`, `basis` and `limitations`; `module-identity-unknown` does not verify correspondence, and unresolved no longer implies empty observations. ([3f6b575](https://github.com/morluto/rea/commit/3f6b5755b3e699bd9373641acffe0f2c5d2ab094)).
Full contribution record: [893 commits and 529 merged PRs](https://github.com/morluto/rea/blob/rea-agents-6.5.0/docs/releases/6.5.0.contributions.json).
## [6.4.0](https://github.com/morluto/rea/compare/rea-agents-6.3.0...rea-agents-6.4.0) (2026-10-11)
### Highlights
+6 -40
View File
@@ -567,27 +567,18 @@ def trace(debugger, config):
before_digest = _hash_file(config["executable"])
before_identity = _file_identity(config["executable"])
except OSError as error:
return {
"status": "target-integrity-error",
"error": str(error),
"target_process_created": False,
}
return {"status": "target-integrity-error", "error": str(error)}
if before_digest != config["expected_sha256"]:
return {
"status": "target-integrity-error",
"error": "selected executable digest changed before LLDB target creation",
"target_process_created": False,
}
error = lldb.SBError()
target = debugger.CreateTarget(
config["executable"], config["architecture"], None, False, error
)
if not target.IsValid():
return {
"status": "target-error",
"error": error.GetCString(),
"target_process_created": False,
}
return {"status": "target-error", "error": error.GetCString()}
module_path_before, module_file_identity_before, module_uuid_before = (
_module_identity(target)
)
@@ -598,7 +589,6 @@ def trace(debugger, config):
return {
"status": "target-integrity-error",
"error": "LLDB executable module resolved to a different pre-launch file identity",
"target_process_created": False,
}
created = [_create_breakpoints(target, spec) for spec in config["breakpoints"]]
breakpoint_index = {}
@@ -632,24 +622,12 @@ def trace(debugger, config):
if not error.Success() or not process.IsValid():
stdout.close()
stderr.close()
return {
"status": "launch-error",
"error": error.GetCString(),
**(
{"target_process_created": True}
if process.IsValid()
else {}
),
}
return {"status": "launch-error", "error": error.GetCString()}
launch_failure = _failed_launch_description(process)
if launch_failure is not None:
stdout.close(expected_exit=True)
stderr.close(expected_exit=True)
return {
"status": "launch-error",
"error": launch_failure,
"target_process_created": True,
}
return {"status": "launch-error", "error": launch_failure}
stdout.mark_launched()
stderr.mark_launched()
# Publish the PID before any post-launch file or module inspection. The
@@ -666,7 +644,6 @@ def trace(debugger, config):
return {
"status": "target-identity-unavailable",
"error": "host could not establish the launched process identity; target was stopped",
"target_process_created": True,
}
try:
after_digest = _hash_file(config["executable"])
@@ -675,11 +652,7 @@ def trace(debugger, config):
process.Kill()
stdout.close(expected_exit=process.GetState() == lldb.eStateExited)
stderr.close(expected_exit=process.GetState() == lldb.eStateExited)
return {
"status": "target-integrity-error",
"error": str(identity_error),
"target_process_created": True,
}
return {"status": "target-integrity-error", "error": str(identity_error)}
if after_digest != config["expected_sha256"] or after_identity != before_identity:
process.Kill()
_wait_for_exit(listener, process, 5)
@@ -688,7 +661,6 @@ def trace(debugger, config):
return {
"status": "target-integrity-error",
"error": "selected executable digest or file identity changed during LLDB launch",
"target_process_created": True,
}
module_path_after, module_file_identity_after, module_uuid_after = (
_module_identity(target)
@@ -704,7 +676,6 @@ def trace(debugger, config):
return {
"status": "target-integrity-error",
"error": "LLDB executable module resolved to a different file identity",
"target_process_created": True,
}
if (
module_uuid_before is not None
@@ -718,7 +689,6 @@ def trace(debugger, config):
return {
"status": "target-integrity-error",
"error": "LLDB executable module identity changed during launch",
"target_process_created": True,
}
context = {
"target": target,
@@ -778,11 +748,7 @@ def trace(debugger, config):
# exit code; preserve the debugger's concrete launch denial.
launch_failure = _failed_launch_description(process)
if launch_failure is not None:
return {
"status": "launch-error",
"error": launch_failure,
"target_process_created": True,
}
return {"status": "launch-error", "error": launch_failure}
launch_identity_stable = (
module_path_before is not None
and module_path_after == module_path_before
-37
View File
@@ -8,7 +8,6 @@ import sys
import tempfile
import types
import unittest
from unittest.mock import patch
fake_lldb = sys.modules.setdefault("lldb", types.ModuleType("lldb"))
@@ -23,42 +22,6 @@ spec.loader.exec_module(tracer)
class NativeTraceBudgetTests(unittest.TestCase):
def test_pre_target_integrity_failure_reports_no_process_created(self):
original_debugger = getattr(tracer.lldb, "SBDebugger", None)
class DebuggerApi:
@staticmethod
def SetInternalVariable(*_arguments):
return None
class Debugger:
def SetAsync(self, _enabled):
return None
def GetInstanceName(self):
return "fixture"
def CreateTarget(self, *_arguments):
raise AssertionError("integrity failure precedes target creation")
tracer.lldb.SBDebugger = DebuggerApi
try:
with patch.object(tracer, "_hash_file", return_value="changed"), patch.object(
tracer, "_file_identity", return_value=(1, 2)
):
result = tracer.trace(
Debugger(),
{"executable": "fixture", "expected_sha256": "expected"},
)
finally:
if original_debugger is None:
del tracer.lldb.SBDebugger
else:
tracer.lldb.SBDebugger = original_debugger
self.assertEqual(result["status"], "target-integrity-error")
self.assertIs(result["target_process_created"], False)
def test_journal_checks_exact_byte_budget_before_writing(self):
record = {"kind": "other-stop", "reason": "signal"}
encoded_size = len((tracer.json.dumps(record) + "\n").encode("utf-8"))
+5 -8
View File
@@ -259,14 +259,14 @@ Register with the variable unset:
<!-- x-release-please-start-version -->
```bash
claude mcp add --scope local rea -- npx -y rea-agents@6.5.0 mcp
claude mcp add --scope local rea -- npx -y rea-agents@6.4.0 mcp
```
On native Windows, use the same project directory and wrap the package-runner
shim so the stdio client can launch it:
```powershell
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.5.0 mcp
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.4.0 mcp
```
<!-- x-release-please-end -->
@@ -398,9 +398,8 @@ The npm release checked on **2026-10-11** was **6.4.0**, published from
[`a56f7bb6`](https://github.com/morluto/rea/commit/a56f7bb668ab23fec8d33f7af353ac7ed2fe1895).
The actual registry package accepted dry-run setup plans for the 17 client IDs
in that release, including Qwen Code, Pi and Hermes. DeepSeek Harness (`dsh`)
is included from 6.5.0 and was absent from the checked npm 6.4.0 package.
The 6.4.0 registry check does not establish 6.5.0 publication or live DeepSeek
API integration.
is on main and is not in npm 6.4.0; `rea setup --client dsh` against `@latest`
reports an unsupported client until a release containing it is published.
OpenCode's plan selected
`OPENCODE_CONFIG_DIR` even when that directory did not yet exist. These checks
used an isolated home on macOS with Node 24; they verify planning and argument
@@ -585,8 +584,6 @@ system application, the canonical application installed by `rea setup` under
diagnostic; to use a Homebrew-installed launcher for CLI/MCP startup, set
`HOPPER_LAUNCHER_PATH` to the path reported by doctor. REA does not run Homebrew
while loading runtime configuration.
Automatic discovery accepts executable regular files, including file symlinks,
and skips directories.
On Linux, it prefers executable `/opt/hopper/bin/Hopper`, then executable
`~/.local/share/rea/hopper/bin/Hopper`, followed by `/opt/hopper-v4/bin/hopper`
and `/usr/bin/hopper`; if none exists, the former remains the diagnostic
@@ -1057,7 +1054,7 @@ For a client that requires manual configuration, use:
"mcpServers": {
"rea": {
"command": "npx",
"args": ["-y", "rea-agents@6.5.0", "mcp"]
"args": ["-y", "rea-agents@6.4.0", "mcp"]
}
}
}
+13 -64
View File
@@ -167,14 +167,7 @@ MCP uses `inspect_managed_members` with
Repeat `--type` or supply multiple unique `type_names` for several types.
Names match the reported `full_name` exactly. Top-level names are namespace
qualified; nested types currently report bare names, which can select several
types with that name. Each type's `enclosing_type_token` records its enclosing
TypeDef. `nesting_read` distinguishes complete source rows (true), incomplete
current rows (false), and historical Evidence (null). A null enclosing token
alone does not establish that a type is top-level.
Each selected type also retains `name_sha256`, a framed digest of its namespace,
name and enclosing names. It uses the same bounded, cached owner as signature
type operands and remains available when selection omits the enclosing types.
It adds one fixed-size digest per returned type rather than repeated name paths.
types with that name.
The result preserves the requested names in `selection.type_names` and reports
unmatched names separately. If metadata names are unreadable, limitations retain
@@ -250,16 +243,9 @@ native/runtime questions retain their own authority and coverage.
Names and semantic labels do not establish durable member identity. Comparison
preserves observed differences, structural inferences, and unknowns separately.
Cross-version matching first pairs members of the same declared type and name.
A current declared type uses its observed `name_sha256`, so same-named nested
types in different enclosing types stay distinct even in selected inventories.
Malformed, missing, contradictory, cyclic or over-deep enclosing identity remains unknown;
the comparison preserves display names without using them to repair that gap.
Historical observations retain an explicitly limited bare-name mode when either
side predates the identity facts. A known current identity gap still remains
unknown beside a historical observation. Matching then pairs
methods by exact CIL/signature identity, then by signature, then by
structural body shape; fields by signature. Exact CIL/signature identity
Cross-version matching first pairs members of the same declared type and name:
methods by exact CIL/signature identity, then by raw signature, then by
structural body shape; fields by raw signature. Exact CIL/signature identity
requires equal raw CIL digests and decoded fingerprints. Equal decoded
fingerprints alone cannot establish an exact match: they omit switch targets
and collapse distinct NaN encodings. Only then may a remap pair a
@@ -267,56 +253,23 @@ renamed or moved member across declared types or names: a method by exact
CIL/signature identity or structural body shape, a field by exact decoded
signature. A remap never takes a member whose own declared type and name remain
unpaired on the other side, so a member is never taken from its own changed
counterpart. A member whose declared identity is unresolved could belong to any type. It
never makes a declared-identity pair, and any remap group it joins stays
ambiguous. Unresolved same-named counterparts prevent definitive added/removed
claims, while observed names and tokens remain available in the result.
The exact-signature key uses names only as part of that
counterpart. A member whose declaring type is unresolved could belong to any
type, so it pairs by name only when no other member shares its exact
CIL/signature key, never by structural shape alone, and any remap group it
joins stays ambiguous. The exact-signature key uses names only as part of that
full tuple; names alone never select a pair. Duplicate tuples remain ambiguous
rather than being paired by token order. Every round compares signatures by
`resolved_sha256`: the raw signature with each TypeDef or TypeRef operand
replaced by a digest of that type's namespace and name, hashed as separate
values so `A` + `B.C` differs from `A.B` + `C`, after its enclosing types for a
nested type (`Outer/Inner`). The defining assembly is not part of the name:
a TypeDef and a TypeRef with the same namespace and name compare as the
same nominal name, and a TypeRef's ResolutionScope assembly is ignored, so a type
forwarded from one assembly to another still pairs. Two different types with
the same full name in different assemblies therefore have equal name digests.
Comparison reports this scope limitation inline; it does not establish CLR
type identity or runtime binding equivalence.
Nesting deeper than 64 levels is not named, and such a signature compares by
its raw digest. Comparison keys keep resolved and raw digests apart, so a raw
digest never pairs with an equal resolved one.
Raw signature bytes name types by metadata row, and rows are build-local, so
the same signature usually has different raw bytes in two builds. A signature
that names no types resolves to its raw digest. When an operand cannot be
resolved, such as a TypeSpec, or when REA could not parse the signature, the
raw digest stands in. Missing or unreadable TypeDef and TypeRef operands are
reported as coverage issues. Invalid NestedClass rows retain their failure
reasons and withhold TypeDef signature identities; type names and nesting are
parsed once by their shared owner. Each current inspection records
`signature_resolution: "type-names-v1"`, even when none of its operands can be
resolved. Historical Evidence defaults this attestation to `null`. When either
observation predates resolution, both sides compare raw digests, which pair only where type rows agree,
and the comparison reports that limitation. The signature is exact whether or
rather than being paired by token order. The raw signature is exact whether or
not REA decoded it, so undecoded signatures pair by that tuple too, but they
never enter structural rounds. An unmatched member is therefore reported as
`unknown`, not added or removed, when its own signature was not decoded or when
the other side has an unpaired member, one-sided or ambiguous, with the same
declared type and name whose signature was not decoded. When both observations
attest type-name resolution, a decoded signature that still compares by its raw
digest is treated the same way: an unmatched member with one, or beside an
unpaired same-named member with one, is `unknown`, because its raw bytes name
types by build-local row. A matched unresolved signature also remains unknown
unless another observed dimension establishes a change; identical row bytes
can reference different TypeSpec contents. Its resolution limitation is retained
even when another dimension changes.
declared type and name whose signature was not decoded.
For a matched method, an unavailable or partial body makes body-shape facets
unknown while preserving observed signature differences. For complete bodies,
a difference in either the raw CIL digest or the decoded fingerprint is reported
in the `cil` dimension, including reordered switch targets with equal decoded
fingerprints. Structural identity compares resolved signatures, the limited
fingerprints. Structural identity compares normalized signatures, the limited
decoded-CIL tuple fingerprint,
constants, and bounded shape context. It reports all candidates at the winning
score and remains ambiguous when the evidence does not distinguish them. The
@@ -370,9 +323,6 @@ function implements a specific managed method.
`verify_managed_native_boundaries` compares declared P/Invoke module and entry
point identities with supplied native export or function evidence. Name/module
matches establish declaration links; they do not establish that a call executed.
When either side lacks a usable module identity, a matching name remains
unresolved and retains the candidate evidence; REA reports a module mismatch
only when both identities are available and differ.
Metadata tokens are not native addresses. Native body mapping for C++/CLI,
ReadyToRun, NativeAOT, and IL2CPP requires separately verified format and provider
@@ -410,9 +360,8 @@ these indirections for member ownership and local attribute-constructor types;
field and method tokens still identify the original definition rows.
`field_list` and `method_list` retain the metadata's list coordinates, which
refer to the pointer table when present rather than necessarily contiguous
definition rows. Invalid or nonmonotonic list coordinates and malformed or
ambiguous pointers leave affected owners unknown with partial coverage while
preserving readable member and attribute facts.
definition rows. Malformed or ambiguous pointers leave the owner unknown with
partial coverage while preserving readable member and attribute facts.
The retained inverse indexes are admitted against the existing representation
budget before allocation.
File diff suppressed because it is too large. Load diff
-53
View File
@@ -10,64 +10,11 @@ SHA returned by Release Please. Before either registry is published, the release
run calls full CI qualification at that exact revision, including coverage,
portable providers, and every native package host. Later main commits cannot
change or cancel that proof.
After qualification, a separate runner with read-only repository/actions
permissions restores the retained tarball and verifier runtime. It installs
that exact archive into an empty prefix, then exercises CLI help/discovery and
the real stdio MCP catalog/session contract. Installation finishes before the
protocol deadline begins, with dependency lifecycle scripts disabled. Only a
successful rehearsal enables the job with npm publishing authority. Both jobs
use the same release-input action; neither rebuilds or repacks the archive.
This rehearsal checks package consumption on the Linux release runner;
qualification retains its broader native-host and real-provider checks.
Before publishing, npm lookup failures and malformed metadata stop the job.
An existing version must match the qualified tarball's SHA-512 and intended
`latest`/`next` dist-tag before it can be reused. Publication never overwrites
an existing version or silently changes a dist-tag. After publication, only
confirmed registry absence is retried within the propagation window. Content
or tag mismatches fail immediately. The post-publication verifier installs
from npm and repeats the same CLI/MCP checks; it also exercises a real update
from an older stable release. For a prerelease on `next`, that updater still
targets stable `latest`, as the public update command does.
The required reverse-tools lane runs fresh coverage and both mutation
preflights. Complete mutation sweeps run separately on a schedule or manual
dispatch and do not block publication; see [the testing guide](testing.md).
Ordinary main pushes and closed, unmerged PRs cannot publish packages. Check
both publication jobs and the published-package canary; a merged PR or GitHub
tag alone does not establish that publication finished. Retry failed jobs in
the original run after a partial publication.
### Recover an already published npm release
Rerunning a job uses its original workflow and source revision. When a verifier
or publishing workflow needs a code fix, land that fix on main first, then use
the separate recovery workflow instead of repeating release creation:
```bash
gh workflow run release-recovery.yml --ref main \
-f version=6.5.0 -f release_run_id=38564502044
```
Select the existing version and its original automatic Release run. Recovery
requires that run's successful exact-source CI gate and npm publish step. It
uses the existing release tag, original qualified tarball and verifier runtime;
expired or missing artifacts stop recovery. The archive's source, version,
original run/attempt and Node version must match, and its SHA-512 must equal
npm's published integrity. The corrected controller runs the public CLI, MCP
catalog/session and previous-version update canary against that released runtime.
The installation completes before MCP's handshake begins; dependency lifecycle
scripts are disabled as in the installed-package verifier.
Only after verification succeeds does recovery publish the released
`server.json` to MCP Registry. It does not create tags or GitHub releases,
publish npm bytes, or change dist-tags. Both publication paths share the same
MCP publisher: a paginated registry lookup preserves an identical active
version, refuses conflicting/inactive metadata, and fails on lookup errors.
The original failed run remains a historical failure; the recovery run records
the new verification and remaining registry publication. Manual frozen-branch
releases are outside this recovery path.
For a release that needs an independently frozen application checkpoint, use
the optional manual path below. It retains explicit prepare and publish phases.
-27
View File
@@ -280,33 +280,6 @@ focused suite also exercises affected MCP routes and the Python Cutter bridge.
Contract, integration, and real-provider lanes complement these focused tests;
simulated providers do not establish real-provider behavior.
Required CI retains fresh reverse-tools coverage and preflights both canonical
Stryker configurations with `--dryRunOnly`. These preflights resolve the owned
targets, instrument the source, and execute the real initial tests. Invalid
configuration, target resolution, and initial test failures still fail the job.
Preflights execute no mutants and produce no mutation-outcome report. Both fast
feedback and full qualification, including exact-revision release qualification,
require coverage and both preflights. A failed, canceled, skipped or missing
selected quality job still fails the required gate.
The separate **Mutation sweep** workflow runs both complete configurations daily
and on manual dispatch, in two isolated jobs. It is a maintenance check outside
the PR and release publication gates. `ci:full` requests full regression,
coverage and provider qualification; it does not request a complete mutation
sweep. Use the Mutation sweep workflow to request that result. Targets,
checkers, concurrency, deadlines and the existing non-blocking mutation-score
threshold are unchanged. `npm run test:mutation:reverse-tools` also retains both
complete passes for local callers.
The two passes write separate JSON and HTML reports under `reports/mutation/`:
`reverse-tools` and `reverse-tools-semantics`. Required CI retains focused
coverage; the sweep retains each configuration's JSON/HTML reports separately.
Both retain available diagnostics for seven days after failure and skip uploads
on cancellation. Artifact names identify the tier or configuration and run
attempt. A successful sweep requires both jobs and their nonempty reports;
partial diagnostics and preflights do not establish complete mutation outcomes
or qualify the release revision for mutation coverage.
Fast CI starts the source-only projects after classification, independently of
build and static checks, and runs five built-dependent shards after the build.
The six test jobs still select every portable test. Full qualification keeps six
@@ -78,12 +78,6 @@ field/filter selection, discovery and output windows. Lower delivered JSON did
not establish stable total-token or speed gains; failed plans and negative
controls remain visible.
The [sigrok selection checkpoint](signal-selection-workflows.md) records two
real hardware recordings, independently verified bytes, repeated decoder/pages,
and an eight-task Codex cohort with mixed token/time results. Existing annotation
controls reduce replies; these measurements introduce no production cache,
default or API changes.
Agent token consumption and end-to-end task time are the primary objectives.
Memory and CPU consumption are also measured and controlled. Correctness,
Evidence provenance, caller choices and existing-user compatibility are
@@ -182,7 +182,6 @@ def main():
'inspect_firmware_regions', 'inspect_managed_artifact',
'inspect_sqlite_database', 'inspect_pcap_capture',
'inspect_packet_capture', 'inspect_packet_fields',
'decode_signal_recording', 'inspect_signal_decoders',
'inspect_managed_members', 'inspect_managed_native_boundaries',
'inspect_artifact', 'inspect_wasm_artifact',
'inspect_evm_interface', 'get_evidence_bundle', 'export_evidence_bundle',
+3 -9
View File
@@ -3,7 +3,6 @@ import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { parseArgs } from "node:util";
import { PACKET_ARTIFACTS } from "./packet-entry.mjs";
import { SIGNAL_ARTIFACTS } from "./signal-entry.mjs";
const { values } = parseArgs({
options: {
@@ -37,8 +36,6 @@ const sameSettings = [
"native_workflow_source_sha256",
"packet_workflow_source_sha256",
"packet_oracle_source_sha256",
"signal_workflow_source_sha256",
"signal_oracle_source_sha256",
"package_lock_sha256",
"node_version",
"sdk_client_version",
@@ -76,14 +73,13 @@ for (const [side, answer] of [
value: answer.evidence_ids,
});
}
// Recording delivery observations preserve each actual receipt's identity, limitations
// PCAP delivery observations preserve each actual receipt's identity, limitations
// and view coverage. Summary/page scopes differ from complete source scope;
// retain them separately rather than equating page exhaustion with capture
// coverage. Repeated equivalent deliveries must also match every observation
// field except the receipt and parent IDs, which identify their own invocations.
if (
Object.hasOwn(PACKET_ARTIFACTS, left.artifact) ||
Object.hasOwn(SIGNAL_ARTIFACTS, left.artifact) ||
["wireshark-http.cap", "wireshark-http_redirects.pcapng"].includes(
left.artifact,
)
@@ -96,7 +92,7 @@ if (
!Array.isArray(answer.delivery_observations) ||
!answer.delivery_observations.length
)
throw new Error("Missing original recording delivery observations");
throw new Error("Missing original PCAP delivery observations");
runSpecific.push({
side,
path: ["delivery_observations"],
@@ -105,9 +101,7 @@ if (
if (
left.delivery === right.delivery &&
(!Object.hasOwn(PACKET_ARTIFACTS, left.artifact) ||
left.packet_selection === right.packet_selection) &&
(!Object.hasOwn(SIGNAL_ARTIFACTS, left.artifact) ||
left.signal_selection === right.signal_selection)
left.packet_selection === right.packet_selection)
) {
answer.delivery_observations = answer.delivery_observations.map(
(observation) =>
@@ -343,36 +343,6 @@
"derivation": "Unmodified official saved sample; decompressed gzip where indicated. Local digest pins acquired bytes, not an upstream-published digest.",
"compressed_source": null
}
},
{
"id": "sigrok-uart-max3232e.sr",
"file": "uart-max3232e.sr",
"bytes": 12742,
"sha256": "f797d60d6b8e12d2a1549b9a5a499ef389f44cb9e870e8610af26b57c3b2be76",
"provenance": {
"project": "sigrok-dumps",
"version": "0ad13477abc959d37fc9a5acbd23901c371c9c76",
"upstreamFile": "uart/maxim_max3232e/max3232e_hello_world_57600_8n1.sr",
"url": "https://raw.githubusercontent.com/sigrokproject/sigrok-dumps/0ad13477abc959d37fc9a5acbd23901c371c9c76/uart/maxim_max3232e/max3232e_hello_world_57600_8n1.sr",
"git_blob_sha1": "52e47b3014819471a2f348a0d8c7694fe304e052",
"reference": "https://raw.githubusercontent.com/sigrokproject/sigrok-dumps/0ad13477abc959d37fc9a5acbd23901c371c9c76/uart/maxim_max3232e/README",
"license_scope": "Repository README declares dumps public domain unless otherwise noted; selected READMEs state no exception; decoder software licensing is separate."
}
},
{
"id": "sigrok-i2c-nunchuk.sr",
"file": "i2c-nunchuk.sr",
"bytes": 20373,
"sha256": "0fa6adb8297d7f3fe5ba19c94ee655c9b0f952a8b5e48a17aaa499a2f6d10d70",
"provenance": {
"project": "sigrok-dumps",
"version": "0ad13477abc959d37fc9a5acbd23901c371c9c76",
"upstreamFile": "i2c/wii_nunchuk/wii_nunchuk_init_reg_3xdata.sr",
"url": "https://raw.githubusercontent.com/sigrokproject/sigrok-dumps/0ad13477abc959d37fc9a5acbd23901c371c9c76/i2c/wii_nunchuk/wii_nunchuk_init_reg_3xdata.sr",
"git_blob_sha1": "a7232fce7b54e68e2be44e15946c90a3c756e8e9",
"reference": "https://raw.githubusercontent.com/sigrokproject/sigrok-dumps/0ad13477abc959d37fc9a5acbd23901c371c9c76/i2c/wii_nunchuk/README",
"license_scope": "Repository README declares dumps public domain unless otherwise noted; selected READMEs state no exception; decoder software licensing is separate."
}
}
]
}
@@ -13,7 +13,6 @@ import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { Ajv2020 } from "ajv/dist/2020.js";
import { PACKET_ARTIFACTS, measurePacketEntry } from "./packet-entry.mjs";
import { SIGNAL_ARTIFACTS, measureSignalEntry } from "./signal-entry.mjs";
const root = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
const { values } = parseArgs({
@@ -32,7 +31,6 @@ const { values } = parseArgs({
schemas: { type: "string", default: "default" },
"sqlite-selection": { type: "string", default: "all" },
"packet-selection": { type: "string", default: "baseline" },
"signal-selection": { type: "string", default: "baseline" },
},
});
if (
@@ -43,13 +41,6 @@ if (
!isAbsolute(values.path) ||
!isAbsolute(values.output) ||
!["complete", "summary"].includes(values.delivery) ||
!["baseline", "focused", "discovery", "pages"].includes(
values["signal-selection"],
) ||
(!Object.hasOwn(SIGNAL_ARTIFACTS, values.artifact) &&
values["signal-selection"] !== "baseline") ||
(Object.hasOwn(SIGNAL_ARTIFACTS, values.artifact) &&
values.delivery !== "complete") ||
!["entry", "entry-and-main", "entry-views"].includes(
values["native-question"],
) ||
@@ -168,12 +159,6 @@ const fingerprints = {
packet_oracle_source_sha256: await fileSha(
fileURLToPath(new URL("./packet-oracle.py", import.meta.url)),
),
signal_workflow_source_sha256: await fileSha(
fileURLToPath(new URL("./signal-entry.mjs", import.meta.url)),
),
signal_oracle_source_sha256: await fileSha(
fileURLToPath(new URL("./signal-oracle.py", import.meta.url)),
),
runtime_files_sha256: sha(JSON.stringify(runtimeFiles)),
runtime_dispatcher_sha256: await fileSha(
resolve(values["runtime-root"], "scripts/rea.mjs"),
@@ -754,12 +739,6 @@ const inspectPcapTarget = async () => {
};
const workflows = {
...Object.fromEntries(
Object.keys(SIGNAL_ARTIFACTS).map((artifact) => [
artifact,
() => measureSignalEntry({ values, call, inputSha, catalogDefinitions }),
]),
),
...Object.fromEntries(
Object.keys(PACKET_ARTIFACTS).map((artifact) => [
artifact,
@@ -1207,9 +1186,6 @@ const report = {
packet_selection: Object.hasOwn(PACKET_ARTIFACTS, values.artifact)
? values["packet-selection"]
: null,
signal_selection: Object.hasOwn(SIGNAL_ARTIFACTS, values.artifact)
? values["signal-selection"]
: null,
provider_settings_sha256: sha(JSON.stringify(selectedProviders)),
provider_search_path_sha256: sha(
selectedProviders.PATH ?? process.env.PATH ?? "",
@@ -1,237 +0,0 @@
import { readFile, writeFile } from "node:fs/promises";
import { resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { isDeepStrictEqual, parseArgs } from "node:util";
import {
SIGNAL_ARTIFACTS,
signalRequest,
signalDataRows,
signalFacts,
verifySignalRows,
verifySignalOracleIdentity,
} from "./signal-entry.mjs";
const { values } = parseArgs({
options: {
trial: { type: "string" },
oracle: { type: "string" },
"runtime-root": { type: "string" },
artifact: { type: "string" },
selection: { type: "string" },
},
});
if (
!values.trial ||
!values.oracle ||
!values["runtime-root"] ||
!Object.hasOwn(SIGNAL_ARTIFACTS, values.artifact ?? "") ||
!["baseline", "focused"].includes(values.selection)
)
throw new Error(
"Use --trial DIR --oracle JSON --runtime-root DIR --artifact ID --selection baseline|focused",
);
const json = async (name) =>
JSON.parse(await readFile(resolve(values.trial, name), "utf8"));
const settings = await json("trial-settings.json"),
summary = await json("summary.json");
const oracle = JSON.parse(await readFile(values.oracle, "utf8"));
const kind = SIGNAL_ARTIFACTS[values.artifact].kind;
const expectedRows =
kind === "uart"
? oracle.rows
: oracle.reads.flatMap((read) => read.bytes.slice(1));
const expected = signalFacts(kind, expectedRows);
const runtime = (file) =>
pathToFileURL(resolve(values["runtime-root"], file)).href;
const { parseEvidence } = await import(runtime("dist/domain/evidence.js"));
const { decodeSignalRecordingInputSchema, signalRecordingSchema } =
await import(runtime("dist/domain/signals/signalRecording.js"));
const { inspectSignalDecodersInputSchema, signalDecodersSchema } = await import(
runtime("dist/domain/signals/signalDecoders.js")
);
const calls = (await readFile(resolve(values.trial, "events.jsonl"), "utf8"))
.split("\n")
.filter(Boolean)
.map(JSON.parse)
.filter(
(event) =>
event.type === "item.completed" && event.item?.type === "mcp_tool_call",
)
.map((event) => event.item);
const reasons = [],
workflowReasons = [],
authenticated = new Map(),
conflicting = new Set();
try {
verifySignalOracleIdentity(values.artifact, oracle);
if (oracle.kind !== kind)
throw new Error("Wrong independent signal question oracle");
if (calls.length !== 1 || calls[0]?.tool !== "decode_signal_recording")
workflowReasons.push(
"Completed public call sequence differs from the fixed single-call plan",
);
const planned = decodeSignalRecordingInputSchema.parse(
signalRequest(
values.artifact,
settings.selected_artifact_path,
values.selection,
),
);
for (const call of calls) {
try {
if (
call.server !== "rea" ||
call.status !== "completed" ||
call.error ||
call.result?.isError
)
throw new Error("Call did not return successful REA observations");
const text =
call.result?.content?.filter((item) => item.type === "text") ?? [];
if (!call.result?.structuredContent && text.length !== 1)
throw new Error("No unambiguous Evidence envelope");
const evidence =
call.result.structuredContent ?? JSON.parse(text[0].text);
parseEvidence(evidence);
const capture = call.tool === "decode_signal_recording";
if (!capture && call.tool !== "inspect_signal_decoders")
throw new Error("Not a signal observation tool");
const input = (
capture
? decodeSignalRecordingInputSchema
: inspectSignalDecodersInputSchema
).parse(call.arguments);
const report = (
capture ? signalRecordingSchema : signalDecodersSchema
).parse(evidence.normalized_result);
if (
evidence.operation !== call.tool ||
!isDeepStrictEqual(evidence.parameters, input) ||
!isDeepStrictEqual(report.selection, input)
)
throw new Error(
"Evidence differs from actual operation or caller choices",
);
if (
capture
? evidence.subject?.local_path !== input.path ||
evidence.subject?.digest?.sha256 !== oracle.sha256 ||
report.artifact.path !== input.path ||
report.artifact.sha256 !== oracle.sha256
: evidence.subject?.local_path !== report.tool.path ||
evidence.subject?.digest?.sha256 !== report.tool.sha256
)
throw new Error("Evidence identifies different recording/tool bytes");
if (capture && !isDeepStrictEqual(input, planned))
workflowReasons.push("Capture selection differs from the fixed plan");
const observation = {
operation: call.tool,
input,
report,
rows: capture ? signalDataRows(kind, report.annotations_text) : null,
};
const id = evidence.evidence_id;
if (conflicting.has(id))
throw new Error("Previously conflicting Evidence identity");
if (
authenticated.has(id) &&
!isDeepStrictEqual(authenticated.get(id), observation)
) {
authenticated.delete(id);
conflicting.add(id);
throw new Error(
"Repeated Evidence identity has inconsistent observations",
);
}
authenticated.set(id, observation);
} catch (error) {
workflowReasons.push(
`Call ${call.tool} did not provide authenticated observations: ${String(error)}`,
);
}
}
const answer = JSON.parse(
(await readFile(resolve(values.trial, "answer.txt"), "utf8")).trim(),
);
if (
!isDeepStrictEqual(
Object.keys(answer).sort(),
[...Object.keys(expected), "evidence_ids", "limitations"].sort(),
)
)
workflowReasons.push("Final answer shape differs from the fixed plan");
if (
!isDeepStrictEqual(
Object.fromEntries(
Object.keys(expected).map((key) => [key, answer[key]]),
),
expected,
)
)
throw new Error("Final answer differs from independent sample facts");
if (
!Array.isArray(answer.evidence_ids) ||
new Set(answer.evidence_ids).size !== answer.evidence_ids.length ||
answer.evidence_ids.some((id) => !authenticated.has(id))
)
throw new Error(
"Final citations are not authenticated signal observations",
);
const rows = new Map();
for (const id of answer.evidence_ids) {
for (const row of authenticated.get(id).rows ?? []) {
const old = rows.get(row.start);
if (old && !isDeepStrictEqual(old, row))
throw new Error("Cited sample ranges have conflicting decoded data");
rows.set(row.start, row);
}
}
verifySignalRows(
kind,
[...rows.values()].sort((a, b) => a.start - b.start),
oracle,
oracle.sha256,
);
const limitations =
typeof answer.limitations === "string"
? [answer.limitations]
: answer.limitations;
if (
!Array.isArray(limitations) ||
limitations.length === 0 ||
limitations.some((value) => typeof value !== "string" || !value.trim())
)
workflowReasons.push("Final answer omits investigation limitations");
if (
summary.exit_code !== 0 ||
summary.timeout ||
summary.selected_artifact_bytes_unchanged !== true ||
settings.selected_artifact_sha256_before !== oracle.sha256 ||
summary.selected_artifact_sha256_after !== oracle.sha256
)
workflowReasons.push(
"Trial did not complete with original selected bytes unchanged",
);
} catch (error) {
reasons.push(String(error));
}
const report = {
factual_correctness: reasons.length ? "failed" : "passed",
workflow_correctness:
reasons.length || workflowReasons.length ? "failed" : "passed",
reasons,
workflow_reasons: workflowReasons,
completed_calls: calls.length,
wall_seconds: summary.wall_seconds,
usage: summary.usage,
selected_artifact_sha256: oracle.sha256,
scope:
"Two pinned single-device recordings; independently sampled bytes and authenticated cited decoder observations; hardware behavior is not inferred",
limitation_prose: "Presence checked; wording requires transcript review",
};
await writeFile(
resolve(values.trial, "signal-review.json"),
JSON.stringify(report, null, 2) + "\n",
);
console.log(JSON.stringify(report));
if (reasons.length || workflowReasons.length) process.exitCode = 1;
@@ -1,264 +0,0 @@
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
import { isDeepStrictEqual, promisify } from "node:util";
const corpus = JSON.parse(
await readFile(new URL("./corpus.json", import.meta.url), "utf8"),
);
export const SIGNAL_ARTIFACTS = {
"sigrok-uart-max3232e.sr": {
kind: "uart",
decoders: "uart:rx=MAX3232E DOUT1:baudrate=57600:format=hex:invert_rx=yes",
annotations: "uart=rx-data",
query: "rx-data",
firstPage: 32,
},
"sigrok-i2c-nunchuk.sr": {
kind: "i2c",
decoders: "i2c:scl=SCL:sda=SDA",
annotations: "i2c=data-read",
query: "data-read",
firstPage: 9,
},
};
export function signalRequest(artifact, path, selection) {
const descriptor = SIGNAL_ARTIFACTS[artifact];
if (!descriptor) throw new Error("Unknown fixed signal question");
return {
path,
decoders: descriptor.decoders,
input_format: null,
annotations: selection === "baseline" ? null : descriptor.annotations,
line_offset: 0,
line_limit: selection === "pages" ? descriptor.firstPage : null,
};
}
export function signalDataRows(kind, text) {
const pattern =
kind === "uart"
? /^(\d+)-(\d+) uart-1: ([0-9A-F]{2})$/
: /^(\d+)-(\d+) i2c-1: Data read: ([0-9A-F]{2})$/;
const rows = [];
for (const line of text.split("\n")) {
const match = pattern.exec(line);
if (!match) continue;
const start = Number(match[1]),
end = Number(match[2]);
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(end) ||
start > end
)
throw new Error("Unsupported decoded sample interval");
rows.push({ start, end, byte: Number.parseInt(match[3], 16) });
}
return rows;
}
export function signalFacts(kind, rows) {
if (kind === "uart") return { kind, bytes: rows.map((row) => row.byte) };
if (kind !== "i2c" || rows.length !== 18)
throw new Error(
"The fixed I2C question requires three complete six-byte reads",
);
return {
kind,
reads: [0, 6, 12].map((offset) =>
rows.slice(offset, offset + 6).map((row) => row.byte),
),
};
}
export function verifySignalOracleIdentity(artifact, oracle) {
const pinned = corpus.artifacts.find((entry) => entry.id === artifact);
if (
!pinned ||
oracle.kind !== SIGNAL_ARTIFACTS[artifact]?.kind ||
oracle.sha256 !== pinned.sha256
)
throw new Error(
"Independent oracle does not identify the pinned signal corpus",
);
}
export function verifySignalRows(kind, rows, oracle, digest) {
const expected =
kind === "uart"
? oracle.rows
: oracle.reads.flatMap((read) =>
read.bytes.slice(1).map((byte, index) => ({
...byte,
after_previous: read.bytes[index].last_clock,
before_next: read.bytes[index + 2]?.first_clock ?? read.stop,
})),
);
if (
oracle.kind !== kind ||
oracle.sha256 !== digest ||
rows.length !== expected.length
)
throw new Error(
"Signal observations differ from independent sample identity/coverage",
);
for (let index = 0; index < rows.length; index++) {
const observed = rows[index],
raw = expected[index];
if (
observed.byte !== raw.byte ||
observed.start < 0 ||
observed.end >= oracle.total_samples
)
throw new Error("Decoded data differs from independent sample bits");
if (
kind === "uart"
? !Number.isSafeInteger(raw.first_data_sample) ||
!Number.isSafeInteger(raw.last_data_sample) ||
observed.start < raw.start ||
observed.end >= raw.end_exclusive ||
observed.start > raw.first_data_sample ||
observed.end < raw.last_data_sample
: !Number.isSafeInteger(raw.after_previous) ||
!Number.isSafeInteger(raw.before_next) ||
observed.start <= raw.after_previous ||
observed.end >= raw.before_next ||
observed.start > raw.first_clock ||
observed.end < raw.last_clock
)
throw new Error(
"Decoded interval does not identify the independently read byte",
);
}
}
export async function measureSignalEntry({
values,
call,
inputSha,
catalogDefinitions,
}) {
const descriptor = SIGNAL_ARTIFACTS[values.artifact];
const runtime = (file) =>
pathToFileURL(resolve(values["runtime-root"], file)).href;
const { parseEvidence } = await import(runtime("dist/domain/evidence.js"));
const { decodeSignalRecordingInputSchema, signalRecordingSchema } =
await import(runtime("dist/domain/signals/signalRecording.js"));
const { inspectSignalDecodersInputSchema, signalDecodersSchema } =
await import(runtime("dist/domain/signals/signalDecoders.js"));
const observations = [];
const checked = (evidence, operation, input, schema) => {
parseEvidence(evidence);
const report = schema.parse(evidence.normalized_result);
if (
evidence.operation !== operation ||
!isDeepStrictEqual(evidence.parameters, input) ||
!isDeepStrictEqual(report.selection, input)
)
throw new Error(
"Signal Evidence differs from the actual requested operation or choices",
);
if (
operation === "decode_signal_recording"
? evidence.subject?.local_path !== values.path ||
evidence.subject?.digest?.sha256 !== inputSha ||
report.artifact.path !== values.path ||
report.artifact.sha256 !== inputSha
: evidence.subject?.local_path !== report.tool.path ||
evidence.subject?.digest?.sha256 !== report.tool.sha256
)
throw new Error(
"Signal Evidence identifies different recording/tool bytes",
);
observations.push({
evidence_id: evidence.evidence_id,
operation,
selection: report.selection,
output_window: report.output_window,
limitations: evidence.limitations,
});
return report;
};
const selection = values["signal-selection"];
for (const name of [
"decode_signal_recording",
...(selection === "discovery" ? ["inspect_signal_decoders"] : []),
])
if (!catalogDefinitions.has(name))
throw new Error(`Missing live signal contract ${name}`);
if (selection === "discovery") {
const input = inspectSignalDecodersInputSchema.parse({
decoder: descriptor.kind,
query: descriptor.query,
limit: 5,
});
const report = checked(
await call("inspect_signal_decoders", input),
"inspect_signal_decoders",
input,
signalDecodersSchema,
);
if (!report.help_lines.some((line) => line.text.includes(descriptor.query)))
throw new Error(
"Installed decoder help does not establish the selected annotation class",
);
}
const input = decodeSignalRecordingInputSchema.parse(
signalRequest(values.artifact, values.path, selection),
);
const first = checked(
await call("decode_signal_recording", input),
"decode_signal_recording",
input,
signalRecordingSchema,
);
let rows = signalDataRows(descriptor.kind, first.annotations_text);
if (selection === "pages") {
const nextInput = decodeSignalRecordingInputSchema.parse({
...input,
line_offset: descriptor.firstPage,
line_limit: null,
});
const next = checked(
await call("decode_signal_recording", nextInput),
"decode_signal_recording",
nextInput,
signalRecordingSchema,
);
const repeated = checked(
await call("decode_signal_recording", input),
"decode_signal_recording",
input,
signalRecordingSchema,
);
if (
first.annotations_text !== repeated.annotations_text ||
!isDeepStrictEqual(first.output_window, repeated.output_window) ||
first.output_window.total_lines !== next.output_window.total_lines
)
throw new Error("Repeated/next display pages changed observation scope");
rows = [...rows, ...signalDataRows(descriptor.kind, next.annotations_text)];
}
const { stdout } = await promisify(execFile)(
"python3",
[
fileURLToPath(new URL("./signal-oracle.py", import.meta.url)),
descriptor.kind,
values.path,
...(descriptor.kind === "uart" ? ["--invert"] : []),
],
{ timeout: 30_000, maxBuffer: 256 * 1024 },
);
const oracle = JSON.parse(stdout);
verifySignalOracleIdentity(values.artifact, oracle);
verifySignalRows(descriptor.kind, rows, oracle, inputSha);
return {
artifact_sha256: inputSha,
facts: signalFacts(descriptor.kind, rows),
delivery_observations: observations,
evidence_ids: [...new Set(observations.map((item) => item.evidence_id))],
};
}
@@ -1,22 +0,0 @@
import { parseArgs } from "node:util";
import { signalRequest, SIGNAL_ARTIFACTS } from "./signal-entry.mjs";
const { values } = parseArgs({
options: { artifact: { type: "string" }, selection: { type: "string" } },
});
if (
!Object.hasOwn(SIGNAL_ARTIFACTS, values.artifact ?? "") ||
!["baseline", "focused"].includes(values.selection)
)
throw new Error(
"Use --artifact fixed-signal-ID --selection baseline|focused",
);
const kind = SIGNAL_ARTIFACTS[values.artifact].kind;
const question =
kind === "uart"
? "Report every decoded RX data octet on the specified DOUT1 channel, in recording order, as decimal integer bytes. Include all repetitions; do not deduplicate data."
: "Report every I2C READ data payload in recording order, grouped into consecutive six-byte payloads. Exclude address bytes and write data. Do not infer button or hardware behavior.";
console.log(`${question}
Use the configured REA MCP and make exactly one decode_signal_recording call with this selection:
${JSON.stringify(signalRequest(values.artifact, "{{artifact_path}}", values.selection))}
Return one JSON object with exactly these keys: kind (${JSON.stringify(kind)}), ${kind === "uart" ? "bytes (an array of decimal octets)" : "reads (an array of six-octet decimal arrays)"}, evidence_ids (unique actual returned Evidence IDs), limitations (nonempty investigation limitations). Preserve source identity and recorded order. Output lines are not sample or annotation counts; selected decoder observations do not prove complete hardware traffic or original application behavior. Do not run shell commands, browse, acquire hardware samples, invent missing data, or fabricate citations.`);
@@ -1,211 +0,0 @@
"""Independent readers for the two pinned, single-device digital SR recordings."""
import argparse
from bisect import bisect_right
import configparser
import hashlib
import io
import json
from pathlib import Path
import re
import struct
import zipfile
def read_edges(path, channels):
size = path.stat().st_size
if size > 32 * 1024 * 1024:
raise ValueError('Compressed SR input exceeds the experiment budget')
with path.open('rb') as source:
admitted = source.read(size + 1)
if len(admitted) != size:
raise ValueError('SR input size changed during admission')
marker = admitted.rfind(b'PK\x05\x06', max(0, len(admitted) - 65557))
if marker < 0 or len(admitted) - marker < 22:
raise ValueError('Missing classic ZIP directory receipt')
header = struct.unpack_from('<4s4H2IH', admitted, marker)
if (marker >= 20 and admitted[marker-20:marker-16] == b'PK\x06\x07'):
raise ValueError('ZIP64 directory is unsupported')
if header[1] or header[2] or header[3] != header[4] or not 1 <= header[4] <= 64 or header[5] > 65536 or marker + 22 + header[7] != len(admitted) or header[6] + header[5] != marker:
raise ValueError('Unsupported or excessive ZIP directory representation')
directory = memoryview(admitted)[header[6]:marker]
position = count = 0
while position < len(directory):
if count >= 64 or len(directory) - position < 46 or directory[position:position+4] != b'PK\x01\x02':
raise ValueError('Unsupported or excessive actual ZIP directory records')
name_size, extra_size, comment_size = struct.unpack_from('<3H', directory, position + 28)
end = position + 46 + name_size + extra_size + comment_size
if end > len(directory):
raise ValueError('Incomplete actual ZIP directory record')
extra = position + 46 + name_size
extra_end = extra + extra_size
while extra < extra_end:
if extra_end - extra < 4:
raise ValueError('Incomplete ZIP extra field')
tag, length = struct.unpack_from('<2H', directory, extra)
if tag == 1:
raise ValueError('ZIP64 member is unsupported')
extra += 4 + length
if extra > extra_end:
raise ValueError('Incomplete ZIP extra field')
count += 1
position = end
if count != header[4]:
raise ValueError('Declared and actual ZIP directory counts differ')
before = hashlib.sha256(admitted).hexdigest()
# Admission and ZipFile parse the same bounded snapshot, not a reopened path.
with zipfile.ZipFile(io.BytesIO(admitted), allowZip64=False) as archive:
entries = archive.infolist()
names = [item.filename for item in entries]
if len(names) != len(set(names)) or len(names) > 64:
raise ValueError("Ambiguous or excessive SR members")
if sum(item.file_size for item in entries) > 32 * 1024 * 1024:
raise ValueError("Expanded SR input exceeds the experiment budget")
if archive.getinfo('metadata').file_size > 8192:
raise ValueError("Excessive SR metadata")
# Declared sizes are untrusted. A bounded read also caps each inflater
# step; logic members below likewise use bounded block reads.
with archive.open('metadata') as source:
metadata_bytes = source.read(8193)
if len(metadata_bytes) > 8192:
raise ValueError("Expanded SR metadata exceeds the experiment budget")
metadata = metadata_bytes.decode('utf-8')
config = configparser.ConfigParser(interpolation=None)
config.read_string(metadata)
if set(config.sections()) != {'global', 'device 1'}:
raise ValueError("Only the pinned single-device profile is supported")
device = config['device 1']
unit = int(device['unitsize'])
probes = int(device['total probes'])
if unit not in [1, 2] or not 1 <= probes <= unit * 8:
raise ValueError("Unsupported digital sample geometry")
rate = re.fullmatch(r'(\d+)\s+(Hz|kHz|MHz)', device['samplerate'])
if rate is None or int(rate[1]) <= 0:
raise ValueError("Unsupported sample-rate representation")
samplerate = int(rate[1]) * {'Hz': 1, 'kHz': 1000, 'MHz': 1000000}[rate[2]]
bits = []
for channel in channels:
matches = [index - 1 for index in range(1, probes + 1)
if device.get(f'probe{index}') == channel]
if len(matches) != 1:
raise ValueError("Channel identity is missing or ambiguous")
bits.append(matches[0])
prefix = device['capturefile']
chunks = [name for name in names if re.fullmatch(re.escape(prefix) + r'-\d+', name)]
if prefix in names and chunks:
raise ValueError("Ambiguous direct and chunked logic data")
if prefix in names:
chunks = [prefix]
else:
chunks.sort(key=lambda name: int(name.rsplit('-', 1)[1]))
if [int(name.rsplit('-', 1)[1]) for name in chunks] != list(range(1, len(chunks) + 1)):
raise ValueError("Missing or ambiguous logic chunk order")
if not chunks or any(archive.getinfo(name).file_size % unit for name in chunks):
raise ValueError("Missing or incomplete digital samples")
edges = [[] for _ in channels]
previous = None
sample = 0
mask = sum(1 << bit for bit in bits)
for name in chunks:
with archive.open(name) as source:
while block := source.read(65536):
values = block if unit == 1 else (item[0] for item in struct.iter_unpack('<H', block))
for value in values:
selected = value & mask
if selected != previous:
for index, bit in enumerate(bits):
level = (selected >> bit) & 1
if not edges[index] or edges[index][-1][1] != level:
if sum(map(len, edges)) >= 65536:
raise ValueError('Selected transitions exceed the experiment representation budget')
edges[index].append((sample, level))
previous = selected
sample += 1
with path.open('rb') as source:
after = source.read(size + 1)
if len(after) != size or hashlib.sha256(after).hexdigest() != before:
raise ValueError("Recording bytes changed during the independent read")
return {'sha256': before, 'metadata': metadata, 'sample_rate_hz': samplerate,
'total_samples': sample, 'channels': dict(zip(channels, bits))}, edges
def uart(path, invert):
metadata, traces = read_edges(path, ['MAX3232E DOUT1'])
edges = [(sample, level ^ invert) for sample, level in traces[0]]
positions = [sample for sample, _ in edges]
rate, baud = metadata['sample_rate_hz'], 57600
def level(sample):
return edges[bisect_right(positions, sample) - 1][1]
rows = []
busy_until = 0
for index, (start, value) in enumerate(edges):
if index == 0 or value != 0 or start < busy_until:
continue
center = lambda halfbits: start + (halfbits * rate) // (2 * baud)
if center(19) >= metadata['total_samples']:
raise ValueError("Incomplete UART frame at the capture boundary")
if level(center(1)) != 0 or level(center(19)) != 1:
raise ValueError(f"UART framing differs from selected polarity at sample {start}")
byte = sum(level(center(3 + 2 * bit)) << bit for bit in range(8))
# A UART receiver becomes ready after sampling the stop bit. Waiting
# a full nominal ten bits would miss a valid slightly faster sender.
rows.append({'start': start, 'end_exclusive': center(19) + 1, 'byte': byte,
'first_data_sample': center(3), 'last_data_sample': center(17)})
busy_until = center(19) + 1
return metadata | {'kind': 'uart', 'baud_rate': baud, 'invert': bool(invert),
'complete_frames': len(rows), 'rows': rows}
def i2c(path):
metadata, traces = read_edges(path, ['SCL', 'SDA'])
transitions = {}
for channel, edges in enumerate(traces):
for sample, value in edges:
transitions.setdefault(sample, {})[channel] = value
clock = data = None
active = None
bits = []
transactions = []
for sample, changes in sorted(transitions.items()):
next_clock, next_data = changes.get(0, clock), changes.get(1, data)
if clock is not None and data is not None:
if clock == next_clock == 1 and next_data != data:
if next_data == 0:
if active is not None:
raise ValueError("The pinned profile does not contain repeated START")
active = {'start': sample, 'bytes': []}
bits = []
elif active is not None:
# SCL may rise with SDA low to prepare STOP; that final
# rise is not the first bit of a new data byte.
if len(bits) == 1 and bits[0][1] == 0:
bits = []
if bits:
raise ValueError("Incomplete I2C byte before STOP")
active['stop'] = sample
transactions.append(active)
active = None
elif active is not None and clock == 0 and next_clock == 1:
if next_data != data:
raise ValueError("Ambiguous simultaneous clock/data transition")
bits.append((sample, next_data))
if len(bits) == 9:
byte = sum(bit << (7-index) for index, (_, bit) in enumerate(bits[:8]))
active['bytes'].append({'byte': byte, 'first_clock': bits[0][0],
'last_clock': bits[7][0], 'ack': bits[8][1] == 0})
bits = []
clock, data = next_clock, next_data
if active is not None or bits:
raise ValueError("Incomplete I2C transaction at capture end")
reads = [transaction for transaction in transactions
if transaction['bytes'] and transaction['bytes'][0]['byte'] & 1]
return metadata | {'kind': 'i2c', 'transactions': transactions, 'reads': reads}
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument('kind', choices=['uart', 'i2c'])
parser.add_argument('path', type=Path)
parser.add_argument('--invert', action='store_true')
args = parser.parse_args()
print(json.dumps(uart(args.path, args.invert) if args.kind == 'uart' else i2c(args.path)))
@@ -1,188 +0,0 @@
# Sigrok selection and repeated-page checkpoint (#3131)
This experiment preserves production APIs, defaults, caller selections and Evidence
semantics. Existing annotation selectors substantially reduce replies, but this
small real-model cohort does **not** establish a consistent reduction in total
input tokens or elapsed task time. No decoder cache or new production preset is
introduced. Complete necessary output stays available through existing explicit
null limits; original defaults remain 50 display lines.
## Immutable corpus and independent questions
The runtime is pinned to `ca7450beffff59325ce3bddc78e181693ce77177`, with Node
24.18.0, SDK 2.3.1 and Linux x86-64. Experiment driver code for model trials is
`23317a58d`; each SDK result separately records workflow/oracle source hashes,
runtime-file and lockfile digests, explicit provider settings and original bytes.
Later integration checks must remain separate from this cohort.
Two official sigrok-dumps recordings are pinned to upstream commit
`0ad13477abc959d37fc9a5acbd23901c371c9c76` in [corpus.json](corpus.json).
Original Git blob IDs, SHA-256, sizes and immutable reference/download URLs are
recorded there. No sample binaries or raw output are committed. The upstream
repository declares dumps public domain unless otherwise noted; the inspected
sample READMEs state no exception. Decoder software licensing is separate.
- MAX3232E UART: 12,742 compressed bytes, 10,000,000 logic bytes, 5,000,000 samples
at the recorded nominal 100 MHz. Stored DOUT1 is bit 1; DIN1 is bit 2. The exact
question requests all RX octets in order, including repetitions. Independent
57,600-baud 8N1 stop-bit sampling with DOUT1 inversion yields 65 bytes: five
copies of `Hello world\r\n`. The source metadata and physical probe numbers
remain distinct from stored bit positions.
- Wii Nunchuk I2C: 20,373 compressed bytes, 20,000,000 samples at nominal 1 MHz.
The source README's physical probes 3/7 are repacked as stored named channels
SCL/SDA at bits 0/1. The question asks every read payload as consecutive
six-byte arrays, excluding address/write bytes and hardware/button inference.
Independent START/STOP, rising-clock, byte and ACK/NACK reading identifies seven
transactions and three read payloads, matching the upstream reference:
`75 7F 77 4F 82 3B`, `75 7F 75 44 82 34`, `75 7F 77 43 83 5D`.
The independent reader accepts the scoped single-device classic-ZIP profile,
checks directory representation before object allocation, bounds compressed and
expanded input to 32 MiB, and limits selected transitions to 65,536. It preserves
source metadata/digest, chunk ordering and channel identity; incomplete samples,
ambiguous names, partial bytes and unsupported transactions fail explicitly.
Receiver rearming follows the verified UART stop sample, accommodating the
recording's slightly faster sender. I2C STOP preparation is distinct from data.
These checks do not establish other formats, decoders, OSes or physical acquisition.
An initial DOUT1 decode without inversion exited successfully but produced wrong
bytes. Its cost/output is retained privately as configuration exploration, outside
eligible measurements. An independent sample oracle prevents equal-but-wrong
producer results from passing. Correct inversion was established before the cohort.
## Actual CLI and SDK evidence
sigrok-cli 0.7.2 reports libsigrok 0.5.2 and libsigrokdecode 0.5.3. Its executable
SHA-256 is `e1b1eb2f25edc17d24a78acbab16ff7e1dfdee74bb2cccb7ad00497ccc7a0c44`.
Selected installed UART/I2C decoder module bytes are fingerprinted privately.
The banner and executable digest do not fingerprint the complete installation;
modules and libraries remain trusted. Both actual CLI and MCP results match raw
sample bytes; data intervals correlate with the independently examined samples.
Fourteen actual SDK workflows comprise two interleaved repetitions per recording
and arm (baseline/focused/discovery), plus first/next/repeated pages. All 22 calls
preserve source bytes and necessary decoded facts. Baseline returns all necessary
annotation output. Focused selects `uart=rx-data` or `i2c=data-read`, with explicit
null line limits because the task needs the entire selected payload. Discovery
first queries the installed decoder help for the already specified class; it does
not establish discovery benefits for unknown protocols.
| Recording | Baseline bytes | Focused bytes | Discovery + focused bytes | First + next + repeat bytes |
| --------- | -------------: | ------------: | ------------------------: | --------------------------: |
| UART | 49,417 | 12,441 | 18,839 | 31,779 |
| I2C | 26,829 | 9,928 | 16,274 | 27,623 |
Focused reply reductions are 74.8% and 63.0%. These are parsed SDK JSON bytes,
not wire bytes or model tokens. Baseline/focused RPC times (ms, repetitions 1/2)
are UART 2010/582 versus 1459/911, and I2C 2851/676 versus 2330/685; n=2,
with order/cold/warm variation, does not establish a general speedup. Pages retain
input/class/window coverage separately, rather than equating display exhaustion
with a complete protocol decode. Repeated first pages match their original text
and scope; reused content-based Evidence identities are valid.
## Complete real-model task costs
Eight actual Codex CLI 0.161.0 tasks requested `gpt-5.6-terra` / `high`, in isolated
profiles using this runtime's REA MCP. Both tools were enabled and preapproved
identically in all arms; shell, browser and extra agents were disabled. The model
alias is a requested identifier, not independently resolved backend identity.
Questions contain no expected payloads. Trial order is baseline 1, focused 1,
focused 2, baseline 2, with UART then I2C in each step.
| Trial | Total input | Cached input | Uncached input (derived) | Output | Seconds | Completed tool calls | Facts / fixed plan |
| --------------- | ----------: | -----------: | -----------------------: | -----: | ------: | -------------------: | ------------------ |
| uart baseline 1 | 92,399 | 49,408 | 42,991 | 1,295 | 86.411 | 1 | passed / passed |
| uart focused 1 | 130,029 | 98,048 | 31,981 | 780 | 49.623 | 1 | passed / passed |
| uart focused 2 | 98,894 | 78,592 | 20,302 | 878 | 73.861 | 1 | passed / passed |
| uart baseline 2 | 90,395 | 48,384 | 42,011 | 926 | 43.974 | 1 | passed / passed |
| i2c baseline 1 | 172,162 | 121,344 | 50,818 | 1,194 | 36.330 | 2 | passed / failed |
| i2c focused 1 | 74,815 | 49,408 | 25,407 | 937 | 114.515 | 1 | passed / passed |
| i2c focused 2 | 74,684 | 48,384 | 26,300 | 668 | 73.877 | 1 | passed / passed |
| i2c baseline 2 | 86,241 | 49,408 | 36,833 | 778 | 41.084 | 1 | passed / passed |
All eight answers match independent raw samples and cite authenticated decoder
observations. I2C baseline 1 also queried generic MCP resources before its single
required decode. The predeclared strict single-call grader marks its plan failed;
its actual 172,162 input tokens and 36.330 seconds remain recorded, separately
from the eligible fixed-plan pairs. It is not retried into a better result.
UART eligible pairs increase total input by 40.7% and 9.4%; elapsed changes are
-42.6% and +68.0%. The one eligible I2C pair reduces total input by 13.4% but
increases elapsed time by 79.8%. Reduced uncached input does not replace total
input as the primary token measure. No reliable whole-task token/time benefit is
established across these two questions; reply projection alone is insufficient.
## Producer, window and resource scope
Eight separately bounded direct producer runs preserve source bytes and render
correct stdout: UART baseline 159/131 ms versus focused 163/130 ms; I2C baseline
355/298 ms versus focused 317/290 ms. This isolates the producer process from REA
RPC, version probing and snapshot work, but startup/decoding/annotation formatting
remain combined. Decoding-only versus formatting-only time is unmeasured.
REA literal-window extraction and JSON encoding were measured separately from
retained producer text (one cold observation and 200 warm iterations per arm).
Warm mean extraction ranges from 0.0040 to 0.1794 ms; JSON encoding from 0.0021 to 0.0121 ms. These exclude file reads, RPC and decoding.
Every page still invokes decoding. Retained output might help repeated inspection,
but this fixed cohort does not demonstrate a completed-task improvement warranting
a new cache. Any future work should own bounded output explicitly by original
Evidence identity and lifetime, with cancellation/cleanup, rather than use a
CLI-digest-only generic cache of an incompletely fingerprinted installation.
Heavy work ran serially under one CPU, 3 GiB early stop / 4 GiB hard memory,
no swap and host-availability checks. Verified payload-exit peak bytes were
SDK 587,513,856; model 889,323,520; direct producer 26,288,128. No OOM/OOM-kill or resource abort occurred in that verified phase; task descendants
were cleaned. Terminal whole-scope counters are unavailable and remain unknown.
## Verification and replay
Eleven independent-oracle regressions and 12 comparator/sample-interval tests pass.
Twenty-four private replays using actual SDK Evidence cover valid answers, failed
calls before/after recovery, wrong bytes, unknown citations and missing limitations.
These are grading checks with injected state, not additional model/provider trials.
The grader retains authenticated observations by identity, rejects conflicts,
combines cited pages in sample order and keeps failed calls/plan deviations separate
from supported facts. Relevant typecheck/lint and pre-push checks pass.
Review hardening ties the oracle to the named corpus digest, bounds each I2C
interval against neighboring byte clocks/transaction STOP, and counts actual
classic-ZIP directory records on the same bounded snapshot before allocating
`ZipFile` objects; ZIP64 is rejected. Forged-count and ZIP64 admission regressions
fail on the original implementation. Replaying all eight original model trials
and 24 injected grading cases with these checks preserves their factual/plan
grades and recorded usage/time; these replays are not new model measurements.
Metadata reads cap inflater output even when ZIP sizes are forged. A streamed
8 MiB malformed metadata member declaring 8 KiB is rejected with traced Python
allocation below 1 MiB; the same regression fails on the preceding implementation.
Compressed snapshots use actual file size plus one growth-detection byte rather
than preallocating the entire admission budget. Both genuine waveform oracles
retain identical bytes, metadata, sample intervals and source digests.
UART interval checks also require coverage of the first and last independently
sampled data-bit centers within the owning frame. Shortened, shifted and
single-sample intervals are rejected even when the decoded byte is correct.
The added oracle coordinates retain the original byte/frame facts; replaying
saved real Evidence and all eight model transcripts preserves the original
grades and costs. This is stronger retrospective verification, not a new cohort.
After rebasing onto main `44d98de2367b99808d78db732ec7d72533266233`,
`npm run build:cached` and the 16 focused comparator/sample-interval/MCP-boundary
tests passed. Two additional real focused MCP workflows independently verified
UART/I2C bytes and intervals on that base. These integration checks are separate
from the original cost cohort and do not replace or extend its measurements.
A subsequent rebase onto main `3b79af28e80785afce9f5998d8f03289402a1847`
passed the 17 focused tests, including the upstream-fixed Hopper doctor case,
and two additional real UART/I2C MCP workflows. Original cohort costs and grades
remain unchanged; these checks verify integration on the updated main only.
Use `measure.mjs --signal-selection baseline|focused|discovery|pages`, preserving
runtime/source fingerprints and explicit provider JSON. Generate the exact task
with `signal-model-prompt.mjs --artifact ID --selection baseline|focused`, run the
isolated `agent-trial.py --mode direct --capture-tool-context`, and grade with
`review-signal.mjs` and the independently produced oracle. Keep original failures,
resources and raw observations private. For migration, existing optional annotation
selection is sufficient for scoped reply reduction; no default/API change follows
from this mixed whole-task result.
@@ -1,149 +0,0 @@
"""Distinct framing, identity and admission failures in the sample-bit oracle."""
import importlib.util
from pathlib import Path
import struct
import tempfile
import tracemalloc
import unittest
from unittest.mock import patch
import zipfile
spec = importlib.util.spec_from_file_location(
'signal_oracle', Path(__file__).with_name('signal-oracle.py'))
oracle = importlib.util.module_from_spec(spec)
spec.loader.exec_module(oracle)
class SignalOracleTests(unittest.TestCase):
def setUp(self):
self.root = tempfile.TemporaryDirectory()
self.addCleanup(self.root.cleanup)
self.path = Path(self.root.name) / 'sample.sr'
def recording(self, data, unit, channels, chunk='logic-1', extra=0):
metadata = '[global]\nsigrok version=0.3.0\n[device 1]\ncapturefile=logic-1\n'
metadata += f'total probes={len(channels)}\nsamplerate=1 MHz\nunitsize={unit}\n'
metadata += ''.join(f'probe{index+1}={name}\n' for index, name in enumerate(channels))
with zipfile.ZipFile(self.path, 'w') as archive:
archive.writestr('version', '2')
archive.writestr('metadata', metadata)
archive.writestr(chunk, data)
for index in range(extra):
archive.writestr(f'extra-{index}', '')
def test_uart_rearms_after_stop_sample_for_a_slightly_faster_sender(self):
levels = [1] * 100
for byte in [0x48, 0x65]:
for bit in [0, *[(byte >> index) & 1 for index in range(8)], 1]:
levels.extend([bit] * 17)
levels.extend([1] * 100)
data = b''.join(struct.pack('<H', (1-level) << 1) for level in levels)
self.recording(data, 2, ['unused', 'MAX3232E DOUT1'])
result = oracle.uart(self.path, True)
self.assertEqual([row['byte'] for row in result['rows']], [0x48, 0x65])
self.assertEqual(result['channels']['MAX3232E DOUT1'], 1)
for row, start in zip(result['rows'], [100, 270]):
self.assertEqual(row['first_data_sample'], start + 26)
self.assertEqual(row['last_data_sample'], start + 147)
def i2c_levels(self, incomplete=False):
states = [(1, 1), (1, 0)]
for byte, ack in [(0xA5, 0), (0x75, 1)]:
values = [(byte >> index) & 1 for index in reversed(range(8))] + [ack]
if incomplete and byte == 0x75:
values = values[:4]
for bit in values:
states.extend([(0, bit), (1, bit)])
states.extend([(0, 0), (1, 0), (1, 1)])
return bytes(clock | data << 1 for clock, data in states)
def test_i2c_stop_setup_is_distinct_from_another_data_byte(self):
self.recording(self.i2c_levels(), 1, ['SCL', 'SDA'])
result = oracle.i2c(self.path)
self.assertEqual(len(result['reads']), 1)
self.assertEqual([row['byte'] for row in result['reads'][0]['bytes']], [0xA5, 0x75])
self.assertFalse(result['reads'][0]['bytes'][-1]['ack'])
def test_incomplete_i2c_byte_is_not_invented_at_stop(self):
self.recording(self.i2c_levels(incomplete=True), 1, ['SCL', 'SDA'])
with self.assertRaisesRegex(ValueError, 'Incomplete I2C byte'):
oracle.i2c(self.path)
def test_channel_names_must_identify_one_stored_bit(self):
self.recording(b'\x00', 1, ['SCL', 'SCL'])
with self.assertRaisesRegex(ValueError, 'ambiguous'):
oracle.read_edges(self.path, ['SCL'])
def test_missing_chunk_is_not_renumbered(self):
self.recording(b'\x00', 1, ['SCL', 'SDA'], chunk='logic-1-2')
with self.assertRaisesRegex(ValueError, 'chunk order'):
oracle.read_edges(self.path, ['SCL', 'SDA'])
def test_partial_sample_word_is_rejected(self):
self.recording(b'\x00', 2, ['unused', 'MAX3232E DOUT1'])
with self.assertRaisesRegex(ValueError, 'incomplete digital samples'):
oracle.read_edges(self.path, ['MAX3232E DOUT1'])
def test_directory_budget_precedes_zip_object_allocation(self):
self.recording(b'\x00', 1, ['SCL', 'SDA'], extra=62)
with patch.object(oracle.zipfile, 'ZipFile', side_effect=AssertionError('Too late')):
with self.assertRaisesRegex(ValueError, 'excessive ZIP directory'):
oracle.read_edges(self.path, ['SCL', 'SDA'])
def test_declared_expansion_is_rejected_before_decompression(self):
self.recording(b'\x00', 1, ['SCL', 'SDA'])
data = bytearray(self.path.read_bytes())
directory = data.index(b'PK\x01\x02')
struct.pack_into('<I', data, directory + 24, 32 * 1024 * 1024 + 1)
self.path.write_bytes(data)
with self.assertRaisesRegex(ValueError, 'Expanded SR input'):
oracle.read_edges(self.path, ['SCL', 'SDA'])
def test_forged_member_count_cannot_allocate_extra_zip_objects(self):
self.recording(b'\x00', 1, ['SCL', 'SDA'], extra=62)
data = bytearray(self.path.read_bytes())
end = data.rfind(b'PK\x05\x06')
struct.pack_into('<2H', data, end + 8, 1, 1)
self.path.write_bytes(data)
with patch.object(oracle.zipfile, 'ZipFile', side_effect=AssertionError('Too late')):
with self.assertRaisesRegex(ValueError, 'actual ZIP directory'):
oracle.read_edges(self.path, ['SCL', 'SDA'])
def test_zip64_locator_cannot_override_the_classic_budget(self):
self.recording(b'\x00', 1, ['SCL', 'SDA'])
data = bytearray(self.path.read_bytes())
end = data.rfind(b'PK\x05\x06')
# Syntactically complete ZIP64 directory/locator before a classic EOCD.
header = struct.unpack_from('<4s4H2IH', data, end)
record = struct.pack('<4sQ2H2I4Q', b'PK\x06\x06', 44, 45, 45, 0, 0,
header[4], header[4], 100000000, header[6])
locator = struct.pack('<4sIQI', b'PK\x06\x07', 0, end, 1)
data[end:end] = record + locator
self.path.write_bytes(data)
with patch.object(oracle.zipfile, 'ZipFile', side_effect=AssertionError('Too late')):
with self.assertRaisesRegex(ValueError, 'ZIP64 directory'):
oracle.read_edges(self.path, ['SCL', 'SDA'])
def test_forged_metadata_size_cannot_expand_past_its_output_budget(self):
# Stream the malformed member so constructing the fixture is bounded.
with zipfile.ZipFile(self.path, 'w', compression=zipfile.ZIP_DEFLATED) as archive:
with archive.open('metadata', 'w') as member:
for _ in range(128):
member.write(b'X' * 65536)
data = bytearray(self.path.read_bytes())
directory = data.index(b'PK\x01\x02')
struct.pack_into('<I', data, directory + 24, 8192)
self.path.write_bytes(data)
tracemalloc.start()
try:
with self.assertRaises(zipfile.BadZipFile):
oracle.read_edges(self.path, ['SCL', 'SDA'])
_, peak = tracemalloc.get_traced_memory()
finally:
tracemalloc.stop()
self.assertLess(peak, 1024 * 1024, 'Metadata allocation followed forged ZIP expansion')
if __name__ == '__main__':
unittest.main()
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "rea-agents",
"version": "6.5.0",
"version": "6.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rea-agents",
"version": "6.5.0",
"version": "6.4.0",
"license": "MIT",
"dependencies": {
"@babel/parser": "8.0.4",
+1 -1
View File
@@ -1,7 +1,7 @@
{
"name": "rea-agents",
"mcpName": "io.github.morluto/rea",
"version": "6.5.0",
"version": "6.4.0",
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
"license": "MIT",
"repository": {
-27
View File
@@ -1,27 +0,0 @@
export type NpmReleaseState =
| { status: "absent" }
| {
status: "published";
integrity: string;
tag: "latest" | "next";
taggedVersion: string | null;
stableVersion: string | null;
};
export function readNpmRelease(
version: string,
request?: (url: string, init: RequestInit) => Promise<Response>,
): Promise<NpmReleaseState>;
export function requireMatchingNpmRelease(
tarball: string,
version: string,
published: NpmReleaseState,
): Promise<void>;
export function stableUpdateVersion(
version: string,
published: NpmReleaseState,
): string;
export function waitForNpmRelease(
version: string,
lookup?: (version: string) => Promise<NpmReleaseState>,
pause?: (milliseconds: number) => Promise<unknown>,
): Promise<Extract<NpmReleaseState, { status: "published" }>>;
-126
View File
@@ -1,126 +0,0 @@
import { appendFile } from "node:fs/promises";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { setTimeout } from "node:timers/promises";
import { z } from "zod";
import { prerelease, valid } from "semver";
import { releaseNpmTag } from "../release-npm-tag.mjs";
import { verifyPublishedPackageIntegrity } from "./verified-package.mjs";
/** Parse one npm metadata snapshot; errors and malformed records are never absence. */
export async function readNpmRelease(version, request = fetch) {
const tag = releaseNpmTag(version);
const response = await request("https://registry.npmjs.org/rea-agents", {
headers: {
Accept: "application/vnd.npm.install-v1+json",
"Cache-Control": "no-cache",
},
signal: AbortSignal.timeout(30_000),
});
if (response.status === 404) return { status: "absent" };
if (!response.ok)
throw new Error(`npm registry lookup failed: HTTP ${response.status}`);
const metadata = z
.object({
name: z.literal("rea-agents"),
versions: z.record(z.string(), z.unknown()),
"dist-tags": z.record(z.string(), z.string()),
})
.parse(await response.json());
if (!Object.hasOwn(metadata.versions, version)) return { status: "absent" };
const published = z
.object({
name: z.literal("rea-agents"),
version: z.literal(version),
dist: z.object({ integrity: z.string().min(1) }),
})
.parse(metadata.versions[version]);
return {
status: "published",
integrity: published.dist.integrity,
tag,
taggedVersion: metadata["dist-tags"][tag] ?? null,
stableVersion: metadata["dist-tags"].latest ?? null,
};
}
/** REA update follows stable latest even when the candidate is published on next. */
export function stableUpdateVersion(version, published) {
if (published.status !== "published" || published.taggedVersion !== version)
throw new Error(`npm does not select ${version} on its release channel`);
const stable = published.stableVersion;
if (
typeof stable !== "string" ||
valid(stable) !== stable ||
prerelease(stable) !== null
)
throw new Error(
"npm latest does not select a stable release for the update canary",
);
return stable;
}
/** Validate content before accepting a previously published or newly visible version. */
export async function requireMatchingNpmRelease(tarball, version, published) {
if (published.status !== "published")
throw new Error(`rea-agents@${version} is not published`);
await verifyPublishedPackageIntegrity(tarball, published.integrity);
if (published.taggedVersion !== version)
throw new Error(
`Expected npm ${published.tag} to select ${version}, observed ${published.taggedVersion ?? "missing"}; refusing to change an existing dist-tag`,
);
}
/** Retry only confirmed absence during the existing registry propagation window. */
export async function waitForNpmRelease(
version,
lookup = readNpmRelease,
pause = setTimeout,
) {
const attempts = 120;
for (let attempt = 1; attempt <= attempts; attempt++) {
const published = await lookup(version);
if (published.status === "published") return published;
if (attempt < attempts) await pause(15_000);
}
throw new Error(
`rea-agents@${version} was not visible on npm after 120 attempts over 30 minutes`,
);
}
async function main() {
const [operation, tarball, version, ...extra] = process.argv.slice(2);
if (
!["inspect", "verify"].includes(operation) ||
tarball === undefined ||
version === undefined ||
extra.length
)
throw new Error(
"Usage: node scripts/ci/npm-release.mjs inspect|verify <qualified-tarball> <version>",
);
const published =
operation === "inspect"
? await readNpmRelease(version)
: await waitForNpmRelease(version);
if (published.status === "published")
await requireMatchingNpmRelease(tarball, version, published);
if (operation === "inspect") {
await appendFile(
z.string().min(1).parse(process.env.GITHUB_OUTPUT),
`published=${published.status === "published"}\n`,
);
} else if (process.env.GITHUB_STEP_SUMMARY !== undefined) {
await appendFile(
process.env.GITHUB_STEP_SUMMARY,
`npm: rea-agents@${version} is published, matches the qualified tarball, and is selected by ${published.tag}.\n`,
);
}
process.stdout.write(`${JSON.stringify({ version, ...published })}\n`);
}
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
await main();
-2
View File
@@ -13,11 +13,9 @@ const GUARDED_WORKFLOWS = new Set([
".github/workflows/pr-readiness.yml",
".github/workflows/ci-full-pr.yml",
".github/workflows/release.yml",
".github/workflows/release-recovery.yml",
".github/workflows/real-ghidra-windows.yml",
".github/workflows/sqlite-runtime.yml",
".github/workflows/ci-timings.yml",
".github/workflows/mutation.yml",
]);
// Sources compiled by build-conformance-fixtures.mjs. The readiness journey
// analyzes the built C fixture as well as its own applications.
-28
View File
@@ -1,28 +0,0 @@
export function selectRecoveryRelease(input: {
repository: string;
version: string;
run: unknown;
jobs: unknown;
release: unknown;
tagSha: string;
}): {
version: string;
source_sha: string;
run_id: number;
run_attempt: number;
};
export function verifyRecoveryPackage(input: {
directory: string;
sourceSha: string;
runId: number;
runAttempt: number;
version: string;
integrity: string;
}): Promise<{
version: string;
sourceSha: string;
runId: number;
runAttempt: number;
integrity: string;
}>;
-222
View File
@@ -1,222 +0,0 @@
import { execFile } from "node:child_process";
import { appendFile, readFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { valid } from "semver";
import { z } from "zod";
import {
validateVerifiedPackage,
verifyPublishedPackageIntegrity,
} from "./verified-package.mjs";
const execute = promisify(execFile);
const sha = z.string().regex(/^[a-f0-9]{40}$/u);
const positive = z.coerce
.number()
.int()
.positive()
.max(Number.MAX_SAFE_INTEGER);
const versionSchema = z
.string()
.refine(
(value) => valid(value) === value && !value.includes("+"),
"Expected an exact release version without build metadata",
);
const repositorySchema = z.string().regex(/^[\w.-]+\/[\w.-]+$/u);
/** Require the original release's successful qualification and npm write. */
export function selectRecoveryRelease({
repository,
version,
run,
jobs,
release,
tagSha,
}) {
repositorySchema.parse(repository);
versionSchema.parse(version);
sha.parse(tagSha);
const original = z
.object({
id: positive,
run_attempt: positive,
path: z.literal(".github/workflows/release.yml"),
event: z.literal("pull_request"),
status: z.literal("completed"),
repository: z.object({ full_name: z.literal(repository) }),
head_repository: z.object({ full_name: z.literal(repository) }),
})
.parse(run);
z.object({
tag_name: z.literal(`rea-agents-${version}`),
draft: z.literal(false),
published_at: z.string().min(1),
}).parse(release);
const parsedJobs = z
.array(
z.object({
name: z.string(),
status: z.string(),
conclusion: z.string().nullable(),
steps: z
.array(
z.object({ name: z.string(), conclusion: z.string().nullable() }),
)
.default([]),
}),
)
.parse(jobs);
const requireJob = (name) => {
const matches = parsedJobs.filter((job) => job.name === name);
if (matches.length !== 1 || matches[0].status !== "completed")
throw new Error(`Expected one completed original job: ${name}`);
return matches[0];
};
for (const name of [
"Release reviewed main merge",
"Qualify exact release revision / CI required",
])
if (requireJob(name).conclusion !== "success")
throw new Error(`Original release did not pass ${name}`);
const publish = requireJob("Publish to npm");
const writes = publish.steps.filter((step) => step.name === "Publish");
const verified = publish.steps.filter(
(step) => step.name === "Verify published npm bytes and dist-tag",
);
if (
!(
(writes.length === 1 && writes[0].conclusion === "success") ||
(verified.length === 1 && verified[0].conclusion === "success")
)
)
throw new Error(
"Original npm publication did not succeed or verify existing bytes",
);
return {
version,
source_sha: tagSha,
run_id: original.id,
run_attempt: original.run_attempt,
};
}
/** Bind existing npm bytes to the original qualified archive, without rebuilding it. */
export async function verifyRecoveryPackage({
directory,
sourceSha,
runId,
runAttempt,
version,
integrity,
}) {
const tarball = await validateVerifiedPackage({
directory,
sourceSha,
runId,
runAttempt,
packageVersion: version,
nodeVersion: process.version,
});
await verifyPublishedPackageIntegrity(tarball, integrity);
return { version, sourceSha, runId, runAttempt, integrity };
}
async function command(program, args, cwd) {
return (
await execute(program, args, { cwd, maxBuffer: 16 * 1024 * 1024 })
).stdout.trim();
}
async function main() {
const [operation, ...extra] = process.argv.slice(2);
if (extra.length || !["resolve", "verify"].includes(operation))
throw new Error(
"Usage: node scripts/ci/release-recovery.mjs resolve|verify",
);
const version = versionSchema.parse(process.env.RECOVERY_VERSION);
const runId = positive.parse(process.env.RECOVERY_RUN_ID);
if (operation === "resolve") {
if (process.env.GITHUB_REF !== "refs/heads/main")
throw new Error("Run release recovery from main");
const repository = repositorySchema.parse(process.env.GITHUB_REPOSITORY);
const api = async (path) =>
JSON.parse(await command("gh", ["api", `repos/${repository}/${path}`]));
const run = await api(`actions/runs/${runId}`);
const pages = z
.array(z.object({ jobs: z.array(z.unknown()) }))
.parse(
JSON.parse(
await command("gh", [
"api",
"--paginate",
"--slurp",
`repos/${repository}/actions/runs/${runId}/attempts/${positive.parse(run.run_attempt)}/jobs?per_page=100`,
]),
),
);
const release = await api(`releases/tags/rea-agents-${version}`);
const tagSha = await command("git", [
"rev-parse",
"--verify",
`refs/tags/rea-agents-${version}^{commit}`,
]);
const selection = selectRecoveryRelease({
repository,
version,
run,
jobs: pages.flatMap((page) => page.jobs),
release,
tagSha,
});
await appendFile(
z.string().min(1).parse(process.env.GITHUB_OUTPUT),
Object.entries(selection)
.map(([key, value]) => `${key}=${value}\n`)
.join(""),
);
process.stdout.write(`${JSON.stringify(selection)}\n`);
return;
}
const sourceDirectory = z
.string()
.min(1)
.parse(process.env.RECOVERY_SOURCE_DIRECTORY);
const sourceSha = sha.parse(process.env.RECOVERY_SOURCE_SHA);
if (
(await command("git", ["rev-parse", "HEAD"], sourceDirectory)) !== sourceSha
)
throw new Error("Recovery checkout differs from the released source");
z.object({ version: z.literal(version) }).parse(
JSON.parse(await readFile(join(sourceDirectory, "package.json"), "utf8")),
);
const integrity = z
.string()
.parse(
JSON.parse(
await command("npm", [
"view",
`rea-agents@${version}`,
"dist.integrity",
"--json",
"--registry=https://registry.npmjs.org",
]),
),
);
const result = await verifyRecoveryPackage({
directory: z.string().min(1).parse(process.env.RECOVERY_ARTIFACT_DIRECTORY),
sourceSha,
runId,
runAttempt: positive.parse(process.env.RECOVERY_RUN_ATTEMPT),
version,
integrity,
});
process.stdout.write(`${JSON.stringify(result)}\n`);
}
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
await main();
-15
View File
@@ -3,7 +3,6 @@
// every workflow.
export const SCOPES = {
reverse_tools: [
".github/workflows/mutation.yml",
"src/application/reverse/**",
"src/application/binary/BinarySession.ts",
"src/cutter/**",
@@ -24,7 +23,6 @@ export const SCOPES = {
"tests/composition/analysis-sessions/binarySession.test.ts",
"tests/boundary/filesystem/directAnalysisSnapshotReplay.test.ts",
"tests/boundary/filesystem/reverseToolsMutationConfig.test.ts",
"tests/boundary/filesystem/mutationWorkflow.test.ts",
"src/server/registerRizinDebugTools.ts",
"tests/boundary/providers/CutterBridge*.test.ts",
"tests/boundary/providers/cutterBridgeProducer.test.ts",
@@ -90,19 +88,6 @@ export const SCOPES = {
],
// Four-host installed-package matrix and the Windows curated lane.
package: [
".github/actions/publish-mcp/**",
".github/actions/prepare-release-package/**",
".github/workflows/release-recovery.yml",
"scripts/ci/release-recovery.mjs",
"scripts/ci/verified-package.mjs",
"scripts/ci/npm-release.*",
"scripts/verify-release-artifact.mjs",
"scripts/lib/release-package-canary.mjs",
"scripts/verify-published-package.mjs",
"tests/boundary/filesystem/releaseRecovery.test.ts",
"tests/boundary/filesystem/mcpRegistryPublication.test.ts",
"tests/boundary/filesystem/npmRelease.test.ts",
"tests/boundary/process/releaseArtifactCanary.test.ts",
"src/windows/**",
"native/windows/**",
"src/process/capture/FilesystemSnapshot.ts",
+5 -22
View File
@@ -5,7 +5,6 @@ import { copyFile, readFile, writeFile } from "node:fs/promises";
import { join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { promisify } from "node:util";
import { valid } from "semver";
const MANIFEST_NAME = "manifest.json";
const TARBALL_NAME = "package.tgz";
@@ -79,7 +78,7 @@ export async function retainVerifiedPackage({
}
/** Validate a retained package against the current checkout and workflow attempt. */
export async function validateVerifiedPackage({
async function validateVerifiedPackage({
directory,
sourceSha,
runId,
@@ -156,8 +155,6 @@ function validateManifestShape(manifest) {
])
if (typeof manifest[key] !== "string" || manifest[key].length === 0)
throw new Error(`Verified package manifest has an invalid ${key}`);
if (valid(manifest.packageVersion) !== manifest.packageVersion)
throw new Error("Verified package manifest has an invalid package version");
if (
typeof manifest.sha256 !== "string" ||
!/^[a-f0-9]{64}$/u.test(manifest.sha256)
@@ -176,16 +173,6 @@ async function sha256File(path) {
return hash.digest("hex");
}
/** Require the registry to serve the exact archive selected for publication. */
export async function verifyPublishedPackageIntegrity(tarball, integrity) {
const hash = createHash("sha512");
for await (const chunk of createReadStream(tarball)) hash.update(chunk);
if (integrity !== `sha512-${hash.digest("base64")}`)
throw new Error(
"Published npm integrity differs from the original qualified package",
);
}
async function validateFromCli(directory) {
const sourceSha = process.env.GITHUB_SHA;
const runId = parsePositiveInteger(
@@ -215,14 +202,10 @@ async function validateFromCli(directory) {
throw new Error(
"GITHUB_OUTPUT is required to publish the validated tarball path",
);
await writeFile(
process.env.GITHUB_OUTPUT,
`tarball=${tarball}\nversion=${packageJson.version}\n`,
{
encoding: "utf8",
flag: "a",
},
);
await writeFile(process.env.GITHUB_OUTPUT, `tarball=${tarball}\n`, {
encoding: "utf8",
flag: "a",
});
process.stdout.write(`${tarball}\n`);
}
@@ -12,20 +12,6 @@ import { browserScenarioSchema } from "../../dist/domain/browserScenario.js";
import { runScenarioCli } from "./browser-scenario-verifier.mjs";
import { verifyScenarioPolicySetup } from "./browser-scenario-service-worker-setup-e2e.mjs";
const diagnosticError = (error) =>
error instanceof Error
? {
name: error.name,
message: error.message.slice(0, 1_000),
reason: error.reason,
stack: error.stack?.slice(0, 2_000),
cause:
error.cause instanceof Error
? error.cause.stack?.slice(0, 3_000)
: typeof error.cause,
}
: { kind: typeof error };
const startSite = async () => {
let otherOrigin;
const server = createServer((request, response) => {
@@ -97,7 +83,6 @@ export async function verifyScenarioServiceWorkers(endpoint, targetId) {
first.setOtherOrigin(second.origin);
second.setOtherOrigin(first.origin);
const observer = await chromium.connectOverCDP(endpoint);
console.log("Service-worker fixture browser:", observer.version());
let provider = createBrowserScenarioProvider(process.env);
let session;
let failure;
@@ -169,42 +154,7 @@ export async function verifyScenarioServiceWorkers(endpoint, targetId) {
state: "visible",
},
]);
let cleanCapture;
try {
cleanCapture = (await runScenarioCli(clean)).normalized_result;
} catch (cause) {
// Retain the original failure. One bounded replay navigates only this
// verifier-owned target and exposes causes the CLI does not serialize.
const state = await page
.locator("#state")
.evaluate(
(element) => ({
text: element.textContent?.slice(0, 100),
blocked: element.getAttribute("data-blocked"),
nativeRegister: navigator.serviceWorker.register
.toString()
.includes("[native code]"),
}),
undefined,
{ timeout: 1_000 },
)
.catch((error) => ({ unavailable: String(error).slice(0, 500) }));
console.error("Service-worker CLI failure state:", {
url: page.url(),
state,
});
const replay = await provider
.captureScenario(clean, { signal: AbortSignal.timeout(5_000) })
.then((result) =>
result.ok ? { ok: true } : diagnosticError(result.error),
)
.catch(diagnosticError);
console.error(
"Service-worker diagnostic replay (original CLI failure retained):",
replay,
);
throw cause;
}
const cleanCapture = (await runScenarioCli(clean)).normalized_result;
assert.ok(cleanCapture.steps.every(({ status }) => status === "completed"));
assert.equal(cleanCapture.browser.cleanup, "disconnected-external");
await assertRestored();
+2 -2
View File
@@ -98,9 +98,9 @@ export async function readReleaseHistory(run, base, target) {
return { base: baseSha, target: targetSha, commits };
}
// GitHub maps these coding-agent trailers to ordinary User actors.
// REA history maps Anthropic and Cursor's agent trailers to ordinary User actors.
// Keep those source identities in the inventory while excluding automation credit.
const AGENT_ACCOUNTS = new Set(["claude", "cursoragent", "codex"]);
const AGENT_ACCOUNTS = new Set(["claude", "cursoragent"]);
// REA's maintainer requested external-contributor thanks without self-credit.
const MAINTAINER_ACCOUNTS = new Set(["morluto"]);
const human = (value) =>
-120
View File
@@ -1,120 +0,0 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { CATALOG_IDENTITY } from "../../dist/catalogIdentity.js";
import { TOOL_CONTRACTS } from "../../dist/contracts/toolContracts.js";
import {
requireMcpLifecycleResult,
sortVerifierNames,
} from "./mcp-verifier-results.mjs";
const execute = promisify(execFile);
/** Install the selected archive or registry specifier before any protocol deadline. */
export async function installCanaryPackage(specifier, workspace, environment) {
const prefix = join(workspace, "installed");
await execute(
"npm",
[
"install",
"--ignore-scripts",
"--no-save",
"--no-package-lock",
"--no-audit",
"--no-fund",
"--prefix",
prefix,
specifier,
],
{ cwd: workspace, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
return join(prefix, "node_modules", "rea-agents");
}
/** Exercise the same installed CLI/MCP contract before and after publication. */
export async function verifyInstalledCanaryPackage(
packageRoot,
version,
workspace,
environment,
) {
const installed = JSON.parse(
await readFile(join(packageRoot, "package.json"), "utf8"),
);
if (installed.name !== "rea-agents" || installed.version !== version)
throw new Error(
`Installed package identity is ${installed.name}@${installed.version}, expected rea-agents@${version}`,
);
const launcher = join(packageRoot, "scripts", "rea.mjs");
for (const option of ["--help", "--llms"])
await execute(process.execPath, [launcher, option], {
cwd: workspace,
env: environment,
maxBuffer: 16 * 1024 * 1024,
});
const transport = new StdioClientTransport({
command: process.execPath,
args: [launcher, "mcp"],
cwd: workspace,
env: environment,
stderr: "pipe",
});
const client = new Client({ name: "release-package-canary", version: "1" });
let stderr = "";
transport.stderr?.on("data", (chunk) => {
if (stderr.length < 16_384)
stderr += chunk.toString("utf8").slice(0, 16_384 - stderr.length);
});
try {
await client.connect(transport);
const tools = await client.listTools();
const toolNames = tools.tools.map(({ name }) => name);
const canonicalToolNames = new Set(TOOL_CONTRACTS.map(({ name }) => name));
const publishedToolNames = new Set(toolNames);
const unknown = toolNames.filter((name) => !canonicalToolNames.has(name));
const missing = [...canonicalToolNames].filter(
(name) => !publishedToolNames.has(name),
);
if (
toolNames.length === 0 ||
publishedToolNames.size !== toolNames.length ||
unknown.length ||
missing.length
)
throw new Error(
`Installed MCP exposed an invalid canonical tool catalog (${toolNames.length} tools, unknown: ${unknown.join(", ") || "none"}, missing: ${missing.join(", ") || "none"})`,
);
const status = requireMcpLifecycleResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
);
const availability = sortVerifierNames(
status.tool_availability.map(({ name }) => name),
);
if (
JSON.stringify(availability) !==
JSON.stringify(sortVerifierNames(canonicalToolNames))
)
throw new Error(
"Installed binary_session omitted canonical tool availability",
);
return {
mcpTools: toolNames.length,
canonicalMcpTools: CATALOG_IDENTITY.counts.mcp_tools,
};
} catch (cause) {
throw new Error(`Installed package MCP verification failed: ${stderr}`, {
cause,
});
} finally {
try {
await client.close();
} finally {
await transport.close();
}
}
}
+6 -12
View File
@@ -1,15 +1,9 @@
import semver from "semver";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
export function releaseNpmTag(version) {
if (version === undefined || semver.valid(version) !== version)
throw new Error("Expected an exact SemVer version.");
return semver.prerelease(version) === null ? "latest" : "next";
const version = process.argv[2];
if (version === undefined || semver.valid(version) !== version) {
console.error("Expected an exact SemVer version.");
process.exitCode = 1;
} else {
process.stdout.write(semver.prerelease(version) === null ? "latest" : "next");
}
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
process.stdout.write(releaseNpmTag(process.argv[2]));
+1 -103
View File
@@ -1,7 +1,6 @@
"""Exercise automatic route discovery and mistakes that break search metadata."""
import contextlib
import errno
import importlib.util
import io
import json
@@ -10,14 +9,13 @@ import shutil
import tempfile
from threading import Thread
import unittest
from unittest.mock import patch
from http.server import ThreadingHTTPServer
from urllib.error import HTTPError
from urllib.request import urlopen
import xml.etree.ElementTree as ET
import polib
from website_i18n import Document, build_translations, checked_catalog, extract_catalogs, languages, prose_units, reading_pages, read_runtime
from website_i18n import Document, build_translations, checked_catalog, extract_catalogs, languages, reading_pages, read_runtime
ROOT = Path(__file__).resolve().parent.parent
@@ -140,106 +138,6 @@ class WebsiteSearchChecks(unittest.TestCase):
class WebsiteMaintenanceChecks(unittest.TestCase):
"""Exercise contributor edits and the static preview through real consumers."""
def test_contributors_render_from_snapshot_and_reject_drift_without_rewriting(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
for name in ("public", "templates"):
shutil.copytree(ROOT / "website" / name, root / "website" / name)
page = root / "website/public/contributors/index.html"
snapshot = root / "website/public/contributors/people.json"
data = json.loads(snapshot.read_text())
people = data["contributors"][:3]
data["contributors"] = list(reversed(people))
data["updated"] = "2026-10-15"
snapshot.write_text(json.dumps(data))
original = page.read_text()
with self.assertRaisesRegex(ValueError, "Shared HTML is stale"):
WEBSITE.sync_pages(root, check=True)
self.assertEqual(page.read_text(), original)
WEBSITE.sync_pages(root)
source = page.read_text()
doc = Document(source)
cards = [node for node in doc.nodes if "data-contributor" in node.attrs]
self.assertEqual([node.attrs["data-contributor"] for node in cards], sorted(
(person["login"] for person in people), key=str.casefold,
))
links = [node.attrs["href"] for node in doc.nodes if node.attrs.get("class") == "contributor-card"]
self.assertCountEqual(links, [person["html_url"] for person in people])
self.assertIn('<time datetime="2026-10-15" translate="no">2026-10-15</time>', source)
self.assertEqual(WEBSITE.sync_pages(root), [])
translated_units = [unit[1] for unit in prose_units(source, "contributors/index.html")]
for person in people:
self.assertFalse(any(person["login"] in unit for unit in translated_units))
def test_contributor_refresh_collects_all_pages_and_preserves_snapshot_on_failure(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
snapshot = root / "website/public/contributors/people.json"
snapshot.parent.mkdir(parents=True)
original = (ROOT / "website/public/contributors/people.json").read_text()
snapshot.write_text(original)
people = json.loads(original)["contributors"][:3]
response = WEBSITE.subprocess.CompletedProcess([], 0, stdout=json.dumps(people[:2]) + "\n" + json.dumps(people[2:]))
with patch.object(WEBSITE.subprocess, "run", return_value=response) as api:
WEBSITE.refresh_contributors(root)
self.assertIn("--paginate", api.call_args.args[0])
self.assertEqual(json.loads(snapshot.read_text())["contributors"], people)
self.assertEqual(set(snapshot.parent.iterdir()), {snapshot})
good = snapshot.read_text()
for output in (json.dumps(people) + "\n[", json.dumps(people + people), "[]", '{"message":"API error"}'):
with self.subTest(output=output), patch.object(WEBSITE.subprocess, "run", return_value=WEBSITE.subprocess.CompletedProcess([], 0, stdout=output)):
with self.assertRaises(ValueError):
WEBSITE.refresh_contributors(root)
self.assertEqual(snapshot.read_text(), good)
with patch.object(WEBSITE.subprocess, "run", side_effect=WEBSITE.subprocess.CalledProcessError(1, ["gh"])):
with self.assertRaises(WEBSITE.subprocess.CalledProcessError):
WEBSITE.refresh_contributors(root)
self.assertEqual(snapshot.read_text(), good)
def test_contributor_refresh_preserves_snapshot_and_cleans_staging_on_save_failure(self):
people = json.loads((ROOT / "website/public/contributors/people.json").read_text())["contributors"][:2]
response = WEBSITE.subprocess.CompletedProcess([], 0, stdout=json.dumps(people))
write_text = Path.write_text
def partial_write(path, text, *args, **kwargs):
write_text(path, text[:20], *args, **kwargs)
raise OSError(errno.ENOSPC, "injected partial write", str(path))
failures = (
("partial write", patch.object(
Path, "write_text", autospec=True, side_effect=partial_write,
)),
("replace", patch.object(
Path, "replace", autospec=True,
side_effect=PermissionError(errno.EACCES, "injected replace failure"),
)),
)
for name, failure in failures:
with self.subTest(failure=name), tempfile.TemporaryDirectory() as directory:
root = Path(directory)
snapshot = root / "website/public/contributors/people.json"
snapshot.parent.mkdir(parents=True)
snapshot.write_bytes(json.dumps({
"repository": "morluto/rea", "updated": "2026-10-14", "contributors": people[:1],
}).encode("utf-8"))
before = snapshot.read_bytes()
with patch.object(WEBSITE.subprocess, "run", return_value=response), failure as save:
with self.assertRaises(OSError):
WEBSITE.refresh_contributors(root)
self.assertEqual(save.call_count, 1)
self.assertEqual(snapshot.read_bytes(), before)
self.assertEqual(set(snapshot.parent.iterdir()), {snapshot})
def test_contributor_snapshot_rejects_malformed_identity_and_unsafe_links(self):
original = json.loads((ROOT / "website/public/contributors/people.json").read_text())
for field, value in (("id", True), ("id", 0), ("login", ""), ("type", "unknown"),
("html_url", "javascript:alert(1)"), ("html_url", "https://github.com.evil.test/person"),
("avatar_url", "https://token@avatars.githubusercontent.com/u/1")):
with self.subTest(field=field, value=value):
data = {**original, "contributors": [{**original["contributors"][0], field: value}]}
with self.assertRaises(ValueError):
WEBSITE.checked_contributors(data)
def test_shared_edits_preserve_body_and_catch_drift_before_rewriting(self):
with tempfile.TemporaryDirectory() as directory:
root = Path(directory)
+105 -47
View File
@@ -1,3 +1,5 @@
import { Client } from "@modelcontextprotocol/client";
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
import { execFile } from "node:child_process";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
@@ -5,23 +7,20 @@ import { delimiter, join } from "node:path";
import { promisify } from "node:util";
import { pathToFileURL } from "node:url";
import { lt, prerelease, rcompare, valid } from "semver";
import { readNpmRelease, stableUpdateVersion } from "./ci/npm-release.mjs";
import { lt, rcompare } from "semver";
import { CATALOG_IDENTITY } from "../dist/catalogIdentity.js";
import { TOOL_CONTRACTS } from "../dist/contracts/toolContracts.js";
import {
installCanaryPackage,
verifyInstalledCanaryPackage,
} from "./lib/release-package-canary.mjs";
requireMcpLifecycleResult,
sortVerifierNames,
} from "./lib/mcp-verifier-results.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const verifierRun = createVerifierRun();
const execFileAsync = promisify(execFile);
const version = process.argv[2];
if (
typeof version !== "string" ||
valid(version) !== version ||
process.argv.length !== 3
)
if (!/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/u.test(version ?? ""))
throw new Error("Usage: node scripts/verify-published-package.mjs <version>");
const canaryRoot = await mkdtemp(join(tmpdir(), "rea-published-canary-"));
@@ -30,41 +29,88 @@ const serverEnvironment = {
npm_config_cache: join(canaryRoot, "npm-cache"),
};
delete serverEnvironment.HOPPER_TARGET_PATH;
let surface;
const transport = new StdioClientTransport({
command: "npm",
args: [
"exec",
"--yes",
`--package=rea-agents@${version}`,
"--",
"rea",
"mcp",
],
cwd: canaryRoot,
env: serverEnvironment,
stderr: "pipe",
});
const client = new Client({
name: "published-package-canary",
version: "1",
});
let stderr = "";
transport.stderr?.on("data", (chunk) => {
if (stderr.length < 16_384) stderr += chunk.toString("utf8");
});
let publishedToolCount = 0;
let update;
try {
const publishedRoot = await installCanaryPackage(
`rea-agents@${version}`,
canaryRoot,
serverEnvironment,
await client.connect(transport);
const tools = await client.listTools();
const toolNames = tools.tools.map(({ name }) => name);
const canonicalToolNames = new Set(TOOL_CONTRACTS.map(({ name }) => name));
const unknownToolNames = toolNames.filter(
(name) => !canonicalToolNames.has(name),
);
surface = await verifyInstalledCanaryPackage(
publishedRoot,
version,
canaryRoot,
serverEnvironment,
const publishedToolNames = new Set(toolNames);
const missingToolNames = [...canonicalToolNames].filter(
(name) => !publishedToolNames.has(name),
);
update = await verifyPublishedUpdate({
version,
canaryRoot,
updaterRoot: publishedRoot,
publishedToolCount = toolNames.length;
if (
publishedToolCount === 0 ||
publishedToolNames.size !== publishedToolCount ||
unknownToolNames.length > 0 ||
missingToolNames.length > 0
)
throw new Error(
`Published MCP exposed an invalid canonical tool catalog (${String(publishedToolCount)} tools, unknown: ${unknownToolNames.join(", ") || "none"}, missing: ${missingToolNames.join(", ") || "none"})`,
);
const statusReply = await client.callTool({
name: "binary_session",
arguments: {},
});
const status = requireMcpLifecycleResult(statusReply, "binary_session");
const availabilityNames = sortVerifierNames(
status.tool_availability.map(({ name }) => name),
);
if (
JSON.stringify(availabilityNames) !==
JSON.stringify(sortVerifierNames(canonicalToolNames))
)
throw new Error(
"Published binary_session omitted canonical tool availability",
);
update = await verifyPublishedUpdate({ version, canaryRoot });
} catch (cause) {
throw new Error(`Published package verification failed: ${stderr}`, {
cause,
});
} finally {
await rm(canaryRoot, { recursive: true, force: true });
try {
await client.close();
await transport.close();
} finally {
await rm(canaryRoot, { recursive: true, force: true });
}
}
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, ...surface, update })}\n`,
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, mcpTools: publishedToolCount, canonicalMcpTools: CATALOG_IDENTITY.counts.mcp_tools, update })}\n`,
);
async function verifyPublishedUpdate({
version: targetVersion,
canaryRoot,
updaterRoot,
}) {
const updateVersion = stableUpdateVersion(
targetVersion,
await readNpmRelease(targetVersion),
);
async function verifyPublishedUpdate({ version: targetVersion, canaryRoot }) {
const prefix = join(canaryRoot, "update-prefix");
const home = join(canaryRoot, "update-home");
const environment = {
@@ -84,12 +130,12 @@ async function verifyPublishedUpdate({
PI_PROFILE: undefined,
XDG_CONFIG_HOME: join(home, ".config"),
OPENCODE_CONFIG: join(home, ".config", "opencode", "opencode.json"),
npm_config_cache: join(canaryRoot, "npm-cache"),
npm_config_cache: join(canaryRoot, "update-cache"),
npm_config_prefix: prefix,
PATH: `${join(prefix, "bin")}${delimiter}${process.env.PATH ?? ""}`,
};
const previousVersion = await findPreviousPublishedVersion(
updateVersion,
targetVersion,
canaryRoot,
environment,
);
@@ -115,6 +161,21 @@ async function verifyPublishedUpdate({
).stdout.trim();
// Older releases may predate the update command. Exercise the new published
// updater's production workflow against that real previous installation.
const updaterPrefix = join(canaryRoot, "updater");
await execFileAsync(
"npm",
[
"install",
"--ignore-scripts",
"--no-save",
"--no-package-lock",
"--prefix",
updaterPrefix,
`rea-agents@${targetVersion}`,
],
{ cwd: canaryRoot, env: environment, maxBuffer: 16 * 1024 * 1024 },
);
const updaterRoot = join(updaterPrefix, "node_modules", "rea-agents");
const harness = join(canaryRoot, "update.mjs");
await writeFile(
harness,
@@ -135,20 +196,20 @@ if (result.status === "failed") process.exitCode = 1;
if (
result.status !== "updated" ||
result.previousVersion !== previousVersion ||
result.latestVersion !== updateVersion ||
result.installedVersion !== updateVersion ||
result.command.at(-1) !== `rea-agents@${updateVersion}`
result.latestVersion !== targetVersion ||
result.installedVersion !== targetVersion ||
result.command.at(-1) !== `rea-agents@${targetVersion}`
)
throw new Error(
`Published REA updater did not update ${previousVersion} to stable ${updateVersion}: ${stdout}`,
`Published REA updater did not update ${previousVersion} to ${targetVersion}: ${stdout}`,
);
const installed = JSON.parse(
await readFile(join(npmRoot, "rea-agents", "package.json"), "utf8"),
);
if (installed.version !== updateVersion)
if (installed.version !== targetVersion)
throw new Error(
`REA update installed ${installed.version}, expected stable ${updateVersion}`,
`REA update installed ${installed.version}, expected ${targetVersion}`,
);
const help = (
@@ -180,10 +241,7 @@ async function findPreviousPublishedVersion(targetVersion, cwd, environment) {
const previous = versions
.filter(
(candidate) =>
typeof candidate === "string" &&
valid(candidate) === candidate &&
prerelease(candidate) === null &&
lt(candidate, targetVersion),
typeof candidate === "string" && lt(candidate, targetVersion),
)
.sort(rcompare)[0];
if (previous === undefined)
-47
View File
@@ -1,47 +0,0 @@
import { mkdtemp, rm } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join, resolve } from "node:path";
import { valid } from "semver";
import {
installCanaryPackage,
verifyInstalledCanaryPackage,
} from "./lib/release-package-canary.mjs";
import { completeVerifierRun, createVerifierRun } from "./lib/verifier-run.mjs";
const [tarball, version, ...extra] = process.argv.slice(2);
if (
tarball === undefined ||
typeof version !== "string" ||
valid(version) !== version ||
extra.length
)
throw new Error(
"Usage: node scripts/verify-release-artifact.mjs <qualified-tarball> <version>",
);
const source = resolve(tarball);
const verifierRun = createVerifierRun();
const workspace = await mkdtemp(join(tmpdir(), "rea-release-rehearsal-"));
const environment = {
...process.env,
npm_config_cache: join(workspace, "npm-cache"),
};
delete environment.HOPPER_TARGET_PATH;
let result;
try {
const packageRoot = await installCanaryPackage(
source,
workspace,
environment,
);
result = await verifyInstalledCanaryPackage(
packageRoot,
version,
workspace,
environment,
);
} finally {
await rm(workspace, { recursive: true, force: true });
}
process.stdout.write(
`${JSON.stringify({ verifier_run: await completeVerifierRun(verifierRun), package: "rea-agents", version, source, ...result })}\n`,
);
+1 -90
View File
@@ -1,17 +1,14 @@
"""Maintain shared HTML and run the static website's contributor commands."""
import argparse
from datetime import date
from functools import partial
from html import escape
from html.parser import HTMLParser
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
import json
from pathlib import Path, PurePosixPath
from string import Template
import subprocess
import sys
import tempfile
from textwrap import indent
from urllib.parse import quote, urlsplit
@@ -24,89 +21,9 @@ NAVIGATION = (
("Blog", "blog/"),
("Guides", "guides/"),
("FAQ", "faq/"),
("Contributors", "contributors/"),
)
def checked_contributors(data):
"""Validate the public snapshot once, before rendering or replacing it."""
if not isinstance(data, dict) or data.get("repository") != "morluto/rea":
raise ValueError("Contributors must come from morluto/rea")
if not isinstance(data.get("updated"), str):
raise ValueError("Contributors need an ISO update date")
date.fromisoformat(data["updated"])
people = data.get("contributors")
if not isinstance(people, list) or not people:
raise ValueError("Contributors must be a nonempty list")
ids, logins = set(), set()
for person in people:
if not isinstance(person, dict):
raise ValueError("Invalid contributor record")
identity, login = person.get("id"), person.get("login")
if type(identity) is not int or identity <= 0 or identity in ids:
raise ValueError("Contributor IDs must be unique positive integers")
if not isinstance(login, str) or not login.strip() or login.casefold() in logins:
raise ValueError("Contributor usernames must be nonempty and unique")
if person.get("type") not in ("User", "Bot"):
raise ValueError(f"Unknown contributor type: {login}")
for key, host in (("html_url", "github.com"), ("avatar_url", "avatars.githubusercontent.com")):
value = person.get(key)
if not isinstance(value, str):
raise ValueError(f"Missing {key}: {login}")
url = urlsplit(value)
if url.scheme != "https" or url.netloc != host or not url.path:
raise ValueError(f"Invalid public {key}: {login}")
ids.add(identity)
logins.add(login.casefold())
return data
def contributor_block(root):
"""Render equal-sized profile links from the checked GitHub snapshot."""
path = root / "website/public/contributors/people.json"
data = checked_contributors(json.loads(path.read_text(encoding="utf-8")))
cards = []
for person in sorted(data["contributors"], key=lambda item: item["login"].casefold()):
login = escape(person["login"], quote=True)
profile = escape(person["html_url"], quote=True)
avatar = escape(person["avatar_url"], quote=True)
cards.append(
f' <li data-contributor="{login}"><a class="contributor-card" href="{profile}">\n'
f' <img src="{avatar}" alt="" width="56" height="56" loading="lazy" decoding="async" referrerpolicy="no-referrer" />\n'
f' <span dir="ltr">@{login}</span>\n'
' </a></li>'
)
template = root / "website/templates/contributors.html.tmpl"
return indent(Template(template.read_text(encoding="utf-8")).substitute(
cards="\n".join(cards), updated=escape(data["updated"], quote=True),
), " ")
def refresh_contributors(root):
"""Validate every GitHub page, then atomically replace the offline snapshot."""
result = subprocess.run(
["gh", "api", "--paginate", "repos/morluto/rea/contributors?per_page=100", "--jq",
"[.[] | {login,id,type,avatar_url,html_url}]"],
capture_output=True, text=True, check=True,
)
decoder, remaining, people = json.JSONDecoder(), result.stdout.strip(), []
while remaining:
page, end = decoder.raw_decode(remaining)
if not isinstance(page, list):
raise ValueError("GitHub returned an invalid contributors page")
people.extend(page)
remaining = remaining[end:].lstrip()
data = checked_contributors({
"repository": "morluto/rea", "updated": date.today().isoformat(), "contributors": people,
})
path = root / "website/public/contributors/people.json"
with tempfile.TemporaryDirectory(dir=path.parent, prefix=f".{path.name}-") as directory:
staged = Path(directory) / path.name
staged.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
staged.replace(path)
print(f"Refreshed {len(people)} contributors. Run sync, translations extract and check.")
def authored_markdown(root):
"""Discover website documentation while excluding local environments and vendors."""
site = root / "website"
@@ -200,13 +117,10 @@ def shared_blocks(root, path, source):
values["language_picker"] = indent(selector, " ") if not metadata.noindex else ""
values["alternates"] = alternates if not metadata.noindex else ""
templates = root / "website/templates"
blocks = {
return {
name: indent(Template((templates / f"{name}.html.tmpl").read_text(encoding="utf-8")).substitute(values), " ")
for name in ("sharing", "theme", "header", "footer")
}
if route == "contributors/":
blocks["contributors"] = contributor_block(root)
return blocks
def sync_pages(root, check=False):
@@ -292,7 +206,6 @@ def main():
sync = commands.add_parser("sync", help="Update shared HTML after editing templates or page metadata")
sync.add_argument("--check", action="store_true", help="Report drift without rewriting pages")
commands.add_parser("check", help="Check shared HTML, prepare assets, verify links and run regression tests")
commands.add_parser("contributors", help="Refresh the public contributor snapshot using GitHub CLI")
links = commands.add_parser("links", help="Audit external targets using gh and HTTP; separate from offline checks")
links.add_argument("--report", help="Write the full external-link JSON report")
create = commands.add_parser("new-page", help="Create a draft reading page")
@@ -325,8 +238,6 @@ def main():
new_page(root, args.route, args.title, args.description)
elif args.command == "sync":
sync_pages(root, check=args.check)
elif args.command == "contributors":
refresh_contributors(root)
elif args.command == "check":
sync_pages(root, check=True)
for name in ("prepare-website.py", "verify-website.py", "test-website.py"):
+2 -2
View File
@@ -8,13 +8,13 @@
"url": "https://github.com/morluto/rea",
"source": "github"
},
"version": "6.5.0",
"version": "6.4.0",
"packages": [
{
"registryType": "npm",
"registryBaseUrl": "https://registry.npmjs.org",
"identifier": "rea-agents",
"version": "6.5.0",
"version": "6.4.0",
"runtimeHint": "npx",
"transport": {
"type": "stdio"
-4
View File
@@ -223,10 +223,6 @@ export const parseBinaryTarget = async (
}
} catch (cause: unknown) {
if (cause instanceof AnalysisCancelledError) return err(cause);
if (signal?.aborted === true && cause === signal.reason)
return err(
new AnalysisCancelledError("resolve_binary_target", { cause }),
);
if (cause instanceof BinaryTargetError) return err(cause);
if (!(cause instanceof Error)) throw cause;
const failure = classifyBinaryTargetFilesystemFailure(candidate, cause);
+4 -3
View File
@@ -108,9 +108,8 @@ export interface DoctorHost {
/** Validated provider settings preserved by setup, independent of readiness. */
providerRegistrationEnvironment?(): Readonly<Record<string, string>>;
installationPaths?(): Promise<readonly string[]>;
/** Undefined checks installed skill roots; explicit selections stay scoped. */
installedSkillIdentity?(
clientIds: readonly string[] | undefined,
clientIds: readonly string[],
): Promise<InstalledSkillIdentity | undefined>;
close?(): Promise<string | undefined>;
clientRegistrations?(): Promise<readonly ClientRegistrationStatus[]>;
@@ -293,7 +292,9 @@ const collectDoctorIdentity = async (
const registrations = (await host.clientRegistrations?.()) ?? [];
const skillClientIds =
scope.skill_clients ??
(scope.clients.length > 0 ? scope.clients : undefined);
(scope.mode === "audit-wide"
? registrations.map(({ client }) => client)
: scope.clients);
const installedSkillIdentity =
await host.installedSkillIdentity?.(skillClientIds);
const skillAligned = skillIdentityAligned(installedSkillIdentity);
+7 -22
View File
@@ -1,4 +1,4 @@
import { realpath, stat } from "node:fs/promises";
import { realpath } from "node:fs/promises";
import { dirname, resolve } from "node:path";
import { stripComments } from "jsonc-parser";
import { z } from "zod";
@@ -149,27 +149,12 @@ export const readClientPolicyBlock = async (
".gemini",
"settings.json",
);
// Only ENOENT proves absence; preserve other metadata failures as unreadable.
const present = await stat(path).then(
() => ok(true),
(cause: unknown) =>
cause instanceof Error && "code" in cause && cause.code === "ENOENT"
? ok(false)
: err({
kind: "unreadable" as const,
path,
detail: `Configuration ${path} could not be read: ${cause instanceof Error ? cause.message : String(cause)}`,
}),
);
if (!present.ok) return present;
const includeWorkspace = present.value
? await workspaceTrusted(settings, sources, resources)
: ok(false);
if (!includeWorkspace.ok) return includeWorkspace;
if (includeWorkspace.value) {
const document = await readConfigurationFile(path, "json", resources);
if (!document.ok) return document;
if (document.value !== undefined) {
const document = await readConfigurationFile(path, "json", resources);
if (!document.ok) return document;
if (document.value !== undefined) {
const trusted = await workspaceTrusted(settings, sources, resources);
if (!trusted.ok) return trusted;
if (trusted.value) {
const source = { path, document: document.value };
const systemIndex = sources.findIndex(
(item) => item.path === settings.systemPath,
+3 -3
View File
@@ -19,12 +19,12 @@ export const workflowAnalysisProfile = (
createAnalysisProfile(REA_WORKFLOW_PROVIDER, {
upstream_analysis_profile: upstream,
...(operation === "binary_overview"
? { workflow_revision: 4 }
? { workflow_revision: 3 }
: operation === "batch_decompile" ||
operation === "get_call_graph" ||
operation === "trace_feature"
? { workflow_revision: 3 }
: { workflow_revision: 1 }),
? { workflow_revision: 2 }
: {}),
});
/** Provider identity for deterministic artifact inventories. */
@@ -2,7 +2,6 @@ import { z } from "zod";
import { AnalysisProtocolError } from "../domain/analysisErrorCore.js";
import type { AnalysisError } from "../domain/analysisErrorBase.js";
import { NonCanonicalEvidenceBundleError } from "../domain/evidenceBundle.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
import {
@@ -46,8 +45,6 @@ export const buildReconstructionObligationLedgerEvidenceValidated = (
});
return ok(createLedgerEvidence(input, ledger));
} catch (cause: unknown) {
if (cause instanceof NonCanonicalEvidenceBundleError)
return err(workflowInputError(OPERATION, cause));
if (cause instanceof z.ZodError)
return err(workflowInputError(OPERATION, cause));
return err(
+11 -37
View File
@@ -361,7 +361,6 @@ const canonicalSkillFiles = async (
interface CanonicalSkillInspection {
readonly canonical: boolean;
readonly manifest: string | undefined;
readonly installedDestination: boolean;
}
interface SkillInspectionFailure {
@@ -373,7 +372,7 @@ interface SkillInspectionFailure {
const inspectCanonicalSkill = async (
home: string,
clientIds: readonly string[] | undefined,
clientIds: readonly string[],
environment: Readonly<NodeJS.ProcessEnv>,
platform: NodeJS.Platform,
resources?: ArtifactResourceScope,
@@ -384,10 +383,7 @@ const inspectCanonicalSkill = async (
environment,
platform,
);
const issues =
clientIds === undefined
? []
: [...(await skillDirectoryIssues(destinations))];
const issues = [...(await skillDirectoryIssues(destinations))];
if (issues.length > 0)
return err({
_tag: "SkillInspectionFailure",
@@ -398,21 +394,7 @@ const inspectCanonicalSkill = async (
let canonical = true;
let manifest: string | undefined;
let failureCause: unknown;
let installedDestination = false;
for (const destination of destinations) {
const { path: root } = destination;
const pathIssues =
clientIds === undefined ? await skillDirectoryIssues([destination]) : [];
if (pathIssues.length > 0) {
issues.push(...pathIssues);
if (failureCause === undefined) failureCause = pathIssues;
installedDestination = true;
canonical = false;
continue;
}
let rootCanonical = true;
let rootHasFiles = false;
const rootIssues: SkillPathIssue[] = [];
for (const { path: root } of destinations) {
for (const relativePath of MANAGED_SKILL_FILES) {
const destination = join(root, relativePath);
let original: SkillFileState | undefined;
@@ -424,34 +406,27 @@ const inspectCanonicalSkill = async (
cause instanceof ArtifactReaderFailure
)
throw cause;
rootIssues.push({
issues.push({
path: destination,
kind: "unreadable",
detail: causeReason(cause),
});
if (failureCause === undefined) failureCause = cause;
rootCanonical = false;
rootHasFiles = true;
if (issues.length === 1) failureCause = cause;
canonical = false;
continue;
}
if (original !== undefined) rootHasFiles = true;
if (relativePath === "SKILL.md" && manifest === undefined)
manifest = original?.bytes.toString("utf8");
// A mismatch settles freshness, but not coverage of later read failures.
if (original === undefined) rootCanonical = false;
else if (rootCanonical) {
if (original === undefined) canonical = false;
else if (canonical) {
const content = await canonicalSkillContent(relativePath);
rootCanonical = original.bytes.equals(Buffer.from(content, "utf8"));
canonical = original.bytes.equals(Buffer.from(content, "utf8"));
}
}
if (clientIds !== undefined || rootHasFiles || rootIssues.length > 0) {
installedDestination = true;
canonical = canonical && rootCanonical;
issues.push(...rootIssues);
}
}
return issues.length === 0
? ok({ canonical, manifest, installedDestination })
? ok({ canonical, manifest })
: err({
_tag: "SkillInspectionFailure",
issues,
@@ -629,7 +604,7 @@ export const installCanonicalSkill = async (
/** Read installed skill metadata and compare every managed location. */
export const readInstalledSkillIdentity = async (
home: string,
clientIds: readonly string[] | undefined,
clientIds: readonly string[],
environment: Readonly<NodeJS.ProcessEnv> = {},
platform: NodeJS.Platform = process.platform,
resources?: ArtifactResourceScope,
@@ -641,7 +616,6 @@ export const readInstalledSkillIdentity = async (
platform,
resources,
);
if (inspection.ok && !inspection.value.installedDestination) return undefined;
const content = inspection.ok
? inspection.value.manifest
: inspection.error.manifest;
@@ -33,9 +33,7 @@ describe("inert keyed archive decoding", () => {
decodeKeyedArchiveBytes(bytes, { offset: 0, limit: 1 }),
).toThrow(
expect.objectContaining({
name: "XmlPlistTextDecodeError",
reason: "invalid-bytes",
cause: expect.any(TypeError),
code: "ERR_ENCODING_INVALID_ENCODED_DATA",
}),
);
}
+1 -2
View File
@@ -9,8 +9,7 @@ export class CaptureFormatError extends Error {
| "limit",
message: string,
readonly pointer = "",
cause?: unknown,
) {
super(message, cause === undefined ? undefined : { cause });
super(message);
}
}
-44
View File
@@ -2,50 +2,6 @@ import { expect, it } from "vitest";
import { CaptureFormatError } from "./CaptureFormatError.js";
import { decodeSazMetadata } from "./SazMetadata.js";
it.each([
["UTF-8", Buffer.from('<?xml version="1.0" encoding="UTF-8"?><Session/>')],
[
"BOM-marked UTF-16LE",
Buffer.concat([
Buffer.from([0xff, 0xfe]),
Buffer.from(
'<?xml version="1.0" encoding="UTF-16"?><Session/>',
"utf16le",
),
]),
],
] as const)("accepts matching %s XML declarations", (_label, bytes) => {
expect(() => decodeSazMetadata(bytes)).not.toThrow();
});
it.each([
[
"UTF-16 declaration on UTF-8 bytes",
Buffer.from('<?xml version="1.0" encoding="UTF-16"?><Session/>'),
"format",
],
[
"UTF-8 declaration on UTF-16 bytes",
Buffer.concat([
Buffer.from([0xff, 0xfe]),
Buffer.from(
'<?xml version="1.0" encoding="UTF-8"?><Session/>',
"utf16le",
),
]),
"format",
],
[
"unsupported declared encoding",
Buffer.from('<?xml version="1.0" encoding="ISO-8859-1"?><Session/>'),
"unsupported",
],
] as const)("rejects %s", (_label, bytes, reason) => {
expect(() => decodeSazMetadata(bytes)).toThrow(
expect.objectContaining({ reason }),
);
});
it("retains U+FFFD in well-formed metadata as an ordinary XML character", () => {
const decoded = JSON.stringify(
decodeSazMetadata(
+8 -28
View File
@@ -2,43 +2,23 @@ import { DOMParser, ParseError, type Node } from "@xmldom/xmldom";
import { SaxesParser } from "saxes";
import type { JsonValue } from "../../domain/jsonValue.js";
import { WEB_NETWORK_CAPTURE_LIMITS } from "../../domain/webNetworkCapture.js";
import {
decodeXmlPlistText,
XmlPlistTextDecodeError,
} from "../../domain/propertyListXmlText.js";
import { CaptureFormatError } from "./CaptureFormatError.js";
/** Preserve XML node order, attribute names and text without interpreting producer-specific flags. */
export const decodeSazMetadata = (bytes: Buffer): JsonValue => {
let xml: string;
try {
xml = decodeXmlPlistText(bytes);
} catch (cause: unknown) {
if (
cause instanceof XmlPlistTextDecodeError &&
cause.reason === "unsupported-encoding"
)
throw new CaptureFormatError(
"unsupported",
`SAZ metadata declares unsupported XML encoding ${cause.declaredEncoding}.`,
"",
cause,
);
if (
cause instanceof XmlPlistTextDecodeError &&
cause.reason === "encoding-mismatch"
)
throw new CaptureFormatError(
"format",
"SAZ metadata encoding declaration disagrees with its bytes.",
"",
cause,
);
const encoding =
bytes[0] === 0xff && bytes[1] === 0xfe
? "utf-16le"
: bytes[0] === 0xfe && bytes[1] === 0xff
? "utf-16be"
: "utf-8";
xml = new TextDecoder(encoding, { fatal: true }).decode(bytes);
} catch {
throw new CaptureFormatError(
"unsupported",
"SAZ metadata must use UTF-8 or BOM-marked UTF-16.",
"",
cause,
);
}
validateSazXml(xml);
-2
View File
@@ -16,7 +16,6 @@ import {
providerStartupTimeoutSchema,
} from "./providerStartupTimeout.js";
import { mcpInputSchemaProfileSchema } from "./mcpInputSchemaProfile.js";
import { mcpOutputSchemaProfileSchema } from "./mcpOutputSchemaProfile.js";
import { mcpResponseBudgetSchema } from "./mcpResponseBudget.js";
import { parseLoaderArgs, parseStringArray } from "./parsers.js";
@@ -24,7 +23,6 @@ const environmentSchema = z.object({
REA_ANALYSIS_PROVIDER: analysisProviderSelectorSchema.default("auto"),
REA_MCP_MAX_RESPONSE_BYTES: mcpResponseBudgetSchema.optional(),
REA_MCP_INPUT_SCHEMA_PROFILE: mcpInputSchemaProfileSchema.optional(),
REA_MCP_OUTPUT_SCHEMA_PROFILE: mcpOutputSchemaProfileSchema.optional(),
REA_IDA_MCP_CONFIG: z
.string()
.min(1)
+2 -2
View File
@@ -1,4 +1,4 @@
import { accessSync, constants, statSync } from "node:fs";
import { accessSync, constants } from "node:fs";
import type { ConfigurationError } from "../domain/configurationErrors.js";
import { ok, type Result } from "../domain/result.js";
@@ -16,7 +16,7 @@ import {
const executableAvailable = (path: string): boolean => {
try {
accessSync(path, constants.X_OK);
return statSync(path).isFile();
return true;
} catch {
return false;
}
@@ -21,7 +21,6 @@ const emptyManagedMembers = (
sha256: string,
mvid: string,
): ManagedMemberInspection => ({
signature_resolution: null,
artifact: {
path: `/examples/${sha256.slice(0, 8)}.dll`,
sha256,
@@ -48,7 +47,6 @@ const emptyManagedMembers = (
requires_mvid: mvid,
},
types: [],
nesting_read: true,
exported_types: [],
fields: [],
methods: [],
@@ -90,7 +88,6 @@ const runtimeMembers = (): ManagedMemberInspection => {
signature: {
raw_length: 3,
raw_sha256: "3".repeat(64),
resolved_sha256: "3".repeat(64),
kind: "method",
parse_status: "decoded",
calling_convention: "default",
@@ -98,7 +98,7 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [
name: "compare_managed_members",
...toolContractMetadata("compare_managed_members"),
description:
"Compare two authenticated inspect_managed_members Evidence records using unique-only tiers: exact declared type, name, and CIL/signature; exact declared type, name, and signature (decoded or not); exact declared type, name, and decoded structural method shape; then exact CIL/signature or decoded structural method shape alone, which never takes a member whose own declared type and name remain unpaired on the other side or whose declaring type is unresolved. Signatures compare type operands by name, not by build-local metadata row. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart's was not, stays unknown rather than added or removed. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",
"Compare two authenticated inspect_managed_members Evidence records using unique-only tiers: exact declared type, name, and CIL/signature; exact declared type, name, and raw signature (decoded or not); exact declared type, name, and decoded structural method shape; then exact CIL/signature or decoded structural method shape alone, which never takes a member whose own declared type and name remain unpaired on the other side or whose declaring type is unresolved. Names alone are never a matching basis; a member whose signature was not decoded, or whose same-named counterpart's was not, stays unknown rather than added or removed. Metadata tokens remain build-local coordinates bound to each artifact SHA-256 and MVID, and the tuple digest does not itself remap them.",
kind: "application",
inputSchema: compareManagedMembersReferenceInputSchema,
outputSchema: comparisonOutputSchema,
@@ -116,7 +116,7 @@ export const MANAGED_WORKFLOW_TOOL_CONTRACTS = [
name: "verify_managed_native_boundaries",
...toolContractMetadata("verify_managed_native_boundaries"),
description:
"Verify managed P/Invoke/native-boundary declarations against authenticated native export or function Evidence without executing managed code or translating managed metadata tokens into native addresses. The workflow preserves declaration-only, verified, inferred, contradicted, and unresolved states; a name candidate with missing managed or native module identity remains unresolved with its candidate evidence retained.",
"Verify managed P/Invoke/native-boundary declarations against authenticated native export or function Evidence without executing managed code or translating managed metadata tokens into native addresses. The workflow preserves declaration-only, verified, inferred, contradicted, and unresolved states.",
kind: "application",
inputSchema: managedNativeVerificationInputSchema,
outputSchema: nativeVerificationOutputSchema,
@@ -64,8 +64,6 @@ const nullableCount = z.number().int().nonnegative().nullable();
/** Keep identity, selection and coverage; member collections remain retained. */
export const managedMemberSummarySchema = z.strictObject({
signature_resolution: member.signature_resolution,
nesting_read: member.nesting_read,
artifact: member.artifact,
module: member.module,
metadata: member.metadata,
@@ -92,8 +90,6 @@ export const managedMemberSummarySchema = z.strictObject({
});
export const managedMembersViewContextSchema = z.strictObject({
signature_resolution: member.signature_resolution,
nesting_read: member.nesting_read,
identity_scope: member.identity_scope,
selection: member.selection.unwrap(),
source_metadata_status: member.metadata.shape.status,
@@ -273,8 +269,6 @@ export const projectManagedMemberView = (
view,
page,
...shared,
signature_resolution: report.signature_resolution,
nesting_read: report.nesting_read,
identity_scope: report.identity_scope,
selection: report.selection,
source_metadata_status: report.metadata.status,
-59
View File
@@ -539,65 +539,6 @@ describe("browser scenario network comparison stability", () => {
});
});
describe("scenario comparison recovery guidance", () => {
it("retains network guidance beside a changed console event", () => {
const before = scenarioCapture({});
const afterBase = scenarioCapture({});
const consoleEvent = afterBase.events.items[0];
if (consoleEvent?.kind !== "console")
throw new Error("Expected console fixture");
const after = browserScenarioCaptureSchema.parse({
...afterBase,
scenario: {
...afterBase.scenario,
network_content: {
...afterBase.scenario.network_content,
response_body: true,
},
},
events: {
...afterBase.events,
items: [{ ...consoleEvent, text: "settings-ready-again" }],
},
});
const compare = (left: typeof before, right: typeof before) =>
compareBrowserScenarios(
compareBrowserScenariosInputSchema.parse({
before_scenario: left,
after_scenario: right,
}),
);
const changed = compare(before, after);
expect(changed.overall_status).toBe("changed");
expect(changed.steps.flatMap((step) => step.artifact_diffs)).toContainEqual(
expect.objectContaining({
artifact: "events",
status: "changed",
reason: expect.stringContaining("Network content selections differ"),
}),
);
const sameSelection = compare(before, scenarioCapture({}));
expect(sameSelection.overall_status).toBe("unchanged");
expect(
sameSelection.steps.flatMap((step) => step.artifact_diffs),
).not.toContainEqual(expect.objectContaining({ artifact: "events" }));
const quietMismatch = compare(
before,
browserScenarioCaptureSchema.parse({ ...after, events: before.events }),
);
expect(
quietMismatch.steps.flatMap((step) => step.artifact_diffs),
).toContainEqual(
expect.objectContaining({
artifact: "events",
status: "unknown",
reason: expect.stringContaining("Network content selections differ"),
}),
);
});
});
describe("browser scenario comparison", () => {
it("records canonical literal normalization and ignores declared volatile fields", () => {
const before = scenarioCapture({
+1 -1
View File
@@ -425,7 +425,7 @@ const compareScenarioEvents = (
after_state: afterState,
before_sha256: beforeSha256,
after_sha256: afterSha256,
reason: input.incomparableReason ?? null,
reason: null,
},
};
if (
-33
View File
@@ -76,39 +76,6 @@ describe("bundle comparison explicit cross-pairs", () => {
});
describe("bundle comparison", () => {
it("does not report display-only bundle differences as unexplained changes", () => {
const target = {
path: "/left/artifact.elf",
sha256: "a".repeat(64),
format: "elf" as const,
architecture: "x86_64" as const,
};
const record = createEvidence(target, PROVIDER, {
operation: "observe",
parameters: {},
result: { observed: true },
confidence: "observed",
authority: "shipped-artifact",
});
const left = createEvidenceBundle([record]);
const rightRecord = {
...record,
subject: {
...record.subject!,
name: "renamed.elf",
local_path: "/right/renamed.elf",
},
};
const right = { ...left, records: [rightRecord] };
const result = compareBundles(left, right);
expect(result).toMatchObject({
status: "unchanged",
summary: { records_unchanged: 1, unresolved: 0 },
changes: [],
});
expect(result.left_bundle_sha256).not.toBe(result.right_bundle_sha256);
});
it("accepts long caller-selected bundle paths", () => {
const path = `/${"deep/".repeat(1_000)}bundle.json`;
expect(
+5 -1
View File
@@ -102,7 +102,11 @@ export const compareBundles = (
).length;
return bundleComparisonResultSchema.parse({
status:
unresolved > 0 ? "unknown" : changes.length > 0 ? "changed" : "unchanged",
leftDigest === rightDigest && changes.length === 0
? "unchanged"
: unresolved > 0
? "unknown"
: "changed",
left_bundle_sha256: leftDigest,
right_bundle_sha256: rightDigest,
summary: {
-18
View File
@@ -5,7 +5,6 @@ import { ANALYSIS_SNAPSHOT_TARGET } from "./analysisSnapshot.fixture.js";
import {
createEvidenceBundle,
createImmutableEvidenceBundle,
NonCanonicalEvidenceBundleError,
describeEvidenceBundleFailure,
evidenceBundleForTarget,
parseEvidenceBundle,
@@ -21,23 +20,6 @@ const targetDigest = "a".repeat(64);
const foreignDigest = "b".repeat(64);
describe("evidence bundle failure classification", () => {
it("tags valid-shaped bundles that fail canonical ordering", () => {
const records = ["first", "second"].map((label) =>
createEvidence(undefined, provider, {
operation: "observe",
parameters: { label },
result: { label },
}),
);
const canonical = createEvidenceBundle(records);
expect(() =>
parseEvidenceBundle({
...canonical,
records: [...canonical.records].reverse(),
}),
).toThrow(NonCanonicalEvidenceBundleError);
});
it("treats only authenticated standalone Evidence as the wrong input kind", () => {
const record = createEvidence(undefined, provider, {
operation: "observe",
+1 -9
View File
@@ -52,14 +52,6 @@ export const evidenceBundleSchema = z.strictObject({
export type EvidenceBundle = z.infer<typeof evidenceBundleSchema>;
const immutableBundles = new WeakMap<object, EvidenceBundle>();
/** Caller input has valid shape but is not in authenticated canonical form. */
export class NonCanonicalEvidenceBundleError extends TypeError {
constructor() {
super("Evidence bundle manifests are not canonical");
this.name = "NonCanonicalEvidenceBundleError";
}
}
/** Build and authenticate a sealed bundle while reusing immutable Evidence records. */
export const createImmutableEvidenceBundle = (
records: readonly Evidence[],
@@ -315,7 +307,7 @@ const authenticateBundle = (
parsed.unknowns,
);
if (JSON.stringify(parsed) !== JSON.stringify(canonical))
throw new NonCanonicalEvidenceBundleError();
throw new TypeError("Evidence bundle manifests are not canonical");
if (seal) {
freezeJsonSnapshot(canonical);
immutableBundles.set(canonical, canonical);
@@ -108,32 +108,6 @@ export const collectElectronWrappedCallPaths = (
],
};
};
const sequentialFallthrough = (
statement: t.Statement,
env: SymbolicEnvironment,
context: PathContext,
): { readonly continues: boolean; readonly context: PathContext } => {
if (symbolicDefiniteExit(statement)) return { continues: false, context };
if (!t.isIfStatement(statement)) return { continues: true, context };
const truth = symbolicTruth(resolver.evaluate(statement.test, env));
if (truth !== undefined)
return symbolicDefiniteExit(
truth ? statement.consequent : statement.alternate,
)
? { continues: false, context }
: { continues: true, context };
if (symbolicDefiniteExit(statement.consequent))
return {
continues: true,
context: condition(context, statement.test, false),
};
if (symbolicDefiniteExit(statement.alternate))
return {
continues: true,
context: condition(context, statement.test),
};
return { continues: true, context };
};
const metadataContext = (
context: PathContext,
value: SymbolicValue,
@@ -560,9 +534,21 @@ export const collectElectronWrappedCallPaths = (
let next = context;
for (const statement of node.body) {
walk(statement, env, next);
const fallthrough = sequentialFallthrough(statement, env, next);
if (!fallthrough.continues) break;
next = fallthrough.context;
if (symbolicDefiniteExit(statement)) break;
if (t.isIfStatement(statement)) {
const truth = symbolicTruth(resolver.evaluate(statement.test, env));
if (truth !== undefined) {
if (
symbolicDefiniteExit(
truth ? statement.consequent : statement.alternate,
)
)
break;
} else if (symbolicDefiniteExit(statement.consequent))
next = condition(next, statement.test, false);
else if (symbolicDefiniteExit(statement.alternate))
next = condition(next, statement.test);
}
}
return;
}
@@ -653,17 +639,13 @@ export const collectElectronWrappedCallPaths = (
const index = parent.body.findIndex(
(statement) => statement === child,
);
if (index >= 0) {
for (const statement of parent.body.slice(0, index)) {
const fallthrough = sequentialFallthrough(
statement,
env,
context,
);
if (!fallthrough.continues) return;
context = fallthrough.context;
}
}
if (
index >= 0 &&
parent.body
.slice(0, index)
.some((statement) => symbolicDefiniteExit(statement))
)
return;
}
}
call(node, env, context, isolatedSeed?.callee);
@@ -165,6 +165,7 @@ describe("feature seed selection controls", () => {
{ kind: "module", nodeKind: "javascript-module", field: "original_source" },
{ kind: "native-export", nodeKind: "native-export", field: "label" },
{ kind: "native-export", nodeKind: "native-export", field: "key" },
{ kind: "route", nodeKind: "endpoint", field: "label" },
{ kind: "route", nodeKind: "endpoint", field: "value" },
{ kind: "route", nodeKind: "endpoint", field: "path" },
{ kind: "channel", nodeKind: "ipc-channel", field: "label" },
@@ -191,30 +192,6 @@ describe("feature seed selection controls", () => {
},
);
it("excludes route labels while preserving generic scalar and array matching", () => {
const scalar = memberNode("endpoint", {
endpoint_kind: "route",
label: "route endpoint",
});
const array = memberNode("endpoint", {
endpoint_kind: "route",
label: ["route endpoint"],
});
expect(
findApplicationFeatureSeeds([scalar], seed("route", "route endpoint")),
).toEqual([]);
expect(
findApplicationFeatureSeeds([array], seed("route", "route endpoint")),
).toEqual([]);
expect(
findApplicationFeatureSeeds([scalar], seed("string", "route endpoint")),
).toEqual([propertyMatch(scalar, "label")]);
expect(
findApplicationFeatureSeeds([array], seed("string", "route endpoint")),
).toEqual([propertyMatch(array, "label[0]")]);
});
it("keeps generic matching and scalar label and identity selection", () => {
const node = memberNode("context-bridge-api", { members: ["openProject"] });
expect(findApplicationFeatureSeeds([node], seed("string"))).toEqual([
+17 -23
View File
@@ -40,9 +40,17 @@ const seedMatch = (
node: ApplicationNode,
seed: ApplicationFeatureSeed,
): ApplicationFeatureSeedMatch[] => {
const mode = featureSeedMatchMode(seed);
const caseSensitive = seed.case_sensitive;
const query = normalized(seed.value, caseSensitive);
if (seed.kind === "node-id")
return node.node_id === seed.value
? [
{
node_id: node.node_id,
kind: node.kind,
basis: "node-id",
field: "node_id",
},
]
: [];
if (!kindMatches(node, seed.kind)) return [];
const requestedEntryPath =
seed.kind === "api" && "entry_path" in seed ? seed.entry_path : undefined;
@@ -61,6 +69,9 @@ const seedMatch = (
})
)
return [];
const mode = featureSeedMatchMode(seed);
const caseSensitive = seed.case_sensitive;
const query = normalized(seed.value, caseSensitive);
const match = candidateFields(node, seed.kind).find((candidate) => {
const value = normalized(candidate.value, caseSensitive);
return mode === "exact" ? value === query : value.includes(query);
@@ -79,9 +90,9 @@ const seedMatch = (
const kindMatches = (
node: ApplicationNode,
kind: ApplicationFeatureSeed["kind"],
kind: Exclude<ApplicationFeatureSeed["kind"], "node-id">,
): boolean => {
if (kind === "string" || kind === "node-id") return true;
if (kind === "string") return true;
if (kind === "route")
return (
node.kind === "endpoint" &&
@@ -105,25 +116,8 @@ const kindMatches = (
const candidateFields = (
node: ApplicationNode,
seedKind: ApplicationFeatureSeed["kind"],
seedKind: Exclude<ApplicationFeatureSeed["kind"], "node-id">,
): CandidateField[] => {
if (seedKind === "node-id")
return [{ basis: "node-id", field: "node_id", value: node.node_id }];
if (seedKind === "route")
return node.observations.flatMap((observation, index) =>
(["value", "path"] as const).flatMap((key) => {
const value = observation.properties[key];
return typeof value === "string"
? [
{
basis: "property" as const,
field: `observations[${String(index)}].properties.${key}`,
value,
},
]
: [];
}),
);
const fields: CandidateField[] = [
...identityFields(node),
...node.observations.flatMap((observation, index) => [
@@ -46,7 +46,6 @@ const classifyLayer = (
): { readonly states: JavaScriptStaticLoadState[] } => {
const nodes = layer.graph.nodes.filter(isLoadStateNode);
const direct = new Map<string, string[]>();
const ambiguous = new Set<string>();
for (const match of matches)
if (
match.status === "matched" &&
@@ -54,10 +53,6 @@ const classifyLayer = (
match.static_node_id !== null
)
append(direct, match.static_node_id, match.runtime_node_id);
else if (match.status === "ambiguous")
for (const candidate of match.candidate_static_nodes)
if (candidate.static_layer_id === layer.layerId)
ambiguous.add(candidate.static_node_id);
const resident = residentNodes(layer, new Set(direct.keys()));
const runtimeScope = createStaticRuntimeScope(layer, runtimeEntities);
const scopedEntities = runtimeEntities.filter(
@@ -82,12 +77,6 @@ const classifyLayer = (
runtimeNodeIds: [],
reason: "containing-asset-was-loaded",
});
if (ambiguous.has(node.node_id))
return state(layer, node, {
status: "unknown",
runtimeNodeIds: [],
reason: "ambiguous-runtime-candidate",
});
const nodeWithinScope = staticNodeWithinRuntimeScope(node, runtimeScope);
if (layerComplete && nodeWithinScope)
return state(layer, node, {
@@ -228,7 +228,6 @@ const staticLoadStateSchema = z.strictObject({
"not-observed-in-bounded-capture",
"layer-outside-runtime-scope",
"static-or-runtime-coverage-incomplete",
"ambiguous-runtime-candidate",
]),
});
@@ -62,20 +62,19 @@ describe("TypeScript import-equals links", () => {
});
describe("CommonJS wrapper parameter redeclarations", () => {
it("does not infer a CommonJS wrapper from resolution mode in an explicit module", () => {
const path = "app/main.mjs";
it("keeps a top-level wrapper require redeclaration", () => {
const path = "app/main.cjs";
const parsed = parseJavaScriptSource(
'var require; require("./dependency.cjs");',
'var require; require("./wrapper.cjs");',
path,
);
expect(parsed).not.toBeNull();
expect(parsed?.program.sourceType).toBe("module");
const ir = analyzeParsedJavaScriptSemantics(
parsed!,
moduleResolutionModeForJavaScriptReference(path, "static-import"),
);
expect(ir.moduleLinks).not.toContainEqual(
expect.objectContaining({ specifier: "./dependency.cjs" }),
expect(ir.moduleLinks).toContainEqual(
expect.objectContaining({ kind: "require", specifier: "./wrapper.cjs" }),
);
});
@@ -687,7 +687,6 @@ const createState = (
moduleLinks: [],
linkedRequireCalls: new WeakSet(),
moduleLinkBindings: new WeakMap(),
defaultExportExpressions: new WeakMap(),
conditionallySkippedExpressions: new WeakSet(),
requiredModuleEffects: new Map(),
ambientBindingWrites: new Map(),
@@ -102,7 +102,7 @@ export const collectSemanticModuleLink = (
kind: "export",
specifier: null,
importedName: null,
localName: defaultExportLocalName(node.declaration),
localName: defaultDeclarationName(node.declaration),
exportedName: "default",
callableId: semanticCallableIdForNode(node.declaration),
location: range(node),
@@ -557,11 +557,6 @@ const addModuleLink = (
callableId: link.callableId ?? null,
};
state.moduleLinks.push(collected);
if (
t.isExportDefaultDeclaration(referenceNode) &&
t.isExpression(referenceNode.declaration)
)
state.defaultExportExpressions.set(collected, referenceNode.declaration);
if (link.localName !== null) {
const binding = resolveSemanticBindingState(
state,
@@ -821,16 +816,13 @@ const declarationNames = (declaration: t.Declaration): string[] => {
return [];
};
const defaultExportLocalName = (
const defaultDeclarationName = (
declaration: t.ExportDefaultDeclaration["declaration"],
): string | null => {
const value = unwrapJavaScriptExpression(declaration).node;
if (t.isIdentifier(value)) return value.name;
return (t.isFunctionDeclaration(value) || t.isClassDeclaration(value)) &&
t.isIdentifier(value.id)
? value.id.name
): string | null =>
(t.isFunctionDeclaration(declaration) || t.isClassDeclaration(declaration)) &&
t.isIdentifier(declaration.id)
? declaration.id.name
: null;
};
const commonJsExportName = (
node: t.Node,
@@ -28,7 +28,6 @@ import { evaluateSemanticExpression } from "./javascriptSemanticValues.js";
import { range } from "./javascriptStaticAnalysisHelpers.js";
import { semanticReturnCoverage } from "./javascriptSemanticCoverage.js";
import { semanticCallableResultExpressions } from "./javascriptSemanticCallableResults.js";
import { semanticReadInitializers } from "./javascriptSemanticMutationInitializers.js";
interface ReturnExpression {
readonly node: t.Node | null;
@@ -105,18 +104,15 @@ export const resolveSemanticModuleCallables = (
if (link.kind === "type-import") return { ...link, callableId: null };
// Named exports follow their lexical binding, including every assignment.
// A declaration's ID or a callable's display name cannot prove that link.
const captured = state.defaultExportExpressions.get(link);
if (link.localName === null && captured === undefined)
if (link.localName === null)
return link.callableId !== null && callableIds.has(link.callableId)
? link
: { ...link, callableId: null };
const binding = state.moduleLinkBindings.get(link);
const resolved =
captured !== undefined
? callableIdsForNode(captured, state, callableIds, new Set(), true)
: binding === undefined
? []
: callableIdsForBinding(binding, state, callableIds, new Set());
binding === undefined
? []
: callableIdsForBinding(binding, state, callableIds, new Set());
const provenance =
binding === undefined || link.specifier !== null
? undefined
@@ -143,29 +139,21 @@ const callableIdsForBinding = (
state: JavaScriptSemanticAnalysisState,
admitted: ReadonlySet<string>,
seen: ReadonlySet<string>,
read?: t.Node,
): string[] => {
// A single initializer does not prove an assignment executes or replace
// the unknown incoming value of a parameter/catch binding.
if (
seen.has(binding.bindingId) ||
(read === undefined && binding.initializers.length !== 1) ||
binding.initializers.length !== 1 ||
binding.definitions.some(
({ kind }) =>
(read === undefined && kind === "assignment") ||
kind === "parameter" ||
kind === "catch",
kind === "assignment" || kind === "parameter" || kind === "catch",
)
)
return [];
const selected =
read === undefined
? binding
: semanticReadInitializers(binding, read, state.parentsByNode);
const initializer = selected.initializers[0];
const initializer = binding.initializers[0];
if (
initializer === undefined ||
selected.initializers.length !== 1 ||
initializer.projection.length > 0 ||
state.conditionallySkippedExpressions.has(initializer.node)
)
@@ -175,7 +163,6 @@ const callableIdsForBinding = (
state,
admitted,
new Set([...seen, binding.bindingId]),
read !== undefined,
);
};
@@ -184,24 +171,17 @@ const callableIdsForNode = (
state: JavaScriptSemanticAnalysisState,
admitted: ReadonlySet<string>,
seen: ReadonlySet<string>,
snapshot = false,
): string[] => {
const unwrapped = unwrapJavaScriptExpression(node).node;
if (unwrapped !== node)
return callableIdsForNode(unwrapped, state, admitted, seen, snapshot);
return callableIdsForNode(unwrapped, state, admitted, seen);
const direct = semanticCallableIdForNode(node);
if (direct !== null && admitted.has(direct)) return [direct];
if (t.isIdentifier(node)) {
const binding = resolveSemanticBindingState(state, node, node.name);
return binding === undefined
? []
: callableIdsForBinding(
binding,
state,
admitted,
seen,
snapshot ? node : undefined,
);
: callableIdsForBinding(binding, state, admitted, seen);
}
return [];
};
@@ -94,11 +94,6 @@ export interface JavaScriptSemanticAnalysisState {
JavaScriptSemanticModuleLink,
JavaScriptSemanticBindingState
>;
/** Default expressions capture their value at the export statement. */
readonly defaultExportExpressions: WeakMap<
JavaScriptSemanticModuleLink,
t.Node
>;
/** Initializer expressions whose write can be skipped by enclosing control flow. */
readonly conditionallySkippedExpressions: WeakSet<t.Node>;
/** Unbound identifier writes retain their source sites for capture ordering. */
@@ -268,10 +263,8 @@ export const isUnmodifiedCommonJsWrapperBinding = (
name: string,
state: JavaScriptSemanticAnalysisState,
): boolean => {
// Parser sourceType separates explicit modules from script/CommonJS source;
// specifier resolution mode does not establish that distinction.
if (
state.program.sourceType !== "script" ||
state.staticImportResolutionMode !== "require" ||
!["module", "exports", "require"].includes(name) ||
!t.isIdentifier(node, { name })
)
@@ -78,12 +78,12 @@ it("does not resolve a parameter named require as a module dependency", () => {
);
});
it("keeps a top-level wrapper require redeclaration", () => {
it("keeps a formatless top-level require redeclaration unresolved", () => {
const analysis = analyzeJavaScriptStaticSource(
`var require; require("./wrapper.cjs");`,
);
expect(analysis.references).toContainEqual(
expect.objectContaining({ specifier: "./wrapper.cjs" }),
expect.objectContaining({ kind: "require", specifier: null }),
);
});
-36
View File
@@ -79,13 +79,6 @@ const customAttributeSchema = z.object({
const managedSignatureSchema = z.object({
raw_length: z.number().int().min(0),
raw_sha256: digestSchema,
// Evidence recorded before this field existed reads back as null.
resolved_sha256: digestSchema
.nullable()
.default(null)
.describe(
"SHA-256 of the raw signature with each TypeDef or TypeRef operand replaced by that type's namespace-qualified name; equals raw_sha256 when the signature names no types. Unlike raw_sha256 it does not depend on build-local metadata rows. Null when the signature could not be decoded or names a type REA could not resolve, such as a TypeSpec.",
),
kind: z.enum(["method", "field", "unknown"]),
parse_status: z.enum(["decoded", "partial", "unsupported", "malformed"]),
calling_convention: z.string().nullable(),
@@ -103,20 +96,8 @@ const managedTypeSchema = z.object({
namespace: z.string(),
name: z.string(),
full_name: z.string(),
name_sha256: digestSchema
.nullable()
.default(null)
.describe(
"Framed nominal namespace/name/enclosing-name digest, independent of build-local rows. Null for historical Evidence or unresolved nesting; defining assemblies and CLR runtime identity are not established.",
),
flags: z.number().int().min(0).max(0xffff_ffff),
extends_token: tokenSchema.nullable(),
enclosing_type_token: tokenSchema
.nullable()
.default(null)
.describe(
"Observed enclosing TypeDef from NestedClass; null when no enclosing entry was available, which alone does not establish top-level identity.",
),
field_list: z.object({
first_row: z.number().int().min(1).nullable(),
last_row: z.number().int().min(0).nullable(),
@@ -457,13 +438,6 @@ export const managedArtifactInspectionSchema = z.object({
/** Provider-neutral, execution-free metadata/signature/IL member inspection. */
export const managedMemberInspectionSchema = z.object({
signature_resolution: z
.literal("type-names-v1")
.nullable()
.default(null)
.describe(
"Signature type-name resolution was examined, including unresolved operands; null means historical Evidence without this attestation.",
),
artifact: z.object({
path: z.string().min(1),
sha256: digestSchema,
@@ -485,16 +459,6 @@ export const managedMemberInspectionSchema = z.object({
requires_mvid: cliMetadataGuidSchema.nullable(),
}),
types: z.array(managedTypeSchema),
// Member Evidence recorded before NestedClass rows were read omits this
// field; null is historical uncertainty. False means a current NestedClass
// row was unreadable or invalid, not that no nested types exist.
nesting_read: z
.boolean()
.nullable()
.default(null)
.describe(
"Whether all NestedClass rows were read and valid. Null for historical Evidence; false means current nesting is incomplete. Individual name digests can remain unresolved for missing, cyclic or over-deep enclosing chains.",
),
// Member Evidence recorded before ExportedType rows were read omits this
// field; null means the table was not examined, not that it was empty.
exported_types: z.array(managedExportedTypeSchema).nullable().default(null),
@@ -18,7 +18,6 @@ import {
buildMethodItems,
} from "./managedMemberComparisonItems.js";
import { keyMembers, sha256 } from "./managedMemberComparisonMatch.js";
import { hasSignatureResolution } from "./managedMemberSignatureIdentity.js";
import { digestSchema } from "../digests.js";
import { prefixedDigestSchema } from "../digests.js";
@@ -232,9 +231,6 @@ export const compareManagedMembers = (
rightComplete: right.result.metadata.status === "complete",
leftSelection: selectedTypes(left.result),
rightSelection: selectedTypes(right.result),
signatureResolutionExamined:
hasSignatureResolution(left.result) &&
hasSignatureResolution(right.result),
};
const methodItems = buildMethodItems(methodMatches, itemContext);
const fieldItems = buildFieldItems(fieldMatches, itemContext);
@@ -3,7 +3,6 @@ import type {
ManagedMemberComparisonSide,
} from "./managedMemberComparison.js";
import type { ManagedMemberInspection } from "./managedArtifact.js";
import { hasSignatureResolution } from "./managedMemberSignatureIdentity.js";
interface ComparisonCoverageInput {
readonly left: ManagedMemberInspection;
@@ -109,27 +108,8 @@ export const comparisonLimitations = (
const limitations: string[] = [
"Metadata tokens are build-local coordinates; matched pairs are remaps, not persistent identities.",
"Methods pair within one declared type and name by exact CIL/signature, then signature, then structural shape; only then may exact CIL/signature or structural shape alone remap across declared types or names, never taking a member whose own declared type and name remain unpaired on the other side or whose declaring type is unresolved. Names alone are never a matching basis.",
"Fields pair by declared type, name, and signature, then remap by exact decoded signature alone under the same rule; an undecoded signature is compared only for an exact signature match, and an unmatched member with one stays unknown.",
"Fields pair by declared type, name, and signature, then remap by exact decoded signature alone under the same rule; an undecoded signature is compared only by its raw bytes, and an unmatched member with one stays unknown.",
];
const namesTypes = (result: ManagedMemberInspection) =>
result.methods.some(
({ signature }) =>
signature.resolved_sha256 !== null &&
signature.resolved_sha256 !== signature.raw_sha256,
) ||
result.fields.some(
({ signature }) =>
signature.resolved_sha256 !== null &&
signature.resolved_sha256 !== signature.raw_sha256,
);
if (
hasSignatureResolution(left) &&
hasSignatureResolution(right) &&
(namesTypes(left) || namesTypes(right))
)
limitations.push(
"Signature type operands compare by namespace, name, and enclosing names only; defining assemblies and CLR type identity are not established by this comparison.",
);
if (left.coverage.state !== "complete" || right.coverage.state !== "complete")
limitations.push("At least one managed member observation is partial.");
if (!sameSelection(left, right))
@@ -140,14 +120,6 @@ export const comparisonLimitations = (
limitations.push(
`Both observations cover only the selected declaring types (${left.selection.type_names.join(", ")}); member totals and statuses describe those types, not the whole assembly.`,
);
if (!hasSignatureResolution(left) || !hasSignatureResolution(right))
limitations.push(
"At least one managed member observation does not attest signature type-name resolution, so signatures compare by raw bytes, which name types by build-local metadata row.",
);
if (left.nesting_read === null || right.nesting_read === null)
limitations.push(
"At least one managed member observation predates enclosing-name identity. The legacy bare-name comparison does not establish the enclosing owner of a nested type.",
);
if (left.exported_types === null || right.exported_types === null)
limitations.push(
"At least one managed member observation predates ExportedType inspection; its exported and forwarded types are unknown.",
@@ -5,9 +5,9 @@ import type { ManagedMemberComparisonResult } from "./managedMemberComparison.js
import {
type ManagedFieldMatches,
type ManagedMethodMatches,
identityOf,
sha256,
} from "./managedMemberComparisonMatch.js";
import { signatureIdentity } from "./managedMemberSignatureIdentity.js";
type Method = ManagedMemberInspection["methods"][number];
type Field = ManagedMemberInspection["fields"][number];
@@ -30,8 +30,6 @@ interface ComparisonItemContext {
/** Declaring types each side selected; null when it inspected every type. */
readonly leftSelection: ReadonlySet<string> | null;
readonly rightSelection: ReadonlySet<string> | null;
/** Whether matching keyed signatures by resolved digests, not raw bytes. */
readonly signatureResolutionExamined: boolean;
}
/** Whether an inventory observed every member of this member's declaring type. */
@@ -44,18 +42,10 @@ const inventoryCovers = (
(selection === null ||
(member.declaring_type !== null && selection.has(member.declaring_type)));
/**
* Declared type and name of each unpaired member, per side, whose signature
* was not decoded, or was decoded but not resolved while matching keyed
* resolved signatures.
*/
/** Declared type and name of each unpaired member, per side, whose signature was not decoded. */
interface UndecodedOneSidedNames {
readonly resolved: boolean;
readonly undecoded: Readonly<Record<"left" | "right", ReadonlySet<string>>>;
readonly unresolved: Readonly<Record<"left" | "right", ReadonlySet<string>>>;
readonly unknownOwners: Readonly<
Record<"left" | "right", ReadonlySet<string>>
>;
readonly left: ReadonlySet<string>;
readonly right: ReadonlySet<string>;
}
const methodDimensions = (
@@ -68,7 +58,7 @@ const methodDimensions = (
const dimensions: MethodItem["dimensions"] = [];
if (left.flags !== right.flags || left.impl_flags !== right.impl_flags)
dimensions.push("metadata");
if (signatureIdentity(left.signature) !== signatureIdentity(right.signature))
if (left.signature.raw_sha256 !== right.signature.raw_sha256)
dimensions.push("signature");
const leftBodyComplete =
left.body.status === "present" && left.body.truncated_instructions === 0;
@@ -146,20 +136,17 @@ const unmatchedComparison = (): UnmatchedComparison => ({
const methodOnlyItem = (
member: OneSided<Method>,
identity: string,
identityUnavailable: boolean,
context: ComparisonItemContext,
undecoded: UndecodedOneSidedNames,
): MethodItem => {
if (member.left !== undefined) {
const limitations = absenceLimitations(member.left, identity, {
const limitations = absenceLimitations(member.left, {
oppositeComplete: inventoryCovers(
context.rightComplete,
context.rightSelection,
member.left,
),
unpaired: undecoded,
ownIdentityUnavailable: identityUnavailable,
oppositeUndecodedNames: undecoded.right,
opposite: "right",
inventory: "method",
});
@@ -175,14 +162,13 @@ const methodOnlyItem = (
};
}
const limitations = absenceLimitations(member.right, identity, {
const limitations = absenceLimitations(member.right, {
oppositeComplete: inventoryCovers(
context.leftComplete,
context.leftSelection,
member.right,
),
unpaired: undecoded,
ownIdentityUnavailable: identityUnavailable,
oppositeUndecodedNames: undecoded.left,
opposite: "left",
inventory: "method",
});
@@ -204,31 +190,17 @@ const methodOnlyItem = (
* decoded, so no shape round could seek a changed counterpart, or the other
* inventory has an unpaired (one-sided or ambiguous) same-named member whose
* signature was not decoded, which may be this member with a changed
* signature. A decoded signature that names a type REA could not name, while
* the comparison otherwise keys resolved signatures, is compared by raw bytes
* that name types by build-local row, so it is treated the same way.
* signature.
*/
const absenceLimitations = (
member: Method | Field,
identity: string,
opposite: {
readonly oppositeComplete: boolean;
readonly ownIdentityUnavailable: boolean;
readonly unpaired: UndecodedOneSidedNames;
readonly oppositeUndecodedNames: ReadonlySet<string>;
readonly opposite: "left" | "right";
readonly inventory: "method" | "field";
},
): string[] => [
...(opposite.ownIdentityUnavailable
? [
"declaring-type-name-not-resolved: Declared type identity could not be resolved; a bare display name cannot establish a counterpart or absence.",
]
: []),
...(opposite.unpaired.unknownOwners[opposite.opposite].has(member.name)
? [
`counterpart-declaring-type-name-not-resolved: The ${opposite.opposite} ${opposite.inventory} inventory has an unpaired same-named member with unresolved enclosing-name identity, so absence was not established.`,
]
: []),
...(opposite.oppositeComplete
? []
: [
@@ -237,46 +209,17 @@ const absenceLimitations = (
...(member.signature.parse_status === "decoded"
? []
: [
`signature-not-decoded: The member's ${member.signature.parse_status} signature was compared only by exact declared type, name, and signature bytes, so a changed or renamed counterpart could not be sought.`,
`signature-not-decoded: The member's ${member.signature.parse_status} signature was compared only by exact declared type, name, and raw bytes, so a changed or renamed counterpart could not be sought.`,
]),
...(member.signature.parse_status === "decoded" &&
opposite.unpaired.undecoded[opposite.opposite].has(identity)
opposite.oppositeUndecodedNames.size > 0 &&
opposite.oppositeUndecodedNames.has(identityOf(member))
? [
`counterpart-signature-not-decoded: The ${opposite.opposite} ${opposite.inventory} inventory has an unpaired member with the same declared type and name whose signature was not decoded, so it may be this member with a changed signature.`,
]
: []),
...(unresolved(member, opposite.unpaired.resolved)
? [
`signature-not-resolved: The member's signature names a type REA could not name, so it was compared by raw bytes, which name types by build-local metadata row; a counterpart whose referenced type moved rows could not be found.`,
]
: []),
...(member.signature.parse_status === "decoded" &&
!unresolved(member, opposite.unpaired.resolved) &&
opposite.unpaired.unresolved[opposite.opposite].has(identity)
? [
`counterpart-signature-not-resolved: The ${opposite.opposite} ${opposite.inventory} inventory has an unpaired member with the same declared type and name whose signature names a type REA could not name, so it may be this member.`,
]
: []),
];
/** A known signature form whose type operands still compare by raw bytes. */
const unresolved = (member: Method | Field, resolved: boolean): boolean =>
resolved &&
member.signature.kind !== "unknown" &&
member.signature.resolved_sha256 === null;
/** Equal build-local bytes cannot establish equal unresolved type operands. */
const pairedSignatureLimitations = (
left: Method | Field,
right: Method | Field,
examined: boolean,
): string[] =>
unresolved(left, examined) || unresolved(right, examined)
? [
"signature-not-resolved: At least one signature names a type REA could not resolve; equal build-local row bytes do not establish equivalent signature types.",
]
: [];
/**
* Declared names of unpaired members whose signature was not decoded: one-sided
* members and candidates of ambiguous groups, which also remain unpaired.
@@ -284,79 +227,47 @@ const pairedSignatureLimitations = (
interface Unpaired {
readonly item: Method | Field;
readonly identity: string;
readonly identityUnavailable: boolean;
}
const undecodedOneSidedNames = (
members: {
readonly leftOnly: readonly Unpaired[];
readonly rightOnly: readonly Unpaired[];
readonly ambiguous: readonly {
readonly left: readonly Unpaired[];
readonly right: readonly Unpaired[];
}[];
},
resolved: boolean,
): UndecodedOneSidedNames => {
const sides = {
left: [
const undecodedOneSidedNames = (members: {
readonly leftOnly: readonly Unpaired[];
readonly rightOnly: readonly Unpaired[];
readonly ambiguous: readonly {
readonly left: readonly Unpaired[];
readonly right: readonly Unpaired[];
}[];
}): UndecodedOneSidedNames => {
const names = (side: readonly Unpaired[]) =>
new Set(
side
.filter(({ item }) => item.signature.parse_status !== "decoded")
.map(({ identity }) => identity),
);
return {
left: names([
...members.leftOnly,
...members.ambiguous.flatMap(({ left }) => left),
],
right: [
]),
right: names([
...members.rightOnly,
...members.ambiguous.flatMap(({ right }) => right),
],
};
const names = (
side: readonly Unpaired[],
keep: (item: Method | Field) => boolean,
) =>
new Set(
side.filter(({ item }) => keep(item)).map(({ identity }) => identity),
);
const undecoded = (item: Method | Field) =>
item.signature.parse_status !== "decoded";
const notResolved = (item: Method | Field) => unresolved(item, resolved);
return {
resolved,
unknownOwners: {
left: new Set(
sides.left.filter((x) => x.identityUnavailable).map((x) => x.item.name),
),
right: new Set(
sides.right
.filter((x) => x.identityUnavailable)
.map((x) => x.item.name),
),
},
undecoded: {
left: names(sides.left, undecoded),
right: names(sides.right, undecoded),
},
unresolved: {
left: names(sides.left, notResolved),
right: names(sides.right, notResolved),
},
]),
};
};
const fieldOnlyItem = (
member: OneSided<Field>,
identity: string,
identityUnavailable: boolean,
context: ComparisonItemContext,
undecoded: UndecodedOneSidedNames,
): FieldItem => {
if (member.left !== undefined) {
const limitations = absenceLimitations(member.left, identity, {
const limitations = absenceLimitations(member.left, {
oppositeComplete: inventoryCovers(
context.rightComplete,
context.rightSelection,
member.left,
),
unpaired: undecoded,
ownIdentityUnavailable: identityUnavailable,
oppositeUndecodedNames: undecoded.right,
opposite: "right",
inventory: "field",
});
@@ -371,14 +282,13 @@ const fieldOnlyItem = (
};
}
const limitations = absenceLimitations(member.right, identity, {
const limitations = absenceLimitations(member.right, {
oppositeComplete: inventoryCovers(
context.leftComplete,
context.leftSelection,
member.right,
),
unpaired: undecoded,
ownIdentityUnavailable: identityUnavailable,
oppositeUndecodedNames: undecoded.left,
opposite: "left",
inventory: "field",
});
@@ -395,22 +305,19 @@ const fieldOnlyItem = (
/**
* Name only evidence shared by the whole group. Exact-signature ambiguities
* include both declared-identity collisions and unresolved signature choices.
* include both declared-identity collisions and unresolved raw-signature choices.
*/
const ambiguityLimitation = (
members: "methods" | "fields",
basis: MethodItem["match"]["basis"],
identityUnavailable: boolean,
): string =>
identityUnavailable
? "declaring-type-name-not-resolved: Declared type identity remains unresolved; REA did not guess a counterpart from display names, signatures or CIL."
: basis === "exact-signature"
? `Multiple managed ${members} share the same signature; REA did not guess declared ownership or a token remap.`
: basis === "exact-il-signature"
? "Multiple managed methods share the same CIL and signature; REA did not guess a token remap."
: members === "methods"
? "Multiple managed methods share the same structural shape; REA did not guess a token remap."
: "Multiple managed fields share the same signature; REA did not guess a token remap.";
basis === "exact-signature"
? `Multiple managed ${members} share the same raw signature; REA did not guess declared ownership or a token remap.`
: basis === "exact-il-signature"
? "Multiple managed methods share the same CIL and signature; REA did not guess a token remap."
: members === "methods"
? "Multiple managed methods share the same structural shape; REA did not guess a token remap."
: "Multiple managed fields share the same signature; REA did not guess a token remap.";
export const buildMethodItems = (
matches: ManagedMethodMatches,
@@ -418,11 +325,6 @@ export const buildMethodItems = (
): MethodItem[] => {
const items: MethodItem[] = [];
for (const pair of matches.pairs) {
const signatureLimitations = pairedSignatureLimitations(
pair.left.item,
pair.right.item,
context.signatureResolutionExamined,
);
const { dimensions: observedDimensions, bodyUnknown } = methodDimensions(
pair.left.item,
pair.right.item,
@@ -439,7 +341,7 @@ export const buildMethodItems = (
})}`,
status: observedChange
? "changed"
: bodyUnknown || signatureLimitations.length > 0
: bodyUnknown
? "unknown"
: "unchanged",
left: methodIdentity(pair.left.item),
@@ -453,14 +355,11 @@ export const buildMethodItems = (
},
dimensions,
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
...signatureLimitations,
...(bodyUnknown
? [
"Method body facets are unknown because at least one side has unavailable or partial body data.",
]
: []),
],
limitations: bodyUnknown
? [
"Method body facets are unknown because at least one side has unavailable or partial body data.",
]
: [],
});
}
for (const ambiguous of matches.ambiguous) {
@@ -482,40 +381,14 @@ export const buildMethodItems = (
},
dimensions: ["availability"],
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
ambiguityLimitation(
"methods",
ambiguous.basis,
ambiguous.left.some((item) => item.identityUnavailable) ||
ambiguous.right.some((item) => item.identityUnavailable),
),
],
limitations: [ambiguityLimitation("methods", ambiguous.basis)],
});
}
const undecoded = undecodedOneSidedNames(
matches,
context.signatureResolutionExamined,
);
const undecoded = undecodedOneSidedNames(matches);
for (const item of matches.leftOnly)
items.push(
methodOnlyItem(
{ left: item.item },
item.identity,
item.identityUnavailable,
context,
undecoded,
),
);
items.push(methodOnlyItem({ left: item.item }, context, undecoded));
for (const item of matches.rightOnly)
items.push(
methodOnlyItem(
{ right: item.item },
item.identity,
item.identityUnavailable,
context,
undecoded,
),
);
items.push(methodOnlyItem({ right: item.item }, context, undecoded));
return items;
};
@@ -525,25 +398,16 @@ export const buildFieldItems = (
): FieldItem[] => {
const items: FieldItem[] = [];
for (const pair of matches.pairs) {
const signatureLimitations = pairedSignatureLimitations(
pair.left.item,
pair.right.item,
context.signatureResolutionExamined,
);
const changed =
signatureIdentity(pair.left.item.signature) !==
signatureIdentity(pair.right.item.signature) ||
pair.left.item.signature.raw_sha256 !==
pair.right.item.signature.raw_sha256 ||
pair.left.item.flags !== pair.right.item.flags;
items.push({
item_id: `mmc_field_${sha256({
left: pair.left.item.token,
right: pair.right.item.token,
})}`,
status: changed
? "changed"
: signatureLimitations.length > 0
? "unknown"
: "unchanged",
status: changed ? "changed" : "unchanged",
left: fieldIdentity(pair.left.item),
right: fieldIdentity(pair.right.item),
match: {
@@ -554,7 +418,7 @@ export const buildFieldItems = (
candidate_right_tokens: [],
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: signatureLimitations,
limitations: [],
});
}
for (const ambiguous of matches.ambiguous)
@@ -574,38 +438,12 @@ export const buildFieldItems = (
candidate_right_tokens: ambiguous.right.map(({ item }) => item.token),
},
evidence_links: [context.leftEvidenceId, context.rightEvidenceId],
limitations: [
ambiguityLimitation(
"fields",
ambiguous.basis,
ambiguous.left.some((item) => item.identityUnavailable) ||
ambiguous.right.some((item) => item.identityUnavailable),
),
],
limitations: [ambiguityLimitation("fields", ambiguous.basis)],
});
const undecoded = undecodedOneSidedNames(
matches,
context.signatureResolutionExamined,
);
const undecoded = undecodedOneSidedNames(matches);
for (const item of matches.leftOnly)
items.push(
fieldOnlyItem(
{ left: item.item },
item.identity,
item.identityUnavailable,
context,
undecoded,
),
);
items.push(fieldOnlyItem({ left: item.item }, context, undecoded));
for (const item of matches.rightOnly)
items.push(
fieldOnlyItem(
{ right: item.item },
item.identity,
item.identityUnavailable,
context,
undecoded,
),
);
items.push(fieldOnlyItem({ right: item.item }, context, undecoded));
return items;
};
@@ -11,10 +11,6 @@ import type {
ManagedMemberComparisonResult,
ManagedMemberComparisonSide,
} from "./managedMemberComparison.js";
import {
comparableSides,
signatureIdentity,
} from "./managedMemberSignatureIdentity.js";
import type { JsonValue } from "../jsonValue.js";
export const sha256 = (value: JsonValue): string => {
@@ -26,7 +22,6 @@ export const sha256 = (value: JsonValue): string => {
type Method = ManagedMemberInspection["methods"][number];
type Field = ManagedMemberInspection["fields"][number];
type MatchBasis =
ManagedMemberComparisonResult["methods"][number]["match"]["basis"];
type ConcreteMatchBasis = Exclude<MatchBasis, "none">;
@@ -35,12 +30,9 @@ interface Keyed<Item> {
readonly item: Item;
/** Declared type and name; an unresolved type is null, the name remains. */
readonly identity: string;
/** Declared identity could not be resolved; display facts remain. */
readonly identityUnavailable: boolean;
readonly exactKey: string | null;
readonly signatureKey: string | null;
/** The name-free exact signature, resolved where both sides resolve it. */
readonly signatureId: string;
readonly rawSignatureKey: string;
readonly structuralKey: string | null;
}
@@ -57,52 +49,47 @@ interface Ambiguous<Item> {
readonly basis: ConcreteMatchBasis;
}
/** A declared type: a historical bare name or a tagged observed nominal digest. */
type DeclaredName = string | string[] | null;
type DeclaredNameOf = (item: Method | Field) => DeclaredName;
const keyMethod = (item: Method, declared: DeclaredNameOf): Keyed<Method> => ({
const keyMethod = (item: Method): Keyed<Method> => ({
item,
identity: identityOf({ declaring_type: declared(item), name: item.name }),
identityUnavailable: declared(item) === null,
signatureId: signatureIdentity(item.signature),
identity: identityOf(item),
rawSignatureKey: item.signature.raw_sha256,
exactKey:
item.body.status === "present" &&
item.body.il_sha256 !== null &&
item.body.normalized_il_sha256 !== null
? sha256([
"method-exact",
signatureIdentity(item.signature),
item.signature.raw_sha256,
item.body.il_sha256,
item.body.normalized_il_sha256,
])
: null,
// The blob, with type operands named, is the exact signature; decoding
// only gates the shape keys.
signatureKey:
declared(item) === null
? null
: sha256([
"method-signature",
declared(item),
item.name,
signatureIdentity(item.signature),
]),
// The raw blob is the exact signature; decoding only feeds shape keys.
signatureKey: sha256([
"method-signature",
item.declaring_type,
item.name,
item.signature.raw_sha256,
]),
structuralKey:
item.signature.parse_status === "decoded" && item.body.status === "present"
? sha256([
"method-structural",
// Decoded type strings carry build-local rows; the identity names them.
signatureIdentity(item.signature),
item.signature.kind,
item.signature.calling_convention,
item.signature.generic_parameter_count,
item.signature.parameter_count,
item.signature.return_type,
item.signature.parameter_types,
item.body.status,
item.body.header_format,
item.body.il_size,
item.body.max_stack,
item.body.init_locals,
item.body.opcode_counts,
item.body.anchors.map(({ opcode, operand_kind, operand }) => [
item.body.anchors.map(({ opcode, operand_kind }) => [
opcode,
operand_kind,
operand_kind === "constant" ? operand : null,
]),
item.body.exception_regions.map((region) => [
region.flags,
@@ -115,26 +102,22 @@ const keyMethod = (item: Method, declared: DeclaredNameOf): Keyed<Method> => ({
: null,
});
const keyField = (item: Field, declared: DeclaredNameOf): Keyed<Field> => ({
const keyField = (item: Field): Keyed<Field> => ({
item,
identity: identityOf({ declaring_type: declared(item), name: item.name }),
identityUnavailable: declared(item) === null,
signatureId: signatureIdentity(item.signature),
identity: identityOf(item),
rawSignatureKey: item.signature.raw_sha256,
exactKey:
item.signature.parse_status === "decoded"
? sha256(["field-exact", signatureIdentity(item.signature)])
? sha256(["field-exact", item.signature.raw_sha256])
: null,
// Only an undecoded field reaches this round: a decoded field with the
// same signature already met its counterpart in the exact round.
signatureKey:
declared(item) === null
? null
: sha256([
"field-signature",
declared(item),
item.name,
signatureIdentity(item.signature),
]),
// same raw signature already met its counterpart in the exact round.
signatureKey: sha256([
"field-signature",
item.declaring_type,
item.name,
item.signature.raw_sha256,
]),
structuralKey: sha256([
"field-structural",
item.signature.kind,
@@ -158,13 +141,13 @@ const matchMethods = (
{
basis: "exact-signature",
key: ({ signatureKey }) => signatureKey,
competitionKey: ({ signatureId }) => signatureId,
competitionKey: ({ rawSignatureKey }) => rawSignatureKey,
},
{
basis: "structural-method-shape",
// An unresolved owner has no declared shape match to prove.
key: declaredAs((keyed) =>
keyed.identityUnavailable ? null : keyed.structuralKey,
keyed.item.declaring_type === null ? null : keyed.structuralKey,
),
competitionKey: ({ structuralKey }) => structuralKey,
},
@@ -197,7 +180,7 @@ const matchFields = (
{
basis: "exact-signature",
key: ({ signatureKey }) => signatureKey,
competitionKey: ({ signatureId }) => signatureId,
competitionKey: ({ rawSignatureKey }) => rawSignatureKey,
},
{
basis: "field-signature",
@@ -235,7 +218,7 @@ export const identityOf = ({
declaring_type,
name,
}: {
readonly declaring_type: DeclaredName;
readonly declaring_type: string | null;
readonly name: string;
}): string => sha256([declaring_type, name]);
@@ -244,9 +227,7 @@ const declaredAs =
<Item>(key: (item: Keyed<Item>) => string | null) =>
(item: Keyed<Item>): string | null => {
const value = key(item);
return value === null || item.identityUnavailable
? null
: sha256([item.identity, value]);
return value === null ? null : sha256([item.identity, value]);
};
const matchByKeys = <Item extends Declared>({
@@ -269,7 +250,7 @@ const matchByKeys = <Item extends Declared>({
const competitionKey = round.competitionKey ?? ((item) => item.exactKey);
const unpairedLeft = unpairedOf(left, pairedLeft);
const unpairedRight = unpairedOf(right, pairedRight);
const declarable = declaredCandidates(
const declarable = ownerlessDeclarable(
unpairedLeft,
unpairedRight,
competitionKey,
@@ -303,7 +284,7 @@ const matchByKeys = <Item extends Declared>({
const refused =
round.remap === true &&
[...leftItems, ...rightItems].some(
({ identityUnavailable }) => identityUnavailable,
({ item }) => item.declaring_type === null,
);
if (!refused && leftItems.length === 1 && rightItems.length === 1) {
const [leftItem] = leftItems;
@@ -344,7 +325,7 @@ const matchByKeys = <Item extends Declared>({
right.filter((item) => !usedRight.has(item)),
ownerlessContested(
[unpairedOf(left, pairedLeft), unpairedOf(right, pairedRight)],
(member) => member.signatureId,
(member) => member.rawSignatureKey,
),
)) {
for (const item of group.left) usedLeft.add(item);
@@ -366,7 +347,7 @@ const unresolvedSignatureAmbiguities = <Item extends Declared>(
contested: ReadonlySet<string>,
): Ambiguous<Item>[] => {
const key = (member: Keyed<Item>): string =>
competitionIdentityKey(member, member.signatureId) ?? "";
competitionIdentityKey(member, member.rawSignatureKey) ?? "";
const leftGroups = groupBy(left, key);
const rightGroups = groupBy(right, key);
const groups: Ambiguous<Item>[] = [];
@@ -416,7 +397,7 @@ const ownerlessContested = <Item extends Declared>(
const key = competitionIdentityKey(member, keyOf(member));
if (key === null) continue;
seen.set(key, (seen.get(key) ?? 0) + 1);
if (member.identityUnavailable) ownerless.add(key);
if (member.item.declaring_type === null) ownerless.add(key);
}
for (const key of ownerless)
if ((seen.get(key) ?? 0) > 1) contested.add(key);
@@ -444,23 +425,44 @@ const remappable = <Item extends Declared>(
return (
(key !== null &&
contested.has(competitionIdentityKey(member, key) ?? "")) ||
member.identityUnavailable ||
member.item.declaring_type === null ||
!kept.has(member.identity)
);
});
};
/** A declared round needs a resolved owner and no unresolved contender. */
const declaredCandidates = <Item extends Declared>(
/**
* Whether a member may pair in a declared-identity round. A member with an
* unresolved owner may, only when no other member on either side shares its
* selected evidence key: its owner might be any of them, so a shared key
* leaves it to the remap rounds, which keep such a group ambiguous. The
* resolved members that share such a key stay out too, so they cannot pair and
* consume the candidate the ownerless member might own.
*/
const ownerlessDeclarable = <Item extends Declared>(
left: readonly Keyed<Item>[],
right: readonly Keyed<Item>[],
keyOf: (item: Keyed<Item>) => string | null,
): ((member: Keyed<Item>) => boolean) => {
const count = (side: readonly Keyed<Item>[]) => {
const seen = new Map<string, number>();
for (const member of side) {
const key = keyOf(member);
if (key !== null) seen.set(key, (seen.get(key) ?? 0) + 1);
}
return seen;
};
const leftSeen = count(left);
const rightSeen = count(right);
const contested = ownerlessContested([left, right], keyOf);
return (member) => {
if (member.identityUnavailable) return false;
const key = competitionIdentityKey(member, keyOf(member));
return key === null || !contested.has(key);
const key = keyOf(member);
return key !== null &&
contested.has(competitionIdentityKey(member, key) ?? "")
? false
: member.item.declaring_type !== null ||
key === null ||
((leftSeen.get(key) ?? 0) <= 1 && (rightSeen.get(key) ?? 0) <= 1);
};
};
@@ -503,69 +505,47 @@ const matchObserved = <Item extends Declared>(
left: readonly Keyed<Item>[],
right: readonly Keyed<Item>[],
) => ReturnType<typeof matchByKeys<Item>>,
keyLeft: (item: Item) => Keyed<Item>,
keyRight: (item: Item) => Keyed<Item>,
key: (item: Item) => Keyed<Item>,
): ReturnType<typeof matchByKeys<Item>> => {
const matched = match(
left.filter(sides.inRight).map(keyLeft),
right.filter(sides.inLeft).map(keyRight),
left.filter(sides.inRight).map(key),
right.filter(sides.inLeft).map(key),
);
return {
...matched,
leftOnly: [
...matched.leftOnly,
...left.filter((item) => !sides.inRight(item)).map(keyLeft),
...left.filter((item) => !sides.inRight(item)).map(key),
],
rightOnly: [
...matched.rightOnly,
...right.filter((item) => !sides.inLeft(item)).map(keyRight),
...right.filter((item) => !sides.inLeft(item)).map(key),
],
};
};
export const keyMembers = (
observedLeft: ManagedMemberComparisonSide,
observedRight: ManagedMemberComparisonSide,
left: ManagedMemberComparisonSide,
right: ManagedMemberComparisonSide,
): {
readonly methodMatches: ReturnType<typeof matchMethods>;
readonly fieldMatches: ReturnType<typeof matchFields>;
} => {
const [left, right] = comparableSides(observedLeft, observedRight);
const sides = { inLeft: observedBy(left), inRight: observedBy(right) };
const modern =
left.result.nesting_read !== null && right.result.nesting_read !== null;
const declared = (inspection: ManagedMemberInspection): DeclaredNameOf => {
const names = new Map(
inspection.types.map((type) => [type.token, type.name_sha256]),
);
return (item) => {
if (inspection.nesting_read === false) return null;
const digest = names.get(item.declaring_type_token ?? "");
// A historical counterpart cannot repair an observed current name gap.
if (inspection.nesting_read !== null && digest == null) return null;
// Historical observations retain their explicitly limited bare-name mode.
if (!modern) return item.declaring_type;
return digest == null ? null : ["nominal-name-sha256", digest];
};
};
const declaredLeft = declared(left.result);
const declaredRight = declared(right.result);
return {
methodMatches: matchObserved(
left.result.methods,
right.result.methods,
sides,
matchMethods,
(item) => keyMethod(item, declaredLeft),
(item) => keyMethod(item, declaredRight),
keyMethod,
),
fieldMatches: matchObserved(
left.result.fields,
right.result.fields,
sides,
matchFields,
(item) => keyField(item, declaredLeft),
(item) => keyField(item, declaredRight),
keyField,
),
};
};
@@ -1,27 +0,0 @@
import { describe, expect, it } from "vitest";
import { signatureIdentity } from "./managedMemberSignatureIdentity.js";
type Signature = Parameters<typeof signatureIdentity>[0];
const digest = "ab".repeat(32);
const signature = (raw: string, resolved: string | null) =>
({ raw_sha256: raw, resolved_sha256: resolved }) as Signature;
describe("managed signature identity", () => {
it("keeps a raw digest apart from an equal resolved digest", () => {
// An undecodable blob can hold another signature's resolved preimage, so
// its raw digest can equal that signature's resolved digest.
expect(signatureIdentity(signature(digest, null))).not.toBe(
signatureIdentity(signature("cd".repeat(32), digest)),
);
});
it("matches resolved digests and raw digests among themselves", () => {
expect(signatureIdentity(signature("01".repeat(32), digest))).toBe(
signatureIdentity(signature("02".repeat(32), digest)),
);
expect(signatureIdentity(signature(digest, null))).toBe(
signatureIdentity(signature(digest, null)),
);
});
});
@@ -1,64 +0,0 @@
import type { ManagedMemberInspection } from "./managedArtifact.js";
import type { ManagedMemberComparisonSide } from "./managedMemberComparison.js";
type Method = ManagedMemberInspection["methods"][number];
type Field = ManagedMemberInspection["fields"][number];
/**
* The exact signature across builds: raw bytes with type operands resolved to
* names. Raw bytes name types by build-local rows, so they stand in only when
* REA could not resolve the operands, or for Evidence recorded before it did.
* The kind is part of the key: an undecodable blob can hold another
* signature's resolved preimage, so its raw digest can equal that resolved
* digest without the two being the same signature.
*/
export const signatureIdentity = ({
raw_sha256,
resolved_sha256,
}: Method["signature"]): string =>
resolved_sha256 === null
? `raw:${raw_sha256}`
: `resolved:${resolved_sha256}`;
/**
* The producer attests examination independently of whether it could resolve
* any operands. A current inventory of only unresolved types is not legacy.
*/
export const hasSignatureResolution = (
result: ManagedMemberInspection,
): boolean =>
result.signature_resolution !== null ||
(result.methods.length === 0 && result.fields.length === 0);
const withoutResolution = <Member extends Method | Field>(
members: readonly Member[],
): Member[] =>
members.map((member) => ({
...member,
signature: { ...member.signature, resolved_sha256: null },
}));
/**
* Compare raw signatures on both sides when either observation predates
* resolved digests; a resolved digest never equals a raw one for a signature
* that names a type, so mixing them would lose pairs a raw comparison finds.
*/
export const comparableSides = (
left: ManagedMemberComparisonSide,
right: ManagedMemberComparisonSide,
): readonly [ManagedMemberComparisonSide, ManagedMemberComparisonSide] => {
if (
hasSignatureResolution(left.result) &&
hasSignatureResolution(right.result)
)
return [left, right];
const raw = (side: ManagedMemberComparisonSide) => ({
...side,
result: {
...side.result,
methods: withoutResolution(side.result.methods),
fields: withoutResolution(side.result.fields),
},
});
return [raw(left), raw(right)];
};
@@ -124,21 +124,13 @@ const moduleCompatible = (
scope: string | null,
symbol: NativeSymbol,
): boolean => {
if (scope === null || scope.trim().length === 0) return false;
if (scope === null) return true;
const expected = normalizeModule(scope);
return [symbol.module_name, symbol.module_path]
.filter(
(value): value is string =>
value !== null && normalizeModule(value).length > 0,
)
.filter((value): value is string => value !== null)
.some((value) => nativeModuleIdentities(value).has(expected));
};
const hasModuleIdentity = (symbol: NativeSymbol): boolean =>
[symbol.module_name, symbol.module_path].some(
(value) => value !== null && normalizeModule(value).length > 0,
);
const candidateNames = (
managed: Pick<
PinvokeImport,
@@ -198,9 +190,6 @@ const matchPinvoke = (
};
const candidates: [NativeSymbol, ...NativeSymbol[]] = [first, ...remaining];
const scopeKnown =
managed.import_scope_name !== null &&
managed.import_scope_name.trim().length > 0;
const exact = candidates.find(
(symbol) =>
managed.import_name === symbol.name &&
@@ -209,13 +198,17 @@ const matchPinvoke = (
const decorated = candidates.find((symbol) =>
moduleCompatible(managed.import_scope_name, symbol),
);
const selected =
exact ??
decorated ??
(scopeKnown
? candidates.find((symbol) => !hasModuleIdentity(symbol))
: undefined) ??
first;
const selected = exact ?? decorated ?? first;
if (!moduleCompatible(managed.import_scope_name, selected))
return {
match: {
status: "contradicted",
basis: "module-mismatch",
confidence: "unknown",
matched_native: selected,
candidates,
},
};
if (exact !== undefined)
return {
match: {
@@ -229,31 +222,11 @@ const matchPinvoke = (
candidates,
},
};
if (decorated !== undefined)
return {
match: {
basis: "decorated-name-candidate",
status: "inferred",
confidence: "inferred",
matched_native: selected,
candidates,
},
};
if (!scopeKnown || !hasModuleIdentity(selected))
return {
match: {
status: "unresolved",
basis: "module-identity-unknown",
confidence: "unknown",
matched_native: selected,
candidates,
},
};
return {
match: {
status: "contradicted",
basis: "module-mismatch",
confidence: "unknown",
basis: "decorated-name-candidate",
status: "inferred",
confidence: "inferred",
matched_native: selected,
candidates,
},
@@ -261,31 +234,26 @@ const matchPinvoke = (
};
const pinvokeLimitations = (
match: PinvokeMatch,
status: PinvokeVerification["status"],
managed: PinvokeImport,
): readonly string[] => [
...(managed.import_scope_name === null ||
managed.import_scope_name.trim().length === 0
...(managed.import_scope_name === null
? ["Managed declaration does not name an import scope/module."]
: []),
...(match.status === "inferred"
...(status === "inferred"
? [
"Native symbol matched a decorated candidate rather than the exact declared import name.",
]
: []),
...(match.status === "contradicted"
...(status === "contradicted"
? [
"A symbol name candidate exists, but the supplied native Evidence module identity does not match the managed import scope.",
]
: []),
...(match.status === "unresolved"
? match.basis === "module-identity-unknown"
? [
"A native name candidate exists, but a required module identity is unavailable; module correspondence is unknown.",
]
: [
"No supplied native export/function Evidence matched this declared import.",
]
...(status === "unresolved"
? [
"No supplied native export/function Evidence matched this declared import.",
]
: []),
];
@@ -315,7 +283,7 @@ export const verifyPinvoke = ({
declaration_verification: item.verification,
};
const selection = matchPinvoke(item, native.symbols, native.accepted);
const limitations = pinvokeLimitations(selection.match, item);
const limitations = pinvokeLimitations(selection.match.status, item);
return {
verification: pinvokeVerificationSchema.parse({
item_id: `mnv_pinvoke_${digest({
@@ -51,7 +51,7 @@ const observedCandidatesSchema = z
.tuple([nativeSymbolSchema])
.rest(nativeSymbolSchema);
export const pinvokeVerificationSchema = z.union([
export const pinvokeVerificationSchema = z.discriminatedUnion("status", [
z.strictObject({
...pinvokeVerificationContextShape,
status: z.literal("verified"),
@@ -76,14 +76,6 @@ export const pinvokeVerificationSchema = z.union([
matched_native: z.null(),
candidates: emptyArraySchema,
}),
z.strictObject({
...pinvokeVerificationContextShape,
status: z.literal("unresolved"),
basis: z.literal("module-identity-unknown"),
confidence: z.literal("unknown"),
matched_native: nativeSymbolSchema,
candidates: observedCandidatesSchema,
}),
z.strictObject({
...pinvokeVerificationContextShape,
status: z.literal("contradicted"),
-25
View File
@@ -1,25 +0,0 @@
import { expect, it } from "vitest";
import {
decodeXmlPlistText,
XmlPlistTextDecodeError,
} from "./propertyListXmlText.js";
it.each([
[
Buffer.from('<?xml version="1.0" encoding="UTF-16"?><plist/>'),
"encoding-mismatch",
],
[
Buffer.from('<?xml version="1.0" encoding="ISO-8859-1"?><plist/>'),
"unsupported-encoding",
],
[Buffer.from([0xff, 0xfe, 0x3c]), "invalid-bytes"],
] as const)("tags XML decoding failures as %s", (bytes, reason) => {
expect(() => decodeXmlPlistText(bytes)).toThrow(
expect.objectContaining({
name: "XmlPlistTextDecodeError",
reason,
} satisfies Partial<XmlPlistTextDecodeError>),
);
});
+4 -35
View File
@@ -1,19 +1,4 @@
export class XmlPlistTextDecodeError extends TypeError {
constructor(
readonly reason:
| "invalid-bytes"
| "unsupported-encoding"
| "encoding-mismatch",
message: string,
readonly declaredEncoding?: string,
options?: ErrorOptions,
) {
super(message, options);
this.name = "XmlPlistTextDecodeError";
}
}
/** Decode BOM-aware XML plist text and verify any declaration against its bytes. */
/** Decode BOM-aware XML plist text with fatal byte validation. */
export const decodeXmlPlistText = (bytes: Uint8Array): string => {
const encoding =
bytes[0] === 0xff && bytes[1] === 0xfe
@@ -21,17 +6,7 @@ export const decodeXmlPlistText = (bytes: Uint8Array): string => {
: bytes[0] === 0xfe && bytes[1] === 0xff
? "utf-16be"
: "utf-8";
let text: string;
try {
text = new TextDecoder(encoding, { fatal: true }).decode(bytes);
} catch (cause: unknown) {
throw new XmlPlistTextDecodeError(
"invalid-bytes",
"XML text bytes are invalid for the detected encoding",
undefined,
{ cause },
);
}
const text = new TextDecoder(encoding, { fatal: true }).decode(bytes);
const declaration = /^<\?xml\s[^?]*\bencoding\s*=\s*["']([^"']+)["']/u.exec(
text,
)?.[1];
@@ -40,20 +15,14 @@ export const decodeXmlPlistText = (bytes: Uint8Array): string => {
declared !== undefined &&
!["utf-8", "utf-16", "utf-16le", "utf-16be"].includes(declared)
)
throw new XmlPlistTextDecodeError(
"unsupported-encoding",
throw new TypeError(
`Unsupported XML encoding declaration: ${declaration}. Supported encodings are UTF-8 and BOM-marked UTF-16 in either byte order.`,
declaration,
);
if (
declared !== undefined &&
declared !== encoding &&
!(encoding !== "utf-8" && declared === "utf-16")
)
throw new XmlPlistTextDecodeError(
"encoding-mismatch",
"XML encoding declaration disagrees with its bytes",
declaration,
);
throw new TypeError("XML encoding declaration disagrees with its bytes");
return text;
};
+18 -64
View File
@@ -1,8 +1,5 @@
import { digestCanonicalValue } from "./canonicalDigest.js";
import {
compareUnicodeCodePoints,
compositeKey,
} from "./unicodeCodePointOrder.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
import type { WebPageInspection } from "./browserObservationSchemas.js";
import {
webCaptureDiffSchema,
@@ -261,34 +258,14 @@ const sharedNetworkResponseFields = (
for (const inspection of [before, after])
for (const request of inspection.network.requests) {
const identity = networkIdentity(request);
retainSharedResponseFields(shared, identity, request);
for (const [index, redirect] of (request.redirects ?? []).entries())
retainSharedResponseFields(
shared,
redirectNetworkIdentity(identity, index),
redirect,
);
const fields = shared.get(identity) ?? new Set(networkResponseFields);
for (const field of networkResponseFields)
if (request[field] === null) fields.delete(field);
shared.set(identity, fields);
}
return shared;
};
const retainSharedResponseFields = (
shared: Map<string, Set<NetworkResponseField>>,
identity: string,
response: Pick<
WebPageInspection["network"]["requests"][number],
NetworkResponseField
>,
): void => {
const fields = shared.get(identity) ?? new Set(networkResponseFields);
for (const field of networkResponseFields)
if (response[field] === null) fields.delete(field);
shared.set(identity, fields);
};
const redirectNetworkIdentity = (identity: string, index: number): string =>
compositeKey([identity, "redirect", index]);
const networkDimension = (
before: WebPageInspection,
after: WebPageInspection,
@@ -382,19 +359,14 @@ const networkDimension = (
"Network capture or selected HTTP/WebSocket payload-shape coverage is attach-window limited, unavailable, or incomplete.",
inventoryComplete,
);
if (responseFieldsComplete) return compared;
const responseEvidenceReason =
"HTTP response or redirect status, MIME type, or byte-count evidence is incomplete.";
if (compared.status === "changed") return compared;
return {
status: "unknown",
total_changes: 0,
changes: [],
reason:
compared.reason === null
? responseEvidenceReason
: `${responseEvidenceReason} ${compared.reason}`,
};
return compared.status === "unchanged" && !responseFieldsComplete
? {
...compared,
status: "unknown",
reason:
"HTTP response status, MIME type, or byte-count evidence is incomplete.",
}
: compared;
};
const networkMap = (
@@ -413,29 +385,11 @@ const networkMap = (
values.push({
...response,
redirects: (request.redirects ?? []).map(
(
{
request_timestamp: _requestTimestamp,
redirect_event_timestamp: _redirectEventTimestamp,
...redirect
},
index,
) => {
const response: Partial<
Record<NetworkResponseField, number | string>
> = {};
for (const field of responseFields.get(
redirectNetworkIdentity(identity, index),
) ?? [])
if (redirect[field] !== null) response[field] = redirect[field];
return {
url: redirect.url,
response_url: redirect.response_url,
method: redirect.method,
resource_type: redirect.resource_type,
...response,
};
},
({
request_timestamp: _requestTimestamp,
redirect_event_timestamp: _redirectEventTimestamp,
...redirect
}) => redirect,
),
initiator: request.initiator,
...(shapes?.request === true
-3
View File
@@ -24,7 +24,6 @@ import {
readManagedMetadataLayout,
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
const flagNames = (flags: number): string[] => {
@@ -385,12 +384,10 @@ export const inspectManagedArtifactBytes = (
resourceIssues,
);
try {
const memberLists = readTypeMemberListFacts(bytes, metadata);
const inventory = readManagedMetadataInventory(
bytes,
metadata,
resourceDirectory,
memberLists,
);
const issues = [...resourceIssues, ...inventory.issues];
const limitations = [
+5 -282
View File
@@ -11,7 +11,6 @@ import {
type ManagedMetadataLayout,
} from "./ManagedMetadataLayout.js";
import { readManagedPeLayout } from "./ManagedPeReader.js";
import { memberListIndexSize } from "./ManagedMemberPointers.js";
import { edges } from "./ManagedMemberRows.js";
import { parseUserStrings } from "./ManagedUserStrings.js";
import { signature, type ManagedMethod } from "./ManagedMemberInspectorCore.js";
@@ -21,7 +20,6 @@ import { comparisonLimitations } from "../domain/managed/managedMemberComparison
import { buildManagedPeFixture as buildMultiMethodManagedPeFixture } from "../../scripts/fixtures/managed/pe.mjs";
import {
buildManagedNestedTypeFixture,
buildManagedPeFixture,
buildNativePeFixture,
fixtureUserStringHeap,
@@ -526,111 +524,6 @@ describe("managed MethodSpec call targets", () => {
});
});
describe("managed nested types", () => {
const inspectNested = (
options: Parameters<typeof buildManagedNestedTypeFixture>[0] = {},
) => {
const bytes = buildManagedNestedTypeFixture(options);
return inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes, "/fixture/nested.dll"),
);
};
it("reports the enclosing type of each nested type", () => {
const result = inspectNested();
expect(
result.types.map(({ full_name, enclosing_type_token }) => [
full_name,
enclosing_type_token,
]),
).toEqual([
["<Module>", null],
["Fixture.A", null],
["Fixture.B", null],
["Inner", "0x02000002"],
["Inner", "0x02000003"],
["Leaf", "0x02000004"],
]);
expect(result.nesting_read).toBe(true);
expect(result.coverage.issues).toEqual([]);
});
it.each([2, 3, 4, 5, 6, 7])(
"keeps nested visibility %i unresolved when its parent entry is missing",
(visibility) => {
const bytes = buildManagedNestedTypeFixture({ nestedClassRows: [] });
const target = () => managedPeFixtureTarget(bytes, "/fixture/nested.dll");
const row = inspectManagedMembersBytes(bytes, target()).types.find(
(type) => type.token === "0x02000004",
)!.row_offset;
bytes.writeUInt32LE((bytes.readUInt32LE(row) & ~7) | visibility, row);
const result = inspectManagedMembersBytes(bytes, target());
expect(result.nesting_read).toBe(true);
expect(
result.types.find((type) => type.token === "0x02000004"),
).toMatchObject({ enclosing_type_token: null, name_sha256: null });
expect(result.coverage.issues).toContainEqual(
expect.objectContaining({
code: "invalid-row",
scope: "metadata.TypeDef:0x02000004",
offset: row,
}),
);
},
);
it("retains a selected child's contradictory parent issue exactly once", () => {
const bytes = buildManagedNestedTypeFixture();
const target = () => managedPeFixtureTarget(bytes, "/fixture/nested.dll");
const row = inspectManagedMembersBytes(bytes, target()).types.find(
(type) => type.token === "0x02000004",
)!.row_offset;
bytes.writeUInt32LE((bytes.readUInt32LE(row) & ~7) | 1, row);
const result = inspectManagedMembersBytes(bytes, target(), {
typeNames: ["Leaf"],
});
expect(result.types).toHaveLength(1);
expect(result.types[0]?.name_sha256).toBeNull();
expect(result.coverage.state).toBe("partial");
expect(
result.coverage.issues.filter(
(issue) => issue.scope === "metadata.TypeDef:0x02000004",
),
).toEqual([expect.objectContaining({ code: "invalid-row", offset: row })]);
});
it("reports NestedClass rows that do not pair distinct existing types once", () => {
const result = inspectNested({
nestedClassRows: [
[4, 4],
[5, 3],
[5, 2],
[6, 9],
[0, 2],
[4, 0],
[9, 2],
],
});
expect(
result.types.map(({ enclosing_type_token }) => enclosing_type_token),
).toEqual([null, null, null, null, "0x02000003", null]);
expect(result.nesting_read).toBe(false);
expect(
result.coverage.issues.map(({ code, scope }) => [code, scope]),
).toEqual([
["invalid-row", "metadata.NestedClass:0x29000001"],
["invalid-row", "metadata.NestedClass:0x29000003"],
["invalid-row", "metadata.NestedClass:0x29000004"],
["invalid-row", "metadata.NestedClass:0x29000005"],
["invalid-row", "metadata.NestedClass:0x29000006"],
["invalid-row", "metadata.NestedClass:0x29000007"],
]);
});
});
describe("managed exported types", () => {
it("reports type forwarders and nested exported types with their implementation scope", () => {
const bytes = buildManagedPeFixture({
@@ -1331,173 +1224,7 @@ describe("managed derived caller names", () => {
});
});
describe("managed TypeDef list ownership bounds", () => {
it("retains rows but leaves ownership unknown for zero TypeDef list starts", () => {
const bytes = buildManagedPeFixture();
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("fixture has no CLI header");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "cli.metadata"),
size,
);
const listOffset =
metadataRowOffset(layout, 2, 1) +
4 +
2 * layout.stringIndexSize +
layout.codedIndexSize("TypeDefOrRef");
const listIndexWidth = Math.max(
layout.tableIndexSize(3),
layout.tableIndexSize(4),
layout.tableIndexSize(5),
layout.tableIndexSize(6),
);
const writeIndex = (offset: number) =>
listIndexWidth === 2
? bytes.writeUInt16LE(0, offset)
: bytes.writeUInt32LE(0, offset);
writeIndex(listOffset);
writeIndex(listOffset + listIndexWidth);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.types[0]).toMatchObject({
field_list: { first_row: null, last_row: null, count: 0 },
method_list: { first_row: null, last_row: null, count: 0 },
});
expect(result.fields[0]).toMatchObject({
token: "0x04000001",
name: "counter",
declaring_type: null,
});
expect(result.methods[0]).toMatchObject({
token: "0x06000001",
name: "Main",
declaring_type: null,
});
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
detail: expect.stringContaining("TypeDef FieldList start 0"),
}),
expect.objectContaining({
code: "invalid-row",
detail: expect.stringContaining("TypeDef MethodList start 0"),
}),
],
});
});
it("does not infer an earlier owner from an invalid next list endpoint", () => {
const bytes = buildManagedPeFixture({ additionalEmptyTypes: 1 });
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("fixture has no CLI header");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "cli.metadata"),
size,
);
const methodStartOffset =
metadataRowOffset(layout, 2, 2) +
4 +
2 * layout.stringIndexSize +
layout.codedIndexSize("TypeDefOrRef") +
memberListIndexSize(layout, 4);
if (memberListIndexSize(layout, 6) === 2)
bytes.writeUInt16LE(0, methodStartOffset);
else bytes.writeUInt32LE(0, methodStartOffset);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods[0]).toMatchObject({
token: "0x06000001",
declaring_type: null,
});
expect(result.types[0]?.method_list).toMatchObject({
first_row: null,
last_row: null,
count: 0,
});
expect(result.coverage.state).toBe("partial");
});
});
describe("managed TypeDef list ordering", () => {
it("leaves adjacent ownership unknown when valid list starts decrease", () => {
const bytes = buildManagedPeFixture({
additionalEmptyTypes: 1,
methodRowCount: 2,
});
const pe = readManagedPeLayout(bytes);
if (pe.cli === null) throw new Error("fixture has no CLI header");
const { rva, size } = pe.cli.metadata;
const layout = readManagedMetadataLayout(
bytes,
pe.rvaToOffset(rva, size, "cli.metadata"),
size,
);
const methodOffset =
4 +
2 * layout.stringIndexSize +
layout.codedIndexSize("TypeDefOrRef") +
memberListIndexSize(layout, 4);
const writeMethodStart = (row: number, value: number): void => {
const offset = metadataRowOffset(layout, 2, row) + methodOffset;
if (memberListIndexSize(layout, 6) === 2)
bytes.writeUInt16LE(value, offset);
else bytes.writeUInt32LE(value, offset);
};
writeMethodStart(1, 2);
writeMethodStart(2, 1);
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.methods.map(({ declaring_type }) => declaring_type)).toEqual([
null,
null,
]);
expect(result.coverage).toMatchObject({
state: "partial",
issues: [
expect.objectContaining({
code: "invalid-row",
detail: expect.stringContaining(
"MethodList start 1 decreases from preceding start 2",
),
}),
],
});
});
it("accepts the one-based start for an empty ordinary member table", () => {
const bytes = buildManagedPeFixture({ methodRowCount: 0 });
const result = inspectManagedMembersBytes(
bytes,
managedPeFixtureTarget(bytes),
);
expect(result.types[0]?.method_list).toEqual({
first_row: null,
last_row: null,
count: 0,
});
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
});
describe("managed metadata reference bounds", () => {
describe("managed member reference bounds", () => {
it("retains members and reports an out-of-range TypeDef coded index as partial", () => {
const bytes = buildManagedPeFixture({ extendsRaw: 12 });
const result = inspectManagedMembersBytes(
@@ -1632,15 +1359,11 @@ it("decodes generic instantiation member signatures as complete coverage", () =>
// shape, with TypeRef/TypeSpec coded tokens kept as encoded.
const bytes = buildManagedPeFixture({
methodSignature: Buffer.from([
0x00, 0x01, 0x01, 0x15, 0x12, 0x09, 0x01, 0x0e,
0x00, 0x01, 0x01, 0x15, 0x12, 0x71, 0x01, 0x0e,
]),
fieldSignature: Buffer.from([
0x06, 0x15, 0x11, 0x0d, 0x02, 0x13, 0x00, 0x1e, 0x01,
0x06, 0x15, 0x11, 0x0a, 0x02, 0x13, 0x00, 0x1e, 0x01,
]),
typeReferences: [
{ namespace: "System", name: "Func`1" },
{ namespace: "System", name: "ValueTuple`2" },
],
});
const result = inspectManagedMembersBytes(
bytes,
@@ -1650,12 +1373,12 @@ it("decodes generic instantiation member signatures as complete coverage", () =>
expect(result.methods[0]?.signature).toMatchObject({
parse_status: "decoded",
return_type: "void",
parameter_types: ["class:9<string>"],
parameter_types: ["class:113<string>"],
issue: null,
});
expect(result.fields[0]?.signature).toMatchObject({
parse_status: "decoded",
field_type: "valuetype:13<var:0,mvar:1>",
field_type: "valuetype:10<var:0,mvar:1>",
});
expect(result.coverage).toMatchObject({ state: "complete", issues: [] });
});
+10 -54
View File
@@ -29,11 +29,8 @@ import {
type TypeRange,
} from "./ManagedMemberInspectorCore.js";
import { edgeTargets } from "./ManagedMethodSpecs.js";
import { managedTypeNameDigests } from "./ManagedTypeNameDigests.js";
import { typeNameLookup } from "./ManagedTypeNames.js";
import { managedTypeReferences } from "./ManagedTypeReferences.js";
import { parseUserStrings } from "./ManagedUserStrings.js";
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import {
edges,
parseExportedTypes,
@@ -74,8 +71,6 @@ const unavailable = (
requires_mvid: null,
},
types: [],
signature_resolution: "type-names-v1",
nesting_read: false,
exported_types: null,
user_strings: null,
fields: [],
@@ -118,14 +113,12 @@ const readMemberInventory = (
rootOffset,
cli.metadata.size,
);
const memberLists = readTypeMemberListFacts(bytes, layout);
const inventory = readManagedMetadataInventory(
bytes,
layout,
readManagedResourceDirectory(pe, issues),
memberLists,
);
return { layout, inventory, memberLists };
return { layout, inventory };
};
const selectTypes = (
@@ -192,13 +185,9 @@ export const inspectManagedMembersBytes = (
return unavailable(target, bytes, pe.cliIssue, typeNames);
const issues: ManagedParseIssue[] = [];
try {
const { layout, inventory, memberLists } = readMemberInventory(
bytes,
pe,
issues,
);
const { layout, inventory } = readMemberInventory(bytes, pe, issues);
appendManagedParseIssues(issues, inventory.issues);
const ranges = typeRanges(bytes, layout, issues, memberLists);
const ranges = typeRanges(bytes, layout, issues);
const {
tokens: selected,
selection,
@@ -208,22 +197,11 @@ export const inspectManagedMembersBytes = (
appendManagedParseIssues(issues, parsedTypes.issues);
const exportedTypes = parseExportedTypes(bytes, layout);
appendManagedParseIssues(issues, exportedTypes.issues);
const typeReferences = managedTypeReferences(bytes, layout, issues);
const signatureTypeName = managedTypeNameDigests(
layout,
issues,
parsedTypes,
typeReferences,
);
const fields = parseFields(bytes, layout, ranges, memberLists, {
signatureTypeName,
selected,
});
const fields = parseFields(bytes, layout, ranges, selected);
const memberRefs = parseMemberRefs(
bytes,
layout,
typeNameLookup(bytes, layout, ranges, issues, typeReferences),
signatureTypeName,
typeNameLookup(bytes, layout, ranges, issues),
);
appendManagedParseIssues(issues, memberRefs.issues);
const methods = parseMethods({
@@ -231,8 +209,6 @@ export const inspectManagedMembersBytes = (
layout,
pe,
ranges,
signatureTypeName,
memberLists,
selected,
methodOffset,
methodLimit,
@@ -252,6 +228,7 @@ export const inspectManagedMembersBytes = (
methods.core,
]);
appendManagedParseIssues(issues, targets.issues);
const coverageIssues = uniqueManagedParseIssues(issues);
const incompleteMembers =
[...fields.fields, ...memberRefs.refs, ...methods.methods].some(
(member) => member.signature.parse_status !== "decoded",
@@ -266,23 +243,6 @@ export const inspectManagedMembersBytes = (
targets.members,
managedDecodeBudget(layout),
);
const types = parsedTypes.types
.filter((type) => selected === null || selected.has(type.token))
.map((type) => {
managedDecodeBudget(layout).reserve(512, type.row_offset);
return {
...type,
name_sha256:
signatureTypeName(
Number.parseInt(type.token.slice(4), 16) * 4,
)?.toString("hex") ?? null,
};
});
const unnamedTypes = types.reduce(
(count, type) => count + Number(type.name_sha256 === null),
0,
);
const coverageIssues = uniqueManagedParseIssues(issues);
return managedMemberInspectionSchema.parse(
admitManagedProjection(
{
@@ -303,9 +263,10 @@ export const inspectManagedMembersBytes = (
requires_artifact_sha256: target.sha256,
requires_mvid: inventory.module?.mvid ?? null,
},
types,
signature_resolution: "type-names-v1",
nesting_read: parsedTypes.nesting.complete,
types:
selected === null
? parsedTypes.types
: parsedTypes.types.filter((type) => selected.has(type.token)),
exported_types: exportedTypes.exportedTypes,
fields: fields.fields,
methods: methods.methods,
@@ -322,11 +283,6 @@ export const inspectManagedMembersBytes = (
issues: coverageIssues,
},
limitations: [
...(unnamedTypes === 0
? []
: [
`${String(unnamedTypes)} returned TypeDef name identity(s) remain unresolved; incomplete, missing, contradictory, cyclic or over-deep enclosing facts cannot be repaired by bare display names.`,
]),
"Metadata tokens are build-local coordinates and are only meaningful with the reported artifact SHA-256 and MVID.",
"CIL instruction anchors are decoded from file-backed method bodies only; no target assembly is loaded or executed.",
"Exported types are reported from ExportedType rows as declared; forwarded types are not resolved in their implementing assemblies.",
+55 -135
View File
@@ -1,5 +1,3 @@
import { createHash } from "node:crypto";
import {
managedFailure,
ManagedReaderFailure,
@@ -10,10 +8,6 @@ import type {
ManagedParseIssue,
} from "../domain/managed/managedArtifact.js";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import {
readManagedTypeNesting,
type ManagedTypeNesting,
} from "./ManagedTypeNesting.js";
import type {
ManagedMetadataLayout,
MetadataTableLayout,
@@ -26,8 +20,10 @@ import {
readMetadataString,
sha256Bytes,
} from "./ManagedMetadataHeaps.js";
import { memberListTable } from "./ManagedMemberPointers.js";
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import {
memberListIndexSize,
memberListTable,
} from "./ManagedMemberPointers.js";
export type ManagedType = ManagedMemberInspection["types"][number];
export type ManagedExportedType = NonNullable<
@@ -51,10 +47,10 @@ export type ManagedExceptionRegion =
export interface TypeRange {
readonly token: string;
readonly fullName: string | null;
readonly fieldStart: number | null;
readonly fieldEnd: number | null;
readonly methodStart: number | null;
readonly methodEnd: number | null;
readonly fieldStart: number;
readonly fieldEnd: number;
readonly methodStart: number;
readonly methodEnd: number;
}
/**
@@ -84,23 +80,18 @@ export interface ParsedInstruction {
const rowRange = (
table: MetadataTableLayout | undefined,
start: number | null,
end: number | null,
start: number,
nextStart: number,
): {
readonly first: number | null;
readonly last: number | null;
readonly count: number;
} => {
const total = table?.rowCount ?? 0;
if (start === null || end === null)
return { first: null, last: null, count: 0 };
const lastExclusive = Math.min(end, total + 1);
if (start >= lastExclusive) return { first: null, last: null, count: 0 };
return {
first: start,
last: lastExclusive - 1,
count: lastExclusive - start,
};
if (total === 0 || start === 0) return { first: null, last: null, count: 0 };
const end = Math.min(nextStart === 0 ? total + 1 : nextStart, total + 1);
if (start >= end) return { first: null, last: null, count: 0 };
return { first: start, last: end - 1, count: end - start };
};
export const fullName = (namespace: string, name: string): string =>
@@ -111,8 +102,9 @@ const readTypeRange = (
layout: ManagedMetadataLayout,
row: number,
issues: ManagedParseIssue[],
memberLists: TypeMemberListFacts,
): TypeRange => {
const methods = memberListTable(layout, 6);
const fields = memberListTable(layout, 4);
const cursor = metadataRowCursor(bytes, layout, 2, row);
cursor.readUInt32();
const nameIndex = cursor.readIndex(layout.stringIndexSize);
@@ -132,14 +124,27 @@ const readTypeRange = (
issues,
) ?? null;
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const current = memberLists.entries[row - 1];
const fieldStart = cursor.readIndex(memberListIndexSize(layout, 4));
const methodStart = cursor.readIndex(memberListIndexSize(layout, 6));
let nextField = (fields?.rowCount ?? 0) + 1;
let nextMethod = (methods?.rowCount ?? 0) + 1;
const typeTable = layout.table(2);
if (typeTable !== undefined && row < typeTable.rowCount) {
const next = metadataRowCursor(bytes, layout, 2, row + 1);
next.readUInt32();
next.readIndex(layout.stringIndexSize);
next.readIndex(layout.stringIndexSize);
next.readIndex(layout.codedIndexSize("TypeDefOrRef"));
nextField = next.readIndex(memberListIndexSize(layout, 4));
nextMethod = next.readIndex(memberListIndexSize(layout, 6));
}
return {
token: metadataToken(2, row),
fullName: declaredName,
fieldStart: current?.field.start.value ?? null,
fieldEnd: current?.field.end ?? null,
methodStart: current?.method.start.value ?? null,
methodEnd: current?.method.end ?? null,
fieldStart,
fieldEnd: nextField,
methodStart,
methodEnd: nextMethod,
};
};
@@ -147,20 +152,14 @@ export const typeRanges = (
bytes: Buffer,
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
memberLists: TypeMemberListFacts,
): readonly TypeRange[] => {
issues.push(...memberLists.issues);
const typeTable = layout.table(2);
const ranges: TypeRange[] = [];
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1)
ranges.push(readTypeRange(bytes, layout, row, issues, memberLists));
ranges.push(readTypeRange(bytes, layout, row, issues));
return ranges;
};
/**
* Enclosing TypeDef row of each nested TypeDef row, from the NestedClass
* table. Nesting is complete only when every row was read and valid.
*/
export const parseTypes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
@@ -168,14 +167,12 @@ export const parseTypes = (
): {
readonly types: readonly ManagedType[];
readonly issues: readonly ManagedParseIssue[];
readonly nesting: ManagedTypeNesting;
} => {
const typeTable = layout.table(2);
const fields = memberListTable(layout, 4);
const methods = memberListTable(layout, 6);
const items: ManagedType[] = [];
const issues: ManagedParseIssue[] = [];
const nesting = readManagedTypeNesting(bytes, layout, issues);
for (let row = 1; row <= (typeTable?.rowCount ?? 0); row += 1) {
readManagedValue(() => {
const cursor = metadataRowCursor(bytes, layout, 2, row);
@@ -196,16 +193,18 @@ export const parseTypes = (
const extendsRaw = cursor.readIndex(
layout.codedIndexSize("TypeDefOrRef"),
);
const currentRange = ranges[row - 1] ?? null;
const fieldStart = cursor.readIndex(memberListIndexSize(layout, 4));
const methodStart = cursor.readIndex(memberListIndexSize(layout, 6));
const nextRange = ranges[row] ?? null;
const fieldRange = rowRange(
fields,
currentRange?.fieldStart ?? null,
currentRange?.fieldEnd ?? null,
fieldStart,
nextRange?.fieldStart ?? 0,
);
const methodRange = rowRange(
methods,
currentRange?.methodStart ?? null,
currentRange?.methodEnd ?? null,
methodStart,
nextRange?.methodStart ?? 0,
);
const extendsReason = metadataCodedTokenInvalidReason(
extendsRaw,
@@ -220,7 +219,6 @@ export const parseTypes = (
offset: extendsOffset,
detail: `TypeDef Extends coded index 0x${extendsRaw.toString(16)} is invalid: ${extendsReason}`,
});
const enclosingRow = nesting.enclosing.get(row);
items.push({
token: metadataToken(2, row),
row_offset: cursor.start,
@@ -234,9 +232,6 @@ export const parseTypes = (
[2, 1, 27],
layout.rowCounts,
),
enclosing_type_token:
enclosingRow === undefined ? null : metadataToken(2, enclosingRow),
name_sha256: null,
field_list: {
first_row: fieldRange.first,
last_row: fieldRange.last,
@@ -250,7 +245,7 @@ export const parseTypes = (
});
}, issues);
}
return { types: items, issues, nesting };
return { types: items, issues };
};
export const readCompressed = (
@@ -508,26 +503,11 @@ const readTypeOperand = (
return token;
};
/** A TypeDefOrRefOrSpec coded index embedded in a signature blob. */
interface TypeOperand {
readonly offset: number;
readonly next: number;
readonly value: number;
}
const readTypeSignature = (
blob: Buffer,
offset: number,
base: SignaturePosition,
{
layout,
operands,
}: {
/** Validate each type operand against these tables, as specs require. */
readonly layout?: ManagedMetadataLayout;
/** Collect each type operand, to resolve it by name. */
readonly operands?: TypeOperand[];
} = {},
layout?: ManagedMetadataLayout,
): { readonly value: string; readonly next: number } => {
const stack: TypeContinuation[] = [];
let cursor = offset;
@@ -555,7 +535,6 @@ const readTypeSignature = (
cursor += 1;
} else if (kind === 0x1f || kind === 0x20) {
const token = readTypeOperand(blob, cursor + 1, layout);
operands?.push({ offset: cursor + 1, ...token });
const modifier = `${kind === 0x1f ? "modreq" : "modopt"}:${String(token.value)}`;
// A modifier frame on top was pushed by the directly preceding byte;
// keep an adjacent run together and in source order.
@@ -565,7 +544,6 @@ const readTypeSignature = (
cursor = token.next;
} else if (kind === 0x11 || kind === 0x12) {
const token = readTypeOperand(blob, cursor + 1, layout);
operands?.push({ offset: cursor + 1, ...token });
value = `${kind === 0x11 ? "valuetype" : "class"}:${String(token.value)}`;
cursor = token.next;
} else if (kind === 0x1e || kind === 0x13) {
@@ -586,7 +564,6 @@ const readTypeSignature = (
cursor + 1,
);
const token = readTypeOperand(blob, cursor + 2, layout, [2, 1]);
operands?.push({ offset: cursor + 2, ...token });
const argumentCount = readCompressed(blob, token.next);
if (argumentCount.value === 0)
return malformedSignature(
@@ -708,7 +685,7 @@ export const specSignature = (
const types: string[] = [];
// Each Type consumes at least one byte, so a false count fails on truncation.
for (let index = 0; index < count; index += 1) {
const type = readTypeSignature(blob, cursor, "type", { layout });
const type = readTypeSignature(blob, cursor, "type", layout);
types.push(type.value);
cursor = type.next;
}
@@ -734,19 +711,13 @@ export const specSignature = (
}
};
type DecodedSignature = Omit<
ManagedSignature,
"raw_length" | "raw_sha256" | "resolved_sha256"
>;
type DecodedSignature = Omit<ManagedSignature, "raw_length" | "raw_sha256">;
/**
* Decode a field or member method signature. A malformed or unsupported blob
* decodes as kind `unknown` with its reason.
*/
const decodeSignature = (
blob: Buffer,
operands: TypeOperand[] = [],
): DecodedSignature => {
const decodeSignature = (blob: Buffer): DecodedSignature => {
try {
if (blob.length === 0)
throw managedFailure("invalid-blob", "signature", "empty signature", 0);
@@ -754,7 +725,7 @@ const decodeSignature = (
// FieldSig (II.23.2.4) is exactly FIELD; a flagged FIELD header is not a
// method header either, so it is malformed below.
if (first === 0x06) {
const fieldType = readTypeSignature(blob, 1, "field", { operands });
const fieldType = readTypeSignature(blob, 1, "field");
return {
kind: "field",
parse_status: fieldType.next === blob.length ? "decoded" : "partial",
@@ -769,15 +740,11 @@ const decodeSignature = (
};
}
const header = readMethodSignatureHeader(blob, 0, "member");
const returnType = readTypeSignature(blob, header.next, "return", {
operands,
});
const returnType = readTypeSignature(blob, header.next, "return");
let offset = returnType.next;
const parameters: string[] = [];
for (let index = 0; index < header.parameterCount; index += 1) {
const parameter = readTypeSignature(blob, offset, "parameter", {
operands,
});
const parameter = readTypeSignature(blob, offset, "parameter");
parameters.push(parameter.value);
offset = parameter.next;
}
@@ -845,58 +812,11 @@ export const signatureGenericArity = (
? (decoded.generic_parameter_count ?? 0)
: null;
/**
* SHA-256 of the namespace-qualified name of a TypeDefOrRefOrSpec coded index,
* or null when it has no name.
*/
export type SignatureTypeName = (codedIndex: number) => Buffer | null;
/**
* Each type operand is replaced by 0xff, which never starts a compressed
* coded index, then the 32-byte name digest; a blob without type operands
* keeps its raw digest. So no decodable signature's resolved preimage is
* another's, but an undecodable blob can hold one, and its raw digest then
* equals that resolved digest. Comparison keys the two kinds apart.
*/
const OPERAND_MARKER = Buffer.from([0xff]);
/**
* Digest the blob with each type operand replaced by its type's name digest,
* so the value does not depend on build-local row numbers.
*/
const resolvedSha256 = (
blob: Buffer,
operands: readonly TypeOperand[],
typeName: SignatureTypeName,
): string | null => {
const hash = createHash("sha256");
let copied = 0;
for (const operand of operands) {
const name = typeName(operand.value);
if (name === null) return null;
hash.update(blob.subarray(copied, operand.offset));
hash.update(OPERAND_MARKER);
hash.update(name);
copied = operand.next;
}
hash.update(blob.subarray(copied));
return hash.digest("hex");
};
export const signature = (
blob: Buffer,
rawSha256?: string,
typeName: SignatureTypeName = () => null,
): ManagedSignature => {
const operands: TypeOperand[] = [];
const decoded = decodeSignature(blob, operands);
return {
raw_length: blob.length,
raw_sha256: rawSha256 ?? sha256Bytes(blob),
resolved_sha256:
decoded.kind === "unknown"
? null
: resolvedSha256(blob, operands, typeName),
...decoded,
};
};
): ManagedSignature => ({
raw_length: blob.length,
raw_sha256: rawSha256 ?? sha256Bytes(blob),
...decodeSignature(blob),
});
+33 -40
View File
@@ -6,7 +6,6 @@ import { inspectManagedMembersBytes } from "./ManagedMemberInspector.js";
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js";
import { memberListIndexSize } from "./ManagedMemberPointers.js";
import { readTypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import { readManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
buildManagedMemberPointerFixture,
@@ -29,15 +28,15 @@ const metadataLayout = (bytes: Buffer) => {
describe("managed declaring type ownership", () => {
it.prop([
fc.tuple(
fc
.array(fc.integer({ min: 1, max: 3 }), { minLength: 3, maxLength: 3 })
.map((starts) => starts.sort((left, right) => left - right)),
fc
.array(fc.integer({ min: 1, max: 3 }), { minLength: 3, maxLength: 3 })
.map((starts) => starts.sort((left, right) => left - right)),
fc.tuple(
...Array.from({ length: 3 }, () => fc.integer({ min: 0, max: 6 })),
),
fc.tuple(
...Array.from({ length: 3 }, () => fc.integer({ min: 0, max: 6 })),
),
),
])(
"matches literal ownership ranges for valid nondecreasing list starts",
"retains first-match field and method owners for arbitrary metadata lists",
([fields, methods]) => {
const bytes = buildManagedMemberPointerFixture();
const layout = metadataLayout(bytes);
@@ -65,16 +64,11 @@ describe("managed declaring type ownership", () => {
] as const) {
// The fixture's pointers reorder physical rows 1,2 to list positions 2,1.
const expected = [2, 1].map((position) => {
let owner: number | null = null;
for (let index = 0; index < starts.length; index += 1) {
const start = starts[index];
const end = starts[index + 1] ?? 3;
if (start !== undefined && position >= start && position < end) {
owner = index;
break;
}
}
return owner === null ? null : members.types[owner]?.full_name;
const owner = starts.findIndex(
(start, index) =>
position >= start && position < (starts[index + 1] ?? 3),
);
return owner < 0 ? null : members.types[owner]?.full_name;
});
expect(members[table].map((member) => member.declaring_type)).toEqual(
expected,
@@ -86,10 +80,10 @@ describe("managed declaring type ownership", () => {
},
);
it("indexes wide valid member lists once per layout", () => {
const count = 65_538;
it("indexes large unordered member lists once per layout", () => {
const count = 100_000;
const bytes = buildManagedPeFixture({
additionalEmptyTypes: count - 3,
additionalEmptyTypes: count - 1,
methodRowCount: count,
});
const layout = metadataLayout(bytes);
@@ -101,25 +95,24 @@ describe("managed declaring type ownership", () => {
2 * layout.stringIndexSize +
layout.codedIndexSize("TypeDefOrRef") +
memberListIndexSize(layout, 4);
// Each preceding TypeDef owns one row; later TypeDefs have empty intervals.
// This exercises the four-byte list-index boundary without malformed starts.
for (let row = 0; row < count; row += 1)
bytes.writeUInt32LE(
Math.min(row + 1, 65_537),
methodOffset + row * types.rowSize,
);
const facts = readTypeMemberListFacts(bytes, layout);
const lookup = memberDeclaringTypeRowLookup(bytes, layout, 6, facts);
expect(lookup(65_536)).toBe(65_536);
expect(lookup(65_535)).toBe(65_535);
expect(memberDeclaringTypeRowLookup(bytes, layout, 6, facts)).toBe(lookup);
// The first reversed range forces first-match semantics, with the owner last.
// 100,000 MethodDef rows require a four-byte method-list index.
bytes.writeUInt32LE(2, methodOffset);
const start = performance.now();
const lookup = memberDeclaringTypeRowLookup(bytes, layout, 6);
expect(
Array.from({ length: count }, (_, row) => lookup(row + 1)).every(
(owner) => owner === count,
),
).toBe(true);
expect(performance.now() - start).toBeLessThan(2000);
expect(memberDeclaringTypeRowLookup(bytes, layout, 6)).toBe(lookup);
expect([lookup(0), lookup(count + 1)]).toEqual([0, 0]);
});
it("admits ownership storage before allocation and retains failed admission", () => {
const bytes = buildManagedPeFixture();
const layout = metadataLayout(bytes);
const facts = readTypeMemberListFacts(bytes, layout);
const budget = managedDecodeBudget(layout);
const capacityFailure = new ManagedReaderFailure({
code: "resource-limit",
@@ -141,12 +134,12 @@ describe("managed declaring type ownership", () => {
}),
);
try {
expect(() =>
memberDeclaringTypeRowLookup(bytes, layout, 6, facts),
).toThrow(capacityFailure);
expect(() =>
memberDeclaringTypeRowLookup(bytes, layout, 6, facts),
).toThrow(capacityFailure);
expect(() => memberDeclaringTypeRowLookup(bytes, layout, 6)).toThrow(
capacityFailure,
);
expect(() => memberDeclaringTypeRowLookup(bytes, layout, 6)).toThrow(
capacityFailure,
);
expect(admission).toHaveBeenCalledTimes(1);
expect(allocations).toBe(0);
} finally {
+30 -28
View File
@@ -1,35 +1,33 @@
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import { metadataRowCursor } from "./ManagedMetadataHeaps.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
memberListIndexSize,
memberListRowLookup,
memberListTable,
} from "./ManagedMemberPointers.js";
import { ManagedReaderFailure } from "./ManagedReaderFailure.js";
import type {
MemberDefinitionTable,
TypeMemberListFacts,
} from "./ManagedTypeMemberLists.js";
type MemberTable = 4 | 6;
type OwnerLookup = (definitionRow: number) => number;
const lookups = new WeakMap<
TypeMemberListFacts,
Map<MemberDefinitionTable, OwnerLookup | ManagedReaderFailure>
ManagedMetadataLayout,
Map<MemberTable, OwnerLookup | ManagedReaderFailure>
>();
/** One first-match TypeDef owner per physical member, shared by every inspection facet. */
export const memberDeclaringTypeRowLookup = (
bytes: Buffer,
layout: ManagedMetadataLayout,
table: MemberDefinitionTable,
facts: TypeMemberListFacts,
table: MemberTable,
): OwnerLookup => {
let byTable = lookups.get(facts);
let byTable = lookups.get(layout);
const cached = byTable?.get(table);
if (cached instanceof ManagedReaderFailure) throw cached;
if (cached !== undefined) return cached;
if (byTable === undefined) {
byTable = new Map();
lookups.set(facts, byTable);
lookups.set(layout, byTable);
}
try {
const positionOf = memberListRowLookup(bytes, layout, table);
@@ -52,26 +50,30 @@ export const memberDeclaringTypeRowLookup = (
}
return root;
};
const startForType = (row: number): number => {
const cursor = metadataRowCursor(bytes, layout, 2, row);
cursor.readUInt32();
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.stringIndexSize);
cursor.readIndex(layout.codedIndexSize("TypeDefOrRef"));
const fieldStart = cursor.readIndex(memberListIndexSize(layout, 4));
return table === 4
? fieldStart
: cursor.readIndex(memberListIndexSize(layout, 6));
};
let start = types === 0 ? 0 : startForType(1);
for (let row = 1; row <= types; row += 1) {
const memberStart =
table === 4
? facts.entries[row - 1]?.field
: facts.entries[row - 1]?.method;
const start = memberStart?.start.value ?? null;
const end = memberStart?.end ?? null;
// A malformed endpoint makes this entire interval uncertain. In
// particular, zero must not become row one through owner-side clamping.
if (start !== null && end !== null) {
const limit = Math.min(end, positions + 1);
for (
let p = unowned(Math.min(start, positions + 1));
p < limit;
p = unowned(p + 1)
) {
owners[p] = row;
next[p] = p + 1;
}
const end = row === types ? positions + 1 : startForType(row + 1);
const limit = Math.min(end, positions + 1);
for (
let p = unowned(Math.min(Math.max(start, 1), positions + 1));
p < limit;
p = unowned(p + 1)
) {
owners[p] = row;
next[p] = p + 1;
}
start = end;
}
const definitions = layout.table(table)?.rowCount ?? 0;
const lookup: OwnerLookup = (definition) =>
+1 -15
View File
@@ -1,8 +1,5 @@
import { managedDecodeBudget } from "./ManagedDecodeBudget.js";
import type {
ManagedMetadataLayout,
MetadataTableLayout,
} from "./ManagedMetadataLayout.js";
import type { ManagedMetadataLayout } from "./ManagedMetadataLayout.js";
import {
managedFailure,
ManagedReaderFailure,
@@ -35,17 +32,6 @@ export const memberListIndexSize = (
? 4
: 2;
/** Normalize a TypeDef list coordinate once for parsing and owner lookup. */
export const validatedMemberListStart = (
start: number,
table: MetadataTableLayout | undefined,
): number | null => {
const total = table?.rowCount ?? 0;
return Number.isSafeInteger(start) && start >= 1 && start <= total + 1
? start
: null;
};
/** Resolve physical definition rows to list positions without changing token identity. */
export const memberListRowLookup = (
bytes: Buffer,
+7 -22
View File
@@ -17,7 +17,6 @@ import {
type ManagedMemberRef,
type ManagedMethod,
type MemberCore,
type SignatureTypeName,
type TypeRange,
} from "./ManagedMemberInspectorCore.js";
import {
@@ -31,7 +30,6 @@ import {
import { readManagedValue } from "./ManagedReaderFailure.js";
import { methodBody } from "./ManagedMethodBodyReader.js";
import { memberDeclaringTypeRowLookup } from "./ManagedMemberOwnership.js";
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
/**
* Declaring-type tokens whose members are decoded in full; null selects every
@@ -49,14 +47,7 @@ export const parseFields = (
bytes: Buffer,
layout: ManagedMetadataLayout,
ranges: readonly TypeRange[],
memberLists: TypeMemberListFacts,
{
signatureTypeName,
selected = null,
}: {
readonly signatureTypeName: SignatureTypeName;
readonly selected?: SelectedTypeTokens;
},
selected: SelectedTypeTokens = null,
): {
readonly fields: readonly ManagedField[];
readonly core: ReadonlyMap<string, MemberCore>;
@@ -67,7 +58,7 @@ export const parseFields = (
const issues: ManagedParseIssue[] = [];
const table = layout.table(4);
const ownerRow = readManagedValue(
() => memberDeclaringTypeRowLookup(bytes, layout, 4, memberLists),
() => memberDeclaringTypeRowLookup(bytes, layout, 4),
issues,
);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
@@ -106,7 +97,7 @@ export const parseFields = (
declaring_type: declared?.fullName ?? null,
name,
flags,
signature: budgetedSignature(sig, layout, signatureTypeName),
signature: budgetedSignature(sig, layout),
});
}, issues);
}
@@ -188,7 +179,6 @@ export const parseMemberRefs = (
bytes: Buffer,
layout: ManagedMetadataLayout,
typeName: (token: string | null) => string | null,
signatureTypeName: SignatureTypeName,
): {
readonly refs: readonly ManagedMemberRef[];
readonly core: ReadonlyMap<string, MemberCore>;
@@ -240,7 +230,7 @@ export const parseMemberRefs = (
layout.rowCounts,
);
const parentName = typeName(parentToken);
const decoded = budgetedSignature(sig, layout, signatureTypeName);
const decoded = budgetedSignature(sig, layout);
refs.push({
token,
row_offset: cursor.start,
@@ -266,8 +256,6 @@ interface ParseMethodsInput {
readonly layout: ManagedMetadataLayout;
readonly pe: ManagedPeLayout;
readonly ranges: readonly TypeRange[];
readonly signatureTypeName: SignatureTypeName;
readonly memberLists: TypeMemberListFacts;
readonly selected?: SelectedTypeTokens;
readonly methodOffset?: number;
readonly methodLimit?: number;
@@ -278,8 +266,6 @@ export const parseMethods = ({
layout,
pe,
ranges,
signatureTypeName,
memberLists,
selected = null,
methodOffset = 0,
methodLimit = Number.MAX_SAFE_INTEGER,
@@ -299,7 +285,7 @@ export const parseMethods = ({
let unscannedMethods = 0;
const table = layout.table(6);
const ownerRow = readManagedValue(
() => memberDeclaringTypeRowLookup(bytes, layout, 6, memberLists),
() => memberDeclaringTypeRowLookup(bytes, layout, 6),
issues,
);
for (let row = 1; row <= (table?.rowCount ?? 0); row += 1) {
@@ -347,7 +333,7 @@ export const parseMethods = ({
layout.blob.size,
);
cursor.readIndex(layout.tableIndexSize(8));
const decoded = budgetedSignature(sig, layout, signatureTypeName);
const decoded = budgetedSignature(sig, layout);
core.set(token, {
...member,
genericArity: signatureGenericArity(decoded),
@@ -489,13 +475,12 @@ const callerNameBytes = (method: ManagedMethod): number =>
const budgetedSignature = (
bytes: Buffer,
layout: ManagedMetadataLayout,
signatureTypeName: SignatureTypeName,
): ReturnType<typeof signature> => {
const budget = managedDecodeBudget(layout);
budget.admitTransient(bytes.length * 256, layout.blob.offset);
try {
return admitManagedProjection(
signature(bytes, budget.digest(bytes, "sha256"), signatureTypeName),
signature(bytes, budget.digest(bytes, "sha256")),
budget,
);
} finally {
+2 -14
View File
@@ -15,7 +15,6 @@ import {
readResource,
} from "./ManagedMetadataInventoryRows.js";
import { metadataToken } from "./ManagedMetadataHeaps.js";
import type { TypeMemberListFacts } from "./ManagedTypeMemberLists.js";
import {
readManagedValue,
uniqueManagedParseIssues,
@@ -150,22 +149,13 @@ const readAttributes = (
bytes: Buffer,
layout: ManagedMetadataLayout,
issues: ManagedParseIssue[],
memberLists: TypeMemberListFacts,
): {
readonly attributes: readonly CustomAttribute[];
readonly targetFrameworks: readonly string[];
} => {
const attributes = readRows(layout.table(12), (row) =>
readManagedValue(
() =>
readCustomAttribute(
bytes,
layout,
row,
heapExtent(layout),
issues,
memberLists,
),
() => readCustomAttribute(bytes, layout, row, heapExtent(layout), issues),
issues,
),
);
@@ -187,9 +177,8 @@ export const readManagedMetadataInventory = (
bytes: Buffer,
layout: ManagedMetadataLayout,
resourceDirectory: ManagedResourceDirectory | null,
memberLists: TypeMemberListFacts,
): ManagedMetadataInventory => {
const issues: ManagedParseIssue[] = [...memberLists.issues];
const issues: ManagedParseIssue[] = [];
validateIdentityTableCounts(layout, issues);
const module =
readManagedValue(
@@ -212,7 +201,6 @@ export const readManagedMetadataInventory = (
bytes,
layout,
issues,
memberLists,
);
return {
module,
Loaded 100 of 221 files, more files were not shown because too many files have changed in this diff. Show more