Skip to content

Commit d565978

Browse files
MaxLinCodeclaude
andcommitted
Rename slotSnapshot→fieldSnapshot and missingSlots→missingFields in proposal entity data
Aligns proposal_option entity schema with the field-based model introduced in phase 1. Also separates targetEntityId from scheduleFields in synthesize-mutation-text since target is no longer part of the schedule fields group (it lives in targetRef on PendingWriteOperation). - index.ts: missingSlots→missingFields, slotSnapshot→fieldSnapshot - proposal-rules.ts: remove flattenScheduleFields adapter, compare scheduleFields directly, rename deriveSlotsCompatibility→deriveFieldsCompatibility - synthesize-mutation-text.ts: accept resolvedFields + targetEntityId, rename internal helpers to field-based names - discourse-state.ts: update comment on resolvedSlotsSchema retention - All tests updated to match new schema shape Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent a19c9ca commit d565978

7 files changed

Lines changed: 124 additions & 107 deletions

File tree

packages/core/src/discourse-state.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export const timeSpecSchema = z.discriminatedUnion("kind", [
7171

7272
export type TimeSpec = z.infer<typeof timeSpecSchema>;
7373

74-
// Kept for entity slotSnapshot fields (proposal_option, task_draft) — not used in discourse state.
74+
// Kept for slot-extractor schemas — not used in discourse state.
7575
export const resolvedSlotsSchema = z.object({
7676
day: z.string().optional(),
7777
time: timeSpecSchema.optional(),

packages/core/src/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1146,7 +1146,7 @@ describe("core package", () => {
11461146
route: "conversation_then_mutation",
11471147
replyText:
11481148
"It sounds like you want to move the dentist reminder after lunch.",
1149-
slotSnapshot: {},
1149+
fieldSnapshot: {},
11501150
},
11511151
},
11521152
],

packages/core/src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -826,8 +826,8 @@ export const conversationProposalOptionEntitySchema =
826826
.optional(),
827827
confirmationRequired: z.boolean().optional(),
828828
originatingTurnText: z.string().min(1).nullable().optional(),
829-
missingSlots: z.array(z.string().min(1)).optional(),
830-
slotSnapshot: resolvedSlotsSchema,
829+
missingFields: z.array(z.string().min(1)).optional(),
830+
fieldSnapshot: resolvedFieldsSchema,
831831
}),
832832
});
833833

packages/core/src/proposal-rules.test.ts

Lines changed: 20 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import { describe, expect, it } from "vitest";
2-
import type {
3-
ConversationEntity,
4-
ResolvedFields,
5-
ResolvedSlots,
6-
TimeSpec,
7-
} from "./index";
2+
import type { ConversationEntity, ResolvedFields, TimeSpec } from "./index";
83
import { deriveProposalCompatibility } from "./proposal-rules";
94

105
function t(hour: number, minute: number): TimeSpec {
116
return { kind: "absolute", hour, minute };
127
}
138

