@hobin/developer 0.1.5 → 0.1.6
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 +35 -11
- package/extensions/developer.ts +381 -92
- package/extensions/state.ts +120 -8
- package/extensions/tui.ts +341 -37
- package/package.json +1 -1
package/extensions/state.ts
CHANGED
|
@@ -18,6 +18,10 @@ export const ROUTE_TOOL = "developer_route_question" as const;
|
|
|
18
18
|
export const JUDGMENT_TOOL = "developer_record_judgment" as const;
|
|
19
19
|
export const LEGACY_ROUTE_TOOL = "route_question" as const;
|
|
20
20
|
export const LEGACY_JUDGMENT_TOOL = "record_judgment" as const;
|
|
21
|
+
export const MAX_RESPONSE_FIELDS = 20;
|
|
22
|
+
export const MAX_RESPONSE_OPTIONS = 20;
|
|
23
|
+
export const MAX_RESPONSE_IDENTIFIER_CHARS = 64;
|
|
24
|
+
export const MAX_RESPONSE_TEXT_CHARS = 2_000;
|
|
21
25
|
|
|
22
26
|
export type DeveloperMode = "off" | "on" | "strict";
|
|
23
27
|
export type JudgmentStatus = "resolved" | "needs-evidence" | "not-applicable" | "blocked";
|
|
@@ -65,9 +69,30 @@ export interface RouteEvent {
|
|
|
65
69
|
directStep?: DirectStepContract;
|
|
66
70
|
}
|
|
67
71
|
|
|
72
|
+
export interface ChoiceResponseOption {
|
|
73
|
+
value: string;
|
|
74
|
+
label: string;
|
|
75
|
+
description?: string;
|
|
76
|
+
detailPrompt?: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ChoiceResponseField {
|
|
80
|
+
id: string;
|
|
81
|
+
prompt: string;
|
|
82
|
+
description?: string;
|
|
83
|
+
options: ChoiceResponseOption[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ChoiceResponseSpec {
|
|
87
|
+
kind: "choice-form";
|
|
88
|
+
fields: ChoiceResponseField[];
|
|
89
|
+
}
|
|
90
|
+
|
|
68
91
|
export interface PendingQuestion {
|
|
69
92
|
id: string;
|
|
70
93
|
question: string;
|
|
94
|
+
context?: string;
|
|
95
|
+
responseSpec?: ChoiceResponseSpec;
|
|
71
96
|
status: PendingQuestionStatus;
|
|
72
97
|
resolutionOwner: QuestionResolutionOwner;
|
|
73
98
|
gate: QuestionGate;
|
|
@@ -128,8 +153,10 @@ export function protocolState(state: DeveloperState): ProtocolState {
|
|
|
128
153
|
if (
|
|
129
154
|
snapshot.hasTag("blocks-direct") ||
|
|
130
155
|
state.pendingQuestions.some((question) => question.status === "blocked")
|
|
131
|
-
)
|
|
132
|
-
|
|
156
|
+
)
|
|
157
|
+
return "blocked";
|
|
158
|
+
if (state.pendingQuestions.some((question) => question.resolutionOwner === "user"))
|
|
159
|
+
return "needs-answer";
|
|
133
160
|
if (snapshot.matches({ questions: "open" })) return "needs-evidence";
|
|
134
161
|
if (snapshot.hasTag("reroute-required")) return "needs-routing";
|
|
135
162
|
if (snapshot.hasTag("verification-required")) return "needs-verification";
|
|
@@ -149,7 +176,12 @@ function isMode(value: unknown): value is DeveloperMode {
|
|
|
149
176
|
}
|
|
150
177
|
|
|
151
178
|
function isJudgmentStatus(value: unknown): value is JudgmentStatus {
|
|
152
|
-
return
|
|
179
|
+
return (
|
|
180
|
+
value === "resolved" ||
|
|
181
|
+
value === "needs-evidence" ||
|
|
182
|
+
value === "not-applicable" ||
|
|
183
|
+
value === "blocked"
|
|
184
|
+
);
|
|
153
185
|
}
|
|
154
186
|
|
|
155
187
|
function isDirectExecutionProfile(value: unknown): value is DirectExecutionProfile {
|
|
@@ -188,7 +220,78 @@ function isQuestionGate(value: unknown): value is QuestionGate {
|
|
|
188
220
|
}
|
|
189
221
|
|
|
190
222
|
function isQuestionUpdateStatus(value: unknown): value is QuestionUpdateStatus {
|
|
191
|
-
return
|
|
223
|
+
return (
|
|
224
|
+
value === "resolved" || value === "not-applicable" || value === "open" || value === "blocked"
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const RESPONSE_IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
229
|
+
|
|
230
|
+
function requiredResponseText(value: unknown, maxChars: number): string | undefined {
|
|
231
|
+
if (typeof value !== "string") return undefined;
|
|
232
|
+
const text = value.trim();
|
|
233
|
+
return text && text.length <= maxChars ? text : undefined;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function optionalResponseText(value: unknown): string | undefined | null {
|
|
237
|
+
if (value === undefined) return undefined;
|
|
238
|
+
return requiredResponseText(value, MAX_RESPONSE_TEXT_CHARS) ?? null;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function parseChoiceResponseOption(
|
|
242
|
+
value: unknown,
|
|
243
|
+
seenValues: Set<string>,
|
|
244
|
+
): ChoiceResponseOption | undefined {
|
|
245
|
+
if (!isObject(value)) return undefined;
|
|
246
|
+
const optionValue = requiredResponseText(value.value, MAX_RESPONSE_IDENTIFIER_CHARS);
|
|
247
|
+
const label = requiredResponseText(value.label, MAX_RESPONSE_TEXT_CHARS);
|
|
248
|
+
const description = optionalResponseText(value.description);
|
|
249
|
+
const detailPrompt = optionalResponseText(value.detailPrompt);
|
|
250
|
+
if (!optionValue || !RESPONSE_IDENTIFIER.test(optionValue)) return undefined;
|
|
251
|
+
if (seenValues.has(optionValue) || !label) return undefined;
|
|
252
|
+
if (description === null || detailPrompt === null) return undefined;
|
|
253
|
+
seenValues.add(optionValue);
|
|
254
|
+
return { value: optionValue, label, description, detailPrompt };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function parseChoiceResponseField(
|
|
258
|
+
value: unknown,
|
|
259
|
+
seenIds: Set<string>,
|
|
260
|
+
): ChoiceResponseField | undefined {
|
|
261
|
+
if (!isObject(value) || !Array.isArray(value.options)) return undefined;
|
|
262
|
+
const id = requiredResponseText(value.id, MAX_RESPONSE_IDENTIFIER_CHARS);
|
|
263
|
+
const prompt = requiredResponseText(value.prompt, MAX_RESPONSE_TEXT_CHARS);
|
|
264
|
+
const description = optionalResponseText(value.description);
|
|
265
|
+
if (!id || !RESPONSE_IDENTIFIER.test(id)) return undefined;
|
|
266
|
+
if (seenIds.has(id) || !prompt || description === null) return undefined;
|
|
267
|
+
if (value.options.length < 2 || value.options.length > MAX_RESPONSE_OPTIONS) return undefined;
|
|
268
|
+
|
|
269
|
+
seenIds.add(id);
|
|
270
|
+
const seenValues = new Set<string>();
|
|
271
|
+
const options: ChoiceResponseOption[] = [];
|
|
272
|
+
for (const rawOption of value.options) {
|
|
273
|
+
const option = parseChoiceResponseOption(rawOption, seenValues);
|
|
274
|
+
if (!option) return undefined;
|
|
275
|
+
options.push(option);
|
|
276
|
+
}
|
|
277
|
+
return { id, prompt, description, options };
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
export function parseChoiceResponseSpec(value: unknown): ChoiceResponseSpec | undefined {
|
|
281
|
+
if (!isObject(value) || value.kind !== "choice-form" || !Array.isArray(value.fields)) {
|
|
282
|
+
return undefined;
|
|
283
|
+
}
|
|
284
|
+
if (value.fields.length === 0 || value.fields.length > MAX_RESPONSE_FIELDS) {
|
|
285
|
+
return undefined;
|
|
286
|
+
}
|
|
287
|
+
const seenIds = new Set<string>();
|
|
288
|
+
const fields: ChoiceResponseField[] = [];
|
|
289
|
+
for (const rawField of value.fields) {
|
|
290
|
+
const field = parseChoiceResponseField(rawField, seenIds);
|
|
291
|
+
if (!field) return undefined;
|
|
292
|
+
fields.push(field);
|
|
293
|
+
}
|
|
294
|
+
return { kind: "choice-form", fields };
|
|
192
295
|
}
|
|
193
296
|
|
|
194
297
|
function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
|
|
@@ -210,9 +313,14 @@ function parsePendingQuestion(value: unknown): PendingQuestion | undefined {
|
|
|
210
313
|
typeof value.resolutionCriteria === "string"
|
|
211
314
|
? value.resolutionCriteria
|
|
212
315
|
: `Obtain evidence that settles: ${value.question}`;
|
|
316
|
+
const context = typeof value.context === "string" ? value.context.trim() || undefined : undefined;
|
|
317
|
+
const responseSpec =
|
|
318
|
+
resolutionOwner === "user" ? parseChoiceResponseSpec(value.responseSpec) : undefined;
|
|
213
319
|
return {
|
|
214
320
|
id: value.id,
|
|
215
321
|
question: value.question,
|
|
322
|
+
context,
|
|
323
|
+
responseSpec,
|
|
216
324
|
status: legacyBlocked ? "blocked" : "open",
|
|
217
325
|
resolutionOwner,
|
|
218
326
|
gate,
|
|
@@ -246,7 +354,8 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
246
354
|
value.protocol !== PREVIOUS_PROTOCOL &&
|
|
247
355
|
value.protocol !== OLDER_PROTOCOL &&
|
|
248
356
|
value.protocol !== LEGACY_PROTOCOL
|
|
249
|
-
)
|
|
357
|
+
)
|
|
358
|
+
return undefined;
|
|
250
359
|
|
|
251
360
|
if (value.kind === "mode") {
|
|
252
361
|
if (!isMode(value.mode)) return undefined;
|
|
@@ -257,7 +366,8 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
257
366
|
if (
|
|
258
367
|
(value.protocol !== PROTOCOL && value.protocol !== PREVIOUS_PROTOCOL) ||
|
|
259
368
|
typeof value.questionId !== "string"
|
|
260
|
-
)
|
|
369
|
+
)
|
|
370
|
+
return undefined;
|
|
261
371
|
return { protocol: PROTOCOL, kind: "focus", questionId: value.questionId };
|
|
262
372
|
}
|
|
263
373
|
|
|
@@ -268,7 +378,8 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
268
378
|
typeof value.owner !== "string" ||
|
|
269
379
|
typeof value.reason !== "string" ||
|
|
270
380
|
!isStringArray(value.knownEvidence) ||
|
|
271
|
-
(value.consideredAlternatives !== undefined &&
|
|
381
|
+
(value.consideredAlternatives !== undefined &&
|
|
382
|
+
!Array.isArray(value.consideredAlternatives)) ||
|
|
272
383
|
(value.targetQuestionId !== undefined && typeof value.targetQuestionId !== "string") ||
|
|
273
384
|
(value.methodLocation !== undefined && typeof value.methodLocation !== "string") ||
|
|
274
385
|
(value.executionProfile !== undefined && !isDirectExecutionProfile(value.executionProfile)) ||
|
|
@@ -338,7 +449,8 @@ export function normalizeDeveloperEvent(value: unknown): DeveloperEvent | undefi
|
|
|
338
449
|
if (!Array.isArray(value.openedQuestions)) return undefined;
|
|
339
450
|
const openedQuestions = value.openedQuestions.map(parsePendingQuestion);
|
|
340
451
|
if (openedQuestions.some((question) => !question)) return undefined;
|
|
341
|
-
if (value.questionUpdates !== undefined && !Array.isArray(value.questionUpdates))
|
|
452
|
+
if (value.questionUpdates !== undefined && !Array.isArray(value.questionUpdates))
|
|
453
|
+
return undefined;
|
|
342
454
|
const questionUpdates = Array.isArray(value.questionUpdates)
|
|
343
455
|
? value.questionUpdates.map(parseQuestionUpdate)
|
|
344
456
|
: [];
|