fix(environment): reject contradictory learned sequences

This commit is contained in:
morluto committed 2026-10-28 04:54:20 +08:00
1 parent 420bae068b
commit ba8f1a236e
5 files changed
+37

No files matched your search

@@ -296,6 +296,7 @@ export class EnvironmentStateWriteSimulation {
updates: [],
sequences: new Map(),
starts: new Map(),
createCollections: [],
steady: new Set(),
unstamped: new Set(),
kept: new Set(),
@@ -29,6 +29,29 @@ const ruleFor = (plan: ReturnType<typeof createOf>, field: string): unknown =>
plan.rules.find((rule) => rule.field === field);
describe("values an application counts and makes with each created entity", () => {
it("does not adopt a sequence that contradicts an earlier accepted create", () => {
const a = { id: 1, title: "A", index: 1 };
const b = { id: 2, title: "B", index: 2 };
const c = { id: 3, title: "C", index: 15 };
const d = { id: 4, title: "D", index: 15 };
const e = { id: 5, title: "E", index: 16 };
const rows: readonly TimedCaptureRow[] = [
["GET", "/api/tasks", [a, b]],
["GET", "/api/tasks/1", a],
["POST", "/api/tasks", c, 201, { title: "C" }],
["GET", "/api/tasks/3", c],
["GET", "/api/tasks", [a, b, c]],
["POST", "/api/tasks", d, 201, { title: "D" }],
["GET", "/api/tasks/4", d],
["GET", "/api/tasks", [a, b, c, d]],
["POST", "/api/tasks", e, 201, { title: "E" }],
["GET", "/api/tasks/5", e],
["GET", "/api/tasks", [a, b, c, d, e]],
];
const plan = createOf(rows);
expect(plan).toMatchObject({ served: false });
expect(ruleFor(plan, "index")).toBeUndefined();
});
it("learns a counter from where the first create stood, a template around it and the object made with each task", () => {
const plan = createOf(COUNTED_TASK_ROWS);
expect(plan).toMatchObject({
@@ -311,6 +311,7 @@ const earlierOf = (draft: WriteDraft) => ({
kept: draft.kept,
clockOf: (field: string) => clockEvidence(draft, field),
startOf: (field: string) => draft.starts.get(field),
createCollections: draft.createCollections,
ampersands: draft.ampersands,
});
@@ -968,6 +969,8 @@ export const applyCreate = (
draft.plan.rules,
);
const inserted = insertCreated(context, draft, entity(stored, id));
context.io.retain(collection.length * 2 + 16);
draft.createCollections.push(collection);
return {
kind: "applied",
targets: [inserted, collectionTarget(type, collection)],
@@ -32,6 +32,7 @@ export interface EarlierEvidence {
readonly clockOf: (field: string) => JsonValue | undefined;
/** The integer the exemplar showed in an unruled field above every value its type held, where a sequence may start. */
readonly startOf?: (field: string) => number | undefined;
readonly createCollections?: readonly string[];
/** How the plan's accepted calls and the type's held entities spelled `&` in each field's slug; absent where none showed one. */
readonly ampersands?: ReadonlyMap<string, AmpersandSpelling>;
}
@@ -269,6 +270,13 @@ const spellsExemplar = (
}
if (rule.rule !== "sequence") return undefined;
const start = calls.earlier.startOf?.(field);
const collections = calls.earlier.createCollections ?? [];
if (
(rule.scope === "type" && collections.length > 1) ||
(rule.scope === "collection" &&
new Set(collections).size !== collections.length)
)
return undefined;
return start !== undefined && sequenceNumber(value) === start
? { ...rule, start }
: undefined;
@@ -96,6 +96,8 @@ export interface WriteDraft {
readonly sequences: Map<string, number>;
/** Integers the exemplar showed in fields no rule wrote, above every value its type held, where a later call's sequence may start. */
readonly starts: Map<string, number>;
/** Collections of accepted create calls, used to check late sequence adoption. */
readonly createCollections: string[];
/** The collection a create writes into when its own path names no state type: the one its returned id was read under. */
readonly linked?: string | undefined;
/** Fields an item action's call found already at the value it sets them to. */