Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f62632e7a6 |
No files matched your search
@@ -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: .
|
||||
@@ -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
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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,3 +1,3 @@
|
||||
{
|
||||
".": "6.5.0"
|
||||
".": "6.4.0"
|
||||
}
|
||||
-113
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
|
||||
@@ -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,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()
|
||||
Generated
+2
-2
@@ -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
@@ -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": {
|
||||
|
||||
@@ -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" }>>;
|
||||
@@ -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();
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}>;
|
||||
@@ -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();
|
||||
@@ -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,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();
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
@@ -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"
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,8 +9,7 @@ export class CaptureFormatError extends Error {
|
||||
| "limit",
|
||||
message: string,
|
||||
readonly pointer = "",
|
||||
cause?: unknown,
|
||||
) {
|
||||
super(message, cause === undefined ? undefined : { cause });
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -425,7 +425,7 @@ const compareScenarioEvents = (
|
||||
after_state: afterState,
|
||||
before_sha256: beforeSha256,
|
||||
after_sha256: afterSha256,
|
||||
reason: input.incomparableReason ?? null,
|
||||
reason: null,
|
||||
},
|
||||
};
|
||||
if (
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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 }),
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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>),
|
||||
);
|
||||
});
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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: [] });
|
||||
});
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
Reference in new issue
Block a user