fix(environment): check reads on the lines serving answers, and refuse plans later calls contradict

Greptile raised these on the list-lines branch against this branch's code.
- The gate chooses a read's line as serving does. A read with no query on a whole list falls back to a checked line that only pages it, and a line whose query repeats a name, which no call can name, is never checked.
- A read made while a modelled write to its targets was still running, starting before that write ended, checks nothing either way.
- A later call that stores a member otherwise than the plan (echoed by one call and stored as something else by another, or ruled differently) refuses the plan as unexplained, rather than keeping the first rule.
- A create keeps the spelling of a collection the capture listed empty: checked list lines of the type join the known spellings, so an encoded argument is not encoded twice.
- containsDelimited walks children one at a time, since spreading a very wide array into push overflowed the stack.
- The guide counts the confirmed write tools among the served catalog and the verifier's expected count.

Attribution bookkeeping moves to environmentStateWriteAttribution.ts and the create's collection choice to environmentStateCollections.ts, keeping files under the line limit. The verdicts over the 50 captures are unchanged plan by plan, and the smoke applies all 31 served write tools.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Edgar ArakelyanandClaude Opus 5.5 committed 2026-10-21 08:22:31 -07:00
1 parent 73c0e9b50a
commit 9f513d07e5
14 files changed
+515 -258

No files matched your search

@@ -27,12 +27,16 @@ import {
withBodies,
writtenFieldChecks,
SimulatedState,
WriteEffects,
type CheckOutcome,
type CheckedLines,
type TouchedTargets,
type ServedLine,
} from "./environmentStateWriteChecks.js";
import {
touchTarget,
WriteEffects,
type PendingTargets,
type TouchedTargets,
} from "./environmentStateWriteAttribution.js";
import { applyCreate } from "./environmentStateWriteCreate.js";
import {
applyDelete,
@@ -91,8 +95,8 @@ export class EnvironmentStateWriteSimulation {
readonly #touched: TouchedTargets = new Map();
readonly #effects = new WriteEffects();
readonly #lastCheck = new Map<string, number>();
/** The earliest modelled write to each target since its last check. */
readonly #touchedSince = new Map<string, number>();
/** The earliest modelled write to each target since its last check, and when the latest of them ended. */
readonly #touchedSince: PendingTargets = new Map();
/** Timeline indexes of writes the gate does not model. */
readonly #unmodelledAt: number[] = [];
readonly #log: {
@@ -207,6 +211,7 @@ export class EnvironmentStateWriteSimulation {
index,
tool: tool.name,
path: record.path,
end: record.end,
});
}
@@ -217,6 +222,7 @@ export class EnvironmentStateWriteSimulation {
readonly index: number;
readonly tool: string;
readonly path: string;
readonly end: number;
},
): void {
if (result.kind === "unknown") {
@@ -239,17 +245,10 @@ export class EnvironmentStateWriteSimulation {
if (target === result.targets[0]) this.#touched.delete(target);
continue;
}
if (!this.#touchedSince.has(target))
this.#touchedSince.set(target, write.index);
const tools = this.#touched.get(target) ?? new Map();
const before = tools.get(write.tool);
tools.set(
write.tool,
result.fields === undefined || before === null
? null
: [...new Set([...(before ?? []), ...result.fields])],
);
this.#touched.set(target, tools);
touchTarget(this.#touched, this.#touchedSince, target, {
...write,
fields: result.fields,
});
}
if (result.kind !== "applied") return;
this.#log.push(write);
@@ -357,7 +356,10 @@ export class EnvironmentStateWriteSimulation {
const outcome = this.#check(view, record);
if (outcome === undefined) return;
const keys = [...outcome.targets, ...outcome.unheld];
if (keys.some((key) => this.#taint.tainted(key))) return;
// A read made while a write to its targets was still running may show either state, so it checks nothing.
const running = (key: string) =>
(this.#touchedSince.get(key)?.until ?? -Infinity) > record.start;
if (keys.some((key) => this.#taint.tainted(key) || running(key))) return;
const exercised = new Set<string>();
for (const key of keys)
for (const name of this.#touched.get(key)?.keys() ?? [])
@@ -392,7 +394,7 @@ export class EnvironmentStateWriteSimulation {
/** Whether a write the gate does not model came after the earliest modelled write a failed check exercised. */
#interfered(keys: readonly string[]): boolean {
const since = Math.min(
...keys.map((key) => this.#touchedSince.get(key) ?? Infinity),
...keys.map((key) => this.#touchedSince.get(key)?.since ?? Infinity),
);
return this.#unmodelledAt.some((index) => index > since);
}