Files
rea/docs/specs/agent-environments/write-rules/spec.md
T
Edgar Arakelyan 41cf869949 fix(environment): bound create learning and invalidate stale write proofs (#6929)
* feat(environment): report what an environment serves with an environment-coverage view

A new inspect_analysis_view kind over capture Evidence. It projects the
state an environment derives with the given template rules and reports
each tool as answered from state, replayed with its view reason, a served
write, or a withheld write with its reason, the records behind it and a
hint about what to record next. The hints cover every write reason, so a
new reason without one fails to compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(environment): build an environment bundle in one command

rea environment-build <capture> [directory] inspects a HAR (or reads
capture Evidence), projects the environment manifest from the in-memory
Evidence, authenticates the pair, prepares the state and writes the bundle
environment-export writes. Every bundle file is byte-identical to the
three-step path for the same capture and options.

The report adds the capture and manifest identities, the coverage summary,
warnings (a loopback capture named after its file, credential-named body
members by pointer) and the next command. --dry-run writes nothing. The
default template rules are the four the published environments use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(environment): cover environment-build and the coverage view

Domain tests check the hint table against the state view's reasons, the
summary, and the view against the environment-state view it summarizes.
Service tests build the fixture capture and compare every file with the
three-step path, a dry run, loopback naming and the refusals. A CLI test
compares a HAR built in one step with the three CLI steps byte for byte,
and an MCP test projects the view from every source kind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(environment): build the real-agentenv lane's bundle with environment-build

The lane now runs the command users run: it builds the Grafana-shaped HAR
with environment-build and the packed checkout, checks the report, then
runs the bundle with agent-env as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): document environment-build and the coverage view

The agent environments guide opens with the one-step build: the report,
the hint for each withheld write reason, and what a bundle ships. The CLI
guide, the capture guide and the skill reference name the command and the
environment-coverage view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): harden environment-build after review

- The credential scan walks with its own stack, so a deeply nested body
  cannot crash the build, and it also reports credential-named header
  names, query names, form body members and string arrays.
- The build checks SIGINT between stages: cancelled before the bundle
  stage it writes nothing, during the write it names the written bundle.
- A dry run opens the package tarball, so it refuses what a build refuses.
- Repeated template rules are refused, a HAR saved as .json gets a hint,
  file-derived names follow the bundle name rules, `localhost.` counts as
  a loopback origin, and the next command is shell-quoted.
- --capture-timeout-ms passes through to the HAR inspection, and a
  missing directory is refused before inspection.
- The no-observation hint covers updates and deletes of entities the state
  never held; the coverage view states that it does not apply the
  serve-time admission checks; the inspect_analysis_view description
  names the view.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): state the build's conditions and record its verification

The guides now state when the one-step bundle equals the three-step one,
which headers inspection excludes, what the credential warning scans, how
cancellation behaves, and that the coverage view does not apply the
serve-time admission checks. The spec folder records the census, the end
to end runs, the gates and the review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): dispatch a pending interrupt before each build stage check

The manifest, state and coverage stages run synchronously, so a SIGINT
that arrived during them was only dispatched once the bundle write had
begun. Each check now yields first, so an interrupt before the bundle
stage cancels the build with nothing written.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): scan scenario credentials, own the dry-run tarball and serialize Evidence once

A browser scenario's retained header values and the query parameters of its recorded URLs are scanned for credential
names as a HAR's are, through a review projection of its records. A dry run admits and releases the package tarball
through the export's regular-file scope, so a close it cannot confirm keeps its owner, and checks for cancellation
after that last await. Evidence is written into one exactly sized buffer instead of chunks joined by a copy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): record the review round's checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* style(environment): format the build spec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): scan every form occurrence and MIME-less scenario bodies

The build's credential scan collapsed form bodies with Object.fromEntries, so a
repeated field kept only its last occurrence: `api_key=secret&api_key=` warned
about nothing although the Evidence ships the value, and repeated values were
undercounted. Form fields are now scanned as ordered name/value pairs, as
headers and query strings already are.

