mirror of
https://github.com/morluto/rea.git
synced 2026-11-03 13:26:10 +00:00
fix(environment): count no write for a collection once a read shows its entity gone
A whole list's read checks the collection, so it exercised every write that changed it since the last check. A create whose entity an unmodelled delete removed, and a 404 then showed gone, was still among them, so a whole list taken afterwards confirmed a create it cannot show. The absent entity now leaves every write's claim on its collection, as a modelled delete's replacement does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
8a374b6b56
commit
40800631d2
8 files changed
+77
-32
No files matched your search
@@ -6,5 +6,7 @@
|
||||
2. **Counters.** Count `number`-of-items members as item totals.
|
||||
3. **Consumers.** One helper says whether a view lists all on a line; serving, the write gate's list checks and the
|
||||
insert retry use it, and an argument-less call may answer on any such line.
|
||||
4. **Tests and docs.** Domain and serving tests as the acceptance lists; the membership row of the environment-state
|
||||
4. **Absences.** When a read shows an entity absent, drop it from every write's claim on its collection, so a write
|
||||
left with none no longer counts there.
|
||||
5. **Tests and docs.** Domain and serving tests as the acceptance lists; the membership row of the environment-state
|
||||
docs and the list answer row of the agent-environment docs.
|
||||
@@ -18,13 +18,19 @@ against the whole collection, and a create's insert rule may be retried on it, a
|
||||
their recorded members. Without such a line, or when every line is one, the view is unchanged and the member is absent,
|
||||
so existing states keep their values.
|
||||
|
||||
A whole line's read checks the collection, so it exercises every write that changed the collection since it was last
|
||||
read. A write whose entity a read has since shown absent, for example a create an unmodelled delete removed and a 404
|
||||
then showed gone, no longer counts there, as when a modelled delete replaces it. Otherwise a whole list taken after the
|
||||
entity was gone would confirm a create it cannot show.
|
||||
|
||||
`numberOfElements` and similar number-of-items members (`number` with `elements`, `entries`, `items` or `results`)
|
||||
count the listed entities, so a whole line's counter follows the state as `totalElements` does.
|
||||
|
||||
## Acceptance
|
||||
|
||||
- Domain: a tool with a whole line and a filtered line names the whole one; a create the whole line later lists is
|
||||
confirmed; a tool whose lines are all whole, or none, names none.
|
||||
confirmed; a tool whose lines are all whole, or none, names none; a whole list taken after a read showed the created
|
||||
entity gone confirms no create.
|
||||
- Serving: after a served create, the whole line lists the created entity with its counters, and the filtered line
|
||||
keeps its recorded members.
|
||||
- Measured: the six published environments compared with the application before and after, on the probes of the
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
- [x] Judge whole-collection membership per checked line.
|
||||
- [x] Count number-of-items members.
|
||||
- [x] Serve, check and place creates per line.
|
||||
- [x] Drop a write from its collection once a read shows its entity absent.
|
||||
- [x] Add domain and serving tests.
|
||||
- [x] Document per-line membership.
|
||||
- [x] Measure the six published environments before and after.
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
# Verification
|
||||
|
||||
Verified commit: 87e94d12 (this branch before this file). Executed on macOS with Node v22.23.1 (the repository pins 24.18.0 in `.nvmrc`, which was
|
||||
not installed); the probes were recorded earlier against the live applications on six disposable Linux VMs.
|
||||
Verified commit: 8a374b6b (this branch with the write-plan branch's head `dc5e2aad` merged in). Executed on macOS with
|
||||
Node v22.23.1 (the repository pins 24.18.0 in `.nvmrc`, which was not installed); the probes were recorded earlier
|
||||
against the live applications on six disposable Linux VMs.
|
||||
|
||||
## Gates
|
||||
|
||||
- `npm run build:cached`, `npm run check:fast` (typecheck and lint), `npm run format:check`, `npx knip`,
|
||||
`npm run scan:todos`, `npm run docs:check`, `npm run verify:test-discovery` and `npm run mcp-catalog:check`: passed.
|
||||
- `npm run jscpd`: 3.46% duplicated lines over 3601 files, under the 3.5% gate.
|
||||
- `npx vitest run --project domain --project services`: 7479 passed, 5 pre-existing skips.
|
||||
- `npm run jscpd`: 3.45% duplicated lines over 3646 files, under the 3.5% gate.
|
||||
- `npx vitest run --project domain --project services`: 7615 passed, 5 pre-existing skips.
|
||||
- `npm run test:prepare`, then `npm run test:focused --` the environment serve and export CLI boundaries, the serve
|
||||
and served-writes MCP boundaries, the analysis-view CLI and MCP boundaries, the compact input schema boundary and
|
||||
tool schema validity: 8 files, 78 passed.
|
||||
@@ -18,37 +19,39 @@ one, with `numberOfElements` and `totalElements` as counters, and a create the w
|
||||
whose lines are all whole, or none, names none) and one serving test in `environmentStateWriteAnswer` (after a served
|
||||
create, the whole line lists the created book with its counters, and the filtered line keeps its recorded member).
|
||||
|
||||
## Six published environments before and after
|
||||
## Fifty published captures
|
||||
|
||||
The six published environments of the fidelity comparison (NetBox, Komga, Mealie, n8n, Shlink, self-hosted Sentry),
|
||||
each built from its original capture, were compared with `environment-compare` before and after this change. The
|
||||
probes were recorded against each application restored from one snapshot: the fidelity comparison's capture re-run,
|
||||
create/read/update/delete scenario and read of every seeded entity, and held-out sessions recorded later with values
|
||||
no capture sent (two creates, updates and deletes of created and seeded entities, and the reads and lists after them).
|
||||
Both builds merged the environment-compare and prefix-variance branches, which the comparison and the manifests need.
|
||||
The environment-state view ran over the 50 published captures with each manifest's template rules, on builds with and
|
||||
without this change. Both merged the prefix-variance branch, which some manifests need.
|
||||
|
||||
- 47 list views in 20 environments gain a whole line, among them Komga's books, series, collections and read lists,
|
||||
BookStack's books, chapters and pages, Mealie's cookbooks, shopping lists, categories and tags, WordPress's posts and
|
||||
comments, Vikunja's labels and Paperless's groups. On that line a call now lists the state's entities, including
|
||||
those a task loads with `data/add` and those served writes create or delete, where it listed only the recorded
|
||||
members.
|
||||
- Served write tools go from 31 to 33: Komga's collection create, which a whole list after the create confirms, and
|
||||
Airbyte's tag delete. Four of the gained lists belong to a type with a served write (Airbyte tags, BookStack books,
|
||||
Komga collections, Linkding tags).
|
||||
- Komga's list views also count `numberOfElements`. No other view or write plan changed.
|
||||
|
||||
## Six environments against their applications
|
||||
|
||||
The six environments of the fidelity comparison (NetBox, Komga, Mealie, n8n, Shlink, self-hosted Sentry) were compared
|
||||
with their applications by `environment-compare` with and without this change. The probes were the comparison's capture
|
||||
re-run, create/read/update/delete scenario and read of every seeded entity, and held-out sessions recorded later with
|
||||
values no capture sent. Both builds merged the environment-compare and prefix-variance branches.
|
||||
|
||||
| Environment | Steps | Before | After | Ignoring order, before | After |
|
||||
| ----------- | ----: | -----: | ----: | ---------------------: | ----: |
|
||||
| NetBox | 479 | 98.5% | 98.5% | 98.5% | 98.5% |
|
||||
| Komga | 600 | 94.7% | 95.2% | 94.7% | 95.5% |
|
||||
| Komga | 600 | 91.5% | 91.5% | 91.5% | 91.5% |
|
||||
| Mealie | 449 | 94.0% | 94.0% | 94.9% | 94.9% |
|
||||
| n8n | 278 | 71.6% | 71.6% | 84.2% | 84.2% |
|
||||
| Shlink | 202 | 87.1% | 87.1% | 90.1% | 90.1% |
|
||||
| Sentry | 372 | 24.5% | 24.5% | 47.6% | 47.6% |
|
||||
|
||||
Komga is the one environment whose list tools have both kinds of line: `/api/v1/collections` lists every collection,
|
||||
`?library_id=…` filters them. After the change the whole line lists created collections with `totalElements` and
|
||||
`numberOfElements` following, which three list reads after creates now agree on; in two more the created collection
|
||||
is listed but placed where the insert rule puts it, not where Komga sorts it (`order`). No step of any environment
|
||||
agreed before and differs after.
|
||||
|
||||
The other environments' lists did not meet the per-line conditions or did not need them:
|
||||
|
||||
- NetBox's sites list was already `all`.
|
||||
- Mealie's recipe lines all carry ordering or search parameters.
|
||||
- Shlink's short-URL list replays (`query-varies`).
|
||||
- n8n's workflow list is `list-differs`.
|
||||
|
||||
Their remaining differences are elsewhere: sets the application returns in varying order, writes the capture never
|
||||
confirmed, preconditions such as n8n's archive-before-delete, and, for Sentry, values the capture's preparation
|
||||
replaced before inspection.
|
||||
No step changed. This Komga environment's capture creates, updates and deletes a collection before any whole list, so
|
||||
with the write-plan branch's head its create and update are unconfirmed and serve nothing a list could show. Before
|
||||
that head (at `f2b95351`), when a list taken after the delete still confirmed them, the same comparison moved Komga
|
||||
from 94.7% to 95.2%: three list reads after creates agreed and two more differed only in where the created collection
|
||||
was placed. None of the other five environments has a list tool with both kinds of line and a served write on its type.
|
||||
@@ -634,8 +634,9 @@ with its recording as the read plans are, as the whole response it would serve:
|
||||
the entity or list rendered into the envelope of the item view's or the line's
|
||||
base recording, with the members the state still holds. A 400, 404 or 410 (or a
|
||||
`null` body) agrees with an entity the copy does not hold. A match confirms
|
||||
the writes that changed its entity or collection since it was last read; a
|
||||
mismatch contradicts them, or, when an unmodelled write came after them, is
|
||||
the writes that changed its entity or collection since it was last read, less
|
||||
those whose entity a read since showed absent, which no longer count for the
|
||||
collection; a mismatch contradicts them, or, when an unmodelled write came after them, is
|
||||
checked only on the members they sent; a mismatch no write explains marks the related writes since as
|
||||
`side-effect`. A plan is `served` when a read confirmed it and none
|
||||
contradicted it; otherwise its `reason` is the first of its build reason
|
||||
|
||||
@@ -271,6 +271,11 @@ export class EnvironmentStateWriteSimulation {
|
||||
this.#touched.delete(target);
|
||||
if (isAbsence(record.status)) {
|
||||
this.#state.remove(entity);
|
||||
this.#effects.removed(
|
||||
this.#touched,
|
||||
target,
|
||||
collectionTarget(entity.type, entity.collection),
|
||||
);
|
||||
return;
|
||||
}
|
||||
const object = this.#input.io.withBody(record.ordinal, (body) =>
|
||||
|
||||
@@ -177,6 +177,18 @@ export class WriteEffects {
|
||||
this.#members.delete(key);
|
||||
}
|
||||
|
||||
/** A read showed the entity absent: no write's effect on it remains, so a write left with no entity drops from the collection. */
|
||||
removed(touched: TouchedTargets, entity: string, collection: string): void {
|
||||
this.#changed.delete(entity);
|
||||
const byTool = this.#members.get(collection);
|
||||
if (byTool === undefined) return;
|
||||
for (const [tool, entities] of byTool) {
|
||||
if (!entities.delete(entity) || entities.size > 0) continue;
|
||||
byTool.delete(tool);
|
||||
touched.get(collection)?.delete(tool);
|
||||
}
|
||||
}
|
||||
|
||||
supersede(
|
||||
touched: TouchedTargets,
|
||||
writer: string,
|
||||
|
||||
@@ -513,6 +513,21 @@ describe("list lines", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("confirms no create by a whole list taken after a read showed its entity gone", () => {
|
||||
const rows: TimedCaptureRow[] = [
|
||||
...BOOK_INIT_ROWS,
|
||||
["POST", "/api/books", THIRD_BOOK, 201, { name: "Third Book" }],
|
||||
// A redirected delete is not modelled; the read after it shows the book gone.
|
||||
["DELETE", "/api/books/3", "", 302],
|
||||
["GET", "/api/books/3", BOOK_NOT_FOUND, 404],
|
||||
["GET", "/api/books", [FIRST_BOOK, SECOND_BOOK]],
|
||||
];
|
||||
expect(planOf(stateOf(rows), "post_books")).toMatchObject({
|
||||
served: false,
|
||||
reason: "unconfirmed",
|
||||
});
|
||||
});
|
||||
|
||||
it("names no line when every line lists its whole collection, or none does", () => {
|
||||
const listOf = (rows: readonly TimedCaptureRow[]) =>
|
||||
stateOf(rows).views.find((view) => view.tool === "get_books");
|
||||
|
||||
Reference in new issue
Block a user