Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b87b0486e | ||
|
|
160d821ac9 | ||
|
|
113679c9a6 | ||
|
|
80673b22a4 | ||
|
|
c74d302cc4 | ||
|
|
2c38becc4e | ||
|
|
0a2b7af905 | ||
|
|
cc146042d1 | ||
|
|
fead4fdaa7 | ||
|
|
4973ce7202 | ||
|
|
06a4c338fc | ||
|
|
60ab148afa | ||
|
|
8e6a957886 |
No files matched your search
@@ -68,9 +68,10 @@ closing-issue reporters. Grouping preserves all those credits in one line.
|
||||
When Highlights, detailed entries or migrations reference the same work,
|
||||
put its thanks in one of those entries, not all of them. Include issue links.
|
||||
Direct-commit bullets carry the commit link and known human credit.
|
||||
Exclude GitHub bots and known agent accounts. REA's observed Anthropic, Cursor
|
||||
and Codex co-author trailers resolve to `@claude`, `@cursoragent` and `@codex`
|
||||
with GitHub type `User`; the credit policy excludes them while retaining their metadata.
|
||||
Exclude GitHub bots and known agent accounts. REA's observed Anthropic, Cursor,
|
||||
Codex, and Copilot co-author trailers resolve to `@claude`, `@cursoragent`,
|
||||
`@codex`, and `@Copilot` with GitHub type `User`; the credit policy excludes
|
||||
them while retaining their metadata.
|
||||
At the REA maintainer's request, omit `@morluto` self-thanks while preserving
|
||||
authorship in the inventory. Other human contributors remain eligible.
|
||||
Use explicit GitHub links for cross-repository references to avoid ambiguous
|
||||
|
||||
@@ -55,6 +55,12 @@ jobs:
|
||||
- uses: ./.github/actions/portable-build-cache
|
||||
with:
|
||||
build_cache_artifact: ${{ inputs.build_cache_artifact }}
|
||||
- name: Install ELF32 cross toolchains for source-owned fixtures
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install --yes --no-install-recommends \
|
||||
gcc-i686-linux-gnu gcc-arm-linux-gnueabi gcc-mips-linux-gnu gcc-mipsel-linux-gnu \
|
||||
libc6-dev-i386-cross libc6-dev-armel-cross libc6-dev-mips-cross libc6-dev-mipsel-cross
|
||||
- name: Acquire exact upstream libraries in an isolated runner environment
|
||||
shell: bash
|
||||
run: |
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
{
|
||||
".": "6.7.0"
|
||||
".": "6.8.0"
|
||||
}
|
||||
+102
@@ -1,5 +1,107 @@
|
||||
# Changelog
|
||||
|
||||
## [6.8.0](https://github.com/morluto/rea/compare/rea-agents-6.7.0...rea-agents-6.8.0) (2026-10-25)
|
||||
|
||||
### Highlights
|
||||
|
||||
- Build local agent environments from network captures, browser scenarios, or a prompt, then serve derived reads and evidence-gated writes and compare them with fresh application captures.
|
||||
- Inspect more shipped formats, including GPU code objects, BEAM, Python bytecode, Unity IL2CPP metadata, Godot PCK, PyInstaller, Node.js SEA, devicetree, and additional PE/ELF tables.
|
||||
- Keep large investigations focused with retained summaries, selected fact queries, configurable recovery limits, and row-oriented CLI exports. Complete Evidence remains available.
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
- **Function analysis:** `analyze_function` now defaults to a retained summary. Set `detail: "complete"` for the previous inline dossier; the CLI retains its complete default and requires `--evidence_output` when selecting a summary. [#5660](https://github.com/morluto/rea/pull/5660). Thanks @CryptoJones, @gensokyo-dev, @RishTheFish21.
|
||||
- **Accessibility:** Accept JSON numbers and booleans in node `value`, alongside strings and null. Update consumers that previously assumed text-only values. [cc90771](https://github.com/morluto/rea/commit/cc90771af14f045538651615147d8bf86b576549), [#6759](https://github.com/morluto/rea/pull/6759), [#6467](https://github.com/morluto/rea/issues/6467). Thanks @coygeek, @gensokyo-dev, @mvanhorn.
|
||||
- **JSON Lines:** `--format jsonl` now emits object-collection rows followed by a `$rea: "jsonl/v1"`, `type: "metadata"` record; scalar selections and unsupported reports are refused. Parse control records separately, select a single supported collection when necessary, and use `--format json` for the previous whole-document output. See [JSON Lines framing](https://github.com/morluto/rea/blob/rea-agents-6.8.0/docs/cli.md#json-lines-record-output). [#5629](https://github.com/morluto/rea/pull/5629), [#5487](https://github.com/morluto/rea/issues/5487). Thanks @gensokyo-dev, @jayykernel, @vijaykiran06.
|
||||
- **Interface Builder:** Keyed-archive graph identities use decimal table UIDs instead of authored `objectID`/`object-id`. Treat authored IDs as attributes and update persisted graph lookups; ibtool and native NIBArchive identities keep their source semantics. [6fbe67d](https://github.com/morluto/rea/commit/6fbe67d66e3d716db69929503a3a232a7ada0860).
|
||||
- **Frida:** `instrument-with-frida` JSON returns the canonical Evidence directly. Read `normalized_result.cleanup_error` instead of the former top-level wrapper field. [48f700e](https://github.com/morluto/rea/commit/48f700ee35e0988ed973e5486dc9db33f2a5d944).
|
||||
- **Reconstruction proof:** ID-array proof records no longer establish verified or ready ledgers. Supply implementation/revision bindings and individual claim outcomes; handle inapplicable, unavailable, and failed proof diagnostics. [f8b65e7](https://github.com/morluto/rea/commit/f8b65e78723b1e2169f94dd1226198ea3139707e).
|
||||
- **Mach-O imports:** Handle a null import `name` when identity is ambiguous; use `raw_name` and `name_status` to distinguish observed expressions from exact symbols. [5d0d243](https://github.com/morluto/rea/commit/5d0d243a45c9132e1aff75d454ab53d86a74cc31), [169512a](https://github.com/morluto/rea/commit/169512a35bb015ff209168fb8bc1c9b31a2c678b).
|
||||
- **Node.js:** Use a runtime satisfying `^22.23.4 || ^24.22.1 || >=26.12.1`; Node 23 and 25 are unsupported. Upgrade Node before installing or updating REA; the installer does not upgrade it. [#6315](https://github.com/morluto/rea/pull/6315), [#6283](https://github.com/morluto/rea/issues/6283). Thanks @GamerDubz, @gensokyo-dev, @ShikamaruNaaraa.
|
||||
- **Browser storage:** Historical hashed storage remains readable, but comparisons stay unknown until both captures are recaptured in the new representation. [#6341](https://github.com/morluto/rea/pull/6341), [#6289](https://github.com/morluto/rea/issues/6289). Thanks @coygeek, @gongfpp.
|
||||
- **Environment serving:** `environment-serve` serves derived state by default, including write tools whose effects are confirmed by recorded evidence. Use `--replay-only` for stateless replay of manifest tools. Read stop-summary `state` instead of `state_evidence_id`. [#5351](https://github.com/morluto/rea/pull/5351), [#6158](https://github.com/morluto/rea/pull/6158). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **MCP schemas:** Input advertisements default to compact. Set `REA_MCP_INPUT_SCHEMA_PROFILE=full` for complete advertised guidance and nested constraints. [#5975](https://github.com/morluto/rea/pull/5975). Thanks @gensokyo-dev.
|
||||
- **Browser console arguments:** Accept null `argument_types` and `text_capture.status: "unavailable"` when the protocol argument inventory is omitted or malformed; preserve that uncertainty. [#6016](https://github.com/morluto/rea/pull/6016), [#5934](https://github.com/morluto/rea/issues/5934). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Reconstruction summaries:** `summary.by_evidence_authority` is keyed by Evidence authority; read the former source-state counts from required `summary.by_source_state`. Regenerate ledgers whose closure digests depended on the old projection. [#5875](https://github.com/morluto/rea/pull/5875), [#5838](https://github.com/morluto/rea/issues/5838). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Swift bindings:** Conformance `type_name` and `protocol_name` may be null; consume typed `type_binding`/`protocol_binding` evidence. Replace the former Objective-C-specific import-binding type/schema with `MachoImportBinding`. [d0067ba](https://github.com/morluto/rea/commit/d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f).
|
||||
- **WebSocket frames:** Accept null opcodes and preserve unknown frame facts; numeric opcodes are restricted to the four-bit protocol range. [0bc2b42](https://github.com/morluto/rea/commit/0bc2b42001f684beffcf47b70e507443b1a5d320).
|
||||
- **Resource-only inspection:** Evidence subjects and executable digests may be null. Use `resource_root` and each resource's reported provenance instead of requiring an executable target. [6a6adf7](https://github.com/morluto/rea/commit/6a6adf703fa44100643947d20fbc670f86260b1b).
|
||||
- **Browser timestamps:** Console and session-timeline timestamps may be null when the protocol supplies no finite/comparable time. Keep unknown time explicit in sorting and comparisons. [690fc51](https://github.com/morluto/rea/commit/690fc518540bae982038a74f47ef7781201785aa), [2a0410f](https://github.com/morluto/rea/commit/2a0410f1717b3126e8a95063d77dea66a597010a).
|
||||
- **Reference sources:** Read failed root enumeration from `rootFailure` rather than a synthetic failed child at `"."`; preserve the top-level source-graph limitation. [6296fdf](https://github.com/morluto/rea/commit/6296fdf560f1ac4aa55985558edaa98300aacdbc).
|
||||
- **Filesystem capture:** Accept `entry_unavailable` and `identity_changed` enumeration omission reasons in addition to the previous reason literals. [c85b44e](https://github.com/morluto/rea/commit/c85b44e1b14851c54e65327a4807f79ad5cd5a61). Thanks @rudrakshagarwal4200-ai.
|
||||
- **APK locales:** Replace numeric legacy selectors such as `es-r419` with `es-419` or `b+es+419`. Two-letter regions remain supported. [29910d1](https://github.com/morluto/rea/commit/29910d16b98fe4b607a7aa15928789a73614b0b9). Thanks @mertkayacs.
|
||||
- **XML plist scalars:** Recognize `$plist_type: "unknown"` and read `element`, `literal`, and `reason`; unresolved XML scalars use this wrapper instead of the former real/null unknown marker or malformed scalar prefixes. Supported extended UTC years preserve their timestamp. [3537e58](https://github.com/morluto/rea/commit/3537e58c1ab29309f06eabf50bc52adba9817a0c).
|
||||
|
||||
### Changes
|
||||
|
||||
- **Binary layouts:** Inspect ELF32 i386, little-endian ARM, and little- or big-endian MIPS `EXEC`/`DYN`/`REL` layouts on Linux x64 using a caller-supplied pwntools environment. [#7052](https://github.com/morluto/rea/pull/7052), [#6934](https://github.com/morluto/rea/issues/6934). Thanks @roblennon, @salmanalmutairi1.
|
||||
- **Agent environments:** Project network captures and Evidence files into manifests, preserve template path pairs and positions, export AgentEnv bundles, and serve manifests as replay environments. [#4954](https://github.com/morluto/rea/pull/4954), [#5293](https://github.com/morluto/rea/pull/5293), [#5664](https://github.com/morluto/rea/pull/5664), [#4999](https://github.com/morluto/rea/pull/4999), [#4975](https://github.com/morluto/rea/pull/4975). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **Capture to environment:** Build environments from browser scenarios or a prompt, extract the initial observed state, and compare the environment with application probe captures. [#5182](https://github.com/morluto/rea/pull/5182), [#6946](https://github.com/morluto/rea/pull/6946), [#5319](https://github.com/morluto/rea/pull/5319), [#5699](https://github.com/morluto/rea/pull/5699). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **State-backed tools:** Render read filters, search, sorting and paging from state; apply confirmed write effects with broader state targets and recording-based write admission. [#5352](https://github.com/morluto/rea/pull/5352), [293832b](https://github.com/morluto/rea/commit/293832bafc9a054914ec514fe906dd4936c7a372), [#5800](https://github.com/morluto/rea/pull/5800), [#5787](https://github.com/morluto/rea/pull/5787), [#5600](https://github.com/morluto/rea/pull/5600), [5375ff0](https://github.com/morluto/rea/commit/5375ff0f9aadb4fd0d46333815aa0431e3df1010), [#6169](https://github.com/morluto/rea/pull/6169), [#6385](https://github.com/morluto/rea/pull/6385), [#6897](https://github.com/morluto/rea/pull/6897). Thanks @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **State learning:** Model application-assigned values, preserve view-specific clock spellings, learn write clocks from scenario timing, derive fields, and learn creates across sorted arrays and referenced entities. [7ec9b6e](https://github.com/morluto/rea/commit/7ec9b6e56e1b3762285dca881db6d900a389ab9c), [#6136](https://github.com/morluto/rea/pull/6136), [957bcfa](https://github.com/morluto/rea/commit/957bcfa149c1a0593bb0aa2466cc56d1d4f62e7d), [#6138](https://github.com/morluto/rea/pull/6138), [#6967](https://github.com/morluto/rea/pull/6967), [#7025](https://github.com/morluto/rea/pull/7025), [#7034](https://github.com/morluto/rea/pull/7034). Thanks @earakely-scale.
|
||||
- **Recorded writes:** Serve creates whose compatible read-backs add omitted fields or express timestamps at a different precision, and whole-object updates that regenerate observed slugs or modification times. Conflicting observations still withhold writes. State views and bundles carrying the new read-back data need REA 6.8.0 or later to serve. [#7080](https://github.com/morluto/rea/pull/7080). Thanks @earakely-scale.
|
||||
- **Native investigations:** Page native inventory summaries, match functions across builds, export pseudocode to resumable directories, and select objdump disassembly by symbol. [#5378](https://github.com/morluto/rea/pull/5378), [#5390](https://github.com/morluto/rea/pull/5390), [#6170](https://github.com/morluto/rea/pull/6170), [#5171](https://github.com/morluto/rea/pull/5171), [#4249](https://github.com/morluto/rea/issues/4249), [#5343](https://github.com/morluto/rea/issues/5343), [#5159](https://github.com/morluto/rea/issues/5159). Thanks @coygeek, @CryptoJones, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **Engine workflows:** Bound a single Ghidra decompilation without closing its session, analyze GameCube/Wii DOL executables, and inspect static CUDA/HIP code objects. [#5662](https://github.com/morluto/rea/pull/5662), [#6240](https://github.com/morluto/rea/pull/6240), [#5813](https://github.com/morluto/rea/pull/5813), [#5340](https://github.com/morluto/rea/issues/5340), [#5345](https://github.com/morluto/rea/issues/5345). Thanks @arielfikru, @coygeek, @gensokyo-dev, @RishTheFish21.
|
||||
- **PE/ELF:** Inspect export tables without an engine, PE imports/delay-loads, Authenticode certificates, TLS callbacks/load configuration, ELF notes, Linux kernel module metadata, and Zstandard section-name tables. [#6278](https://github.com/morluto/rea/pull/6278), [#6509](https://github.com/morluto/rea/pull/6509), [#4830](https://github.com/morluto/rea/pull/4830), [#7008](https://github.com/morluto/rea/pull/7008), [#4837](https://github.com/morluto/rea/pull/4837), [#7010](https://github.com/morluto/rea/pull/7010), [#6987](https://github.com/morluto/rea/pull/6987), [#4057](https://github.com/morluto/rea/issues/4057). Thanks @gensokyo-dev, @kaoru0822-kitauji, @mvanhorn, @Rinkia, @ShuaixinHuang.
|
||||
- **Apple/native metadata:** Inspect IPS crash reports, decode Swift nominal declarations, and select universal Mach-O slices in Go/Rust metadata readers. [#6511](https://github.com/morluto/rea/pull/6511), [#6257](https://github.com/morluto/rea/pull/6257), [#6219](https://github.com/morluto/rea/pull/6219), [#5341](https://github.com/morluto/rea/issues/5341), [#4102](https://github.com/morluto/rea/issues/4102). Thanks @coygeek, @gensokyo-dev, @kaoru0822-kitauji, @mvanhorn, @Rinkia.
|
||||
- **Packaged applications:** Triage desktop installation directories and inspect/extract Godot PCK, PyInstaller archives, and Node.js single-executable application blobs. [#5207](https://github.com/morluto/rea/pull/5207), [#3634](https://github.com/morluto/rea/pull/3634), [afa5da7](https://github.com/morluto/rea/commit/afa5da745e0b139e68029a96b776ae30e48ea2b2), [#2433](https://github.com/morluto/rea/pull/2433), [#7000](https://github.com/morluto/rea/pull/7000), [#4399](https://github.com/morluto/rea/issues/4399). Thanks @0xthearchitect, @coygeek, @gensokyo-dev, @mvanhorn.
|
||||
- **Unity and managed code:** Inventory IL2CPP images/types/methods and metadata versions 24/27; read member names and literals, link native methods and generic instances, and export Ghidra seeds. Project managed call/field-access edges and page managed member summaries. [#3807](https://github.com/morluto/rea/pull/3807), [#5326](https://github.com/morluto/rea/pull/5326), [52d1a72](https://github.com/morluto/rea/commit/52d1a72af047a73b787b8935b032786a34966036), [#5327](https://github.com/morluto/rea/pull/5327), [#5785](https://github.com/morluto/rea/pull/5785), [#6260](https://github.com/morluto/rea/pull/6260), [#5167](https://github.com/morluto/rea/pull/5167), [#3784](https://github.com/morluto/rea/issues/3784), [#5112](https://github.com/morluto/rea/issues/5112), [#4056](https://github.com/morluto/rea/issues/4056), [#5148](https://github.com/morluto/rea/issues/5148). Thanks @8rulerstar, @alexlanghart, @elricfd, @gensokyo-dev, @jaelliot, @kaoru0822-kitauji, @Rinkia.
|
||||
- **Android:** Inspect APK signing declarations and structured manifest components; report static-final DEX field values. [e93e3b1](https://github.com/morluto/rea/commit/e93e3b1e7d6e7e4f49c2de24b92452ec29e853f8), [#4158](https://github.com/morluto/rea/pull/4158), [#5025](https://github.com/morluto/rea/pull/5025), [#6981](https://github.com/morluto/rea/pull/6981), [#4537](https://github.com/morluto/rea/issues/4537), [#6397](https://github.com/morluto/rea/issues/6397). Thanks @Abelo9996, @BaaRai, @FerroLx, @gensokyo-dev, @sserrgo12.
|
||||
- **Bytecode and firmware:** Inspect and query CPython pyc and BEAM facts, inspect flattened devicetrees, and attribute MMIO peripheral regions in native disassembly. [#4826](https://github.com/morluto/rea/pull/4826), [#6928](https://github.com/morluto/rea/pull/6928), [#4833](https://github.com/morluto/rea/pull/4833), [#6943](https://github.com/morluto/rea/pull/6943), [#7009](https://github.com/morluto/rea/pull/7009), [#5348](https://github.com/morluto/rea/pull/5348), [#6922](https://github.com/morluto/rea/issues/6922), [#6938](https://github.com/morluto/rea/issues/6938). Thanks @gensokyo-dev, @liziye627-design, @mvanhorn.
|
||||
- **JavaScript:** Retain queryable static route inventories, link assignment values to writes, and accept source selections as a JSON array or file. [#6019](https://github.com/morluto/rea/pull/6019), [#6206](https://github.com/morluto/rea/pull/6206), [#5029](https://github.com/morluto/rea/pull/5029), [#4772](https://github.com/morluto/rea/issues/4772), [#6193](https://github.com/morluto/rea/issues/6193). Thanks @gensokyo-dev, @IvenKooLab, @jacobmalibiran, @JoshOsullivan-au, @kaoru0822-kitauji, @KJLavender.
|
||||
- **Data inspection:** Decode selected protobuf/gRPC bodies, inspect GGUF/safetensors declarations, and produce byte-entropy profiles. [#5031](https://github.com/morluto/rea/pull/5031), [#5603](https://github.com/morluto/rea/pull/5603), [#2331](https://github.com/morluto/rea/pull/2331), [#4986](https://github.com/morluto/rea/issues/4986), [#3323](https://github.com/morluto/rea/issues/3323). Thanks @AayushJB03, @gensokyo-dev, @kaoru0822-kitauji, @KJLavender, @michaelkillgta, @mvanhorn.
|
||||
- **CLI input/output:** Accept structured request documents on stdin and expose version/runtime output formats. Row-oriented JSONL exports use the migration above. [#7040](https://github.com/morluto/rea/pull/7040), [#5486](https://github.com/morluto/rea/issues/5486), [#6530](https://github.com/morluto/rea/pull/6530), [169512a](https://github.com/morluto/rea/commit/169512a35bb015ff209168fb8bc1c9b31a2c678b), [#5483](https://github.com/morluto/rea/issues/5483). Thanks @Abelo9996, @ahmed-husssain, @CassieFox23, @DeryFerd, @eco-null, @ezrabrilliant, @GamerDubz, @gensokyo-dev, @gongfpp, @hifzabuildsai, @jayykernel, @mhamzasakhi, @pablodiazjorge, @Plastic-time, @raidnouredden-ux, @SandstromPL, @ShuaixinHuang, @Sriram-PR, @XiaoBinGan.
|
||||
- **MCP and setup:** Add opt-in Streamable HTTP and FreeBuff setup registration. FreeBuff support first appears in 6.8.0; installed native FreeBuff loading and skill discovery remain unverified. [#4955](https://github.com/morluto/rea/pull/4955), [#2736](https://github.com/morluto/rea/pull/2736), [#3243](https://github.com/morluto/rea/issues/3243). Thanks @0xLLVEL, @codebuff-team, @earakely-scale, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **Recovery controls:** Adjust per-call WASM/SQLite/firmware deadlines, MCP startup budgets, regex matching deadlines/worker heap, capture decoder heap, ASAR header admission, Apple dispatch input, and the bridge request ceiling. MCP runtime limit recovery reports actionable settings. [#5798](https://github.com/morluto/rea/pull/5798), [#5818](https://github.com/morluto/rea/pull/5818), [aa53c06](https://github.com/morluto/rea/commit/aa53c065387c58b4189d82c47f1f3445fc505e30), [#5863](https://github.com/morluto/rea/pull/5863), [#6304](https://github.com/morluto/rea/pull/6304), [#6267](https://github.com/morluto/rea/pull/6267), [#6301](https://github.com/morluto/rea/pull/6301), [#6299](https://github.com/morluto/rea/pull/6299), [#6209](https://github.com/morluto/rea/pull/6209), [#6233](https://github.com/morluto/rea/pull/6233), [#6376](https://github.com/morluto/rea/pull/6376), [#7049](https://github.com/morluto/rea/pull/7049), [#5797](https://github.com/morluto/rea/issues/5797), [#5812](https://github.com/morluto/rea/issues/5812), [#5851](https://github.com/morluto/rea/issues/5851), [#6176](https://github.com/morluto/rea/issues/6176), [#3917](https://github.com/morluto/rea/issues/3917). Thanks @gensokyo-dev, @jaychou813, @kaoru0822-kitauji.
|
||||
- **APK output:** Set `string_tables_only` or CLI `--string-tables-only` to omit duplicate primary strings and avoid their output-capacity charge. [7c458f0](https://github.com/morluto/rea/commit/7c458f04b9155707c8ba406f8b455b10016f1b99), [#4990](https://github.com/morluto/rea/pull/4990), [#4982](https://github.com/morluto/rea/issues/4982). Thanks @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0.
|
||||
- **Retained analysis:** Reuse successful validation of authenticated immutable native/imported-JavaScript parents and captures. Query retained Go build identity, Rust dependencies, JavaScript source-to-bundle mappings, and native export facts without repeating full output. [8f7d12e](https://github.com/morluto/rea/commit/8f7d12e30d63f3d3b4cfe2a90336362841438f56), [#4962](https://github.com/morluto/rea/pull/4962), [#5349](https://github.com/morluto/rea/pull/5349), [#5524](https://github.com/morluto/rea/pull/5524), [#5815](https://github.com/morluto/rea/pull/5815), [#6128](https://github.com/morluto/rea/pull/6128), [#6907](https://github.com/morluto/rea/pull/6907), [#4810](https://github.com/morluto/rea/issues/4810), [#5499](https://github.com/morluto/rea/issues/5499), [#5802](https://github.com/morluto/rea/issues/5802), [#6112](https://github.com/morluto/rea/issues/6112), [#6900](https://github.com/morluto/rea/issues/6900). Thanks @gensokyo-dev, @kaoru0822-kitauji, @N0zoM1z0.
|
||||
- **Compact results:** Use retained Frida/GDB/objdump summaries, omit redundant Ghidra raw results, and defer the analysis runtime during MCP catalog discovery. [#5661](https://github.com/morluto/rea/pull/5661), [#5614](https://github.com/morluto/rea/pull/5614), [#5355](https://github.com/morluto/rea/pull/5355), [#4424](https://github.com/morluto/rea/pull/4424), [#5640](https://github.com/morluto/rea/issues/5640), [#5584](https://github.com/morluto/rea/issues/5584), [#5337](https://github.com/morluto/rea/issues/5337). Thanks @coygeek, @gensokyo-dev, @kaoru0822-kitauji, @leeguooooo.
|
||||
- **Analysis work:** Reuse Hopper literal-string records, JavaScript workers and captured prefix selections, Interface Builder validation and plist scalar schemas; bound stable reads to one snapshot buffer and avoid decoding omitted GGUF strings. [#5017](https://github.com/morluto/rea/pull/5017), [#5819](https://github.com/morluto/rea/pull/5819), [31de177](https://github.com/morluto/rea/commit/31de177404461d122761d8243abe4fd24564f4da), [#5885](https://github.com/morluto/rea/pull/5885), [85e38ac](https://github.com/morluto/rea/commit/85e38ac199dff15e8c43343d9170db7e0e46d284), [#6966](https://github.com/morluto/rea/pull/6966), [#5781](https://github.com/morluto/rea/pull/5781), [#5010](https://github.com/morluto/rea/issues/5010), [#5855](https://github.com/morluto/rea/issues/5855), [#6954](https://github.com/morluto/rea/issues/6954), [#5774](https://github.com/morluto/rea/issues/5774). Thanks @gensokyo-dev, @kaoru0822-kitauji.
|
||||
- **Native scheduling:** Batch modern IDA callee lookups, schedule decompilation reads on demand, index NativeAOT type closure, and retain call-path ancestry once per visited node. [0c6fed9](https://github.com/morluto/rea/commit/0c6fed90b74c75a9fd23d1ec1b510a7274f85887), [06fe7ee](https://github.com/morluto/rea/commit/06fe7eecbc8c387414b4c1aa0d88ad6314f54f0c), [ce61680](https://github.com/morluto/rea/commit/ce61680c04c95a9cdfad5335632a99542af26d37), [5e4ec0f](https://github.com/morluto/rea/commit/5e4ec0fa1b678aa3bf20704ec9110224ba8c1676).
|
||||
- **Selected processing:** Prepare CLI positional schemas only for the selected command, separate entropy reads from report resolution, prune typed-array reference escapes, index state-write cutoffs, decode only selected replay query values, and skip payload text during WAT framing. [315b234](https://github.com/morluto/rea/commit/315b23403f0819967878cee418c3f9cb4b1a69d9), [#6950](https://github.com/morluto/rea/pull/6950), [#5139](https://github.com/morluto/rea/pull/5139), [#5350](https://github.com/morluto/rea/pull/5350), [96ea32b](https://github.com/morluto/rea/commit/96ea32b7e4e28d3c9cd79a1de4b8e1ba3b4244e9), [70835d3](https://github.com/morluto/rea/commit/70835d3d5f030deadf606e1c79c70a60883e50c9), [#5766](https://github.com/morluto/rea/pull/5766), [#6947](https://github.com/morluto/rea/issues/6947), [#5760](https://github.com/morluto/rea/issues/5760). Thanks @galleonli, @gensokyo-dev, @kaoru0822-kitauji.
|
||||
|
||||
### Fixes
|
||||
|
||||
- **IPS report fields:** Preserve the recorded terminating process and PID, termination flags, parent PID, and build/platform metadata. [#7079](https://github.com/morluto/rea/pull/7079), [#7078](https://github.com/morluto/rea/issues/7078). Thanks @rodricksz4h5.
|
||||
- **Ghidra language selection:** Accept stock language IDs containing `LEBE` or slash-separated processor names, and report conflicting DOL language overrides as configuration errors naming `REA_GHIDRA_LANGUAGE_ID`. [#7070](https://github.com/morluto/rea/pull/7070), [#7072](https://github.com/morluto/rea/issues/7072), [#7081](https://github.com/morluto/rea/pull/7081), [#7082](https://github.com/morluto/rea/issues/7082). Thanks @Adolanium.
|
||||
- **Setup and Hopper:** Preserve the Linux embedding runtime, supported x86_64 host restriction, readiness-based provider choices, orderly demo-helper shutdown, and installed apt dependency versions. Keep cleanup and skill publication ownership through retries. [#5041](https://github.com/morluto/rea/pull/5041), [#3422](https://github.com/morluto/rea/pull/3422), [f5d1012](https://github.com/morluto/rea/commit/f5d1012fabfe99af82523c3078ce9d93eb3780c1), [#4966](https://github.com/morluto/rea/pull/4966), [#6974](https://github.com/morluto/rea/pull/6974), [211e353](https://github.com/morluto/rea/commit/211e3533b34f81ae0910ad4f53f2a3d3c8587d3e), [#6920](https://github.com/morluto/rea/pull/6920), [#5015](https://github.com/morluto/rea/issues/5015), [#3419](https://github.com/morluto/rea/issues/3419), [#4921](https://github.com/morluto/rea/issues/4921), [#6975](https://github.com/morluto/rea/issues/6975). Thanks @Abel-I-Akhigbe, @Adolanium, @DavidFB-creator, @DeryFerd, @gensokyo-dev, @kaoru0822-kitauji, @sx4im.
|
||||
- **Client setup:** Treat empty APPDATA as unset, distinguish MSIX Claude Desktop process views, preserve aliased Hermes server tables, reuse client dialect/alias bindings, and materialize the pinned package-runner cache before the first start. [#5126](https://github.com/morluto/rea/pull/5126), [#4903](https://github.com/morluto/rea/pull/4903), [#3785](https://github.com/morluto/rea/pull/3785), [b7eeded](https://github.com/morluto/rea/commit/b7eededf5af5061a834f93f57f2c888acf870344), [#7032](https://github.com/morluto/rea/pull/7032), [#5104](https://github.com/morluto/rea/issues/5104), [#4278](https://github.com/morluto/rea/issues/4278), [#3320](https://github.com/morluto/rea/issues/3320), [#7027](https://github.com/morluto/rea/issues/7027). Thanks @CmsChase, @coygeek, @myso-kr, @pablodiazjorge, @vaidehi-snk, @wxj006007.
|
||||
- **Target admission:** Preserve thin arm64_32 Mach-O support, distinguish JVM identity before FAT admission, validate FAT format/CPU identity, preserve original DOS header padding, and reject NULs in local objdump/rizin paths. [#5788](https://github.com/morluto/rea/pull/5788), [6843dbe](https://github.com/morluto/rea/commit/6843dbea9bce4709ee2e8bf46c38f0bbf640716b), [497037e](https://github.com/morluto/rea/commit/497037e6e3975cbb37cfd89ea0ad300faa2a4769), [#7013](https://github.com/morluto/rea/pull/7013), [#3600](https://github.com/morluto/rea/pull/3600), [#5713](https://github.com/morluto/rea/issues/5713), [#6510](https://github.com/morluto/rea/issues/6510). Thanks @coygeek, @dhruvac29, @kaoru0822-kitauji, @Lew1sWong, @qorvr.
|
||||
- **Mach-O and dylibs:** Preserve container framing, facts when dyld rejects a missing UUID, verified-signature snapshot identity, unsigned diagnostics, absolute paths inside the selected root, legacy platform rules, lazy-load payloads, and reasons for rejecting present dylibs. [cf3288d](https://github.com/morluto/rea/commit/cf3288d8cf7a09685acf89103b36c32359be56fa), [2b263a7](https://github.com/morluto/rea/commit/2b263a7fe9e9d4c185c7d6af6b4e67b21a73af30), [eb7acc7](https://github.com/morluto/rea/commit/eb7acc70bf06f119e8639e30a78081d5ba82b0bf), [1502cfb](https://github.com/morluto/rea/commit/1502cfbc8ca0d1d1d86eebf334018dfb418bbac0), [90062c0](https://github.com/morluto/rea/commit/90062c092228dea9d33fadc0d7e1b968105b8b2c), [#5650](https://github.com/morluto/rea/pull/5650), [3b67b36](https://github.com/morluto/rea/commit/3b67b362aa2cd79eac5af57d0965e81a7d0b8c2b), [#5551](https://github.com/morluto/rea/issues/5551). Thanks @coygeek.
|
||||
- **Native facts:** Admit direct dispatch-metadata providers; preserve record-local Objective-C omissions, snapshot profiles, class-import binding identity, stripped class labels, version-like names, and qualified Swift protocol identities. [50b6975](https://github.com/morluto/rea/commit/50b6975d861069e2c6cfa202780621449f863802), [9eec57b](https://github.com/morluto/rea/commit/9eec57b469087fe9bff3d29dcc02f47074159e91), [0ba4d6f](https://github.com/morluto/rea/commit/0ba4d6f43ad610dacc861a1548be1a5c4712cd61), [4d7de00](https://github.com/morluto/rea/commit/4d7de00d4cc9a95ec2b7a3123b04e01db2daafa0), [22c84ce](https://github.com/morluto/rea/commit/22c84ce64aa6c7601aa81af86d4117b5a4c2feac), [4852c83](https://github.com/morluto/rea/commit/4852c830af1e3b9df437235b7a8fa8970a6d1c59), [aa9043f](https://github.com/morluto/rea/commit/aa9043f6b61c453e4735795cfe6a559bf6b6cc16).
|
||||
- **Apple resources:** Preserve independent plist scalars, processing instructions, nonstandard table widths, unsupported NIB connector coverage, and strict IPS JSON surrogate validation; classify Interface Builder objects by their declared superclass chains. [fd728f2](https://github.com/morluto/rea/commit/fd728f271e7de7bcfa63890b573715720d5f948c), [#5806](https://github.com/morluto/rea/pull/5806), [4526862](https://github.com/morluto/rea/commit/4526862728af01f1135714079c0d8a8c754eb364), [14cd78c](https://github.com/morluto/rea/commit/14cd78c3224edcf79fca152915077112f547b9ed), [#6990](https://github.com/morluto/rea/pull/6990), [#5805](https://github.com/morluto/rea/issues/5805), [#5228](https://github.com/morluto/rea/pull/5228), [#5218](https://github.com/morluto/rea/issues/5218). Thanks @coygeek, @DeryFerd, @kaoru0822-kitauji, @Mangolycheematcha, @mhamzasakhi, @Shreyash021104.
|
||||
- **Archives and files:** Complete positional ZIP reads, preserve bounded ASAR signatures, report stable symlink loops as resolution failures, retain target applicability, and report unknown traversal coverage. Retain reported integrity contradictions for members present in only one compared artifact. [#4414](https://github.com/morluto/rea/pull/4414), [e8da7e1](https://github.com/morluto/rea/commit/e8da7e1edd075feec0e6beedec93d5645fed8125), [#5571](https://github.com/morluto/rea/pull/5571), [#5763](https://github.com/morluto/rea/pull/5763), [#5867](https://github.com/morluto/rea/pull/5867), [#5521](https://github.com/morluto/rea/issues/5521), [#5731](https://github.com/morluto/rea/issues/5731), [#5837](https://github.com/morluto/rea/issues/5837), [#7076](https://github.com/morluto/rea/pull/7076). Thanks @Afloat16, @coygeek, @jdotc1, @kaoru0822-kitauji, @WenyuChiou.
|
||||
- **Extraction and file ownership:** Restrict destination-case recovery to owned collisions, retain physical-read failures through late cancellation/cleanup, preserve cleanup capabilities for retry, and retain independent firmware extraction failures. [932fbb7](https://github.com/morluto/rea/commit/932fbb7d84a9a995b9aee3c1be7da05a8c4d7bbb), [9499667](https://github.com/morluto/rea/commit/949966712e68d8857a50a165ecddf04c2d7e6d3b), [56679dd](https://github.com/morluto/rea/commit/56679dd2ae31dda58e181f8bf99775014f7bb82b), [b3c912a](https://github.com/morluto/rea/commit/b3c912ab79ad147a37b6cfbe1552a1a5eecec629), [7298bd0](https://github.com/morluto/rea/commit/7298bd08d134860b84b4073f6703021916a5a4c5).
|
||||
- **Processes and cancellation:** Retain partial samples and shutdown gaps, unresolved descendants and caller abort reasons; bind cleanup errors to their owners, propagate comparison contradictions, and report no normal exit code for signalled targets. [ba866a2](https://github.com/morluto/rea/commit/ba866a26ecbe179a717e746cbc0d1605ae616ad5), [#5270](https://github.com/morluto/rea/pull/5270), [#6144](https://github.com/morluto/rea/pull/6144), [a721c42](https://github.com/morluto/rea/commit/a721c42c07dc674c32ab939627355b8a1bee97a5), [#6298](https://github.com/morluto/rea/pull/6298), [#6486](https://github.com/morluto/rea/pull/6486), [#5935](https://github.com/morluto/rea/issues/5935), [#5944](https://github.com/morluto/rea/issues/5944), [#6100](https://github.com/morluto/rea/issues/6100), [#6101](https://github.com/morluto/rea/issues/6101), [#6110](https://github.com/morluto/rea/issues/6110), [#6113](https://github.com/morluto/rea/issues/6113), [#6121](https://github.com/morluto/rea/issues/6121), [#6131](https://github.com/morluto/rea/issues/6131), [#6132](https://github.com/morluto/rea/issues/6132), [#6218](https://github.com/morluto/rea/issues/6218), [#6445](https://github.com/morluto/rea/issues/6445). Thanks @agorevski, @coygeek, @gongfpp, @kwakhyun, @mhamzasakhi.
|
||||
- **Provider cleanup:** Preserve Windows native handles until close is confirmed, native/Inspector/Electron cleanup ownership, Kuna probe identity and cleanup, IDA registration configuration, and Hopper preparation ownership. [#5772](https://github.com/morluto/rea/pull/5772), [87ab054](https://github.com/morluto/rea/commit/87ab054ea384b3fcbae2998b1f94685edcd5f624), [ff78165](https://github.com/morluto/rea/commit/ff78165978de9dbcf9fa16c2acf74bf85a69dc44), [04ae07a](https://github.com/morluto/rea/commit/04ae07ab8c9ae1a6697b6ad15489bbde515da8b5), [dd0015a](https://github.com/morluto/rea/commit/dd0015aaf156c5701ef9598bcc6c885e6b8da803), [b231c28](https://github.com/morluto/rea/commit/b231c28cf64c2f799ada99343f8f21e0e6acb15e), [#4505](https://github.com/morluto/rea/issues/4505), [#5616](https://github.com/morluto/rea/issues/5616).
|
||||
- **Provider diagnostics:** Give Ghidra restart recovery, stable Homebrew aliases, unsupported-host results for Linux-only integrations, correct IDA regex/readiness handling, and clear diagnostics when Windows refuses a verified native addon. [#5356](https://github.com/morluto/rea/pull/5356), [7c6142e](https://github.com/morluto/rea/commit/7c6142efdc19a42c148d0e8c96b69d18e63c7f87), [#5793](https://github.com/morluto/rea/pull/5793), [#4862](https://github.com/morluto/rea/pull/4862), [99ccfb6](https://github.com/morluto/rea/commit/99ccfb6e709ac4bc0afe176315280abde9ebcb9f), [#6832](https://github.com/morluto/rea/pull/6832), [#5730](https://github.com/morluto/rea/issues/5730), [#4860](https://github.com/morluto/rea/issues/4860), [#5723](https://github.com/morluto/rea/issues/5723). Thanks @coygeek, @dark13b, @gensokyo-dev, @jgt87, @jpmorris41488, @Lew1sWong.
|
||||
- **MCP results:** Preserve protocol envelopes on oversized errors, enforce and advertise absolute Evidence paths, retain typed output-filter errors and selected JSON paths, and give client/provider timeout recovery. Compact advertisements now normalize reported enum and union shapes for strict gateway translation. [562dab0](https://github.com/morluto/rea/commit/562dab00c5cbf76a9b2695bad93792eccdc9a49d), [3cf6206](https://github.com/morluto/rea/commit/3cf6206614e65b5ebb6284a5709090dfb8f9aaee), [0e83d92](https://github.com/morluto/rea/commit/0e83d92c583ae36afe00b2c23dd2a0f1d025cbca), [4714c7e](https://github.com/morluto/rea/commit/4714c7eaa3d1d8b6a5df0aff586e844b9d9ab06d), [1bfa60a](https://github.com/morluto/rea/commit/1bfa60ac20e1a344bcf3132c08d15b6860b05c61), [#5608](https://github.com/morluto/rea/pull/5608), [#3806](https://github.com/morluto/rea/pull/3806), [#5598](https://github.com/morluto/rea/issues/5598), [#3800](https://github.com/morluto/rea/issues/3800). Thanks @gensokyo-dev, @Hahaa909, @wang1xiang.
|
||||
- **Browser lifecycles:** Retain historical outcomes through cleanup, stop late navigation before session release, stop expired work, observe aborted fetch settlement, and mark later scenario actions skipped after failure or cancellation. [2e5171e](https://github.com/morluto/rea/commit/2e5171e391b83f8a467d0861fd452c1f78621fd8), [599fdac](https://github.com/morluto/rea/commit/599fdac029912f96c511f859660483405d2de26f), [b2fad36](https://github.com/morluto/rea/commit/b2fad362f2b37f81dc9a6f784f4521e4ca24d41e), [13a8640](https://github.com/morluto/rea/commit/13a864020414577006dce58d7fdaa877ddd0cb52), [#6023](https://github.com/morluto/rea/pull/6023), [#5943](https://github.com/morluto/rea/issues/5943). Thanks @coygeek, @kaoru0822-kitauji.
|
||||
- **Browser comparisons:** Preserve frame identity, default origin scope, unknown body/shape coverage, correspondence and uncertainty, and independent retained-observation budgets. [#4040](https://github.com/morluto/rea/pull/4040), [#4413](https://github.com/morluto/rea/pull/4413), [#4476](https://github.com/morluto/rea/pull/4476), [a0c22ff](https://github.com/morluto/rea/commit/a0c22ff0125b8af7612db54d3708413e2bfecb25), [a0fcc60](https://github.com/morluto/rea/commit/a0fcc60d89eb0cc09b08181eb9f961acfd07d8e9), [#3878](https://github.com/morluto/rea/issues/3878), [#3910](https://github.com/morluto/rea/issues/3910), [#3941](https://github.com/morluto/rea/issues/3941), [#4385](https://github.com/morluto/rea/issues/4385), [#4477](https://github.com/morluto/rea/issues/4477). Thanks @Adolanium, @coygeek, @eco-null, @gensokyo-dev, @kaoru0822-kitauji, @XY-code1.
|
||||
- **Electron observations:** Preserve listener callback ownership and count registrations by source callsite; block trash calls in the active hook, retain window handles through cancelled selection, and preserve cancellation during finalization. [dfe8378](https://github.com/morluto/rea/commit/dfe8378cf5a727e3d93d47a5986233175c6ef898), [5064007](https://github.com/morluto/rea/commit/506400748eb1d6c674f0cf51330073929c45f939), [816a9dc](https://github.com/morluto/rea/commit/816a9dc98886e6d824cd8ae88b68731b161d5dda), [1707d17](https://github.com/morluto/rea/commit/1707d171d750955bf6387e028e378183b360ffe3), [4e2a566](https://github.com/morluto/rea/commit/4e2a566d6c7e053ecbbee1b684311ed31ea163bf).
|
||||
- **JavaScript source maps:** Keep unmapped and missing mappings unknown, preserve partial indexed-map facts and section paths, propagate preparation capability errors, and retain observed worker failures through cancellation. [a4dbb36](https://github.com/morluto/rea/commit/a4dbb36d8ea83e9f94dd289ac1a2d220d2c72c9b), [2b2e362](https://github.com/morluto/rea/commit/2b2e362a00fc5ba2f93fa6c62e2d8780072d1b4b), [498a667](https://github.com/morluto/rea/commit/498a667234b0140c1cfa43c819b35454ab11d14e), [#6149](https://github.com/morluto/rea/pull/6149), [#6214](https://github.com/morluto/rea/pull/6214), [f01dcca](https://github.com/morluto/rea/commit/f01dccab77f712fb4ac936d0a6e92a70d12f0789), [#6082](https://github.com/morluto/rea/issues/6082), [#6061](https://github.com/morluto/rea/issues/6061). Thanks @coygeek, @diyasharma05, @kaoru0822-kitauji.
|
||||
- **JavaScript semantics:** Preserve overflowing timer uncertainty, unavailable-scope comparisons, directory-form URL/ESM path intent, and artifact confinement. Replaced globals lose built-in effects; erased imports no longer become runtime bindings. Treat a known truthy `fork()` `silent` option as piped stdio unless a string/array `stdio` override takes precedence; unresolved options stay dynamic. [#3456](https://github.com/morluto/rea/pull/3456), [8d198d6](https://github.com/morluto/rea/commit/8d198d6ec707402d75276f516e0090d71767b7e4), [#4646](https://github.com/morluto/rea/pull/4646), [#2700](https://github.com/morluto/rea/pull/2700), [#4565](https://github.com/morluto/rea/pull/4565), [#6994](https://github.com/morluto/rea/pull/6994), [#3400](https://github.com/morluto/rea/issues/3400), [#3882](https://github.com/morluto/rea/issues/3882), [#3367](https://github.com/morluto/rea/issues/3367), [#7083](https://github.com/morluto/rea/pull/7083), [#7084](https://github.com/morluto/rea/issues/7084). Thanks @Adolanium, @amyanger, @coygeek, @gensokyo-dev, @HawkForge, @rohitg00, @viniciusdsandrade.
|
||||
- **JavaScript routes and imports:** Treat path data as route candidates, preserve route flag and lexical RPC identities, package-main trailing-slash selection, catch aliases, and factory parameter ownership. [#6360](https://github.com/morluto/rea/pull/6360), [38ecc02](https://github.com/morluto/rea/commit/38ecc0248e1993d25a2f7dd350024484cce797bb), [#4219](https://github.com/morluto/rea/pull/4219), [21097b1](https://github.com/morluto/rea/commit/21097b192b1ab94647e3446b1a2a4dd00e924d5e), [f774dc4](https://github.com/morluto/rea/commit/f774dc420b3befe2b8e68971b4116da2d62fc800), [#6349](https://github.com/morluto/rea/issues/6349), [#4189](https://github.com/morluto/rea/issues/4189). Thanks @coygeek, @hemv-857, @kaoru0822-kitauji, @Nikhi00718.
|
||||
- **Managed/IL2CPP:** Preserve extension-only scope uncertainty and type discovery after member limits, nested names, ReadyToRun/declaration facts, selected type scope, valid shared-suffix strings, and truncated IL2CPP metadata prefixes. [#5674](https://github.com/morluto/rea/pull/5674), [#5312](https://github.com/morluto/rea/pull/5312), [87fa1b6](https://github.com/morluto/rea/commit/87fa1b613d2b6bda1722855a00f1390f27731b64), [bc2d494](https://github.com/morluto/rea/commit/bc2d494030efd1ccabaeaa0bd49226aa87c378a2), [1f0eb22](https://github.com/morluto/rea/commit/1f0eb228e50908ee2a2aaa72d329178ae1a2cdbd), [4c5fae5](https://github.com/morluto/rea/commit/4c5fae5179dd54c4a0d427cd2a115b9c887e3841), [b3aadaf](https://github.com/morluto/rea/commit/b3aadaf474e59b33e5ff465236008a67eccefb87), [#5467](https://github.com/morluto/rea/issues/5467), [#5146](https://github.com/morluto/rea/issues/5146). Thanks @coygeek, @eco-null, @elricfd, @umi008.
|
||||
- **Format boundaries:** Preserve independent facts across malformed inputs in model, GPU, WASM, Rust, Android, and SQLite inspection; report optional metadata warnings without discarding core results. [eb41f35](https://github.com/morluto/rea/commit/eb41f358cf83d5626f09de68332f9718f9aa6482), [#5789](https://github.com/morluto/rea/pull/5789), [00a9e95](https://github.com/morluto/rea/commit/00a9e95de91a4bd4d2a41f8aee704a53fa575750), [fc63fe8](https://github.com/morluto/rea/commit/fc63fe8682654086261540dcf3c59afc36c7d58f), [39a91d4](https://github.com/morluto/rea/commit/39a91d4db96ca13224c25936330ea84fe78970e5), [6c6e2ad](https://github.com/morluto/rea/commit/6c6e2ad62b20f810b54f5a567e66fc55241514b2), [929a0a7](https://github.com/morluto/rea/commit/929a0a76c870d9991e015dde58308fd9df0280b8), [d137a4c](https://github.com/morluto/rea/commit/d137a4cdde0049de967f1d5cd429ed9e1f7d0a10), [ef475fc](https://github.com/morluto/rea/commit/ef475fcf8b0601c61052b7091568ba574546417d), [#5749](https://github.com/morluto/rea/issues/5749). Thanks @coygeek, @gensokyo-dev.
|
||||
- **Capture decoding:** Preserve malformed protobuf trailer facts and wire/schema identity, HTTP/2 URI and UTF-8 failure facts, WebMCP property coverage, and offline capture availability without a browser provider. Report invalid record ordinals as selection errors with actual bounds where available, and trim surrounding SP/HTAB from gRPC-web trailer values while preserving internal whitespace. [9cf7a7c](https://github.com/morluto/rea/commit/9cf7a7cb0d3c72a1121fde280cb11bf911af8055), [b3c3b21](https://github.com/morluto/rea/commit/b3c3b21e43cab4e71b460d08829d8c6e4e9b9c21), [4cb1f1d](https://github.com/morluto/rea/commit/4cb1f1d820df4498ee464bcf72a5f47d40cf8dab), [51e5938](https://github.com/morluto/rea/commit/51e593877642ab42fb5bf25812f4a4110ee7ac26), [d23be5c](https://github.com/morluto/rea/commit/d23be5c0898c39c9f0c54e2e0b43ee746d92d316), [#6725](https://github.com/morluto/rea/pull/6725), [#3947](https://github.com/morluto/rea/issues/3947), [#7067](https://github.com/morluto/rea/pull/7067), [#7068](https://github.com/morluto/rea/issues/7068), [#7069](https://github.com/morluto/rea/pull/7069), [#7071](https://github.com/morluto/rea/issues/7071). Thanks @Adolanium, @coygeek, @Sriram-PR.
|
||||
- **Evidence and errors:** Authenticate ledger Evidence before committing; preserve parent-schema diagnostics, canonical correction details and owned cleanup outcomes; identify the rejected function-Evidence side/kind. Fixed allowances now name their actual limits. [0407bbf](https://github.com/morluto/rea/commit/0407bbfbe65e8fa475e8bb0c78c30170e2c2f55e), [#6361](https://github.com/morluto/rea/pull/6361), [1629266](https://github.com/morluto/rea/commit/1629266e3c08c90fed6bba5383528af7986a12b7), [#6969](https://github.com/morluto/rea/pull/6969), [#5860](https://github.com/morluto/rea/pull/5860), [#6034](https://github.com/morluto/rea/issues/6034), [#5932](https://github.com/morluto/rea/issues/5932), [#5847](https://github.com/morluto/rea/issues/5847). Thanks @AyoubElleuch, @coygeek, @dhruvac29, @kaoru0822-kitauji.
|
||||
- **ADB and firmware recovery:** Report an unparsed push checksum as unverified, normalize absent installer-package sentinels, describe extraction-relative Unblob links, and avoid recommending a budget increase at its fixed ceiling. [#6010](https://github.com/morluto/rea/pull/6010), [#4073](https://github.com/morluto/rea/pull/4073), [#4573](https://github.com/morluto/rea/pull/4573), [#6971](https://github.com/morluto/rea/pull/6971), [#5924](https://github.com/morluto/rea/issues/5924), [#4175](https://github.com/morluto/rea/issues/4175). Thanks @coygeek, @Ganesh2325, @kaoru0822-kitauji, @Rinkia, @XiaoBinGan, @yankikucuk.
|
||||
- **CLI formatting:** Share Markdown report formatting, exclude inherited row values, and preserve arrays of empty objects as JSON. Render Markdown table headers and cells literally while keeping intraword underscores readable. [b784912](https://github.com/morluto/rea/commit/b784912811fd10ac9dc425696dc0e9af632afa34), [#6984](https://github.com/morluto/rea/pull/6984), [#7058](https://github.com/morluto/rea/pull/7058), [#7057](https://github.com/morluto/rea/issues/7057), [#7073](https://github.com/morluto/rea/pull/7073). Thanks @Plastic-time, @ShuaixinHuang, @viniciusdsandrade.
|
||||
- **Format identity:** Preserve Python marshal scalar/decode limits, empty optional BEAM chunks, PE resource branch identities and certificate alignment, unsupported compressed ELF notes, and codec-framed GPU bundle extents. [0603223](https://github.com/morluto/rea/commit/06032237c6755e3dd5f9a8db732c89958b238cc7), [c46064c](https://github.com/morluto/rea/commit/c46064c3f6eaba121c5de7b17390bfdecdd132ef), [66671e8](https://github.com/morluto/rea/commit/66671e874888e98f7e2b27e68a7c6fb320bbb827), [03077d5](https://github.com/morluto/rea/commit/03077d54afc49613db4e38f70c506bc60f5223aa), [#6721](https://github.com/morluto/rea/pull/6721), [9fb8c11](https://github.com/morluto/rea/commit/9fb8c114a03ad60e57c11586967c6df845a3f97e), [f6a1e2c](https://github.com/morluto/rea/commit/f6a1e2c892dc23c7583c3d03af9edb01632ff5a5), [#6694](https://github.com/morluto/rea/issues/6694). Thanks @coygeek, @Deepak-Kambala.
|
||||
- **WASM and APK metadata:** Preserve ambiguous WASM identity and partial facts, glue URL query/fragment text across line terminators, APK signer identity, and raw issuer identity when comparison rules are unknown. [af8f0ba](https://github.com/morluto/rea/commit/af8f0badf64fd3546392b6aa4625777904b2f051), [#6802](https://github.com/morluto/rea/pull/6802), [f783f0f](https://github.com/morluto/rea/commit/f783f0f1e82472cdddd4de2156d974f729e332a6), [115a78f](https://github.com/morluto/rea/commit/115a78fe241276c34917b496621954c5f29bfc15), [#6639](https://github.com/morluto/rea/issues/6639). Thanks @coygeek, @gensokyo-dev, @gongfpp.
|
||||
- **Source identity:** Preserve Next route spelling, module grammar and loader request identity, directory package-main behavior, accessor effects, exact source targets, and resolver-discovery versus module-loading distinctions. [ddd7ac7](https://github.com/morluto/rea/commit/ddd7ac7b846b48191ead1f7dcd0e3bd735468fa8), [9974512](https://github.com/morluto/rea/commit/99745126009b15060d71c38809418858a07d5afa), [f2e5fd1](https://github.com/morluto/rea/commit/f2e5fd148a85fa737302dd194a33172c4561093e), [baf1678](https://github.com/morluto/rea/commit/baf16784af3592534687052431c145daef55cfd1), [6e324b2](https://github.com/morluto/rea/commit/6e324b224405f2befd953234f6dc210cf72ad50a), [75daf0c](https://github.com/morluto/rea/commit/75daf0c7df108f04ab41f34aec1a31d4a7ceb322), [f91b7f8](https://github.com/morluto/rea/commit/f91b7f8009c9af27a5e0148e3b07d20bfe3b7a2a).
|
||||
- **Graphs and selected facts:** Preserve JavaScript graph omissions/version facts, Electron native-loader facts, web JSON identity/coverage, module-path selection, feature-trace qualifications, selected native paths, endpoint context, and mixed Swift demangler outcomes. [30db499](https://github.com/morluto/rea/commit/30db499db310f073d9bf3d3acfd77985e1cb7e2e), [5a7bddb](https://github.com/morluto/rea/commit/5a7bddb3aeb0345854e76b79d529aed1c40e2054), [79805f2](https://github.com/morluto/rea/commit/79805f244a1ec0850f3109929168a9c8ebb1b908), [f719dc4](https://github.com/morluto/rea/commit/f719dc46c51a55b3926eec9659d06e7dfb95132b), [7784fdb](https://github.com/morluto/rea/commit/7784fdbad78f8b384d513f0e7dae8e983cd48ef8), [76500f1](https://github.com/morluto/rea/commit/76500f12b54e07b1ea700a403b2593890d63b42a), [676d29f](https://github.com/morluto/rea/commit/676d29f9eef794cea668f5f09f0ff510c848a513), [edf7b3d](https://github.com/morluto/rea/commit/edf7b3d32c3248f221e27f7ce5a3d34e8590285b), [93de7bc](https://github.com/morluto/rea/commit/93de7bc7e324a761162a31625b838ba0d4e3824d).
|
||||
- **Managed boundaries:** Honor native platform symbol lookup, consume resolved IL2CPP ELF declarations, and attribute malformed comparison inputs to the supplied argument. [64fa1db](https://github.com/morluto/rea/commit/64fa1db1309218283df19f3ee4dba61f5fbd2a67), [87745af](https://github.com/morluto/rea/commit/87745af52ab0ccd3a172c2534c516d41b0095035), [96ad94f](https://github.com/morluto/rea/commit/96ad94f69eda5d68c7335ff775b6927b1376fd2b).
|
||||
- **Provider and worker recovery:** Preserve output loss after successful command exit, physical refusal diagnostics, bounded worker control framing, JavaScript worker-phase/budget attribution, admitted ambiguity choices, and selected EVM limiter availability. Scope objdump debug-link limitations to DWARF requests and keep process-capture preparation nonexecuting. [e639edf](https://github.com/morluto/rea/commit/e639edf6204c05285d96840478926592daf95876), [171b33b](https://github.com/morluto/rea/commit/171b33b2c4defc6cbea49545bedf4272b72d1ea2), [34563fc](https://github.com/morluto/rea/commit/34563fcc9bd92af30668820dcac5635e27181094), [3f9357b](https://github.com/morluto/rea/commit/3f9357b5990d166a38440f56f439452266650093), [cec8f1e](https://github.com/morluto/rea/commit/cec8f1e8f305d6332272457a6ee419d3c5fb6359), [a74d859](https://github.com/morluto/rea/commit/a74d859a19122fb8f1282b3632d320997b1caae0), [c284168](https://github.com/morluto/rea/commit/c284168c4b1cd214ce7f504d43da28528d82d603), [4fb936f](https://github.com/morluto/rea/commit/4fb936fe86bb7a07f94f819ae8510301f2ef69c9).
|
||||
- **Setup and limits:** Retain unknown environment-budget facts, selected-skill/maintenance uncertainty, Hopper URL admission and bookmark verification, and listener failure recovery that matches the system cause. [a718037](https://github.com/morluto/rea/commit/a7180370332350bc0f998fbd643888221759570e), [d1fbebb](https://github.com/morluto/rea/commit/d1fbebb578538dc94fd5e376e6e1959218efa835), [60accad](https://github.com/morluto/rea/commit/60accad2de8f2862fe91901502fe26cc5390fb36), [fd7da77](https://github.com/morluto/rea/commit/fd7da77fc02dcfaf7d1bc9f2fb240d71cdd27c59), [0c440bf](https://github.com/morluto/rea/commit/0c440bfc07f3cb343e2e836d27cccaed539cfe9c).
|
||||
- **State and Evidence:** Retain export tarball ownership and SQLite primary outcomes through cleanup retry, count emitted SQLite schema names across encodings, scope display refresh to matching snapshot bindings, and report retained-result validation failures consistently. Reconstruction uses one effective completion scope and kind-based labels when names are blank. [ab58edc](https://github.com/morluto/rea/commit/ab58edcdf50b4bd687900ee2dc362ecc5c3dce15), [1006714](https://github.com/morluto/rea/commit/100671427105791a5c5913a918b63a394f6bc83e), [a47788a](https://github.com/morluto/rea/commit/a47788a6a4abb067273f954275a9d0a269adf1e7), [8aa40e9](https://github.com/morluto/rea/commit/8aa40e9a61155cff8b6f3c138c601bf805b20653), [b8c1b45](https://github.com/morluto/rea/commit/b8c1b45316882beeab4a9de49381d94e131fab0f), [a35b933](https://github.com/morluto/rea/commit/a35b9335a0124674ee5e3abe8c075f1fff6b4fa6), [#6137](https://github.com/morluto/rea/pull/6137), [#6126](https://github.com/morluto/rea/issues/6126). Thanks @carlogiovannineri-rgb, @mohithhhh.
|
||||
- **Capture coverage:** Name browser bundle budgets and deadline recovery, separate domain readiness from worker attribution, attribute rendered-frame omissions to their producing stage, share offline-capture host admission with discovery, preserve selected signal-file semantics, and retain short Flutter GNU build IDs. [#6036](https://github.com/morluto/rea/pull/6036), [ba78133](https://github.com/morluto/rea/commit/ba781330d6d28b22d15498e63c144fb40e086156), [5934387](https://github.com/morluto/rea/commit/5934387f4185129efe9dc8b7338d8bc3fa5b25bd), [ad629b2](https://github.com/morluto/rea/commit/ad629b2e31c3a9495b27179360b508fa021fe86a), [928ba76](https://github.com/morluto/rea/commit/928ba76882bfd216dde8ed79118789b2c3aad4f9), [678c4cf](https://github.com/morluto/rea/commit/678c4cfa68e2fee12f620075e12dc87d9b1195b0).
|
||||
- **Additional boundary facts:** Retain parser format/query occurrence identity and independent malformed-input facts, unresolved nested Swift identifiers, and completed signing-capture coverage; describe crash documents without assuming a provider. [5f702dd](https://github.com/morluto/rea/commit/5f702dd6ccd9e8e505e224854b91678439c3b41f), [99d2d7a](https://github.com/morluto/rea/commit/99d2d7ad22bcbd780919760008aba6a88e7d1077), [90e433d](https://github.com/morluto/rea/commit/90e433df06b3f83417176b8db1b0a905f9df1d3a), [#5548](https://github.com/morluto/rea/pull/5548), [ee12d51](https://github.com/morluto/rea/commit/ee12d517a25d34853d40e31fc039c7d1576c60e1), [#5538](https://github.com/morluto/rea/issues/5538). Thanks @coygeek, @ShuaixinHuang.
|
||||
- **Documentation:** Add the Romanian README. [#7026](https://github.com/morluto/rea/pull/7026). Thanks @An3treiu.
|
||||
|
||||
Full contribution record: [1740 commits and 497 merged PRs](https://github.com/morluto/rea/blob/rea-agents-6.8.0/docs/releases/6.8.0.contributions.json).
|
||||
|
||||
## [6.7.0](https://github.com/morluto/rea/compare/rea-agents-6.6.0...rea-agents-6.7.0) (2026-10-18)
|
||||
|
||||
### Highlights
|
||||
|
||||
@@ -15,6 +15,11 @@ import sys
|
||||
PROFILE = "pwntools@4.15.0;pyelftools@0.33;unicorn@2.1.2"
|
||||
PACKAGES = {"pwntools": "4.15.0", "pyelftools": "0.33", "unicorn": "2.1.2"}
|
||||
OUTPUT_BYTES = 64 * 1024 * 1024
|
||||
# (e_machine, ELF class bits, byte order) -> architecture pwntools must also report.
|
||||
PROFILES = {(62, 64, "little"): "amd64", (3, 32, "little"): "i386", (40, 32, "little"): "arm",
|
||||
(8, 32, "little"): "mips", (8, 32, "big"): "mips"}
|
||||
PROFILE_SCOPE = ("Layout profile requires ELF64 x86-64, ELF32 i386, ELF32 ARM little-endian "
|
||||
"or ELF32 MIPS little- or big-endian objects.")
|
||||
|
||||
|
||||
class LayoutFailure(Exception):
|
||||
@@ -116,13 +121,16 @@ def inspect_elf(path, cache):
|
||||
raise LayoutFailure("format", "Selected object does not contain an ELF header.")
|
||||
if len(content) < 16:
|
||||
raise LayoutFailure("format", "Selected ELF identification is truncated.")
|
||||
if content[4] != 2 or content[5] != 1:
|
||||
raise LayoutFailure("unsupported-target", "Initial layout profile requires ELF64 little-endian objects.")
|
||||
if len(content) < 64:
|
||||
raise LayoutFailure("format", "Selected ELF64 header is truncated.")
|
||||
if int.from_bytes(content[18:20], "little") != 62:
|
||||
raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF objects.")
|
||||
if int.from_bytes(content[16:18], "little") not in (1, 2, 3):
|
||||
if content[4] not in (1, 2) or content[5] not in (1, 2):
|
||||
raise LayoutFailure("unsupported-target", "Selected ELF declares an unknown class or byte order.")
|
||||
bits, header_size = (32, 52) if content[4] == 1 else (64, 64)
|
||||
order = "little" if content[5] == 1 else "big"
|
||||
if len(content) < header_size:
|
||||
raise LayoutFailure("format", f"Selected ELF{bits} header is truncated.")
|
||||
profile = (int.from_bytes(content[18:20], order), bits, order)
|
||||
if profile not in PROFILES:
|
||||
raise LayoutFailure("unsupported-target", PROFILE_SCOPE)
|
||||
if int.from_bytes(content[16:18], order) not in (1, 2, 3):
|
||||
raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.")
|
||||
from pwnlib.context import context
|
||||
with context.local(cache_dir=str(cache), log_level="warning"):
|
||||
@@ -185,8 +193,8 @@ def inspect_elf(path, cache):
|
||||
if type is None or section.header.sh_type == type)
|
||||
try:
|
||||
image = LayoutELF(str(path), checksec=False)
|
||||
if image.arch != "amd64" or image.bits != 64 or image.endian != "little":
|
||||
raise LayoutFailure("unsupported-target", "Initial layout profile requires x86-64 ELF64 little-endian objects.")
|
||||
if (image.arch, image.bits, image.endian) != (PROFILES[profile], bits, order):
|
||||
raise LayoutFailure("unsupported-target", PROFILE_SCOPE)
|
||||
if image.header.e_type not in ("ET_EXEC", "ET_DYN", "ET_REL"):
|
||||
raise LayoutFailure("unsupported-target", "Selected ELF type is outside the EXEC/DYN/REL layout profile; recorded core analysis is separate.")
|
||||
length = len(content)
|
||||
@@ -208,7 +216,7 @@ def inspect_elf(path, cache):
|
||||
})
|
||||
if h.sh_type in ("SHT_SYMTAB", "SHT_DYNSYM"):
|
||||
if h.sh_entsize < image.structs.Elf_Sym.sizeof():
|
||||
raise LayoutFailure("format", "ELF symbol entry is smaller than the decoded Elf64_Sym structure.")
|
||||
raise LayoutFailure("format", f"ELF symbol entry is smaller than the decoded Elf{bits}_Sym structure.")
|
||||
if h.sh_link >= len(all_sections) or all_sections[h.sh_link].header.sh_type != "SHT_STRTAB":
|
||||
raise LayoutFailure("format", f"ELF symbol table {index} links to invalid string table {h.sh_link}.")
|
||||
strings = all_sections[h.sh_link]
|
||||
|
||||
+4
-1
@@ -31,7 +31,10 @@ stackshots, return no crash result.
|
||||
`process`, `exception`, `termination`, `faulting_thread`, `threads`, and
|
||||
`images` reproduce documented body fields when present, including each image's
|
||||
`name` and each frame's `symbolLocation` (as `symbol_location`, the byte offset
|
||||
from the start of `recorded_symbol`). Integers keep their
|
||||
from the start of `recorded_symbol`). `termination` keeps the terminating
|
||||
process as `by_proc` and `by_pid` and its `flags`; `process.parent_pid` keeps
|
||||
`parentPid` next to `parent`; and `metadata` keeps `build_version` and
|
||||
`platform`. Integers keep their
|
||||
decimal literals and a hexadecimal form, including arm64e registers above
|
||||
2^53. Frame roles follow the recorded frame index: row zero of the resolved
|
||||
faulting thread is `faulting_instruction`, row zero of other threads is
|
||||
|
||||
@@ -8,9 +8,12 @@ ordinary binary inspection use the same modular capability.
|
||||
|
||||
## Bring your own engine
|
||||
|
||||
Initial real verification covers **Linux x64, ELF64 x86-64 little-endian
|
||||
EXEC/DYN/REL**. Provide an absolute Python executable whose environment already
|
||||
contains unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2:
|
||||
Real verification covers **Linux x64** hosts and **EXEC/DYN/REL** objects in
|
||||
these profiles: ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32
|
||||
little- or big-endian MIPS. Other machines, classes and byte orders, including
|
||||
AArch64 and big-endian ARM, return `unsupported_target`. Provide an absolute
|
||||
Python executable whose environment already contains unchanged pwntools 4.15.0,
|
||||
pyelftools 0.33 and Unicorn 2.1.2:
|
||||
|
||||
```sh
|
||||
REA_PWNTOOLS_PYTHON=/absolute/isolated-env/bin/python \
|
||||
|
||||
+4
-2
@@ -924,8 +924,10 @@ redirected, which keeps large results smaller for agents and scripts; pipe
|
||||
through `jq .` for indented files. Output selection and formatting do not change
|
||||
operation status.
|
||||
|
||||
`--format md` escapes backslashes and pipes in table headers and cells, and
|
||||
renders cell line breaks as `<br>`. These are presentation changes; retained
|
||||
`--format md` escapes backslashes, pipes, and inline Markdown syntax in table
|
||||
headers and cells so source text is displayed literally, and renders cell line
|
||||
breaks as `<br>`. Underscores between letters or digits, as in `source_index`,
|
||||
cannot form emphasis and stay unescaped. These are presentation changes; retained
|
||||
Evidence and JSON output keep the original values. Nested objects and arrays
|
||||
use JSON inside their owning Markdown cells, retaining the field names and
|
||||
values instead of replacing them with an object label. Table widths are measured
|
||||
|
||||
@@ -15,7 +15,7 @@ snapshot parser and a separate optional annotation extension. Check the
|
||||
[release boundary](installation.md#released-package-and-main) when using npm.
|
||||
|
||||
See [import language and analysis seeds](installation.md#ghidra-import-language-and-analysis-seeds)
|
||||
to override the header-detected language or seed analysis. An override that conflicts with a fixed raw, DOS, PSP, MIPS or RISC-V import
|
||||
to override the header-detected language or seed analysis. An override that conflicts with a fixed raw, DOS, PSP, MIPS, RISC-V or GameCube DOL import
|
||||
is a configuration error naming the conflicting settings. It does not make an
|
||||
otherwise admitted target unsupported; REA preserves the requested settings.
|
||||
An inaccessible language
|
||||
|
||||
@@ -319,14 +319,14 @@ Register with the variable unset:
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
```bash
|
||||
claude mcp add --scope local rea -- npx -y rea-agents@6.7.0 mcp
|
||||
claude mcp add --scope local rea -- npx -y rea-agents@6.8.0 mcp
|
||||
```
|
||||
|
||||
On native Windows, use the same project directory and wrap the package-runner
|
||||
shim so the stdio client can launch it:
|
||||
|
||||
```powershell
|
||||
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.7.0 mcp
|
||||
claude mcp add --scope local rea -- cmd.exe /d /c npx -y rea-agents@6.8.0 mcp
|
||||
```
|
||||
|
||||
<!-- x-release-please-end -->
|
||||
@@ -620,8 +620,8 @@ hosted connector; see [its configuration](client-configuration.md#other-client-s
|
||||
|
||||
`droid`, `dsh`, `kimi`, `qoder`, `trae`, `workbuddy`, and `zcode` require
|
||||
`rea-agents@6.5.0` or later. The 6.4.0 registry check above does not cover them.
|
||||
`freebuff` is not in a release yet - `6.7.0` is the latest - so it also needs a
|
||||
release containing it.
|
||||
`freebuff` requires `rea-agents@6.8.0` or later. Older releases reject
|
||||
`rea setup --client freebuff` as an unsupported client.
|
||||
|
||||
See [client configuration and troubleshooting](client-configuration.md) for
|
||||
configuration paths, profiles, trust policies, and schema-limit workarounds.
|
||||
@@ -1471,7 +1471,7 @@ For a client that requires manual configuration, use:
|
||||
"mcpServers": {
|
||||
"rea": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "rea-agents@6.7.0", "mcp"]
|
||||
"args": ["-y", "rea-agents@6.8.0", "mcp"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,8 @@
|
||||
# Implementation plan
|
||||
|
||||
1. **Read-backs.** A create's observation looks for the first read of its entity before the next write; an item read's
|
||||
fields and clock spellings are taken when it agrees otherwise, and the read observes; a list read adds fields only. The plan's observation records the
|
||||
read and its shape, and serving merges the same objects.
|
||||
2. **Sent members.** An update's sent member stored as a fresh clock after it sent back the held one is a `clock`; one
|
||||
stored as the slug of another member stored as sent is a `slug`.
|
||||
3. **Schema, tests and docs.**
|
||||
@@ -0,0 +1,47 @@
|
||||
# Writes whose answers show the entity otherwise than reads, and whole-object updates
|
||||
|
||||
Issue: https://github.com/morluto/rea/issues/4933
|
||||
|
||||
## Outcome
|
||||
|
||||
Agents built environments from a prompt for five applications the prompt flow had never seen (Vikunja, Ghost, EspoCRM,
|
||||
Paperless-ngx and Miniflux). Two write-model gaps withheld most of their creates and edits:
|
||||
|
||||
- **A create's answer is another view of the entity than its reads.** Paperless answers a tag create without the
|
||||
`document_count` its item read shows. EspoCRM answers a contact create without nine fields its item read shows.
|
||||
Vikunja answers a label create with a partial `created_by` and clocks to the nanosecond, where reads show the full
|
||||
user and whole seconds. Ghost answers a tag create without the `count` its list shows. The state built the entity from
|
||||
the answer alone, so the read-back contradicted the create.
|
||||
- **A whole-object update sends back what the application derives.** Paperless's UI sends the tag's old `slug` with a
|
||||
new name, and the application slugs the name anew. Ghost's UI sends the `updated_at` it read, and the application
|
||||
stamps a new one. The update was withheld as `unexplained-change`, or its first call's value was pinned so that later
|
||||
calls conflicted.
|
||||
|
||||
## Behaviour
|
||||
|
||||
- **Read-backs.** When the first read of a created entity before the next write shows fields the create's answer lacks,
|
||||
or spells its clocks to another precision, the observed entity takes them, and the plan records the read
|
||||
(`observation.read`, with its shape).
|
||||
- An item read that agrees with the answer otherwise lends its fields and its clock spellings (whole seconds of a
|
||||
nanosecond clock, or microseconds of one), and no longer confirms the create. One that disagrees on another value,
|
||||
or omits an answer field its item view renders, still checks the create, which it contradicts.
|
||||
- A list read, which may show fewer keys of an object, only adds fields the answer lacks and still checks the create.
|
||||
- A read that shows nothing new leaves the observation as it was and still confirms the create.
|
||||
- Serving merges the same two recorded objects, so a served create answers in its answer's shape and stores the
|
||||
fields of both. Generated clocks in the create answer retain that answer's recorded precision independently of
|
||||
the stored read-back precision.
|
||||
- **Restamped clocks.** An update member sent back as the clock the entity held, and stored as a fresh clock near the
|
||||
call, is a `clock` rule, accepted with any value.
|
||||
- **Re-derived slugs.** An update member stored as the slug of another member the call stored as sent is a `slug` rule
|
||||
from that member, accepted with any value.
|
||||
- **Compatible saves.** A call that echoes an already-derived slug or holds a clock in its own recorded second does
|
||||
not conflict with a demonstrated derivation, in either call order. A slug derivation must reproduce earlier saves
|
||||
as well; an explicit custom slug that disagrees still withholds the update.
|
||||
|
||||
Plans and views that need none of this are unchanged.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- The held-out recordings serve EspoCRM's contact create and Paperless's tag edit.
|
||||
- A create whose item read-back disagrees with its answer on another value is still contradicted.
|
||||
- No write tool of the 69 recorded captures goes from served to withheld.
|
||||
@@ -0,0 +1,8 @@
|
||||
# Tasks
|
||||
|
||||
- [x] Take a create's read-back fields into its observation, and serve the same merge.
|
||||
- [x] Learn restamped clocks and re-derived slugs for sent update members.
|
||||
- [x] Add view and serving tests that fail without the change.
|
||||
- [x] Document the observation member and the rules.
|
||||
- [x] Measure the held-out and recorded captures.
|
||||
- [x] Run static, formatting, documentation and test gates.
|
||||
@@ -0,0 +1,48 @@
|
||||
# Verification
|
||||
|
||||
Executed on macOS with Node v24.22.1 at `2f807efb` (main `8e6a9578` and this branch).
|
||||
|
||||
## Gates
|
||||
|
||||
- `npm run check` (build, typecheck, format, lint, typed lint, module boundaries, architecture guards) and
|
||||
`npm run docs:check`: passed.
|
||||
- `npx vitest run --project domain --project services --project adapters`: 17,624 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 analysis-view, environment build, compare, export and serve CLI boundaries, the
|
||||
analysis-view, environment bundle, serve and served-writes MCP boundaries, tool schema validity, output schema profile
|
||||
and MCP schema validation: 12 files, 106 passed.
|
||||
- The eight new view and serving tests fail on main and pass here.
|
||||
|
||||
## Recorded captures
|
||||
|
||||
The 69 recorded captures and the five held-out agent recordings, built as environment-state views on main and on this
|
||||
branch:
|
||||
|
||||
| Captures | Served on main | Served here | Served to withheld |
|
||||
| ----------------------------------------- | -------------- | ----------- | ------------------ |
|
||||
| 69 recorded | 132 | 132 | none |
|
||||
| 5 held-out (20 write tools with a target) | 8 | 10 | none |
|
||||
|
||||
- **Now served:** EspoCRM's contact create (its read-back adds nine fields the answer lacks) and Paperless's tag edit
|
||||
(the stale slug is slugged anew from the name).
|
||||
- **Past the read-back, withheld by other gaps:**
|
||||
- Paperless's tag create: its list's `all` member lists every id, which state rendering does not model.
|
||||
- Ghost's tag create and edit: the state does not list the created tags in Ghost's tag lists.
|
||||
- **Still contradicted:** Vikunja's label create, whose item read-back shows a fully loaded `created_by` where the answer
|
||||
showed a partial one.
|
||||
- Three plans of the recorded captures (Airbyte, BookStack, PhotoPrism) now record a read-back; their served state is
|
||||
unchanged.
|
||||
|
||||
## Served write tasks
|
||||
|
||||
The held-out round's write tasks, rebuilt from the same agent recordings with this branch, with
|
||||
`fireworks_ai/gpt-oss-120b` and `fireworks_ai/glm-5p3-flash`:
|
||||
|
||||
| Application | Main (gpt-oss, GLM) | This branch (gpt-oss, GLM) |
|
||||
| ---------------------------------------------- | ------------------- | -------------------------- |
|
||||
| EspoCRM contacts: create, update email, delete | 0/3, 0/3 | 1/3, 1/3 (create) |
|
||||
| Paperless tags: create, rename, delete | 1/3, 1/3 | 1/3, 2/3 (rename with GLM) |
|
||||
|
||||
- gpt-oss's rename did not apply. Its trajectory was not kept, so the cause is unconfirmed. A served `PUT` requires
|
||||
every member the recorded call sent (about 14 here), and GLM sent them all.
|
||||
- EspoCRM's served update accepts only the members its recording edited, so the email change is refused, as on main.
|
||||
+6
-2
@@ -2008,12 +2008,16 @@ coverage limits.
|
||||
|
||||
### Offline binary layout
|
||||
|
||||
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, absolute
|
||||
`npm run verify:binary:layout` requires Linux x64, GCC/binutils, the
|
||||
`i686-linux-gnu`, `arm-linux-gnueabi`, `mips-linux-gnu` and `mipsel-linux-gnu`
|
||||
cross `gcc`/`readelf` toolchains, absolute
|
||||
`REA_PWNTOOLS_PYTHON` with pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2 and
|
||||
absolute `REA_VERIFY_STRACE_COMMAND`. It compiles ephemeral source-owned ELF
|
||||
fixtures and checks public CLI/MCP, lossless addresses/names, file ranges,
|
||||
mitigation inferences, malformed/unsupported input, original file hashes and
|
||||
released process ownership. Exec syscall tracing must identify only the declared
|
||||
released process ownership. Cross-compiled ELF32 executables and relocatable
|
||||
objects check the reported class, machine and byte order, and every section
|
||||
name, address, offset and size against GNU `readelf`. Exec syscall tracing must identify only the declared
|
||||
Node/Python launchers; no target binary is executed. Core/debugger claims need
|
||||
separate verification lanes. Pass an installed package entrypoint as the script's
|
||||
first argument to verify packaging independently of the checkout.
|
||||
|
||||
@@ -746,7 +746,16 @@ 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
|
||||
fields keep the observed value. When the first read of a created entity before
|
||||
the next write shows fields the create's answer lacks, or its clocks to another
|
||||
precision, the observed entity takes them (`observation.read`). An item read
|
||||
that agrees with the answer otherwise lends its fields and clock spellings and
|
||||
no longer confirms the create; one that disagrees or omits an answer field its
|
||||
item view renders still checks it. Clock agreement uses the coarser recorded
|
||||
precision, including microseconds. A list read
|
||||
only adds fields the answer lacks and still checks it. Serving answers a create
|
||||
in its answer's shape and clock precision and stores the fields of both with
|
||||
the item read-back's clock precision. 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
|
||||
@@ -778,7 +787,14 @@ 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
|
||||
echo when stored as sent. A clock sent back as the entity held it and stored as
|
||||
a fresh clock near the call is a `clock`, and a member stored as the slug of
|
||||
another member stored as sent is a `slug`, both accepted with any value, as a
|
||||
whole-object write that sends what it read shows. A save that echoes an
|
||||
already-derived slug or holds a clock in its own second remains compatible
|
||||
with that derivation in either call order; earlier saves must also reproduce
|
||||
a newly learned slug. Any other member stored
|
||||
differently is accepted only with its
|
||||
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`
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"version": "6.7.0",
|
||||
"version": "6.8.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rea-agents",
|
||||
"version": "6.7.0",
|
||||
"version": "6.8.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/parser": "8.0.4",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rea-agents",
|
||||
"mcpName": "io.github.morluto/rea",
|
||||
"version": "6.7.0",
|
||||
"version": "6.8.0",
|
||||
"description": "Reverse engineer anything from your terminal or agent with one CLI and MCP server.",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
|
||||
+5294
File diff suppressed because it is too large.
Load diff
@@ -104,7 +104,7 @@ export async function readReleaseHistory(run, base, target) {
|
||||
|
||||
// GitHub maps these coding-agent trailers to ordinary User actors.
|
||||
// Keep those source identities in the inventory while excluding automation credit.
|
||||
const AGENT_ACCOUNTS = new Set(["claude", "cursoragent", "codex"]);
|
||||
const AGENT_ACCOUNTS = new Set(["claude", "cursoragent", "codex", "copilot"]);
|
||||
// REA's maintainer requested external-contributor thanks without self-credit.
|
||||
const MAINTAINER_ACCOUNTS = new Set(["morluto"]);
|
||||
const human = (value) =>
|
||||
|
||||
@@ -35,11 +35,27 @@ if (
|
||||
!isAbsolute(strace ?? "")
|
||||
)
|
||||
throw new Error(
|
||||
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld and strip must be available. No target is executed.",
|
||||
"verify:binary:layout requires Linux x64, absolute REA_PWNTOOLS_PYTHON (pwntools 4.15.0/pyelftools 0.33/Unicorn 2.1.2) and REA_VERIFY_STRACE_COMMAND. gcc, ld, strip and the i686/ARM/MIPS/MIPSel linux-gnu cross gcc and readelf must be available. No target is executed.",
|
||||
);
|
||||
const run = createVerifierRun();
|
||||
const execute = promisify(execFile);
|
||||
for (const command of ["gcc", "ld", "strip", "/usr/bin/prlimit", strace]) {
|
||||
const ELF32_TOOLCHAINS = [
|
||||
["i686-linux-gnu", "EM_386", "little"],
|
||||
["arm-linux-gnueabi", "EM_ARM", "little"],
|
||||
["mips-linux-gnu", "EM_MIPS", "big"],
|
||||
["mipsel-linux-gnu", "EM_MIPS", "little"],
|
||||
];
|
||||
for (const command of [
|
||||
"gcc",
|
||||
"ld",
|
||||
"strip",
|
||||
"/usr/bin/prlimit",
|
||||
strace,
|
||||
...ELF32_TOOLCHAINS.flatMap(([prefix]) => [
|
||||
`${prefix}-gcc`,
|
||||
`${prefix}-readelf`,
|
||||
]),
|
||||
]) {
|
||||
try {
|
||||
await execute(command, [command === strace ? "-V" : "--version"], {
|
||||
timeout: 10_000,
|
||||
@@ -328,6 +344,163 @@ try {
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
// ELF32 profiles: cross-compiled, never executed, checked against GNU readelf.
|
||||
for (const [
|
||||
prefix,
|
||||
machine,
|
||||
byteOrder,
|
||||
kind,
|
||||
flags,
|
||||
] of ELF32_TOOLCHAINS.flatMap((toolchain) => [
|
||||
[...toolchain, "executable", ["-fPIE", "-pie"]],
|
||||
[...toolchain, "relocatable", ["-c", "-fPIC"]],
|
||||
])) {
|
||||
const path = join(root.path, `${prefix}-${kind}`);
|
||||
await execute(`${prefix}-gcc`, ["-O1", ...flags, source, "-o", path], {
|
||||
timeout: 30_000,
|
||||
maxBuffer: 1024 * 1024,
|
||||
});
|
||||
const { stdout } = await execute(`${prefix}-readelf`, ["-SWsr", path], {
|
||||
timeout: 10_000,
|
||||
maxBuffer: 16 * 1024 * 1024,
|
||||
env: { ...process.env, LC_ALL: "C" },
|
||||
});
|
||||
const expectedSections = [
|
||||
...stdout.matchAll(
|
||||
/^\s*\[\s*(\d+)\]\s+(\S+)\s+\S+\s+([0-9a-f]+)\s+([0-9a-f]+)\s+([0-9a-f]+)\s+[0-9a-f]+\s+[A-Za-z]*\s+(\d+)\s+\d+\s+\d+\s*$/gm,
|
||||
),
|
||||
]
|
||||
.filter(([, index]) => index !== "0")
|
||||
.map(([, index, name, address, offset, size, link]) => ({
|
||||
index: Number(index),
|
||||
name,
|
||||
address: BigInt(`0x${address}`),
|
||||
offset: BigInt(`0x${offset}`),
|
||||
size: BigInt(`0x${size}`),
|
||||
link: Number(link),
|
||||
}));
|
||||
assert.ok(
|
||||
expectedSections.length > 0,
|
||||
`readelf listed no sections for ${path}`,
|
||||
);
|
||||
const expectedSymbols = [];
|
||||
for (const [, name, count, body] of stdout.matchAll(
|
||||
/^Symbol table '([^']+)' contains (\d+) entr(?:y|ies):\n((?:[^\n]+\n?)*)/gm,
|
||||
)) {
|
||||
const table = expectedSections.find((section) => section.name === name);
|
||||
assert.notEqual(table, undefined, `readelf symbol table absent: ${name}`);
|
||||
const rows = [...body.matchAll(/^\s*(\d+):\s+([0-9a-f]+)\s+(\d+)\s/gm)];
|
||||
assert.equal(rows.length, Number(count), `readelf symbol count: ${name}`);
|
||||
for (const [, index, value, size] of rows)
|
||||
expectedSymbols.push({
|
||||
table_index: table.index,
|
||||
entry_index: Number(index),
|
||||
value: BigInt(`0x${value}`),
|
||||
size: BigInt(size),
|
||||
});
|
||||
}
|
||||
assert.ok(
|
||||
expectedSymbols.length > 0,
|
||||
`readelf listed no symbols for ${path}`,
|
||||
);
|
||||
const expectedRelocations = [];
|
||||
for (const [, name, count, body] of stdout.matchAll(
|
||||
/^Relocation section '([^']+)' at offset 0x[0-9a-f]+ contains (\d+) entr(?:y|ies):\n((?:[^\n]+\n?)*)/gm,
|
||||
)) {
|
||||
const section = expectedSections.find((section) => section.name === name);
|
||||
assert.notEqual(
|
||||
section,
|
||||
undefined,
|
||||
`readelf relocation section absent: ${name}`,
|
||||
);
|
||||
const rows = [...body.matchAll(/^\s*([0-9a-f]+)\s+([0-9a-f]+)\s+\S+/gm)];
|
||||
assert.equal(
|
||||
rows.length,
|
||||
Number(count),
|
||||
`readelf relocation count: ${name}`,
|
||||
);
|
||||
for (const [entryIndex, [, offset, info]] of rows.entries()) {
|
||||
// ELF32 r_info uses the low byte for type and the upper 24 bits for
|
||||
// the symbol index. readelf has already decoded the file's byte order.
|
||||
const decodedInfo = BigInt(`0x${info}`);
|
||||
expectedRelocations.push({
|
||||
section_index: section.index,
|
||||
entry_index: entryIndex,
|
||||
reported_offset: BigInt(`0x${offset}`),
|
||||
type: Number(decodedInfo & 0xffn),
|
||||
symbol_table_index: section.link,
|
||||
symbol_index: Number(decodedInfo >> 8n),
|
||||
});
|
||||
}
|
||||
}
|
||||
assert.ok(
|
||||
expectedRelocations.length > 0,
|
||||
`readelf listed no relocations for ${path}`,
|
||||
);
|
||||
const bytes = await readFile(path);
|
||||
for (const mode of ["cli", "mcp"]) {
|
||||
const value = await inspect(mode, path);
|
||||
assert.deepEqual(value.architecture, {
|
||||
machine,
|
||||
bits: 32,
|
||||
byte_order: byteOrder,
|
||||
});
|
||||
assert.equal(
|
||||
value.image_type,
|
||||
kind === "executable" ? "ET_DYN" : "ET_REL",
|
||||
);
|
||||
for (const section of expectedSections) {
|
||||
const reported = value.sections[section.index];
|
||||
assert.equal(reported.name.display, section.name);
|
||||
assert.equal(BigInt(reported.address), section.address);
|
||||
assert.equal(BigInt(reported.offset), section.offset);
|
||||
assert.equal(BigInt(reported.size), section.size);
|
||||
}
|
||||
assert.equal(value.sections.length, expectedSections.length + 1);
|
||||
assert.deepEqual(
|
||||
value.symbols.map(({ table_index, entry_index, value, size }) => ({
|
||||
table_index,
|
||||
entry_index,
|
||||
value: BigInt(value),
|
||||
size: BigInt(size),
|
||||
})),
|
||||
expectedSymbols,
|
||||
`${mode} symbol values differ from readelf for ${path}`,
|
||||
);
|
||||
assert.deepEqual(
|
||||
value.relocations.map(
|
||||
({
|
||||
section_index,
|
||||
entry_index,
|
||||
reported_offset,
|
||||
type,
|
||||
symbol_table_index,
|
||||
symbol_index,
|
||||
}) => ({
|
||||
section_index,
|
||||
entry_index,
|
||||
reported_offset: BigInt(reported_offset),
|
||||
type,
|
||||
symbol_table_index,
|
||||
symbol_index,
|
||||
}),
|
||||
),
|
||||
expectedRelocations,
|
||||
`${mode} relocations differ from readelf for ${path}`,
|
||||
);
|
||||
if (kind === "executable") {
|
||||
assert.ok(value.segments.length > 0);
|
||||
assert.equal(value.linkage.interpreters.length, 1);
|
||||
assert.ok(
|
||||
value.linkage.needed_libraries.some(
|
||||
(library) => library.display === "libc.so.6",
|
||||
),
|
||||
);
|
||||
}
|
||||
assert.deepEqual(await readFile(path), bytes);
|
||||
cases++;
|
||||
}
|
||||
}
|
||||
const protectedReport = reports.get("protected");
|
||||
// Ordinary data markers are not a request to discover/decompress a kernel config.
|
||||
const markerBytes = Buffer.from(await readFile(join(root.path, "protected")));
|
||||
|
||||
+2
-2
@@ -8,13 +8,13 @@
|
||||
"url": "https://github.com/morluto/rea",
|
||||
"source": "github"
|
||||
},
|
||||
"version": "6.7.0",
|
||||
"version": "6.8.0",
|
||||
"packages": [
|
||||
{
|
||||
"registryType": "npm",
|
||||
"registryBaseUrl": "https://registry.npmjs.org",
|
||||
"identifier": "rea-agents",
|
||||
"version": "6.7.0",
|
||||
"version": "6.8.0",
|
||||
"runtimeHint": "npx",
|
||||
"transport": {
|
||||
"type": "stdio"
|
||||
|
||||
@@ -378,6 +378,31 @@ it.each([
|
||||
if (!result.ok) expect(result.error._tag).toBe("AnalysisInputError");
|
||||
});
|
||||
|
||||
it("reports an ordinal past the capture's records as an out-of-range selection", async () => {
|
||||
const report = fixture();
|
||||
const result = await new WebNetworkCaptureService({
|
||||
inspect: () => Promise.resolve(ok(report)),
|
||||
}).inspect({
|
||||
capture_path: "/capture.har",
|
||||
format: "har",
|
||||
record_ordinals: [0, 5],
|
||||
});
|
||||
if (result.ok) throw new Error("expected an out-of-range ordinal");
|
||||
expect(projectAnalysisError(result.error)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
details: {
|
||||
issues: [
|
||||
{
|
||||
path: ["record_ordinals", 1],
|
||||
reason: "out_of_range",
|
||||
minimum: 0,
|
||||
maximum: report.total_records - 1,
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects a port changing the artifact identity or producer ordinal sequence", async () => {
|
||||
const report = fixture();
|
||||
report.artifact.path = "/different.har";
|
||||
|
||||
@@ -155,16 +155,25 @@ export class WebNetworkCaptureService {
|
||||
const selected =
|
||||
input.record_ordinals ?? value.records.map(({ ordinal }) => ordinal);
|
||||
const records = [];
|
||||
for (const ordinal of selected) {
|
||||
for (const [index, ordinal] of selected.entries()) {
|
||||
const record = safeValue.records[ordinal];
|
||||
// A selection past the capture's records is a caller range fault.
|
||||
if (record === undefined)
|
||||
return err(
|
||||
new AnalysisInputError(OPERATION, undefined, [
|
||||
{
|
||||
path: ["record_ordinals"],
|
||||
reason: "invalid_format",
|
||||
message: `Producer ordinal ${ordinal} is outside the retained capture (${value.total_records} records).`,
|
||||
},
|
||||
value.total_records === 0
|
||||
? {
|
||||
path: ["record_ordinals", index],
|
||||
reason: "invalid_value",
|
||||
message: "The selected capture contains no records.",
|
||||
}
|
||||
: {
|
||||
path: ["record_ordinals", index],
|
||||
reason: "out_of_range",
|
||||
minimum: 0,
|
||||
maximum: value.total_records - 1,
|
||||
message: `Producer ordinal ${ordinal} is outside the retained capture (${value.total_records} records).`,
|
||||
},
|
||||
]),
|
||||
);
|
||||
records.push({
|
||||
|
||||
@@ -22,6 +22,8 @@ const report: AppleIps = {
|
||||
bundle_id: null,
|
||||
incident_id: null,
|
||||
name: null,
|
||||
build_version: null,
|
||||
platform: null,
|
||||
unlisted_keys: [],
|
||||
},
|
||||
process: {
|
||||
@@ -29,6 +31,7 @@ const report: AppleIps = {
|
||||
path: null,
|
||||
pid: null,
|
||||
parent: null,
|
||||
parent_pid: null,
|
||||
cpu_type: null,
|
||||
translated: null,
|
||||
os_version: null,
|
||||
@@ -200,19 +203,23 @@ it.each(["threads", "images"] as const)(
|
||||
);
|
||||
|
||||
it.each([
|
||||
[43, false],
|
||||
[44, true],
|
||||
[131, false],
|
||||
[132, true],
|
||||
] as const)(
|
||||
"checks repeated integer text against %s UTF-16 bytes",
|
||||
async (maximum, accepted) => {
|
||||
const value = { decimal: "12345", hex: "0x3039" };
|
||||
const repeated: AppleIps = {
|
||||
...report,
|
||||
process: { ...report.process, pid: value },
|
||||
metadata: { ...report.metadata, platform: value },
|
||||
process: { ...report.process, pid: value, parent_pid: value },
|
||||
termination: {
|
||||
namespace: null,
|
||||
code: value,
|
||||
indicator: null,
|
||||
flags: value,
|
||||
by_pid: value,
|
||||
by_proc: null,
|
||||
reasons: null,
|
||||
unlisted_keys: [],
|
||||
},
|
||||
|
||||
@@ -56,6 +56,28 @@ it("records the decoded layout once in its Evidence", async () => {
|
||||
expect(evidence.raw_result).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ machine: "EM_X86_64", bits: 64, byte_order: "little" }, "x86_64"],
|
||||
[{ machine: "EM_386", bits: 32, byte_order: "little" }, "x86"],
|
||||
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, "arm"],
|
||||
[{ machine: "EM_MIPS", bits: 32, byte_order: "big" }, "mips"],
|
||||
] as const)(
|
||||
"names the reported %j machine in its Evidence subject",
|
||||
async (architecture, expected) => {
|
||||
const layout = { ...binaryLayoutFixture(), architecture };
|
||||
const service = new BinaryLayoutService({
|
||||
inspectAndClose(input, options) {
|
||||
return this.inspect(input, options);
|
||||
},
|
||||
identity: BINARY_LAYOUT_TEST_PROVIDER,
|
||||
inspect: () => Promise.resolve(ok(layout)),
|
||||
});
|
||||
const result = await service.inspect({ path: layout.artifact.path });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(parseEvidence(result.value).subject?.architecture).toBe(expected);
|
||||
},
|
||||
);
|
||||
|
||||
it("preserves a provider's actionable unsupported reason", async () => {
|
||||
const failure = new AnalysisCapabilityUnavailableError(
|
||||
"fixture-layout",
|
||||
|
||||
@@ -15,6 +15,12 @@ import { err, ok, type Result } from "../../domain/result.js";
|
||||
import { completeProviderEvidence } from "../ProviderOperationExecution.js";
|
||||
|
||||
const OPERATION = "inspect_binary_layout";
|
||||
const EVIDENCE_ARCHITECTURES = {
|
||||
EM_X86_64: "x86_64",
|
||||
EM_386: "x86",
|
||||
EM_ARM: "arm",
|
||||
EM_MIPS: "mips",
|
||||
} as const;
|
||||
|
||||
/** Shared CLI/MCP validation and Evidence composition for offline layout inspection. */
|
||||
export class BinaryLayoutService {
|
||||
@@ -87,7 +93,7 @@ export class BinaryLayoutService {
|
||||
{
|
||||
path: value.artifact.path,
|
||||
format: "elf",
|
||||
architecture: "x86_64",
|
||||
architecture: EVIDENCE_ARCHITECTURES[value.architecture.machine],
|
||||
sha256: value.artifact.sha256,
|
||||
},
|
||||
this.provider.identity,
|
||||
|
||||
@@ -10,6 +10,12 @@ import {
|
||||
referencedCreateRows,
|
||||
shelvedRows,
|
||||
} from "../../../tests/fixtures/environmentStateDerivedValues.js";
|
||||
import {
|
||||
LISTED_LABEL_ROWS,
|
||||
READ_BACK_TAG_ROWS,
|
||||
RESTAMPED_ROWS,
|
||||
STALE_SLUG_ROWS,
|
||||
} from "../../../tests/fixtures/environmentStateWriteViews.js";
|
||||
import { servedFrom } from "../../../tests/fixtures/environmentStateServing.js";
|
||||
import {
|
||||
bookClock,
|
||||
@@ -180,3 +186,72 @@ describe("served objects that repeat a named entity, and sorted arrays", () => {
|
||||
).toEqual(["fog", "ice", "wind"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("served writes whose answers show the entity otherwise than reads do", () => {
|
||||
it("answers a create as the application answered it and holds the fields its read-back showed", () => {
|
||||
const tags = servedFrom(READ_BACK_TAG_ROWS);
|
||||
const created = objectOf(
|
||||
tags.answer("post_tags", { body: { name: "Saltmarsh Gate" } }).body,
|
||||
);
|
||||
expect(Object.keys(created).sort()).toEqual(
|
||||
["created", "id", "name", "slug"].sort(),
|
||||
);
|
||||
expect(created).toMatchObject({
|
||||
name: "Saltmarsh Gate",
|
||||
slug: "saltmarsh-gate",
|
||||
created: firstServedClock(READ_BACK_TAG_ROWS).replace("Z", ".000000000Z"),
|
||||
});
|
||||
expect(
|
||||
tags.answer("get_tags_by_id", { id: String(created["id"]) }).body,
|
||||
).toMatchObject({
|
||||
name: "Saltmarsh Gate",
|
||||
document_count: 0,
|
||||
created: firstServedClock(READ_BACK_TAG_ROWS),
|
||||
});
|
||||
});
|
||||
|
||||
it("lists a created label with the count its list read showed", () => {
|
||||
const labels = servedFrom(LISTED_LABEL_ROWS);
|
||||
labels.answer("post_labels", { body: { title: "Slipway" } });
|
||||
expect((labels.answer("get_labels").body as JsonValue[]).at(-1)).toEqual({
|
||||
id: 3,
|
||||
title: "Slipway",
|
||||
count: { posts: 0 },
|
||||
});
|
||||
});
|
||||
|
||||
it("slugs a renamed tag anew whatever slug the call sends back", () => {
|
||||
const tags = servedFrom(STALE_SLUG_ROWS);
|
||||
const renamed = objectOf(
|
||||
tags.answer("put_tags_by_id", {
|
||||
id: "2",
|
||||
body: {
|
||||
id: 2,
|
||||
slug: "tide-tables",
|
||||
name: "Neap Tide Tables",
|
||||
document_count: 0,
|
||||
created: "2026-10-01T10:00:00Z",
|
||||
},
|
||||
}).body,
|
||||
);
|
||||
expect(renamed).toMatchObject({
|
||||
name: "Neap Tide Tables",
|
||||
slug: "neap-tide-tables",
|
||||
});
|
||||
});
|
||||
|
||||
it("stamps a post's modification time anew rather than storing the one the call sends back", () => {
|
||||
const posts = servedFrom(RESTAMPED_ROWS);
|
||||
const edited = objectOf(
|
||||
posts.answer("put_posts_by_id", {
|
||||
id: "1",
|
||||
body: { id: 1, title: "Winter rota v4", updated_at: bookClock(4) },
|
||||
}).body,
|
||||
);
|
||||
expect(edited["title"]).toBe("Winter rota v4");
|
||||
expect(edited["updated_at"]).not.toBe(bookClock(4));
|
||||
expect(Date.parse(String(edited["updated_at"]))).toBeGreaterThan(
|
||||
Date.parse(bookClock(4)),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
type StateWritePlan,
|
||||
type WriteCall,
|
||||
} from "../../domain/analysisView/environmentStateWriteRules.js";
|
||||
import { withRead } from "../../domain/analysisView/environmentStateWriteObserve.js";
|
||||
import {
|
||||
formatClock,
|
||||
generatedIntegerDigits,
|
||||
@@ -139,17 +140,30 @@ const callBody = ({
|
||||
: err(unloadable(tool, `Member ${member}`));
|
||||
};
|
||||
|
||||
/** The entity a recorded observation shows: the values a create keeps and an update's constants. */
|
||||
/** The object a recorded response shows at a pointer. */
|
||||
const recordedObject = (
|
||||
serving: StateServing,
|
||||
at: { readonly ordinal: number; readonly pointer: string },
|
||||
): JsonObject | undefined => {
|
||||
const exchange = serving.source.exchanges.get(at.ordinal);
|
||||
const body = exchange === undefined ? undefined : recordedJson(exchange);
|
||||
const object = body === undefined ? undefined : valueAt(body, at.pointer);
|
||||
return isObject(object) ? object : undefined;
|
||||
};
|
||||
|
||||
/** The entity a recorded observation shows, with a create's read-back over it: the values a create keeps and an update's constants. */
|
||||
export const observedEntity = (
|
||||
serving: StateServing,
|
||||
plan: StateWritePlan,
|
||||
): JsonObject | undefined => {
|
||||
if (plan.observation === null) return undefined;
|
||||
const exchange = serving.source.exchanges.get(plan.observation.ordinal);
|
||||
const body = exchange === undefined ? undefined : recordedJson(exchange);
|
||||
const object =
|
||||
body === undefined ? undefined : valueAt(body, plan.observation.pointer);
|
||||
return isObject(object) ? object : undefined;
|
||||
const object = recordedObject(serving, plan.observation);
|
||||
if (object === undefined || plan.observation.read === undefined)
|
||||
return object;
|
||||
const read = recordedObject(serving, plan.observation.read);
|
||||
return read === undefined
|
||||
? undefined
|
||||
: withRead(object, read, plan.observation.read.shape);
|
||||
};
|
||||
|
||||
/** One past the largest integer these entities hold in the field, read as the exemplar spells it, or undefined when none holds one. */
|
||||
@@ -347,7 +361,15 @@ export const writeAnswer = (
|
||||
: writtenResponse(
|
||||
plan.response,
|
||||
recorded,
|
||||
rendered,
|
||||
{
|
||||
...rendered,
|
||||
clocks:
|
||||
plan.observation?.read?.shape === "item"
|
||||
? plan.rules.flatMap((rule) =>
|
||||
rule.rule === "clock" ? [rule.field] : [],
|
||||
)
|
||||
: [],
|
||||
},
|
||||
optionalFields(plan),
|
||||
);
|
||||
if (!body.ok) return err(refused(tool, body.error));
|
||||
|
||||
@@ -73,6 +73,25 @@ it("retains allowed whitespace, visible text, and valid UTF-8 obs-text", async (
|
||||
expect(result.examined_bytes).toBe(5 + Buffer.byteLength(text));
|
||||
});
|
||||
|
||||
it("excludes trailing whitespace from trailer field values", async () => {
|
||||
const text =
|
||||
"grpc-status: 0 \r\ngrpc-message: alpha\tbeta \t\r\nx-empty: \t\r\n";
|
||||
const result = await decodeProtobufBody(
|
||||
trailerFrame(text),
|
||||
"grpc-web",
|
||||
source,
|
||||
same,
|
||||
new ProtobufBudget(),
|
||||
);
|
||||
expect(result.status).toBe("complete");
|
||||
expect(result.frames[0]?.trailers).toEqual([
|
||||
{ name: "grpc-status", value: "0" },
|
||||
{ name: "grpc-message", value: "alpha\tbeta" },
|
||||
{ name: "x-empty", value: "" },
|
||||
]);
|
||||
expect(result.examined_bytes).toBe(5 + Buffer.byteLength(text));
|
||||
});
|
||||
|
||||
it.each(["grpc-web", "grpc-web-text"] as const)(
|
||||
"keeps valid trailer fields around an opaque non-UTF-8 value in %s framing",
|
||||
async (framing) => {
|
||||
|
||||
@@ -84,9 +84,13 @@ export const parseGrpcWebTrailers = (
|
||||
"gRPC-web trailer field value contains a forbidden control character",
|
||||
});
|
||||
}
|
||||
// RFC 9110 section 5.5: a field value excludes surrounding whitespace.
|
||||
let valueEnd = lineEnd;
|
||||
while (valueEnd > valueStart && [9, 32].includes(bytes[valueEnd - 1]!))
|
||||
valueEnd -= 1;
|
||||
let value: string;
|
||||
try {
|
||||
value = decoder.decode(bytes.subarray(valueStart, lineEnd));
|
||||
value = decoder.decode(bytes.subarray(valueStart, valueEnd));
|
||||
} catch (cause) {
|
||||
if (!(cause instanceof TypeError)) throw cause;
|
||||
decoder = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
|
||||
|
||||
@@ -43,13 +43,27 @@ const isObjectArray = (value: unknown): value is Record<string, unknown>[] =>
|
||||
) &&
|
||||
value.some((item) => Object.keys(item).length > 0);
|
||||
|
||||
const isWordCharacter = (character: string | undefined): boolean =>
|
||||
character !== undefined && /[\p{L}\p{N}]/u.test(character);
|
||||
|
||||
/** Underscore runs between word characters cannot open or close emphasis. */
|
||||
const escapeInlineSyntax = (
|
||||
match: string,
|
||||
offset: number,
|
||||
text: string,
|
||||
): string =>
|
||||
match.startsWith("_") &&
|
||||
isWordCharacter(text[offset - 1]) &&
|
||||
isWordCharacter(text[offset + match.length])
|
||||
? match
|
||||
: match.replace(/./g, "\\$&");
|
||||
|
||||
const escapeTableCell = (value: string): string =>
|
||||
value
|
||||
.replace(/&/g, "&")
|
||||
.replace(/</g, "<")
|
||||
.replace(/>/g, ">")
|
||||
.replace(/\\/g, "\\\\")
|
||||
.replace(/\|/g, "\\|")
|
||||
.replace(/[\\|`*[\]~]|_+/g, escapeInlineSyntax)
|
||||
.replace(/\r\n|\r|\n/g, "<br>");
|
||||
|
||||
const renderTable = (
|
||||
|
||||
@@ -13,10 +13,12 @@ export interface GhidraLanguageOverride {
|
||||
|
||||
// Ghidra language IDs are four colon-separated fields; variants may contain
|
||||
// spaces ("x86:LE:16:Real Mode") but never leading or trailing whitespace.
|
||||
// Stock .ldefs also use "/" in processors ("M16C/60") and LEBE for ARM BE8
|
||||
// (big-endian data, little-endian instructions).
|
||||
export const ghidraLanguageIdSchema = z
|
||||
.string()
|
||||
.regex(
|
||||
/^[A-Za-z0-9_.+-]+:(?:LE|BE):[1-9]\d*:[A-Za-z0-9_.+-](?:[A-Za-z0-9_.+ -]*[A-Za-z0-9_.+-])?$/u,
|
||||
/^[A-Za-z0-9_.+-]+(?:\/[A-Za-z0-9_.+-]+)*:(?:LE|BE|LEBE):[1-9]\d*:[A-Za-z0-9_.+-](?:[A-Za-z0-9_.+ -]*[A-Za-z0-9_.+-])?$/u,
|
||||
"must be a Ghidra language ID such as x86:LE:32:default",
|
||||
);
|
||||
|
||||
|
||||
@@ -250,7 +250,7 @@ export const BINARY_DIAGNOSTICS_TOOL_CONTRACTS = [
|
||||
...toolContractMetadata("inspect_binary_layout"),
|
||||
kind: "native-provider",
|
||||
description:
|
||||
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Initial profile: ELF64 x86-64 little-endian EXEC/DYN/REL on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
|
||||
"Inspect an explicit local binary without launching it as a host process. Returns file-backed section/segment ranges, linked addresses, original symbol/REL/RELA table identities, complete encoded RELR tables and upstream-derived offsets with unknown per-word locations/addends, raw name bytes, dependency/interpreter names and static mitigation inferences inline with artifact SHA-256 Evidence. Profile: EXEC/DYN/REL ELF64 x86-64, ELF32 i386, ELF32 little-endian ARM and ELF32 little- or big-endian MIPS on Linux x64 via caller-supplied Python with unchanged pwntools 4.15.0, pyelftools 0.33 and Unicorn 2.1.2. Runtime addresses/library paths, overall relocation inventory completeness and complete derived GOT/PLT coverage remain unknown. Zero executable entry values mean absence; relocatable entries are not applicable. Upstream can emulate PLT instructions with Unicorn for derived static maps. Uses bounded complete output and owned temporary files/processes; malformed, unsupported or oversized objects return no partial success. Select detail summary to retain the complete layout Evidence in this session and receive only its summary view, then inspect sections, symbols, mitigations or linkage with inspect_analysis_view. pwntools_timeout_ms is each owned pwntools command's deadline (omitted means 30000 ms, range 1..2147483647, next call). cpuSeconds 30 and the output, diagnostic, and address-space byte allowances have no runtime override.",
|
||||
inputSchema: inspectBinaryLayoutRequestSchema,
|
||||
outputSchema: analysisDetailOutputSchemaOf(
|
||||
binaryLayoutSchema,
|
||||
|
||||
@@ -350,7 +350,20 @@ const writePlanSchema = z.strictObject({
|
||||
.nullable()
|
||||
.describe("The first successful recorded call the plan was built from."),
|
||||
observation: z
|
||||
.strictObject({ ordinal: ordinalSchema, pointer: z.string() })
|
||||
.strictObject({
|
||||
ordinal: ordinalSchema,
|
||||
pointer: z.string(),
|
||||
read: z
|
||||
.strictObject({
|
||||
ordinal: ordinalSchema,
|
||||
pointer: z.string(),
|
||||
shape: z.enum(["item", "list"]),
|
||||
})
|
||||
.exactOptional()
|
||||
.describe(
|
||||
"A create's later read of the entity: an item read lends fields the response lacks and its clock spellings, a list read only adds fields the response lacks.",
|
||||
),
|
||||
})
|
||||
.nullable()
|
||||
.describe(
|
||||
"The record whose response showed the written entity or document, and a JSON pointer to it.",
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
echoed,
|
||||
integerOf,
|
||||
readClock,
|
||||
respellClock,
|
||||
isUuid,
|
||||
replaceDelimited,
|
||||
slugOf,
|
||||
@@ -488,7 +489,12 @@ export const optionalFields = (
|
||||
export const writtenResponse = (
|
||||
response: WriteResponse,
|
||||
recorded: JsonValue | undefined,
|
||||
written: { readonly fields: JsonObject | undefined; readonly id: string },
|
||||
written: {
|
||||
readonly fields: JsonObject | undefined;
|
||||
readonly id: string;
|
||||
/** Generated clocks keep the response's spelling independently of the stored entity's. */
|
||||
readonly clocks?: readonly string[];
|
||||
},
|
||||
optional: readonly string[] = [],
|
||||
): Applied<JsonValue | undefined> => {
|
||||
if (response.shape === "empty") return ok(undefined);
|
||||
@@ -525,7 +531,17 @@ export const writtenResponse = (
|
||||
recorded,
|
||||
pointer,
|
||||
Object.fromEntries(
|
||||
fields.map((field) => [field, written.fields![field]!]),
|
||||
fields.map((field) => {
|
||||
const value = written.fields![field]!;
|
||||
const clock =
|
||||
written.clocks?.includes(field) === true
|
||||
? readClock(shown[field])
|
||||
: undefined;
|
||||
return [
|
||||
field,
|
||||
clock === undefined ? value : respellClock(value, clock.format),
|
||||
];
|
||||
}),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -104,7 +104,35 @@ export interface UpdateCall {
|
||||
readonly kept: readonly string[];
|
||||
}
|
||||
|
||||
/** Whether a rule a later call showed gives an earlier call a value other than the one it showed for a field it did not send. */
|
||||
/** Ambiguous saves may echo an already-derived slug or hold a clock in its own second. Keep the demonstrated derivation; call evidence still has to reproduce it. */
|
||||
export const reconciledUpdateRule = (
|
||||
held: FieldRule,
|
||||
later: FieldRule,
|
||||
kept: ReadonlySet<string>,
|
||||
): FieldRule | undefined => {
|
||||
const alike = alikeRules(held, later, kept);
|
||||
if (alike !== undefined) return alike;
|
||||
for (const [derived, ambiguous] of [
|
||||
[held, later],
|
||||
[later, held],
|
||||
] as const) {
|
||||
if (
|
||||
derived.rule === "slug" &&
|
||||
ambiguous.rule === "echo" &&
|
||||
ambiguous.sorted !== true
|
||||
)
|
||||
return derived;
|
||||
if (
|
||||
derived.rule === "clock" &&
|
||||
ambiguous.rule === "held-clock" &&
|
||||
same(derived.format, ambiguous.format)
|
||||
)
|
||||
return derived;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
/** Whether a later rule reproduces earlier calls, including echoes promoted to derivations. */
|
||||
export const contradictsEarlierCalls = (
|
||||
draft: {
|
||||
readonly plan: { readonly rules: readonly FieldRule[] };
|
||||
@@ -112,11 +140,12 @@ export const contradictsEarlierCalls = (
|
||||
},
|
||||
rules: readonly FieldRule[],
|
||||
): boolean => {
|
||||
const ruled = new Set(draft.plan.rules.map((rule) => rule.field));
|
||||
const ruled = new Map(draft.plan.rules.map((rule) => [rule.field, rule]));
|
||||
return rules.some((rule) => {
|
||||
if (
|
||||
!(unsentChange(rule) || regenerates(rule) || rule.rule === "clock") ||
|
||||
ruled.has(rule.field)
|
||||
(ruled.has(rule.field) &&
|
||||
!(rule.rule === "slug" && ruled.get(rule.field)?.rule === "echo"))
|
||||
)
|
||||
return false;
|
||||
// Compute a structured rule's digest once, rather than once per earlier call.
|
||||
@@ -148,9 +177,12 @@ export const contradictsEarlierCalls = (
|
||||
);
|
||||
}
|
||||
const from = rule.rule === "slug" ? sent.get(rule.from) : undefined;
|
||||
// A slug rule runs only when its source is sent; otherwise serving keeps the field.
|
||||
return (
|
||||
typeof from === "string" &&
|
||||
!sameObserved(observation, observedValue(slugOf(from)))
|
||||
rule.rule === "slug" &&
|
||||
(typeof from === "string"
|
||||
? !sameObserved(observation, observedValue(slugOf(from)))
|
||||
: !kept.includes(rule.field))
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -205,7 +237,10 @@ export const conflictsWithPlan = (
|
||||
(identity !== null && fixed.has(identity)) ||
|
||||
explained.rules.some((rule) => {
|
||||
const held = rules.get(rule.field);
|
||||
return held !== undefined && alikeRules(held, rule, kept) === undefined;
|
||||
return (
|
||||
held !== undefined &&
|
||||
reconciledUpdateRule(held, rule, kept) === undefined
|
||||
);
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { FieldRule, WriteCall } from "./environmentStateWriteRules.js";
|
||||
import {
|
||||
integerOf,
|
||||
readClock,
|
||||
slugOf,
|
||||
sortedStrings,
|
||||
} from "./environmentStateWriteValues.js";
|
||||
import { freshUuid, renewedRule } from "./environmentStateWriteGenerated.js";
|
||||
@@ -139,7 +140,35 @@ export const sameSecondClocks = (evidence: UpdateEvidence): string[] =>
|
||||
inCallSecond(evidence.after[field], evidence.startMs),
|
||||
);
|
||||
|
||||
/** How an update stored a sent member: stamped anew as the one held clock it sent back other than a creation clock, echoed, renewed as a fresh UUID, or as a constant. */
|
||||
/** Another sent member stored as sent whose slug the application stored in place of what this member sent. */
|
||||
const sentSlugSource = (
|
||||
evidence: UpdateEvidence,
|
||||
member: string,
|
||||
): string | undefined => {
|
||||
const stored = evidence.after[member];
|
||||
if (typeof stored !== "string") return undefined;
|
||||
return Object.entries(evidence.call.body).find(
|
||||
([other, value]) =>
|
||||
other !== member &&
|
||||
typeof value === "string" &&
|
||||
same(evidence.after[other], value) &&
|
||||
slugOf(value) === stored,
|
||||
)?.[0];
|
||||
};
|
||||
|
||||
/** Whether a call sent back the clock the entity held, as a whole-object write does, and the application stamped it anew near the call. */
|
||||
const restampedClock = (
|
||||
evidence: UpdateEvidence,
|
||||
member: string,
|
||||
sent: JsonValue,
|
||||
): boolean =>
|
||||
!CREATION_CLOCK.test(member) &&
|
||||
readClock(sent) !== undefined &&
|
||||
same(evidence.before[member], sent) &&
|
||||
readClock(evidence.after[member]) !== undefined &&
|
||||
isClockNear(evidence.after[member]!, evidence.startMs);
|
||||
|
||||
/** How an update stored a sent member: stamped anew as the one held clock it sent back other than a creation clock, echoed, stamped anew or slugged from another member in place of what it sent, renewed as a fresh UUID, or as a constant. */
|
||||
export const sentRule = (
|
||||
evidence: UpdateEvidence,
|
||||
member: string,
|
||||
@@ -156,6 +185,14 @@ export const sentRule = (
|
||||
return { field: member, rule: "echo" };
|
||||
if (same(evidence.after[member], sortedStrings(sent)))
|
||||
return { field: member, rule: "echo", sorted: true };
|
||||
if (restampedClock(evidence, member, sent))
|
||||
return {
|
||||
field: member,
|
||||
rule: "clock",
|
||||
format: readClock(evidence.after[member])!.format,
|
||||
};
|
||||
const from = sentSlugSource(evidence, member);
|
||||
if (from !== undefined) return { field: member, rule: "slug", from };
|
||||
return freshUuid(evidence, member)
|
||||
? { field: member, rule: "unique" }
|
||||
: { field: member, rule: "constant", value: evidence.after[member]! };
|
||||
|
||||
@@ -61,6 +61,8 @@ import {
|
||||
import {
|
||||
laterObservation,
|
||||
newEntityIn,
|
||||
respelledInstant,
|
||||
withRead,
|
||||
type Observed,
|
||||
} from "./environmentStateWriteObserve.js";
|
||||
import {
|
||||
@@ -97,6 +99,57 @@ const respondedId = (
|
||||
return members.length === 1 ? scalarText(members[0]) : undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* A create's answer with the first read of its entity before the next write over it, when that read shows fields the
|
||||
* answer lacks or spells its clocks to another precision: the state holds what reads show. An item read that agrees
|
||||
* with the answer otherwise lends it its fields and then observes rather than checks; one that disagrees still checks
|
||||
* the create. A list read, which may show fewer keys of an object, only adds fields the answer lacks and still checks.
|
||||
*/
|
||||
const withReadBack = (
|
||||
context: EffectContext,
|
||||
step: WriteStep,
|
||||
target: {
|
||||
readonly type: string;
|
||||
readonly collection: string;
|
||||
readonly idField: string;
|
||||
},
|
||||
own: Observed,
|
||||
): Observed => {
|
||||
const later = laterObservation(searchFrom(context, step), {
|
||||
type: target.type,
|
||||
collection: target.collection,
|
||||
idField: target.idField,
|
||||
id: own.id,
|
||||
held: () => false,
|
||||
isWrite: context.isWrite,
|
||||
});
|
||||
if (later === undefined) return own;
|
||||
const view = context.viewOf(later.ordinal);
|
||||
if (
|
||||
view?.shape === "item" &&
|
||||
(view.fields.some(
|
||||
(field) =>
|
||||
Object.hasOwn(own.object, field) && !Object.hasOwn(later.object, field),
|
||||
) ||
|
||||
Object.entries(later.object).some(
|
||||
([field, value]) =>
|
||||
Object.hasOwn(own.object, field) &&
|
||||
!same(own.object[field], value) &&
|
||||
!respelledInstant(own.object[field], value),
|
||||
))
|
||||
)
|
||||
return own;
|
||||
const shape = view?.shape === "item" ? "item" : "list";
|
||||
const object = withRead(own.object, later.object, shape);
|
||||
if (same(object, own.object)) return own;
|
||||
if (shape === "item") context.observed(later.ordinal);
|
||||
return {
|
||||
...own,
|
||||
object,
|
||||
read: { ordinal: later.ordinal, pointer: later.pointer, shape },
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* The written entity a create's response or a later read shows. A linked create's read must name an
|
||||
* id its response returned, as the read that linked it did, envelopes included.
|
||||
@@ -116,7 +169,7 @@ const createdObservation = (
|
||||
const own = context.io.withBody(step.record.ordinal, (body) =>
|
||||
newEntityIn(step.record.ordinal, body, target.idField, held),
|
||||
);
|
||||
if (own !== undefined) return own;
|
||||
if (own !== undefined) return withReadBack(context, step, target, own);
|
||||
const { id, ids } = context.io.withBody(step.record.ordinal, (body) => {
|
||||
const returned = target.linked ? respondedIds(body) : [];
|
||||
// A response that names the created id alone ties the later read to that id.
|
||||
@@ -320,6 +373,9 @@ const buildCreate = (
|
||||
observation: {
|
||||
ordinal: evidence.observed.ordinal,
|
||||
pointer: evidence.observed.pointer,
|
||||
...(evidence.observed.read === undefined
|
||||
? {}
|
||||
: { read: evidence.observed.read }),
|
||||
},
|
||||
response: context.io.withBody(step.record.ordinal, (body) =>
|
||||
responseShapeOf(
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
type UpdateCall,
|
||||
conflictsWithPlan,
|
||||
refinedBody,
|
||||
reconciledUpdateRule,
|
||||
} from "./environmentStateWriteCalls.js";
|
||||
import {
|
||||
laterObservation,
|
||||
@@ -58,7 +59,6 @@ import {
|
||||
observationNeeds,
|
||||
} from "./environmentStateWriteAddress.js";
|
||||
import {
|
||||
alikeRules,
|
||||
keptOrders,
|
||||
pathValuesOf,
|
||||
siblingTrimmedClocks,
|
||||
@@ -394,7 +394,7 @@ const refineUpdate = (
|
||||
const shown = later.get(rule.field);
|
||||
return shown === undefined
|
||||
? rule
|
||||
: (alikeRules(rule, shown, draft.kept) ?? rule);
|
||||
: (reconciledUpdateRule(rule, shown, draft.kept) ?? rule);
|
||||
}),
|
||||
...evidence.explained.rules.filter((rule) => !ruled.has(rule.field)),
|
||||
];
|
||||
@@ -703,7 +703,11 @@ const contradictsGeneratedClock = (
|
||||
if (
|
||||
rule.rule !== "clock" ||
|
||||
!Object.hasOwn(after, rule.field) ||
|
||||
explained.unstamped.includes(rule.field)
|
||||
explained.unstamped.includes(rule.field) ||
|
||||
explained.rules.some(
|
||||
(candidate) =>
|
||||
candidate.field === rule.field && candidate.rule === "held-clock",
|
||||
)
|
||||
)
|
||||
return false;
|
||||
const previous = readClock(before[rule.field]);
|
||||
|
||||
@@ -15,6 +15,41 @@ import {
|
||||
type StateView,
|
||||
} from "./environmentStateRules.js";
|
||||
import { queryValues, replayReason } from "./environmentStateVariants.js";
|
||||
import { readClock } from "./environmentStateWriteValues.js";
|
||||
|
||||
/** A read a create's observation takes fields from: an item read's respell the response's clocks and add fields, a list read's only add fields. */
|
||||
export interface ObservedRead {
|
||||
readonly ordinal: number;
|
||||
readonly pointer: string;
|
||||
readonly shape: "item" | "list";
|
||||
}
|
||||
|
||||
/** Whether two clocks agree at the coarser recorded fraction precision, retaining all digits through nanoseconds. */
|
||||
export const respelledInstant = (
|
||||
left: JsonValue | undefined,
|
||||
right: JsonValue | undefined,
|
||||
): boolean => {
|
||||
const [a, b] = [readClock(left), readClock(right)];
|
||||
if (a?.format.kind !== "iso" || b?.format.kind !== "iso") return false;
|
||||
const [fine, coarse] =
|
||||
a.format.fraction >= b.format.fraction ? [a, b] : [b, a];
|
||||
if (coarse.format.kind !== "iso") return false;
|
||||
const unit = 10 ** (9 - coarse.format.fraction);
|
||||
const fraction = (clock: typeof fine) =>
|
||||
(((clock.ms % 1000) + 1000) % 1000) * 1_000_000 + clock.subMillisecond;
|
||||
return (
|
||||
Math.floor(fine.ms / 1000) === Math.floor(coarse.ms / 1000) &&
|
||||
Math.floor(fraction(fine) / unit) * unit === fraction(coarse)
|
||||
);
|
||||
};
|
||||
|
||||
/** A create's response object with a read's fields: an item read's values win, a list read only adds fields the response lacks. */
|
||||
export const withRead = (
|
||||
response: JsonObject,
|
||||
read: JsonObject,
|
||||
shape: ObservedRead["shape"],
|
||||
): JsonObject =>
|
||||
shape === "item" ? { ...response, ...read } : { ...read, ...response };
|
||||
|
||||
/** Where an observed entity object was found: the record and a JSON pointer into its response. */
|
||||
export interface Observed {
|
||||
@@ -22,6 +57,8 @@ export interface Observed {
|
||||
readonly pointer: string;
|
||||
readonly object: JsonObject;
|
||||
readonly id: string;
|
||||
/** A create's later read whose fields the object adds to the response's, and whose clock spellings an item read lends it. */
|
||||
readonly read?: ObservedRead;
|
||||
}
|
||||
|
||||
/** Objects a response carries where an entity may be, in precedence tiers: the root, one member, then the element of a one-element array at the root or in one member. */
|
||||
|
||||
@@ -253,10 +253,15 @@ export interface StateWritePlan {
|
||||
readonly served: boolean;
|
||||
readonly reason: WriteReason | null;
|
||||
readonly exemplar_ordinal: number | null;
|
||||
/** The record whose response showed the written entity or document, and where. */
|
||||
/** The record whose response showed the written entity or document, and where; a create's later read whose fields the entity adds to the response's. */
|
||||
readonly observation: {
|
||||
readonly ordinal: number;
|
||||
readonly pointer: string;
|
||||
readonly read?: {
|
||||
readonly ordinal: number;
|
||||
readonly pointer: string;
|
||||
readonly shape: "item" | "list";
|
||||
};
|
||||
} | null;
|
||||
readonly response: WriteResponse | null;
|
||||
readonly body: WriteBody | null;
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { JsonValue } from "../jsonValue.js";
|
||||
|
||||
import {
|
||||
timedCaptureEvidence,
|
||||
type TimedCaptureRow,
|
||||
} from "../../../tests/fixtures/analysisView.js";
|
||||
import {
|
||||
LISTED_LABEL_ROWS,
|
||||
READ_BACK_TAG_ROWS,
|
||||
RESTAMPED_ROWS,
|
||||
STALE_SLUG_ROWS,
|
||||
clockSaveRows,
|
||||
slugSaveRows,
|
||||
slugOnlySaveRows,
|
||||
} from "../../../tests/fixtures/environmentStateWriteViews.js";
|
||||
import { environmentStateViewOf } from "../../../tests/fixtures/environmentStateWrites.js";
|
||||
|
||||
const planOf = (rows: readonly TimedCaptureRow[], tool: string) => {
|
||||
const plan = environmentStateViewOf(timedCaptureEvidence(rows)).writes.find(
|
||||
(candidate) => candidate.tool === tool,
|
||||
);
|
||||
if (plan === undefined) throw new Error(`No plan for ${tool}`);
|
||||
return plan;
|
||||
};
|
||||
|
||||
describe("creates whose answer shows the entity otherwise than reads do", () => {
|
||||
it.each(["123456", "123457"])(
|
||||
"checks read-back precision beyond milliseconds (%s)",
|
||||
(fraction) => {
|
||||
const rows = READ_BACK_TAG_ROWS.map(
|
||||
([method, path, body = null, ...rest]): TimedCaptureRow => {
|
||||
const rewrite = (value: JsonValue): JsonValue => {
|
||||
if (Array.isArray(value)) return value.map(rewrite);
|
||||
if (value !== null && typeof value === "object")
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, child]) => [
|
||||
key,
|
||||
key === "created" && typeof child === "string"
|
||||
? child.replace(
|
||||
/(?:\.\d+)?Z$/u,
|
||||
method === "POST" ? ".123456000Z" : `.${fraction}Z`,
|
||||
)
|
||||
: child,
|
||||
]),
|
||||
);
|
||||
return value;
|
||||
};
|
||||
return [method, path, rewrite(body), ...rest];
|
||||
},
|
||||
);
|
||||
expect(planOf(rows, "post_tags").served).toBe(fraction === "123456");
|
||||
},
|
||||
);
|
||||
|
||||
it("keeps an item read's omission as contradictory evidence even when a later list confirms the added fields", () => {
|
||||
const rows = READ_BACK_TAG_ROWS.map(
|
||||
([method, path, body = null, ...rest]): TimedCaptureRow => [
|
||||
method,
|
||||
path,
|
||||
body !== null &&
|
||||
typeof body === "object" &&
|
||||
!Array.isArray(body) &&
|
||||
(method === "POST" || path === "/api/tags/1" || path === "/api/tags/2")
|
||||
? { ...body, detail: "Only in the answer" }
|
||||
: body,
|
||||
...rest,
|
||||
],
|
||||
);
|
||||
expect(planOf(rows, "post_tags")).toMatchObject({
|
||||
served: false,
|
||||
reason: "contradicted",
|
||||
});
|
||||
});
|
||||
it("serves a create whose item read adds a count and spells the clock otherwise, taking the read's fields", () => {
|
||||
expect(planOf(READ_BACK_TAG_ROWS, "post_tags")).toMatchObject({
|
||||
served: true,
|
||||
observation: {
|
||||
ordinal: 3,
|
||||
pointer: "",
|
||||
read: { ordinal: 4, pointer: "", shape: "item" },
|
||||
},
|
||||
rules: expect.arrayContaining([
|
||||
{
|
||||
field: "created",
|
||||
rule: "clock",
|
||||
format: expect.objectContaining({ fraction: 0 }),
|
||||
},
|
||||
]),
|
||||
});
|
||||
});
|
||||
|
||||
it("serves a create whose list read adds a count the answer lacks", () => {
|
||||
expect(planOf(LISTED_LABEL_ROWS, "post_labels")).toMatchObject({
|
||||
served: true,
|
||||
observation: { read: { ordinal: 2, pointer: "/2", shape: "list" } },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("whole-object updates that send back what the application derives", () => {
|
||||
it.each([
|
||||
[true, true],
|
||||
[false, true],
|
||||
[true, false],
|
||||
[false, false],
|
||||
] as const)(
|
||||
"checks slug-only saves too (derived first: %s, changed: %s)",
|
||||
(derivedFirst, changed) => {
|
||||
expect(
|
||||
planOf(slugOnlySaveRows(derivedFirst, changed), "patch_tags_by_id")
|
||||
.served,
|
||||
).toBe(!changed);
|
||||
},
|
||||
);
|
||||
it.each([true, false])(
|
||||
"reconciles slug saves in either order (derived first: %s)",
|
||||
(derivedFirst) => {
|
||||
expect(
|
||||
planOf(slugSaveRows(derivedFirst), "put_tags_by_id"),
|
||||
).toMatchObject({
|
||||
served: true,
|
||||
rules: expect.arrayContaining([
|
||||
{ field: "slug", rule: "slug", from: "name" },
|
||||
]),
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([true, false])(
|
||||
"rejects a slug rule contradicted by an explicit custom slug (derived first: %s)",
|
||||
(derivedFirst) => {
|
||||
expect(
|
||||
planOf(slugSaveRows(derivedFirst, true), "put_tags_by_id").served,
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([true, false])(
|
||||
"reconciles held and fresh clocks in either order (derived first: %s)",
|
||||
(derivedFirst) => {
|
||||
expect(
|
||||
planOf(clockSaveRows(derivedFirst), "put_posts_by_id"),
|
||||
).toMatchObject({
|
||||
served: true,
|
||||
rules: expect.arrayContaining([
|
||||
expect.objectContaining({ field: "updated_at", rule: "clock" }),
|
||||
]),
|
||||
});
|
||||
},
|
||||
);
|
||||
it("slugs a sent slug anew from the name the call stored", () => {
|
||||
expect(planOf(STALE_SLUG_ROWS, "put_tags_by_id")).toMatchObject({
|
||||
served: true,
|
||||
rules: expect.arrayContaining([
|
||||
{ field: "slug", rule: "slug", from: "name" },
|
||||
{ field: "name", rule: "echo" },
|
||||
]),
|
||||
body: { accepted: expect.arrayContaining(["slug", "name"]) },
|
||||
});
|
||||
});
|
||||
|
||||
it("stamps anew a modification time each call sends back as it read it", () => {
|
||||
expect(planOf(RESTAMPED_ROWS, "put_posts_by_id")).toMatchObject({
|
||||
served: true,
|
||||
rules: expect.arrayContaining([
|
||||
expect.objectContaining({ field: "updated_at", rule: "clock" }),
|
||||
]),
|
||||
body: { accepted: expect.arrayContaining(["updated_at"]) },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -324,17 +324,20 @@ const buildArtifactChangeBase = (input: ClassifyPathInput) => ({
|
||||
evidence_links: [...input.evidenceLinks],
|
||||
});
|
||||
|
||||
const classifyPresence = (input: ClassifyPathInput): ArtifactChange | null => {
|
||||
const classifyPresence = (
|
||||
input: ClassifyPathInput,
|
||||
base: ReturnType<typeof buildArtifactChangeBase>,
|
||||
): ArtifactChange | null => {
|
||||
const covered = input.path === "." ? input.rootCovered : input.entriesCovered;
|
||||
if (input.leftOccurrence === undefined)
|
||||
return {
|
||||
...buildArtifactChangeBase(input),
|
||||
...base,
|
||||
classification: covered ? "added" : "unknown",
|
||||
dimensions: [covered ? "content" : "availability"],
|
||||
};
|
||||
if (input.rightOccurrence === undefined)
|
||||
return {
|
||||
...buildArtifactChangeBase(input),
|
||||
...base,
|
||||
classification: covered ? "removed" : "unknown",
|
||||
dimensions: [covered ? "content" : "availability"],
|
||||
};
|
||||
@@ -372,11 +375,12 @@ const classifyAvailability = (
|
||||
};
|
||||
|
||||
const classifyPath = (input: ClassifyPathInput): ArtifactChange | null => {
|
||||
const presence = classifyPresence(input);
|
||||
if (presence !== null) return presence;
|
||||
const base = buildArtifactChangeBase(input);
|
||||
// A declared/observed mismatch is known even without an opposite occurrence.
|
||||
const integrity = classifyIntegrity(input, base);
|
||||
if (integrity !== null) return integrity;
|
||||
const presence = classifyPresence(input, base);
|
||||
if (presence !== null) return presence;
|
||||
const declaration = classifyPyInstallerDeclaration(input, base);
|
||||
if (declaration.handled) return declaration.change;
|
||||
const availability = classifyAvailability(input, base);
|
||||
|
||||
@@ -52,6 +52,36 @@ describe("child-process options detection", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("reads fork's silent option when stdio is not a string or array", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
import { fork, spawn } from "node:child_process";
|
||||
|
||||
fork("piped.js", { silent: true });
|
||||
fork("inherited.js", [], { silent: false });
|
||||
fork("nullish-stdio.js", { silent: 1, stdio: null });
|
||||
fork("explicit-stdio.js", { silent: true, stdio: "inherit" });
|
||||
fork("dynamic.js", { silent: process.env.QUIET });
|
||||
spawn("tool", { silent: true });
|
||||
fork("boolean-stdio.js", { stdio: false, silent: true });
|
||||
fork("object-stdio.js", { stdio: {}, silent: false });
|
||||
fork("unresolved-stdio.js", { stdio: streams, silent: true });
|
||||
spawn("tool", { stdio: false });
|
||||
`);
|
||||
|
||||
expect(ir.childProcessSpawns.map(({ stdioMode }) => stdioMode)).toEqual([
|
||||
"pipe",
|
||||
"default",
|
||||
"pipe",
|
||||
"inherit",
|
||||
"dynamic",
|
||||
"default",
|
||||
"pipe",
|
||||
"default",
|
||||
"dynamic",
|
||||
"dynamic",
|
||||
]);
|
||||
});
|
||||
|
||||
it("preserves behavior with explicit args arrays", () => {
|
||||
const ir = analyzeJavaScriptSemantics(`
|
||||
import { spawn, execFile, fork } from "node:child_process";
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
} from "./javascriptSemanticExpressionShapes.js";
|
||||
import {
|
||||
semanticStaticPropertyName,
|
||||
staticJavaScriptTruthiness,
|
||||
staticObjectProperty,
|
||||
unwrapJavaScriptExpression,
|
||||
} from "./javascriptAstValues.js";
|
||||
@@ -424,15 +425,40 @@ const stdioMode = (
|
||||
)
|
||||
return "default";
|
||||
const lookup = staticObjectProperty(expression, "stdio", context.isNullish);
|
||||
if (lookup.status === "missing") return "default";
|
||||
if (lookup.status === "missing")
|
||||
return unsetStdioMode(expression, method, context);
|
||||
if (lookup.status !== "explicit") return "dynamic";
|
||||
const value = unwrapJavaScriptExpression(lookup.property.value).node;
|
||||
if (context.isNullish(value)) return "default";
|
||||
if (context.isNullish(value))
|
||||
return unsetStdioMode(expression, method, context);
|
||||
if (t.isStringLiteral(value)) return value.value;
|
||||
if (t.isArrayExpression(value)) return "array";
|
||||
// fork ignores any other known stdio value and falls back to `silent`.
|
||||
if (method === "fork" && staticJavaScriptTruthiness(value) !== undefined)
|
||||
return unsetStdioMode(expression, method, context);
|
||||
return "dynamic";
|
||||
};
|
||||
|
||||
/**
|
||||
* Without a string or array `stdio`, Node's `fork` pipes the standard streams
|
||||
* when `silent` is truthy and inherits them otherwise.
|
||||
*/
|
||||
const unsetStdioMode = (
|
||||
options: t.Node,
|
||||
method: JavaScriptSemanticChildProcessSpawn["method"],
|
||||
context: ChildSpawnContext,
|
||||
): string => {
|
||||
if (method !== "fork") return "default";
|
||||
const silent = staticObjectProperty(options, "silent", context.isNullish);
|
||||
if (silent.status === "missing") return "default";
|
||||
if (silent.status !== "explicit") return "dynamic";
|
||||
const value = unwrapJavaScriptExpression(silent.property.value).node;
|
||||
if (context.isNullish(value)) return "default";
|
||||
const truthy = staticJavaScriptTruthiness(value);
|
||||
if (truthy === undefined) return "dynamic";
|
||||
return truthy ? "pipe" : "default";
|
||||
};
|
||||
|
||||
const objectBinding = (
|
||||
node: t.Expression | t.Super,
|
||||
state: JavaScriptSemanticAnalysisState,
|
||||
|
||||
@@ -40,6 +40,8 @@ it("rejects extra caller arguments and admits an empty crash inventory", () => {
|
||||
bundle_id: null,
|
||||
incident_id: null,
|
||||
name: null,
|
||||
build_version: null,
|
||||
platform: null,
|
||||
unlisted_keys: [],
|
||||
},
|
||||
process: {
|
||||
@@ -47,6 +49,7 @@ it("rejects extra caller arguments and admits an empty crash inventory", () => {
|
||||
path: null,
|
||||
pid: null,
|
||||
parent: null,
|
||||
parent_pid: null,
|
||||
cpu_type: null,
|
||||
translated: null,
|
||||
os_version: null,
|
||||
|
||||
@@ -122,6 +122,8 @@ const metadataSchema = z.strictObject({
|
||||
bundle_id: z.string().nullable(),
|
||||
incident_id: z.string().nullable(),
|
||||
name: z.string().nullable(),
|
||||
build_version: z.string().nullable(),
|
||||
platform: recordedIntegerSchema.nullable(),
|
||||
unlisted_keys: z.array(z.string()),
|
||||
});
|
||||
|
||||
@@ -141,6 +143,9 @@ const terminationSchema = z
|
||||
namespace: z.string().nullable(),
|
||||
code: recordedIntegerSchema.nullable(),
|
||||
indicator: z.string().nullable(),
|
||||
flags: recordedIntegerSchema.nullable(),
|
||||
by_pid: recordedIntegerSchema.nullable(),
|
||||
by_proc: z.string().nullable(),
|
||||
reasons: z.array(z.string()).nullable(),
|
||||
unlisted_keys: z.array(z.string()),
|
||||
})
|
||||
@@ -275,6 +280,7 @@ export const appleIpsSchema = z.strictObject({
|
||||
path: z.string().nullable(),
|
||||
pid: recordedIntegerSchema.nullable(),
|
||||
parent: z.string().nullable(),
|
||||
parent_pid: recordedIntegerSchema.nullable(),
|
||||
cpu_type: z.string().nullable(),
|
||||
translated: z.boolean().nullable(),
|
||||
os_version: osVersionSchema,
|
||||
@@ -314,8 +320,12 @@ export const appleIpsIntegerTextBytes = (
|
||||
/** The named report fields own which observed and derived integers are retained. */
|
||||
export const appleIpsReportIntegerTextBytes = (report: AppleIps): number => {
|
||||
let bytes =
|
||||
appleIpsIntegerTextBytes(report.metadata.platform) +
|
||||
appleIpsIntegerTextBytes(report.process.pid) +
|
||||
appleIpsIntegerTextBytes(report.termination?.code ?? null);
|
||||
appleIpsIntegerTextBytes(report.process.parent_pid) +
|
||||
appleIpsIntegerTextBytes(report.termination?.code ?? null) +
|
||||
appleIpsIntegerTextBytes(report.termination?.flags ?? null) +
|
||||
appleIpsIntegerTextBytes(report.termination?.by_pid ?? null);
|
||||
for (const image of report.images)
|
||||
bytes +=
|
||||
appleIpsIntegerTextBytes(image.base) +
|
||||
|
||||
@@ -121,6 +121,21 @@ it("retains high linked values and NOBITS without inventing file backing", () =>
|
||||
expect(value.runtime_load_base).toBeNull();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[{ machine: "EM_386", bits: 32, byte_order: "little" }, true],
|
||||
[{ machine: "EM_ARM", bits: 32, byte_order: "little" }, true],
|
||||
[{ machine: "EM_MIPS", bits: 32, byte_order: "little" }, true],
|
||||
[{ machine: "EM_MIPS", bits: 32, byte_order: "big" }, true],
|
||||
[{ machine: "EM_X86_64", bits: 32, byte_order: "little" }, false],
|
||||
[{ machine: "EM_ARM", bits: 32, byte_order: "big" }, false],
|
||||
[{ machine: "EM_386", bits: 64, byte_order: "little" }, false],
|
||||
[{ machine: "EM_MIPS", bits: 64, byte_order: "big" }, false],
|
||||
])("admits only profiled architecture %j: %s", (architecture, admitted) => {
|
||||
expect(
|
||||
binaryLayoutSchema.safeParse({ ...report(), architecture }).success,
|
||||
).toBe(admitted);
|
||||
});
|
||||
|
||||
it.each([
|
||||
"file-backed-outside",
|
||||
"header-outside",
|
||||
@@ -276,6 +291,9 @@ it.each([
|
||||
"wrong-unknown-target",
|
||||
"missing-string-table",
|
||||
"wrong-string-table-kind",
|
||||
"elf32-symbol-entry",
|
||||
"elf32-undersized-symbol-entry",
|
||||
"elf64-elf32-sized-symbol-entry",
|
||||
])("validates relocation symbol reference semantics: %s", (problem) => {
|
||||
const value = binaryLayoutSchema.parse(report());
|
||||
value.artifact.bytes = 512;
|
||||
@@ -359,6 +377,17 @@ it.each([
|
||||
if (problem === "owner-link-mismatch") owner.link = 0;
|
||||
if (problem === "wrong-meaning")
|
||||
relocation.symbol_reference_meaning = "zero-symbol-value";
|
||||
// Elf32_Sym is 16 bytes and Elf64_Sym 24; the minimum follows the ELF class.
|
||||
if (problem.includes("elf32")) {
|
||||
if (problem.startsWith("elf32"))
|
||||
value.architecture = {
|
||||
machine: "EM_386",
|
||||
bits: 32,
|
||||
byte_order: "little",
|
||||
};
|
||||
table.size = "0x20";
|
||||
table.entry_size = problem.includes("undersized") ? "0xc" : "0x10";
|
||||
}
|
||||
if (problem.includes("target")) {
|
||||
value.image_type = "ET_REL";
|
||||
value.entry_point.meaning = "not-applicable";
|
||||
@@ -411,6 +440,7 @@ it.each([
|
||||
"valid-section-target",
|
||||
"undefined-target",
|
||||
"inactive-target",
|
||||
"elf32-symbol-entry",
|
||||
].includes(problem),
|
||||
);
|
||||
});
|
||||
@@ -33,11 +33,23 @@ const binaryLayoutObjectSchema = z.strictObject({
|
||||
bytes: index,
|
||||
}),
|
||||
format: z.literal("elf"),
|
||||
architecture: z.strictObject({
|
||||
machine: z.literal("EM_X86_64"),
|
||||
bits: z.literal(64),
|
||||
byte_order: z.literal("little"),
|
||||
}),
|
||||
architecture: z.union([
|
||||
z.strictObject({
|
||||
machine: z.literal("EM_X86_64"),
|
||||
bits: z.literal(64),
|
||||
byte_order: z.literal("little"),
|
||||
}),
|
||||
z.strictObject({
|
||||
machine: z.enum(["EM_386", "EM_ARM"]),
|
||||
bits: z.literal(32),
|
||||
byte_order: z.literal("little"),
|
||||
}),
|
||||
z.strictObject({
|
||||
machine: z.literal("EM_MIPS"),
|
||||
bits: z.literal(32),
|
||||
byte_order: z.enum(["little", "big"]),
|
||||
}),
|
||||
]),
|
||||
image_type: z.enum(["ET_EXEC", "ET_DYN", "ET_REL"]),
|
||||
entry_point: z.strictObject({
|
||||
reported_value: unsignedHex,
|
||||
@@ -384,7 +396,9 @@ const validateRelocationReferences = (
|
||||
(table?.type === "SHT_SYMTAB" || table?.type === "SHT_DYNSYM");
|
||||
const positiveReferenceExists =
|
||||
tableExists &&
|
||||
BigInt(table.entry_size) >= 24n &&
|
||||
// Elf32_Sym is 16 bytes; Elf64_Sym is 24.
|
||||
BigInt(table.entry_size) >=
|
||||
(value.architecture.bits === 32 ? 16n : 24n) &&
|
||||
BigInt(relocation.symbol_index) <
|
||||
BigInt(table.size) / BigInt(table.entry_size) &&
|
||||
symbols.has(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/** Generated by scripts/generate-package-metadata.mjs; do not edit. */
|
||||
export const PACKAGE_METADATA = {
|
||||
name: "rea-agents",
|
||||
version: "6.7.0", // x-release-please-version
|
||||
version: "6.8.0", // x-release-please-version
|
||||
pinnedNodeVersion: "24.22.1",
|
||||
serverSdkVersion: "2.3.1",
|
||||
clientSdkVersion: "2.3.1",
|
||||
|
||||
@@ -33,6 +33,7 @@ import {
|
||||
} from "./GhidraPowerPcProfile.js";
|
||||
import { ghidraRawBinaryLanguage } from "./GhidraRawBinaryLanguage.js";
|
||||
import {
|
||||
GHIDRA_GAMECUBE_DOL_LANGUAGE,
|
||||
ghidraDolLanguage,
|
||||
ghidraDolLanguageUnsupportedReason,
|
||||
ghidraDolProfileParameters,
|
||||
@@ -180,14 +181,16 @@ export const resolveGhidraAnalysisProfile = async (
|
||||
if (target.format === "gamecube-dol") {
|
||||
// The DOL admission owns the processor family; only a BE32 PowerPC variant may replace the stock language.
|
||||
const reason = ghidraDolLanguageUnsupportedReason(languageOverride);
|
||||
if (reason !== null)
|
||||
// The DOL is admitted; only the selected language conflicts with it.
|
||||
if (reason !== null && languageOverride !== undefined)
|
||||
return Promise.resolve(
|
||||
err(
|
||||
new AnalysisUnsupportedTargetError(
|
||||
"resolve_analysis_profile",
|
||||
target.path,
|
||||
reason,
|
||||
),
|
||||
languageOverrideConflict(languageOverride, reason, {
|
||||
languageId: GHIDRA_GAMECUBE_DOL_LANGUAGE.languageId,
|
||||
...(languageOverride.compilerSpecId === undefined
|
||||
? {}
|
||||
: { compilerSpecId: languageOverride.compilerSpecId }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
const language = ghidraDolLanguage(languageOverride);
|
||||
|
||||
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { parseBinaryTarget } from "../application/BinaryTargetResolver.js";
|
||||
import { createTestTempDirectory } from "../../tests/fixtures/temporaryDirectory.js";
|
||||
import { parseConfig } from "../config/parseConfig.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
import type { BinaryTarget } from "../domain/binaryTargetTypes.js";
|
||||
import { buildGamecubeDol } from "../domain/gamecubeDol.fixture.js";
|
||||
import { parseDolHeader } from "../domain/gamecubeDol.js";
|
||||
@@ -160,11 +161,42 @@ describe("GameCube DOL Ghidra profile", () => {
|
||||
host,
|
||||
);
|
||||
const resolved = await ghidra.resolveAnalysisProfile(await dolTarget());
|
||||
expect(resolved.ok).toBe(false);
|
||||
if (!resolved.ok)
|
||||
expect(resolved.error.message).toContain(
|
||||
"requires a PowerPC:BE:32:* language",
|
||||
);
|
||||
if (resolved.ok) throw new Error("expected an override conflict");
|
||||
expect(resolved.error.message).toContain(
|
||||
"requires a PowerPC:BE:32:* language",
|
||||
);
|
||||
// The setting conflicts with the admitted DOL; the target stays supported.
|
||||
expect(projectAnalysisError(resolved.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
details: {
|
||||
settings: [
|
||||
expect.objectContaining({ setting: "REA_GHIDRA_LANGUAGE_ID" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("names only the language when a compiler spec accompanies a conflicting language", async () => {
|
||||
const ghidra = new GhidraProvider(
|
||||
await installedConfig({
|
||||
REA_GHIDRA_LANGUAGE_ID: "x86:LE:32:default",
|
||||
REA_GHIDRA_COMPILER_SPEC_ID: "gcc",
|
||||
}),
|
||||
silentLogger,
|
||||
{},
|
||||
host,
|
||||
);
|
||||
const resolved = await ghidra.resolveAnalysisProfile(await dolTarget());
|
||||
if (resolved.ok) throw new Error("expected an override conflict");
|
||||
// DOL accepts a compiler-spec override, so only the language is blamed.
|
||||
expect(projectAnalysisError(resolved.error)).toMatchObject({
|
||||
code: "configuration_invalid",
|
||||
details: {
|
||||
settings: [
|
||||
expect.objectContaining({ setting: "REA_GHIDRA_LANGUAGE_ID" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("does not admit DOL through Windows Ghidra P0", async () => {
|
||||
|
||||
@@ -156,6 +156,25 @@ describe("Ghidra language override configuration", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
"ARM:LEBE:32:v8LEInstruction",
|
||||
"ARM:LEBE:32:v7LEInstruction",
|
||||
"M16C/60:LE:16:default",
|
||||
"M16C/80:LE:16:default",
|
||||
])("accepts the stock Ghidra language ID %s", (languageId) => {
|
||||
const parsed = parseConfig({ REA_GHIDRA_LANGUAGE_ID: languageId });
|
||||
if (!parsed.ok) throw parsed.error;
|
||||
expect(parsed.value.ghidraLanguageOverride).toEqual({ languageId });
|
||||
});
|
||||
|
||||
it.each(["ARM:BELE:32:v8", "/x86:LE:32:default", "x86:LE:32:default "])(
|
||||
"still refuses the malformed language ID %j",
|
||||
(languageId) => {
|
||||
const parsed = parseConfig({ REA_GHIDRA_LANGUAGE_ID: languageId });
|
||||
expect(parsed.ok).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it("requires a language for a compiler spec, as analyzeHeadless does", () => {
|
||||
const parsed = parseConfig({ REA_GHIDRA_COMPILER_SPEC_ID: "borlandcpp" });
|
||||
if (parsed.ok) throw new Error("Expected a configuration error");
|
||||
|
||||
@@ -122,7 +122,14 @@ const terminationShape = (): JsonShape => {
|
||||
return {
|
||||
kind: "object",
|
||||
members: {
|
||||
...scalarMembers("namespace", "code", "indicator"),
|
||||
...scalarMembers(
|
||||
"namespace",
|
||||
"code",
|
||||
"indicator",
|
||||
"flags",
|
||||
"byPid",
|
||||
"byProc",
|
||||
),
|
||||
reasons: {
|
||||
kind: "array",
|
||||
items: SCALAR,
|
||||
@@ -150,6 +157,7 @@ export const ipsBodyShape = (input: InspectAppleIpsInput): JsonShape => {
|
||||
"procName",
|
||||
"procPath",
|
||||
"pid",
|
||||
"parentPid",
|
||||
"parentProc",
|
||||
"cpuType",
|
||||
"translated",
|
||||
@@ -202,6 +210,8 @@ export const IPS_METADATA_SHAPE: JsonShape = {
|
||||
"bundleID",
|
||||
"incident_id",
|
||||
"name",
|
||||
"build_version",
|
||||
"platform",
|
||||
),
|
||||
beforeUnlisted: unlistedKeys,
|
||||
};
|
||||
|
||||
@@ -383,11 +383,7 @@ it("keeps ReportCrash-shaped producer facts and reports complete coverage", () =
|
||||
description: "(Data Abort) byte read Translation fault",
|
||||
});
|
||||
expect(parsed.exception?.unlisted_keys).toEqual(["rawCodes"]);
|
||||
expect(parsed.termination?.unlisted_keys).toEqual([
|
||||
"flags",
|
||||
"byProc",
|
||||
"byPid",
|
||||
]);
|
||||
expect(parsed.termination?.unlisted_keys).toEqual([]);
|
||||
expect(parsed.unlisted_body_keys).toContain("sharedCache");
|
||||
expect(parsed.unlisted_body_keys).not.toContain("osVersion");
|
||||
expect(parsed.metadata.unlisted_keys).toContain("slice_uuid");
|
||||
@@ -402,6 +398,37 @@ it("keeps ReportCrash-shaped producer facts and reports complete coverage", () =
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the documented terminating process, parent pid and metadata members", () => {
|
||||
const parsed = report({});
|
||||
expect(parsed.termination).toMatchObject({
|
||||
flags: { decimal: "0", hex: "0x0" },
|
||||
by_pid: { decimal: "4242", hex: "0x1092" },
|
||||
by_proc: "exc handler",
|
||||
});
|
||||
expect(parsed.process.parent_pid).toEqual({ decimal: "100", hex: "0x64" });
|
||||
expect(parsed.metadata).toMatchObject({
|
||||
build_version: "",
|
||||
platform: { decimal: "1", hex: "0x1" },
|
||||
});
|
||||
expect(parsed.metadata.unlisted_keys).not.toContain("build_version");
|
||||
expect(parsed.metadata.unlisted_keys).not.toContain("platform");
|
||||
expect(parsed.unlisted_body_keys).not.toContain("parentPid");
|
||||
expect(parsed.coverage.status).toBe("complete");
|
||||
const wrong = parse(
|
||||
Buffer.from(
|
||||
appleIpsReport().replace('"byPid": 4242', '"byPid": "4242"'),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
expect(wrong.termination?.by_pid).toBeNull();
|
||||
expect(wrong.coverage.type_mismatches).toContainEqual(
|
||||
expect.objectContaining({
|
||||
path: "body.termination.byPid",
|
||||
expected: "integer",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps the recorded image name and symbol offset", () => {
|
||||
const parsed = report({});
|
||||
expect(parsed.images.map((image) => image.name)).toEqual([
|
||||
@@ -511,8 +538,10 @@ it.each(["-1", "4294967296", `1${"0".repeat(1000)}`])(
|
||||
);
|
||||
|
||||
it("bounds rejected index text before retaining it", () => {
|
||||
const bytes = withBody(
|
||||
'{"faultingThread":4294967296,"threads":[],"usedImages":[]}',
|
||||
// A bare metadata line: its recorded platform integer would also count toward this allowance.
|
||||
const bytes = Buffer.from(
|
||||
'{"bug_type":"309"}\n{"faultingThread":4294967296,"threads":[],"usedImages":[]}',
|
||||
"utf8",
|
||||
);
|
||||
expect(() =>
|
||||
parseAppleIps(bytes, artifact(bytes), {
|
||||
|
||||
@@ -86,6 +86,9 @@ const terminationOf = (
|
||||
namespace: read.text(members.namespace, "body.termination.namespace"),
|
||||
code: read.integer(members.code, "body.termination.code"),
|
||||
indicator: read.text(members.indicator, "body.termination.indicator"),
|
||||
flags: read.integer(members.flags, "body.termination.flags"),
|
||||
by_pid: read.integer(members.byPid, "body.termination.byPid"),
|
||||
by_proc: read.text(members.byProc, "body.termination.byProc"),
|
||||
reasons:
|
||||
reasonItems === null
|
||||
? null
|
||||
@@ -353,6 +356,8 @@ const metadataOf = (text: string, read: FieldReader): AppleIps["metadata"] => {
|
||||
bundle_id: read.text(members.bundleID, "metadata.bundleID"),
|
||||
incident_id: read.text(members.incident_id, "metadata.incident_id"),
|
||||
name: read.text(members.name, "metadata.name"),
|
||||
build_version: read.text(members.build_version, "metadata.build_version"),
|
||||
platform: read.integer(members.platform, "metadata.platform"),
|
||||
unlisted_keys: [...json.unlisted],
|
||||
};
|
||||
};
|
||||
@@ -433,6 +438,7 @@ export const parseAppleIps = (
|
||||
path: read.text(body.procPath, "body.procPath"),
|
||||
pid: read.integer(body.pid, "body.pid"),
|
||||
parent: read.text(body.parentProc, "body.parentProc"),
|
||||
parent_pid: read.integer(body.parentPid, "body.parentPid"),
|
||||
cpu_type: read.text(body.cpuType, "body.cpuType"),
|
||||
translated: read.boolean(body.translated, "body.translated"),
|
||||
os_version: osVersionOf(body.osVersion, read),
|
||||
|
||||
@@ -4,7 +4,10 @@ import { Cli, Formatter } from "incur";
|
||||
import { estimateTokenCount } from "tokenx";
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
import { parseCliOutputArguments } from "../../../src/cliOutput.js";
|
||||
import {
|
||||
parseCliOutputArguments,
|
||||
renderCliDocument,
|
||||
} from "../../../src/cliOutput.js";
|
||||
import {
|
||||
createCliResultOutput,
|
||||
streamCliCommandResults,
|
||||
@@ -80,7 +83,7 @@ it("renders only each record's own fields in heterogeneous Markdown tables", asy
|
||||
"name",
|
||||
"constructor",
|
||||
"toString",
|
||||
"__proto__",
|
||||
"\\_\\_proto\\_\\_",
|
||||
"enabled",
|
||||
"count",
|
||||
]);
|
||||
@@ -346,10 +349,9 @@ it.each(["json", "jsonl", "yaml", "toon", "md"] as const)(
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line)),
|
||||
).toEqual(expected);
|
||||
else if (format === "md") {
|
||||
expect(outputs[0]).toContain(`| ${JSON.stringify(expected[0]!.rows)} |`);
|
||||
expect(outputs[0]).not.toContain("[object Object]");
|
||||
} else expect(outputs[0]).toBe(`${Formatter.format(expected, format)}\n`);
|
||||
else if (format === "md")
|
||||
expect(outputs[0]).toBe(renderCliDocument(expected, format, false));
|
||||
else expect(outputs[0]).toBe(`${Formatter.format(expected, format)}\n`);
|
||||
expect(projectionSource[0]?.rows[0]?.retained).toEqual([0, 10]);
|
||||
},
|
||||
);
|
||||
@@ -406,10 +408,9 @@ it.each(["json", "jsonl", "yaml", "toon", "md"] as const)(
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line)),
|
||||
).toEqual(expected);
|
||||
else if (format === "md") {
|
||||
expect(outputs[0]).toContain(`| ${JSON.stringify(expected[0]!.rows)} |`);
|
||||
expect(outputs[0]).not.toContain("[object Object]");
|
||||
} else expect(outputs[0]).toBe(`${Formatter.format(expected, format)}\n`);
|
||||
else if (format === "md")
|
||||
expect(outputs[0]).toBe(renderCliDocument(expected, format, false));
|
||||
else expect(outputs[0]).toBe(`${Formatter.format(expected, format)}\n`);
|
||||
expect(projectionSource).toEqual(before);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,105 @@
|
||||
import { readFile, writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
|
||||
import { parseFragment, type DefaultTreeAdapterTypes } from "parse5";
|
||||
import { createMarkdownRenderer, disposeMdItInstance } from "vitepress";
|
||||
import { afterAll, expect } from "vitest";
|
||||
|
||||
import { createEvidence } from "../../../src/domain/evidence.js";
|
||||
import { createEvidenceBundle } from "../../../src/domain/evidenceBundle.js";
|
||||
import { renderCliDocument } from "../../../src/cliOutput.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { cliTest } from "../../support/cli/cliFixture.js";
|
||||
|
||||
afterAll(() => disposeMdItInstance());
|
||||
|
||||
const renderedCells = (html: string): string[] => {
|
||||
const text = (node: DefaultTreeAdapterTypes.Node): string =>
|
||||
node.nodeName === "#text" && "value" in node
|
||||
? node.value
|
||||
: "childNodes" in node
|
||||
? node.childNodes.map(text).join("")
|
||||
: "";
|
||||
const cells: string[] = [];
|
||||
const visit = (node: DefaultTreeAdapterTypes.Node): void => {
|
||||
if ("tagName" in node && (node.tagName === "th" || node.tagName === "td"))
|
||||
cells.push(text(node));
|
||||
else if ("childNodes" in node) node.childNodes.forEach(visit);
|
||||
};
|
||||
visit(parseFragment(html));
|
||||
return cells;
|
||||
};
|
||||
|
||||
cliTest(
|
||||
"preserves literal Evidence keys and values when Markdown tables are rendered",
|
||||
async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-markdown-literal-cells-");
|
||||
const path = join(root, "bundle.json");
|
||||
const literal =
|
||||
"`code` **bold** _name_ ~~deleted~~ [label](target) ";
|
||||
const punctuation = "\\`tick` \\*star* \\[label](target) a|b <T> ©";
|
||||
const identifiers = "x__y__z _a_b_ 中_文 snake_case_";
|
||||
const row = {
|
||||
[literal]: literal,
|
||||
punctuation,
|
||||
nested: { text: literal, values: ["__name__", "plain"] },
|
||||
empty: "",
|
||||
zero: 0,
|
||||
disabled: false,
|
||||
absent: null,
|
||||
source_index: identifiers,
|
||||
};
|
||||
const result = { scalar: { [literal]: literal }, rows: [row] };
|
||||
const record = createEvidence(
|
||||
undefined,
|
||||
{ id: "fixture", name: "Fixture", version: "1" },
|
||||
{ operation: "ordinary-json", parameters: {}, result },
|
||||
);
|
||||
const saved = JSON.stringify(createEvidenceBundle([record]));
|
||||
await writeFile(path, saved);
|
||||
const base = [
|
||||
"inspect-evidence-bundle",
|
||||
JSON.stringify({ path, detail: "complete" }),
|
||||
"--filter-output",
|
||||
"records.0.normalized_result",
|
||||
];
|
||||
const json = await cli.run({ arguments: [...base, "--json"] });
|
||||
const markdown = await cli.run({ arguments: [...base, "--format", "md"] });
|
||||
expect(json.exitCode, json.stderr).toBe(0);
|
||||
expect(markdown.exitCode, markdown.stderr).toBe(0);
|
||||
expect(JSON.parse(json.stdout)).toEqual({
|
||||
records: { "0": { normalized_result: result } },
|
||||
});
|
||||
expect(await readFile(path, "utf8")).toBe(saved);
|
||||
// Intraword underscores cannot form emphasis, so identifiers stay readable.
|
||||
expect(markdown.stdout).toContain(" source_index ");
|
||||
expect(markdown.stdout).toContain(
|
||||
" x__y__z \\_a_b\\_ 中_文 snake_case\\_ ",
|
||||
);
|
||||
|
||||
const renderer = await createMarkdownRenderer(root, { linkify: false });
|
||||
const html = renderer.render(markdown.stdout);
|
||||
const expectedCells = [
|
||||
"Key",
|
||||
"Value",
|
||||
literal,
|
||||
literal,
|
||||
...Object.keys(row),
|
||||
literal,
|
||||
punctuation,
|
||||
JSON.stringify(row.nested),
|
||||
"",
|
||||
"0",
|
||||
"false",
|
||||
"null",
|
||||
identifiers,
|
||||
];
|
||||
expect(renderedCells(html)).toEqual(expectedCells);
|
||||
expect(html).not.toMatch(/<(?:em|strong|s|code|img)(?:\s|>)/u);
|
||||
|
||||
// Standalone reports share the same table owner as command output.
|
||||
expect(
|
||||
renderedCells(renderer.render(renderCliDocument(result, "md", false))),
|
||||
).toEqual(expectedCells);
|
||||
},
|
||||
);
|
||||
@@ -16,7 +16,7 @@ it("retains nested facts in standalone Markdown table cells and mixed arrays", (
|
||||
];
|
||||
const markdown = renderCliDocument(value, "md", false);
|
||||
expect(markdown).toContain('{"path":"alpha.txt"}');
|
||||
expect(markdown).toContain('[{"path":"sub/beta.txt"}]');
|
||||
expect(markdown).toContain('\\[{"path":"sub/beta.txt"}\\]');
|
||||
expect(markdown).toContain("caller\\|value");
|
||||
expect(markdown).not.toContain("[object Object]");
|
||||
expect(
|
||||
|
||||
@@ -107,6 +107,7 @@ beforeAll(async () => {
|
||||
author("claude"),
|
||||
author("cursoragent"),
|
||||
author("codex"),
|
||||
author("Copilot"),
|
||||
author("morluto"),
|
||||
]),
|
||||
associatedPullRequests: page([]),
|
||||
@@ -235,14 +236,16 @@ it("inventories side branches and paginated credits, then checks and renders syn
|
||||
inventory.commits.flatMap(({ authors }) =>
|
||||
authors.map(({ user }) => user?.login),
|
||||
),
|
||||
).toEqual(expect.arrayContaining(["claude", "cursoragent", "codex"]));
|
||||
).toEqual(
|
||||
expect.arrayContaining(["claude", "cursoragent", "codex", "Copilot"]),
|
||||
);
|
||||
expect(inventory.pullRequests.map((pr) => pr.number)).toEqual([101]);
|
||||
expect(f.record).toContain(
|
||||
"Thanks @alice, @bob, @reporter, @second-reporter.",
|
||||
);
|
||||
expect(f.record).toContain("Thanks @dana.");
|
||||
expect(f.record).not.toMatch(
|
||||
/@(?:merger|automation|platformbot|claude|cursoragent|codex|morluto)/u,
|
||||
/@(?:merger|automation|platformbot|claude|cursoragent|codex|Copilot|morluto)/u,
|
||||
);
|
||||
const body = join(f.directory, "body.md");
|
||||
await writeFile(
|
||||
@@ -340,6 +343,7 @@ it("rejects lost grouped credits, fabricated references, bots, and stale PR bodi
|
||||
f.bullet + " Thanks @automation[bot].",
|
||||
"Unverified human credit @automation[bot]",
|
||||
],
|
||||
[f.bullet + " Thanks @Copilot.", "Unverified human credit @copilot"],
|
||||
]) {
|
||||
await writeFile(
|
||||
join(f.directory, "CHANGELOG.md"),
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
occurrenceIdForLocation,
|
||||
} from "../../../src/domain/artifactIdentity.js";
|
||||
import { artifactInspectionResultSchema } from "../../../src/domain/artifactInspection.js";
|
||||
import { parseArtifactInventoryEvidence } from "../../../src/domain/artifactInventoryEvidence.js";
|
||||
import {
|
||||
ok as resultOk,
|
||||
err as resultErr,
|
||||
@@ -1258,6 +1259,9 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
|
||||
const root = await createTestTempDirectory("rea-asar-attribution-");
|
||||
const source = join(root, "source");
|
||||
await mkdir(source);
|
||||
await writeFile(join(source, "keep.txt"), "unchanged sibling\n");
|
||||
const absentArchive = join(root, "absent.asar");
|
||||
await createPackageWithOptions(source, absentArchive, {});
|
||||
const original = "console.log('ok');\n";
|
||||
const changed = "console.log('no');\n";
|
||||
await writeFile(join(source, "main.js"), original);
|
||||
@@ -1280,7 +1284,12 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
|
||||
await server.connect(st);
|
||||
await client.connect(ct);
|
||||
const observations = [];
|
||||
for (const path of [originalArchive, staleArchive, currentArchive]) {
|
||||
for (const path of [
|
||||
originalArchive,
|
||||
staleArchive,
|
||||
currentArchive,
|
||||
absentArchive,
|
||||
]) {
|
||||
expect(
|
||||
(await client.callTool({ name: "open_binary", arguments: { path } }))
|
||||
.isError,
|
||||
@@ -1298,20 +1307,32 @@ it("attributes ASAR integrity contradictions to their actual inputs and withhold
|
||||
PROCESS_COMPARISON_EVIDENCE,
|
||||
])
|
||||
expect(session.recordEvidence(record).ok).toBe(true);
|
||||
const [originalEvidence, stale, current] = observations;
|
||||
const [originalEvidence, stale, current, absent] = observations;
|
||||
if (
|
||||
originalEvidence === undefined ||
|
||||
stale === undefined ||
|
||||
current === undefined
|
||||
current === undefined ||
|
||||
absent === undefined
|
||||
)
|
||||
throw new Error("Expected three artifact observations");
|
||||
for (const [left, right, mismatches] of [
|
||||
[stale, current, [stale.evidence_id]],
|
||||
[current, stale, [stale.evidence_id]],
|
||||
[stale, stale, [stale.evidence_id]],
|
||||
[originalEvidence, current, []],
|
||||
throw new Error("Expected four artifact observations");
|
||||
for (const [left, right, mismatches, presence] of [
|
||||
[stale, current, [stale.evidence_id], "changed"],
|
||||
[current, stale, [stale.evidence_id], "changed"],
|
||||
[stale, stale, [stale.evidence_id], "changed"],
|
||||
[originalEvidence, current, [], "changed"],
|
||||
[absent, stale, [stale.evidence_id], "added"],
|
||||
[stale, absent, [stale.evidence_id], "removed"],
|
||||
[absent, current, [], "added"],
|
||||
[current, absent, [], "removed"],
|
||||
] as const)
|
||||
await assertIntegrityComparison(client, session, left, right, mismatches);
|
||||
await assertIntegrityComparison(
|
||||
client,
|
||||
session,
|
||||
left,
|
||||
right,
|
||||
mismatches,
|
||||
presence,
|
||||
);
|
||||
} finally {
|
||||
await Promise.allSettled([client.close(), server.close(), session.close()]);
|
||||
}
|
||||
@@ -1323,6 +1344,7 @@ const assertIntegrityComparison = async (
|
||||
left: ReturnType<typeof toolEvidence>,
|
||||
right: ReturnType<typeof toolEvidence>,
|
||||
mismatches: readonly string[],
|
||||
presence: "added" | "removed" | "changed",
|
||||
) => {
|
||||
const compared = await client.callTool({
|
||||
name: "compare_artifacts",
|
||||
@@ -1332,12 +1354,31 @@ const assertIntegrityComparison = async (
|
||||
const comparison = toolEvidence(compared.structuredContent);
|
||||
expect(comparison.normalized_result).toMatchObject({
|
||||
status: mismatches.length === 0 ? "changed" : "contradiction",
|
||||
summary: { contradiction: mismatches.length === 0 ? 0 : 1 },
|
||||
integrity_mismatch_evidence_ids: mismatches,
|
||||
});
|
||||
const unknown = session
|
||||
.listUnknowns({ domain: "artifact-comparison" })
|
||||
.find((item) => item.question.includes(comparison.evidence_id));
|
||||
expect(unknown).toBeDefined();
|
||||
const [leftMember, rightMember] = [left, right].map((record) =>
|
||||
parseArtifactInventoryEvidence(record).inventory.occurrences.find(
|
||||
({ logical_path }) => logical_path === "main.js",
|
||||
),
|
||||
);
|
||||
expect(comparison.normalized_result).toMatchObject({
|
||||
changes: expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
logical_path: "main.js",
|
||||
classification: mismatches.length > 0 ? "contradiction" : presence,
|
||||
...(mismatches.length > 0 ? { dimensions: ["integrity"] } : {}),
|
||||
left_occurrence_id: leftMember?.occurrence_id ?? null,
|
||||
left_artifact_id: leftMember?.artifact_id ?? null,
|
||||
right_occurrence_id: rightMember?.occurrence_id ?? null,
|
||||
right_artifact_id: rightMember?.artifact_id ?? null,
|
||||
}),
|
||||
]),
|
||||
});
|
||||
if (mismatches.length > 0) {
|
||||
expect(unknown?.contradicting_evidence_ids).toEqual(mismatches);
|
||||
expect(unknown?.supporting_evidence_ids).toEqual(
|
||||
|
||||
@@ -24,6 +24,10 @@ import {
|
||||
renamedBook,
|
||||
} from "../../fixtures/environmentStateWrites.js";
|
||||
import { referencedCreateRows } from "../../fixtures/environmentStateDerivedValues.js";
|
||||
import {
|
||||
READ_BACK_TAG_ROWS,
|
||||
slugOnlySaveRows,
|
||||
} from "../../fixtures/environmentStateWriteViews.js";
|
||||
import { unsupportedInputLiteralSchemaError } from "../../fixtures/mcpClientSchemaConstraints.js";
|
||||
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
||||
import { startListeningProcess } from "../../support/mcp/streamableHttpProcess.js";
|
||||
@@ -34,6 +38,66 @@ const HTTP_BOUNDARY_TIMEOUT_MS = 90_000;
|
||||
/** The book the first served create of "Boundary Book" makes: one second past the served clock's start. */
|
||||
const BOUNDARY_BOOK = book(3, "Boundary Book", bookClock(13));
|
||||
|
||||
processTest(
|
||||
"keeps create response clocks separate from enriched item reads through the SDK",
|
||||
async ({ processes }) => {
|
||||
const rows = READ_BACK_TAG_ROWS;
|
||||
const root = await createTestTempDirectory("rea-write-views-sdk-");
|
||||
const files = await writeEnvironmentServeFiles(
|
||||
root,
|
||||
environmentStateServeFixture(timedCaptureEvidence(rows)),
|
||||
);
|
||||
const started = await startListeningProcess(processes, [
|
||||
"environment-serve",
|
||||
files.manifestPath,
|
||||
"--evidence",
|
||||
files.capturePath,
|
||||
"--port",
|
||||
"0",
|
||||
]);
|
||||
const client = new Client({ name: "write-views", version: "1" });
|
||||
await client.connect(new StreamableHTTPClientTransport(started.url));
|
||||
try {
|
||||
const { tools } = await client.listTools();
|
||||
expect(tools.map(({ name }) => name)).toEqual(
|
||||
expect.arrayContaining(["post_tags", "get_tags_by_id"]),
|
||||
);
|
||||
const created = await client.callTool({
|
||||
name: "post_tags",
|
||||
arguments: { body: { name: "Boundary Tag" } },
|
||||
});
|
||||
expect(created.isError).toBeFalsy();
|
||||
const clock = bookClock(rows.length + 1);
|
||||
expect(created.structuredContent).toMatchObject({
|
||||
body: {
|
||||
id: 3,
|
||||
name: "Boundary Tag",
|
||||
created: clock.replace("Z", ".000000000Z"),
|
||||
},
|
||||
state: { source: "state" },
|
||||
});
|
||||
expect(created.structuredContent).not.toHaveProperty(
|
||||
"body.document_count",
|
||||
);
|
||||
const read = await client.callTool({
|
||||
name: "get_tags_by_id",
|
||||
arguments: { id: "3" },
|
||||
});
|
||||
expect(read.structuredContent).toMatchObject({
|
||||
body: {
|
||||
id: 3,
|
||||
name: "Boundary Tag",
|
||||
created: clock,
|
||||
document_count: 0,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
},
|
||||
HTTP_BOUNDARY_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
processTest(
|
||||
"discovers empty fixed values and applies omitted recorded queries through the SDK before enforcing writes",
|
||||
async ({ processes }) => {
|
||||
@@ -411,15 +475,28 @@ processTest(
|
||||
HTTP_BOUNDARY_TIMEOUT_MS,
|
||||
);
|
||||
|
||||
processTest(
|
||||
"withholds a create from public tools when its sent reference contradicts the recorded response",
|
||||
async ({ processes }) => {
|
||||
const root = await createTestTempDirectory("rea-create-reference-mcp-");
|
||||
const rows = referencedCreateRows({
|
||||
processTest.for([
|
||||
{
|
||||
label: "a create whose sent reference contradicts its response",
|
||||
rows: referencedCreateRows({
|
||||
id: 1,
|
||||
name: "Different Hall",
|
||||
floor: "ground",
|
||||
});
|
||||
}),
|
||||
read: "get_books",
|
||||
write: "post_books",
|
||||
},
|
||||
...[true, false].map((derivedFirst) => ({
|
||||
label: `a slug-only edit incompatible with a derived save (derived first: ${String(derivedFirst)})`,
|
||||
rows: slugOnlySaveRows(derivedFirst, true),
|
||||
read: "get_tags",
|
||||
write: "patch_tags_by_id",
|
||||
})),
|
||||
])(
|
||||
"withholds $label from public tools",
|
||||
{ timeout: HTTP_BOUNDARY_TIMEOUT_MS },
|
||||
async ({ rows, read, write }, { processes }) => {
|
||||
const root = await createTestTempDirectory("rea-create-reference-mcp-");
|
||||
const files = await writeEnvironmentServeFiles(
|
||||
root,
|
||||
environmentStateServeFixture(timedCaptureEvidence(rows)),
|
||||
@@ -439,11 +516,10 @@ processTest(
|
||||
await client.connect(new StreamableHTTPClientTransport(started.url));
|
||||
try {
|
||||
const { tools } = await client.listTools();
|
||||
expect(tools.map(({ name }) => name)).toContain("get_books");
|
||||
expect(tools.map(({ name }) => name)).not.toContain("post_books");
|
||||
expect(tools.map(({ name }) => name)).toContain(read);
|
||||
expect(tools.map(({ name }) => name)).not.toContain(write);
|
||||
} finally {
|
||||
await client.close();
|
||||
}
|
||||
},
|
||||
HTTP_BOUNDARY_TIMEOUT_MS,
|
||||
);
|
||||
+292
@@ -0,0 +1,292 @@
|
||||
import type { TimedCaptureRow } from "./analysisView.js";
|
||||
import { bookClock } from "./environmentStateWrites.js";
|
||||
|
||||
const tag = (id: number, name: string, slug: string, created: string) => ({
|
||||
id,
|
||||
slug,
|
||||
name,
|
||||
document_count: id === 1 ? 2 : 0,
|
||||
created,
|
||||
});
|
||||
|
||||
const SEED_TAGS = [
|
||||
tag(1, "Lamp Oil", "lamp-oil", "2026-10-01T09:00:00Z"),
|
||||
tag(2, "Tide Tables", "tide-tables", "2026-10-01T10:00:00Z"),
|
||||
];
|
||||
|
||||
/** A tag as a create answers it: without the document count reads show, and with its clock to the nanosecond. */
|
||||
const answered = (id: number, name: string, slug: string, at: number) => ({
|
||||
id,
|
||||
slug,
|
||||
name,
|
||||
created: bookClock(at).replace("Z", ".123456789Z"),
|
||||
});
|
||||
|
||||
/** Two tag creates whose answers lack the document count, and spell the clock otherwise, than the item read after each. */
|
||||
export const READ_BACK_TAG_ROWS: readonly TimedCaptureRow[] = [
|
||||
["GET", "/api/tags", SEED_TAGS],
|
||||
["GET", "/api/tags/1", SEED_TAGS[0]!],
|
||||
["GET", "/api/tags/2", SEED_TAGS[1]!],
|
||||
[
|
||||
"POST",
|
||||
"/api/tags",
|
||||
answered(3, "Harbour Notices", "harbour-notices", 3),
|
||||
201,
|
||||
{ name: "Harbour Notices" },
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/tags/3",
|
||||
tag(3, "Harbour Notices", "harbour-notices", bookClock(3)),
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/tags",
|
||||
[...SEED_TAGS, tag(3, "Harbour Notices", "harbour-notices", bookClock(3))],
|
||||
],
|
||||
[
|
||||
"POST",
|
||||
"/api/tags",
|
||||
answered(4, "Quarry Notes", "quarry-notes", 6),
|
||||
201,
|
||||
{ name: "Quarry Notes" },
|
||||
],
|
||||
["GET", "/api/tags/4", tag(4, "Quarry Notes", "quarry-notes", bookClock(6))],
|
||||
[
|
||||
"GET",
|
||||
"/api/tags",
|
||||
[
|
||||
...SEED_TAGS,
|
||||
tag(3, "Harbour Notices", "harbour-notices", bookClock(3)),
|
||||
tag(4, "Quarry Notes", "quarry-notes", bookClock(6)),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
const label = (id: number, title: string, posts: number) => ({
|
||||
id,
|
||||
title,
|
||||
count: { posts },
|
||||
});
|
||||
|
||||
/** Two label creates whose answers lack the post count only the list shows; no item read follows them. */
|
||||
export const LISTED_LABEL_ROWS: readonly TimedCaptureRow[] = [
|
||||
["GET", "/api/labels", [label(1, "Ferry", 3), label(2, "Pier", 1)]],
|
||||
[
|
||||
"POST",
|
||||
"/api/labels",
|
||||
{ id: 3, title: "Mooring" },
|
||||
201,
|
||||
{ title: "Mooring" },
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/labels",
|
||||
[label(1, "Ferry", 3), label(2, "Pier", 1), label(3, "Mooring", 0)],
|
||||
],
|
||||
["POST", "/api/labels", { id: 4, title: "Buoy" }, 201, { title: "Buoy" }],
|
||||
[
|
||||
"GET",
|
||||
"/api/labels",
|
||||
[
|
||||
label(1, "Ferry", 3),
|
||||
label(2, "Pier", 1),
|
||||
label(3, "Mooring", 0),
|
||||
label(4, "Buoy", 0),
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
/** Two renames that send the tag whole, its old slug included, which the application slugs anew from the name. */
|
||||
export const STALE_SLUG_ROWS: readonly TimedCaptureRow[] = [
|
||||
["GET", "/api/tags", SEED_TAGS],
|
||||
["GET", "/api/tags/1", SEED_TAGS[0]!],
|
||||
["GET", "/api/tags/2", SEED_TAGS[1]!],
|
||||
[
|
||||
"PUT",
|
||||
"/api/tags/1",
|
||||
{
|
||||
...SEED_TAGS[0]!,
|
||||
name: "Lamp Oil Deliveries",
|
||||
slug: "lamp-oil-deliveries",
|
||||
},
|
||||
200,
|
||||
{ ...SEED_TAGS[0]!, name: "Lamp Oil Deliveries" },
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/tags/1",
|
||||
{
|
||||
...SEED_TAGS[0]!,
|
||||
name: "Lamp Oil Deliveries",
|
||||
slug: "lamp-oil-deliveries",
|
||||
},
|
||||
],
|
||||
[
|
||||
"PUT",
|
||||
"/api/tags/2",
|
||||
{
|
||||
...SEED_TAGS[1]!,
|
||||
name: "Spring Tide Tables",
|
||||
slug: "spring-tide-tables",
|
||||
},
|
||||
200,
|
||||
{ ...SEED_TAGS[1]!, name: "Spring Tide Tables" },
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/tags/2",
|
||||
{
|
||||
...SEED_TAGS[1]!,
|
||||
name: "Spring Tide Tables",
|
||||
slug: "spring-tide-tables",
|
||||
},
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/tags",
|
||||
[
|
||||
{
|
||||
...SEED_TAGS[0]!,
|
||||
name: "Lamp Oil Deliveries",
|
||||
slug: "lamp-oil-deliveries",
|
||||
},
|
||||
{
|
||||
...SEED_TAGS[1]!,
|
||||
name: "Spring Tide Tables",
|
||||
slug: "spring-tide-tables",
|
||||
},
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
const POST = {
|
||||
id: 1,
|
||||
title: "Winter rota",
|
||||
updated_at: "2026-10-01T09:00:00Z",
|
||||
};
|
||||
|
||||
/** Two edits that send back the modification time they read, which the application stamps anew. */
|
||||
export const RESTAMPED_ROWS: readonly TimedCaptureRow[] = [
|
||||
["GET", "/api/posts", [POST]],
|
||||
["GET", "/api/posts/1", POST],
|
||||
[
|
||||
"PUT",
|
||||
"/api/posts/1",
|
||||
{ ...POST, title: "Winter rota v2", updated_at: bookClock(2) },
|
||||
200,
|
||||
{ ...POST, title: "Winter rota v2" },
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/posts/1",
|
||||
{ ...POST, title: "Winter rota v2", updated_at: bookClock(2) },
|
||||
],
|
||||
[
|
||||
"PUT",
|
||||
"/api/posts/1",
|
||||
{ ...POST, title: "Winter rota v3", updated_at: bookClock(4) },
|
||||
200,
|
||||
{ ...POST, title: "Winter rota v3", updated_at: bookClock(2) },
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/posts/1",
|
||||
{ ...POST, title: "Winter rota v3", updated_at: bookClock(4) },
|
||||
],
|
||||
[
|
||||
"GET",
|
||||
"/api/posts",
|
||||
[{ ...POST, title: "Winter rota v3", updated_at: bookClock(4) }],
|
||||
],
|
||||
];
|
||||
|
||||
/** One save leaves its slug as sent; the other demonstrates regeneration, in either recording order. */
|
||||
export const slugSaveRows = (
|
||||
derivedFirst: boolean,
|
||||
customSlug = false,
|
||||
): readonly TimedCaptureRow[] => {
|
||||
const saved = { ...SEED_TAGS[0]!, ...(customSlug ? { slug: "custom" } : {}) };
|
||||
const renamed = {
|
||||
...SEED_TAGS[1]!,
|
||||
name: "Spring Tide Tables",
|
||||
slug: "spring-tide-tables",
|
||||
};
|
||||
const saves: TimedCaptureRow[][] = [
|
||||
[
|
||||
["PUT", "/api/tags/1", saved, 200, saved],
|
||||
["GET", "/api/tags/1", saved],
|
||||
],
|
||||
[
|
||||
[
|
||||
"PUT",
|
||||
"/api/tags/2",
|
||||
renamed,
|
||||
200,
|
||||
{ ...SEED_TAGS[1]!, name: renamed.name },
|
||||
],
|
||||
["GET", "/api/tags/2", renamed],
|
||||
],
|
||||
];
|
||||
return [
|
||||
...STALE_SLUG_ROWS.slice(0, 3),
|
||||
...saves[derivedFirst ? 1 : 0]!,
|
||||
...saves[derivedFirst ? 0 : 1]!,
|
||||
["GET", "/api/tags", [saved, renamed]],
|
||||
];
|
||||
};
|
||||
|
||||
/** A slug-only PATCH and a save that demonstrates deriving the slug from the name. */
|
||||
export const slugOnlySaveRows = (
|
||||
derivedFirst: boolean,
|
||||
changed: boolean,
|
||||
): readonly TimedCaptureRow[] =>
|
||||
slugSaveRows(derivedFirst, changed).map(
|
||||
([method, path, body = null, status, request, ...rest]): TimedCaptureRow =>
|
||||
method === "PUT"
|
||||
? [
|
||||
"PATCH",
|
||||
path,
|
||||
body,
|
||||
status ?? 200,
|
||||
path === "/api/tags/1"
|
||||
? { slug: changed ? "custom" : "lamp-oil" }
|
||||
: request,
|
||||
...rest,
|
||||
]
|
||||
: [method, path, body, status ?? 200, request, ...rest],
|
||||
);
|
||||
|
||||
/** One whole-object save falls in its held clock's second; the other visibly stamps a fresh clock. */
|
||||
export const clockSaveRows = (
|
||||
derivedFirst: boolean,
|
||||
): readonly TimedCaptureRow[] => {
|
||||
const heldIndex = derivedFirst ? 5 : 3;
|
||||
const freshIndex = derivedFirst ? 3 : 5;
|
||||
const held = { ...POST, updated_at: bookClock(heldIndex) };
|
||||
const fresh = { ...POST, id: 2 };
|
||||
const saved = { ...held, title: "Same-second save" };
|
||||
const stamped = {
|
||||
...fresh,
|
||||
title: "Fresh save",
|
||||
updated_at: bookClock(freshIndex),
|
||||
};
|
||||
const saves: TimedCaptureRow[][] = [
|
||||
[
|
||||
["PUT", "/api/posts/1", saved, 200, saved],
|
||||
["GET", "/api/posts/1", saved],
|
||||
],
|
||||
[
|
||||
["PUT", "/api/posts/2", stamped, 200, { ...fresh, title: stamped.title }],
|
||||
["GET", "/api/posts/2", stamped],
|
||||
],
|
||||
];
|
||||
return [
|
||||
["GET", "/api/posts", [held, fresh]],
|
||||
["GET", "/api/posts/1", held],
|
||||
["GET", "/api/posts/2", fresh],
|
||||
...saves[derivedFirst ? 1 : 0]!,
|
||||
...saves[derivedFirst ? 0 : 1]!,
|
||||
["GET", "/api/posts", [saved, stamped]],
|
||||
];
|
||||
};
|
||||
Reference in new issue
Block a user