Files
rea/src/domain/analysisView/environmentStateWriteTaint.ts
T
Edgar ArakelyanandClaude Opus 5.5 b9fbcd7057 feat(environment): apply the write effects a capture confirms to the served state
The environment-state view now builds a write plan for every tool whose
method may change state (create, update, delete or document, by the state
type or document its path names) from its first successful recorded call:
the request body members the application stored as sent, values it generated
(identities, sequences, UUIDs, clock values in their recorded format), values
derived from the call (copies, path values, slugs, strings around a generated
id) and the recorded response's shape. It replays the capture's timeline on a
copy of the initial state, applying each modelled write and checking every
later read the served state would answer; a plan is served only when a later
read confirms it and none contradicts it. Writes it does not model leave the
entities and collections below their paths unknown until a read shows them
again, and failures they may explain are checked only on the members a write
sent. Unserved plans carry their reason.

Served from the state, the catalog adds the confirmed write tools, described
by their effect and body members. A call applies its effect atomically to the
live store and answers with a recorded status and a rendered body; refused
calls leave the state unchanged. Generated values are deterministic per state
Evidence and write count, sequences never reuse a value, and the clock starts
at the capture's end: data/reset and data/add restart them. Reads of an
entity the state does not hold answer the recorded 400, 404 or 410, and a
list line that only pages its collection and listed all of it answers calls
without query arguments from the state.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 09:25:35 -07:00

104 lines
3.6 KiB
TypeScript

import {
isParam,
lastSegmentValue,
normalizedPath,
segmentsOf,
} from "./environmentStateRules.js";
import {
collectionTarget,
documentTarget,
entityTarget,
} from "./environmentStateWriteChecks.js";
import { prefixRelated } from "./environmentStateWriteRules.js";
/** Whether the state holds an entity, so a write below its item path names it. */
type Held = (type: string, collection: string, id: string) => boolean;
/**
* What writes the gate does not model leave unknown: an entity below whose item path one wrote,
* or every entity of a collection when the path names none it holds (until a read shows each
* again); a collection one wrote at; everything under a path one wrote above collections; and
* targets whose check failed.
*/
export class WriteTaint {
readonly #prefixes: string[] = [];
readonly #targets = new Set<string>();
readonly #entities = new Set<string>();
/** Collections whose entities are all unknown, with the ones a read has shown since. */
readonly #collections = new Map<string, Set<string>>();
constructor(
private readonly types: readonly string[],
private readonly documents: ReadonlyMap<string, string>,
private readonly held: Held,
) {}
/** Record an unmodelled write's path against every related type and document. */
write(path: string): void {
const segments = segmentsOf(path);
for (const type of this.types) {
if (!prefixRelated(type, path)) continue;
const depth = segmentsOf(type).length;
const collection = `/${segments.slice(0, depth).join("/")}`;
if (segments.length > depth) {
const id = lastSegmentValue(`/${segments[depth]!}`);
if (this.held(type, collection, id))
this.#entities.add(entityTarget(type, collection, id));
else
this.#collections.set(collectionTarget(type, collection), new Set());
} else if (segments.length === depth)
this.#targets.add(collectionTarget(type, collection));
else this.#prefixes.push(normalizedPath(path));
}
for (const [tool, documentPath] of this.documents)
if (prefixRelated(documentPath, path))
this.#targets.add(documentTarget(tool));
}
/** Stop checking a target whose check failed. */
fail(target: string): void {
this.#targets.add(target);
}
tainted(target: string): boolean {
return this.#targets.has(target);
}
/** Whether an unmodelled write above a collection path left everything under it unknown. */
under(collection: string): boolean {
const concrete = segmentsOf(collection);
return this.#prefixes.some((prefix) =>
segmentsOf(prefix).every(
(segment, index) => isParam(segment) || segment === concrete[index],
),
);
}
unknown(type: string, collection: string, id: string): boolean {
const target = entityTarget(type, collection, id);
const seen = this.#collections.get(collectionTarget(type, collection));
return (
this.#entities.has(target) || (seen !== undefined && !seen.has(target))
);
}
/** Whether a list read of a collection lists, or holds, an entity left unknown. */
blocksList(
type: string,
collection: string,
ids: readonly string[],
): boolean {
return (
this.#collections.has(collectionTarget(type, collection)) ||
ids.some((id) => this.#entities.has(entityTarget(type, collection, id)))
);
}
/** The state saw the entity again, or a modelled write replaced it. */
known(type: string, collection: string, id: string): void {
const target = entityTarget(type, collection, id);
this.#entities.delete(target);
this.#collections.get(collectionTarget(type, collection))?.add(target);
}
}