The review projection of a capture_browser_scenario dropped a request body
recorded without a media type, so the scan never saw it. Review keeps such a
body's text with an empty media type; the manifest projection still reads only
bodies it can classify.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): scan structured HAR form params and the request side of unattributed scenario transactions

A HAR request recorded with postData.params and no text is scanned by those parameters, each occurrence counting, and
the review projection keeps the request side of a scenario transaction whose response cannot be attributed, so the
credential warning covers what evidence.json ships in both cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): validate build paths and read dry-run tarballs

* fix(environment): report bodies too large for the credential scan

Text bodies over the scan's 4 MiB limit, including a response decoded only as a prefix, are reported as a
credential-scan-incomplete warning by record and side, never by content, so an absent credential-named-members
warning no longer reads as a complete check. The spec and agent-environments guide also state the path validation and
dry-run tarball read that e52ae1cd added.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(verification): release cold source transform services

* fix(javascript): retain capture context along covered mutation paths

* fix(environment): admit serialized exports and scan HAR form fallbacks

* fix(environment): admit in-memory build inputs against heap headroom and scan SaveHar null-text forms

The inspected HAR's Evidence and the projected manifest are checked against live heap headroom before they are
serialized, with the same check the file route uses, so a capture too large to parse and seal is refused as
resource_constraint before anything is written. A request whose postData text is SaveHar's null is scanned by its
structured params.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): measure the credential scan limit in UTF-8 bytes

A body counts against the 4 MiB limit by its UTF-8 length, not its UTF-16 code units, so multibyte text over the
limit is reported as unscanned instead of parsed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(javascript): erase inline type imports before binding values

* fix(environment): parse credential form bodies by their media type

* fix(environment): review untyped retained form fields

* fix(environment): bound request comparisons and preserve write proofs

* fix(environment): keep write proofs and bundle review complete

* fix(environment): finalize entity overrides with simulated views

* test(environment): restore 0895c89b's untyped form tests and hint

Integrating main took main's credential scan and coverage view, which
dropped 0895c89b's tests and its body-unusable hint. Main's scan passes
the tests, now reading the scenario by event. The hint names the
response too, since extraction and serving also withhold a write as
body-unusable when its recorded response cannot be read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(environment): scan a scenario content event recorded twice

Main's event scan reads both content events of a transaction whose
content the capture recorded twice; the test fails on 0895c89b, whose
review projection dropped that transaction's response. The spec names
event pointers among the first 20.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(environment): record the review round's checks

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(environment): retain contradictions and trim retired state

* fix(environment): distinguish mixed array order from changed values

* fix(environment): keep dependent create state pinned

* fix(environment): keep checking retired view observations

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-24 23:57:30 +08:00

7.5 KiB

Serve writes by what their recordings show

Issue: https://github.com/morluto/rea/issues/4933

Outcome

After state targets, 30 write tools across the 50 published capture-built environments had a state target but were refused (contradicted, an unexplained change, or a side effect), and 59 still had none. A diagnosis traced each refused tool to the fields that differed between the gate's state and the recorded read, and named seven shared causes. Seven of the 30 are refusals REA should keep (asynchronous jobs, rendered markup, reverse embeds, appended relation rows), and six need only a better recording. This change addresses the causes REA can fix:

  • Blame too broad. A failed read contradicted every write it exercised, whatever fields differed.
  • Unsent update changes. An update changing a field it did not send had no rule unless the change was a top-level clock, unique value, slug, renewal or increment.
  • View order. Collections took their initial order from the earliest sighting, and arrays a view lists sorted were compared in written order.
  • Targets at a collection or an action path. Bulk writes naming entities in the body, and actions at /{type}/{id}/{verb}, had no target.
  • Per-response members. A UUID or clock minted on every response made a view's envelope vary.
  • Nested counters. A list total inside a paging object (pageInfo.totalRows) stayed at its recorded value.

