fix(apple): preserve nested Rosetta state and its shared register budget

This commit is contained in:
morluto committed 2026-10-25 08:57:47 +08:00
commit 07fa932ee5
583 files changed
+18687 -3797

No files matched your search

+7
View File
@@ -36,6 +36,9 @@ unchecked. Maintainers can help resolve open questions.
Use "none" or "not applicable" only when established. -->
- Semantic owner and earliest changed stage:
<!-- Name the producer of parsed facts or resources and its affected consumers.
Summarize superseded paths removed and any distinct policy, lifecycle,
lazy-loading, or compatibility responsibility retained. -->
- Contribution scope fit and linked maintainer decision:
- CLI and MCP/tool-catalog contract:
- Provider, bridge, target-format, or platform compatibility:
@@ -68,6 +71,10 @@ source, link, or rendered-document checks. -->
- User-visible CLI/MCP output (if applicable):
- Remaining proof gaps:
For boundary fixes, identify the applicable [boundary regressions](https://github.com/morluto/rea/blob/main/docs/testing.md#boundary-regressions)
and [lifecycle regressions](https://github.com/morluto/rea/blob/main/docs/testing.md#lifecycle-regressions)
covered by the evidence above, and explain relevant gaps.
For evidence-bearing changes:
- [ ] Observed, derived, and inferred claims remain distinguishable.
+93 -3
View File
@@ -1,7 +1,12 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "import", "unicorn", "oxc"],
"plugins": ["typescript", "import", "unicorn", "oxc", "vitest"],
"rules": {
"rea/no-buffer-deep-equality": "error",
"rea/cancellation-cause": "error",
"no-async-promise-executor": "error",
"no-unreachable": "error",
"no-unsafe-optional-chaining": "error",
"complexity": ["warn", 20],
"max-depth": ["warn", 4],
"max-lines": [
@@ -23,7 +28,37 @@
"max-params": ["warn", 4],
"no-var": "warn",
"no-var-requires": "error",
"typescript/no-import-type-side-effects": "error"
"typescript/no-import-type-side-effects": "error",
"vitest/no-standalone-expect": "off",
"vitest/no-conditional-expect": "off",
"vitest/expect-expect": "off",
"vitest/no-disabled-tests": "off",
"vitest/require-mock-type-parameters": "off",
"vitest/require-to-throw-message": "off",
"import/no-duplicates": [
"warn",
{
"preferInline": false
}
],
"vitest/valid-expect": [
"warn",
{
"maxArgs": 2
}
],
"rea/no-evidence-json-preadmission": "error",
"rea/no-positional-tool-contract": "error",
"rea/no-fixture-root-cleanup": "error",
"typescript/no-floating-promises": "error",
"typescript/no-misused-promises": "warn",
"typescript/return-await": ["error", "error-handling-correctness-only"],
"typescript/switch-exhaustiveness-check": [
"error",
{
"considerDefaultExhaustiveForUnions": true
}
]
},
"overrides": [
{
@@ -36,6 +71,12 @@
"no-var-requires": "off"
}
},
{
"files": ["src/**/*.ts"],
"rules": {
"typescript/no-misused-promises": "error"
}
},
{
"files": ["tests/**/*.ts", "src/**/*.test.ts", "src/**/*.fixture.ts"],
"rules": {
@@ -47,7 +88,8 @@
"skipBlankLines": true,
"skipComments": true
}
]
],
"typescript/no-misused-promises": "warn"
}
},
{
@@ -91,6 +133,45 @@
}
]
}
},
{
"files": ["tests/**/*.test.ts", "src/**/*.test.ts"],
"rules": {
"vitest/valid-expect": [
"warn",
{
"maxArgs": 2
}
],
"vitest/valid-expect-in-promise": "error",
"vitest/no-focused-tests": "error"
}
},
{
"files": [
"scripts/lib/http3-capture-assertions.test.mjs",
"scripts/lib/http3-verifier-lifecycle.test.mjs"
],
"rules": {
"typescript/no-floating-promises": [
"error",
{
"allowForKnownSafeCalls": [
{
"from": "package",
"package": "@types/node",
"name": "test"
}
]
}
]
}
},
{
"files": ["scripts/fixtures/javascript-recovery/code-split/entry.mjs"],
"rules": {
"typescript/no-floating-promises": "off"
}
}
],
"env": {
@@ -109,5 +190,14 @@
".devin",
".factory",
"third_party"
],
"options": {
"reportUnusedDisableDirectives": "error"
},
"jsPlugins": [
{
"name": "rea",
"specifier": "./scripts/lint/index.mjs"
}
]
}
+6 -5
View File
@@ -80,7 +80,7 @@ Coordinate expensive checks across agents. If a test unexpectedly consumes heavy
For tool design, follow [docs/tool-design.md](docs/tool-design.md): start from the analyst question, reuse existing contracts, preserve caller choices, and return useful evidence inline. Limits must follow real format, protocol, authority, or resource constraints. Account for representation expansion before allocation; derive completeness from examined coverage.
For test changes or provider claims, use [docs/testing.md](docs/testing.md). Prefer real public CLI/MCP workflows, then production-boundary integration, then producer goldens. Keep module tests for distinct semantics stronger workflows cannot reliably reproduce. Preserve malformed-input, cancellation, cleanup, permission, capacity, and target-identity coverage when pruning. Report unverified coverage explicitly; simulated providers do not establish real-provider behavior.
For test changes or provider claims, use [docs/testing.md](docs/testing.md). Select applicable [boundary regressions](docs/testing.md#boundary-regressions) for parser, inventory, snapshot, and projection fixes. Prefer real public CLI/MCP workflows, then production-boundary integration, then producer goldens. Keep module tests for distinct semantics stronger workflows cannot reliably reproduce. Preserve malformed-input, cancellation, cleanup, permission, capacity, and target-identity coverage when pruning. Report unverified coverage explicitly; simulated providers do not establish real-provider behavior.
Use Conventional Commit subjects and PR titles. Mark breaking changes with `!` or a `BREAKING CHANGE:` footer. Describe behavior changes, relevant validation, and real-provider verification scope.
@@ -108,10 +108,11 @@ distinguish source-derived conclusions, proposed checks, and unavailable coverag
Address actionable automated review findings and explain disagreements.
Leave the template's **Author readiness** declarations unchecked for the human
owner to complete. Never attest to human review on their behalf. Keep the PR in
draft until the human declares the PR initially ready. Later pushes do not
invalidate that declaration or require another human sign-off. Keep the PR body
and affected validation current as work changes. The readiness check validates
owner to complete. Never attest to human review on their behalf. Draft status
is optional; preserve the PR's current draft status unless the user asks to change
it. Later pushes do not invalidate the readiness declaration or require another
human sign-off. Keep the PR body and affected validation current as work changes.
The readiness check validates
the filled declaration; CI and automated review results do not establish that
human review occurred, maintainer scope approval, or merge permission.
+27 -3
View File
@@ -25,6 +25,18 @@ acquiring targets; optional adapter failures must preserve successful peers. Run
`npm run verify:test-discovery` after adding or moving tests. Resolved import
boundaries are checked by `npm run verify:module-boundaries`, included in lint.
Before retiring an alias or facade, trace runtime registration, dynamic imports,
JavaScript verifiers, subprocess scripts, and packaged entrypoints as well as
TypeScript references. Tests may be the last consumers of a superseded internal
path; migrate their distinct assertions to the current production path before
removing it. Preserve validation, lazy loading, caller policy, and resource
acquisition and cleanup supplied by the old boundary. Published APIs, persisted
formats, and independently versioned bridges require an explicit compatibility
decision; an internal representation alone does not establish that obligation.
See [interpreting boundary facts once](docs/tool-design.md#interpret-boundary-facts-once)
for admission ownership and the distinction between repeated validation and
independent checks.
For analysis workflows and agent interfaces, follow the
[performance priorities](AGENTS.md#performance). Explain the selected output
scope, reuse/cache decisions, and relevant before/after measurements when these
@@ -89,7 +101,11 @@ Human ownership and review remain required when CI or an automated reviewer
reports success. Readiness requests maintainer review; it does not grant scope
approval or permission to merge.
Draft PRs may request early feedback with open questions and incomplete checks.
Draft status is optional. Draft PRs may request early feedback with open questions
and incomplete checks. Agents preserve the current draft status unless asked to
change it; missing readiness declarations or later updates do not require a PR
to become a draft.
Leave the readiness declaration unchecked until the human owner has completed
it. The declaration records initial readiness; later pushes do not invalidate
it or require another human sign-off. Keep the PR description and affected
@@ -368,9 +384,17 @@ Real-provider execution is uncached.
### Temporary files and diagnostics
Tests use `createTestTempDirectory` from `tests/fixtures/temporaryDirectory.ts`
for exact-path, awaited cleanup, including failures and timeouts.
for exact-path, awaited cleanup, including failures and timeouts. It registers
deletion with Vitest's `onTestFinished`; do not also add its result to a roots
array or an `afterEach` removal. `createTestWorkspace` from
`tests/support/workspace/workspaceFixture.ts` leaves cleanup with its caller,
which must await `removeTestWorkspace`. Keep explicit resource teardown before
directory deletion, using the case's `finally` or `afterEach`; Vitest runs
`afterEach` before `onTestFinished` and awaits finish hooks in reverse
registration order.
`npm run verify:test-temp-hygiene` runs the suite under a fresh `TMPDIR` and
rejects remaining REA-owned paths. Never glob-clean shared `/tmp/rea-*` content.
rejects remaining REA-owned paths. It cannot detect duplicate deletion owners or
prove resource-close ordering. Never glob-clean shared `/tmp/rea-*` content.
`REA_LOG_LEVEL` selects `trace`, `debug`, `info`, `warn`, `error`, `fatal`, or
`silent`. Logs go to stderr: MCP defaults to `info`, and one-shot CLI logging
+21 -5
View File
@@ -683,7 +683,12 @@ A view that names a field in `sorted_arrays` lists that string array sorted, as
a read-back showed the application does, while the state keeps it in the order
it was written, so a write's own answer echoes it. If a later counted read
contradicts that ordering, the view stays recorded and its earlier reads no
longer confirm served writes. Members a view names in
longer confirm served writes. A view that names a field in `nested_fields`
always showed that field's objects with the same keys, in any order, and fewer
than an entity stores, such
as a list showing a parent without the parent's own nested type; a stored
object it never recorded, such as a created entity's parent, is rendered with
those keys. Members a view names in
`per_response_members`, such as a request id each response mints, keep the
value its recording carried. A list's `counters` and `query.totals` are
top-level members or members one level down in a paging object
@@ -721,7 +726,7 @@ every MCP session and `data/get` then see:
| Effect | Call |
| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `create` | Adds an entity to the collection the path names, built from the observed entity with each ruled field taken from the call or generated, at the plan's `insert` position. A plan tied to its exemplar's collection writes only there. |
| `update` | Sets the sent members on the entity the path names, constants for members accepted only with their recorded value, and the clock, nested clock, increment, slug, stamp, derived, unique and renewed changes the plan models. |
| `update` | Sets the sent members on the entity the path names, constants for members accepted only with their recorded value, and the clock, nested clock, increment, slug, stamp, derived, composed, reference, unique and renewed changes the plan models. |
| `update` (action) | A `POST`, `PUT` or `PATCH` to an item path plus an action segment (`/{type}/{id}/archive`) sets, on the entity before the segment, each scalar the recorded calls showed the action set (`set`), with any sent members; it is served only when a recorded call found those fields already set, so a toggle never is. |
| `delete` | Removes the entity the path names. |
| bulk `update` or `delete` | A `DELETE`, `PUT` or `PATCH` at a collection, or a `POST` to it plus `delete`, `remove` or `destroy`, applies its effect to every entity the body names, as recorded: an array of ids or of objects carrying the id field, at the root or as the body's one array member (`{"IDs": […], "Read": true}`). |
@@ -923,6 +928,9 @@ Known limits of served writes:
updates require `updated_at` to match the entity (a `held-clock`).
- A slug derived from a title can repeat across served creates with the same
title, where the application would make it unique.
- A `composed` string copies sent text as sent: an application that escapes or
renders it (HTML entities, Markdown) differs for text that needs it, as
BookStack's `description_html` would for a description holding `&`.
- A served create's sequence id is the state's next value, not the gaps an
application's own sequence would leave.
- Nested clocks are served in the spelling of the write's own response, which
@@ -1057,9 +1065,11 @@ Numeric arrays retain that containing field's clock context through nested
arrays; an object's own fields establish their own context. Numeric values at
the body root, in root arrays, or under measurement fields remain literal.
Generated strings normalize only where this confirmed, served write's entity
response rules identify generation. A `unique` rule or UUID identity rule must
still compare two UUIDs; a `renewed` rule applies only at its recorded pointers,
Generated values normalize only where this confirmed, served write's entity
response rules identify generation. A UUID identity rule must still compare two
UUIDs, and a `unique` rule two UUIDs or two generated integers (10 to 20
digits) of one JSON kind and length; a `renewed` rule applies only at its
recorded pointers,
with `*` matching actual array items. Token/sequence strings retain the existing
format check: equal length of at least eight, the same character classes, and
a digit. Numeric sequences continue to compare literally unless an ID mapping
@@ -1071,6 +1081,12 @@ Admitted generated values must also preserve aliases and distinct identities
within that response. UUID identity uses the generator's case-insensitive
matching; token and sequence strings retain their literal identity. This
correspondence ends with the answer and is not inferred across calls.
A `composed` response field must spell the value its parts build from that
response's own fields. Its parts then compare by their rendered text or their
source fields' clock and ID rules. A coherent archive URL can therefore differ
only in normalized clock digits without reporting a value difference; an
incoherent URL still differs even when both sides repeat the same stale text.
Normalized compositions count under `generated`; source fields are counted once.
The report counts each kind of normalised
value; the generation rules remain model-derived facts, rather than new
observations of the application.
+4 -1
View File
@@ -40,7 +40,10 @@ When `faultingThread` is missing or does not resolve to a recorded thread
object, row-zero roles are `unknown`; the recorded lookup remains available.
A translated process records Rosetta's registers in a nested
`threadState.rosetta` object; each one is reported as `rosetta.<name>` and
counts toward the register budget.
counts toward the register budget. A [public ReportCrash `.ips` log](https://github.com/Mortennn/Dozer/issues/189)
shows `rosetta.tmp2`, `tmp1`, and `tmp0` beside ordinary x86 registers. The
test fixture quotes that register excerpt inside an authored report body; it
does not generate a new Rosetta crash during the test.
Observed JSON strings retain `""`; `null` means the field was missing or JSON
`null`. This includes image paths, frame symbols, process text, and the older
string `osVersion` form. Unlisted JSON keys and generic register names also
+2 -2
View File
@@ -23,7 +23,7 @@ graph TD
SESSION["SessionProviderRouter + BinarySession<br/>one immutable deep binding per target<br/>disjoint auxiliary routing"]
REGISTRY["AnalysisProviderRegistry<br/>sorted deep candidates<br/>deterministic selection, no fallback"]
RECORDS["src/application/investigation/<br/>composed InvestigationRecords + narrow ports<br/>EvidenceLedger and Unknown ownership"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots<br/>compatibility facade and post-commit notifications"]
SNAPSHOT["BinarySessionRecords + AnalysisSnapshotCache<br/>binary target/profile snapshots and session file ownership<br/>record coordination and post-commit notifications"]
JS_RECON["src/application/javascript/ + src/artifacts/javascript/<br/>JavaScript artifact reconstruction<br/>directory/ASAR reads<br/>AST-only bundle projection"]
end
@@ -75,7 +75,7 @@ graph TD
end
subgraph "Domain Layer"
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>evidence, comparisons, application graphs"]
DOMAIN["src/domain/<br/>Pure side-effect-free modules<br/>format declarations, Evidence, comparisons, application graphs"]
end
subgraph "Configuration"
+27
View File
@@ -598,6 +598,22 @@ Network and console coverage starts only after REA attaches and enables the rele
Source maps are not fetched by inspection. Bundle analysis fetches them only when `fetch_source_maps` is selected, with exact-origin redirect checks and no credentials, cookies, or referrer. Each fetch operation uses `source_map_fetch_timeout_ms` (default 30000) and a 64 MiB aggregate streamed-response budget across redirects. The byte budget has no runtime override. A fetch overrun is an item limitation, not a thrown analysis timeout. A deadline or byte-limit failure remains explicitly unavailable and makes bundle completeness partial. Source-map decoding also bounds raw resolved identities to 64 MiB and aggregate inline evidence to 32 MiB. Failed maps preserve their failure context when it fits; if another context cannot fit, collection stops with the full requested count, the returned processed count, and an explicit unknown-results limitation. Retained mappings and original sources are returned inline. See [source-location limits](web-source-location.md) for the structural decoder bounds. Original source artifacts with `.ts`, `.tsx`, `.mts`, or `.cts` suffixes carry `text/typescript`; other source names retain `text/javascript`.
A fully read response that fails UTF-8, JSON, mapping, or structural validation
retains `artifact: null`, empty original-source, module-edge, and mapping
arrays, and an explicit failure limitation. Invalid content is `invalid`;
decoder or structural capacity refusal is `fetch_failed`. Its optional
`response_identity` records the complete body's SHA-256 digest, byte count,
and `fetch-response-body-bytes` representation without retaining malformed
text or charging that text to the 32 MiB output budget. These are bytes
delivered by Fetch after HTTP content decoding and before UTF-8 decoding, not
HTTP wire bytes. The identity is absent when the body was not fully read, and
older observations may omit it. A response whose text cannot fit the existing
output budget is likewise `fetch_failed` with text omitted and completed-body
identity; JSON parsing and source-map decoding are not attempted. Successful and partial maps keep their
full text artifacts. Their original text, including an initial BOM or XSSI
line, remains distinct from the parsing view that removes that framing.
Collection reads the body once and does not raise its response or output limits.
When fetching is selected, source-map `excluded` facts preserve the capture
ledger's reasons and counts for declarations rejected before download. No
disallowed URL details are retained or fetched. `requested` and `processed`
@@ -605,6 +621,17 @@ count the admitted fetch requests; exclusions are counted separately. Excluded
declarations make the collection unavailable, or partial when maps were retained.
An empty collection with no declarations remains `not_requested`.
If the caller cancels while later source maps are being fetched, the failure's
partial Evidence retains the captured page and every completed map item. Its
`normalized_result.kind` is `web-bundle-acquisition` and
`analysis_status` is `not_performed`: no script-analysis result is claimed.
`source_maps.requested` remains the admitted request count,
`source_maps.processed` counts completed items, and any unprocessed requests
have an explicit unknown-results limitation. Capture exclusions remain in the
same collection. The cancellation error keeps the caller's exact reason;
an already observed response resource or cleanup failure keeps its own error
category and cleanup owner.
Source-map items retain the script's `declared_url` separately from the observed
`effective_url`. Successful maps use the latter as their source-resolution base.
Failed fetches retain the last response URL observed within the approved origins;
+4 -2
View File
@@ -18,8 +18,10 @@ default, any floor, the maximum, the schema, the parser, and the recovery
text. Register that schema on `environmentSchema` in
[`environment.ts`](https://github.com/morluto/rea/blob/main/src/config/environment.ts).
`parseConfig` calls `parseEnvironment`. Doctor uses that same parser through
`nativeAnalysisConfigurationError`, so startup and `rea doctor` accept the
same values. Add the name to `SELECTED_REGISTRATION_SETTINGS` so `rea setup`
`inspectNativeAnalysisConfiguration`, which also preserves a supplied Hopper
timeout's parsed diagnostic when another setting is invalid. Startup and
`rea doctor` accept the same values. Add the name to
`SELECTED_REGISTRATION_SETTINGS` so `rea setup`
copies it into the client registration. Unset keeps today's behavior. An
invalid value is a `ConfigurationError` and projects as `configuration_invalid`.
Do not fall back to the default.
+29 -4
View File
@@ -241,7 +241,7 @@ record addresses remain distinct candidates, and conflicting file mappings
remain malformed.
Module records establish names through their declared NUL-terminated string pointers. NUL/0xFF padding, spaces, Unicode and suffix-shared storage are supported. A pointer to a longer string does not match just its suffix; a pointer directly into a shared suffix may name it. Exact reversed UTF-8 matching avoids rescanning the file for every name and retains duplicate-name owners until module records resolve them. A contributor investigated a Linux
`GameAssembly.so` from Unity 2023.2.13f1 whose static registration was not located. When no static pair is resolved on x86-64, the reader searches executable ranges for bytes consistent with RIP-relative LEA/MOV references to module-array candidates. Exactly one referenced array candidate must remain. `code_gen_modules_field` is then `null`, and Evidence is **inferred**: instruction boundaries, the absence of a static registration elsewhere, and runtime use were not verified. The module records and slots themselves remain file observations. Static-table associations are **derived**. Otherwise the result is `code-registration-not-found`.
`GameAssembly.so` from Unity 2023.2.13f1 whose static registration was not located. When no static pair is resolved on x86-64 or arm64, the reader searches executable ranges for bytes consistent with a reference to a module-array candidate: RIP-relative LEA/MOV on x86-64, an adjacent `adrp`/`add` pair on arm64. Exactly one referenced array candidate must remain. `code_gen_modules_field` is then `null`, and Evidence is **inferred**: instruction boundaries, the absence of a static registration elsewhere, and runtime use were not verified. The module records and slots themselves remain file observations. Static-table associations are **derived**. Otherwise the result is `code-registration-not-found`. Cpp2IL's StrippedCodeRegSupport plugin also recovers the module list of an inlined CodeRegistration from the module names; this fallback additionally requires the exact located module set and a single code reference. A Unity 2022.3.62f3 macOS Master build checked by a contributor keeps neither registration symbol; its type table is then inferred as described below.
The binary/metadata pair is caller-selected. Name/count/token agreement is structural evidence, not a cryptographic same-build proof. File-offset locations refer to the primary metadata artifact; linked virtual addresses refer to `binary.artifact`. Both SHA-256 identities remain available in compact native pages as `artifact` and `binary_source`.
@@ -299,6 +299,30 @@ The result adds:
`Il2CppMetadataRegistration.typesCount` and its value. That record is found as
the single place whose `fieldOffsetsCount` and `typeDefinitionsSizesCount` both
equal the metadata type-definition count and whose type table is file mapped.
Linking reads only that table. When no record on x86-64 or arm64 has both of
those counts, the reader collects maximal runs of pointers to Il2CppType-shaped
records (an assigned Il2CppTypeEnum kind from 0x01 to 0x1e; class and
value-type data below the type-definition count) that are long enough for every
type index the metadata references. Exactly one such run may exist in the
image, it must start and end inside its mapping (a run touching either end may
continue in a neighbouring mapping), and its start must be loaded by the same
instruction forms as the module-array fallback. `types_field` is then `null`,
`types_count` is that run's length rather than an observed `typesCount`, and
Evidence is **inferred**. The run length is at least every referenced type
index but can differ from `typesCount`: a type-shaped pointer after the table
would lengthen it, and an unreadable slot (such as an unresolved relocation)
would end it early. Only the type indices the metadata references are read. A
second long-enough run anywhere, a run that a type-shaped neighbour extends
before the loaded address, a run touching a mapping end or a slot the loader
binds or leaves unresolved (its stored word is only a placeholder), and a covered
128-byte record whose two counts match but whose other fields are unusable all
leave `metadata-registration-not-found` rather than a guess; the search stops
at the second run. The length requirement only separates the type table from
other pointer runs when the metadata references most of it: on the one Master
build checked, the highest referenced index is 6,031 of 6,576 entries. A
metadata referencing few type indices usually
leaves several short runs and fails closed.
Counts use their declared 32-bit width independently of alignment padding.
The complete 128-byte registration must have unambiguous loader coverage. Its
used fields through byte 112 must be file-backed; a trailing declared zero-fill
@@ -354,7 +378,8 @@ the result has no rows, and `generic_method_pointers_field`,
`generic_method_pointers_count` and `generic_method_table_stride` are `null`.
A table of a few rows can still leave two layouts agreeing, which fails as
ambiguous rather than picking one. Metadata versions 24 and 39 and builds
without a static CodeRegistration (`code_gen_modules_field` `null`) fail as
without a static CodeRegistration (`code_gen_modules_field` `null`) or
MetadataRegistration (`types_field` `null`) fail as
`generic-methods-unsupported`: for version 24 the type-definition generic
container offset and the generic class layout have not been checked against a
real build, so it is refused rather than guessed.
@@ -419,10 +444,10 @@ The builds checked are listed after the module search rules above.
| `code-registration-not-found` | input error | No unique module table or executable-byte array candidate was resolved. |
| `invalid-method-token` | input error | A token is outside its module's method-pointer rows, or a MethodDef row is reused within one image. |
| `invalid-method-pointer` | input error | A slot is unavailable or its nonzero address lacks unambiguous executable file coverage. |
| `metadata-registration-not-found` | input error | No single MetadataRegistration matches the type-definition count. |
| `metadata-registration-not-found` | input error | No single MetadataRegistration or code-loaded type-table candidate was resolved. |
| `invalid-type` | input error | A type index or `Il2CppType` record is out of range, unmapped or malformed. |
| `unsupported-type-kind` | unsupported | A valid type kind has no implemented rendering. |
| `generic-methods-unsupported` | unsupported | Generic instances were requested for metadata version 24 or 39 or a build without a static CodeRegistration. |
| `generic-methods-unsupported` | unsupported | Generic instances were requested for metadata version 24 or 39, or a build lacks either static registration. |
| `generic-method-registration-not-found` | input error | No CodeRegistration layout bounds every genericMethodTable row, or the generic tables are unreadable. |
| `ambiguous-generic-method-registration` | input error | More than one CodeRegistration layout bounds every genericMethodTable row. |
+6
View File
@@ -681,6 +681,12 @@ search permissions (`0700`) on a directory you own before retrying.
On supported Linux distributions, approved setup verifies Hopper's official
`.deb`, `.rpm`, or Arch package before invoking the native package manager.
On apt systems, Hopper's runtime dependencies are installed with
`--no-upgrade` in their own transaction, so setup no longer names an
already-installed dependency as an upgrade target. apt can still upgrade a
package that the Hopper package or the Python library repair requires at a
newer version. Arch's `pacman -S --needed` still updates an outdated
dependency.
The official release feed selects the current public demo without a version
pin. REA verifies the package's detached PGP signature against Hopper's stable
primary fingerprint `7C2D73BAFAF5B9A710A69DA1A26DF815099B1BCD`, then checks
@@ -0,0 +1,12 @@
# Implementation plan
1. **Defaults.** `WriteBody.defaults` and its schema; a create's echo of a member a call leaves out takes its default.
2. **Merging.** Explain a later create on its own and merge it: agreeing rules, references one call shows alone, joined
bodies and defaults, the plan's rule order.
3. **Sorted echoes.** Learn `sorted` for creates and updates, store sorted strings when serving, and agree plain and
sorted echoes in update and create plans.
4. **References.** A references module that learns and resolves them; created and updated fields resolve them against
the replayed or live state; the members they read are accepted.
5. **Nested view keys.** The gate's view spelling learner, moved to its own module, notes object keys; rendering
applies them.
6. **Schema, tests and docs.**
@@ -0,0 +1,48 @@
# Learn creates from every call, sorted arrays and referenced entities
Issue: https://github.com/morluto/rea/issues/4933
## Outcome
After the derived-values change, the creates prompt-built environments still withheld failed for four reasons the
write model did not cover:
- A create plan was learned from the tool's first call alone, so a later call that sent a member the first did not
(Linkding's `shared`), or left one out, was refused even when it agreed with the plan everywhere else.
- Linkding stores `tag_names` sorted, so a call that sent its tags in another order did not echo them, and its create
was withheld or contradicted.
- Homebox's item create answers its parent and type as whole objects (`parent`, `location`, `entityType`) beside the
`parentId` and `entityTypeId` it sent. Kept as constants, they pinned those members to their recorded values.
- Lists show some objects with fewer keys than an entity holds: Homebox lists an item's parent without the parent's
own type, and Keycloak lists a user's `access` with one key. A created entity's object rendered whole, so the list
check failed.
## Behaviour
- **Every call.** A later create is explained on its own and merged into the plan when both calls show the same fields,
but for a reference whose member a call without it did not send (one an earlier create added stays), every field
both rule they rule alike, and a field neither rules holds one value. Members either call sent are accepted, only
those every call sent are required, and an echoed member a call left out takes the value that call stored
(`body.defaults`). A clock widens to `trimmed` only when no held entity or earlier call showed it at a fixed width. A
later create that disagrees leaves the plan as it was and is judged as before; one that left out a member the plan
fills while showing that field otherwise is `unexplained-change`. Merged rules keep the plan's order.
- **Sorted arrays.** An echo is `sorted` when the application stored the sent strings in code-point order rather than
as sent; serving stores them sorted. A plain echo and a sorted one agree, and the plan keeps the sorted one, since a
call whose array was already in order cannot show it; once a call stored strings out of order as sent, they disagree.
- **References.** A created or updated object field that holds fields of the entity a sent member names, as the state
holds it, is a `reference` (member, type, fields). Several members naming such entities resolve to the one named like
the field. The member is accepted with any value. Replay and serving look the entity up in the state they hold;
serving refuses a call naming one the environment does not hold or holds in more than one collection, and the field
is absent, from the entity and the create's answer, when the call does not send the member. An update that sends the
member naming the entity the field already repeats keeps a reference an earlier call showed.
- **Nested view keys.** A view that always shows a field's objects with the same keys, in any order, and fewer than
an entity stores, names them in `nested_fields` in the first recording's order, and a stored object it never
recorded renders with those keys.
Plans and views that need none of this are unchanged.
## Acceptance
- Linkding's prompt-recorded bookmark create whose read-back lists tags sorted is served, and Homebox's item create is
explained without pinning its parent or type.
- No write tool of the 69 recorded captures goes from served to withheld.
@@ -0,0 +1,10 @@
# Tasks
- [x] Merge later create calls into the plan, with defaults for members a call leaves out.
- [x] Learn and serve sorted echoes.
- [x] Learn, replay and serve references to held entities.
- [x] Learn the keys views show of nested objects and render written objects with them.
- [x] Add view, serving and unit tests.
- [x] Document the rules and view member.
- [x] Measure the recorded captures and served write tasks.
- [x] Run static, formatting, documentation and test gates.
@@ -0,0 +1,61 @@
# Verification
Executed on macOS with Node v24.22.1 at `043053d1` (main `79845063`, the derived-values branch `8ce1127b` and this
branch). Recorded captures and served write tasks were measured with a local, unpushed build of this branch merged with
the scenario-timing branch (#6967), as the derived-values branch was; its baseline is the derived-values branch with the
same merge.
## Gates
- `npm run -s typecheck`, `lint`, `format:check`, `docs:check`, `mcp-catalog:check`, `verify:test-discovery`,
`scan:todos` and `npx knip`: passed.
- `npx vitest run --project domain --project services --project adapters`: 17,258 passed, 25 skipped, 1 failed ("stops
Inspector target authorization after the current stat settles"), which fails the same way on main in this environment.
- `npm run test:focused --` the environment build, export and serve CLI boundaries, the analysis-view CLI and MCP
boundaries, the environment serve, served-writes and bundle MCP boundaries, MCP schema validation and output schema
constraints: 10 files, 82 passed.
## Tests
- View: a later create sending a member the first did not, or leaving one out, widens the plan with the member's default;
a later create storing a member otherwise than sent stays unexplained; sent tags stored sorted give a sorted echo; a
create embedding the shelf its `shelf_id` names gives a reference, and the list that shows the shelf by id and name
learns `nested_fields`.
- Serving: a create leaving the member out stores its default and one sending it stores it; a create embeds the named
shelf as the environment holds it, lists it with the listed keys, and is refused for a shelf it does not hold; sent
tags are stored sorted.
- Unit: references (the member named like the field among several, too few keys, a disagreeing value, an unheld entity)
and merges (a constant that differs, a member one fixes and the other accepts, a reference one call shows alone).
## Write plans over recorded captures
The 69 captures the derived-values branch measured: 50 published, 18 Claude Code recorded from a prompt, and one timed
Grafana recording. After merging main `ed79bf93` (which bounds create learning), the census was run again against main
with the scenario-timing branch: 124 served there, 132 with both branches, no write going from served to withheld; the
columns below are the measurements before that merge, whose totals it reproduces.
| Captures | Write plans | Served before both branches | Derived values | This branch |
| ------------------------- | ----------- | --------------------------- | -------------- | ----------- |
| 50 published | 187 | 68 | 68 | 70 |
| 19 recorded from a prompt | 120 | 56 | 61 | 62 |
- No write goes from served to withheld.
- Gains: Keycloak's published user create and update, whose list shows each user's `access` with one key, so a created
user now lists as recorded and its plan's sorted insert checks; and the round-4 Linkding bookmark create, whose tags
the application stores sorted.
- Explained but still withheld: Homebox's item create now merges with the location create, with references for
`parent`, `location` and `entityType` and `parentId` and `entityTypeId` accepted with any value. Its read-back list
answers items only, sorted by name, from a line with no parameter saying so, which the list model does not cover.
- Linkding's create whose later call sends `shared` merges too, and is withheld as `side-effect`: the recording also
added a bookmark through the UI that no recorded write explains.
## Served write tasks
The same captures rebuilt by the baseline and by this branch, with `fireworks_ai/gpt-oss-120b` and
`fireworks_ai/glm-5p3-flash`. The tasks are new; each checks the result from the environment's state, including that
nothing else changed.
| Capture | Tasks | Baseline (gpt-oss, GLM) | This branch (gpt-oss, GLM) |
| --------------------------- | ------------------------------------------ | ----------------------- | -------------------------- |
| Linkding, recorded round 4 | 2 bookmark creates, one with two tags | 0/2, 0/2 | 2/2, 2/2 |
| Keycloak, published capture | create a user, change another user's email | 0/2, 0/2 | 2/2, 2/2 |
@@ -0,0 +1,12 @@
# Implementation plan
1. **Clock formats.** A `trimmed` ISO format, `fixedWidth` on a clock reading, fitting a format to another value, and
calendar digits, in a clock formats module.
2. **Composed strings.** A module that indexes a create's candidate parts once, segments a string, checks the type's
other entities and an update's before value, and spells the string from written fields.
3. **Explaining.** Create explanation composes unexplained strings after templates, learns trimmed clocks from the
type and generated integers; update explanation composes unsent changes after the existing rules.
4. **Later calls.** A later create or update widens the plan's clocks to a trimmed spelling it shows; a later create's
unique value may be a generated integer.
5. **Serving.** Derived fields are rebuilt after other rules; unique integers are seeded by length and kind.
6. **Schema, tests and docs.** The plan schema, view, serving and unit tests, and the write-plan docs.
@@ -0,0 +1,59 @@
# Explain values an application derives from other fields
Issue: https://github.com/morluto/rea/issues/4933
## Outcome
Environments an agent built from a prompt withheld creates in three of five applications, each for a value the write
model could not explain, though the application derives it deterministically:
- Linkding's `web_archive_snapshot_url` is `https://web.archive.org/web/<creation time as YYYYMMDDhhmmss>/<sent url>`.
The plan kept it as a constant, which pinned `url` to its recorded value, so a later create with another URL was
refused.
- Grafana's folder `url` is `/dashboards/f/<uid>/<slug of the title>`, which pinned `uid` the same way, and its numeric
`id` is a large time-based integer the plan copied from the first create, which the second create's read-back
contradicted.
- Homebox, written in Go, spells nanosecond clocks with trailing fraction zeros dropped (`…34.38812218Z`), so a later
create's clock did not follow the format learned from the first.
## Behaviour
- **Composed strings.** A created field no other rule explains may be a `composed` string: literal text and parts read
from the fields other rules write, each a field's value, its slug, or a clock's calendar digits (`YYYYMMDDhhmmss`,
`YYYYMMDD`, `YYYY-MM-DD`, `YYYY/MM/DD`, in the clock's zone). Parts are found left to right, longest first, delimited
by non-alphanumerics; generated values may be short, other values need eight characters, as a copy does. A create
composes a string only when at least one other entity of the type spells it with the same parts and none spells it
otherwise, and never when one text comes from two sources serving could tell apart (two clocks the write stamps are
one source when they spell the same zone), and never when another entity holds the field as something other than the
parts spell, a non-string value included. An update's unsent change may be composed from members it stored as sent and
fields it left as they were, when the same parts also spell the value before the call and no other entity spells it
otherwise. Serving rebuilds the string from the written fields after every other rule and refuses a call when a part's
field holds nothing to spell, and a composed field no longer pins the members it reads. A composition learned on a
later call is checked against every earlier call, and a later create whose response spells a composed field otherwise
is withheld.
- **Trimmed clocks.** A clock format may be `trimmed`: its values differ only in how many fraction digits they show and
none ends a fraction in a zero. It is learned from the type's held values of the field, from a later create, and from
a later update; other clocks the plan stamps take the spelling too, and no clock takes it once a held entity, the
exemplar or any call the plan accepted showed the field at a fixed width; a later call that then shows it shorter is
withheld. `fraction` is the most digits shown, and serving drops trailing zeros and the point with them.
- **Generated integers.** A created integer of 10 to 20 digits that no entity of the type holds, where every held entity
holds a distinct one of the same length and JSON kind and none is a clock, is `unique`. Serving draws a seeded integer
of that length and kind that no held entity has. One that another field of the created entity also holds, as a value,
a number at any depth or a delimited part of a nested string, is not `unique`, and the create is withheld as
`unexplained-change`, since seeding cannot keep the two equal. A create is also withheld when another string holds its
digits and no `template` or `composed` rule rebuilds that string from it, or when a later create answers the field
with anything but an integer of the same length and kind. Compare treats two such integers of one JSON kind and length
under a `unique` rule as generated values.
Plans that need none of this are unchanged.
Later creates must retain the exemplar's unique-value family (UUID, integer with
the same digit count and JSON kind, or opaque token). Their recorded composed
fields must equal the values the plan rebuilds; simulation cannot replace a
contradictory captured value and then count that replacement as confirmation.
## Acceptance
- The prompt-built Linkding, Homebox and Grafana captures serve the creates and renames they withheld, and write
tasks that use them pass.
- No write tool of the 50 published captures or the earlier prompt-built captures goes from served to withheld.
@@ -0,0 +1,10 @@
# Tasks
- [x] Read and fit trimmed clock formats, and spell calendar digits.
- [x] Compose created strings and unsent update changes from other fields.
- [x] Learn generated integers as unique values and serve them by length and kind.
- [x] Widen plan clocks on later creates and updates, and spread a trimmed spelling to the plan's other clocks.
- [x] Add view, serving and unit tests.
- [x] Document the rules.
- [x] Measure the prompt-built captures, the 50 published captures and the served write tasks.
- [x] Run static, formatting, documentation and test gates.
@@ -0,0 +1,91 @@
# Verification
Executed on macOS with Node v24.22.1 at `a1edeafd` (main `79845063` plus this branch). The prompt-built recordings and
evaluations used a local, unpushed build of this branch merged with the scenario-timing branch (#6967), whose request
times the clock rules need from browser scenarios, on Ubuntu 24.04 VMs with snap Chromium 155. The baseline is the same
merge without this branch.
## Gates
Follow-up on 2026-10-25, after merging main `a5bfd0440`, with Node 24.22.1 and npm 11.16.0 on macOS:
- Environment write/compare source tests: 26 files, 839 passed.
- Analysis-view CLI/MCP, environment served-write MCP, environment-compare CLI, managed bundle read guards, and setup
registration prewarm boundaries: 6 distinct files, 77 passed. The managed low-headroom failure is fixed by main's
Evidence ownership change (#7036); the setup tests share host preparation to satisfy lint without removing cases.
- `check:fast`, `format:check`, and `docs:check`: passed.
- New capture-to-plan regressions reproduce missing composition parts on earlier updates (null and structured sources)
and fixed-width clock evidence from either the selected entity or another held entity. Rule tests exercise several
separator/zone groups in both orders, choosing each group's widest trimmed peer while preserving fixed-width fields.
- Compare uses the domain clock matcher on parsed readings. Tests cover shorter fractions, absent fractions, maximum
width, trailing zeros, separators, zones, and response-entity scope; the compiled CLI accepts a Go-style create whose
live clocks have eight digits and served clocks have none, preserving all input Evidence files.
- The original application's recordings and model evaluations below were not rerun in this follow-up.
Original author validation:
- `npm run -s typecheck`, `lint`, `format:check`, `docs:check`, `mcp-catalog:check`, `verify:test-discovery`,
`scan:todos` and `npx knip`: passed.
- `npx vitest run --project domain --project services --project adapters`: 17,246 passed, 25 skipped, 1 failed ("stops
Inspector target authorization after the current stat settles"), which fails the same way on main in this environment.
- `npm run test:prepare`, then `npm run test:focused --` the environment build, export and serve CLI boundaries, the
analysis-view CLI and MCP boundaries, the environment serve, served-writes and bundle MCP boundaries, MCP schema
validation and output schema constraints: 10 files, 82 passed.
## Tests
- View: Go-spelled tag creates and renames are served with a trimmed clock; bookmark creates are served with a composed
snapshot address and accept any URL; folder creates and renames are served with a composed address and a generated
integer id.
- Serving: a served tag create spells its clocks without trailing zeros; a served bookmark create composes its snapshot
address from the served clock and the sent URL; a served folder create gets a fresh sixteen-digit id and an address
that follows its uid and title, and a rename moves the address.
- Unit: fitting clock formats (trimmed, fixed width, separator and zone), calendar digits, seeded integers, generated
integer detection, composition and its refusals (one call alone, a held entity spelling otherwise, two sources with
one text, a member stored otherwise than sent, a value the parts do not spell before the call), and a wide body
composed within the existing time limit.
## Write plans over recorded captures
Every write plan of 69 captures, built by the baseline and by this branch: the 50 published captures, 18 captures
Claude Code recorded from a prompt for the prompt branch's checks, and one new timed Grafana recording.
| Captures | Write plans | Served on the baseline | Served on this branch |
| ------------------------- | ----------- | ---------------------- | --------------------- |
| 50 published | 187 | 68 | 68 |
| 19 recorded from a prompt | 120 | 56 | 61 |
- No write goes from served to withheld. The five gains are Linkding's bookmark create, Homebox's tag create, and
Grafana's folder create in two recordings and folder rename in the timed one.
- Published plans change only where a value is now composed: Linkding's bookmark create and BookStack's book create and
update (`description_html` as `<p>` around the description) now accept the member they read.
- Unchanged by design: Grafana's published folder, whose uid and title slug are the same text, so its address names two
sources; Ghost's post update, whose `lexical` keeps the `derived` rule that explained it before; BookStack's page HTML
and Jellyfin's playlist path, which other pages and playlists of the capture spell otherwise.
- What still withholds a create the composed value now explains: a later Linkding create that sends a member the first
did not (`shared`), Homebox item creates that differ from the first location create, and a Linkding create whose
read-back lists `tag_names` sorted.
## Served write tasks
The same timed captures, rebuilt by the baseline and by this branch, with the models `fireworks_ai/gpt-oss-120b` and
`fireworks_ai/glm-5p3-flash`. Create tasks are new; recorded write tasks are the applications' existing ones with entity
counts taken relative to each bundle's state.
| Application | Tasks | Baseline (gpt-oss, GLM) | This branch (gpt-oss, GLM) |
| ----------- | ---------------------------------- | ----------------------- | -------------------------- |
| Linkding | 2 bookmark creates | 0/2, 0/2 | 2/2, 2/2 |
| Linkding | 3 recorded (update, notes, delete) | 3/3, 3/3 | 3/3, 3/3 |
| Homebox | 2 tag creates | 0/2, 0/2 | 2/2, 2/2 |
| Homebox | 2 recorded deletes | 0/2, 0/2 | 0/2, 0/2 |
| Grafana | folder create and rename | not served | 1/2, 2/2 |
| Grafana | 2 recorded deletes | | 2/2, 2/2 |
- Smoke and retrieval match on both builds (Linkding 9/9 and 8/9, Homebox 3/3 and 3/3); Grafana's new recording passes
its smoke task and 9/9 retrieval with both models.
- gpt-oss missed the Grafana rename by sending the update without the title it was asked to change, ten times; the
served environment refused each call with the members it accepts.
- Homebox's recorded deletes fail on both builds: this recording's served entity list holds its locations only, so the
models never find the item to delete.
- The created bookmarks' snapshot addresses and the created and renamed folders' addresses are checked from the
environment's state.
+117 -10
View File
@@ -140,6 +140,61 @@ static tasks. Both lint caches include the website, documentation configuration,
and experiment code scanned by the checker; a regression compares the actual
lint inventory with Turbo's resolved inputs.
Static checks locate candidates; they do not establish semantic ownership.
`jscpd` finds token clones, and Knip counts test references as uses. Module
boundaries and architecture guards enforce their declared import and source
rules. Passing them does not prove that two parsers agree, a wrapper is still
needed at runtime, or cleanup has one owner.
The lint gate treats `no-async-promise-executor`, `no-unreachable`, and
`no-unsafe-optional-chaining` as errors. These catch lost async executor failures,
dead code, and unsafe uses of optional-chain results. `no-unsafe-finally` remains
a warning because cleanup paths can deliberately throw aggregated failures;
review whether they preserve the primary failure. Two local rules
cover narrower observed failures: `rea/no-buffer-deep-equality` flags Vitest
deep comparisons of Buffers from recognized constructors and file reads,
including simple `const` aliases; `rea/cancellation-cause` requires an explicit
cause when a directly checked caller `AbortSignal` is converted to an
`AnalysisCancelledError`. The rules do not infer arbitrary Buffer types,
follow indirect cancellation helpers, or prove that an explicit cause equals
the caller's reason. Keep focused boundary and lifecycle tests for those facts.
`rea/no-evidence-json-preadmission` rejects direct generic JSON parses in
`createEvidence` payload expressions; it does not analyze intermediate values.
`rea/no-positional-tool-contract` rejects catalog indices used to select
operation names, schemas or types; display-only selection remains allowed.
`rea/no-fixture-root-cleanup` follows `const` fixture-root aliases into teardown
deletion; mutable bindings and mixed root arrays remain outside its scope.
Focused `.only` declarations, broken assertion promise chains, and unused disable
directives are errors. `vitest/valid-expect` warns (`maxArgs: 2`) because valid stored and
collected assertions can be awaited later. `import/no-duplicates` warns with
`preferInline: false`.
Fast lint stays syntax-based. `npm run lint:typed` runs serially with pinned
`oxlint-tsgolint` 7.0.2005 and records native plus typed diagnostics in
`.cache/static-checks/typed-oxlint.json`. Production `src` treats
`no-misused-promises` as an error; tests/fixtures warn. `no-floating-promises` is
an error throughout the lint inventory. `return-await` is an error in
error-handling scopes so asynchronous completion stays inside its catch or
cleanup boundary. Exhaustiveness is an error when a union member has neither a
case nor a default. Intentional defaults count as handling the remaining union;
new members covered by those defaults therefore require behavioral review.
Two HTTP/3 source-test files allow the runner-owned `node:test` registration API;
ordinary unowned promises in those files still fail. The authored code-split
recovery input is exempt from floating-promise policy because it is analysis
input, rather than application orchestration. Preserve framework-owned callbacks
such as an awaited `Promise.finally`; inspect the actual completion owner before
changing a callback solely to satisfy its declared `void` return type.
`npm run knip:production` writes advisory candidates to
`.cache/static-checks/production-reachability.json`; execution/report errors fail.
Ordinary Knip remains. `check`, `check:pr`, and `check:ci:deep` run reachability
then typed lint sequentially after cached work. Typed lint builds the project
type graph and stays separate from fast feedback. Its default Go settings are
`GOMAXPROCS=2` and `GOMEMLIMIT=1024MiB`; caller-selected settings take precedence.
Routine iterations and rebases need focused regressions and relevant checks.
Before handing off a PR, run `npm run check` and generated-document checks when
applicable; CI owns the full suite and coverage. Use the full local gate for
@@ -169,6 +224,14 @@ with the consumer preloaded as well as alone. For simulated protocol traces,
use an acknowledged event barrier before asserting complete observations;
a zero-duration capture does not guarantee queued events have arrived.
Place faults at the semantic boundary under test: the selected read, EOF, hash
completion, or cleanup attempt. Global call ordinals can shift during setup or
refactoring without reaching that phase. For controlled asynchronous gates, use
`awaitAcknowledgement` from `tests/fixtures/lifecycleAcknowledgement.ts` to race
the acknowledgement against triggering-operation settlement and a bounded
deadline. Register release gates before launch, then release and join launched
work in `finally` before restoring mocks, even when the barrier fails.
Focused contract tests should import the named contract family they exercise;
reserve the full tool catalog for catalog-wide assertions. Resolve contracts by
operation name rather than array position.
@@ -200,6 +263,10 @@ work from fixture generation, schema compilation, subprocess startup and cleanup
Choose elapsed, CPU and process-family memory limits before the run; a worker
count or test timeout alone does not enforce those limits.
Keep capacity fixtures intact when diagnosing a test failure. For byte equality
of large buffers, use `Buffer.equals` or `Buffer.compare`; a generic structural
diff can allocate far more than the production operation.
Remove demonstrated repeated work while preserving distinct assertions and
resource ownership. Share immutable fixture preparation where safe; keep fresh
sessions when lifecycle or concurrency is the behavior under test. Compare the
@@ -308,6 +375,13 @@ Trivial helper assertions need no replacement. Remove abandoned production
scaffolding only after checking runtime and verifier consumers, including
compiled imports.
After an owner changes, move fault injection and event spies to the path the
production consumer actually uses, and assert that the intended boundary was
reached. Keep independent expectations from authored input, producer output, or
an external oracle; comparing two paths through the same replacement algorithm
does not preserve the old oracle. A negative probe inside a conditional or loop
must assert that it ran.
Use representative scenarios rather than Cartesian matrices of equivalent
cases. Share setup without hiding inputs or expected evidence. For successful
`Result` values, throw the typed error before asserting the value; a preceding
@@ -335,6 +409,34 @@ that a real loader accepts the artifact. If multiple producer revisions share a
format identifier but differ in behavior, record that ambiguity rather than
using one installed version as an oracle for all of them.
### Boundary regressions
For parser, inventory, snapshot, or projection fixes, select the applicable
cases below. Assert the producing boundary's facts and their meaning in affected
consumers. Add [lifecycle regressions](#lifecycle-regressions) when resource
ownership or finalization changes; a single malformed fixture cannot establish
the whole contract.
| Failure class | Regression that catches it |
| ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Format assumptions | Pair valid padding, offsets, or names containing delimiters with malformed input. Identify the producer revision when behavior varies by version; an unrelated runtime API's range is not an on-disk format limit. |
| Raw identity and normalization | Exercise allowed empty or unusual source names/metadata keys and values that normalize to one lookup or display value. Preserve admitted raw spelling under the existing redactions for transport credentials and explicitly marked sensitive values. Distinguish local paths, file URLs and HTTP paths; assert source identity through consumers. |
| Missing, empty, and unknown values | Distinguish omitted, null, empty, zero, and false where the format admits them. Pair a malformed optional field with usable sibling facts; assert uncertainty without inventing absence or discarding the row. |
| Completed work followed by failure | Complete one input, then fail or cancel a later input. Assert earlier digests, metadata, and parsed facts survive with the later failure reason and accurate processed/unprocessed coverage. For cancellation, assert the exact caller abort reason; where a primary failure was already produced, assert it survives. |
| Independent limits and failures | Trigger a sample/output limit alongside a read or parse failure. Assert both dimensions survive; reaching a limit does not erase sampling failures or prove complete coverage. |
| Representation expansion and capacity | Keep a valid set near the limit and a large malformed input followed by a valid input. Cover JSON escaping, expanded paths or decoded rows as applicable. Assert serialized bounds, retained identities, later valid results, and unknown omissions without charging discarded interpretations as retained output. |
| Snapshots, caches, and comparison | Vary metadata that affects the result while keeping payload bytes equal. Exercise historical records with newly introduced fields omitted, current caller inputs on replay, and reuse across differing selections. Assert correct invalidation or unknown coverage, current request provenance, and unchanged saved Evidence and IDs. |
| Public success and error delivery | Exercise affected public CLI/MCP paths, including partial Evidence on failure. Assert retained facts and named contracts; retrieve partial Evidence when MCP returns a reference. If SDK cancellation suppresses the reply, check server settlement and any retained Evidence. An error-code assertion alone misses lost observations. |
For partial Evidence delivery, verify that same-session readback preserves
identity and provenance without rerunning the provider. Include a no-writer
control that keeps completed Evidence inline and preserves the original failure.
When producer order is unspecified, synchronize fixtures on the observed request
or completion event. A filename, generated key, or fixed delay does not prove
which item completed before cancellation. Preserve real producer ordering and
use a controlled seam for the phase that needs deterministic failure.
Packed bridge validation uses Python's standard-library AST and tar readers
with `-I -S`: caller-local modules, `PYTHONPATH`, and site startup hooks must
not replace those readers or execute during validation. Controlled `ast.py`
@@ -384,15 +486,16 @@ the affected owners and applicable scenarios below. Include temporary snapshots
and one-shot commands. Use public CLI/MCP workflows where practical and a
controlled production seam for interleavings that cannot be reproduced reliably.
| Scenario | Assertion and control |
| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Target admitted; startup still pending | Status preserves the readiness distinction and observed startup activity; readiness requires the provider's documented proof. |
| First handshake is late, absent, invalid, or recovered during close | Initial and recovery paths retain the same authenticated identity facts. Foreign run/token controls cannot authorize cleanup; authenticated idle/native work survives the unauthenticated listener deadline. |
| Launch returns an error with a valid process handle | Cover already-exited and still-live owned processes. Verify observed exit separately from creation; failed termination or unavailable identity remains unresolved. |
| Cancellation arrives after workload exit, during result reads or temporary-root removal | Return typed cancellation with available partial observations and actual cleanup status. The non-aborted control succeeds; existing primary/cleanup failures keep their precedence. |
| Children have mixed health or cleanup outcomes | Missing peer health does not erase an observed failure or activity. Idle plus unknown stays unknown. Retry a failed child without closing a successful peer again, including after a close-option change. |
| Cleanup fails after success or primary failure | Public output retains the completed observation or primary diagnostic, remaining resources, and owner recovery action. Restore the denied resource and verify the owning cleanup retry. |
| Target exits without an acknowledgement, or transport disappears while it stays alive | Positive owned exit proof resolves target lifetime; other resources still need cleanup. Lost transport alone does not prove exit. Verify unrelated/attached targets survive and same-connection recovery works when supported. |
| Scenario | Assertion and control |
| --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Target admitted; startup still pending | Status preserves the readiness distinction and observed startup activity; readiness requires the provider's documented proof. |
| First handshake is late, absent, invalid, or recovered during close | Initial and recovery paths retain the same authenticated identity facts. Foreign run/token controls cannot authorize cleanup; authenticated idle/native work survives the unauthenticated listener deadline. |
| Launch returns an error with a valid process handle | Cover already-exited and still-live owned processes. Verify observed exit separately from creation; failed termination or unavailable identity remains unresolved. |
| Cancellation arrives after workload exit, during result reads or temporary-root removal | Return typed cancellation with available partial observations and actual cleanup status. The non-aborted control succeeds; existing primary/cleanup failures keep their precedence. |
| Children have mixed health or cleanup outcomes | Missing peer health does not erase an observed failure or activity. Idle plus unknown stays unknown. Retry a failed child without closing a successful peer again, including after a close-option change. |
| Cleanup fails after success or primary failure | Public output retains the completed observation or primary diagnostic, remaining resources, and owner recovery action. Restore the denied resource and verify the owning cleanup retry. |
| Cleanup owners recover after an error was returned | Retain the original child and aggregate errors and project them after each owner releases. A successful peer stays released; the aggregate remains incomplete until all owners release. Preserve the first cause even when rejection is `undefined`. |
| Target exits without an acknowledgement, or transport disappears while it stays alive | Positive owned exit proof resolves target lifetime; other resources still need cleanup. Lost transport alone does not prove exit. Verify unrelated/attached targets survive and same-connection recovery works when supported. |
Shared process-supervision controls separate leader exit from output closure.
Cover output during held or failed cleanup, bounded retention and drain timeout,
@@ -559,7 +662,11 @@ creation outside the workspace seam and its narrowly documented boundary/package
exceptions.
The shared temporary-directory and workspace helpers preserve caller labels
under an owned `rea-` prefix when needed. `npm run verify:test-temp-hygiene`
under an owned `rea-` prefix when needed. Their
[cleanup ownership differs](https://github.com/morluto/rea/blob/main/CONTRIBUTING.md#temporary-files-and-diagnostics):
`createTestTempDirectory` registers `onTestFinished` deletion, while
`createTestWorkspace` requires caller cleanup. Close resources before removing
their directory. `npm run verify:test-temp-hygiene`
uses the same ownership rule to find retained fixtures under a private temporary
root. Third-party caches remain outside that ownership rule.
+65 -7
View File
@@ -85,6 +85,21 @@ satisfy. A plan without execution does not establish runtime behavior.
### Interpret boundary facts once
The format or protocol reader owns interpretation. Pass its parsed facts and
explicit uncertainty to consumers; do not make adapters reparse display text,
reconstruct omitted values, or infer success from missing diagnostics.
Client registration inspections, for example, carry command identity and
activation independently of readiness. An owned registration may be disabled
or stale; unresolved ownership remains unknown. Consumers use those facts
instead of reclassifying the raw command.
A completed read can establish byte identity even when decoding or format
validation fails. State which representation a digest identifies, such as file
bytes, Fetch response-body bytes, or UTF-8 text. Preserve admitted source values
separately from normalized lookup and display values. When the retention
contract keeps only a digest and size, state that the bytes or text are
unavailable; the digest does not establish format validity.
Comparison needs separate proofs for lookup, correspondence, content, and
coverage. A hash cannot supply the proof that two retained values belong to
corresponding objects.
@@ -103,10 +118,6 @@ Complete traversal or inventory does not make unavailable values equal. A
partial aggregate fingerprint difference cannot establish modification when
omissions alone explain it; independently proven differences still survive.
The format or protocol reader owns interpretation. Pass its parsed facts and
explicit uncertainty to consumers; do not make adapters reparse display text,
reconstruct omitted values, or infer success from missing diagnostics.
Keep format recognition separate from provider admission. A recognizable header
can supply useful observations while later structure remains unresolved or a
provider cannot accept the artifact. Report the reason at the affected boundary
@@ -133,6 +144,12 @@ establish each transition. Include temporary files and one-shot commands, as
well as persistent engine sessions. Use the existing provider contracts;
providers need not expose identical state machines.
In multi-stage work, retain each completed, admitted observation before awaiting
the next stage. Later reads, validation, cancellation, or finalization can fail
without erasing those facts. Carry them through typed failure outcomes with
explicit coverage of unperformed work; do not construct a successful result for
stages that never ran.
- Keep target admission, provider readiness, active work, observed process exit,
and resource release separate. A valid process handle, successful launcher,
or selected target establishes only its own fact. Publish available identity
@@ -207,6 +224,12 @@ providers need not expose identical state machines.
applies rather than duplicate close/finally handling.
- Give descriptor-backed reads an owner before opening the file. A rejected
close must retain the exact handle and preserve any earlier failure.
Carry [`openRegularFile`](https://github.com/morluto/rea/blob/main/src/filesystem/RegularFile.ts)'s
admitted handle and initial metadata rather than restatting only to rebuild
those facts. Keep fresh descriptor and selected-path identity checks after
work, comparing against the original baseline instead of replacing it with
a later stat. Preserve explicit bigint reads when ordinary admission metadata
cannot supply the required size or timestamp precision.
Stable file reads expose completed values separately from cancellation so
consumers retain acquired identity facts after successful close or cleanup retry.
If an abortable caller stops waiting, keep the underlying work in that owner's
@@ -237,9 +260,38 @@ through the producing owner and affected CLI/MCP consumers.
## Implement and evaluate
Put provider protocols/parsing in adapters and shared CLI/MCP workflows in the
application layer. Share Evidence provenance, unknown projection, and session
retention semantics; matching payloads alone do not establish parity.
Put provider wire-protocol parsing in adapters and shared CLI/MCP workflows in
the application layer. Reusable format declarations may live in pure domain
owners: [`peOptionalHeader.ts`](https://github.com/morluto/rea/blob/main/src/domain/peOptionalHeader.ts)
decodes PE structure, while consumers retain machine admission, directory-address
interpretation, and file/mapping bounds. Share Evidence provenance, unknown
projection, and session retention semantics; matching payloads alone do not
establish parity.
Let `createEvidence` own JSON validation and snapshotting of ordinary observation
payloads. `EvidenceObservationInput` accepts semantically parsed parameters,
results, and raw results whose JSON validity still needs admission. Keep format,
selection, and identity validation at their producing boundary. Invalid JSON
issues are rooted at `parameters`, `normalized_result`, or `raw_result` in the
Evidence envelope.
`EvidenceObservation` retains the stronger JSON contract used by producer
helpers and `createImmutableEvidenceSteps`. The cooperative constructor also
requires an authenticated immutable result; preserve that ownership proof.
For objects, proof comes from completed sealing by
[`immutableJson.ts`](https://github.com/morluto/rea/blob/main/src/domain/immutableJson.ts);
`readonly` or `Object.freeze` alone does not establish it. JSON primitives are
directly eligible. Seal owned data and complete cooperative sealing before
reusing its result.
When a failed or cancelled operation retains completed, valid partial Evidence,
use the shared [`ToolResultDelivery`](https://github.com/morluto/rea/blob/main/src/server/ToolResultDelivery.ts)
path with the session's Evidence writer. Keep the original failure and the
named contract's target-content marking; replace the inline partial with a
retained reference only after recording succeeds. An absent writer or a writer
returning a failure `Result` leaves the partial inline, as does a partial that
is not valid Evidence. Unexpected recorder exceptions propagate. An `evidence_id`
member alone does not establish Evidence validity or retention.
Measure the resource before choosing a budget and enforce it before the costly
step. Account for value alternatives, trace-frame products, retained records,
@@ -248,6 +300,12 @@ completeness from examined records and exhaustiveness, rather than diagnostic
prose or an empty failure list. Preserve observations independently of cleanup
success.
Price mutually exclusive success and failure representations separately.
Replace a reserved failure envelope when pricing success, and release discarded
decoding state from retained-output accounting. Retaining additional failed-body
text changes the capacity available to later results; treat that as a contract
change and cover its effect on later valid items.
For example, if ten applicable records are discovered, eight are examined, and
five are returned, the five returned records do not establish complete coverage
of the ten. Keep discovered, applicable, examined, and retained scope distinct.
+13
View File
@@ -71,6 +71,19 @@ selected byte SHA-256 and size, section payload ranges, full header/detail outpu
and its UTF-8 SHA-256. Producer format drift and invalid or truncated modules
are failures, with validation diagnostics and partial Evidence retaining
selected byte identity and tool provenance.
If inspection stops after selected files were read, partial Evidence also keeps
the completed candidate file identities and glue parse observations. A stable
read remains observed when descriptor finalization later fails; cleanup still
requires retry. Partial Evidence for a glue read whose finalization fails
before decoding retains its digest with `not-performed` parse status. Invalid
UTF-8 retains the digest with failed parse status. Neither case invents
references, and a failed file read contributes no invented facts.
These independent observations do not establish that the selected WASM module is
valid: partial results state whether WABT validation is unknown, rejected the
module, or succeeded before a later inspection failure.
CLI errors include this partial Evidence inline. MCP errors can return its
`retained-evidence` reference in `details.partial_observation`; use
`get_evidence_bundle` with `detail: "complete"` to retrieve the matching record.
Core validation remains authoritative when an optional custom section cannot
be interpreted. WABT warning output is preserved in `custom_section_warnings`;
+42 -7
View File
@@ -737,12 +737,45 @@ request body must be a usable JSON object (as response bodies are usable), and
the written entity must show in that call's response or in a later read before
the next write. Each field of a created entity gets the first rule that
explains it: generated `identity` (a sequence, UUID or token, by the recorded
value's shape), `echo` of a body member, `copy` of a body string of 8 or more
characters, a `path` value, a `slug` of a body member, a `clock` value within 10
value's shape), `echo` of a body member (`sorted` when the application stored
its strings in code-point order rather than as sent), `copy` of a body string of
8 or more characters, a `path` value, a `slug` of a body member, a `clock` value within 10
minutes of the request in its recorded format, a `sequence` one past the
largest value of its collection or type, a `unique` UUID or opaque token, a
`nested-clock` object whose clocks the request stamped, or a `template` around
a generated value; other fields keep the observed value. A body that wraps one
largest value of its collection or type, a `unique` UUID, opaque token or
integer of 10 or more digits that no entity of the type holds (where every one
holds a distinct integer of the same length, as time-based ids do), a
`nested-clock` object whose clocks the request stamped, a `template` around
a generated value, a `reference` to an entity, or a `composed` string; other
fields keep the observed value. A `reference` is an object holding fields of the
entity a sent member names, as the state holds it (a parent beside `parentId`):
the member is accepted with any value, serving looks the entity up and refuses a
call naming one the environment does not hold or holds in more than one
collection, and the field is absent, from the entity and the create's answer,
when the call does not send the member. An id naming entities of one type in
several collections teaches no reference. Each later create widens its plan
when, explained on its own, it agrees with it: both calls show the same fields
but for a reference whose member a call without it did not send (one an earlier
create added stays), every field both rule they rule alike (an echo one showed
sorted stays sorted, unless a call stored its strings out of order as sent),
and a field neither rules holds one value. Members either call sent are accepted, only those every call sent are
required, and an echoed member a call left out takes the value that call stored
(`body.defaults`). A clock rule widens to `trimmed` only when no held entity or
earlier call showed that clock at a fixed width. A later create that disagrees
leaves the plan as it was. A create whose observed reference differs from the
entity the plan rebuilds, whether or not it sent the reference member, is
`unexplained-change`. So is a create that left out an echoed member yet shows a
value different from the learned default.
A `composed` string is literal text and parts read from the fields other rules
write: a field's value, its slug, or a clock's calendar digits
(`YYYYMMDDhhmmss`, `YYYYMMDD`, `YYYY-MM-DD` or `YYYY/MM/DD`, in the clock's
zone). A create composes a string only when at least one other entity of the
type spells it with the same parts and none spells it otherwise, and never when
one text could come from two sources serving could tell apart. A clock format
is `trimmed` when the field's values differ only in how many fraction digits
they show and none ends a fraction in a zero, as Go spells nanoseconds: its
`fraction` is the most it shows, and serving drops trailing zeros and the point
with them. Other clocks the plan stamps take that spelling too unless their own
value shows a fixed width. A body that wraps one
object in a one-element array is that object, wrapped so on every call
(`body.envelope`). An update's members
echo when stored as sent; a member stored differently is accepted only with its
@@ -750,8 +783,10 @@ recorded value (`body.fixed`) and stores the value the call that showed it
recorded (the `value` of its `constant` rule), and a field it changed without
sending must be a clock, a nested clock, an increment, a slug, a fresh `unique`
value, a `stamp` of the acting user, a value `derived` from the one member it
stores nowhere, or for an item action a `set` of a scalar to the value it
showed, or the plan is `unexplained-change`; an increment, slug, stamp, derived
stores nowhere, a `reference` to the entity a sent member names (kept by a call that names the entity the field already repeats), a `composed` string whose parts read members it stored as sent
or fields it left as they were and spell the value before the call too, or for
an item action a `set` of a scalar to the value it showed, or the plan is
`unexplained-change`; an increment, slug, stamp, derived
value or set must hold for every call of the tool, and an action is served
only once a call found what it sets already set. A write is served only
with the query its first call sent, which serving applies to a call that
+15
View File
@@ -111,6 +111,10 @@ are made.
## Verification
Browser bundle collection uses absolute section offsets for nested inline maps.
If caller cancellation stops browser map fetching after some items complete,
the failed bundle request retains those items with the captured page in partial
Evidence and marks bundle analysis `not_performed`. This is distinct from the
local source-map decoder's failure semantics above.
An enclosing column offset contributes only when the nested section starts on
that enclosing section's first generated line. Parent boundaries still clip
preceding browser mappings when a nested section starts with a gap or has no
@@ -136,6 +140,17 @@ requests are left unprocessed and reported as unknown through the top-level
items.
Malformed UTF-8 map responses are reported as invalid rather than repaired with
replacement characters.
For browser fetching, an `invalid` map or structural or decoder capacity
failure has `artifact: null`, empty decoded arrays, and an explicit limitation.
When its bounded response body was read in full, optional `response_identity`
records that body's SHA-256 digest and byte count after HTTP content decoding
and before UTF-8 decoding. It is not a digest of partial reads or a claim that
the map was valid; historical items may omit it. Malformed text is omitted so
it does not consume the 32 MiB inline budget needed by later valid maps. A
response whose text cannot fit that budget is refused before JSON parsing and
source-map decoding, and retains the same
completed-body identity. Successful and partial maps keep their full original
text, including BOM and XSSI framing; only their parsing view removes it.
Focused tests cover duplicate mappings, indexed offsets/overlap/local indexes,
32-bit VLQ overflow, nullable content, UTF-16/CRLF positions, immutable artifact
+35
View File
@@ -0,0 +1,35 @@
{
"$schema": "./node_modules/knip/schema.json",
"entry": [
"src/main.ts!",
"src/cli.ts!",
"src/cliOutput.ts!",
"src/cliBuiltinArguments.ts!",
"src/cli/cliResultOutput.ts!",
"src/mcpDoctor.ts!",
"src/composition/javascriptAnalysisWorkerProcess.ts!",
"src/composition/webBundleAnalysisWorkerProcess.ts!",
"src/sqlite/SqliteDatabaseWorker.ts!",
"src/evm/EvmoleInterfaceWorker.ts!",
"src/browser/history/NodeCaptureProcess.ts!",
"src/javascript/sourceMaps/SourceMapCodecProcess.ts!",
"scripts/rea.mjs!",
"scripts/rea-android-workspace-guardian.mjs!",
"scripts/electron-active-hook.cjs!",
"scripts/prepack.mjs!",
"scripts/prepare.mjs!",
"scripts/verify-windows-native-artifact.mjs!"
],
"project": ["src/**/*.ts!", "!src/**/*.test.ts", "!src/**/*.fixture.ts"],
"ignoreExportsUsedInFile": true,
"include": [
"files",
"exports",
"nsExports",
"types",
"nsTypes",
"enumMembers",
"namespaceMembers",
"duplicates"
]
}
+36 -1
View File
@@ -2,7 +2,7 @@ REA npm dependency inventory
Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.
License identifiers are upstream lockfile declarations, not a legal assessment.
Runtime includes transitive and optional platform packages; development includes build, test and website tooling.
Locked package entries: 956; runtime: 180.
Locked package entries: 963; runtime: 180.
Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.
@algolia/abtesting@1.25.0 | development | MIT
@@ -1100,6 +1100,36 @@ commander@13.1.0 | runtime | MIT
Package: https://www.npmjs.com/package/@oxfmt/binding-win32-x64-msvc/v/0.72.0
Archive: https://registry.npmjs.org/@oxfmt/binding-win32-x64-msvc/-/binding-win32-x64-msvc-0.72.0.tgz
Integrity: sha512-uhooIN+DCzWX5uH7mQkSVCsODqsGluGJ4n3ch4+mITtWKpESMDjUJUSDBAVFuYQDwqY2McPDKdKJbM0oZVW/Qw==
@oxlint-tsgolint/darwin-arm64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/darwin-arm64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/darwin-arm64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2005.tgz
Integrity: sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==
@oxlint-tsgolint/darwin-x64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/darwin-x64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/darwin-x64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2005.tgz
Integrity: sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA==
@oxlint-tsgolint/linux-arm64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/linux-arm64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/linux-arm64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2005.tgz
Integrity: sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q==
@oxlint-tsgolint/linux-x64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/linux-x64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/linux-x64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2005.tgz
Integrity: sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA==
@oxlint-tsgolint/win32-arm64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/win32-arm64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/win32-arm64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2005.tgz
Integrity: sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ==
@oxlint-tsgolint/win32-x64@7.0.2005 | development, optional | MIT
Lockfile path: node_modules/@oxlint-tsgolint/win32-x64
Package: https://www.npmjs.com/package/@oxlint-tsgolint/win32-x64/v/7.0.2005
Archive: https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2005.tgz
Integrity: sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw==
@oxlint/binding-android-arm-eabi@1.87.0 | development, optional | MIT
Lockfile path: node_modules/@oxlint/binding-android-arm-eabi
Package: https://www.npmjs.com/package/@oxlint/binding-android-arm-eabi/v/1.87.0
@@ -3540,6 +3570,11 @@ oxlint@1.87.0 | development | MIT
Package: https://www.npmjs.com/package/oxlint/v/1.87.0
Archive: https://registry.npmjs.org/oxlint/-/oxlint-1.87.0.tgz
Integrity: sha512-drxVlaW7Oqq7wSy1CN25znRu00aRq8n57Q2wniJRxCmtYVz07ufCm7EVDPQFZURrTgQIjzKrVEHAD/HcvJXObg==
oxlint-tsgolint@7.0.2005 | development | MIT
Lockfile path: node_modules/oxlint-tsgolint
Package: https://www.npmjs.com/package/oxlint-tsgolint/v/7.0.2005
Archive: https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2005.tgz
Integrity: sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw==
p-limit@2.3.0 | development | MIT
Lockfile path: node_modules/p-limit
Package: https://www.npmjs.com/package/p-limit/v/2.3.0
+103
View File
@@ -73,6 +73,7 @@
"lint-staged": "16.4.0",
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
"oxlint-tsgolint": "7.0.2005",
"release-please": "17.3.0",
"rolldown": "1.2.12",
"tsrs": "0.18.0-ts7.1.0-dev.20260929",
@@ -3491,6 +3492,90 @@
"node": "^20.19.0 || >=22.12.0"
}
},
"node_modules/@oxlint-tsgolint/darwin-arm64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-arm64/-/darwin-arm64-7.0.2005.tgz",
"integrity": "sha512-VqDZxcZVjlonNLuuvraVNgDQt+1ndwa4yJo8OqsK4HYh/61ba9IlMStCVfi/aaIqRoDhNbAKC8gdcTrXqnb+Ug==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@oxlint-tsgolint/darwin-x64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/darwin-x64/-/darwin-x64-7.0.2005.tgz",
"integrity": "sha512-4kUfLRNYoVpv0Tm3jPYrvxyfOc/LmdvFC3pKZ4tL6K7lA7K/VS81SAtmxRzbYXYSyb3SBWhwsuX+df4neqzxxA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@oxlint-tsgolint/linux-arm64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-arm64/-/linux-arm64-7.0.2005.tgz",
"integrity": "sha512-rhUNcPsBlKvhXtrbhbBUidtRBRdJ8fbEHN9H5Q5N59gby18ruGuDA7KUxl7oRnUzp797uykYQhkBDm+0KpfK/Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxlint-tsgolint/linux-x64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/linux-x64/-/linux-x64-7.0.2005.tgz",
"integrity": "sha512-tKs9yinDQZOHygM0e8EJnjTU3mzJtdfkOZP6Udu82iLMePUSHOhTJ9zzU/mSd1hQxURI4LTRRiFhfzC+PzwdNA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@oxlint-tsgolint/win32-arm64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-arm64/-/win32-arm64-7.0.2005.tgz",
"integrity": "sha512-VYPnu1gjfbSv+ROhuyNfMs382D4zQU2ZRc8Uc3nuEZo1k2kf+1KfTrg4hrlWh73h9GI+dUWyo+DYCQzUBlPwcQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@oxlint-tsgolint/win32-x64": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/@oxlint-tsgolint/win32-x64/-/win32-x64-7.0.2005.tgz",
"integrity": "sha512-ekU89cTGy9fpQzuvxIsFHrnoP8XoFjGIRgPEW6eqgY2NYjGy9pwpjF79o04WXRMJFKRrcaVqkZXUUBdLsqpQDw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@oxlint/binding-android-arm-eabi": {
"version": "1.87.0",
"resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.87.0.tgz",
@@ -10137,6 +10222,24 @@
}
}
},
"node_modules/oxlint-tsgolint": {
"version": "7.0.2005",
"resolved": "https://registry.npmjs.org/oxlint-tsgolint/-/oxlint-tsgolint-7.0.2005.tgz",
"integrity": "sha512-6ccPGu4u4J9bwYPHXBFPee3RZrX3BboRdU7k14zRDDB1lvN3O7J/vPRFdaEfa1+CWx4xnQQOp4ctoaF66oO0uw==",
"dev": true,
"license": "MIT",
"bin": {
"tsgolint": "bin/tsgolint.js"
},
"optionalDependencies": {
"@oxlint-tsgolint/darwin-arm64": "7.0.2005",
"@oxlint-tsgolint/darwin-x64": "7.0.2005",
"@oxlint-tsgolint/linux-arm64": "7.0.2005",
"@oxlint-tsgolint/linux-x64": "7.0.2005",
"@oxlint-tsgolint/win32-arm64": "7.0.2005",
"@oxlint-tsgolint/win32-x64": "7.0.2005"
}
},
"node_modules/p-limit": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
+7 -4
View File
@@ -112,7 +112,7 @@
"prebuild": "npm run deps:check",
"build": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run artifacts:skills --force",
"precheck": "npm run deps:check",
"check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards",
"check": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check && npm run verify:architecture-guards && npm run knip:production && npm run lint:typed",
"precheck:fast": "npm run deps:check",
"check:fast": "node scripts/run-turbo.mjs run typecheck lint --output-logs=errors-only",
"precheck:changed": "npm run deps:check",
@@ -120,14 +120,14 @@
"precheck:test": "npm run deps:check",
"check:test": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run",
"precheck:pr": "npm run deps:check",
"check:pr": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos",
"check:pr": "node scripts/run-exclusive.mjs artifacts node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check test:run && npm run test:python && npm run docs:check && npm run jscpd && npm run scan:todos && npm run knip:production && npm run lint:typed",
"precheck:ci": "npm run deps:check",
"precheck:ci:fast": "npm run deps:check",
"precheck:ci:deep": "npm run deps:check",
"check:ci": "npm run check:ci:fast && npm run check:ci:deep",
"check:ci:fast": "node scripts/run-exclusive.mjs artifacts npm run check:ci:fast:tasks",
"check:ci:fast:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check",
"check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python",
"check:ci:deep": "node scripts/run-exclusive.mjs artifacts npm run check:ci:deep:tasks && npm run verify:architecture-guards && npm run verify:test-discovery && npm run test:python && npm run knip:production && npm run lint:typed",
"check:ci:deep:tasks": "node scripts/run-turbo.mjs run knip metadata:check jscpd scan:todos",
"check:ci:tasks": "node scripts/run-turbo.mjs run typecheck lint:built format:check knip metadata:check jscpd scan:todos",
"ci:static-context": "node scripts/ci/static-task-context.mjs",
@@ -326,7 +326,9 @@
"docs:prepare": "node scripts/generate-package-metadata.mjs && node scripts/run-turbo.mjs run artifacts:product-catalog artifacts:managed-evidence artifacts:error-schema",
"verify:web:qlog": "npm run build:cached && node scripts/verify-qlog-captures.mjs",
"licenses:generate": "node scripts/generate-third-party-inventory.mjs",
"licenses:check": "node scripts/generate-third-party-inventory.mjs --check"
"licenses:check": "node scripts/generate-third-party-inventory.mjs --check",
"lint:typed": "node scripts/ci/static-check-report.mjs typed-lint",
"knip:production": "node scripts/ci/production-reachability-report.mjs"
},
"lint-staged": {
"*.{cjs,cts,js,jsx,mjs,mts,ts,tsx}": [
@@ -404,6 +406,7 @@
"lint-staged": "16.4.0",
"oxfmt": "0.72.0",
"oxlint": "1.87.0",
"oxlint-tsgolint": "7.0.2005",
"release-please": "17.3.0",
"rolldown": "1.2.12",
"tsrs": "0.18.0-ts7.1.0-dev.20260929",
+1 -1
View File
@@ -108,7 +108,7 @@ function classifyPackage(base, head) {
continue;
}
if (
/^(test(?::|$)|check(?::|$)|lint(?::|$)|format(?::|$)|knip$|jscpd$)/.test(
/^(test(?::|$)|check(?::|$)|lint(?::|$)|format(?::|$)|knip(?::|$)|jscpd$)/.test(
script,
)
)
@@ -0,0 +1,33 @@
export interface ProductionReachabilityFinding {
readonly name: string;
readonly namespace?: string;
readonly kind?: string;
readonly specifier?: string;
readonly line?: number;
readonly col?: number;
readonly pos?: number;
}
export interface ProductionReachabilityIssue {
readonly file: string;
readonly owners?: readonly { readonly name: string }[];
readonly files?: readonly ProductionReachabilityFinding[];
readonly exports?: readonly ProductionReachabilityFinding[];
readonly nsExports?: readonly ProductionReachabilityFinding[];
readonly types?: readonly ProductionReachabilityFinding[];
readonly nsTypes?: readonly ProductionReachabilityFinding[];
readonly enumMembers?: readonly ProductionReachabilityFinding[];
readonly namespaceMembers?: readonly ProductionReachabilityFinding[];
readonly duplicates?: readonly (readonly ProductionReachabilityFinding[])[];
}
export interface ProductionReachabilityReport {
readonly issues: readonly ProductionReachabilityIssue[];
}
export function parseProductionReachabilityReport(
text: string,
): ProductionReachabilityReport;
export function recordProductionReachabilityReport(
root?: string,
): Promise<ProductionReachabilityReport>;
@@ -0,0 +1,86 @@
import { spawnSync } from "node:child_process";
import { mkdir, rm, writeFile } from "node:fs/promises";
import { createRequire } from "node:module";
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { z } from "zod";
const require = createRequire(import.meta.url);
const finding = z.object({
name: z.string(),
namespace: z.string().optional(),
kind: z.string().optional(),
specifier: z.string().optional(),
line: z.number().optional(),
col: z.number().optional(),
pos: z.number().optional(),
});
const issue = z.strictObject({
file: z.string(),
owners: z.array(z.object({ name: z.string() })).optional(),
files: z.array(finding).optional(),
exports: z.array(finding).optional(),
nsExports: z.array(finding).optional(),
types: z.array(finding).optional(),
nsTypes: z.array(finding).optional(),
enumMembers: z.array(finding).optional(),
namespaceMembers: z.array(finding).optional(),
duplicates: z.array(z.array(finding)).optional(),
});
const reportSchema = z.strictObject({ issues: z.array(issue) });
/** Admit the installed Knip JSON reporter's candidate records. */
export const parseProductionReachabilityReport = (text) =>
reportSchema.parse(JSON.parse(text));
/** Run production reachability without turning candidates into failures. */
export const recordProductionReachabilityReport = async (
root = process.cwd(),
) => {
const reportDirectory = join(root, ".cache", "static-checks");
const reportPath = join(reportDirectory, "production-reachability.json");
// Findings are advisory. Execution, configuration and report-admission failures
// still fail, and must never leave an older successful report at this path.
await mkdir(reportDirectory, { recursive: true });
await rm(reportPath, { force: true });
const result = spawnSync(
process.execPath,
[
join(dirname(require.resolve("knip")), "..", "bin", "knip.js"),
"--config",
"knip.production.json",
"--production",
"--reporter",
"json",
"--no-exit-code",
],
{ cwd: root, encoding: "utf8", maxBuffer: 32 * 1024 * 1024 },
);
if (result.stderr) process.stderr.write(result.stderr);
if (result.error !== undefined) throw result.error;
if (result.status !== 0) {
if (result.stdout) process.stderr.write(result.stdout);
throw new Error(
result.status === null
? `Production reachability analysis terminated by ${result.signal}`
: `Production reachability analysis failed with status ${result.status}`,
);
}
const report = parseProductionReachabilityReport(result.stdout);
await writeFile(reportPath, `${JSON.stringify(report, null, 2)}\n`);
console.log(
`Production reachability: ${report.issues.length} files with advisory candidates.`,
);
console.log(`Full report: ${reportPath}`);
console.log(
"Candidates require runtime, verifier and published-contract review before removal; ordinary Knip remains separate.",
);
return report;
};
if (
process.argv[1] !== undefined &&
resolve(process.argv[1]) === fileURLToPath(import.meta.url)
)
await recordProductionReachabilityReport();
+35 -11
View File
@@ -48,7 +48,12 @@ const duplicateReport = z
})
.passthrough();
const run = (packageName, entrypoint, arguments_) => {
const run = (
packageName,
entrypoint,
arguments_,
environment = process.env,
) => {
const executable = join(
dirname(require.resolve(`${packageName}/package.json`)),
entrypoint,
@@ -56,6 +61,7 @@ const run = (packageName, entrypoint, arguments_) => {
const result = spawnSync(process.execPath, [executable, ...arguments_], {
encoding: "utf8",
maxBuffer: 32 * 1024 * 1024,
env: environment,
});
if (result.stderr) process.stderr.write(result.stderr);
if (result.error !== undefined) throw result.error;
@@ -66,14 +72,30 @@ const run = (packageName, entrypoint, arguments_) => {
try {
await mkdir(reportRoot, { recursive: true });
if (mode === "lint") {
const reportPath = join(reportRoot, "oxlint.json");
if (mode === "lint" || mode === "typed-lint") {
const typed = mode === "typed-lint";
const reportPath = join(
reportRoot,
typed ? "typed-oxlint.json" : "oxlint.json",
);
await rm(reportPath, { force: true });
const result = run("oxlint", "bin/oxlint", [
...extraArguments,
"--format",
"json",
]);
const result = run(
"oxlint",
"bin/oxlint",
[
...(typed ? ["--type-aware", "--threads", "1"] : []),
...extraArguments,
"--format",
"json",
],
typed
? {
...process.env,
GOMAXPROCS: process.env.GOMAXPROCS ?? "2",
GOMEMLIMIT: process.env.GOMEMLIMIT ?? "1024MiB",
}
: process.env,
);
let report;
try {
report = lintReport.parse(JSON.parse(result.stdout));
@@ -89,7 +111,7 @@ try {
({ severity }) => severity === "error",
);
console.log(
`Lint: ${report.number_of_files} files, ${errors.length} errors, ${warnings.length} warnings.`,
`${typed ? "Typed lint" : "Lint"}: ${report.number_of_files} files, ${errors.length} errors, ${warnings.length} warnings.`,
);
for (const diagnostic of errors) {
const location = diagnostic.labels[0]?.span;
@@ -106,7 +128,9 @@ try {
)
.slice(0, 8))
console.log(` ${rule}: ${count} warnings`);
console.log(`Full diagnostics: ${reportPath}; npm run lint:details`);
console.log(
`Full diagnostics: ${reportPath}${typed ? "" : "; npm run lint:details"}`,
);
process.exitCode = result.status;
} else if (mode === "duplicates") {
const output = join(reportRoot, "jscpd");
@@ -143,7 +167,7 @@ try {
console.log(`Full diagnostics: ${reportPath}; npm run jscpd:details`);
process.exitCode = result.status;
} else {
throw new Error("Expected lint or duplicates");
throw new Error("Expected lint, typed-lint or duplicates");
}
} catch (cause) {
console.error(cause instanceof Error ? cause.message : String(cause));
+5 -7
View File
@@ -1,5 +1,5 @@
import { createVerifierHttpServer } from "../lib/verifier-http-server.mjs";
import { once } from "node:events";
import { createServer } from "node:http";
import { gzipSync } from "node:zlib";
export const networkFixtureSecret = "私密-network-fixture";
@@ -23,7 +23,7 @@ const html = `<!doctype html><meta charset="utf-8"><body><button id="start">Run<
export async function startBrowserNetworkSite() {
const counts = new Map();
const timers = new Set();
const server = createServer(async (request, response) => {
const handleRequest = async (request, response) => {
const path = new URL(request.url, "http://fixture.test").pathname;
counts.set(path, (counts.get(path) ?? 0) + 1);
if (path === "/same") {
@@ -89,7 +89,8 @@ export async function startBrowserNetworkSite() {
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
response.end(html);
}
});
};
const { server, close } = createVerifierHttpServer(handleRequest);
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
@@ -100,10 +101,7 @@ export async function startBrowserNetworkSite() {
counts,
async close() {
for (const timer of timers) clearTimeout(timer);
server.closeAllConnections();
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
await close();
},
};
}
+5 -9
View File
@@ -1,5 +1,5 @@
import { createVerifierHttpServer } from "../lib/verifier-http-server.mjs";
import { once } from "node:events";
import { createServer } from "node:http";
/** The CSRF cookie and the stored bearer token the page holds; request steps must send both and the capture must not keep them. */
export const requestFixtureCsrf = "csrf-fixture-7c1d2e";
@@ -52,7 +52,7 @@ export async function startBrowserRequestSite() {
};
const writes = [];
const hosts = [];
const server = createServer(async (request, response) => {
const handleRequest = async (request, response) => {
const url = new URL(request.url, "http://fixture.test");
hosts.push(`${request.headers.host} ${url.pathname}`);
const chunks = [];
@@ -108,7 +108,8 @@ export async function startBrowserRequestSite() {
}
const [status, value] = answerItems(store, request.method, id, body);
json(response, status, value);
});
};
const { server, close } = createVerifierHttpServer(handleRequest);
server.listen(0, "127.0.0.1");
await once(server, "listening");
const address = server.address();
@@ -120,11 +121,6 @@ export async function startBrowserRequestSite() {
/** Host header and path of every request the server received. */
hosts,
items: store.items,
async close() {
server.closeAllConnections();
await new Promise((resolve, reject) =>
server.close((error) => (error ? reject(error) : resolve())),
);
},
close,
};
}
@@ -0,0 +1,18 @@
import { tsImport } from "tsx/esm/api";
const { agentEvaluationPassed } = await tsImport(
new URL(
"../../tests/evaluation/model-evals/AgentEvaluationReport.ts",
import.meta.url,
).href,
import.meta.url,
);
/** Fail the runner after publishing a report with any rejected scenario. */
export const assertAgentEvaluationReportPassed = (results) => {
const failed = results.filter((result) => !agentEvaluationPassed(result));
if (failed.length > 0)
throw new Error(
`Agent routing/workflow/answer checks failed: ${failed.map(({ id }) => id).join(", ")}`,
);
};
+3 -1
View File
@@ -529,9 +529,11 @@ export async function verifyGhidraBoundaries(
regexError.remediation.action,
/literal mode or simplify the regex/u,
);
// The retry advice lives in the remediation; the reason states that the
// session survived the overflow.
assert.match(
regexError.details.reason,
/exhausted its stack.*Use literal mode/u,
/exhausted its stack.*session and annotations remain available/u,
);
rejectedCalls++;
const nestedPattern = "(".repeat(12000) + "a" + ")".repeat(12000);
+12
View File
@@ -0,0 +1,12 @@
import type { IncomingMessage, Server, ServerResponse } from "node:http";
/** Own admitted fixture requests through transport closure and report unexpected failures. */
export function createVerifierHttpServer(
handleRequest: (
request: IncomingMessage,
response: ServerResponse,
) => Promise<void>,
): {
readonly server: Server;
close(): Promise<void>;
};
+39
View File
@@ -0,0 +1,39 @@
import { createServer } from "node:http";
import { withVerifierCleanup } from "./verifier-cleanup.mjs";
/** Own admitted fixture requests through transport closure and report unexpected failures. */
export function createVerifierHttpServer(handleRequest) {
const pending = new Set();
let failure;
const server = createServer((request, response) => {
const work = handleRequest(request, response)
.catch((cause) => {
failure ??= { cause };
response.destroy();
})
.finally(() => {
pending.delete(work);
});
pending.add(work);
});
return {
server,
close() {
return withVerifierCleanup(
() =>
new Promise((resolve, reject) => {
server.close((cause) =>
cause === undefined ? resolve() : reject(cause),
);
server.closeAllConnections();
}),
[
async () => {
await Promise.all(pending);
if (failure !== undefined) throw failure.cause;
},
],
);
},
};
}
+12
View File
@@ -0,0 +1,12 @@
/** Resolve the lexical binding instead of treating a matching name as an API. */
export const findBinding = (sourceCode, identifier) => {
for (
let scope = sourceCode.getScope(identifier);
scope !== null;
scope = scope.upper
) {
const variable = scope.set.get(identifier.name);
if (variable !== undefined) return variable;
}
return undefined;
};
+4
View File
@@ -0,0 +1,4 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const cancellationCauseRule: Parameters<RuleTester["run"]>[1];
export default cancellationCauseRule;
+200
View File
@@ -0,0 +1,200 @@
import { findBinding } from "./bindings.mjs";
/**
* Require an explicit cancellation reason when a directly checked caller signal
* is converted to an AnalysisCancelledError. This is deliberately syntactic:
* internal shutdown and cancellation forwarded through other owners are outside
* the rule.
*/
const coreModule = /(?:^|\/)analysisErrorCore(?:\.js|\.ts)?$/;
const unwrapChain = (node) =>
node?.type === "ChainExpression" ? node.expression : node;
const propertyName = (property) =>
property.computed
? property.property.type === "Literal"
? property.property.value
: undefined
: property.property.name;
const annotationType = (node) => {
const annotation = node?.typeAnnotation?.typeAnnotation;
return annotation?.type === "TSTypeReference" &&
annotation.typeName.type === "Identifier"
? annotation.typeName
: undefined;
};
const soleDefinition = (variable) =>
variable?.defs.length === 1 ? variable.defs[0] : undefined;
const isKnownType = (node, kind, context) => {
const type = annotationType(node);
if (type === undefined) return false;
const definition = soleDefinition(findBinding(context.sourceCode, type));
if (kind === "AbortSignal")
return type.name === "AbortSignal" && definition === undefined;
return (
definition?.type === "ImportBinding" &&
definition.parent?.type === "ImportDeclaration" &&
/(?:^|\/)AnalysisProvider(?:\.js|\.ts)?$/.test(
definition.parent.source.value,
) &&
definition.node.type === "ImportSpecifier" &&
definition.node.imported.name === "ExecutionOptions"
);
};
const unmodified = (variable) =>
variable !== undefined &&
variable.references.every(
(reference) => !reference.isWrite() || reference.init,
);
const isOptionsSignal = (node, context, seen) => {
node = unwrapChain(node);
if (node?.type !== "MemberExpression" || propertyName(node) !== "signal")
return false;
return isKnownBinding(node.object, "ExecutionOptions", context, seen);
};
const isKnownBinding = (node, kind, context, seen) => {
if (node?.type !== "Identifier") return false;
const variable = findBinding(context.sourceCode, node);
if (!unmodified(variable) || seen.has(variable)) return false;
seen.add(variable);
const definition = soleDefinition(variable);
if (definition?.type === "Parameter")
return isKnownType(definition.name, kind, context);
if (definition?.type !== "Variable" || definition.parent?.kind !== "const")
return false;
const declaration = definition.node;
if (isKnownType(declaration.id, kind, context)) return true;
return kind === "AbortSignal"
? isKnownSignal(declaration.init, context, seen)
: false;
};
const isKnownSignal = (node, context, seen = new Set()) =>
isKnownBinding(unwrapChain(node), "AbortSignal", context, seen) ||
isOptionsSignal(node, context, seen);
const isAbortRead = (node, context) => {
node = unwrapChain(node);
return (
node?.type === "MemberExpression" &&
propertyName(node) === "aborted" &&
isKnownSignal(node.object, context)
);
};
const isTrueLiteral = (node) => node?.type === "Literal" && node.value === true;
const positiveAbortGuard = (test, context) => {
if (isAbortRead(test, context)) return true;
if (
test?.type === "BinaryExpression" &&
(test.operator === "===" || test.operator === "==")
)
return (
(isAbortRead(test.left, context) && isTrueLiteral(test.right)) ||
(isTrueLiteral(test.left) && isAbortRead(test.right, context))
);
if (test?.type === "LogicalExpression" && test.operator === "&&")
return (
positiveAbortGuard(test.left, context) ||
positiveAbortGuard(test.right, context)
);
return false;
};
const isCanonicalConstructor = (node, context) => {
if (node?.type !== "Identifier") return false;
const definition = soleDefinition(findBinding(context.sourceCode, node));
return (
definition?.type === "ImportBinding" &&
definition.parent?.type === "ImportDeclaration" &&
coreModule.test(definition.parent.source.value) &&
definition.node.type === "ImportSpecifier" &&
definition.node.imported.name === "AnalysisCancelledError"
);
};
const hasExplicitCause = (node) => {
const options = node.arguments[1];
if (options !== undefined && options.type !== "ObjectExpression") return true; // Forwarded context is owned elsewhere.
if (options === undefined) return false;
return options.properties.some(
(property) =>
property.type === "SpreadElement" ||
(property.type === "Property" &&
(property.computed
? property.key.type !== "Literal" || property.key.value === "cause"
: (property.key.type === "Identifier"
? property.key.name
: property.key.value) === "cause")),
);
};
const inspectExpression = (expression, context) => {
if (expression?.type === "NewExpression") {
if (
isCanonicalConstructor(expression.callee, context) &&
!hasExplicitCause(expression)
)
context.report({
node: expression,
messageId: "missingCause",
});
return;
}
if (expression?.type === "CallExpression") {
// Common Result adapter: return err(new AnalysisCancelledError(...)).
for (const argument of expression.arguments)
inspectExpression(argument, context);
} else if (expression?.type === "AssignmentExpression") {
inspectExpression(expression.right, context);
}
};
const inspectBranch = (branch, context) => {
if (branch.type === "BlockStatement") {
for (const statement of branch.body) inspectBranch(statement, context);
} else if (branch.type === "VariableDeclaration") {
for (const declaration of branch.declarations)
inspectExpression(declaration.init, context);
} else if (
branch.type === "ThrowStatement" ||
branch.type === "ReturnStatement"
) {
inspectExpression(branch.argument, context);
} else if (branch.type === "ExpressionStatement") {
inspectExpression(branch.expression, context);
}
};
const cancellationCauseRule = {
meta: {
type: "problem",
docs: {
description:
"Preserve a directly checked caller AbortSignal reason in AnalysisCancelledError",
},
messages: {
missingCause:
"Pass the checked AbortSignal's reason as an explicit cancellation cause.",
},
},
create(context) {
return {
IfStatement(node) {
if (positiveAbortGuard(node.test, context))
inspectBranch(node.consequent, context);
},
};
},
};
export default cancellationCauseRule;
+17
View File
@@ -0,0 +1,17 @@
import noBufferDeepEquality from "./no-buffer-deep-equality.mjs";
import cancellationCause from "./cancellation-cause.mjs";
import noEvidenceJsonPreadmission from "./rules/no-evidence-json-preadmission.mjs";
import noFixtureRootCleanup from "./rules/no-fixture-root-cleanup.mjs";
import noPositionalToolContract from "./rules/no-positional-tool-contract.mjs";
export default {
meta: { name: "rea" },
rules: {
"no-buffer-deep-equality": noBufferDeepEquality,
"cancellation-cause": cancellationCause,
"no-evidence-json-preadmission": noEvidenceJsonPreadmission,
"no-fixture-root-cleanup": noFixtureRootCleanup,
"no-positional-tool-contract": noPositionalToolContract,
},
};
@@ -0,0 +1,4 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const noBufferDeepEquality: Parameters<RuleTester["run"]>[1];
export default noBufferDeepEquality;
+166
View File
@@ -0,0 +1,166 @@
import { findBinding } from "./bindings.mjs";
const memberName = (node) =>
node?.type === "MemberExpression" &&
!node.computed &&
node.property.type === "Identifier"
? node.property.name
: null;
const unwrap = (node) => {
while (
node?.type === "TSAsExpression" ||
node?.type === "TSSatisfiesExpression" ||
node?.type === "TSNonNullExpression" ||
node?.type === "TSTypeAssertion" ||
node?.type === "ParenthesizedExpression"
)
node = node.expression;
return node;
};
const importedKind = (variable) => {
const definition = variable?.defs[0];
if (definition?.type !== "ImportBinding") return null;
const source = definition.parent?.source?.value;
const node = definition.node;
if (node?.type === "ImportNamespaceSpecifier") {
if (source === "node:fs/promises") return "fsPromises";
if (source === "node:fs") return "fs";
return null;
}
if (node?.type !== "ImportSpecifier") return null;
const imported = node.imported?.name ?? node.imported?.value;
if (source === "vitest" && imported === "expect") return "expect";
if (source === "node:buffer" && imported === "Buffer") return "Buffer";
if (source === "node:fs/promises" && imported === "readFile")
return "readFile";
if (source === "node:fs" && imported === "readFileSync")
return "readFileSync";
return null;
};
const constInitializer = (variable) => {
if (variable?.defs.length !== 1) return null;
const definition = variable.defs[0];
if (
definition?.type !== "Variable" ||
definition.node?.type !== "VariableDeclarator" ||
definition.parent?.kind !== "const" ||
variable.references.some(
(reference) => reference.isWrite() && !reference.init,
)
)
return null;
return definition.node.init;
};
/** Flag byte comparisons that make Vitest enumerate a Node Buffer. */
const noBufferDeepEquality = {
meta: {
type: "suggestion",
messages: {
nativeBytes:
"Compare Node Buffers with Buffer.equals or Buffer.compare; deep equality can enumerate every byte and exhaust memory.",
},
},
create(context) {
const { sourceCode } = context;
const symbolKind = (expression, seen = new Set()) => {
const node = unwrap(expression);
if (node?.type === "Identifier") {
const variable = findBinding(sourceCode, node);
if (
(variable === undefined || variable.defs.length === 0) &&
sourceCode.isGlobalReference(node)
)
return node.name === "Buffer" ? "Buffer" : null;
const imported = importedKind(variable);
if (imported !== null) return imported;
if (variable === undefined || seen.has(variable)) return null;
const initializer = constInitializer(variable);
if (initializer === null) return null;
seen.add(variable);
return symbolKind(initializer, seen);
}
const property = memberName(node);
if (property === null) return null;
const owner = symbolKind(node.object, seen);
if (owner === "fsPromises" && property === "readFile") return "readFile";
if (owner === "fs" && property === "readFileSync") return "readFileSync";
if (
owner === "Buffer" &&
["from", "alloc", "allocUnsafe", "allocUnsafeSlow", "concat"].includes(
property,
)
)
return "bufferFactory";
return null;
};
const isBuffer = (expression, seen = new Set(), awaited = false) => {
const node = unwrap(expression);
if (node?.type === "AwaitExpression")
return isBuffer(node.argument, seen, true);
if (node?.type === "Identifier") {
const variable = findBinding(sourceCode, node);
if (variable === undefined || seen.has(variable)) return false;
const initializer = constInitializer(variable);
if (initializer === null) return false;
seen.add(variable);
return isBuffer(initializer, seen, awaited);
}
if (node?.type !== "CallExpression" || node.optional) return false;
const kind = symbolKind(node.callee);
return (
kind === "bufferFactory" ||
((kind === "readFileSync" || (kind === "readFile" && awaited)) &&
node.arguments.length === 1 &&
node.arguments[0].type !== "SpreadElement")
);
};
const expectedCall = (object, settlement = null) => {
const node = unwrap(object);
const property = memberName(node);
if (property === "not") return expectedCall(node.object, settlement);
if (property === "resolves" || property === "rejects")
return expectedCall(node.object, property);
return node?.type === "CallExpression" &&
!node.optional &&
symbolKind(node.callee) === "expect"
? { call: node, settlement }
: null;
};
return {
CallExpression(node) {
if (node.optional) return;
const matcher = memberName(node.callee);
if (matcher !== "toEqual" && matcher !== "toStrictEqual") return;
const assertion = expectedCall(node.callee.object);
if (
assertion === null ||
![1, 2].includes(assertion.call.arguments.length) ||
assertion.call.arguments.some(
(argument) => argument.type === "SpreadElement",
) ||
node.arguments.length !== 1
)
return;
if (
(assertion.settlement !== "rejects" &&
isBuffer(
assertion.call.arguments[0],
new Set(),
assertion.settlement === "resolves",
)) ||
isBuffer(node.arguments[0])
)
context.report({
node: node.callee.property,
messageId: "nativeBytes",
});
},
};
},
};
export default noBufferDeepEquality;
+4
View File
@@ -0,0 +1,4 @@
export declare const resolveImportSource: (
filename: string,
specifier: unknown,
) => string | undefined;
+20
View File
@@ -0,0 +1,20 @@
import { dirname, isAbsolute, resolve } from "node:path";
import { fileURLToPath } from "node:url";
/** Resolve local import spellings without assigning local meaning to bare or HTTP imports. */
export const resolveImportSource = (filename, specifier) => {
if (typeof specifier !== "string") return undefined;
if (isAbsolute(specifier)) return resolve(specifier);
if (specifier.startsWith("file:")) {
try {
const url = new URL(specifier);
if (url.search !== "" || url.hash !== "") return undefined;
return fileURLToPath(url);
} catch {
return undefined;
}
}
if (specifier.startsWith("./") || specifier.startsWith("../"))
return resolve(dirname(filename), specifier);
return undefined;
};
@@ -0,0 +1,4 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -0,0 +1,137 @@
import { findBinding } from "../bindings.mjs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveImportSource } from "./import-source.mjs";
const domain = fileURLToPath(new URL("../../../src/domain/", import.meta.url));
const payloadFields = new Set(["parameters", "result", "rawResult"]);
const propertyName = (node) => {
if (!node.computed && node.property?.type === "Identifier")
return node.property.name;
if (
node.property?.type === "Literal" &&
typeof node.property.value === "string"
)
return node.property.value;
return undefined;
};
const fieldName = (node) => {
if (node.type !== "Property") return undefined;
if (!node.computed && node.key.type === "Identifier") return node.key.name;
if (node.key.type === "Literal" && typeof node.key.value === "string")
return node.key.value;
return undefined;
};
const importedBinding = (sourceCode, node) => {
if (node.type !== "Identifier") return undefined;
const variable = findBinding(sourceCode, node);
if (!variable || variable.defs.length !== 1) return undefined;
const definition = variable.defs[0];
return definition.type === "ImportBinding" ? definition : undefined;
};
const isImported = (context, node, module, names) => {
const member = node.type === "MemberExpression" && !node.optional;
const binding = importedBinding(
context.sourceCode,
member ? node.object : node,
);
if (!binding || binding.parent?.type !== "ImportDeclaration") return false;
const declaration = binding.parent;
if (declaration.importKind === "type" || binding.node.importKind === "type")
return false;
const specifier = declaration.source.value;
const target = resolveImportSource(
context.physicalFilename ?? context.filename,
specifier,
);
if (
target !== resolve(domain, `${module}.js`) &&
target !== resolve(domain, `${module}.ts`)
)
return false;
if (member)
return (
binding.node.type === "ImportNamespaceSpecifier" &&
names.has(propertyName(node))
);
if (binding.node.type !== "ImportSpecifier") return false;
return names.has(binding.node.imported.name ?? binding.node.imported.value);
};
const evidenceCreator = new Set(["createEvidence"]);
const genericJsonSchemas = new Set(["jsonValueSchema", "jsonObjectSchema"]);
export default {
meta: {
type: "suggestion",
docs: {
description:
"Leave ordinary Evidence JSON payload admission with createEvidence.",
},
schema: [],
messages: {
redundantJsonAdmission:
"Pass the {{field}} payload directly to createEvidence; it owns generic JSON admission. Keep semantic validation at its own boundary.",
},
},
create(context) {
return {
CallExpression(node) {
if (
node.optional ||
!isImported(context, node.callee, "evidence", evidenceCreator)
)
return;
const observation = node.arguments[2];
if (observation?.type !== "ObjectExpression") return;
for (const [index, property] of observation.properties.entries()) {
const field = fieldName(property);
if (
!payloadFields.has(field) ||
property.kind !== "init" ||
property.method
)
continue;
// A later spread or unknown key may replace this field before admission.
if (
observation.properties
.slice(index + 1)
.some(
(later) =>
later.type !== "Property" ||
fieldName(later) === undefined ||
fieldName(later) === field,
)
)
continue;
const parsed = property.value;
if (
parsed.type !== "CallExpression" ||
parsed.optional ||
parsed.arguments.length !== 1
)
continue;
const parse = parsed.callee;
if (
parse.type !== "MemberExpression" ||
parse.optional ||
propertyName(parse) !== "parse"
)
continue;
if (
isImported(context, parse.object, "jsonValue", genericJsonSchemas)
)
context.report({
node: parsed,
messageId: "redundantJsonAdmission",
data: { field },
});
}
},
};
},
};
@@ -0,0 +1,3 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -0,0 +1,149 @@
import { findBinding } from "../bindings.mjs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { resolveImportSource } from "./import-source.mjs";
const root = fileURLToPath(new URL("../../../", import.meta.url));
const fixtureModule = resolve(root, "tests/fixtures/temporaryDirectory");
const workspaceModule = resolve(
root,
"tests/support/workspace/workspaceFixture",
);
const hooks = new Set(["afterEach", "afterAll", "onTestFinished"]);
const unwrap = (node) => {
while (
node &&
[
"AwaitExpression",
"TSAsExpression",
"TSNonNullExpression",
"ChainExpression",
].includes(node.type)
)
node = node.argument ?? node.expression;
return node;
};
const isFunction = (node) =>
node &&
[
"FunctionDeclaration",
"FunctionExpression",
"ArrowFunctionExpression",
].includes(node.type);
export default {
meta: {
type: "problem",
docs: {
description: "Let createTestTempDirectory own removal of its root.",
},
schema: [],
messages: {
duplicate:
"createTestTempDirectory already owns root cleanup; remove the duplicate teardown deletion, retaining cleanup of other resources.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const filename = context.physicalFilename ?? context.filename;
const calls = [];
const variable = (node) => findBinding(sourceCode, node);
const imported = (node) => {
const member = node?.type === "MemberExpression" ? node : undefined;
const binding = member ? member.object : node;
if (binding?.type !== "Identifier") return undefined;
const definition = variable(binding)?.defs[0];
if (
definition?.type !== "ImportBinding" ||
(member
? definition.node.type !== "ImportNamespaceSpecifier"
: definition.node.type !== "ImportSpecifier")
)
return undefined;
const declaration = definition.parent ?? definition.node.parent;
const specifier = declaration.source?.value;
if (typeof specifier !== "string") return undefined;
return {
name: member
? member.computed
? member.property.value
: member.property.name
: (definition.node.imported.name ?? definition.node.imported.value),
module:
resolveImportSource(filename, specifier)?.replace(
/\.(?:[cm]?[jt]s)$/u,
"",
) ?? specifier,
};
};
const fixtureRoot = (raw, seen = new Set()) => {
const node = unwrap(raw);
if (node?.type === "CallExpression") {
const identity = imported(node.callee);
return (
identity?.module === fixtureModule &&
identity.name === "createTestTempDirectory"
);
}
if (node?.type !== "Identifier") return false;
const binding = variable(node);
if (!binding || seen.has(binding)) return false;
seen.add(binding);
const definition = binding.defs[0];
// Mutable ownership and mixed root registries require flow analysis, not
// a guess based on the variable name or one assignment.
return (
definition?.type === "Variable" &&
definition.parent.kind === "const" &&
definition.node.id.type === "Identifier" &&
fixtureRoot(definition.node.init, seen)
);
};
const callback = (raw) => {
const node = unwrap(raw);
if (isFunction(node)) return node;
if (node?.type !== "Identifier") return undefined;
const definition = variable(node)?.defs[0];
if (definition?.type === "FunctionName") return definition.node;
if (
definition?.type === "Variable" &&
definition.parent.kind === "const"
) {
const value = unwrap(definition.node.init);
if (isFunction(value)) return value;
}
return undefined;
};
return {
CallExpression(node) {
calls.push(node);
},
"Program:exit"() {
const teardowns = new Set();
for (const call of calls) {
const identity = imported(call.callee);
if (identity?.module === "vitest" && hooks.has(identity.name)) {
const body = callback(call.arguments[0]);
if (body) teardowns.add(body);
}
}
for (const call of calls) {
const identity = imported(call.callee);
if (!identity) continue;
const removesRoot =
(identity.module === "node:fs/promises" &&
["rm", "rmdir"].includes(identity.name)) ||
(identity.module === "node:fs" &&
["rm", "rmSync", "rmdir", "rmdirSync"].includes(identity.name)) ||
(identity.module === workspaceModule &&
identity.name === "removeTestWorkspace");
if (!removesRoot || !fixtureRoot(call.arguments[0])) continue;
let parent = call.parent;
while (parent && !isFunction(parent)) parent = parent.parent;
if (teardowns.has(parent))
context.report({ node: call, messageId: "duplicate" });
}
},
};
},
};
@@ -0,0 +1,3 @@
import type { RuleTester } from "oxlint/plugins-dev";
declare const rule: Parameters<RuleTester["run"]>[1];
export default rule;
@@ -0,0 +1,218 @@
import { findBinding } from "../bindings.mjs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { collectToolCatalogSources } from "./tool-catalog-sources.mjs";
import { resolveImportSource } from "./import-source.mjs";
const root = fileURLToPath(new URL("../../../", import.meta.url));
const catalogs = collectToolCatalogSources(
resolve(root, "src/contracts/toolContracts.ts"),
);
const unwrap = (node) => {
while (
node &&
[
"TSAsExpression",
"TSSatisfiesExpression",
"TSNonNullExpression",
"ChainExpression",
].includes(node.type)
)
node = node.expression;
return node;
};
const numeric = (node) =>
(node?.type === "Literal" &&
(typeof node.value === "number" ||
(typeof node.value === "string" &&
/^(?:0|[1-9][0-9]*)$/u.test(node.value)))) ||
(node?.type === "UnaryExpression" &&
["+", "-"].includes(node.operator) &&
node.argument.type === "Literal" &&
typeof node.argument.value === "number");
export default {
meta: {
type: "problem",
docs: {
description:
"Select canonical tool contracts by name, never catalog position.",
},
schema: [],
messages: {
positional:
"Select a named tool contract; catalog ordering must not select an operation or schema.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const filename = context.physicalFilename ?? context.filename;
const variable = (node) => findBinding(sourceCode, node);
const imported = (node) => {
if (node?.type !== "Identifier") return undefined;
const definition = variable(node)?.defs[0];
if (definition?.type !== "ImportBinding") return undefined;
const declaration = definition.parent ?? definition.node.parent;
const specifier = declaration.source?.value;
const path = resolveImportSource(filename, specifier)?.replace(
/\.(?:[cm]?[jt]s)$/u,
"",
);
const expected = catalogs.get(path);
if (expected === undefined) return undefined;
return { definition, expected };
};
const isCatalog = (raw, seen = new Set()) => {
const node = unwrap(raw);
if (!node) return false;
if (node.type === "Identifier") {
const binding = variable(node);
if (!binding || seen.has(binding)) return false;
seen.add(binding);
const identity = imported(node);
if (identity) {
const specifier = identity.definition.node;
return (
specifier.type === "ImportSpecifier" &&
identity.expected.has(
specifier.imported.name ?? specifier.imported.value,
)
);
}
const definition = binding.defs[0];
return (
definition?.type === "Variable" &&
definition.parent.kind === "const" &&
definition.node.id.type === "Identifier" &&
isCatalog(definition.node.init, seen)
);
}
if (node.type === "MemberExpression" || node.type === "TSQualifiedName") {
const object =
node.type === "MemberExpression" ? node.object : node.left;
const identity = imported(object);
if (identity?.definition.node.type !== "ImportNamespaceSpecifier")
return false;
const property =
node.type === "MemberExpression" ? node.property : node.right;
const name = node.computed ? property.value : property.name;
return identity.expected.has(name);
}
return false;
};
const selectedContract = (raw, seen = new Set()) => {
const node = unwrap(raw);
if (!node) return undefined;
if (
node.type === "MemberExpression" &&
node.computed &&
numeric(node.property) &&
isCatalog(node.object)
)
return node;
if (node.type === "CallExpression") {
const callee = unwrap(node.callee);
if (
callee?.type === "MemberExpression" &&
(callee.computed ? callee.property.value : callee.property.name) ===
"at" &&
numeric(node.arguments[0]) &&
isCatalog(callee.object)
)
return node;
}
if (node.type !== "Identifier") return undefined;
const binding = variable(node);
if (!binding || seen.has(binding)) return undefined;
seen.add(binding);
const definition = binding.defs[0];
if (definition?.type !== "Variable" || definition.parent.kind !== "const")
return undefined;
if (definition.node.id.type === "Identifier")
return selectedContract(definition.node.init, seen);
if (
definition.node.id.type === "ArrayPattern" &&
definition.node.id.elements.some(
(element) =>
element?.type === "Identifier" && element.name === node.name,
) &&
isCatalog(definition.node.init)
)
return definition.node;
return undefined;
};
const bindingFields = new Set(["name", "inputSchema", "outputSchema"]);
const isRegistrationOptions = (node) => {
if (node?.type !== "Identifier") return false;
const definition = variable(node)?.defs[0];
if (
definition?.type !== "ImportBinding" ||
definition.node.type !== "ImportSpecifier"
)
return false;
const declaration = definition.parent ?? definition.node.parent;
const specifier = declaration.source?.value;
const path = resolveImportSource(filename, specifier)?.replace(
/\.(?:[cm]?[jt]s)$/u,
"",
);
const name =
definition.node.imported.name ?? definition.node.imported.value;
return (
path === resolve(root, "src/server/toolRegistrationOptions") &&
[
"toolRegistrationOptions",
"applicationToolRegistrationOptions",
].includes(name)
);
};
const reported = new Set();
const reportSelection = (raw) => {
const node = selectedContract(raw);
if (node && !reported.has(node)) {
reported.add(node);
context.report({ node, messageId: "positional" });
}
};
return {
MemberExpression(node) {
const name = node.computed ? node.property.value : node.property.name;
const parent = node.parent;
const writesProperty =
(parent?.type === "AssignmentExpression" && parent.left === node) ||
parent?.type === "UpdateExpression" ||
(parent?.type === "UnaryExpression" && parent.operator === "delete");
if (bindingFields.has(name) && !writesProperty)
reportSelection(node.object);
},
CallExpression(node) {
if (isRegistrationOptions(unwrap(node.callee)))
reportSelection(node.arguments[0]);
},
VariableDeclarator(node) {
if (
node.id.type === "ObjectPattern" &&
node.id.properties.some(
(property) =>
property.type === "Property" &&
bindingFields.has(
property.computed ? property.key.value : property.key.name,
),
)
)
reportSelection(node.init);
},
TSIndexedAccessType(node) {
if (
node.objectType.type === "TSTypeQuery" &&
node.indexType.type === "TSLiteralType" &&
numeric(node.indexType.literal) &&
isCatalog(node.objectType.exprName)
)
context.report({ node, messageId: "positional" });
},
};
},
};
@@ -0,0 +1,4 @@
export declare const collectToolCatalogSources: (
entryPath: string,
readSource?: (path: string) => string,
) => ReadonlyMap<string, ReadonlySet<string>>;
+118
View File
@@ -0,0 +1,118 @@
import { parse } from "@babel/parser";
import { readFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
const withoutExtension = (path) => path.replace(/\.(?:[cm]?[jt]s)$/u, "");
const arrayExpression = (raw) => {
let node = raw;
while (
node &&
["TSAsExpression", "TSSatisfiesExpression"].includes(node.type)
)
node = node.expression;
return node;
};
/** Read only source composition, never execute the runtime contract catalog. */
export const collectToolCatalogSources = (
entryPath,
readSource = (path) => readFileSync(path, "utf8"),
) => {
const catalogs = new Map();
const modules = new Map();
const active = new Set();
const complete = new Set();
const moduleFacts = (path) => {
let facts = modules.get(path);
if (facts) return facts;
// Read/parse errors intentionally escape: a broken inventory cannot disable
// the rule by presenting an empty set of catalog bindings.
const ast = parse(readSource(path), {
sourceType: "module",
plugins: ["typescript"],
});
const imports = new Map();
const exports = new Map();
const locals = new Map();
for (const statement of ast.program.body) {
if (statement.type === "ImportDeclaration") {
for (const specifier of statement.specifiers) {
if (specifier.type === "ImportSpecifier")
imports.set(specifier.local.name, {
source: statement.source.value,
name: specifier.imported.name ?? specifier.imported.value,
});
}
}
const declarations =
statement.type === "ExportNamedDeclaration"
? statement.declaration
: statement;
if (declarations?.type === "VariableDeclaration")
for (const declaration of declarations.declarations)
if (declaration.id.type === "Identifier") {
locals.set(declaration.id.name, declaration.init);
if (statement.type === "ExportNamedDeclaration")
exports.set(declaration.id.name, declaration.init);
}
}
facts = { imports, exports, locals };
modules.set(path, facts);
return facts;
};
const visit = (path, name) => {
const key = JSON.stringify([path, name]);
if (complete.has(key)) return;
if (active.has(key))
throw new Error(`Cyclic tool catalog composition: ${path}#${name}`);
active.add(key);
const facts = moduleFacts(path);
const array = arrayExpression(facts.exports.get(name));
if (array?.type !== "ArrayExpression")
throw new Error(`Expected exported tool catalog array: ${path}#${name}`);
const source = withoutExtension(path);
const names = catalogs.get(source) ?? new Set();
names.add(name);
catalogs.set(source, names);
const composition = (raw, localPath = new Set()) => {
const expression = arrayExpression(raw);
if (expression?.type === "ArrayExpression") {
for (const element of expression.elements)
if (element?.type === "SpreadElement")
composition(element.argument, localPath);
return;
}
// Calls such as HOPPER_MEMORY_TOOL_DEFINITIONS.map(official) construct
// entries locally; they are not imported catalog arrays. Do not execute
// or infer catalog ownership from arbitrary computed expressions.
if (expression?.type !== "Identifier") return;
const binding = facts.imports.get(expression.name);
if (binding) {
if (!binding.source.startsWith("."))
throw new Error(`Expected local catalog module: ${path}#${name}`);
visit(
`${withoutExtension(resolve(dirname(path), binding.source))}.ts`,
binding.name,
);
return;
}
if (localPath.has(expression.name))
throw new Error(
`Cyclic local catalog composition: ${path}#${expression.name}`,
);
if (!facts.locals.has(expression.name))
throw new Error(
`Unresolved local catalog spread: ${path}#${expression.name}`,
);
composition(
facts.locals.get(expression.name),
new Set([...localPath, expression.name]),
);
};
composition(array);
active.delete(key);
complete.add(key);
};
visit(entryPath, "TOOL_CONTRACTS");
return catalogs;
};
+3 -6
View File
@@ -15,6 +15,7 @@ import { tsImport } from "tsx/esm/api";
import { PRODUCT_IDENTITY } from "../dist/identity.js";
import { createAgentEvaluationCommands } from "./lib/agent-evaluation-commands.mjs";
import { assertAgentEvaluationReportPassed } from "./lib/agent-evaluation-report-gate.mjs";
import {
closeVerifierResources,
withVerifierCleanup,
@@ -44,7 +45,7 @@ const {
).href,
import.meta.url,
);
const { agentEvaluationPassed, summarizeFactualCorrectness } = await tsImport(
const { summarizeFactualCorrectness } = await tsImport(
new URL(
"../tests/evaluation/model-evals/AgentEvaluationReport.ts",
import.meta.url,
@@ -413,11 +414,7 @@ await withVerifierCleanup(
await writeFile(resolve(reportPath), encoded);
}
const failed = results.filter((result) => !agentEvaluationPassed(result));
if (failed.length > 0)
throw new Error(
`Agent routing/workflow/answer checks failed: ${failed.map(({ id }) => id).join(", ")}`,
);
assertAgentEvaluationReportPassed(results);
},
// Even a deliberately retained fixture directory must never retain auth.
[
+1 -5
View File
@@ -245,11 +245,7 @@ export function dossierSummary(dossier) {
};
}
export async function assertSession(
session,
expectedDigest,
expectedTargetSha256,
) {
export function assertSession(session, expectedDigest, expectedTargetSha256) {
if (
session.provider.id !== "ghidra" ||
session.provider.version !== SUPPORTED_GHIDRA_VERSION ||
+7 -7
View File
@@ -61,12 +61,13 @@ export const snapshotAdbInput = async (
let primaryCause: unknown;
try {
source = new OwnedFileHandle(
await (dependencies.openSource ?? openRegularFile)(sourcePath, {
symlinks: "follow",
signal,
}),
);
const { handle, initial } = await (
dependencies.openSource ?? openRegularFile
)(sourcePath, {
symlinks: "follow",
signal,
});
source = new OwnedFileHandle(handle);
const destinationPath = join(root.path, basename(sourcePath));
destination = new OwnedFileHandle(
await (dependencies.openDestination ?? open)(
@@ -75,7 +76,6 @@ export const snapshotAdbInput = async (
0o600,
),
);
const initial = await source.handle.stat();
if (!Number.isSafeInteger(initial.size) || initial.size < 0)
throw new RangeError(
`Regular file size cannot be represented safely: ${sourcePath}`,
+7 -4
View File
@@ -19,6 +19,7 @@ import {
RegularFileAdmissionFailure,
assertRegularFileActive,
sameRegularFileState,
type StableRegularFileDescriptor,
} from "../filesystem/RegularFile.js";
import type { AndroidOperation } from "../domain/android/androidAnalysis.js";
@@ -173,9 +174,12 @@ const copyAndroidSnapshot = async (
// Close each descriptor through its owner. A rejected close keeps the
// copy outcome and stays retained until that owner is retried; the
// partial file is removed only after those close attempts.
const source = adoptSnapshotHandle(
await openSnapshotSource(sourcePath, signal, retain(sourcePath)),
const { handle, initial } = await openSnapshotSource(
sourcePath,
signal,
retain(sourcePath),
);
const source = adoptSnapshotHandle(handle);
return await withOwnedFileHandle(
sourcePath,
source.owner,
@@ -184,7 +188,6 @@ const copyAndroidSnapshot = async (
...retain(sourcePath),
},
async () => {
const initial = await source.handle.stat();
assertRegularFileActive(signal);
let copiedBytes = 0;
const hash = createHash("sha256");
@@ -286,7 +289,7 @@ const openSnapshotSource = async (
retention: {
readonly retainFailedClose?: (owner: OwnedFileHandle) => void;
},
): Promise<FileHandle> => {
): Promise<StableRegularFileDescriptor> => {
try {
return await openRegularFile(path, { symlinks: "follow", signal });
} catch (cause: unknown) {
+2 -2
View File
@@ -341,7 +341,7 @@ const requireEngineArchive = async (
),
},
options.signal,
async (handle) => {
async ({ handle, initial }) => {
try {
// Metadata enumeration borrows this descriptor. The file workflow
// alone owns admission, final close, cancellation and failed retry.
@@ -349,7 +349,7 @@ const requireEngineArchive = async (
jar,
"zip",
JAR_METADATA_READ_MAX_BYTES,
{ handle, initial: await handle.stat() },
{ handle, initial },
);
const missing = new Set([
"com/atxx/jhmcp/JadxSession.class",
@@ -524,7 +524,7 @@ const registerSignatureInputAdmissionCases = () => {
corrupt.writeUInt32LE(0x06064b51, endOffset);
const restored = Buffer.from(corrupt);
restored.writeUInt32LE(0x06064b50, endOffset);
expect(restored).toEqual(valid);
expect(Buffer.compare(restored, valid)).toBe(0);
for (const [name, bytes] of [
["valid", valid],
["restored", restored],
+7 -4
View File
@@ -18,10 +18,13 @@ it("binds readable certificate fields to the exact carried DER despite embedded
createHash("sha256").update(outer).digest("hex"),
);
expect(
new X509Certificate(
`-----BEGIN CERTIFICATE-----\n${Buffer.from(outer).toString("base64")}\n-----END CERTIFICATE-----\n`,
).raw,
).toEqual(Buffer.from(outer));
Buffer.compare(
new X509Certificate(
`-----BEGIN CERTIFICATE-----\n${Buffer.from(outer).toString("base64")}\n-----END CERTIFICATE-----\n`,
).raw,
Buffer.from(outer),
),
).toBe(0);
}
});
it.each([
+1 -1
View File
@@ -250,7 +250,7 @@ sdkInfo:
coverage: "complete",
},
});
expect(await readFile(apk.path)).toEqual(original);
expect(Buffer.compare(await readFile(apk.path), original)).toBe(0);
});
it.each([
@@ -192,11 +192,17 @@ it.each([false, true])(
await fs.writeFile(path, Buffer.from([0xff]));
vi.mocked(fs.open).mockImplementation(async (name, flags, mode) => {
const handle = await actual.open(name, flags, mode);
const read = handle.read.bind(handle);
let readComplete = false;
handle.read = (async (...args: Parameters<typeof handle.read>) => {
const value = await read(...args);
if (value.bytesRead === 0) readComplete = true;
return value;
}) as typeof handle.read;
const stat = handle.stat.bind(handle);
let calls = 0;
handle.stat = ((...args: Parameters<typeof handle.stat>) =>
stat(...args).then((value) => {
if (++calls === 3)
if (readComplete)
controller.abort({ caller: "after bytes before interpretation" });
return value;
})) as typeof handle.stat;
+2 -4
View File
@@ -68,9 +68,8 @@ export class ApktoolWorkspace {
path,
this.#files,
signal,
async (source) => {
async ({ handle: source, initial }) => {
admitted = true;
const initial = await source.stat();
const output = join(this.path, "input.apk");
const file = await open(output, "wx", 0o600);
await withArtifactFileHandle(
@@ -180,9 +179,8 @@ export class ApktoolWorkspace {
path,
this.#files,
signal,
async (file) => {
async ({ handle: file, initial }) => {
admitted = true;
const initial = await file.stat();
const bytes = Buffer.alloc(budget + 1);
let length = 0;
while (length < bytes.length) {
+5
View File
@@ -11,6 +11,7 @@ import type { AnalysisProfileCommitment } from "../domain/analysisProfile.js";
import type { JsonValue } from "../domain/jsonValue.js";
import type { Result } from "../domain/result.js";
import type {
Evidence,
EvidenceLocation,
EvidenceSubjectTarget,
} from "../domain/evidence.js";
@@ -72,6 +73,7 @@ interface CreateAnalysisExecutionOptions {
readonly relationshipCoverage?: RelationshipCoverage;
readonly referencesTruncated?: boolean;
readonly locations?: readonly EvidenceLocation[];
readonly sourceEvidence?: readonly Evidence[];
readonly subject?: EvidenceSubjectTarget | null;
}
@@ -122,6 +124,9 @@ export function createAnalysisExecution(
? {}
: { referencesTruncated: options.referencesTruncated }),
locations: [...(options.locations ?? [])],
...(options.sourceEvidence === undefined
? {}
: { sourceEvidence: Object.freeze([...options.sourceEvidence]) }),
...(options.subject === undefined ? {} : { subject: options.subject }),
};
}
+6 -2
View File
@@ -78,12 +78,16 @@ export const resolveAppBundleExecutable = async (
try {
const layout = await presentLayout(path);
if (layout !== undefined)
return resolveLayoutExecutable(path, layout, fileSystem);
return await resolveLayoutExecutable(path, layout, fileSystem);
const wrapped = await wrappedBundle(path);
if (wrapped !== undefined) {
const wrappedLayout = await presentLayout(wrapped);
if (wrappedLayout !== undefined)
return resolveLayoutExecutable(wrapped, wrappedLayout, fileSystem);
return await resolveLayoutExecutable(
wrapped,
wrappedLayout,
fileSystem,
);
}
} catch (cause: unknown) {
if (cause instanceof BinaryTargetError) return err(cause);
+17 -15
View File
@@ -26,7 +26,10 @@ import {
import { extname } from "node:path";
import { BinaryTargetError } from "../domain/configurationErrors.js";
import { sameRegularFileState } from "../filesystem/RegularFile.js";
import {
sameRegularFileState,
type StableRegularFileDescriptor,
} from "../filesystem/RegularFile.js";
import { AnalysisCancelledError } from "../domain/analysisErrorCore.js";
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -153,10 +156,10 @@ export const parseBinaryTarget = async (
symlinks: "reject",
...(signal === undefined ? {} : { signal }),
},
async (handle) => {
async (source) => {
try {
return await readOpenedBinaryTarget(
handle,
source,
{ path, sourcePath, ...bundle },
options,
);
@@ -232,7 +235,7 @@ type TargetResolution = Result<
/** Classify and hash the one descriptor admitted by the lifetime owner. */
const readOpenedBinaryTarget = async (
handle: FileHandle,
source: StableRegularFileDescriptor,
selected: {
readonly path: string;
readonly sourcePath: string;
@@ -240,13 +243,12 @@ const readOpenedBinaryTarget = async (
},
options: BinaryTargetResolverOptions,
): Promise<TargetResolution> => {
const { handle, initial: metadata } = source;
const { path, sourcePath, bundleInfoPlist } = selected;
const bundle = bundleInfoPlist === undefined ? {} : { bundleInfoPlist };
const hostArchitecture = options.hostArchitecture ?? process.arch;
const { targetKind, formatHint, signal, architecture, entryPoint } = options;
throwIfTargetResolutionCancelled(signal);
const metadata = await handle.stat();
throwIfTargetResolutionCancelled(signal);
if (formatHint === "raw-binary") {
if (targetKind !== undefined && targetKind !== "executable")
return err(
@@ -366,7 +368,7 @@ const readOpenedBinaryTarget = async (
const artifactFormat =
targetKind === "database"
? undefined
: await detectArtifactFormat(path, handle, signal);
: await detectArtifactFormat(path, source, signal);
if (
targetKind === "database" ||
(targetKind === undefined && path.toLowerCase().endsWith(".hop"))
@@ -389,7 +391,7 @@ const readOpenedBinaryTarget = async (
: ok({ ...identity, kind: "artifact", format: artifactFormat });
}
const detected = await readExecutableMetadata(
handle,
source,
hostArchitecture,
signal,
);
@@ -616,7 +618,7 @@ const throwIfTargetResolutionCancelled = (signal?: AbortSignal): void => {
const detectArtifactFormat = async (
path: string,
handle: FileHandle,
source: StableRegularFileDescriptor,
signal?: AbortSignal,
): Promise<
| Exclude<
@@ -633,6 +635,7 @@ const detectArtifactFormat = async (
>
| undefined
> => {
const { handle, initial } = source;
throwIfTargetResolutionCancelled(signal);
const lower = path.toLowerCase();
const magic = Buffer.alloc(4096);
@@ -664,8 +667,7 @@ const detectArtifactFormat = async (
return "pkg";
if (lower.endsWith(".dmg")) {
throwIfTargetResolutionCancelled(signal);
const size = (await handle.stat()).size;
throwIfTargetResolutionCancelled(signal);
const size = initial.size;
if (size >= 512) {
const trailer = Buffer.alloc(4);
const read = await handle.read(trailer, 0, trailer.length, size - 512);
@@ -696,7 +698,7 @@ const detectArtifactFormat = async (
if (xmlState === "matched") return "plist";
}
if (bytesRead === MAX_XML_PLIST_PROBE_BYTES) {
const { size } = await handle.stat();
const { size } = initial;
throwIfTargetResolutionCancelled(signal);
if (size > bytesRead)
throw new BinaryTargetError(
@@ -837,10 +839,11 @@ const resolveAppBundle = async (
};
const readExecutableMetadata = async (
handle: FileHandle,
source: StableRegularFileDescriptor,
hostArchitecture: NodeJS.Architecture,
signal?: AbortSignal,
): Promise<Result<ExecutableMetadata, ExecutableMetadataFailure>> => {
const { handle, initial } = source;
throwIfTargetResolutionCancelled(signal);
const prefix = Buffer.alloc(4096);
const prefixRead = await handle.read(prefix, 0, prefix.length, 0);
@@ -848,8 +851,7 @@ const readExecutableMetadata = async (
const bytes = prefix.subarray(0, prefixRead.bytesRead);
// Header commitments such as Mach-O load commands and FAT slices are
// checked against the whole file, not the probed prefix.
const fileSize = (await handle.stat()).size;
throwIfTargetResolutionCancelled(signal);
const fileSize = initial.size;
if (bytes.length >= 2 && bytes[0] === 0x4d && bytes[1] === 0x5a) {
let evidence = parseMzHeaderEvidence(bytes, fileSize);
let signatureEvidence:
+20 -8
View File
@@ -4,7 +4,6 @@ import {
type Evidence,
type EvidenceObservation,
} from "../domain/evidence.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { sanitizeBrowserUrl } from "../domain/browserObservation.js";
import type {
InspectWebPageInput,
@@ -16,6 +15,7 @@ import type {
} from "../domain/browserObservationSchemas.js";
import type {
AnalyzeWebBundleInput,
WebBundleAcquisitionPartialObservation,
WebBundleAnalysis,
} from "../domain/webBundleAnalysis.js";
import type {
@@ -51,6 +51,7 @@ type BrowserEvidenceResult =
| BrowserTargetList
| WebPageInspection
| WebBundleAnalysis
| WebBundleAcquisitionPartialObservation
| WebObservationSession
| WebMcpDiscovery
| BrowserCaptureComparison
@@ -66,36 +67,47 @@ type BrowserEvidenceOperation =
| "capture_web_screenshot"
| "compare_web_screenshots";
const isBundleAcquisition = (
result: BrowserEvidenceResult,
): result is WebBundleAcquisitionPartialObservation =>
"kind" in result && result.kind === "web-bundle-acquisition";
/** Create Evidence for one request-scoped external browser observation. */
export const createBrowserEvidence = (
operation: BrowserEvidenceOperation,
input: BrowserEvidenceInput,
result: BrowserEvidenceResult,
provider: ProviderIdentity,
): Evidence =>
createEvidence(undefined, provider, {
): Evidence => {
const acquisition = isBundleAcquisition(result);
const observed = acquisition ? result.inspection : result;
return createEvidence(undefined, provider, {
predicateType: browserPredicate(operation),
operation,
parameters: browserParameters(input, result),
result: jsonValueSchema.parse(result),
result,
confidence: "observed",
authority: "external-service",
environment:
"browser" in result
"browser" in observed
? {
id: `${result.browser.product}@${result.browser.revision}`,
id: `${observed.browser.product}@${observed.browser.revision}`,
platform: "unknown",
architecture: "unknown",
isolation: "none",
}
: null,
limitations: [
...result.limitations,
...("browser" in result
...observed.limitations,
...(acquisition && result.source_maps.limitation !== undefined
? [result.source_maps.limitation]
: []),
...("browser" in observed
? ["Browser host platform and architecture were not observed."]
: []),
],
});
};
const browserParameters = (
input: BrowserEvidenceInput,
@@ -163,6 +163,47 @@ it("retains captured page Evidence when bundle analysis fails after capture", as
}
});
it("projects completed source-map acquisition without claiming bundle analysis", async () => {
const inspection = passiveNetworkCapture();
const partial = {
kind: "web-bundle-acquisition" as const,
inspection,
source_maps: {
status: "unavailable" as const,
requested: 1,
processed: 0,
items: [],
limitation: "One requested source map was not processed.",
},
analysis_status: "not_performed" as const,
};
const reason = { requested: "stop after capture" };
const failure = new AnalysisCancelledError("analyze_web_bundle", {
cause: reason,
partialObservation: partial,
});
const browser: BrowserObservationPort = {
...provider(async () => ok(report)),
analyzeBundle: async () => err(failure),
};
const selected = analyzeWebBundleInputSchema.parse({
...input,
cdp_endpoint: input.cdp_endpoint.recorded,
target_id: "page",
fetch_source_maps: true,
});
const result = await analyzeWebBundle(browser, selected);
expect(result.ok).toBe(false);
if (result.ok) return;
expect(result.error).toBe(failure);
expect(result.error.cause).toBe(reason);
expect(parseEvidence(result.error.partialObservation)).toMatchObject({
operation: "analyze_web_bundle",
parameters: expect.objectContaining({ fetch_source_maps: true }),
normalized_result: partial,
});
});
it("retains completed bundle facts as bundle Evidence after cancellation", async () => {
const { AnalysisCancelledError } =
await import("../domain/analysisErrorCore.js");
@@ -4,6 +4,7 @@ import type {
ListBrowserTargetsInput,
} from "../domain/browserObservation.js";
import {
webBundleAcquisitionPartialObservationSchema,
webBundleAnalysisSchema,
type AnalyzeWebBundleInput,
} from "../domain/webBundleAnalysis.js";
@@ -288,6 +289,24 @@ const retainBrowserObservation = (
errorOperation: "inspect_web_page" | "analyze_web_bundle",
): Result<Evidence, AnalysisError> => {
const partial = error.partialObservation;
if (
errorOperation === "analyze_web_bundle" &&
partial !== undefined &&
"kind" in partial &&
partial.kind === "web-bundle-acquisition"
) {
const acquisition =
webBundleAcquisitionPartialObservationSchema.safeParse(partial);
if (acquisition.success) {
error.partialObservation = createBrowserEvidence(
"analyze_web_bundle",
input,
acquisition.data,
provider.identity(),
);
return err(error);
}
}
if (
errorOperation === "analyze_web_bundle" &&
partial !== undefined &&
+2 -2
View File
@@ -7,7 +7,7 @@ import {
type Evidence,
type EvidenceObservation,
} from "../domain/evidence.js";
import { jsonObjectSchema, jsonValueSchema } from "../domain/jsonValue.js";
import { jsonObjectSchema } from "../domain/jsonValue.js";
const browserScenarioParameters = (
scenario: BrowserScenario,
@@ -26,7 +26,7 @@ export const createBrowserScenarioEvidence = (
predicateType: "rea.browser-scenario-capture",
operation: "capture_browser_scenario",
parameters: browserScenarioParameters(scenario),
result: jsonValueSchema.parse(capture),
result: capture,
confidence: "observed",
authority: "controlled-replay",
environment: {
+6 -1
View File
@@ -24,6 +24,7 @@ import {
import {
inspectClientRegistrationCommand,
type ClientRegistrationCommandFacts,
type ClientRegistrationCommandIdentity,
packageRunnerRegistrationCommand,
} from "./ClientRegistrationIdentity.js";
import {
@@ -94,6 +95,8 @@ interface ClientRegistrationReadOptions {
export interface ClientRegistrationInspection {
readonly status: ClientRegistrationStatus;
readonly activation: "enabled" | "disabled" | "unknown";
/** Ownership remains unknown until this inspection has parsed the launcher. */
readonly commandIdentity: ClientRegistrationCommandIdentity;
}
/** Project diagnostics from the same inspection used to select maintenance. */
@@ -118,8 +121,9 @@ export const readClientRegistrationInspections = async (
const record = (
status: ClientRegistrationStatus,
activation: ClientRegistrationInspection["activation"] = "unknown",
commandIdentity: ClientRegistrationCommandIdentity = "unknown",
): void => {
inspections.push({ status, activation });
inspections.push({ status, activation, commandIdentity });
};
for (const client of supportedClients(
home,
@@ -330,6 +334,7 @@ export const readClientRegistrationInspections = async (
: undefined,
),
activation,
commandFacts.identity,
);
} catch (cause: unknown) {
if (
+10 -27
View File
@@ -8,10 +8,7 @@ import type { SessionProviderRoute } from "./binary/SessionProviderRouter.js";
import type { ResolvedSessionOpen } from "./binary/BinarySessionOpen.js";
import { silentLogger } from "../logger.js";
import type { Logger } from "pino";
import {
createEvidence,
withEvidenceCallerInputPath,
} from "../domain/evidence.js";
import { withEvidenceCallerInputPath } from "../domain/evidence.js";
import { createImmutableEvidenceBundle } from "../domain/evidenceBundle.js";
import type { Evidence } from "../domain/evidence.js";
import type { NativeToolName } from "../contracts/native/nativeToolContracts.js";
@@ -58,7 +55,6 @@ import {
recordWorkflowUnknowns,
} from "./WorkflowEvidence.js";
import type { AnalysisProviderSelector } from "../contracts/providerSelection.js";
import { artifactInspectionResultSchema } from "../domain/artifactInspection.js";
import { resolveXrefsAddress } from "./XrefsAddressResolution.js";
import {
AnalysisCancelledError,
@@ -644,30 +640,17 @@ const executeAnalysisTool = async (input: {
);
}
if (!result.ok) return result;
const evidence = createEvidence(
result.value.subject === undefined
? input.openedTarget
: (result.value.subject ?? undefined),
result.value.provider,
const evidence = createAnalysisExecutionEvidence(
tool,
input.evidenceParameters,
{
operation: tool,
parameters: input.evidenceParameters,
result: result.value.result,
...(result.value.analysisProfile === undefined
? {}
: { analysisProfile: result.value.analysisProfile }),
rawResult: result.value.rawResult,
limitations: result.value.limitations,
...(result.value.relationshipCoverage === undefined
? {}
: { relationshipCoverage: result.value.relationshipCoverage }),
locations: result.value.locations,
evidenceLinks:
tool === "inspect_artifact"
? artifactInspectionResultSchema.parse(result.value.result)
.evidence_links
: [],
...result.value,
subject:
result.value.subject === undefined
? input.openedTarget
: result.value.subject,
},
"observed",
);
return ok({ evidence });
};
+79 -19
View File
@@ -12,6 +12,16 @@ import { HopperReleaseError } from "./HopperReleaseFeed.js";
import { MAC_HOPPER_SYSTEM_LAUNCHER } from "./MacHopperPaths.js";
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
const configurationHost = (
environment: Readonly<NodeJS.ProcessEnv>,
): DoctorHost => ({
...systemDoctorHost({ environment: { ...environment } }),
...host({
installationPaths: () => Promise.resolve([]),
clientRegistrations: () => Promise.resolve([]),
}),
});
describe("doctor Hopper startup timeout", () => {
const timeoutHost = (
value: string | undefined,
@@ -157,16 +167,6 @@ describe("doctor Hopper startup timeout", () => {
});
describe("doctor native analysis configuration", () => {
const configurationHost = (
environment: Readonly<NodeJS.ProcessEnv>,
): DoctorHost => ({
...systemDoctorHost({ environment: { ...environment } }),
...host({
installationPaths: () => Promise.resolve([]),
clientRegistrations: () => Promise.resolve([]),
}),
});
it.each([
["REA_ANALYSIS_PROVIDER", "Hopper"],
["REA_REGEX_MATCHING_DEADLINE_MS", "0"],
@@ -202,9 +202,78 @@ describe("doctor native analysis configuration", () => {
details: { setting },
}),
);
expect(
result.checks.some(({ name }) => name === "hopper-startup-timeout"),
).toBe(false);
},
);
it.each([
["REA_GHIDRA_STARTUP_TIMEOUT_MS", "abc"],
["HOPPER_LOADER_ARGS_JSON", "{"],
["REA_GHIDRA_COMPILER_SPEC_ID", "gcc"],
["CALLER_SETTING", "private\0value"],
] as const)(
"keeps a valid supplied Hopper timeout when %s rejects native admission",
async (setting, value) => {
const result = await runDoctor(
undefined,
configurationHost({
[setting]: value,
REA_HOPPER_STARTUP_TIMEOUT_MS: "120001",
}),
{ providers: ["hopper"] },
);
expect(result.healthy).toBe(false);
expect(result.scope_checks).toContainEqual(
expect.objectContaining({
name: "hopper-startup-timeout",
ok: true,
detail: "REA_HOPPER_STARTUP_TIMEOUT_MS=120001",
}),
);
expect(result.scope_checks).toContainEqual(
expect.objectContaining({
name: `runtime-configuration:${setting}`,
ok: false,
}),
);
},
);
it("reports an invalid Hopper timeout alongside an unrelated NUL admission refusal", async () => {
const result = await runDoctor(
undefined,
configurationHost({
CALLER_SETTING: "private\0value",
REA_HOPPER_STARTUP_TIMEOUT_MS: "0",
}),
{ providers: ["hopper"] },
);
expect(result.healthy).toBe(false);
expect(result.scope_checks).toContainEqual(
expect.objectContaining({
name: "hopper-startup-timeout",
ok: false,
detail:
"REA_HOPPER_STARTUP_TIMEOUT_MS: must be a positive decimal integer",
}),
);
expect(result.scope_checks).toContainEqual(
expect.objectContaining({
name: "runtime-configuration:CALLER_SETTING",
ok: false,
detail: expect.stringContaining("NUL"),
}),
);
expect(
result.scope_checks.filter(
({ name }) => name === "hopper-startup-timeout",
),
).toHaveLength(1);
expect(JSON.stringify(result.checks)).not.toContain("private");
});
it("keeps a lowercase provider preference ready for the selected engine", async () => {
const result = await runDoctor(
undefined,
@@ -257,15 +326,6 @@ describe("doctor native analysis configuration", () => {
});
describe("doctor MCP startup readiness", () => {
const configurationHost = (
environment: Readonly<NodeJS.ProcessEnv>,
): DoctorHost => ({
...systemDoctorHost({ environment: { ...environment } }),
...host({
installationPaths: () => Promise.resolve([]),
clientRegistrations: () => Promise.resolve([]),
}),
});
const alignedCodex: ClientRegistrationStatus = {
client: "codex",
config_path: "/home/user/.codex/config.toml",
+7 -12
View File
@@ -11,9 +11,11 @@ import { constants } from "node:fs";
import { access, stat } from "node:fs/promises";
import { ArtifactResourceScope } from "../artifacts/ArtifactResourceScope.js";
import type { RetainedResourceScope } from "./AnalysisProvider.js";
import { nativeAnalysisConfigurationError } from "../config/parseConfig.js";
import {
inspectNativeAnalysisConfiguration,
type NativeAnalysisConfigurationInspection,
} from "../config/parseConfig.js";
import { homeDirectoryFromEnvironment } from "../config/homeDirectory.js";
import type { ConfigurationSettingIssue } from "../domain/configurationErrors.js";
import { snapshotEnvironment } from "../process/snapshotEnvironment.js";
import { join } from "node:path";
@@ -108,12 +110,11 @@ export interface DoctorHost {
readonly runtime: JavaScriptRuntime;
readonly homeDirectory: string;
readonly configuredHopperPath?: string;
readonly configuredHopperStartupTimeout?: string;
/**
* Settings `parseConfig` rejects before native analysis starts.
* Undefined means this host did not run that admission.
*/
readonly configurationIssues?: readonly ConfigurationSettingIssue[];
readonly nativeConfiguration?: NativeAnalysisConfigurationInspection;
readonly configuredIlspyCmdPath?: string;
macosVersion(): Promise<string | undefined>;
linuxDistribution(): Promise<LinuxDistribution | undefined>;
@@ -539,7 +540,7 @@ export const systemDoctorHost = (
const platform = options.platform ?? process.platform;
const architecture = options.architecture ?? process.arch;
const environment = options.environment ?? process.env;
const configurationError = nativeAnalysisConfigurationError(
const nativeConfiguration = inspectNativeAnalysisConfiguration(
snapshotEnvironment(environment, platform),
);
const hostExecFileOutput = options.execFileOutput ?? execFileOutput;
@@ -576,13 +577,7 @@ export const systemDoctorHost = (
...(environment.HOPPER_LAUNCHER_PATH === undefined
? {}
: { configuredHopperPath: environment.HOPPER_LAUNCHER_PATH }),
...(environment.REA_HOPPER_STARTUP_TIMEOUT_MS === undefined
? {}
: {
configuredHopperStartupTimeout:
environment.REA_HOPPER_STARTUP_TIMEOUT_MS,
}),
configurationIssues: configurationError?.settings ?? [],
nativeConfiguration,
...(environment.REA_ILSPY_CMD_PATH === undefined
? {}
: { configuredIlspyCmdPath: environment.REA_ILSPY_CMD_PATH }),
+7 -10
View File
@@ -2,7 +2,6 @@ import {
SUPPORTED_NODE_VERSION_PROSE,
supportsJavaScriptRuntime,
} from "../domain/runtimeVersion.js";
import { providerStartupTimeoutSchema } from "../config/providerStartupTimeout.js";
import { hopperLauncherDiscovery } from "./HopperLauncherDiscovery.js";
import { LINUX_HOPPER_SETUP_HOSTS, supportsReaHost } from "./LinuxHopper.js";
import type {
@@ -193,16 +192,14 @@ const hopperCheck = (
const optionalHopperStartupTimeoutCheck = (
host: DoctorHost,
): DoctorCheck | undefined => {
if (host.configuredHopperStartupTimeout === undefined) return undefined;
const parsed = providerStartupTimeoutSchema.safeParse(
host.configuredHopperStartupTimeout,
);
const parsed = host.nativeConfiguration?.configuredHopperStartupTimeout;
if (parsed === undefined) return undefined;
return check(
"hopper-startup-timeout",
parsed.success,
parsed.success
? `REA_HOPPER_STARTUP_TIMEOUT_MS=${String(parsed.data)}`
: `REA_HOPPER_STARTUP_TIMEOUT_MS: ${parsed.error.issues.map(({ message }) => message).join("; ")}`,
parsed.ok,
parsed.ok
? `REA_HOPPER_STARTUP_TIMEOUT_MS=${String(parsed.value)}`
: `REA_HOPPER_STARTUP_TIMEOUT_MS: ${parsed.error.settings.map(({ constraint }) => constraint).join("; ")}`,
{
classification: "config_drift",
remediation:
@@ -219,7 +216,7 @@ const optionalHopperStartupTimeoutCheck = (
const runtimeConfigurationChecks = (
host: DoctorHost,
): readonly DoctorCheck[] => {
const issues = host.configurationIssues;
const issues = host.nativeConfiguration?.issues;
if (issues === undefined || issues.length === 0) return [];
const grouped: { setting: string; constraints: string[] }[] = [];
for (const issue of issues) {
+26
View File
@@ -277,6 +277,32 @@ describe("Linux Hopper installation", () => {
);
},
);
it.each([true, false])(
"keeps installed apt dependencies at their versions while Hopper may upgrade (root: %s)",
(isRoot) => {
const prefix = isRoot ? [] : ["pkexec"];
expect(
linuxPackageManagerCommands("deb", "/tmp/Hopper package", isRoot).map(
({ executable, args }) => [executable, ...args],
),
).toEqual([
[
...prefix,
"apt-get",
"install",
"-y",
"--no-upgrade",
"xvfb",
"xauth",
"python3",
"libx11-6",
"libxtst6",
],
[...prefix, "apt-get", "install", "-y", "/tmp/Hopper package"],
]);
},
);
});
describe("Linux Hopper package installation", () => {
+6 -1
View File
@@ -818,7 +818,12 @@ export const linuxPackageManagerCommands = (
: ["xorg-server-xvfb", "xorg-xauth", "python", "libx11", "libxtst"];
const commands =
family === "deb"
? [["apt-get", "install", "-y", archive, ...runtimeDependencies]]
? [
// apt-get install upgrades listed packages that are already
// installed, so dependencies are not named as upgrade targets.
["apt-get", "install", "-y", "--no-upgrade", ...runtimeDependencies],
["apt-get", "install", "-y", archive],
]
: family === "rpm"
? [["dnf", "install", "-y", archive, ...runtimeDependencies]]
: [
@@ -6,9 +6,17 @@ import { err, ok } from "../domain/result.js";
import * as pythonRuntime from "./LinuxHopperPython.js";
import {
linuxHopperInstallDisclosure,
linuxPackageManagerCommands,
systemLinuxHopperInstallHost,
} from "./LinuxHopper.js";
/** Package transactions that install Hopper before any runtime library repair. */
const debCommands = linuxPackageManagerCommands(
"deb",
"/tmp/verified-hopper.deb",
false,
).length;
const missingPython = {
interpreter: "/usr/bin/python3",
version: [3, 14],
@@ -37,13 +45,13 @@ describe("Linux Hopper package runtime", () => {
{
name: "installs and verifies the matching future Python library",
inspections: [ok(missingPython), ok(readyPython)],
packageCalls: 2,
packageCalls: debCommands + 1,
status: "installed",
},
{
name: "skips the extra APT command for an already loadable library",
inspections: [ok(readyPython)],
packageCalls: 1,
packageCalls: debCommands,
status: "installed",
},
{
@@ -54,13 +62,13 @@ describe("Linux Hopper package runtime", () => {
library: { status: "unknown", reason: "No INSTSONAME" },
} satisfies pythonRuntime.LinuxHopperPythonRuntime),
],
packageCalls: 1,
packageCalls: debCommands,
status: "runtime_unavailable",
},
{
name: "rejects APT success when the library still cannot load",
inspections: [ok(missingPython), ok(missingPython)],
packageCalls: 2,
packageCalls: debCommands + 1,
status: "runtime_unavailable",
},
{
@@ -73,7 +81,7 @@ describe("Linux Hopper package runtime", () => {
),
),
],
packageCalls: 1,
packageCalls: debCommands,
status: "runtime_unavailable",
},
])("$name", async ({ inspections, packageCalls, status }) => {
@@ -97,9 +105,9 @@ describe("Linux Hopper package runtime", () => {
).toMatchObject({ status });
expect(command).toHaveBeenCalledTimes(packageCalls);
expect(inspect).toHaveBeenCalledTimes(inspections.length);
if (packageCalls === 2) {
if (packageCalls > debCommands) {
const isRoot = process.getuid?.() === 0;
expect(command.mock.calls[1]?.slice(0, 2)).toEqual([
expect(command.mock.calls[debCommands]?.slice(0, 2)).toEqual([
isRoot ? "apt-get" : "pkexec",
[...(isRoot ? [] : ["apt-get"]), "install", "-y", "libpython3.14"],
]);
@@ -128,10 +136,10 @@ describe("Linux Hopper package runtime", () => {
stderr: "original package error",
outputTruncated: true,
};
const command = vi
.spyOn(boundedCommands, "execFileBoundedOutput")
.mockResolvedValueOnce(commandSuccess)
.mockResolvedValueOnce(failed);
const command = vi.spyOn(boundedCommands, "execFileBoundedOutput");
for (let index = 0; index < debCommands; index++)
command.mockResolvedValueOnce(commandSuccess);
command.mockResolvedValueOnce(failed);
const inspect = vi
.spyOn(pythonRuntime, "inspectLinuxHopperPythonRuntime")
.mockResolvedValue(ok(missingPython));
@@ -139,7 +147,7 @@ describe("Linux Hopper package runtime", () => {
expect(
await host.installPackage("deb", "/tmp/verified-hopper.deb"),
).toMatchObject({ status: "failed", failure: failed });
expect(command).toHaveBeenCalledTimes(2);
expect(command).toHaveBeenCalledTimes(debCommands + 1);
expect(inspect).toHaveBeenCalledTimes(1);
} finally {
try {
@@ -184,7 +192,7 @@ describe("Linux Hopper package mapping", () => {
message: expect.stringContaining(path),
},
});
expect(command).toHaveBeenCalledTimes(1);
expect(command).toHaveBeenCalledTimes(debCommands);
expect(inspect).toHaveBeenCalledTimes(1);
} finally {
try {
@@ -197,13 +205,62 @@ describe("Linux Hopper package mapping", () => {
});
});
describe("Linux Hopper package transaction failures", () => {
it.each(Array.from({ length: debCommands }, (_, index) => index + 1))(
"stops after failed install transaction %i without inspecting Python",
async (failing) => {
const directory = await createTestTempDirectory(
"rea-linux-install-failure-",
);
const host = systemLinuxHopperInstallHost(
{ XDG_CACHE_HOME: directory },
async () => undefined,
);
const failed = {
exitCode: 100,
stdout: "",
stderr: "transaction " + String(failing) + " failed",
outputTruncated: false,
};
let calls = 0;
const command = vi
.spyOn(boundedCommands, "execFileBoundedOutput")
.mockImplementation(async () =>
++calls === failing ? failed : commandSuccess,
);
const inspect = vi.spyOn(
pythonRuntime,
"inspectLinuxHopperPythonRuntime",
);
try {
expect(
await host.installPackage("deb", "/tmp/verified-hopper.deb"),
).toMatchObject({
status: "failed",
failure: { exitCode: 100, stderr: failed.stderr },
});
expect(command).toHaveBeenCalledTimes(failing);
expect(inspect).not.toHaveBeenCalled();
} finally {
try {
await host.close?.();
} finally {
inspect.mockRestore();
command.mockRestore();
}
}
},
);
});
describe("Linux Hopper package cancellation and disclosure", () => {
it.each([
{ phase: "before", commands: 0, inspections: 0 },
{ phase: "first transaction", commands: 1, inspections: 0 },
{ phase: "query", commands: 1, inspections: 1 },
{ phase: "runtime transaction", commands: 2, inspections: 1 },
{ phase: "verification", commands: 2, inspections: 2 },
{ phase: "archive transaction", commands: debCommands, inspections: 0 },
{ phase: "query", commands: debCommands, inspections: 1 },
{ phase: "runtime transaction", commands: debCommands + 1, inspections: 1 },
{ phase: "verification", commands: debCommands + 1, inspections: 2 },
])(
"honors cancellation at $phase without starting the next stage",
async ({ phase, commands, inspections }) => {
@@ -224,7 +281,9 @@ describe("Linux Hopper package cancellation and disclosure", () => {
commandCount++;
if (
(phase === "first transaction" && commandCount === 1) ||
(phase === "runtime transaction" && commandCount === 2)
(phase === "archive transaction" && commandCount === debCommands) ||
(phase === "runtime transaction" &&
commandCount === debCommands + 1)
)
controller.abort(cause);
return commandSuccess;
@@ -267,8 +326,8 @@ describe("Linux Hopper package cancellation and disclosure", () => {
it("discloses the conditional privileged library install without returning probe code", () => {
const disclosure = linuxHopperInstallDisclosure("deb", false);
expect(disclosure.commands).toHaveLength(2);
expect(disclosure.commands[1]).toBe(
expect(disclosure.commands).toHaveLength(debCommands + 1);
expect(disclosure.commands[debCommands]).toBe(
'pkexec apt-get install -y "<libpython matching /usr/bin/python3, if not loadable>"',
);
expect(linuxHopperInstallDisclosure("rpm", false).commands).toHaveLength(1);
+1 -1
View File
@@ -772,7 +772,7 @@ export class LinuxHopperPublisher {
{ cause },
);
}
return this.#verifyArchive(
return await this.#verifyArchive(
parsed.data.family,
release.value,
join(root, "archive"),
@@ -3,7 +3,6 @@ import { describe, expect, it } from "vitest";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { createEvidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { javascriptApplicationAnalysisResultSchema } from "../domain/javascript/javascriptApplicationAnalysis.js";
import { artifactEvidence } from "../domain/javascript/javascriptApplicationGraph.fixture.js";
import {
@@ -108,7 +107,7 @@ const semanticApplication = (
predicateType: example.predicate_type,
operation: example.operation,
parameters: example.parameters,
result: jsonValueSchema.parse({ ...base, semantic_graph: graph }),
result: { ...base, semantic_graph: graph },
confidence: "derived",
authority: "shipped-artifact",
},
@@ -13,8 +13,10 @@ import { describe, expect, it } from "vitest";
import { createEvidence } from "../domain/evidence.js";
import { createEvidenceBundle } from "../domain/evidenceBundle.js";
import { emptyUnverifiedProcessCapture } from "../domain/process/processCapture.fixture.js";
import { FINALIZED_PROCESS_CAPTURE_EXAMPLE } from "../domain/process/processCaptureExample.js";
import {
emptyUnverifiedProcessCapture,
FINALIZED_PROCESS_CAPTURE_EXAMPLE,
} from "../domain/process/processCapture.fixture.js";
import { PROCESS_PROVIDER } from "../domain/process/processEvidenceProvider.js";
import { parseProcessCapture } from "../domain/process/processCaptureParsing.js";
import {
@@ -33,7 +35,6 @@ import {
createJavaScriptApplicationGraph,
createJavaScriptApplicationNode,
} from "../domain/javascript/javascriptApplicationGraph.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { MANAGED_WORKFLOW_PROVIDER } from "../domain/investigationProviders.js";
import { createProcessCaptureEvidence } from "./process/ProcessEvidence.js";
@@ -90,7 +91,7 @@ const applicationWithUnknowns = (
predicateType: "rea.managed-application-graph",
operation: "project_managed_application_graph",
parameters: {},
result: jsonValueSchema.parse({
result: {
projection_id: `magp_${"a".repeat(64)}`,
root_artifact_sha256: APPLICATION_GRAPH_DIGESTS.asar,
source_evidence: {
@@ -113,7 +114,7 @@ const applicationWithUnknowns = (
coverage: { status: "complete-within-inputs" },
evidence_links: [source.evidence_id],
limitations: [],
}),
},
confidence: "inferred",
authority: "analyst-inference",
evidenceLinks: [source.evidence_id],
@@ -212,7 +213,7 @@ describe("reconstruction obligation candidates", () => {
predicateType: "rea.process-capture",
operation: "capture_process_scenario",
parameters: {},
result: jsonValueSchema.parse({
result: {
...rest,
manifest,
exit: {
@@ -224,7 +225,7 @@ describe("reconstruction obligation candidates", () => {
],
},
},
}),
},
});
const generated = deriveCandidates(createEvidenceBundle([evidence]), []);
@@ -308,7 +309,7 @@ it("keeps each semantic candidate bound to its own file and authority context",
predicateType: example.predicate_type,
operation: example.operation,
parameters: example.parameters,
result: jsonValueSchema.parse({ ...base, semantic_graph: semanticGraph }),
result: { ...base, semantic_graph: semanticGraph },
confidence: "derived",
authority: "shipped-artifact",
evidenceLinks: sourceRecords.map(({ evidence_id }) => evidence_id),
@@ -415,7 +416,7 @@ it("falls back to a kind-based title when a semantic node label is blank", () =>
predicateType: example.predicate_type,
operation: example.operation,
parameters: example.parameters,
result: jsonValueSchema.parse({ ...base, semantic_graph: semanticGraph }),
result: { ...base, semantic_graph: semanticGraph },
confidence: "derived",
authority: "shipped-artifact",
evidenceLinks: [source.evidence_id],
@@ -12,7 +12,6 @@ import {
createEvidenceBundle,
type EvidenceBundle,
} from "../domain/evidenceBundle.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import {
reconstructionObligationLedgerSchema,
type ReconstructionObligationLedgerInput,
@@ -47,12 +46,10 @@ export const processEvidence = (
predicateType: "rea.process-capture",
operation: "capture_process_scenario",
parameters: {},
result: jsonValueSchema.parse(
processCaptureSchema.parse({
...EMPTY_PROCESS_CAPTURE_EXAMPLE,
...overrides,
}),
),
result: processCaptureSchema.parse({
...EMPTY_PROCESS_CAPTURE_EXAMPLE,
...overrides,
}),
confidence: "observed",
authority: "controlled-replay",
});
@@ -70,9 +67,7 @@ export const boundProofEvidence = (
predicateType: "rea.reconstruction-proof",
operation: "verify_reconstruction_obligations",
parameters: {},
result: jsonValueSchema.parse(
reconstructionObligationProofSchema.parse(result),
),
result: reconstructionObligationProofSchema.parse(result),
confidence: "observed",
authority,
},
+1 -2
View File
@@ -206,9 +206,8 @@ export const withRegularFileCompletion = async <Value>(
...(signal === undefined ? {} : { signal }),
symlinks: options.symlinks ?? "follow",
},
async (handle) => {
async ({ handle, initial: stats }) => {
admitted = handle;
const stats = await handle.stat();
assertRegularFileActive(signal);
const value = await read(handle, stats);
assertRegularFileActive(signal);
+2 -2
View File
@@ -6,6 +6,7 @@ import {
effectiveClientConfiguration,
readConfigurationText,
type ClientConfigurationSource,
type ClientConfigurationFileError,
} from "./ClientConfigurationFiles.js";
import { err, ok, type Result } from "../domain/result.js";
import { ConfigurationError } from "../domain/configurationErrors.js";
@@ -55,7 +56,6 @@ import {
preserveCurrentRecoveryBackup,
selectCurrentRecoveryBackup,
} from "./SetupRecoveryBackup.js";
import type { ClientConfigurationFileError } from "./ClientConfigurationFiles.js";
import type {
ClientConfigurationInspection,
ClientConfigurationResult,
@@ -376,7 +376,7 @@ const persistClientConfiguration = async ({
!clientConfigurationValuesEqual(readback.servers[name], entry),
)
) {
return rollbackOrReadbackFailure(
return await rollbackOrReadbackFailure(
publication,
original,
recoveryBackupPath,
+6 -4
View File
@@ -1,7 +1,6 @@
import { z } from "zod";
import { RegularFileCleanupFailure } from "../filesystem/OwnedRegularFile.js";
import { classifyClientRegistrationCommand } from "./ClientRegistrationIdentity.js";
import {
readClientRegistrationInspections,
type ClientRegistrationStatus,
@@ -75,7 +74,11 @@ export const existingMaintenanceScope = async (
);
const clients: string[] = [];
const unresolved: ClientRegistrationStatus[] = [];
for (const { status: registration, activation } of registrations) {
for (const {
status: registration,
activation,
commandIdentity,
} of registrations) {
if (registration.state === "unknown" || registration.state === "invalid") {
unresolved.push(registration);
continue;
@@ -83,8 +86,7 @@ export const existingMaintenanceScope = async (
if (
activation !== "enabled" ||
(registration.state !== "aligned" && registration.state !== "stale") ||
classifyClientRegistrationCommand(registration.command, entryPoint) !==
"owned"
commandIdentity !== "owned"
)
continue;
clients.push(registration.client);
+1
View File
@@ -81,6 +81,7 @@ export const parseUtf8Json = (
return {
ok: false,
error: "JSON input is not valid UTF-8",
userMessage: "JSON input is not valid UTF-8",
cause,
};
}
+1 -1
View File
@@ -261,7 +261,7 @@ export class WebModuleTraceService {
},
{
operation: OPERATION,
parameters: jsonObjectSchema.parse(input),
parameters: input,
result: normalized.data,
rawResult: resolution?.rawResult ?? null,
confidence: "derived",
+1 -2
View File
@@ -7,7 +7,6 @@ import {
AnalysisOutputError,
} from "../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { jsonObjectSchema } from "../domain/jsonValue.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { err, ok, type Result } from "../domain/result.js";
import {
@@ -162,7 +161,7 @@ export class WebRuntimeService {
createEvidence(undefined, this.provider.identity(), {
predicateType: `rea.${operation.replaceAll("_", "-")}`,
operation,
parameters: jsonObjectSchema.parse(input),
parameters: input,
result,
confidence: "observed",
authority: "external-service",
+1 -2
View File
@@ -21,7 +21,6 @@ import { AnalysisError } from "../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { analysisErrorWithCleanupFailure } from "../domain/analysisErrorCleanup.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { jsonValueSchema } from "../domain/jsonValue.js";
import { ProviderAdapterError } from "../domain/providerAdapterError.js";
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
import { err, ok, type Result } from "../domain/result.js";
@@ -136,7 +135,7 @@ export const exportWebScriptsValidated = async (
capture_path: input.capture_path,
output_directory: input.output_directory,
},
result: jsonValueSchema.parse(result),
result,
rawResult: null,
confidence: "derived",
authority: "historical-reference",
+1 -2
View File
@@ -9,7 +9,6 @@ import {
} from "../domain/analysisErrorCore.js";
import { createEvidence, type Evidence } from "../domain/evidence.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { jsonObjectSchema } from "../domain/jsonValue.js";
import { err, ok, type Result } from "../domain/result.js";
import {
selectedWebScriptArtifactsSchema,
@@ -171,7 +170,7 @@ export class WebSourceLocationService {
},
{
operation: OPERATION,
parameters: jsonObjectSchema.parse(input),
parameters: input,
result: normalized.data,
rawResult: { source_map_text: map.text },
confidence: "derived",
@@ -11,7 +11,11 @@ import {
import { describe, expect, it } from "vitest";
import { JAVASCRIPT_APPLICATION_EVIDENCE_EXAMPLE } from "../../contracts/javascript/javascriptRuntimeReconciliationExample.js";
import { createEvidence, parseEvidence } from "../../domain/evidence.js";
import {
createEvidence,
parseEvidence,
rebindEvidenceToTargetPath,
} from "../../domain/evidence.js";
import { inspectAnalysisViewInputSchema } from "../../domain/analysisView/analysisView.js";
import { functionDossierSchema } from "../../domain/hopperValues.js";
import { ghidraFunctionDossier } from "../../domain/ghidraValues.fixture.js";
@@ -24,6 +28,7 @@ import {
inspectAnalysisView,
inspectAnalysisViewValidated,
summarizeRetainedAnalysis,
projectRetainedAnalysis,
} from "./AnalysisViewService.js";
import type { Evidence } from "../../domain/evidence.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
@@ -39,6 +44,84 @@ import {
} from "../../../tests/fixtures/browserScenarioCapture.js";
import { projectAnalysisError } from "../../domain/analysisErrorProjection.js";
it.each([false, true])(
"summarizes the selected observation path rather than older retained display metadata (history=%s)",
(history) => {
const makeParent = (path: string) =>
createEvidence(
{ path, format: "pe", sha256: "c".repeat(64) },
{ id: "ghidra", name: "Ghidra", version: "12.1.4" },
{
operation: "analyze_function",
parameters: {},
result: functionDossierSchema.parse(ghidraFunctionDossier()),
},
);
const previous = makeParent("/old/fixture.exe");
const selected = makeParent("/selected/fixture.exe");
expect(selected.evidence_id).toBe(previous.evidence_id);
const ledger = new EvidenceLedger();
if (history) expect(ledger.record(previous).ok).toBe(true);
const summary = summarizeRetainedAnalysis(selected, {
recordEvidence: (evidence) => ledger.record(evidence),
evidenceById: (id) => ledger.forAnalysis(id),
});
if (!summary.ok) throw summary.error;
expect(summary.value.evidence_links).toEqual([selected.evidence_id]);
expect(summary.value.subject?.local_path).toBe(
selected.subject?.local_path,
);
expect(ledger.get(selected.evidence_id)?.subject?.local_path).toBe(
history ? previous.subject?.local_path : selected.subject?.local_path,
);
},
);
it("keeps retained admission and payload ownership for a large capture when only its display path changes", async () => {
const parent = analysisViewCaptureEvidence(
analysisViewObjectDenseCapture(40_000),
);
const ledger = new EvidenceLedger();
expect(ledger.record(parent).ok).toBe(true);
const retained = ledger.forAnalysis(parent.evidence_id)!;
const payload = retained.normalized_result;
const subject = retained.subject!;
const selected = rebindEvidenceToTargetPath(retained, {
path: "/selected/capture.har",
sha256: subject.digest.sha256,
format: subject.format,
...(subject.architecture === null
? {}
: { architecture: subject.architecture }),
});
const retention = {
recordEvidence: (evidence: Evidence) => ledger.record(evidence),
evidenceById: (id: string) => ledger.forAnalysis(id),
};
const view = inspectAnalysisViewInputSchema.parse({
source: { kind: "retained-evidence", evidence_id: parent.evidence_id },
view: { kind: "api-surface" },
}).view;
for (const evidence of [retained, selected]) {
const result = projectRetainedAnalysis(evidence, retention, view);
if (!result.ok) throw result.error;
expect(result.value.subject?.local_path).toBe(evidence.subject?.local_path);
expect(result.value.evidence_links).toEqual([parent.evidence_id]);
}
expect(ledger.forAnalysis(parent.evidence_id)).toBe(retained);
expect(retained.normalized_result).toBe(payload);
expect(retained.subject?.local_path).toBe(parent.subject?.local_path);
expect(
await inspectAnalysisView({
source: { kind: "inline", evidence: selected },
view: { kind: "api-surface" },
}),
).toMatchObject({
ok: false,
error: { _tag: "AnalysisResourceConstraintError", resource: "memory" },
});
});
it.each(["layout", "javascript"])(
"carries the retained %s source snapshot into repeatable shared views",
async (kind) => {
@@ -410,6 +493,23 @@ describe("summarizeRetainedAnalysis", () => {
}),
).toEqual(err(conflict));
});
it("still rejects missing or mismatched retained identities after a successful writer reply", () => {
for (const [retained, reason] of [
[undefined, "missing"],
[analysisViewLayoutEvidence(), "identity_mismatch"],
] as const) {
expect(
summarizeRetainedAnalysis(parent, {
recordEvidence: () => ok("added"),
evidenceById: () => retained,
}),
).toMatchObject({
ok: false,
error: { _tag: "EvidenceIntegrityError", reason },
});
}
});
});
it("seals inferred and literal HAR pages, including a null artifact digest", async () => {
@@ -2,7 +2,6 @@ import { setImmediate } from "node:timers/promises";
import { dispatchQueuedInterrupts } from "../../process/queuedInterrupts.js";
import { readImmutableEvidenceBundle } from "../EvidenceBundleFiles.js";
import { INSPECT_ANALYSIS_VIEW_OPERATION } from "../../domain/analysisView/analysisViewErrors.js";
import { jsonValueSchema } from "../../domain/jsonValue.js";
import { ANALYSIS_VIEW_PROVIDER } from "../../domain/investigationProviders.js";
import type { EvidenceLookup } from "../EvidenceInputResolver.js";
@@ -17,7 +16,12 @@ import {
import { analysisInputErrorFromIssues } from "../../domain/inputIssueProjection.js";
import { EvidenceIntegrityError } from "../../domain/evidenceErrors.js";
import { safeParseEvidence } from "../../domain/evidenceValidation.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import {
createEvidence,
rebindEvidenceToTargetPath,
type Evidence,
type EvidenceSubjectTarget,
} from "../../domain/evidence.js";
import {
inspectAnalysisViewInputSchema,
prepareAnalysisViewProjection,
@@ -180,7 +184,20 @@ export const inspectAnalysisViewValidated = (
}
const resolved = resolveAnalysisViewSource(input.source, lookup);
if (!resolved.ok) return resolved;
const parent = resolved.value;
return projectResolvedAnalysisView(input, projection, resolved.value);
};
/** Keep payload ownership separate from this delivery's display-only subject. */
const projectResolvedAnalysisView = (
input: ReadyAnalysisViewInput,
projection: ReturnType<typeof prepareAnalysisViewProjection>,
parent: Evidence,
selectedTarget?: EvidenceSubjectTarget,
): Result<Evidence, AnalysisError> => {
const presentation =
selectedTarget === undefined
? parent
: rebindEvidenceToTargetPath(parent, selectedTarget);
const projected = projection.project({
evidenceId: parent.evidence_id,
operation: parent.operation,
@@ -191,15 +208,15 @@ export const inspectAnalysisViewValidated = (
parent.normalized_result,
// Sealing an Evidence root deep-freezes it; only the root is registered.
sealedResult: isImmutableJsonSnapshot(parent),
subject: parent.subject,
subject: presentation.subject,
limitations: parent.limitations,
parameters: parent.parameters,
artifact:
parent.subject === null
presentation.subject === null
? null
: {
path: parent.subject.local_path,
sha256: parent.subject.digest.sha256,
path: presentation.subject.local_path,
sha256: presentation.subject.digest.sha256,
},
});
if (!projected.ok) return projected;
@@ -212,7 +229,7 @@ export const inspectAnalysisViewValidated = (
`Parent subject SHA-256 ${parent.subject.digest.sha256} differs from analysis artifact SHA-256 ${projected.value.artifact?.sha256 ?? "unknown"}.`,
),
);
return ok(createAnalysisViewEvidence(input, parent, projected.value));
return ok(createAnalysisViewEvidence(input, presentation, projected.value));
};
/** Session Evidence ownership required to deliver a view instead of a complete record. */
@@ -255,7 +272,7 @@ export const requireAnalysisViewRetention = (
/**
* Retain complete analysis Evidence in the session, then project its summary
* view from the retained record. Callers page or select the rest through
* with this observation's subject path. Callers page or select the rest through
* inspect_analysis_view with the view's parent Evidence ID.
*/
export const summarizeRetainedAnalysis = (
@@ -274,12 +291,29 @@ export const projectRetainedAnalysis = (
if (!ready.ok) return ready;
const recorded = ready.value.recordEvidence(evidence);
if (!recorded.ok) return recorded;
return inspectAnalysisViewValidated(
{
source: { kind: "retained-evidence", evidence_id: evidence.evidence_id },
view,
},
ready.value.evidenceById,
const source = {
kind: "retained-evidence" as const,
evidence_id: evidence.evidence_id,
};
const retained = resolveEvidenceInput(source, ready.value.evidenceById);
if (!retained.ok) return retained;
// Duplicate semantic records retain their original ledger metadata. Detail
// selection changes presentation, not the historical lookup or payload owner.
const subject = evidence.subject;
return projectResolvedAnalysisView(
{ source, view },
prepareAnalysisViewProjection(view),
retained.value,
subject === null
? undefined
: {
path: subject.local_path,
sha256: subject.digest.sha256,
format: subject.format,
...(subject.architecture === null
? {}
: { architecture: subject.architecture }),
},
);
};
@@ -346,9 +380,9 @@ const createAnalysisViewEvidence = (
operation: INSPECT_ANALYSIS_VIEW_OPERATION,
parameters: {
parent_evidence_id: parent.evidence_id,
view: jsonValueSchema.parse(input.view),
view: input.view,
},
result: jsonValueSchema.parse(result),
result,
rawResult: null,
confidence:
// Grouping is whole-capture, so any templated group or parameter makes the result inferred;
@@ -11,7 +11,6 @@ import {
import { AnalysisCancelledError } from "../../domain/analysisErrorCore.js";
import type { AnalysisError } from "../../domain/analysisErrorBase.js";
import { createEvidence, type Evidence } from "../../domain/evidence.js";
import { jsonObjectSchema } from "../../domain/jsonValue.js";
import { err, type Result } from "../../domain/result.js";
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
@@ -62,7 +61,7 @@ export class AndroidAnalysisService {
execution.provider,
{
operation,
parameters: jsonObjectSchema.parse(request.data.input),
parameters: request.data.input,
result: execution.result,
rawResult: execution.rawResult,
limitations: execution.limitations,
@@ -930,6 +930,63 @@ it("does not replay pre-envelope call graphs under the revised workflow profile"
).toBe(false);
});
it("declines source-bearing snapshot entries while replaying omitted and empty source controls", () => {
const cache = new AnalysisSnapshotCache();
const target = ANALYSIS_SNAPSHOT_TARGET;
const profile = ANALYSIS_SNAPSHOT_PROFILE;
const source = createEvidence(target, ANALYSIS_SNAPSHOT_PROVIDER, {
operation: "source",
parameters: {},
result: "observed source",
});
const parameters = { address: "0x1000", document: "fixture" };
cache.record({
target,
profile,
operation: "procedure_info",
parameters,
execution: createAnalysisExecution(
"parent observation",
ANALYSIS_SNAPSHOT_PROVIDER,
{
subject: target,
analysisProfile: profile,
sourceEvidence: [source],
},
),
});
expect(
cache.lookup(target, profile, "procedure_info", parameters),
).toBeUndefined();
expect(cache.entries()).toEqual([]);
for (const sourceEvidence of [undefined, []] as const) {
const control = {
address: sourceEvidence === undefined ? "0x2000" : "0x3000",
document: "fixture",
};
cache.record({
target,
profile,
operation: "procedure_info",
parameters: control,
execution: createAnalysisExecution(
"source-free observation",
ANALYSIS_SNAPSHOT_PROVIDER,
{
subject: target,
analysisProfile: profile,
...(sourceEvidence === undefined ? {} : { sourceEvidence }),
},
),
});
expect(
cache.lookup(target, profile, "procedure_info", control)?.result,
).toBe("source-free observation");
}
expect(cache.entries()).toHaveLength(2);
});
describe("executions that report truncated references", () => {
const record = (referencesTruncated: boolean | undefined) => {
const cache = new AnalysisSnapshotCache();
@@ -436,6 +436,9 @@ export class AnalysisSnapshotCache {
// The snapshot format does not carry reference truncation; a replay would
// present a capped read as complete, so it is not recorded.
if (execution.referencesTruncated === true) return;
// Provider snapshot entries do not carry execution source records. Replay
// must not discard the source closure that supplies the parent's Evidence links.
if ((execution.sourceEvidence?.length ?? 0) > 0) return;
const queryId = analysisQueryId(
snapshotTarget(target),
snapshotBinding(profile),
+16 -17
View File
@@ -13,7 +13,7 @@ import {
import { AnalysisError } from "../../domain/analysisErrorBase.js";
import { err, ok, type Result } from "../../domain/result.js";
import type { JsonValue } from "../../domain/jsonValue.js";
import { createEvidence } from "../../domain/evidence.js";
import { createAnalysisExecutionEvidence } from "../../domain/analysisObservation.js";
import type {
AnalysisClient,
AnalysisExecution,
@@ -33,6 +33,7 @@ import {
type SessionProviderRoute,
} from "./SessionProviderRouter.js";
import { InvestigationRecords } from "../investigation/InvestigationRecords.js";
import { recordEvidenceSources } from "../investigation/recordEvidenceSources.js";
import { BinarySessionRecords } from "./BinarySessionRecords.js";
import { binarySessionStatus } from "./BinarySessionStatus.js";
import {
@@ -1256,26 +1257,24 @@ export class BinarySession
name !== "inspect_keyed_archive" &&
name !== "trace_dylib_resolution"
) {
const evidence = createEvidence(
execution.value.subject === undefined
? active.target
: (execution.value.subject ?? undefined),
execution.value.provider,
const evidence = createAnalysisExecutionEvidence(
name,
arguments_,
{
operation: name,
parameters: arguments_,
result: execution.value.result,
...execution.value,
analysisProfile: profile,
rawResult: execution.value.rawResult,
limitations: execution.value.limitations,
...(execution.value.relationshipCoverage === undefined
? {}
: {
relationshipCoverage: execution.value.relationshipCoverage,
}),
locations: execution.value.locations,
subject:
execution.value.subject === undefined
? active.target
: execution.value.subject,
},
"observed",
);
const sourcesRecorded = recordEvidenceSources(
(source) => this.recordEvidence(source),
execution.value.sourceEvidence ?? [],
);
if (!sourcesRecorded.ok) return sourcesRecorded;
const recorded = this.recordEvidence(evidence);
if (!recorded.ok) return recorded;
const retained = { ...execution.value, evidenceId: evidence.evidence_id };
@@ -150,9 +150,13 @@ const sameTree = async (left: string, right: string): Promise<number> => {
const files = await listFiles(left);
expect(await listFiles(right)).toEqual(files);
for (const file of files)
expect(await readFile(join(left, file)), file).toEqual(
await readFile(join(right, file)),
);
expect(
Buffer.compare(
await readFile(join(left, file)),
await readFile(join(right, file)),
),
file,
).toBe(0);
return files.length;
};
@@ -35,9 +35,10 @@ import {
type WrittenBundleFile,
} from "./environmentExport.js";
import {
loadEnvironmentSource,
composeEnvironmentSource,
parseCaptureEvidence,
parseEvidenceFile,
parseViewEvidence,
} from "./environmentSource.js";
import { prepareServedState } from "./environmentState.js";
@@ -288,9 +289,21 @@ export const buildEnvironment = async (
ENVIRONMENT_BUILD_OPERATION,
);
if (!manifest.ok) return manifest;
const source = loadEnvironmentSource(
const parsedManifest = parseViewEvidence(
ENVIRONMENT_BUILD_OPERATION,
"manifest",
manifest.value.value,
"environment-manifest",
);
if (!parsedManifest.ok) return parsedManifest;
const recorded = parseCaptureEvidence(
ENVIRONMENT_BUILD_OPERATION,
input.capture.value,
);
if (!recorded.ok) return recorded;
const source = composeEnvironmentSource(
parsedManifest.value,
recorded.value,
ENVIRONMENT_BUILD_OPERATION,
);
if (!source.ok) return source;
@@ -306,11 +319,6 @@ export const buildEnvironment = async (
name,
operation: ENVIRONMENT_EXPORT_OPERATION,
});
const recorded = parseCaptureEvidence(
ENVIRONMENT_BUILD_OPERATION,
input.capture.value,
);
if (!recorded.ok) return recorded;
warnings.push(...credentialScanWarnings(recorded.value));
const packageSource: EnvironmentPackageSource =
@@ -66,8 +66,11 @@ it("retains the admitted tarball descriptor and refuses a pathname replacement d
error: { _tag: "AnalysisArtifactChangedError", path: tarball },
});
expect(
await readFile(join(directory, "envs/grafana-example/rea-agents.tgz")),
).toEqual(originalBytes);
Buffer.compare(
await readFile(join(directory, "envs/grafana-example/rea-agents.tgz")),
originalBytes,
),
).toBe(0);
} finally {
writer.mockRestore();
}
@@ -158,8 +161,11 @@ it("uses confirmed publication after the output owner's descriptor cleanup compl
});
expect(result).toMatchObject({ ok: true, value: { status: "exported" } });
expect(
await readFile(join(directory, "envs/grafana-example/evidence.json")),
).toEqual(inputs.value.evidence.bytes);
Buffer.compare(
await readFile(join(directory, "envs/grafana-example/evidence.json")),
inputs.value.evidence.bytes,
),
).toBe(0);
} finally {
commit.mockRestore();
}
Loaded 100 of 583 files, more files were not shown because too many files have changed in this diff. Show more