@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.
- package/package.json +3 -3
- package/src/__tests__/store-table.integration.test.ts +1 -1
- package/src/bun-db/__tests__/coerce-row-plain-date.test.ts +0 -2
- package/src/bun-db/__tests__/coerce-row-temporal.test.ts +1 -2
- package/src/db/__tests__/multi-row-insert.integration.test.ts +3 -1
- package/src/db/__tests__/schema-migration.integration.test.ts +0 -1
- package/src/db/__tests__/source-shadow-create.integration.test.ts +3 -1
- package/src/engine/feature-ast/__tests__/parse.test.ts +222 -5
- package/src/engine/feature-ast/__tests__/patch.test.ts +58 -0
- package/src/engine/feature-ast/__tests__/render-roundtrip.test.ts +76 -1
- package/src/engine/feature-ast/extractors/ai-steps.ts +366 -0
- package/src/engine/feature-ast/extractors/index.ts +5 -0
- package/src/engine/feature-ast/parse.ts +135 -0
- package/src/engine/feature-ast/patch.ts +99 -16
- package/src/engine/feature-ast/patterns.ts +51 -0
- package/src/engine/feature-ast/render.ts +71 -0
- package/src/engine/pattern-library/__tests__/library.test.ts +39 -0
- package/src/engine/pattern-library/library.ts +6 -0
- package/src/engine/pattern-library/mixed-schemas.ts +103 -1
- package/src/files/__tests__/storage-tracking.integration.test.ts +0 -2
- package/src/lifecycle/signal-handlers.ts +2 -2
- package/src/db/__tests__/sql-inventory.test.ts +0 -81
- package/src/db/sql-inventory.ts +0 -232
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
// roadmap C-Notes for the canonical-comment-attach Pattern that would
|
|
31
31
|
// preserve prefixed `// kumiko-comment:` markers across roundtrips.
|
|
32
32
|
|
|
33
|
+
import type { ObjectLiteralExpression } from "ts-morph";
|
|
33
34
|
import { type CallExpression, type Node, type SourceFile, SyntaxKind } from "ts-morph";
|
|
34
35
|
import { readNameLiteral, readNameOrRef } from "./extractors/shared";
|
|
35
36
|
import type { FeaturePattern, FeaturePatternKind } from "./patterns";
|
|
@@ -72,6 +73,9 @@ export type PatternId =
|
|
|
72
73
|
| { readonly kind: "multiStreamProjection"; readonly name: string }
|
|
73
74
|
| { readonly kind: "defineEvent"; readonly eventName: string }
|
|
74
75
|
| { readonly kind: "extendsRegistrar"; readonly extensionName: string }
|
|
76
|
+
| { readonly kind: "ai.generate"; readonly stepKey: string }
|
|
77
|
+
| { readonly kind: "ai.extract"; readonly stepKey: string }
|
|
78
|
+
| { readonly kind: "ai.classify"; readonly stepKey: string }
|
|
75
79
|
// Singleton patterns — only one per feature, kind alone identifies them.
|
|
76
80
|
| { readonly kind: "requires" }
|
|
77
81
|
| { readonly kind: "optionalRequires" }
|
|
@@ -188,10 +192,10 @@ export function replacePattern(
|
|
|
188
192
|
// Whole call-statement spans from the CallExpression's start through
|
|
189
193
|
// its enclosing ExpressionStatement (which carries the trailing `;`).
|
|
190
194
|
const enclosingStatement = call.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
|
|
191
|
-
const startNode = enclosingStatement ?? call;
|
|
195
|
+
const startNode = isAiStepId(id) ? call : (enclosingStatement ?? call);
|
|
192
196
|
|
|
193
197
|
const startPos = startNode.getStart();
|
|
194
|
-
const endPos = startNode.getEnd();
|
|
198
|
+
const endPos = isAiStepId(id) ? aiStepPatchSpan(sourceFile, call).end : startNode.getEnd();
|
|
195
199
|
|
|
196
200
|
// Detect column of the original call's first non-whitespace character;
|
|
197
201
|
// the rendered pattern starts at column 0 and gets indented to match.
|
|
@@ -219,26 +223,51 @@ export function removePattern(sourceFile: SourceFile, id: PatternId): void {
|
|
|
219
223
|
if (!call) {
|
|
220
224
|
throw new Error(`removePattern: no call found for ${describeId(id)}`);
|
|
221
225
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
226
|
+
if (isAiStepId(id)) {
|
|
227
|
+
const { start, end } = aiStepPatchSpan(sourceFile, call);
|
|
228
|
+
sourceFile.replaceText([start, end], "");
|
|
229
|
+
} else {
|
|
230
|
+
const enclosingStatement = call.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
|
|
231
|
+
const target = enclosingStatement ?? call;
|
|
232
|
+
|
|
233
|
+
// Erase from the start of the line containing the statement (so leading
|
|
234
|
+
// indentation goes with it) through the trailing newline, including the
|
|
235
|
+
// *leading* blank line that addPattern emits — keeps blank-line counts
|
|
236
|
+
// stable under add → remove cycles. We don't touch leading comments.
|
|
237
|
+
const startPos = lineStart(sourceFile, target.getStart());
|
|
238
|
+
const endPos = lineEnd(sourceFile, target.getEnd());
|
|
239
|
+
|
|
240
|
+
// Collapse a preceding blank line if there is one (avoids a double
|
|
241
|
+
// blank line between the now-adjacent statements).
|
|
242
|
+
const collapseStart = collapsePrecedingBlankLine(sourceFile, startPos);
|
|
243
|
+
sourceFile.replaceText([collapseStart, endPos + 1], "");
|
|
244
|
+
}
|
|
236
245
|
}
|
|
237
246
|
|
|
238
247
|
// =============================================================================
|
|
239
248
|
// Lookup
|
|
240
249
|
// =============================================================================
|
|
241
250
|
|
|
251
|
+
function isAiStepId(
|
|
252
|
+
id: PatternId,
|
|
253
|
+
): id is Extract<PatternId, { kind: "ai.generate" | "ai.extract" | "ai.classify" }> {
|
|
254
|
+
return id.kind === "ai.generate" || id.kind === "ai.extract" || id.kind === "ai.classify";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function aiStepPatchSpan(
|
|
258
|
+
sourceFile: SourceFile,
|
|
259
|
+
call: CallExpression,
|
|
260
|
+
): { start: number; end: number } {
|
|
261
|
+
const start = call.getStart();
|
|
262
|
+
let end = call.getEnd();
|
|
263
|
+
const text = sourceFile.getFullText();
|
|
264
|
+
while (end < text.length && (text[end] === " " || text[end] === " ")) end++;
|
|
265
|
+
if (text[end] === ",") end++;
|
|
266
|
+
while (end < text.length && (text[end] === " " || text[end] === " ")) end++;
|
|
267
|
+
if (end < text.length && text[end] === "\n") end++;
|
|
268
|
+
return { start, end };
|
|
269
|
+
}
|
|
270
|
+
|
|
242
271
|
function findSetupCallback(
|
|
243
272
|
sourceFile: SourceFile,
|
|
244
273
|
): { call: CallExpression; body: Node } | undefined {
|
|
@@ -293,6 +322,9 @@ export const SINGLETON_KINDS: ReadonlySet<PatternId["kind"]> = new Set([
|
|
|
293
322
|
* feature can fix it explicitly.
|
|
294
323
|
*/
|
|
295
324
|
function findCallForId(sourceFile: SourceFile, id: PatternId): CallExpression | undefined {
|
|
325
|
+
if (id.kind === "ai.generate" || id.kind === "ai.extract" || id.kind === "ai.classify") {
|
|
326
|
+
return findAiStepCall(sourceFile, id);
|
|
327
|
+
}
|
|
296
328
|
const setup = findSetupCallback(sourceFile);
|
|
297
329
|
if (!setup) return undefined;
|
|
298
330
|
const registrarParam = setup.call
|
|
@@ -319,6 +351,53 @@ function findCallForId(sourceFile: SourceFile, id: PatternId): CallExpression |
|
|
|
319
351
|
return matches[0];
|
|
320
352
|
}
|
|
321
353
|
|
|
354
|
+
const AI_STEP_FACTORY: Readonly<Record<"ai.generate" | "ai.extract" | "ai.classify", string>> = {
|
|
355
|
+
"ai.generate": "aiGenerateStep",
|
|
356
|
+
"ai.extract": "aiExtractStep",
|
|
357
|
+
"ai.classify": "aiClassifyStep",
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
function resolveSameFileObjectLiteral(
|
|
361
|
+
node: import("ts-morph").Node,
|
|
362
|
+
): ObjectLiteralExpression | undefined {
|
|
363
|
+
const direct = node.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
364
|
+
if (direct) return direct;
|
|
365
|
+
const identifier = node.asKind(SyntaxKind.Identifier);
|
|
366
|
+
if (!identifier) return undefined;
|
|
367
|
+
const varDecl = node.getSourceFile().getVariableDeclaration(identifier.getText());
|
|
368
|
+
return varDecl?.getInitializer()?.asKind(SyntaxKind.ObjectLiteralExpression);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function readAiStepKey(call: CallExpression): string | undefined {
|
|
372
|
+
const arg = call.getArguments()[0];
|
|
373
|
+
if (!arg) return undefined;
|
|
374
|
+
const obj = resolveSameFileObjectLiteral(arg);
|
|
375
|
+
if (!obj) return undefined;
|
|
376
|
+
const prop = obj.getProperty("stepKey");
|
|
377
|
+
if (!prop) return undefined;
|
|
378
|
+
const assign = prop.asKind(SyntaxKind.PropertyAssignment);
|
|
379
|
+
if (assign) {
|
|
380
|
+
const init = assign.getInitializer();
|
|
381
|
+
if (!init) return undefined;
|
|
382
|
+
return readNameLiteral(init);
|
|
383
|
+
}
|
|
384
|
+
const shorthand = prop.asKind(SyntaxKind.ShorthandPropertyAssignment);
|
|
385
|
+
if (shorthand) return readNameLiteral(shorthand.getNameNode());
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function findAiStepCall(sourceFile: SourceFile, id: PatternId): CallExpression | undefined {
|
|
390
|
+
if (id.kind !== "ai.generate" && id.kind !== "ai.extract" && id.kind !== "ai.classify") {
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
393
|
+
const factory = AI_STEP_FACTORY[id.kind];
|
|
394
|
+
for (const call of sourceFile.getDescendantsOfKind(SyntaxKind.CallExpression)) {
|
|
395
|
+
if (call.getExpression().getText() !== factory) continue;
|
|
396
|
+
if (callMatchesId(call, id)) return call;
|
|
397
|
+
}
|
|
398
|
+
return undefined;
|
|
399
|
+
}
|
|
400
|
+
|
|
322
401
|
function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
323
402
|
switch (id.kind) {
|
|
324
403
|
// Singletons: kind alone identifies the call.
|
|
@@ -421,6 +500,10 @@ function callMatchesId(call: CallExpression, id: PatternId): boolean {
|
|
|
421
500
|
);
|
|
422
501
|
case "extendsRegistrar":
|
|
423
502
|
return matchFirstArgString(call, id.extensionName);
|
|
503
|
+
case "ai.generate":
|
|
504
|
+
case "ai.extract":
|
|
505
|
+
case "ai.classify":
|
|
506
|
+
return readAiStepKey(call) === id.stepKey;
|
|
424
507
|
default: {
|
|
425
508
|
const _exhaustive: never = id;
|
|
426
509
|
return _exhaustive;
|
|
@@ -582,6 +582,51 @@ export type EnvSchemaPattern = {
|
|
|
582
582
|
readonly schemaBody: SourceLocation;
|
|
583
583
|
};
|
|
584
584
|
|
|
585
|
+
/** Live-editable policy defaults for an AI pipeline step instance (#256). */
|
|
586
|
+
export type AiStepPolicy = {
|
|
587
|
+
readonly enabled: boolean;
|
|
588
|
+
readonly providerId?: string;
|
|
589
|
+
readonly model?: string;
|
|
590
|
+
readonly params: Record<string, unknown>;
|
|
591
|
+
};
|
|
592
|
+
|
|
593
|
+
/** Preserves an unresolvable argument reference across parse → render (#998). */
|
|
594
|
+
export type AiStepOpaqueArgs = {
|
|
595
|
+
readonly __raw: string;
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
type AiStepCommonPatternFields = {
|
|
599
|
+
readonly source: SourceLocation;
|
|
600
|
+
/** Whole-args reference when the spec is not an inline/same-file object. */
|
|
601
|
+
readonly argsSource?: AiStepOpaqueArgs;
|
|
602
|
+
readonly stepKey?: string | AiStepOpaqueArgs;
|
|
603
|
+
readonly promptKey?: string | AiStepOpaqueArgs;
|
|
604
|
+
readonly promptFallback?: string | AiStepOpaqueArgs;
|
|
605
|
+
readonly defaults?: AiStepPolicy | AiStepOpaqueArgs;
|
|
606
|
+
readonly paramsSchemaSource?: SourceLocation;
|
|
607
|
+
};
|
|
608
|
+
|
|
609
|
+
// `aiGenerateStep({...})` inside `stepsPipeline` — Tier-3 freeform generation.
|
|
610
|
+
export type AiGeneratePattern = AiStepCommonPatternFields & {
|
|
611
|
+
readonly kind: "ai.generate";
|
|
612
|
+
readonly inputBody?: SourceLocation;
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
// `aiExtractStep({...})` — schema-driven structured extraction.
|
|
616
|
+
export type AiExtractPattern = AiStepCommonPatternFields & {
|
|
617
|
+
readonly kind: "ai.extract";
|
|
618
|
+
readonly outputSchemaSource?: SourceLocation;
|
|
619
|
+
readonly instructionsBody?: SourceLocation;
|
|
620
|
+
readonly documentBody?: SourceLocation;
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// `aiClassifyStep({...})` — action-catalogue classification.
|
|
624
|
+
export type AiClassifyPattern = AiStepCommonPatternFields & {
|
|
625
|
+
readonly kind: "ai.classify";
|
|
626
|
+
readonly actions?: readonly { readonly type: string; readonly description: string }[];
|
|
627
|
+
readonly inputBody?: SourceLocation;
|
|
628
|
+
};
|
|
629
|
+
|
|
585
630
|
// Catch-all — r.* calls the visitor doesn't recognise. Designer renders
|
|
586
631
|
// "unknown call (cannot edit)", AI patcher leaves them unchanged. When
|
|
587
632
|
// an UnknownPattern shows up in the wild it's a signal that a new r.*
|
|
@@ -635,6 +680,9 @@ export type FeaturePattern =
|
|
|
635
680
|
| DefineEventPattern
|
|
636
681
|
| ExtendsRegistrarPattern
|
|
637
682
|
| EnvSchemaPattern
|
|
683
|
+
| AiGeneratePattern
|
|
684
|
+
| AiExtractPattern
|
|
685
|
+
| AiClassifyPattern
|
|
638
686
|
// Catch-all
|
|
639
687
|
| UnknownPattern;
|
|
640
688
|
|
|
@@ -688,6 +736,9 @@ export function getEditability(pattern: FeaturePattern): Editability {
|
|
|
688
736
|
case "projection":
|
|
689
737
|
case "multiStreamProjection":
|
|
690
738
|
case "defineEvent":
|
|
739
|
+
case "ai.generate":
|
|
740
|
+
case "ai.extract":
|
|
741
|
+
case "ai.classify":
|
|
691
742
|
return "mixed";
|
|
692
743
|
case "authClaims":
|
|
693
744
|
case "extendsRegistrar":
|
|
@@ -23,6 +23,11 @@
|
|
|
23
23
|
|
|
24
24
|
import { isRawRefSentinel } from "./extractors/shared";
|
|
25
25
|
import type {
|
|
26
|
+
AiClassifyPattern,
|
|
27
|
+
AiExtractPattern,
|
|
28
|
+
AiGeneratePattern,
|
|
29
|
+
AiStepOpaqueArgs,
|
|
30
|
+
AiStepPolicy,
|
|
26
31
|
AuthClaimsPattern,
|
|
27
32
|
ClaimKeyPattern,
|
|
28
33
|
ConfigPattern,
|
|
@@ -143,6 +148,12 @@ export function renderPattern(pattern: FeaturePattern): string {
|
|
|
143
148
|
return renderTreeActions(pattern);
|
|
144
149
|
case "envSchema":
|
|
145
150
|
return renderEnvSchema(pattern);
|
|
151
|
+
case "ai.generate":
|
|
152
|
+
return renderAiGenerate(pattern);
|
|
153
|
+
case "ai.extract":
|
|
154
|
+
return renderAiExtract(pattern);
|
|
155
|
+
case "ai.classify":
|
|
156
|
+
return renderAiClassify(pattern);
|
|
146
157
|
case "unknown":
|
|
147
158
|
return renderUnknown(pattern);
|
|
148
159
|
default: {
|
|
@@ -439,6 +450,66 @@ function renderHook(p: HookPattern): string {
|
|
|
439
450
|
return lines.join("\n");
|
|
440
451
|
}
|
|
441
452
|
|
|
453
|
+
function renderEditableString(value: string | AiStepOpaqueArgs): string {
|
|
454
|
+
return typeof value === "string" ? JSON.stringify(value) : value.__raw;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
function renderEditableDefaults(value: AiStepPolicy | AiStepOpaqueArgs): string {
|
|
458
|
+
if (isRawRefSentinel(value)) return value.__raw;
|
|
459
|
+
return renderValue(value);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function renderAiStepCommonFields(
|
|
463
|
+
lines: string[],
|
|
464
|
+
p: AiGeneratePattern | AiExtractPattern | AiClassifyPattern,
|
|
465
|
+
): void {
|
|
466
|
+
if (p.stepKey !== undefined) lines.push(` stepKey: ${renderEditableString(p.stepKey)},`);
|
|
467
|
+
if (p.promptKey !== undefined) lines.push(` promptKey: ${renderEditableString(p.promptKey)},`);
|
|
468
|
+
if (p.promptFallback !== undefined) {
|
|
469
|
+
lines.push(` promptFallback: ${renderEditableString(p.promptFallback)},`);
|
|
470
|
+
}
|
|
471
|
+
if (p.paramsSchemaSource !== undefined) {
|
|
472
|
+
lines.push(` paramsSchema: ${p.paramsSchemaSource.raw},`);
|
|
473
|
+
}
|
|
474
|
+
if (p.defaults !== undefined) {
|
|
475
|
+
lines.push(` defaults: ${renderEditableDefaults(p.defaults)},`);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function renderAiGenerate(p: AiGeneratePattern): string {
|
|
480
|
+
if (p.argsSource) return `aiGenerateStep(${p.argsSource.__raw})`;
|
|
481
|
+
const lines: string[] = ["aiGenerateStep({"];
|
|
482
|
+
renderAiStepCommonFields(lines, p);
|
|
483
|
+
if (p.inputBody !== undefined) lines.push(` input: ${p.inputBody.raw},`);
|
|
484
|
+
lines.push("});");
|
|
485
|
+
return lines.join("\n");
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
function renderAiExtract(p: AiExtractPattern): string {
|
|
489
|
+
if (p.argsSource) return `aiExtractStep(${p.argsSource.__raw})`;
|
|
490
|
+
const lines: string[] = ["aiExtractStep({"];
|
|
491
|
+
renderAiStepCommonFields(lines, p);
|
|
492
|
+
if (p.outputSchemaSource !== undefined) {
|
|
493
|
+
lines.push(` outputSchema: ${p.outputSchemaSource.raw},`);
|
|
494
|
+
}
|
|
495
|
+
if (p.instructionsBody !== undefined) {
|
|
496
|
+
lines.push(` instructions: ${p.instructionsBody.raw},`);
|
|
497
|
+
}
|
|
498
|
+
if (p.documentBody !== undefined) lines.push(` document: ${p.documentBody.raw},`);
|
|
499
|
+
lines.push("});");
|
|
500
|
+
return lines.join("\n");
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function renderAiClassify(p: AiClassifyPattern): string {
|
|
504
|
+
if (p.argsSource) return `aiClassifyStep(${p.argsSource.__raw})`;
|
|
505
|
+
const lines: string[] = ["aiClassifyStep({"];
|
|
506
|
+
renderAiStepCommonFields(lines, p);
|
|
507
|
+
if (p.actions !== undefined) lines.push(` actions: ${renderValue(p.actions)},`);
|
|
508
|
+
if (p.inputBody !== undefined) lines.push(` input: ${p.inputBody.raw},`);
|
|
509
|
+
lines.push("});");
|
|
510
|
+
return lines.join("\n");
|
|
511
|
+
}
|
|
512
|
+
|
|
442
513
|
function renderJob(p: JobPattern): string {
|
|
443
514
|
const lines: string[] = ["r.job({"];
|
|
444
515
|
lines.push(` name: ${JSON.stringify(p.jobName)},`);
|
|
@@ -60,6 +60,9 @@ const ALL_KINDS: FeaturePatternKind[] = [
|
|
|
60
60
|
"exposesApi",
|
|
61
61
|
"treeActions",
|
|
62
62
|
"envSchema",
|
|
63
|
+
"ai.generate",
|
|
64
|
+
"ai.extract",
|
|
65
|
+
"ai.classify",
|
|
63
66
|
"unknown",
|
|
64
67
|
];
|
|
65
68
|
|
|
@@ -342,6 +345,42 @@ function makePlaceholderPattern(kind: FeaturePatternKind): FeaturePattern {
|
|
|
342
345
|
return { kind, source: PLACEHOLDER_LOC, definitions: {} };
|
|
343
346
|
case "envSchema":
|
|
344
347
|
return { kind, source: PLACEHOLDER_LOC, schemaBody: PLACEHOLDER_BODY_LOC };
|
|
348
|
+
|
|
349
|
+
case "ai.generate":
|
|
350
|
+
return {
|
|
351
|
+
kind,
|
|
352
|
+
source: PLACEHOLDER_LOC,
|
|
353
|
+
stepKey: "generate",
|
|
354
|
+
promptKey: "demo:generate",
|
|
355
|
+
promptFallback: "Write a summary.",
|
|
356
|
+
defaults: { enabled: true, params: {} },
|
|
357
|
+
paramsSchemaSource: PLACEHOLDER_BODY_LOC,
|
|
358
|
+
inputBody: PLACEHOLDER_BODY_LOC,
|
|
359
|
+
};
|
|
360
|
+
case "ai.extract":
|
|
361
|
+
return {
|
|
362
|
+
kind,
|
|
363
|
+
source: PLACEHOLDER_LOC,
|
|
364
|
+
stepKey: "extract",
|
|
365
|
+
promptKey: "demo:extract",
|
|
366
|
+
promptFallback: "Extract fields.",
|
|
367
|
+
defaults: { enabled: true, params: {} },
|
|
368
|
+
paramsSchemaSource: PLACEHOLDER_BODY_LOC,
|
|
369
|
+
outputSchemaSource: PLACEHOLDER_BODY_LOC,
|
|
370
|
+
instructionsBody: PLACEHOLDER_BODY_LOC,
|
|
371
|
+
};
|
|
372
|
+
case "ai.classify":
|
|
373
|
+
return {
|
|
374
|
+
kind,
|
|
375
|
+
source: PLACEHOLDER_LOC,
|
|
376
|
+
stepKey: "classify",
|
|
377
|
+
promptKey: "demo:classify",
|
|
378
|
+
promptFallback: "Classify the input.",
|
|
379
|
+
defaults: { enabled: true, params: {} },
|
|
380
|
+
paramsSchemaSource: PLACEHOLDER_BODY_LOC,
|
|
381
|
+
actions: [{ type: "approve", description: "Approve" }],
|
|
382
|
+
inputBody: PLACEHOLDER_BODY_LOC,
|
|
383
|
+
};
|
|
345
384
|
case "unknown":
|
|
346
385
|
return { kind, source: PLACEHOLDER_LOC, methodName: "x" };
|
|
347
386
|
case "usesApi":
|
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
|
|
16
16
|
import type { FeaturePatternKind } from "../feature-ast/patterns";
|
|
17
17
|
import {
|
|
18
|
+
aiClassifySchema,
|
|
19
|
+
aiExtractSchema,
|
|
20
|
+
aiGenerateSchema,
|
|
18
21
|
authClaimsSchema,
|
|
19
22
|
defineEventSchema,
|
|
20
23
|
hookSchema,
|
|
@@ -94,6 +97,9 @@ export const PATTERN_LIBRARY: Readonly<Record<FeaturePatternKind, PatternFormSch
|
|
|
94
97
|
exposesApi: exposesApiSchema,
|
|
95
98
|
treeActions: treeActionsSchema,
|
|
96
99
|
envSchema: envSchemaSchema,
|
|
100
|
+
"ai.generate": aiGenerateSchema,
|
|
101
|
+
"ai.extract": aiExtractSchema,
|
|
102
|
+
"ai.classify": aiClassifySchema,
|
|
97
103
|
unknown: unknownSchema,
|
|
98
104
|
} satisfies Readonly<Record<FeaturePatternKind, PatternFormSchema>>;
|
|
99
105
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Mixed pattern schemas (header form + opaque body source).
|
|
2
2
|
|
|
3
3
|
import { accessRuleField, HOOK_TYPE_OPTIONS, HTTP_METHOD_OPTIONS } from "./shared-fields";
|
|
4
|
-
import type { PatternFormSchema } from "./types";
|
|
4
|
+
import type { FormFieldSpec, PatternFormSchema } from "./types";
|
|
5
5
|
|
|
6
6
|
// --- Mixed patterns (header form + opaque body source) --------------------
|
|
7
7
|
|
|
@@ -230,6 +230,108 @@ export const jobSchema: PatternFormSchema = {
|
|
|
230
230
|
],
|
|
231
231
|
};
|
|
232
232
|
|
|
233
|
+
const aiStepCommonFields = [
|
|
234
|
+
{
|
|
235
|
+
path: "stepKey",
|
|
236
|
+
label: { en: "Step key", de: "Step-Key" },
|
|
237
|
+
input: "text",
|
|
238
|
+
required: true,
|
|
239
|
+
},
|
|
240
|
+
{
|
|
241
|
+
path: "promptKey",
|
|
242
|
+
label: { en: "Prompt key", de: "Prompt-Key" },
|
|
243
|
+
input: "text",
|
|
244
|
+
required: true,
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
path: "defaults",
|
|
248
|
+
label: { en: "Defaults", de: "Defaults" },
|
|
249
|
+
hint: { en: "enabled, providerId, model, params" },
|
|
250
|
+
input: "json-readonly",
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
path: "paramsSchemaSource",
|
|
254
|
+
label: { en: "Params schema (source)", de: "Params-Schema (Source)" },
|
|
255
|
+
input: "code-block",
|
|
256
|
+
language: "typescript",
|
|
257
|
+
readOnly: true,
|
|
258
|
+
},
|
|
259
|
+
] as const satisfies readonly FormFieldSpec[];
|
|
260
|
+
|
|
261
|
+
export const aiGenerateSchema: PatternFormSchema = {
|
|
262
|
+
kind: "ai.generate",
|
|
263
|
+
label: { en: "AI generate step", de: "AI-Generate-Step" },
|
|
264
|
+
summary: { en: "Tier-3 workflow step for freeform text generation." },
|
|
265
|
+
category: "behaviour",
|
|
266
|
+
editability: "mixed",
|
|
267
|
+
fields: [
|
|
268
|
+
...aiStepCommonFields,
|
|
269
|
+
{
|
|
270
|
+
path: "inputBody",
|
|
271
|
+
label: { en: "Input resolver (source)", de: "Input-Resolver (Source)" },
|
|
272
|
+
input: "code-block",
|
|
273
|
+
language: "typescript",
|
|
274
|
+
readOnly: true,
|
|
275
|
+
},
|
|
276
|
+
],
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
export const aiExtractSchema: PatternFormSchema = {
|
|
280
|
+
kind: "ai.extract",
|
|
281
|
+
label: { en: "AI extract step", de: "AI-Extract-Step" },
|
|
282
|
+
summary: { en: "Schema-driven structured extraction step." },
|
|
283
|
+
category: "behaviour",
|
|
284
|
+
editability: "mixed",
|
|
285
|
+
fields: [
|
|
286
|
+
...aiStepCommonFields,
|
|
287
|
+
{
|
|
288
|
+
path: "outputSchemaSource",
|
|
289
|
+
label: { en: "Output schema (source)", de: "Output-Schema (Source)" },
|
|
290
|
+
input: "code-block",
|
|
291
|
+
language: "typescript",
|
|
292
|
+
readOnly: true,
|
|
293
|
+
},
|
|
294
|
+
{
|
|
295
|
+
path: "instructionsBody",
|
|
296
|
+
label: { en: "Instructions (source)", de: "Instructions (Source)" },
|
|
297
|
+
input: "code-block",
|
|
298
|
+
language: "typescript",
|
|
299
|
+
readOnly: true,
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
path: "documentBody",
|
|
303
|
+
label: { en: "Document resolver (source)", de: "Document-Resolver (Source)" },
|
|
304
|
+
input: "code-block",
|
|
305
|
+
language: "typescript",
|
|
306
|
+
readOnly: true,
|
|
307
|
+
},
|
|
308
|
+
],
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export const aiClassifySchema: PatternFormSchema = {
|
|
312
|
+
kind: "ai.classify",
|
|
313
|
+
label: { en: "AI classify step", de: "AI-Classify-Step" },
|
|
314
|
+
summary: { en: "Action-catalogue classification step." },
|
|
315
|
+
category: "behaviour",
|
|
316
|
+
editability: "mixed",
|
|
317
|
+
fields: [
|
|
318
|
+
...aiStepCommonFields,
|
|
319
|
+
{
|
|
320
|
+
path: "actions",
|
|
321
|
+
label: { en: "Actions", de: "Aktionen" },
|
|
322
|
+
input: "json-readonly",
|
|
323
|
+
required: true,
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
path: "inputBody",
|
|
327
|
+
label: { en: "Input resolver (source)", de: "Input-Resolver (Source)" },
|
|
328
|
+
input: "code-block",
|
|
329
|
+
language: "typescript",
|
|
330
|
+
readOnly: true,
|
|
331
|
+
},
|
|
332
|
+
],
|
|
333
|
+
};
|
|
334
|
+
|
|
233
335
|
export const notificationSchema: PatternFormSchema = {
|
|
234
336
|
kind: "notification",
|
|
235
337
|
label: { en: "Notification", de: "Benachrichtigung" },
|
|
@@ -180,7 +180,6 @@ describe("tenant-storage-usage MSP", () => {
|
|
|
180
180
|
const [first] = await selectMany(stack.db, tenantStorageUsageTable, {
|
|
181
181
|
tenantId: admin.tenantId,
|
|
182
182
|
});
|
|
183
|
-
expect(first?.["lastUpdatedAt"]).toBeInstanceOf(Temporal.Instant);
|
|
184
183
|
|
|
185
184
|
// Postgres NOW() resolution is microseconds; a second upload a beat
|
|
186
185
|
// later must produce a strictly later timestamp (or at least not an
|
|
@@ -193,7 +192,6 @@ describe("tenant-storage-usage MSP", () => {
|
|
|
193
192
|
const [second] = await selectMany(stack.db, tenantStorageUsageTable, {
|
|
194
193
|
tenantId: admin.tenantId,
|
|
195
194
|
});
|
|
196
|
-
expect(second?.["lastUpdatedAt"]).toBeInstanceOf(Temporal.Instant);
|
|
197
195
|
if (!first?.["lastUpdatedAt"] || !second?.["lastUpdatedAt"]) throw new Error("missing rows");
|
|
198
196
|
expect(
|
|
199
197
|
Temporal.Instant.compare(second["lastUpdatedAt"], first["lastUpdatedAt"]),
|
|
@@ -47,14 +47,14 @@ export function attachSignalHandlers(
|
|
|
47
47
|
.then(() => exitFn(0))
|
|
48
48
|
.catch(() => exitFn(1));
|
|
49
49
|
};
|
|
50
|
-
process.on(sig, handler);
|
|
50
|
+
(process as NodeJS.EventEmitter).on(sig, handler);
|
|
51
51
|
listeners.set(sig, handler);
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
return {
|
|
55
55
|
detach: () => {
|
|
56
56
|
for (const [sig, handler] of listeners) {
|
|
57
|
-
process.off(sig, handler);
|
|
57
|
+
(process as NodeJS.EventEmitter).off(sig, handler);
|
|
58
58
|
}
|
|
59
59
|
listeners.clear();
|
|
60
60
|
},
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
-
import {
|
|
3
|
-
formatReport,
|
|
4
|
-
isRawSqlAllowed,
|
|
5
|
-
joinPath,
|
|
6
|
-
scanRepo,
|
|
7
|
-
toBaselineJson,
|
|
8
|
-
} from "../sql-inventory";
|
|
9
|
-
|
|
10
|
-
const cleanups: string[] = [];
|
|
11
|
-
|
|
12
|
-
afterEach(async () => {
|
|
13
|
-
for (const dir of cleanups) {
|
|
14
|
-
await Bun.spawn(["rm", "-rf", dir]).exited;
|
|
15
|
-
}
|
|
16
|
-
cleanups.length = 0;
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
async function tempRepo(files: Record<string, string>): Promise<string> {
|
|
20
|
-
const root = joinPath(import.meta.dir, `.tmp-sql-inv-${crypto.randomUUID()}`);
|
|
21
|
-
cleanups.push(root);
|
|
22
|
-
await Promise.all(
|
|
23
|
-
Object.entries(files).map(([rel, content]) => Bun.write(joinPath(root, rel), content)),
|
|
24
|
-
);
|
|
25
|
-
return root;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
describe("sql-inventory", () => {
|
|
29
|
-
test("isRawSqlAllowed permits db/queries and bun-db/query", () => {
|
|
30
|
-
expect(isRawSqlAllowed("/repo/packages/framework/src/db/queries/event-store.ts")).toBe(true);
|
|
31
|
-
expect(isRawSqlAllowed("/repo/packages/framework/src/bun-db/query.ts")).toBe(true);
|
|
32
|
-
expect(
|
|
33
|
-
isRawSqlAllowed("/repo/packages/bundled-features/src/sessions/db/queries/cleanup.ts"),
|
|
34
|
-
).toBe(true);
|
|
35
|
-
expect(isRawSqlAllowed("/repo/samples/apps/marketing-demo/src/db/queries/seed-counts.ts")).toBe(
|
|
36
|
-
true,
|
|
37
|
-
);
|
|
38
|
-
expect(isRawSqlAllowed("/repo/bin/commands/schema.ts")).toBe(true);
|
|
39
|
-
expect(isRawSqlAllowed("/repo/scripts/codemod-bun-db-swap.ts")).toBe(true);
|
|
40
|
-
expect(
|
|
41
|
-
isRawSqlAllowed("/repo/packages/bundled-features/src/sessions/handlers/cleanup.job.ts"),
|
|
42
|
-
).toBe(false);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test("scanRepo classifies production vs test hits", async () => {
|
|
46
|
-
const root = await tempRepo({
|
|
47
|
-
"packages/framework/src/db/queries/demo.ts": `export async function x(db: unknown) {
|
|
48
|
-
return asRawClient(db).unsafe("SELECT 1");
|
|
49
|
-
}`,
|
|
50
|
-
"packages/framework/src/handlers/bad.ts": `export async function y(db: unknown) {
|
|
51
|
-
return asRawClient(db).unsafe("DELETE FROM read_users");
|
|
52
|
-
}`,
|
|
53
|
-
"packages/framework/src/__tests__/ok.integration.ts": `await asRawClient(db).unsafe("DELETE FROM read_users");`,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const report = await scanRepo(root);
|
|
57
|
-
expect(report.summary.byBucket.allowed).toBeGreaterThanOrEqual(1);
|
|
58
|
-
expect(report.summary.byBucket.tests).toBeGreaterThanOrEqual(1);
|
|
59
|
-
expect(report.summary.byBucket.disallowed).toBeGreaterThanOrEqual(1);
|
|
60
|
-
expect(formatReport(report)).toContain("sql inventory");
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
test("toBaselineJson normalizes machine-specific root + scannedAt, keeps the rest", async () => {
|
|
64
|
-
const root = await tempRepo({
|
|
65
|
-
"packages/framework/src/handlers/bad.ts": `export async function y(db: unknown) {
|
|
66
|
-
return asRawClient(db).unsafe("DELETE FROM read_users");
|
|
67
|
-
}`,
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const report = await scanRepo(root);
|
|
71
|
-
expect(report.root).toBe(root);
|
|
72
|
-
expect(report.scannedAt).not.toBe("");
|
|
73
|
-
|
|
74
|
-
const parsed = JSON.parse(toBaselineJson(report));
|
|
75
|
-
expect(parsed.root).toBe(".");
|
|
76
|
-
expect(parsed.scannedAt).toBe("");
|
|
77
|
-
expect(parsed.root).not.toContain(root);
|
|
78
|
-
expect(parsed.summary.disallowed).toBe(report.summary.disallowed);
|
|
79
|
-
expect(parsed.hits).toHaveLength(report.hits.length);
|
|
80
|
-
});
|
|
81
|
-
});
|