@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.
@@ -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
- ) return "blocked";
132
- if (state.pendingQuestions.some((question) => question.resolutionOwner === "user")) return "needs-answer";
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 value === "resolved" || value === "needs-evidence" || value === "not-applicable" || value === "blocked";
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 value === "resolved" || value === "not-applicable" || value === "open" || value === "blocked";
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
- ) return undefined;
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
- ) return undefined;
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 && !Array.isArray(value.consideredAlternatives)) ||
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)) return undefined;
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
  : [];