@hobin/developer 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,399 +1,555 @@
1
- import { applyDeveloperEvent, developerSnapshot, initialState } from "./machine.ts";
1
+ import {
2
+ applyDeveloperEvent,
3
+ developerSnapshot,
4
+ initialState,
5
+ } from "./machine.ts";
2
6
 
3
7
  export {
4
- applyDeveloperEvent,
5
- canApplyDeveloperEvent,
6
- developerMachine,
7
- developerSnapshot,
8
- initialState,
8
+ applyDeveloperEvent,
9
+ canApplyDeveloperEvent,
10
+ developerMachine,
11
+ developerSnapshot,
12
+ initialState,
9
13
  } from "./machine.ts";
10
14
 
11
- export const PROTOCOL = "developer/v4" as const;
12
- export const PREVIOUS_PROTOCOL = "developer/v3" as const;
13
- export const OLDER_PROTOCOL = "developer/v2" as const;
14
- export const LEGACY_PROTOCOL = "developer/v1" as const;
15
- export const MODE_ENTRY = "developer.mode" as const;
15
+ export const PROTOCOL = "developer/v5" as const;
16
+ export const ACTIVATION_ENTRY = "developer.activation" as const;
16
17
  export const FOCUS_ENTRY = "developer.question-focus" as const;
17
18
  export const ROUTE_TOOL = "developer_route_question" as const;
18
19
  export const JUDGMENT_TOOL = "developer_record_judgment" as const;
19
- export const LEGACY_ROUTE_TOOL = "route_question" as const;
20
- export const LEGACY_JUDGMENT_TOOL = "record_judgment" as const;
21
-
22
- export type DeveloperMode = "off" | "on" | "strict";
23
- export type JudgmentStatus = "resolved" | "needs-evidence" | "not-applicable" | "blocked";
20
+ export const MAX_RESPONSE_FIELDS = 20;
21
+ export const MAX_RESPONSE_OPTIONS = 20;
22
+ export const MAX_RESPONSE_IDENTIFIER_CHARS = 64;
23
+ export const MAX_RESPONSE_TEXT_CHARS = 2_000;
24
+
25
+ export type JudgmentStatus =
26
+ | "resolved"
27
+ | "needs-evidence"
28
+ | "not-applicable"
29
+ | "blocked";
24
30
  export type PendingQuestionStatus = "open" | "blocked";
