From 308aebf0eb80a64a91d7c61b2947361de0ebfe6b Mon Sep 17 00:00:00 2001 From: Edgar Arakelyan Date: Tue, 27 Oct 2026 15:11:40 -0700 Subject: [PATCH] fix(environment): make no count of a made-object member earlier creates held alike A count only a later create shows starts at the exemplar's value, which is right only while the exemplar is the one create accepted: creates accepted after it showed the member unchanged, which a count would have moved. The merge now refuses such a count, as the top-level sequence check does. Co-Authored-By: Claude Opus 5.5 --- .../environmentStateCreateCalls.test.ts | 49 +++++++++++++++++++ .../environmentStateWriteCreateCalls.ts | 13 ++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/src/domain/analysisView/environmentStateCreateCalls.test.ts b/src/domain/analysisView/environmentStateCreateCalls.test.ts index 198d8bc78a..26aa4c3f3e 100644 --- a/src/domain/analysisView/environmentStateCreateCalls.test.ts +++ b/src/domain/analysisView/environmentStateCreateCalls.test.ts @@ -215,3 +215,52 @@ describe("merging a later create into a plan", () => { ).toBeUndefined(); }); }); + +describe("merging a later create's made object", () => { + const made = (counts: { member: string; start: number }[]): FieldRule => ({ + field: "subscription", + rule: "made-object", + clocks: ["created"], + format: { kind: "iso", separator: "T", fraction: 0, zone: "Z" }, + own: ["entity_id"], + counts, + }); + const body = { + accepted: ["title"], + fixed: [], + required: ["title"], + identity: null, + kinds: [], + }; + const title: FieldRule = { field: "title", rule: "echo" }; + const subscription = (id: number, task: number) => ({ + id, + entity: "task", + entity_id: task, + created: "2026-10-26T10:00:00Z", + }); + const plan = { rules: [title, made([])], body }; + const exemplar = { title: "First", subscription: subscription(31, 20) }; + const later = { + entity: { title: "Third", subscription: subscription(32, 22) }, + body: { title: "Third" }, + rules: [title, made([{ member: "id", start: 32 }])], + contract: body, + }; + const earlier = (accepted: number) => ({ + kept: new Set(), + clockOf: () => undefined, + createCollections: Array.from({ length: accepted }, () => "/api/tasks"), + }); + + it("counts a member only the later create shows while the exemplar is the one create accepted", () => { + expect( + mergedCreatePlan(plan, exemplar, later, earlier(1))?.rules, + ).toContainEqual(made([{ member: "id", start: 31 }])); + }); + + it("makes no count of a member creates past the exemplar held alike", () => { + // Creates of 31 and 31 then 32 are no count: serving would make 31, 32, 33. + expect(mergedCreatePlan(plan, exemplar, later, earlier(2))).toBeUndefined(); + }); +}); diff --git a/src/domain/analysisView/environmentStateWriteCreateCalls.ts b/src/domain/analysisView/environmentStateWriteCreateCalls.ts index d2e5bcdbe2..1383545146 100644 --- a/src/domain/analysisView/environmentStateWriteCreateCalls.ts +++ b/src/domain/analysisView/environmentStateWriteCreateCalls.ts @@ -55,7 +55,12 @@ const agreeing = ( const alike = alikeRules(held, later, values.earlier.kept); if (alike !== undefined) return alike; if (held.rule === "made-object" && later.rule === "made-object") - return madeAlike(held, later, ownMember(values.exemplar, values.field)); + return madeAlike( + held, + later, + ownMember(values.exemplar, values.field), + values.earlier, + ); const spelled = ampersandSpelled( held, { rule: later, entity: values.laterEntity }, @@ -73,11 +78,12 @@ const agreeing = ( type MadeObject = Extract; -/** Two made-object rules as one: the same clocks, format and own members, and the earlier counts among the later's; a count only the later call shows starts at the exemplar's value, which no earlier object could show past. */ +/** Two made-object rules as one: the same clocks, format and own members, and the earlier counts among the later's; a count only the later call shows starts at the exemplar's value, which no earlier object could show past, and only while the exemplar is the one create accepted, as earlier creates held it still. */ const madeAlike = ( held: MadeObject, later: MadeObject, exemplar: JsonValue | undefined, + earlier: EarlierEvidence, ): MadeObject | undefined => { if ( !same(held.clocks, later.clocks) || @@ -91,6 +97,9 @@ const madeAlike = ( const counts: MadeObject["counts"] = []; for (const { member } of later.counts) { const kept = held.counts.find((count) => count.member === member); + // Accepted creates past the exemplar showed this member unchanged, which a count would have moved. + if (kept === undefined && (earlier.createCollections ?? []).length > 1) + return undefined; const start = kept?.start ?? sequenceNumber(