@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.
- package/README.md +128 -86
- package/SOURCES.md +8 -8
- package/extensions/developer.ts +1713 -1176
- package/extensions/machine.ts +418 -286
- package/extensions/references/behavior-preserving-structural-change.md +14 -13
- package/extensions/state.ts +486 -330
- package/extensions/tool-policy.ts +70 -64
- package/extensions/tui.ts +1713 -653
- package/package.json +1 -1
- package/skills/sketch/references/responsibility-and-variation.md +1 -1
package/extensions/state.ts
CHANGED
|
@@ -1,399 +1,555 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
applyDeveloperEvent,
|
|
3
|
+
developerSnapshot,
|
|
4
|
+
initialState,
|
|
5
|
+
} from "./machine.ts";
|
|
2
6
|
|
|
3
7
|
export {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
applyDeveloperEvent,
|
|
9
|
+
canApplyDeveloperEvent,
|
|
10
|
+
developerMachine,
|
|
11
|
+
developerSnapshot,
|
|
12
|
+
initialState,
|
|
9
13
|
} from "./machine.ts";
|
|
10
14
|
|
|
11
|
-
export const PROTOCOL = "developer/
|
|
12
|
-
export 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
|
|
20
|
-
export const
|
|
21
|
-
|
|
22
|
-
export
|
|
23
|
-
|
|
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 =
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
56
|
+
protocol: typeof PROTOCOL;
|
|
57
|
+
kind: "focus";
|
|
58
|
+
questionId: string;
|
|
40
59
|
}
|
|
41
60
|
|
|
42
|
-
export interface
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
export interface ImplementationStepContract {
|
|
62
|
+
movement: string;
|
|
63
|
+
stopCondition: string;
|
|
64
|
+
verification: string;
|
|
46
65
|
}
|
|
47
66
|
|
|
48
67
|
export interface RouteAlternative {
|
|
49
|
-
|
|
50
|
-
|
|
68
|
+
target: string;
|
|
69
|
+
reason: string;
|
|
51
70
|
}
|
|
52
71
|
|
|
53
72
|
export interface RouteEvent {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
119
|
+
questionId: string;
|
|
120
|
+
status: QuestionUpdateStatus;
|
|
121
|
+
result: string;
|
|
122
|
+
basis: string[];
|
|
83
123
|
}
|
|
84
124
|
|
|
85
125
|
export interface JudgmentEvent {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
export type DeveloperEvent =
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
190
|
+
return Boolean(value) && typeof value === "object";
|
|
141
191
|
}
|
|
142
192
|
|
|
143
193
|
function isStringArray(value: unknown): value is string[] {
|
|
144
|
-
|
|
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
|
-
|
|
200
|
+
return (
|
|
201
|
+
value === "resolved" ||
|
|
202
|
+
value === "needs-evidence" ||
|
|
203
|
+
value === "not-applicable" ||
|
|
204
|
+
value === "blocked"
|
|
205
|
+
);
|
|
153
206
|
}
|
|
154
207
|
|
|
155
|
-
function
|
|
156
|
-
|
|
208
|
+
function isImplementationProfile(
|
|
209
|
+
value: unknown,
|
|
210
|
+
): value is ImplementationProfile {
|
|
211
|
+
return value === "ordinary" || value === "behavior-preserving-structure";
|
|
157
212
|
}
|
|
158
213
|
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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(
|
|
183
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
export function normalizeDeveloperEvent(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const DEVELOPER_TOOL_NAMES = new Set<string>([
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
}
|