fix(environment): make the write gate and serving agree on lines, rules, inserts and documents

Five places where the gate approved a write serving would answer
differently:
- list and document reads are matched to checked lines as serving
  matches calls (path and query values decoded, the query in any
  order), so a reordered query no longer hides a contradiction;
- an increment or slug applies to every call, so a plan is
  unexplained-change when one call changes such a field and another
  leaves it as it was, in either order;
- an item read confirming a create no longer stops a later list from
  choosing another insert candidate; only a list check fixes the rule;
- the gate runs serving's document body check (accepted members, JSON
  types from the held and initial document), so a recorded write that
  serving would refuse leaves the plan unserved;
- a served create decodes its path values from the collection the call
  names, as the gate decoded the recorded path, whichever spelling the
  call used.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-20 14:12:34 -07:00
1 parent df2bded16e
commit 2086da6cc7
11 files changed
+343 -52

No files matched your search

@@ -18,9 +18,11 @@ import {
checkDocumentRead,
checkItemRead,
checkListRead,
checkedLines,
collectionTarget,
entityTarget,
isAbsence,
requestLine,
succeeded,
withBodies,
writtenFieldChecks,
@@ -60,9 +62,6 @@ export interface WriteSimulationInput {
readonly io: WriteIo;
}
const requestLine = (record: StateRecordFacts): string =>
record.path + (record.query ?? "");
const APPLY = {
create: applyCreate,
update: applyUpdate,
@@ -82,7 +81,7 @@ export class EnvironmentStateWriteSimulation {
readonly #toolOf = new Map<number, StateToolFacts>();
readonly #types = new Map<string, StateType>();
/** Each list or document view's checked request lines, with the recording a call of the line is answered on. */
readonly #checkedLines = new Map<string, ReadonlyMap<string, number>>();
readonly #checkedLines: ReadonlyMap<string, ReadonlyMap<string, number>>;
readonly #observations = new Set<number>();
readonly #taint: WriteTaint;
/** Modelled writes per target since its last check, with the fields each wrote (null: all). */
@@ -99,6 +98,8 @@ export class EnvironmentStateWriteSimulation {
}[] = [];
/** Entities a create inserted, by target, with the create's tool. */
readonly #inserted = new Map<string, string>();
/** Creates whose insert rule a passing list check relied on, so it is no longer changed. */
readonly #placed = new Set<string>();
constructor(input: WriteSimulationInput) {
this.#input = input;
@@ -108,22 +109,7 @@ export class EnvironmentStateWriteSimulation {
for (const tool of input.tools)
for (const ordinal of tool.record_ordinals)
this.#toolOf.set(ordinal, tool);
const byOrdinal = new Map(
input.records.map((record) => [record.ordinal, record]),
);
for (const view of input.views.values())
if (view.shape !== "recorded" && view.shape !== "item")
this.#checkedLines.set(
view.tool,
new Map(
view.checked_record_ordinals.flatMap((ordinal) => {
const record = byOrdinal.get(ordinal);
return record === undefined
? []
: [[requestLine(record), ordinal] as const];
}),
),
);
this.#checkedLines = checkedLines(input);
this.#taint = new WriteTaint(
[...this.#types.keys()],
new Map(
@@ -156,6 +142,7 @@ export class EnvironmentStateWriteSimulation {
contradictedBy: undefined,
sideEffect: false,
calls: 0,
updates: [],
});
}
this.#input.records.forEach((record, index) => this.#step(record, index));
@@ -188,6 +175,8 @@ export class EnvironmentStateWriteSimulation {
observed: (ordinal) => this.#observations.add(ordinal),
unknown: (type, collection, id) =>
this.#taint.unknown(type, collection, id),
initialDocument: (tool) =>
this.#input.documents.find((document) => document.tool === tool)?.value,
};
}
@@ -363,8 +352,10 @@ export class EnvironmentStateWriteSimulation {
outcome.pass ||
(view.shape === "list" && this.#retryInsert(view, record, exercised));
if (pass)
for (const name of exercised)
for (const name of exercised) {
this.#drafts.get(name)!.confirmed.push(record.ordinal);
if (view.shape === "list") this.#placed.add(name);
}
else if (this.#interfered(keys)) {
// An unmodelled write since these writes may explain the difference: check their own members only.
if (view.shape === "item") this.#checkMembers(view, record);
@@ -488,7 +479,8 @@ export class EnvironmentStateWriteSimulation {
if (creates.size !== 1) return false;
const [name] = creates;
const draft = this.#drafts.get(name!)!;
if (!exercised.has(name!) || draft.confirmed.length > 0) return false;
// An item read confirms the entity, not where a list places it.
if (!exercised.has(name!) || this.#placed.has(name!)) return false;
for (const candidate of draft.candidates.slice(1)) {
for (const entity of inserted) this.#state.remove(entity);
for (const entity of inserted) {