mirror of
https://github.com/morluto/rea.git
synced 2026-10-28 10:25:42 +00:00
Serve write tools whose later reads confirm the effect, and keep main's state-file ownership and part-wise reply measurement. Squashed rebase of #5600 onto the rebased #5352 tip.
407 lines
21 KiB
Markdown
407 lines
21 KiB
Markdown
# Apply write effects to the served state
|
||
|
||
Issue: https://github.com/morluto/rea/issues/4933
|
||
|
||
## Outcome
|
||
|
||
An environment served from its state also serves the write tools whose effects
|
||
the capture itself confirms:
|
||
|
||
- a create adds an entity;
|
||
- an update changes the fields the call sends;
|
||
- a delete removes the entity;
|
||
- a document write changes a document.
|
||
|
||
Later reads, list membership and `data/get` show the change, and `data/reset`
|
||
undoes it.
|
||
|
||
The `environment-state` view decides which write tools are served. It replays
|
||
the capture's own timeline on the extracted initial state: each recorded write
|
||
applies its modelled effect, and each later read that the state answers is
|
||
rendered and compared with the recorded response. A write tool is served only
|
||
when at least one later read reproduces after its effect and none contradicts
|
||
it. Every other write tool stays unserved, listed with the reason.
|
||
|
||
Values the application generates are deterministic: identities, sequences and
|
||
clock values follow the formats the capture recorded, so the same calls after
|
||
a reset give the same state.
|
||
|
||
## Write plans
|
||
|
||
The view adds two members to its result:
|
||
|
||
- `writes`: one plan per manifest tool whose method may change state
|
||
(`changesState`);
|
||
- `write_clock`: the instant the served clock starts from.
|
||
|
||
**Effects.** Each plan classifies its tool:
|
||
|
||
| Effect | Tool | Target |
|
||
| ---------- | --------------------------------------------------------------------- | ------------------------- |
|
||
| `create` | `POST` to a state type's `collection_template` | an entity of that type |
|
||
| `update` | `PUT`, `PATCH` or `POST` to that template plus one trailing parameter | the entity the path names |
|
||
| `delete` | `DELETE` to that same item template | the entity the path names |
|
||
| `document` | `PUT`, `PATCH` or `POST` to a document view's path | that document |
|
||
| `none` | anything else | reason `no-state-target` |
|
||
|
||
**Exemplar.** A plan is built from its first successful (2xx) recorded call,
|
||
the exemplar.
|
||
|
||
- **Request body.** The exemplar's request body must be a JSON object that
|
||
could enter the state: a JSON-like media type, complete UTF-8 text within
|
||
256 KiB, exact numbers, no `__proto__` member. Otherwise the reason is
|
||
`body-unusable`. A `DELETE` takes no body.
|
||
- **The written entity.** The entity is observed in the exemplar's response,
|
||
located by its identity value at the root, in one member, or in a
|
||
one-element array, as item views locate it. When the response does not carry
|
||
it, the entity is observed in the first later read that shows it: an item
|
||
read of that entity, or for a create, the one entity a collection read newly
|
||
lists. A create whose response names its id alone (a scalar, the identity
|
||
field, or one member) is observed only through a read of that id. With
|
||
neither, the reason is `no-observation`.
|
||
|
||
**Field rules for creates.** Each field of the observed entity gets the first
|
||
rule that explains its value. Rules that read the call come first, so an
|
||
identity the client sends is an echo:
|
||
|
||
1. **Echo.** A body member of the same name with an equal value.
|
||
2. **Copy.** A body string of 8 or more characters, found at a JSON pointer of
|
||
the body (the shallowest match).
|
||
3. **Path.** The value of a path parameter of the call, percent-decoded.
|
||
Serving decodes it from the collection the call names, whichever spelling
|
||
the call uses.
|
||
4. **Slug.** The slug of a body string member: lower case, runs of other
|
||
characters joined by `-`.
|
||
5. **Identity.** The type's `id_field` is generated. A sequence when it is an
|
||
integer (or a string of digits); otherwise a UUID when it has the
|
||
`8-4-4-4-12` hex shape; otherwise a token of the same length and character
|
||
classes.
|
||
6. **Clock.** A timestamp within 10 minutes of the exemplar's start time. The
|
||
format is kept: ISO-8601 with its separator, fraction digits and zone, or
|
||
epoch seconds or milliseconds.
|
||
7. **Sequence.** An integer one greater than the largest value of the field in
|
||
the collection, else in the type.
|
||
8. **Unique.** A UUID-shaped string that no other entity of the type shares.
|
||
9. **Template.** A string containing a generated value as a delimited token
|
||
(for example a URL that ends in the new id). It is rebuilt around the new
|
||
value.
|
||
10. **Constant.** Otherwise the exemplar's value.
|
||
|
||
When a constant, or the text a template keeps, contains a path value of the
|
||
exemplar's call as a delimited token, the create is served only for calls
|
||
naming the exemplar's collection.
|
||
|
||
**Body members of a create.** A member is either accepted or not:
|
||
|
||
- **Accepted.** An echoed member, or the identity field's value. Every member
|
||
the exemplar sent is required.
|
||
- **Not accepted.** A member the application stored with a different value
|
||
(transformed), or one it did not show. A call that sends such a member is a
|
||
tool error naming the accepted members.
|
||
|
||
**Updates.** Body members follow the same accepted and not-accepted rules,
|
||
except that a member the application stored differently is accepted only with
|
||
the value a recorded call sent it with, and then stores the value that call
|
||
recorded (its `constant` rule carries it). The type's identity field never
|
||
takes a rule: sent as the path's id it is the identity member, sent as another
|
||
value the application ignored it is accepted with that value and stores
|
||
nothing, and a stored id that changed is `unexplained-change`. A plan whose
|
||
calls use a member both as the identity and as a fixed member, or fix it at
|
||
different values, is `unexplained-change`, as serving could not take every
|
||
recorded call. So is an update a later read shows dropped a field the
|
||
entity held, since serving keeps every field an update does not write; a write's
|
||
own response may leave fields out.
|
||
`PUT` requires every member the exemplar sent; `PATCH` and `POST` require at
|
||
least one. A field the exemplar's update changed without sending it is
|
||
explained as one of:
|
||
|
||
- clock;
|
||
- an increment by one;
|
||
- a slug of an echoed member.
|
||
|
||
Any other change is `unexplained-change`. An increment or a slug applies to
|
||
every call that could take it, so the plan is also `unexplained-change` when
|
||
such a rule would give a call of the tool another value than the one it
|
||
recorded, or when a later call treats a member otherwise than the plan: one
|
||
call echoes it and another stores something else, or the two rule it
|
||
differently.
|
||
|
||
**Deletes.** A delete removes the entity. Its response is the exemplar's
|
||
response, with the entity re-rendered when the response carries it.
|
||
|
||
**Documents.** A document write merges the body's members (replaces them, for
|
||
`PUT`) into the document object that holds most of them: the root or one
|
||
member. Later calls write at the exemplar's object. Document reads answer only on their recorded line, so a write is
|
||
served only at the path the state holds the document at, and the gate treats a
|
||
recorded write at another path as an unmodelled write. A recorded write whose
|
||
body serving would refuse (a member of another JSON type than the document
|
||
holds) is `unexplained-change`.
|
||
|
||
**Responses.** A write answers with the status and content type of one
|
||
recorded response:
|
||
|
||
| Response | Body |
|
||
| ---------- | --------------------------------------------------------------------------------------------------------- |
|
||
| `entity` | the written entity, rendered into the recorded envelope at the plan's pointer through the recorded fields |
|
||
| `identity` | the recorded body with the new identity at the plan's pointer |
|
||
| `empty` | no body |
|
||
| `recorded` | the recorded body |
|
||
|
||
A rendered body missing a field the recorded response carried is a tool error,
|
||
and the state is left unchanged.
|
||
|
||
**List position.** A create's `insert` rule places the new entity in its
|
||
collection's order: sorted by the identity field or a clock field (ascending
|
||
or descending), `first` or `last`. The rule is the first candidate consistent
|
||
with both the initial `all`-membership recordings and the position a later
|
||
recorded list shows. With no evidence, the rule is `last`.
|
||
|
||
## The gate
|
||
|
||
The view replays every primary-origin record in timeline order on a copy of
|
||
the initial state.
|
||
|
||
**Writes.** Only a 2xx write is modelled. A modelled write applies its effect, using the values it recorded
|
||
for generated fields and the stored values for fields it transformed. A
|
||
modelled write whose call the plan does not explain brings its entity to the
|
||
recorded outcome instead, without attribution. A later create whose generated
|
||
values take another form than serving gives them (a sequence value out of
|
||
order, a clock in another format, a non-UUID unique value) makes the plan
|
||
`unexplained-change`; integer identities are left to the id mapping. An update or
|
||
delete names its entity as an item read does (below).
|
||
|
||
Any other write that may have changed state, including a redirected or
|
||
unanswered one, is unmodelled, and what it may have changed
|
||
depends on where its path lies relative to each type's collection paths
|
||
(segment-prefix related, parameters matching any segment):
|
||
|
||
- **Below an item path:** the named entity is unknown until a read shows it
|
||
again. When the path names no held entity, every entity of that collection
|
||
is unknown.
|
||
- **At a collection path:** that collection is not checked again.
|
||
- **Above collection paths:** everything under the path is not checked again.
|
||
|
||
An item read of an unknown entity shows it again: the copy takes its recorded
|
||
values. Writes that changed it since its last check are checked only on the
|
||
members they sent.
|
||
|
||
**Reads.** A read is checked when the served environment would answer it
|
||
from the state:
|
||
|
||
- an item read of a state-backed item view, of the entity serving would name:
|
||
in a held collection the read's path matches, each segment verbatim or once
|
||
percent-decoded and verbatim spellings first, the id percent-decoded once,
|
||
then as written;
|
||
- a list or document read on one of its view's checked request lines,
|
||
chosen as replay chooses the line for the read's call: each path parameter
|
||
the read's value verbatim or once percent-decoded, preferring verbatim
|
||
spellings, then recorded order, and the query values decoded, in any order.
|
||
A read with no query on a whole list falls back, as serving does, on a
|
||
checked line that only pages it. A line whose query repeats a name with
|
||
different values is never chosen, since no call's arguments can name it; one
|
||
repeating a name with one value is, as a call naming it matches both. A list
|
||
is compared as served from the collection of that line. A read on a page
|
||
(`limit`, `size`, `per_page` and the like, or a `_start`–`_end` range; a size
|
||
of zero is no bound on a line whose recording listed items under it, while its
|
||
other sizes and range still bound the page) of a
|
||
whole list checks nothing once the collection holds more entities than the
|
||
page, which serving then answers from its recording.
|
||
- A read made while a modelled write to its entity or collection was still
|
||
running, starting before that write ended, checks nothing: it may show the
|
||
state before or after the write. Nor does such a read show an unknown entity
|
||
again.
|
||
- A read a write's effect was learned from checks nothing of that write, but
|
||
still contradicts the other writes it exercises.
|
||
|
||
**Comparison.** The read's answer is the full response serving would give from
|
||
the copy: an item read's entity rendered into its item view's base recording,
|
||
or a list read's members as serving would list them (so a member the copy no
|
||
longer holds fails the check) rendered into its line's base recording. It is
|
||
compared with the recording, as the read gate compares, with volatile fields taking the
|
||
observed values. A recorded 400, 404 or 410, or a 2xx with a JSON `null`
|
||
body, for an entity the copy does not hold agrees with it. A successful read
|
||
whose usable body no longer holds the entity or the list fails, as does one of
|
||
an entity lacking a field every recording of its view carried, which serving
|
||
refuses to answer with.
|
||
|
||
**Attribution.**
|
||
|
||
- A check exercises the write tools that changed its entity or collection
|
||
since the previous check of it, less those whose effect later writes erased.
|
||
An update's effect is the fields it sent or changed, derived ones included.
|
||
A later update overwrites the fields it sets (an increment adds to its field
|
||
instead), a recorded outcome every field, and a create or delete the whole
|
||
entity. A document write's effect is the members it merged at its
|
||
object, or everything under the object it replaces, the whole document at the
|
||
root; an entity's fields match by name only, whatever their names look like. A write with nothing left on an entity no
|
||
longer counts there, nor in a collection where no entity it changed keeps any
|
||
of its effect. A mismatch contradicts them. A pass confirms those it shows
|
||
something of: a create's or delete's membership, or a changed field the
|
||
read's view shows and does not treat as volatile; a read that shows none of
|
||
an update's changes confirms nothing of it, and leaves it for a later check
|
||
of that entity or collection.
|
||
- A mismatch contradicts each exercised write that no unmodelled write came
|
||
after its latest call on any of the read's entities or collections where its
|
||
effect survives, whatever earlier write is still pending. An item read checks the writes an unmodelled
|
||
write did follow only on the members they sent, and a list with such a write
|
||
is not checked again: the unmodelled write may explain the rest.
|
||
- A mismatch on an entity or collection that no write changed contradicts,
|
||
as `side-effect`, the modelled writes since that previous check whose paths
|
||
are segment-prefix related to the read's path.
|
||
- After a failed item check, the copy takes the read's recorded values (or
|
||
drops the entity on a recorded absence). A list whose check failed is not
|
||
checked again.
|
||
- A list that fails only for where one create placed its entity passes when
|
||
another of that create's insert candidates reproduces it, which then
|
||
becomes the plan's rule, unless an earlier list check relied on the rule
|
||
(an item read's confirmation does not fix it).
|
||
- The read that supplied a create's or update's observation does not check
|
||
it, but checks the other writes it exercises.
|
||
|
||
**Verdicts.** A plan is served when at least one check confirms it and none
|
||
contradicts it. Otherwise the reason, in this precedence, is:
|
||
|
||
1. the first build reason;
|
||
2. `contradicted`, with `contradicted_by` naming the first contradicting read;
|
||
3. `side-effect`;
|
||
4. `unconfirmed`;
|
||
5. `incomplete-entities`: an update or delete that answers with the entity,
|
||
when no initial entity holds every field its recorded answer shows (for
|
||
example a delete that answers with an edit view of the entity the reads
|
||
never showed), so it could answer only for entities the environment itself
|
||
created.
|
||
|
||
`checked_record_ordinals` lists the confirming reads.
|
||
|
||
## Serving
|
||
|
||
**Catalog.** In state mode, the catalog adds the write tools whose plan is
|
||
served. Their descriptions say the tool:
|
||
|
||
- applies its effect to the served state;
|
||
- names its target type;
|
||
- lists its required and accepted body members.
|
||
|
||
The discovery budget charges them. `--replay-only` and the replay fallback are
|
||
unchanged.
|
||
|
||
**Calls.** A served write answers synchronously against the live store and
|
||
changes it atomically, so every MCP session and `data/get` see one state.
|
||
|
||
**Refusals.** These are tool errors that leave the state unchanged:
|
||
|
||
- a body that is not an object;
|
||
- a missing required member;
|
||
- a member that is not accepted;
|
||
- a value of another JSON type than the exemplar's, for documents as well as
|
||
entities;
|
||
- an update or delete of an entity the state does not hold;
|
||
- a create in another collection when the plan is tied to its own;
|
||
- a document write at a path other than the one the state holds it at;
|
||
- a body member or a resulting value `data/add` would refuse (a number not
|
||
exact as `data/get` writes it, a `__proto__` member, nesting past the
|
||
limit), increments included, so `data/get` always loads back;
|
||
- a create, update or document write that would take what `data/get` reports
|
||
past the 32 MiB `data/add` intake limit, as a `resource_constraint` error;
|
||
- a rendered response missing a recorded field.
|
||
|
||
When the tool, or else an item view of the type, recorded a 400, 404 or 410
|
||
response, an update or delete of a missing entity answers with the latest such
|
||
recording instead.
|
||
|
||
A create in a collection held under more than one spelling (`acme1` and
|
||
`%61cme1`) goes to the one the call spells verbatim, as item calls choose their
|
||
entity; a collection keeps its spelling after its last entity is deleted. A
|
||
reset or load knows the spellings of the Evidence's state, of the state it
|
||
installs and of the type's checked list lines (so a collection the capture
|
||
listed empty keeps its spelling), never those of a state it replaced. When two
|
||
spellings fit a call equally, the one holding entities wins. `data/add` refuses a document that `data/get` would write back past the
|
||
32 MiB intake limit once it adds the members a document may omit, such as
|
||
`types` and the state Evidence id. A derived state `data/get` would write
|
||
past that limit is not served: the environment serves replay and reports the
|
||
state `unavailable`.
|
||
|
||
**Clock and generators.** The store's clock starts at `write_clock` and
|
||
advances one second before each served write; clock fields take its value in
|
||
their recorded format.
|
||
|
||
- **Sequences:** one more than the largest current value in their scope, the
|
||
largest the state started with at its last reset or load, and the largest
|
||
value they generated, so a deleted entity's value is not reused, as database
|
||
sequences do not reuse values.
|
||
- **UUIDs and tokens:** derived from a SHA-256 of the state Evidence id, the
|
||
tool, the field and a write counter, skipping values the type already holds.
|
||
|
||
`data/reset` and `data/add` restart the clock, the counter and the
|
||
sequences.
|
||
|
||
**Missing entities.** An item read of an entity the state does not hold
|
||
answers with the tool's latest recorded 400, 404 or 410 response, or 2xx JSON
|
||
`null` response, the absences the gate accepts, when it has one, labelled
|
||
`state.view: "missing"`. Otherwise it stays a tool error.
|
||
|
||
**Lists of a whole collection.** Written entities matter only if lists show
|
||
them, so the read gate's `membership: "all"` also covers a line that only pages
|
||
its list (`limit`, `offset`, `page`, `per_page`, `perpage`, `page_size`,
|
||
`pagesize`, `size`, `start`, `_start`, `_end`) when its recording listed the
|
||
whole collection. A list call with no query argument answers from such a line
|
||
of an `all` view of the collection its path arguments name, with
|
||
`replay.exact_match` false; a collection with no such line answers from its
|
||
recording. A call that asks for a page
|
||
still answers from its recording.
|
||
|
||
**Provenance.** Write answers carry `state: {source: "state", view: "create" |
|
||
"update" | "delete", type, collection, id}`, or `{source: "state", view:
|
||
"document-write", path}`.
|
||
|
||
**Data plane and export.**
|
||
|
||
- `data/get` reports written entities without record citations; they load
|
||
back through `data/add` unchanged.
|
||
- `environment-export` counts served write tools in the bundle's tool count
|
||
and README. Its check stays a read tool.
|
||
|
||
## Not modelled
|
||
|
||
These stay unserved, or are labelled in the description:
|
||
|
||
- side effects on counters, relations and copies of entities in other
|
||
collections;
|
||
- inputs the application transforms;
|
||
- members the capture never sent;
|
||
- reordering after an update;
|
||
- membership of filtered or paged lists, which answer their recorded members;
|
||
- write paths the manifest left literal;
|
||
- writes whose request body the capture did not retain;
|
||
- referential rules on delete: a delete removes the entity alone, so an
|
||
application that refuses to delete a referenced entity, or deletes its
|
||
dependents with it, is not modelled;
|
||
- authorization.
|
||
|
||
## Acceptance
|
||
|
||
- **Domain tests:** classification, every field rule, request-body admission,
|
||
observation from a later read, the insert rules, every verdict and reason,
|
||
attribution and resynchronisation, and taint by an unmodelled write.
|
||
- **Services tests:**
|
||
- store mutations and their atomicity;
|
||
- every refusal;
|
||
- generated values and their determinism across reset;
|
||
- `data/get` round trips of written entities;
|
||
- the recorded 404 for missing entities;
|
||
- the catalog and descriptions in each mode.
|
||
- **Boundary test:** a write tool changes the next read over MCP.
|
||
|
||
**Real captures.**
|
||
|
||
- The view runs over every showcase capture within its budget, and the
|
||
verification reports, per capture, the write tools served and the reasons
|
||
for the rest.
|
||
- One environment, chosen at random among those serving a write tool, is
|
||
exported, run in the AgentEnv framework, and graded from `data/get` after an
|
||
agent's create, update and delete.
|
||
- The served model is compared with that application running live on the same
|
||
calls.
|
||
|
||
No test reaches an external network.
|