14-
function sf(slots: ResolvedSlots): ResolvedFields {
15-
return { scheduleFields: slots };
9+
type ScheduleFields = NonNullable<ResolvedFields["scheduleFields"]>;
10+
11+
function sf(fields: ScheduleFields): ResolvedFields {
12+
return { scheduleFields: fields };
1613
}
1714

1815
type ProposalOption = Extract<ConversationEntity, { kind: "proposal_option" }>;
1916

2017
function makeProposal(
21-
overrides: Partial<ProposalOption["data"]> & { slotSnapshot: ResolvedSlots },
18+
overrides: Partial<ProposalOption["data"]> & {
19+
fieldSnapshot: ResolvedFields;
20+
},
2221
): ProposalOption {
2322
return {
2423
id: "proposal-1",
@@ -38,10 +37,10 @@ function makeProposal(
3837
}
3938

4039
describe("deriveProposalCompatibility", () => {
41-
describe("slot-based compatibility", () => {
42-
it("is compatible when committed slots match the snapshot", () => {
40+
describe("field-based compatibility", () => {
41+
it("is compatible when committed fields match the snapshot", () => {
4342
const proposal = makeProposal({
44-
slotSnapshot: { time: t(15, 0), day: "friday" },
43+
fieldSnapshot: sf({ time: t(15, 0), day: "friday" }),
4544
});
4645

4746
const result = deriveProposalCompatibility(
@@ -53,9 +52,9 @@ describe("deriveProposalCompatibility", () => {
5352
expect(result.compatible).toBe(true);
5453
});
5554

56-
it("is incompatible when a committed slot differs from the snapshot", () => {
55+
it("is incompatible when a committed field differs from the snapshot", () => {
5756
const proposal = makeProposal({
58-
slotSnapshot: { time: t(15, 0) },
57+
fieldSnapshot: sf({ time: t(15, 0) }),
5958
});
6059

6160
const result = deriveProposalCompatibility(
@@ -68,9 +67,9 @@ describe("deriveProposalCompatibility", () => {
6867
expect(result.reason).toMatch(/differs from proposal snapshot/);
6968
});
7069

71-
it("is compatible when committed slot is new (not in snapshot)", () => {
70+
it("is compatible when committed field is new (not in snapshot)", () => {
7271
const proposal = makeProposal({
73-
slotSnapshot: { day: "friday" },
72+
fieldSnapshot: sf({ day: "friday" }),
7473
});
7574

7675
const result = deriveProposalCompatibility(
@@ -82,9 +81,9 @@ describe("deriveProposalCompatibility", () => {
8281
expect(result.compatible).toBe(true);
8382
});
8483

85-
it("is compatible when committed slots are empty", () => {
84+
it("is compatible when committed fields are empty", () => {
8685
const proposal = makeProposal({
87-
slotSnapshot: { time: t(15, 0), day: "friday" },
86+
fieldSnapshot: sf({ time: t(15, 0), day: "friday" }),
8887
});
8988

9089
const result = deriveProposalCompatibility(
@@ -98,7 +97,7 @@ describe("deriveProposalCompatibility", () => {
9897

9998
it("detects duration change as incompatible", () => {
10099
const proposal = makeProposal({
101-
slotSnapshot: { duration: 30 },
100+
fieldSnapshot: sf({ duration: 30 }),
102101
});
103102

104103
const result = deriveProposalCompatibility(
@@ -116,7 +115,7 @@ describe("deriveProposalCompatibility", () => {
116115
it("is incompatible when action kind changes from plan to edit", () => {
117116
const proposal = makeProposal({
118117
originatingTurnText: "schedule a meeting",
119-
slotSnapshot: { time: t(15, 0) },
118+
fieldSnapshot: sf({ time: t(15, 0) }),
120119
});
121120

122121
const result = deriveProposalCompatibility(
@@ -132,7 +131,7 @@ describe("deriveProposalCompatibility", () => {
132131
it("skips action kind check for clarification answers", () => {
133132
const proposal = makeProposal({
134133
originatingTurnText: "move the meeting",
135-
slotSnapshot: { time: t(15, 0) },
134+
fieldSnapshot: sf({ time: t(15, 0) }),
136135
});
137136

138137
const result = deriveProposalCompatibility(

packages/core/src/proposal-rules.ts

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type {
22
ConversationEntity,
33
ResolvedFields,
4-
ResolvedSlots,
54
TurnClassifierOutput,
65
TurnInterpretationType,
76
} from "./index";
@@ -101,15 +100,18 @@ export function matchesProposalTarget(
101100
return resolvedEntityIds.includes(targetEntityId);
102101
}
103102

103+
type ScheduleFields = NonNullable<ResolvedFields["scheduleFields"]>;
104+
104105
export function deriveProposalCompatibility(
105106
turnType: TurnInterpretationType,
106107
resolvedFields: ResolvedFields,
107108
proposal: ProposalOption,
108109
) {
109-
const committedSlots = flattenScheduleFields(resolvedFields);
110+
const committedSchedule = resolvedFields.scheduleFields ?? {};
111+
const snapshotSchedule = proposal.data.fieldSnapshot.scheduleFields ?? {};
110112

111113
if (turnType === "clarification_answer") {
112-
return deriveSlotsCompatibility(committedSlots, proposal.data.slotSnapshot);
114+
return deriveFieldsCompatibility(committedSchedule, snapshotSchedule);
113115
}
114116

115117
const currentActionKind = turnType === "edit_request" ? "edit" : "plan";
@@ -124,24 +126,18 @@ export function deriveProposalCompatibility(
124126
};
125127
}
126128

127-
return deriveSlotsCompatibility(committedSlots, proposal.data.slotSnapshot);
128-
}
129-
130-
// Adapter: flatten grouped schedule fields back to ResolvedSlots for
131-
// comparison against proposal.data.slotSnapshot (entity shape not yet migrated).
132-
function flattenScheduleFields(fields: ResolvedFields): ResolvedSlots {
133-
return fields.scheduleFields ?? {};
129+
return deriveFieldsCompatibility(committedSchedule, snapshotSchedule);
134130
}
135131

136-
function deriveSlotsCompatibility(
137-
committedSlots: ResolvedSlots,
138-
snapshotSlots: ResolvedSlots,
132+
function deriveFieldsCompatibility(
133+
committedFields: ScheduleFields,
134+
snapshotFields: ScheduleFields,
139135
) {
140-
const scalarKeys = ["day", "duration", "target"] as const;
136+
const scalarKeys = ["day", "duration"] as const;
141137

142138
for (const key of scalarKeys) {
143-
const committed = committedSlots[key];
144-
const snapshot = snapshotSlots[key];
139+
const committed = committedFields[key];
140+
const snapshot = snapshotFields[key];
145141

146142
if (
147143
committed !== undefined &&
@@ -150,19 +146,19 @@ function deriveSlotsCompatibility(
150146
) {
151147
return {
152148
compatible: false,
153-
reason: `Committed slot "${key}" differs from proposal snapshot, so it needs fresh consent.`,
149+
reason: `Committed field "${key}" differs from proposal snapshot, so it needs fresh consent.`,
154150
};
155151
}
156152
}
157153

158154
if (
159-
committedSlots.time !== undefined &&
160-
snapshotSlots.time !== undefined &&
161-
!timeSpecsEqual(committedSlots.time, snapshotSlots.time)
155+
committedFields.time !== undefined &&
156+
snapshotFields.time !== undefined &&
157+
!timeSpecsEqual(committedFields.time, snapshotFields.time)
162158
) {
163159
return {
164160
compatible: false,
165-
reason: `Committed slot "time" differs from proposal snapshot, so it needs fresh consent.`,
161+
reason: `Committed field "time" differs from proposal snapshot, so it needs fresh consent.`,
166162
};
167163
}
168164

0 commit comments

Comments
 (0)