mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
fix(apple): preserve nested Rosetta state and its shared register budget
This commit is contained in:
commit
07fa932ee5
583 files changed
+18687
-3797
No files matched your search
@@ -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
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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. |
|
||||
|
||||
|
||||
@@ -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
@@ -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
@@ -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.
|
||||
|
||||
@@ -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`;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Generated
+103
@@ -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
@@ -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
@@ -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();
|
||||
@@ -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));
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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(", ")}`,
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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;
|
||||
},
|
||||
],
|
||||
);
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
import type { RuleTester } from "oxlint/plugins-dev";
|
||||
|
||||
declare const cancellationCauseRule: Parameters<RuleTester["run"]>[1];
|
||||
export default cancellationCauseRule;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -0,0 +1,4 @@
|
||||
export declare const resolveImportSource: (
|
||||
filename: string,
|
||||
specifier: unknown,
|
||||
) => string | undefined;
|
||||
@@ -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>>;
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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.
|
||||
[
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 });
|
||||
};
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 }),
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ export class WebModuleTraceService {
|
||||
},
|
||||
{
|
||||
operation: OPERATION,
|
||||
parameters: jsonObjectSchema.parse(input),
|
||||
parameters: input,
|
||||
result: normalized.data,
|
||||
rawResult: resolution?.rawResult ?? null,
|
||||
confidence: "derived",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
Reference in new issue
Block a user