mirror of
https://github.com/morluto/rea.git
synced 2026-10-27 01:45:32 +00:00
Greptile raised these on this branch, the fidelity branch and the list-lines branch; five follow from the previous round's changes. - An entity field whose name looks like a pointer is matched by name; only a document's pointers cover the members under them. - A failed read contradicts each exercised write that no unmodelled write came after, instead of skipping them all when the oldest pending write preceded one; the shared pending `since` time goes. - An item read, update or delete names its entity as serving's namedEntity does: held collections its path matches, verbatim spellings first, the id decoded once, then as written; `book%2531` no longer reaches `book1`. - A plan whose calls use a member both as the identity and as a fixed member, or fix it at different values, is refused, as serving could not take every recorded call. - A size of zero bounds nothing only on a line whose recording listed items under it (pageBound), in serving and in the gate. - An update whose observation drops a field the entity held is refused, as serving keeps every field it does not write. - A later create whose clock takes another format, or whose unique value is not a UUID, is refused. - A derived state data/get would write past the 32 MiB intake limit is not served (admitServedState); the environment serves replay. 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>
284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
import type { StateView } from "./environmentStateRules.js";
|
|
|
|
/** The fields a read of a view shows of its entities, less volatile ones it does not compare; all of a document's. */
|
|
export const shownFields = (
|
|
view: StateView,
|
|
volatileOf: (type: string) => ReadonlySet<string>,
|
|
): ReadonlySet<string> | "all" => {
|
|
if (view.shape !== "item" && view.shape !== "list") return "all";
|
|
const volatile = volatileOf(view.type);
|
|
return new Set(view.fields.filter((field) => !volatile.has(field)));
|
|
};
|
|
|
|
/** Modelled writes per target since its last check, with the fields each wrote (null: all). */
|
|
export type TouchedTargets = Map<string, Map<string, readonly string[] | null>>;
|
|
|
|
/** Per target, when the latest modelled write since its last check ended. */
|
|
export type PendingTargets = Map<string, { until: number }>;
|
|
|
|
/** Whether a modelled write to the target was still running when a read began at `start`, so the read may show either state. */
|
|
export const runningAt = (
|
|
pending: PendingTargets,
|
|
key: string,
|
|
start: number,
|
|
): boolean => (pending.get(key)?.until ?? -Infinity) > start;
|
|
|
|
/** The tools whose writes a read exercises: those that changed its targets since they were last checked, less those that borrowed the read to learn their effect. */
|
|
export const exercisedBy = (
|
|
touched: TouchedTargets,
|
|
keys: readonly string[],
|
|
borrowers: ReadonlySet<string> | undefined,
|
|
): Set<string> => {
|
|
const exercised = new Set<string>();
|
|
for (const key of keys)
|
|
for (const name of touched.get(key)?.keys() ?? [])
|
|
if (!borrowers?.has(name)) exercised.add(name);
|
|
return exercised;
|
|
};
|
|
|
|
/** Record a modelled write on a target: the fields it wrote (undefined: all) and when it ended. */
|
|
export const touchTarget = (
|
|
touched: TouchedTargets,
|
|
pending: PendingTargets,
|
|
target: string,
|
|
write: {
|
|
readonly tool: string;
|
|
readonly end: number;
|
|
readonly fields: readonly string[] | undefined;
|
|
},
|
|
): void => {
|
|
pending.set(target, {
|
|
until: Math.max(pending.get(target)?.until ?? -Infinity, write.end),
|
|
});
|
|
const tools = touched.get(target) ?? new Map();
|
|
const before = tools.get(write.tool);
|
|
tools.set(
|
|
write.tool,
|
|
write.fields === undefined || before === null
|
|
? null
|
|
: [...new Set([...(before ?? []), ...write.fields])],
|
|
);
|
|
touched.set(target, tools);
|
|
};
|
|
|
|
/** The fields of a write's effect on an entity no later write has overwritten: all of them for a create or delete (null). */
|
|
type ChangedFields = readonly string[] | null;
|
|
|
|
/** What a later write overwrites: the whole entity, the fields it sets, or every field (a recorded outcome). A document's fields are pointers, each covering the members under it. */
|
|
interface Overwrite {
|
|
readonly whole: boolean;
|
|
readonly fields: ReadonlySet<string> | "all";
|
|
readonly pointers: boolean;
|
|
}
|
|
|
|
/** Whether a write's fields cover a field: the same one, or, for a document's pointers, one above it. */
|
|
const covers = (write: Overwrite, field: string): boolean => {
|
|
const { fields } = write;
|
|
if (fields === "all" || fields.has(field)) return true;
|
|
return (
|
|
write.pointers && [...fields].some((over) => field.startsWith(`${over}/`))
|
|
);
|
|
};
|
|
|
|
/** What an earlier effect keeps after a write: nothing once every field it holds is overwritten; an update leaves a create's or delete's whole effect. */
|
|
const remaining = (
|
|
claim: ChangedFields,
|
|
write: Overwrite,
|
|
): ChangedFields | undefined => {
|
|
if (write.whole) return undefined;
|
|
if (claim === null) return null;
|
|
if (write.fields === "all") return undefined;
|
|
const left = claim.filter((field) => !covers(write, field));
|
|
return left.length > 0 ? left : undefined;
|
|
};
|
|
|
|
/** Add a write's changed fields under a key: all of them once any write there changed the whole entity. */
|
|
const record = (
|
|
map: Map<string, ChangedFields>,
|
|
key: string,
|
|
whole: boolean,
|
|
changed: readonly string[],
|
|
): Map<string, ChangedFields> => {
|
|
const before = map.get(key);
|
|
return map.set(
|
|
key,
|
|
whole || before === null
|
|
? null
|
|
: [...new Set([...(before ?? []), ...changed])],
|
|
);
|
|
};
|
|
|
|
/**
|
|
* What each write changed since the targets it touched were last checked, less what later writes overwrote, so a
|
|
* write whose effect is gone drops out: from the entity's target, and from its collection's once no entity it changed
|
|
* there keeps any of it.
|
|
*/
|
|
export class WriteEffects {
|
|
/** Per entity target, the fields each tool changed there. */
|
|
readonly #changed = new Map<string, Map<string, ChangedFields>>();
|
|
/** Per collection target, the entities each tool's claim there rests on, with the fields it changed on each. */
|
|
readonly #members = new Map<
|
|
string,
|
|
Map<string, Map<string, ChangedFields>>
|
|
>();
|
|
|
|
/** Forget a checked target's writes, as the touched targets do: all, or all but those a read showed nothing of. */
|
|
checked(key: string, unseen?: ReadonlySet<string>): void {
|
|
for (const byTool of [this.#changed, this.#members])
|
|
if (unseen === undefined) byTool.delete(key);
|
|
else
|
|
for (const tool of byTool.get(key)?.keys() ?? [])
|
|
if (!unseen.has(tool)) byTool.get(key)!.delete(tool);
|
|
}
|
|
|
|
/** Whether a read showing `fields` of a target shows anything of a tool's effect there: a create's or delete's always, an update's through a field it changed; unknown claims count as shown. */
|
|
shows(
|
|
tool: string,
|
|
key: string,
|
|
fields: ReadonlySet<string> | "all",
|
|
): boolean {
|
|
const visible = (claim: ChangedFields | undefined): boolean =>
|
|
fields === "all" ||
|
|
claim === undefined ||
|
|
claim === null ||
|
|
claim.some((field) => fields.has(field));
|
|
const own = this.#changed.get(key);
|
|
if (own !== undefined) return visible(own.get(tool));
|
|
const members = this.#members.get(key)?.get(tool);
|
|
return members === undefined || [...members.values()].some(visible);
|
|
}
|
|
|
|
/** A write's effect replaces what earlier writes left of theirs, then joins them. */
|
|
supersede(
|
|
touched: TouchedTargets,
|
|
writer: string,
|
|
effect: {
|
|
readonly targets: readonly string[];
|
|
readonly fields?: readonly string[];
|
|
readonly changed?: {
|
|
readonly fields: readonly string[];
|
|
readonly accumulated: readonly string[];
|
|
};
|
|
/** Set when the fields are a document's pointers. */
|
|
readonly pointers?: boolean;
|
|
},
|
|
): void {
|
|
const [entity, collection] = effect.targets as [string, string?];
|
|
const changed = effect.changed?.fields ?? effect.fields ?? [];
|
|
const whole = effect.fields === undefined;
|
|
this.#overwrite(
|
|
touched,
|
|
{ entity, collection, writer },
|
|
{
|
|
whole,
|
|
fields: new Set(
|
|
changed.filter(
|
|
(field) => !effect.changed?.accumulated.includes(field),
|
|
),
|
|
),
|
|
pointers: effect.pointers === true,
|
|
},
|
|
);
|
|
this.#changed.set(
|
|
entity,
|
|
record(this.#changed.get(entity) ?? new Map(), writer, whole, changed),
|
|
);
|
|
if (collection === undefined) return;
|
|
const byTool = this.#members.get(collection) ?? new Map();
|
|
byTool.set(
|
|
writer,
|
|
record(byTool.get(writer) ?? new Map(), entity, whole, changed),
|
|
);
|
|
this.#members.set(collection, byTool);
|
|
}
|
|
|
|
/** A recorded outcome is the entity's new baseline: what earlier writes changed on it is gone, its membership is not. */
|
|
observed(touched: TouchedTargets, targets: readonly string[]): void {
|
|
const [entity, collection] = targets as [string, string?];
|
|
this.#overwrite(
|
|
touched,
|
|
{ entity, collection, writer: undefined },
|
|
{ whole: false, fields: "all", pointers: false },
|
|
);
|
|
}
|
|
|
|
#overwrite(
|
|
touched: TouchedTargets,
|
|
at: {
|
|
readonly entity: string;
|
|
readonly collection: string | undefined;
|
|
readonly writer: string | undefined;
|
|
},
|
|
write: Overwrite,
|
|
): void {
|
|
const own = this.#changed.get(at.entity);
|
|
for (const [tool, claim] of own ?? []) {
|
|
const left = tool === at.writer ? claim : remaining(claim, write);
|
|
if (left !== undefined) own!.set(tool, left);
|
|
else {
|
|
own!.delete(tool);
|
|
touched.get(at.entity)?.delete(tool);
|
|
}
|
|
}
|
|
if (at.collection === undefined) return;
|
|
const byTool = this.#members.get(at.collection);
|
|
for (const [tool, entities] of byTool ?? []) {
|
|
const claim = entities.get(at.entity);
|
|
if (tool === at.writer || claim === undefined) continue;
|
|
const left = remaining(claim, write);
|
|
if (left !== undefined) entities.set(at.entity, left);
|
|
else entities.delete(at.entity);
|
|
if (entities.size > 0) continue;
|
|
byTool!.delete(tool);
|
|
touched.get(at.collection)?.delete(tool);
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Of the tools a read exercised, those it shows something of on some target, and per target those it shows nothing of. */
|
|
export const shownWrites = (
|
|
touched: TouchedTargets,
|
|
effects: WriteEffects,
|
|
read: {
|
|
readonly keys: readonly string[];
|
|
readonly exercised: ReadonlySet<string>;
|
|
readonly fields: ReadonlySet<string> | "all";
|
|
},
|
|
): {
|
|
readonly confirmed: string[];
|
|
readonly unseen: Map<string, Set<string>>;
|
|
} => {
|
|
const confirmed: string[] = [];
|
|
const unseen = new Map<string, Set<string>>();
|
|
for (const name of read.exercised) {
|
|
const on = read.keys.filter((key) => touched.get(key)?.has(name));
|
|
const seen = on.filter((key) => effects.shows(name, key, read.fields));
|
|
for (const key of on)
|
|
if (!seen.includes(key))
|
|
unseen.set(key, (unseen.get(key) ?? new Set()).add(name));
|
|
if (seen.length > 0) confirmed.push(name);
|
|
}
|
|
return { confirmed, unseen };
|
|
};
|
|
|
|
/** A checked target forgets its writes, but for those a passing read showed nothing of, which a later check still takes. */
|
|
export const forgetChecked = (
|
|
pending: {
|
|
readonly touched: TouchedTargets;
|
|
readonly since: PendingTargets;
|
|
readonly effects: WriteEffects;
|
|
},
|
|
key: string,
|
|
unseen: ReadonlySet<string> | undefined,
|
|
): void => {
|
|
const writes = pending.touched.get(key);
|
|
if (unseen === undefined || writes === undefined) {
|
|
pending.touched.delete(key);
|
|
pending.effects.checked(key);
|
|
pending.since.delete(key);
|
|
return;
|
|
}
|
|
for (const name of writes.keys()) if (!unseen.has(name)) writes.delete(name);
|
|
pending.effects.checked(key, unseen);
|
|
};
|