Behaviour

  • Blame. A failed item read with no interfering unmodelled write contradicts the creates and deletes it exercised. It contradicts an update only on a field that update wrote or its response showed, and a field the state lacks that the read and the update's response both show is the update's change. A read that embeds a created entity elsewhere and the state does not contradicts the create.
  • Constant guard. A create member is accepted only with its recorded value when a constant derives from it: its echoed text inside the constant, a referenced entity's string, or an instant beside a sent date, matched by related field names. Only successful calls sent with the exemplar's query show that a member may vary.
  • Update rules. An update's unsent change can be a modified-by stamp beside a created-by counterpart (with its id sibling), a value derived from the one sent member stored nowhere (accepted only while the target holds the recorded before-value), or a sent clock other than a creation clock left in the call's own second, which every call must then send with the value the entity holds. An unsent clock left in the call's own second keeps the update unconfirmed until another call shows it stamped. Objects whose clocks moved with the request are nested clocks, stored from the read-back on creates and updates and refused once any call left the object as it was; opaque tokens of a type's own length are generated; an enveloped body is learned as its one object. A write takes only its recorded query, and a call sent with another neither confirms nor teaches its plan: a create or update only brings the state to what it recorded, and a delete, document or bulk call leaves its target unknown. An echoed reference to a nested object or a state word paired with other fields stays at its recorded value. The served catalog names these values.
  • View order. A collection's initial order comes from the first list line that shows every entity its type's sightings do and applies no order. A view renders a string array sorted only after a counted read-back shows an order that neither the written order nor existing-values-first explains, among the entities that read covers in its own collection. A later read contradicting a learned sort makes that view recorded and removes its earlier write confirmations and saved overrides; types and entities with no surviving state view leave the snapshot. The read's formerly sorted fields are compared as string multisets, so inconsistent ordering supplies no new proof and does not hide real value mismatches or contradict an independent view's valid write confirmation. Retired views keep a comparison-only representation to check later reads for value contradictions; they never regain serving authority or confirm writes through those checks. Sort and direction parameters share one vocabulary and are never learned as inert. A create placed by disagreeing sorts or a random id falls back to last.
  • Bulk writes. A write at a type's collection whose body names entities by id, as an id array or objects carrying the id field, deletes or updates each one, all or none; serving refuses it unless every id is held. Body ids are taken as written, and a call naming one entity twice is refused. Each entity's generated values come from that entity, sharing the call's clock and counters. A delete requires the members its exemplar sent besides the ids, with their recorded values. A recorded response is served when it names none of the ids and counts nothing, or is an array with one element per entity the body names, in the body's order, each that entity with the first element's members or its id spelled as the first spells its own; serving renders each element from the entity the call wrote, and any other response naming the ids refuses the tool. Comparison translates a bulk body's ids with the mapping of the collection the write targets.
  • Item actions. A write to an item path plus a literal segment that names no collection, whose calls answer nothing or the entity (never a 201, and with every field its item read always shows), updates that entity; a scalar it changes without sending is set on every call. It is served only once a call found every field it sets already at that value, so a toggle is never served. An action never takes a read another write to the entity still needs.
  • Per-response members. A top-level member whose value is a UUID or a clock is minted per response on a path when it differs on every repeat of that path's successful GET lines with no write between, and across the capture it differed on at least two repeats and never stayed the same. A shared view leaves it out only when every concrete path it answers independently proves it minted; a collection's proof never transfers to its item paths. Item envelopes and the write gate's item, list and document reads compare without these common proved members. The view names it in per_response_members, and serving keeps the recorded value.
  • Nested counters. An item total in a top-level object beside the list is a counter on the same evidence as a top-level one, rendered from the state through its pointer. A top-level member whose name starts with / is named by its pointer (/~1total).

Acceptance

  • Over the 50 published captures, served write tools rise and none is lost except where the evidence cannot show the write's own effect; write tools with no state target fall.
  • Read calls that state serving answers keep matching their recordings, before the first write and across sessions.
  • Every newly served write answers a call other than the recorded one correctly, or refuses it.

Not included

  • Soft deletes, appended relation rows, rendered markup and reverse embeds; creates completed from later reads.
  • Templating literal ids by value (an opt-in rule served two deletes whose targets came from unserved writes wrongly).
  • Identity aliases and resource names; per-response values that differ as instants.
  • A replayed write leaves the state as it was, so a served write on an entity only a replayed write made refuses.