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(