25
- export type QuestionResolutionOwner = "agent" | "user" | "environment" | "unknown";
26
- export type QuestionGate = "none" | "before-direct" | "before-completion";
27
- export type QuestionUpdateStatus = "resolved" | "not-applicable" | "open" | "blocked";
28
- export type DirectExecutionProfile = "ordinary" | "behavior-preserving-structure";
29
-
30
- export interface ModeEvent {
31
- protocol: typeof PROTOCOL;
32
- kind: "mode";
33
- mode: DeveloperMode;
31
+ export type QuestionResolutionOwner =
32
+ | "agent"
33
+ | "user"
34
+ | "environment"
35
+ | "unknown";
36
+ export type QuestionGate =
37
+ | "none"
38
+ | "before-implementation"
39
+ | "before-completion";
40
+ export type QuestionUpdateStatus =
41
+ | "resolved"
42
+ | "not-applicable"
43
+ | "open"
44
+ | "blocked";
45
+ export type ImplementationProfile =
46
+ | "ordinary"
47
+ | "behavior-preserving-structure";
48
+
49
+ export interface ActivationEvent {
50
+ protocol: typeof PROTOCOL;
51
+ kind: "activation";
52
+ enabled: boolean;
34
53
  }
35
54
 
36
55
  export interface FocusEvent {
37
- protocol: typeof PROTOCOL;
38
- kind: "focus";
39
- questionId: string;
56
+ protocol: typeof PROTOCOL;
57
+ kind: "focus";
58
+ questionId: string;
40
59
  }
41
60
 
42
- export interface DirectStepContract {
43
- movement: string;
44
- stopCondition: string;
45
- verification: string;
61
+ export interface ImplementationStepContract {
62
+ movement: string;
63
+ stopCondition: string;
64
+ verification: string;
46
65
  }
47
66
 
48
67
  export interface RouteAlternative {
49
- owner: string;
50
- reason: string;
68
+ target: string;
69
+ reason: string;
51
70
  }
52
71
 
53
72
  export interface RouteEvent {
54
- protocol: typeof PROTOCOL;
55
- kind: "route";
56
- routeId: string;
57
- question: string;
58
- owner: string;
59
- reason: string;
60
- knownEvidence: string[];
61
- consideredAlternatives: RouteAlternative[];
62
- targetQuestionId?: string;
63
- methodLocation?: string;
64
- executionProfile?: DirectExecutionProfile;
65
- directStep?: DirectStepContract;
73
+ protocol: typeof PROTOCOL;
74
+ kind: "route";
75
+ routeId: string;
76
+ question: string;
77
+ target: string;
78
+ reason: string;
79
+ knownEvidence: string[];
80
+ consideredAlternatives: RouteAlternative[];
81
+ targetQuestionId?: string;
82
+ methodLocation?: string;
83
+ executionProfile?: ImplementationProfile;
84
+ implementationStep?: ImplementationStepContract;
85
+ }
86
+
87
+ export interface ChoiceResponseOption {
88
+ value: string;
89
+ label: string;
90
+ description?: string;
91
+ detailPrompt?: string;
92
+ }
93
+
94
+ export interface ChoiceResponseField {
95
+ id: string;
96
+ prompt: string;
97
+ description?: string;
98
+ options: ChoiceResponseOption[];
99
+ }
100
+
101
+ export interface ChoiceResponseSpec {
102
+ kind: "choice-form";
103
+ fields: ChoiceResponseField[];
66
104
  }
67
105
 
68
106
  export interface PendingQuestion {
69
- id: string;
70
- question: string;
71
- status: PendingQuestionStatus;
72
- resolutionOwner: QuestionResolutionOwner;
73
- gate: QuestionGate;
74
- resolutionCriteria: string;
75
- sourceRouteId: string;
107
+ id: string;
108
+ question: string;
109
+ context?: string;
110
+ responseSpec?: ChoiceResponseSpec;
111
+ status: PendingQuestionStatus;
112
+ resolutionOwner: QuestionResolutionOwner;
113
+ gate: QuestionGate;
114
+ resolutionCriteria: string;
115
+ sourceRouteId: string;
76
116
  }
77
117
 
78
118
  export interface QuestionUpdate {
79
- questionId: string;
80
- status: QuestionUpdateStatus;
81
- result: string;
82
- basis: string[];
119
+ questionId: string;
120
+ status: QuestionUpdateStatus;
121
+ result: string;
122
+ basis: string[];
83
123
  }
84
124
 
85
125
  export interface JudgmentEvent {
86
- protocol: typeof PROTOCOL;
87
- kind: "judgment";
88
- routeId: string;
89
- question: string;
90
- owner: string;
91
- status: JudgmentStatus;
92
- result: string;
93
- basis: string[];
94
- openedQuestions: PendingQuestion[];
95
- questionUpdates: QuestionUpdate[];
96
- artifacts: string[];
97
- changedArtifacts: boolean;
98
- }
99
-
100
- export type DeveloperEvent = ModeEvent | FocusEvent | RouteEvent | JudgmentEvent;
126
+ protocol: typeof PROTOCOL;
127
+ kind: "judgment";
128
+ routeId: string;
129
+ question: string;
130
+ target: string;
131
+ status: JudgmentStatus;
132
+ result: string;
133
+ basis: string[];
134
+ openedQuestions: PendingQuestion[];
135
+ questionUpdates: QuestionUpdate[];
136
+ artifacts: string[];
137
+ changedArtifacts: boolean;
138
+ }
139
+
140
+ export type DeveloperEvent =
141
+ | ActivationEvent
142
+ | FocusEvent
143
+ | RouteEvent
144
+ | JudgmentEvent;
101
145
 
102
146
  export interface DeveloperState {
103
- mode: DeveloperMode;
104
- activeRoute?: RouteEvent;
105
- lastRoute?: RouteEvent;
106
- lastJudgment?: JudgmentEvent;
107
- routeHistory: RouteEvent[];
108
- judgmentHistory: JudgmentEvent[];
109
- pendingQuestions: PendingQuestion[];
110
- focusedQuestionId?: string;
111
- rerouteRequired: boolean;
112
- implementationFramingRequired: boolean;
113
- verificationRequired: boolean;
147
+ enabled: boolean;
148
+ activeRoute?: RouteEvent;
149
+ lastRoute?: RouteEvent;
150
+ lastJudgment?: JudgmentEvent;
151
+ routeHistory: RouteEvent[];
152
+ judgmentHistory: JudgmentEvent[];
153
+ pendingQuestions: PendingQuestion[];
154
+ focusedQuestionId?: string;
155
+ rerouteRequired: boolean;
156
+ implementationFramingRequired: boolean;
157
+ verificationRequired: boolean;
114
158
  }
115
159
 
116
160
  export type ProtocolState =
117
- | "idle"
118
- | "needs-judgment"
119
- | "needs-evidence"
120
- | "needs-answer"
121
- | "needs-routing"
122
- | "needs-verification"
123
- | "blocked";
161
+ | "idle"
162
+ | "needs-judgment"
163
+ | "needs-evidence"
164
+ | "needs-answer"
165
+ | "needs-routing"
166
+ | "needs-verification"
167
+ | "blocked";
124
168
 
125
169
  export function protocolState(state: DeveloperState): ProtocolState {
126
- const snapshot = developerSnapshot(state);
127
- if (!snapshot.matches({ route: "idle" })) return "needs-judgment";
128
- if (
129
- snapshot.hasTag("blocks-direct") ||
130
- state.pendingQuestions.some((question) => question.status === "blocked")
131
- ) return "blocked";
132
- if (state.pendingQuestions.some((question) => question.resolutionOwner === "user")) return "needs-answer";
133
- if (snapshot.matches({ questions: "open" })) return "needs-evidence";
134
- if (snapshot.hasTag("reroute-required")) return "needs-routing";
135
- if (snapshot.hasTag("verification-required")) return "needs-verification";
136
- return "idle";
170
+ const snapshot = developerSnapshot(state);
171
+ if (!snapshot.matches({ route: "idle" })) return "needs-judgment";
172
+ if (
173
+ snapshot.hasTag("blocks-implementation") ||
174
+ state.pendingQuestions.some((question) => question.status === "blocked")
175
+ )
176
+ return "blocked";
177
+ if (
178
+ state.pendingQuestions.some(
179
+ (question) => question.resolutionOwner === "user",
180
+ )
181
+ )
182
+ return "needs-answer";
183
+ if (snapshot.matches({ questions: "open" })) return "needs-evidence";
184
+ if (snapshot.hasTag("reroute-required")) return "needs-routing";
185
+ if (snapshot.hasTag("verification-required")) return "needs-verification";
186
+ return "idle";
137
187
  }
138
188
 
139
189
  function isObject(value: unknown): value is Record<string, unknown> {
140
- return Boolean(value) && typeof value === "object";
190
+ return Boolean(value) && typeof value === "object";
141
191
  }
142
192
 
143
193
  function isStringArray(value: unknown): value is string[] {
144
- return Array.isArray(value) && value.every((item) => typeof item === "string");
145
- }
146
-
147
- function isMode(value: unknown): value is DeveloperMode {
148
- return value === "off" || value === "on" || value === "strict";
194
+ return (
195
+ Array.isArray(value) && value.every((item) => typeof item === "string")
196
+ );
149
197
  }
150
198
 
151
199
  function isJudgmentStatus(value: unknown): value is JudgmentStatus {
152
- return value === "resolved" || value === "needs-evidence" || value === "not-applicable" || value === "blocked";
200
+ return (
201
+ value === "resolved" ||
202
+ value === "needs-evidence" ||
203
+ value === "not-applicable" ||
204
+ value === "blocked"
205
+ );
153
206
  }
154
207
 
155
- function isDirectExecutionProfile(value: unknown): value is DirectExecutionProfile {
156
- return value === "ordinary" || value === "behavior-preserving-structure";
208
+ function isImplementationProfile(
209
+ value: unknown,
210
+ ): value is ImplementationProfile {
211
+ return value === "ordinary" || value === "behavior-preserving-structure";
157
212
  }
158
213
 
159
- function parseDirectStep(value: unknown): DirectStepContract | undefined {
160
- if (!isObject(value)) return undefined;
161
- if (
162
- typeof value.movement !== "string" ||
163
- typeof value.stopCondition !== "string" ||
164
- typeof value.verification !== "string"
165
- ) {
166
- return undefined;
167
- }
168
- return {
169
- movement: value.movement,
170
- stopCondition: value.stopCondition,
171
- verification: value.verification,
172
- };
214
+ function parseImplementationStep(
215
+ value: unknown,
216
+ ): ImplementationStepContract | undefined {
217
+ if (!isObject(value)) return undefined;
218
+ if (
219
+ typeof value.movement !== "string" ||
220
+ typeof value.stopCondition !== "string" ||
221
+ typeof value.verification !== "string"
222
+ ) {
223
+ return undefined;
224
+ }
225
+ return {
226
+ movement: value.movement,
227
+ stopCondition: value.stopCondition,
228
+ verification: value.verification,
229
+ };
173
230
  }
174
231
 
175
232
  function parseRouteAlternative(value: unknown): RouteAlternative | undefined {
176
- if (!isObject(value) || typeof value.owner !== "string" || typeof value.reason !== "string") {
177
- return undefined;
178
- }
179
- return { owner: value.owner, reason: value.reason };
233
+ if (
234
+ !isObject(value) ||
235
+ typeof value.target !== "string" ||
236
+ typeof value.reason !== "string"
237
+ ) {
238
+ return undefined;
239
+ }
240
+ return { target: value.target, reason: value.reason };
180
241
  }
181
242
 
182
- function isQuestionResolutionOwner(value: unknown): value is QuestionResolutionOwner {
183
- return value === "agent" || value === "user" || value === "environment" || value === "unknown";
243
+ function isQuestionResolutionOwner(
244
+ value: unknown,
245
+ ): value is QuestionResolutionOwner {
246
+ return (
247
+ value === "agent" ||
248
+ value === "user" ||
249
+ value === "environment" ||
250
+ value === "unknown"
251
+ );
184
252
  }
185
253
 
186
254
  function isQuestionGate(value: unknown): value is QuestionGate {
187
- return value === "none" || value === "before-direct" || value === "before-completion";
255
+ return (
256
+ value === "none" ||
257
+ value === "before-implementation" ||
258
+ value === "before-completion"
259
+ );
188
260
  }
189
261
 
190
262
  function isQuestionUpdateStatus(value: unknown): value is QuestionUpdateStatus {
191
- return value === "resolved" || value === "not-applicable" || value === "open" || value === "blocked";
263
+ return (
264
+ value === "resolved" ||
265
+ value === "not-applicable" ||
266
+ value === "open" ||
267
+ value === "blocked"
268
+ );
269
+ }
270
+
271
+ const RESPONSE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
272
+
273
+ function requiredResponseText(
274
+ value: unknown,
275
+ maxChars: number,
276
+ ): string | undefined {
277
+ if (typeof value !== "string") return undefined;
278
+ const text = value.trim();
279
+ return text && text.length <= maxChars ? text : undefined;
280
+ }
281
+
282
+ function optionalResponseText(value: unknown): string | undefined | null {
283
+ if (value === undefined) return undefined;
284
+ return requiredResponseText(value, MAX_RESPONSE_TEXT_CHARS) ?? null;
285
+ }
286
+
287
+ function parseChoiceResponseOption(
288
+ value: unknown,
289
+ seenValues: Set<string>,
290
+ ): ChoiceResponseOption | undefined {
291
+ if (!isObject(value)) return undefined;
292
+ const optionValue = requiredResponseText(
293
+ value.value,
294
+ MAX_RESPONSE_IDENTIFIER_CHARS,
295
+ );
296
+ const label = requiredResponseText(value.label, MAX_RESPONSE_TEXT_CHARS);
297
+ const description = optionalResponseText(value.description);
298
+ const detailPrompt = optionalResponseText(value.detailPrompt);
299
+ if (!optionValue || !RESPONSE_IDENTIFIER.test(optionValue)) return undefined;
300
+ if (seenValues.has(optionValue) || !label) return undefined;
301
+ if (description === null || detailPrompt === null) return undefined;
302
+ seenValues.add(optionValue);
303
+ return { value: optionValue, label, description, detailPrompt };
304
+ }
305
+
306
+ function parseChoiceResponseField(
307
+ value: unknown,
308
+ seenIds: Set<string>,
309
+ ): ChoiceResponseField | undefined {
310
+ if (!isObject(value) || !Array.isArray(value.options)) return undefined;
311
+ const id = requiredResponseText(value.id, MAX_RESPONSE_IDENTIFIER_CHARS);
312
+ const prompt = requiredResponseText(value.prompt, MAX_RESPONSE_TEXT_CHARS);
313
+ const description = optionalResponseText(value.description);
314
+ if (!id || !RESPONSE_IDENTIFIER.test(id)) return undefined;
315
+ if (seenIds.has(id) || !prompt || description === null) return undefined;
316
+ if (value.options.length < 2 || value.options.length > MAX_RESPONSE_OPTIONS)
317
+ return undefined;
318
+
319
+ seenIds.add(id);
320
+ const seenValues = new Set<string>();
321
+ const options: ChoiceResponseOption[] = [];
322
+ for (const rawOption of value.options) {
323
+ const option = parseChoiceResponseOption(rawOption, seenValues);
324
+ if (!option) return undefined;
325
+ options.push(option);
326
+ }
327
+ return { id, prompt, description, options };
328
+ }
329
+
330
+ export function parseChoiceResponseSpec(
331
+ value: unknown,
332
+ ): ChoiceResponseSpec | undefined {
333
+ if (
334
+ !isObject(value) ||
335
+ value.kind !== "choice-form" ||
336
+ !Array.isArray(value.fields)
337
+ ) {
338
+ return undefined;
339
+ }
340
+ if (value.fields.length === 0 || value.fields.length > MAX_RESPONSE_FIELDS) {
341
+ return undefined;
342
+ }
343
+ const seenIds = new Set<string>();
344
+ const fields: ChoiceResponseField[] = [];
345
+ for (const rawField of value.fields) {
346
+ const field = parseChoiceResponseField(rawField, seenIds);
347
+ if (!field) return undefined;
348
+ fields.push(field);
349
+ }
350
+ return { kind: "choice-form", fields };
192
351
  }
193
352
 
194
353
  function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
195
- if (!isObject(value)) return undefined;
196
- if (
197
- typeof value.id !== "string" ||
198
- typeof value.question !== "string" ||
199
- (value.status !== "open" && value.status !== "blocked" && value.status !== "needs-evidence") ||
200
- typeof value.sourceRouteId !== "string"
201
- ) {
202
- return undefined;
203
- }
204
- const legacyBlocked = value.status === "blocked";
205
- let resolutionOwner: QuestionResolutionOwner = legacyBlocked ? "unknown" : "agent";
206
- if (isQuestionResolutionOwner(value.resolutionOwner)) resolutionOwner = value.resolutionOwner;
207
- let gate: QuestionGate = legacyBlocked ? "before-completion" : "none";
208
- if (isQuestionGate(value.gate)) gate = value.gate;
209
- const resolutionCriteria =
210
- typeof value.resolutionCriteria === "string"
211
- ? value.resolutionCriteria
212
- : `Obtain evidence that settles: ${value.question}`;
213
- return {
214
- id: value.id,
215
- question: value.question,
216
- status: legacyBlocked ? "blocked" : "open",
217
- resolutionOwner,
218
- gate,
219
- resolutionCriteria,
220
- sourceRouteId: value.sourceRouteId,
221
- };
354
+ if (!isObject(value)) return undefined;
355
+ if (
356
+ typeof value.id !== "string" ||
357
+ typeof value.question !== "string" ||
358
+ (value.status !== "open" &&
359
+ value.status !== "blocked" &&
360
+ value.status !== "needs-evidence") ||
361
+ typeof value.sourceRouteId !== "string"
362
+ ) {
363
+ return undefined;
364
+ }
365
+ const legacyBlocked = value.status === "blocked";
366
+ let resolutionOwner: QuestionResolutionOwner = legacyBlocked
367
+ ? "unknown"
368
+ : "agent";
369
+ if (isQuestionResolutionOwner(value.resolutionOwner))
370
+ resolutionOwner = value.resolutionOwner;
371
+ let gate: QuestionGate = legacyBlocked ? "before-completion" : "none";
372
+ if (isQuestionGate(value.gate)) gate = value.gate;
373
+ const resolutionCriteria =
374
+ typeof value.resolutionCriteria === "string"
375
+ ? value.resolutionCriteria
376
+ : `Obtain evidence that settles: ${value.question}`;
377
+ const context =
378
+ typeof value.context === "string"
379
+ ? value.context.trim() || undefined
380
+ : undefined;
381
+ const responseSpec =
382
+ resolutionOwner === "user"
383
+ ? parseChoiceResponseSpec(value.responseSpec)
384
+ : undefined;
385
+ return {
386
+ id: value.id,
387
+ question: value.question,
388
+ context,
389
+ responseSpec,
390
+ status: legacyBlocked ? "blocked" : "open",
391
+ resolutionOwner,
392
+ gate,
393
+ resolutionCriteria,
394
+ sourceRouteId: value.sourceRouteId,
395
+ };
222
396
  }
223
397
 
224
398
  function parseQuestionUpdate(value: unknown): QuestionUpdate | undefined {
225
- if (
226
- !isObject(value) ||
227
- typeof value.questionId !== "string" ||
228
- !isQuestionUpdateStatus(value.status) ||
229
- typeof value.result !== "string" ||
230
- !isStringArray(value.basis)
231
- ) {
232
- return undefined;
233
- }
234
- return {
235
- questionId: value.questionId,
236
- status: value.status,
237
- result: value.result,
238
- basis: value.basis,
239
- };
240
- }
241
-
242
- export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefined {
243
- if (!isObject(value)) return undefined;
244
- if (
245
- value.protocol !== PROTOCOL &&
246
- value.protocol !== PREVIOUS_PROTOCOL &&
247
- value.protocol !== OLDER_PROTOCOL &&
248
- value.protocol !== LEGACY_PROTOCOL
249
- ) return undefined;
250
-
251
- if (value.kind === "mode") {
252
- if (!isMode(value.mode)) return undefined;
253
- return { protocol: PROTOCOL, kind: "mode", mode: value.mode };
254
- }
255
-
256
- if (value.kind === "focus") {
257
- if (
258
- (value.protocol !== PROTOCOL && value.protocol !== PREVIOUS_PROTOCOL) ||
259
- typeof value.questionId !== "string"
260
- ) return undefined;
261
- return { protocol: PROTOCOL, kind: "focus", questionId: value.questionId };
262
- }
263
-
264
- if (value.kind === "route") {
265
- if (
266
- typeof value.routeId !== "string" ||
267
- typeof value.question !== "string" ||
268
- typeof value.owner !== "string" ||
269
- typeof value.reason !== "string" ||
270
- !isStringArray(value.knownEvidence) ||
271
- (value.consideredAlternatives !== undefined && !Array.isArray(value.consideredAlternatives)) ||
272
- (value.targetQuestionId !== undefined && typeof value.targetQuestionId !== "string") ||
273
- (value.methodLocation !== undefined && typeof value.methodLocation !== "string") ||
274
- (value.executionProfile !== undefined && !isDirectExecutionProfile(value.executionProfile)) ||
275
- (value.directStep !== undefined && !parseDirectStep(value.directStep))
276
- ) {
277
- return undefined;
278
- }
279
- const consideredAlternatives = Array.isArray(value.consideredAlternatives)
280
- ? value.consideredAlternatives.map(parseRouteAlternative)
281
- : [];
282
- if (consideredAlternatives.some((alternative) => !alternative)) return undefined;
283
- return {
284
- protocol: PROTOCOL,
285
- kind: "route",
286
- routeId: value.routeId,
287
- question: value.question,
288
- owner: value.owner,
289
- reason: value.reason,
290
- knownEvidence: value.knownEvidence,
291
- consideredAlternatives: consideredAlternatives as RouteAlternative[],
292
- targetQuestionId: value.targetQuestionId,
293
- methodLocation: value.methodLocation,
294
- executionProfile: value.executionProfile,
295
- directStep: value.directStep === undefined ? undefined : parseDirectStep(value.directStep),
296
- };
297
- }
298
-
299
- if (value.kind !== "judgment") return undefined;
300
- if (
301
- typeof value.routeId !== "string" ||
302
- typeof value.question !== "string" ||
303
- typeof value.owner !== "string" ||
304
- !isJudgmentStatus(value.status) ||
305
- typeof value.result !== "string" ||
306
- !isStringArray(value.basis) ||
307
- !isStringArray(value.artifacts)
308
- ) {
309
- return undefined;
310
- }
311
-
312
- if (value.protocol === LEGACY_PROTOCOL) {
313
- if (!isStringArray(value.openQuestions)) return undefined;
314
- return {
315
- protocol: PROTOCOL,
316
- kind: "judgment",
317
- routeId: value.routeId,
318
- question: value.question,
319
- owner: value.owner,
320
- status: value.status,
321
- result: value.result,
322
- basis: value.basis,
323
- openedQuestions: value.openQuestions.map((question, index) => ({
324
- id: `question:${value.routeId}:legacy:${index + 1}`,
325
- question,
326
- status: "open",
327
- resolutionOwner: "agent",
328
- gate: "none",
329
- resolutionCriteria: `Obtain evidence that settles: ${question}`,
330
- sourceRouteId: String(value.routeId),
331
- })),
332
- questionUpdates: [],
333
- artifacts: value.artifacts,
334
- changedArtifacts: false,
335
- };
336
- }
337
-
338
- if (!Array.isArray(value.openedQuestions)) return undefined;
339
- const openedQuestions = value.openedQuestions.map(parsePendingQuestion);
340
- if (openedQuestions.some((question) => !question)) return undefined;
341
- if (value.questionUpdates !== undefined && !Array.isArray(value.questionUpdates)) return undefined;
342
- const questionUpdates = Array.isArray(value.questionUpdates)
343
- ? value.questionUpdates.map(parseQuestionUpdate)
344
- : [];
345
- if (questionUpdates.some((update) => !update)) return undefined;
346
- return {
347
- protocol: PROTOCOL,
348
- kind: "judgment",
349
- routeId: value.routeId,
350
- question: value.question,
351
- owner: value.owner,
352
- status: value.status,
353
- result: value.result,
354
- basis: value.basis,
355
- openedQuestions: openedQuestions as PendingQuestion[],
356
- questionUpdates: questionUpdates as QuestionUpdate[],
357
- artifacts: value.artifacts,
358
- changedArtifacts: typeof value.changedArtifacts === "boolean" ? value.changedArtifacts : false,
359
- };
399
+ if (
400
+ !isObject(value) ||
401
+ typeof value.questionId !== "string" ||
402
+ !isQuestionUpdateStatus(value.status) ||
403
+ typeof value.result !== "string" ||
404
+ !isStringArray(value.basis)
405
+ ) {
406
+ return undefined;
407
+ }
408
+ return {
409
+ questionId: value.questionId,
410
+ status: value.status,
411
+ result: value.result,
412
+ basis: value.basis,
413
+ };
414
+ }
415
+
416
+ export function normalizeDeveloperEvent(
417
+ value: unknown,
418
+ ): DeveloperEvent | undefined {
419
+ if (!isObject(value) || value.protocol !== PROTOCOL) return undefined;
420
+
421
+ if (value.kind === "activation") {
422
+ if (typeof value.enabled !== "boolean") return undefined;
423
+ return { protocol: PROTOCOL, kind: "activation", enabled: value.enabled };
424
+ }
425
+
426
+ if (value.kind === "focus") {
427
+ if (typeof value.questionId !== "string") return undefined;
428
+ return { protocol: PROTOCOL, kind: "focus", questionId: value.questionId };
429
+ }
430
+
431
+ if (value.kind === "route") {
432
+ if (
433
+ typeof value.routeId !== "string" ||
434
+ typeof value.question !== "string" ||
435
+ typeof value.target !== "string" ||
436
+ typeof value.reason !== "string" ||
437
+ !isStringArray(value.knownEvidence) ||
438
+ (value.consideredAlternatives !== undefined &&
439
+ !Array.isArray(value.consideredAlternatives)) ||
440
+ (value.targetQuestionId !== undefined &&
441
+ typeof value.targetQuestionId !== "string") ||
442
+ (value.methodLocation !== undefined &&
443
+ typeof value.methodLocation !== "string") ||
444
+ (value.executionProfile !== undefined &&
445
+ !isImplementationProfile(value.executionProfile)) ||
446
+ (value.implementationStep !== undefined &&
447
+ !parseImplementationStep(value.implementationStep))
448
+ ) {
449
+ return undefined;
450
+ }
451
+ const consideredAlternatives = Array.isArray(value.consideredAlternatives)
452
+ ? value.consideredAlternatives.map(parseRouteAlternative)
453
+ : [];
454
+ if (consideredAlternatives.some((alternative) => !alternative))
455
+ return undefined;
456
+ return {
457
+ protocol: PROTOCOL,
458
+ kind: "route",
459
+ routeId: value.routeId,
460
+ question: value.question,
461
+ target: value.target,
462
+ reason: value.reason,
463
+ knownEvidence: value.knownEvidence,
464
+ consideredAlternatives: consideredAlternatives as RouteAlternative[],
465
+ targetQuestionId: value.targetQuestionId,
466
+ methodLocation: value.methodLocation,
467
+ executionProfile: value.executionProfile,
468
+ implementationStep:
469
+ value.implementationStep === undefined
470
+ ? undefined
471
+ : parseImplementationStep(value.implementationStep),
472
+ };
473
+ }
474
+
475
+ if (value.kind !== "judgment") return undefined;
476
+ if (
477
+ typeof value.routeId !== "string" ||
478
+ typeof value.question !== "string" ||
479
+ typeof value.target !== "string" ||
480
+ !isJudgmentStatus(value.status) ||
481
+ typeof value.result !== "string" ||
482
+ !isStringArray(value.basis) ||
483
+ !isStringArray(value.artifacts) ||
484
+ !Array.isArray(value.openedQuestions)
485
+ ) {
486
+ return undefined;
487
+ }
488
+
489
+ const openedQuestions = value.openedQuestions.map(parsePendingQuestion);
490
+ if (openedQuestions.some((question) => !question)) return undefined;
491
+ if (
492
+ value.questionUpdates !== undefined &&
493
+ !Array.isArray(value.questionUpdates)
494
+ )
495
+ return undefined;
496
+ const questionUpdates = Array.isArray(value.questionUpdates)
497
+ ? value.questionUpdates.map(parseQuestionUpdate)
498
+ : [];
499
+ if (questionUpdates.some((update) => !update)) return undefined;
500
+ return {
501
+ protocol: PROTOCOL,
502
+ kind: "judgment",
503
+ routeId: value.routeId,
504
+ question: value.question,
505
+ target: value.target,
506
+ status: value.status,
507
+ result: value.result,
508
+ basis: value.basis,
509
+ openedQuestions: openedQuestions as PendingQuestion[],
510
+ questionUpdates: questionUpdates as QuestionUpdate[],
511
+ artifacts: value.artifacts,
512
+ changedArtifacts:
513
+ typeof value.changedArtifacts === "boolean"
514
+ ? value.changedArtifacts
515
+ : false,
516
+ };
360
517
  }
361
518
 
362
519
  interface BranchEntryLike {
363
- type: string;
364
- customType?: string;
365
- data?: unknown;
366
- message?: { role?: string; toolName?: string; details?: unknown };
367
- }
368
-
369
- const DEVELOPER_TOOL_NAMES = new Set<string>([
370
- ROUTE_TOOL,
371
- JUDGMENT_TOOL,
372
- LEGACY_ROUTE_TOOL,
373
- LEGACY_JUDGMENT_TOOL,
374
- ]);
375
-
376
- export function eventFromBranchEntry(entry: BranchEntryLike): DeveloperEvent | undefined {
377
- if (
378
- entry.type === "custom" &&
379
- (entry.customType === MODE_ENTRY || entry.customType === FOCUS_ENTRY)
380
- ) {
381
- return normalizeDeveloperEvent(entry.data);
382
- }
383
- if (
384
- entry.type === "message" &&
385
- entry.message?.role === "toolResult" &&
386
- entry.message.toolName &&
387
- DEVELOPER_TOOL_NAMES.has(entry.message.toolName)
388
- ) {
389
- return normalizeDeveloperEvent(entry.message.details);
390
- }
391
- return undefined;
392
- }
393
-
394
- export function reconstructState(entries: ReadonlyArray<BranchEntryLike>): DeveloperState {
395
- return entries.reduce((state, entry) => {
396
- const event = eventFromBranchEntry(entry);
397
- return event ? applyDeveloperEvent(state, event) : state;
398
- }, initialState());
520
+ type: string;
521
+ customType?: string;
522
+ data?: unknown;
523
+ message?: { role?: string; toolName?: string; details?: unknown };
524
+ }
525
+
526
+ const DEVELOPER_TOOL_NAMES = new Set<string>([ROUTE_TOOL, JUDGMENT_TOOL]);
527
+
528
+ export function eventFromBranchEntry(
529
+ entry: BranchEntryLike,
530
+ ): DeveloperEvent | undefined {
531
+ if (
532
+ entry.type === "custom" &&
533
+ (entry.customType === ACTIVATION_ENTRY || entry.customType === FOCUS_ENTRY)
534
+ ) {
535
+ return normalizeDeveloperEvent(entry.data);
536
+ }
537
+ if (
538
+ entry.type === "message" &&
539
+ entry.message?.role === "toolResult" &&
540
+ entry.message.toolName &&
541
+ DEVELOPER_TOOL_NAMES.has(entry.message.toolName)
542
+ ) {
543
+ return normalizeDeveloperEvent(entry.message.details);
544
+ }
545
+ return undefined;
546
+ }
547
+
548
+ export function reconstructState(
549
+ entries: ReadonlyArray<BranchEntryLike>,
550
+ ): DeveloperState {
551
+ return entries.reduce((state, entry) => {
552
+ const event = eventFromBranchEntry(entry);
553
+ return event ? applyDeveloperEvent(state, event) : state;
554
+ }, initialState());
399
555
  }