@cosmicdrift/kumiko-framework 0.215.4 → 0.215.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.
@@ -0,0 +1,366 @@
1
+ import type { CallExpression, Node, ObjectLiteralExpression, SourceFile } from "ts-morph";
2
+ import { SyntaxKind } from "ts-morph";
3
+ import type {
4
+ AiClassifyPattern,
5
+ AiExtractPattern,
6
+ AiGeneratePattern,
7
+ AiStepOpaqueArgs,
8
+ AiStepPolicy,
9
+ } from "../patterns";
10
+ import type { SourceLocation } from "../source-location";
11
+ import { sourceLocationFromNode } from "../source-location";
12
+ import {
13
+ type ExtractOutput,
14
+ fail,
15
+ findFunctionLiteral,
16
+ isPlainObject,
17
+ isRawRefSentinel,
18
+ ok,
19
+ readDataLiteralNode,
20
+ readStringOrRaw,
21
+ } from "./shared";
22
+
23
+ type AiStepKind = AiGeneratePattern["kind"] | AiExtractPattern["kind"] | AiClassifyPattern["kind"];
24
+
25
+ type AiStepCommonExtracted = {
26
+ readonly source: SourceLocation;
27
+ readonly argsSource?: AiStepOpaqueArgs;
28
+ readonly stepKey?: string | AiStepOpaqueArgs;
29
+ readonly promptKey?: string | AiStepOpaqueArgs;
30
+ readonly promptFallback?: string | AiStepOpaqueArgs;
31
+ readonly defaults?: AiStepPolicy | AiStepOpaqueArgs;
32
+ readonly paramsSchemaSource?: SourceLocation;
33
+ };
34
+
35
+ function resolveObjectLiteralArg(node: Node): ObjectLiteralExpression | undefined {
36
+ const direct = node.asKind(SyntaxKind.ObjectLiteralExpression);
37
+ if (direct) return direct;
38
+ const identifier = node.asKind(SyntaxKind.Identifier);
39
+ if (!identifier) return undefined;
40
+ const varDecl = node.getSourceFile().getVariableDeclaration(identifier.getText());
41
+ return varDecl?.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
42
+ }
43
+
44
+ function readPropertyInitializer(
45
+ obj: ObjectLiteralExpression,
46
+ propertyName: string,
47
+ ): import("ts-morph").Expression | undefined {
48
+ const prop = obj.getProperty(propertyName);
49
+ if (!prop) return undefined;
50
+ const assign = prop.asKind(SyntaxKind.PropertyAssignment);
51
+ if (assign) return assign.getInitializer();
52
+ const shorthand = prop.asKind(SyntaxKind.ShorthandPropertyAssignment);
53
+ if (shorthand) return shorthand.getNameNode();
54
+ return undefined;
55
+ }
56
+
57
+ function readEditableStringProp(
58
+ obj: ObjectLiteralExpression,
59
+ propertyName: string,
60
+ ): string | AiStepOpaqueArgs | undefined {
61
+ const init = readPropertyInitializer(obj, propertyName);
62
+ if (!init) return undefined;
63
+ const value = readStringOrRaw(init);
64
+ if (value === undefined) return undefined;
65
+ return value;
66
+ }
67
+
68
+ function readEditableDefaults(
69
+ node: import("ts-morph").Expression,
70
+ ): AiStepPolicy | AiStepOpaqueArgs | undefined {
71
+ const value = readDataLiteralNode(node);
72
+ if (isRawRefSentinel(value)) return value;
73
+ if (!isPlainObject(value)) return undefined;
74
+ if (typeof value["enabled"] !== "boolean") return undefined;
75
+ if (!isPlainObject(value["params"])) return undefined;
76
+ return {
77
+ enabled: value["enabled"],
78
+ params: value["params"] as Record<string, unknown>,
79
+ ...(typeof value["providerId"] === "string" && { providerId: value["providerId"] }),
80
+ ...(typeof value["model"] === "string" && { model: value["model"] }),
81
+ };
82
+ }
83
+
84
+ function readClassifyActions(
85
+ node: import("ts-morph").Expression,
86
+ ): readonly { readonly type: string; readonly description: string }[] | undefined {
87
+ const value = readDataLiteralNode(node);
88
+ if (!Array.isArray(value)) return undefined;
89
+ const out: { type: string; description: string }[] = [];
90
+ for (const entry of value) {
91
+ if (!isPlainObject(entry)) return undefined;
92
+ if (typeof entry["type"] !== "string" || typeof entry["description"] !== "string") {
93
+ return undefined;
94
+ }
95
+ out.push({ type: entry["type"], description: entry["description"] });
96
+ }
97
+ return out;
98
+ }
99
+
100
+ function extractAiStepCommon(
101
+ call: CallExpression,
102
+ sourceFile: SourceFile,
103
+ kind: AiStepKind,
104
+ ): ExtractOutput<AiStepCommonExtracted> {
105
+ const source = sourceLocationFromNode(call, sourceFile);
106
+ const arg = call.getArguments()[0];
107
+ if (!arg) {
108
+ return fail(kind, source, "expected one argument object");
109
+ }
110
+
111
+ const obj = resolveObjectLiteralArg(arg);
112
+ if (!obj) {
113
+ if (isRawRefSentinel(readDataLiteralNode(arg))) {
114
+ return ok({
115
+ source,
116
+ argsSource: { __raw: arg.getText() },
117
+ });
118
+ }
119
+ return fail(
120
+ kind,
121
+ source,
122
+ "argument must be an inline object literal or a same-file const resolving to one",
123
+ );
124
+ }
125
+
126
+ const stepKey = readEditableStringProp(obj, "stepKey");
127
+ if (stepKey === undefined) {
128
+ return fail(kind, source, "missing `stepKey` property");
129
+ }
130
+ const promptKey = readEditableStringProp(obj, "promptKey");
131
+ if (promptKey === undefined) {
132
+ return fail(kind, source, "missing `promptKey` property");
133
+ }
134
+ const promptFallbackInit = readPropertyInitializer(obj, "promptFallback");
135
+ if (!promptFallbackInit) {
136
+ return fail(kind, source, "missing `promptFallback` property");
137
+ }
138
+ const promptFallback = readStringOrRaw(promptFallbackInit);
139
+ if (promptFallback === undefined) {
140
+ return fail(kind, source, "`promptFallback` must be a string literal or identifier reference");
141
+ }
142
+
143
+ const defaultsInit = readPropertyInitializer(obj, "defaults");
144
+ if (!defaultsInit) {
145
+ return fail(kind, source, "missing `defaults` property");
146
+ }
147
+ const defaults = readEditableDefaults(defaultsInit);
148
+ if (defaults === undefined) {
149
+ return fail(kind, source, "`defaults` could not be read as a StepPolicy literal");
150
+ }
151
+
152
+ const paramsSchemaInit = readPropertyInitializer(obj, "paramsSchema");
153
+ if (!paramsSchemaInit) {
154
+ return fail(kind, source, "missing `paramsSchema` property");
155
+ }
156
+
157
+ return ok({
158
+ source,
159
+ stepKey,
160
+ promptKey,
161
+ promptFallback,
162
+ defaults,
163
+ paramsSchemaSource: sourceLocationFromNode(paramsSchemaInit, sourceFile),
164
+ });
165
+ }
166
+
167
+ function requireResolvedCommon(
168
+ kind: AiStepKind,
169
+ pattern: AiStepCommonExtracted,
170
+ ): ExtractOutput<
171
+ Required<
172
+ Pick<
173
+ AiStepCommonExtracted,
174
+ "stepKey" | "promptKey" | "promptFallback" | "defaults" | "paramsSchemaSource"
175
+ >
176
+ > & { readonly source: SourceLocation }
177
+ > {
178
+ if (pattern.argsSource) {
179
+ return fail(kind, pattern.source, "expected resolved inline object, got opaque args reference");
180
+ }
181
+ const { stepKey, promptKey, promptFallback, defaults, paramsSchemaSource, source } = pattern;
182
+ if (
183
+ stepKey === undefined ||
184
+ promptKey === undefined ||
185
+ promptFallback === undefined ||
186
+ defaults === undefined ||
187
+ paramsSchemaSource === undefined
188
+ ) {
189
+ return fail(kind, source, "resolved AI step is missing required header fields");
190
+ }
191
+ return ok({ source, stepKey, promptKey, promptFallback, defaults, paramsSchemaSource });
192
+ }
193
+
194
+ export function extractAiGenerate(
195
+ call: CallExpression,
196
+ sourceFile: SourceFile,
197
+ ): ExtractOutput<AiGeneratePattern> {
198
+ const common = extractAiStepCommon(call, sourceFile, "ai.generate");
199
+ if (common.kind === "error") return common;
200
+ if (common.pattern.argsSource) {
201
+ return ok({
202
+ kind: "ai.generate",
203
+ source: common.pattern.source,
204
+ argsSource: common.pattern.argsSource,
205
+ });
206
+ }
207
+
208
+ const arg = call.getArguments()[0];
209
+ const obj = arg ? resolveObjectLiteralArg(arg) : undefined;
210
+ if (!obj) {
211
+ return fail("ai.generate", common.pattern.source, "expected resolvable argument object");
212
+ }
213
+ const inputInit = readPropertyInitializer(obj, "input");
214
+ if (!inputInit) {
215
+ return fail("ai.generate", common.pattern.source, "missing `input` property");
216
+ }
217
+ const fn = findFunctionLiteral(inputInit);
218
+ if (!fn) {
219
+ return fail(
220
+ "ai.generate",
221
+ common.pattern.source,
222
+ "`input` must be an inline arrow function or function expression",
223
+ );
224
+ }
225
+
226
+ const resolved = requireResolvedCommon("ai.generate", common.pattern);
227
+ if (resolved.kind === "error") return resolved;
228
+ return ok({
229
+ kind: "ai.generate",
230
+ source: resolved.pattern.source,
231
+ stepKey: resolved.pattern.stepKey,
232
+ promptKey: resolved.pattern.promptKey,
233
+ promptFallback: resolved.pattern.promptFallback,
234
+ defaults: resolved.pattern.defaults,
235
+ paramsSchemaSource: resolved.pattern.paramsSchemaSource,
236
+ inputBody: sourceLocationFromNode(fn, sourceFile),
237
+ });
238
+ }
239
+
240
+ export function extractAiExtract(
241
+ call: CallExpression,
242
+ sourceFile: SourceFile,
243
+ ): ExtractOutput<AiExtractPattern> {
244
+ const common = extractAiStepCommon(call, sourceFile, "ai.extract");
245
+ if (common.kind === "error") return common;
246
+ if (common.pattern.argsSource) {
247
+ return ok({
248
+ kind: "ai.extract",
249
+ source: common.pattern.source,
250
+ argsSource: common.pattern.argsSource,
251
+ });
252
+ }
253
+
254
+ const arg = call.getArguments()[0];
255
+ const obj = arg ? resolveObjectLiteralArg(arg) : undefined;
256
+ if (!obj) {
257
+ return fail("ai.extract", common.pattern.source, "expected resolvable argument object");
258
+ }
259
+
260
+ const outputSchemaInit = readPropertyInitializer(obj, "outputSchema");
261
+ if (!outputSchemaInit) {
262
+ return fail("ai.extract", common.pattern.source, "missing `outputSchema` property");
263
+ }
264
+ const instructionsInit = readPropertyInitializer(obj, "instructions");
265
+ if (!instructionsInit) {
266
+ return fail("ai.extract", common.pattern.source, "missing `instructions` property");
267
+ }
268
+ const instructionsFn = findFunctionLiteral(instructionsInit);
269
+ if (!instructionsFn) {
270
+ return fail(
271
+ "ai.extract",
272
+ common.pattern.source,
273
+ "`instructions` must be an inline arrow function or function expression",
274
+ );
275
+ }
276
+
277
+ const documentInit = readPropertyInitializer(obj, "document");
278
+ let documentBody: SourceLocation | undefined;
279
+ if (documentInit) {
280
+ const documentFn = findFunctionLiteral(documentInit);
281
+ if (!documentFn) {
282
+ return fail(
283
+ "ai.extract",
284
+ common.pattern.source,
285
+ "`document` must be an inline arrow function or function expression",
286
+ );
287
+ }
288
+ documentBody = sourceLocationFromNode(documentFn, sourceFile);
289
+ }
290
+
291
+ const resolved = requireResolvedCommon("ai.extract", common.pattern);
292
+ if (resolved.kind === "error") return resolved;
293
+ return ok({
294
+ kind: "ai.extract",
295
+ source: resolved.pattern.source,
296
+ stepKey: resolved.pattern.stepKey,
297
+ promptKey: resolved.pattern.promptKey,
298
+ promptFallback: resolved.pattern.promptFallback,
299
+ defaults: resolved.pattern.defaults,
300
+ paramsSchemaSource: resolved.pattern.paramsSchemaSource,
301
+ outputSchemaSource: sourceLocationFromNode(outputSchemaInit, sourceFile),
302
+ instructionsBody: sourceLocationFromNode(instructionsFn, sourceFile),
303
+ ...(documentBody !== undefined && { documentBody }),
304
+ });
305
+ }
306
+
307
+ export function extractAiClassify(
308
+ call: CallExpression,
309
+ sourceFile: SourceFile,
310
+ ): ExtractOutput<AiClassifyPattern> {
311
+ const common = extractAiStepCommon(call, sourceFile, "ai.classify");
312
+ if (common.kind === "error") return common;
313
+ if (common.pattern.argsSource) {
314
+ return ok({
315
+ kind: "ai.classify",
316
+ source: common.pattern.source,
317
+ argsSource: common.pattern.argsSource,
318
+ });
319
+ }
320
+
321
+ const arg = call.getArguments()[0];
322
+ const obj = arg ? resolveObjectLiteralArg(arg) : undefined;
323
+ if (!obj) {
324
+ return fail("ai.classify", common.pattern.source, "expected resolvable argument object");
325
+ }
326
+
327
+ const actionsInit = readPropertyInitializer(obj, "actions");
328
+ if (!actionsInit) {
329
+ return fail("ai.classify", common.pattern.source, "missing `actions` property");
330
+ }
331
+ const actions = readClassifyActions(actionsInit);
332
+ if (actions === undefined) {
333
+ return fail(
334
+ "ai.classify",
335
+ common.pattern.source,
336
+ "`actions` must be an inline array of { type, description } objects",
337
+ );
338
+ }
339
+
340
+ const inputInit = readPropertyInitializer(obj, "input");
341
+ if (!inputInit) {
342
+ return fail("ai.classify", common.pattern.source, "missing `input` property");
343
+ }
344
+ const inputFn = findFunctionLiteral(inputInit);
345
+ if (!inputFn) {
346
+ return fail(
347
+ "ai.classify",
348
+ common.pattern.source,
349
+ "`input` must be an inline arrow function or function expression",
350
+ );
351
+ }
352
+
353
+ const resolved = requireResolvedCommon("ai.classify", common.pattern);
354
+ if (resolved.kind === "error") return resolved;
355
+ return ok({
356
+ kind: "ai.classify",
357
+ source: resolved.pattern.source,
358
+ stepKey: resolved.pattern.stepKey,
359
+ promptKey: resolved.pattern.promptKey,
360
+ promptFallback: resolved.pattern.promptFallback,
361
+ defaults: resolved.pattern.defaults,
362
+ paramsSchemaSource: resolved.pattern.paramsSchemaSource,
363
+ actions,
364
+ inputBody: sourceLocationFromNode(inputFn, sourceFile),
365
+ });
366
+ }
@@ -1,3 +1,8 @@
1
+ export {
2
+ extractAiClassify,
3
+ extractAiExtract,
4
+ extractAiGenerate,
5
+ } from "./ai-steps";
1
6
  export {
2
7
  extractDefineEvent,
3
8
  extractNotification,
@@ -24,7 +24,9 @@
24
24
  import type {
25
25
  ArrowFunction,
26
26
  CallExpression,
27
+ Expression,
27
28
  Node,
29
+ ObjectLiteralExpression,
28
30
  ParameterDeclaration,
29
31
  SourceFile,
30
32
  } from "ts-morph";
@@ -32,6 +34,9 @@ import { Project, SyntaxKind } from "ts-morph";
32
34
 
33
35
  import {
34
36
  type ExtractOutput,
37
+ extractAiClassify,
38
+ extractAiExtract,
39
+ extractAiGenerate,
35
40
  extractAuthClaims,
36
41
  extractClaimKey,
37
42
  extractConfig,
@@ -152,6 +157,8 @@ export function parseSourceFile(sourceFile: SourceFile): ParseResult {
152
157
  const errors: ParseError[] = [];
153
158
 
154
159
  walkSetupCallback(setupCallback.getBody(), registrarParamName, sourceFile, patterns, errors);
160
+ walkAiStepCalls(setupCallback.getBody(), registrarParamName, sourceFile, patterns, errors);
161
+ patterns.sort((a, b) => a.source.start.line - b.source.start.line);
155
162
 
156
163
  return { featureName, patterns, errors };
157
164
  }
@@ -380,6 +387,134 @@ function extractRegistrarMethodName(
380
387
  return propAccess.getName();
381
388
  }
382
389
 
390
+ function readObjectPropertyInitializer(
391
+ obj: ObjectLiteralExpression,
392
+ propertyName: string,
393
+ ): Expression | undefined {
394
+ const prop = obj.getProperty(propertyName);
395
+ if (!prop) return undefined;
396
+ const assign = prop.asKind(SyntaxKind.PropertyAssignment);
397
+ if (assign) return assign.getInitializer();
398
+ const shorthand = prop.asKind(SyntaxKind.ShorthandPropertyAssignment);
399
+ if (shorthand) return shorthand.getNameNode();
400
+ return undefined;
401
+ }
402
+
403
+ function resolveSameFileObjectLiteralArg(node: Node): ObjectLiteralExpression | undefined {
404
+ const direct = node.asKind(SyntaxKind.ObjectLiteralExpression);
405
+ if (direct) return direct;
406
+ const identifier = node.asKind(SyntaxKind.Identifier);
407
+ if (!identifier) return undefined;
408
+ const varDecl = node.getSourceFile().getVariableDeclaration(identifier.getText());
409
+ return varDecl?.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
410
+ }
411
+
412
+ function resolveStepsArrayRoot(stepsInit: Expression): Node | undefined {
413
+ const directArray = stepsInit.asKind(SyntaxKind.ArrayLiteralExpression);
414
+ if (directArray) return directArray;
415
+
416
+ const pipelineCall = stepsInit.asKind(SyntaxKind.CallExpression);
417
+ if (pipelineCall?.getExpression().getText() !== "stepsPipeline") {
418
+ return undefined;
419
+ }
420
+
421
+ const closureArg = pipelineCall.getArguments()[0];
422
+ if (!closureArg) return undefined;
423
+ const fn = findFunctionLiteral(closureArg);
424
+ if (!fn) return undefined;
425
+
426
+ const fnBody =
427
+ fn.asKind(SyntaxKind.ArrowFunction)?.getBody() ??
428
+ fn.asKind(SyntaxKind.FunctionExpression)?.getBody();
429
+ const exprBody = fnBody?.asKind(SyntaxKind.ArrayLiteralExpression);
430
+ if (exprBody) return exprBody;
431
+
432
+ if (fnBody?.isKind(SyntaxKind.Block)) {
433
+ for (const stmt of fnBody.getStatements()) {
434
+ const ret = stmt.asKind(SyntaxKind.ReturnStatement);
435
+ const retArray = ret?.getExpression()?.asKind(SyntaxKind.ArrayLiteralExpression);
436
+ if (retArray) return retArray;
437
+ }
438
+ }
439
+
440
+ return undefined;
441
+ }
442
+
443
+ function collectWorkflowStepArrayRoots(body: Node): Node[] {
444
+ const roots: Node[] = [];
445
+ for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
446
+ if (call.getExpression().getText() !== "defineWorkflow") continue;
447
+ const obj = resolveSameFileObjectLiteralArg(call.getArguments()[0] ?? call);
448
+ if (!obj) continue;
449
+ const stepsInit = readObjectPropertyInitializer(obj, "steps");
450
+ if (!stepsInit) continue;
451
+ const arrayRoot = resolveStepsArrayRoot(stepsInit);
452
+ if (arrayRoot) roots.push(arrayRoot);
453
+ }
454
+ return roots;
455
+ }
456
+
457
+ function walkAiStepCallsInNode(
458
+ node: Node,
459
+ sourceFile: SourceFile,
460
+ patterns: FeaturePattern[],
461
+ errors: ParseError[],
462
+ ): void {
463
+ for (const call of node.getDescendantsOfKind(SyntaxKind.CallExpression)) {
464
+ const callee = call.getExpression().getText();
465
+ switch (callee) {
466
+ case "aiGenerateStep": {
467
+ const result = extractAiGenerate(call, sourceFile);
468
+ if (result.kind === "pattern") patterns.push(result.pattern);
469
+ else errors.push(result.error);
470
+ break;
471
+ }
472
+ case "aiExtractStep": {
473
+ const result = extractAiExtract(call, sourceFile);
474
+ if (result.kind === "pattern") patterns.push(result.pattern);
475
+ else errors.push(result.error);
476
+ break;
477
+ }
478
+ case "aiClassifyStep": {
479
+ const result = extractAiClassify(call, sourceFile);
480
+ if (result.kind === "pattern") patterns.push(result.pattern);
481
+ else errors.push(result.error);
482
+ break;
483
+ }
484
+ default:
485
+ break;
486
+ }
487
+ }
488
+ }
489
+
490
+ function walkAiStepCalls(
491
+ body: Node,
492
+ registrarParamName: string,
493
+ sourceFile: SourceFile,
494
+ patterns: FeaturePattern[],
495
+ errors: ParseError[],
496
+ visitedWrapperDecls: Set<Node> = new Set(),
497
+ ): void {
498
+ for (const arrayRoot of collectWorkflowStepArrayRoots(body)) {
499
+ walkAiStepCallsInNode(arrayRoot, sourceFile, patterns, errors);
500
+ }
501
+
502
+ for (const call of body.getDescendantsOfKind(SyntaxKind.CallExpression)) {
503
+ const wrapper = resolveRegistrarWrapperCall(call, registrarParamName);
504
+ if (!wrapper) continue;
505
+ if (visitedWrapperDecls.has(wrapper.declNode)) continue;
506
+ visitedWrapperDecls.add(wrapper.declNode);
507
+ walkAiStepCalls(
508
+ wrapper.body,
509
+ wrapper.paramName,
510
+ wrapper.sourceFile,
511
+ patterns,
512
+ errors,
513
+ visitedWrapperDecls,
514
+ );
515
+ }
516
+ }
517
+
383
518
  // =============================================================================
384
519
  // Internal — pattern extractors (skeleton, implementation in C1.5)
385
520
  // =============================================================================