@auto-engineer/narrative 1.104.0 → 1.106.0

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.
Files changed (41) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +6 -6
  3. package/.turbo/turbo-type-check.log +1 -1
  4. package/CHANGELOG.md +92 -0
  5. package/dist/src/data-narrative-builders.d.ts +10 -0
  6. package/dist/src/data-narrative-builders.d.ts.map +1 -1
  7. package/dist/src/data-narrative-builders.js +13 -0
  8. package/dist/src/data-narrative-builders.js.map +1 -1
  9. package/dist/src/index.d.ts +2 -2
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +1 -1
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/narrative-context.d.ts.map +1 -1
  14. package/dist/src/narrative-context.js.map +1 -1
  15. package/dist/src/narrative.d.ts +2 -1
  16. package/dist/src/narrative.d.ts.map +1 -1
  17. package/dist/src/narrative.js.map +1 -1
  18. package/dist/src/schema.d.ts +1661 -327
  19. package/dist/src/schema.d.ts.map +1 -1
  20. package/dist/src/schema.js +16 -3
  21. package/dist/src/schema.js.map +1 -1
  22. package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
  23. package/dist/src/transformers/model-to-narrative/generators/flow.js +4 -0
  24. package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
  25. package/dist/src/transformers/model-to-narrative/generators/imports.d.ts +1 -1
  26. package/dist/src/transformers/model-to-narrative/generators/imports.d.ts.map +1 -1
  27. package/dist/src/transformers/model-to-narrative/generators/imports.js +1 -0
  28. package/dist/src/transformers/model-to-narrative/generators/imports.js.map +1 -1
  29. package/dist/tsconfig.tsbuildinfo +1 -1
  30. package/ketchup-plan.md +5 -6
  31. package/package.json +4 -4
  32. package/src/data-narrative-builders.specs.ts +32 -0
  33. package/src/data-narrative-builders.ts +22 -0
  34. package/src/index.ts +2 -2
  35. package/src/model-to-narrative.specs.ts +78 -0
  36. package/src/narrative-context.ts +5 -5
  37. package/src/narrative.ts +2 -1
  38. package/src/schema.specs.ts +88 -0
  39. package/src/schema.ts +19 -2
  40. package/src/transformers/model-to-narrative/generators/flow.ts +23 -3
  41. package/src/transformers/model-to-narrative/generators/imports.ts +1 -0
package/ketchup-plan.md CHANGED
@@ -1,4 +1,4 @@
1
- # Ketchup Plan: Validate Slice Requests Against Model Messages
1
+ # Ketchup Plan: DataTarget Schema Target-Only Event Items
2
2
 
3
3
  ## TODO
4
4
 
@@ -6,8 +6,7 @@
6
6
 
7
7
  ## DONE
8
8
 
9
- - [x] Burst 1: Skeleton + parse safety + export convertJsonAstToSdl (4b1d0b12)
10
- - [x] Burst 2: Mutation validation (wrong op type, missing input, type mismatch, message not found) (4059d393)
11
- - [x] Burst 3: Queryoperation type + state + top-level fields (73c8c99c)
12
- - [x] Burst 4: Query nested field validation (inline objects, referenced messages, unresolvable) (39d3d083)
13
- - [x] Burst 5: Export from index.ts + integration test (885a805f)
9
+ - [x] Burst 1: Add DataTargetSchema to schema.ts, DataTarget type, DataTargetItem, exports (81188d6b)
10
+ - [x] Burst 2: Add target() builder factory in data-narrative-builders.ts (757b8475)
11
+ - [x] Burst 3: Update stripTypeDiscriminator completed in burst 1 (type change propagated)
12
+ - [x] Burst 4: Update flow code generator to handle target-only items (057c6e56)
package/package.json CHANGED
@@ -26,9 +26,9 @@
26
26
  "typescript": "^5.9.2",
27
27
  "zod": "^3.22.4",
28
28
  "zod-to-json-schema": "^3.22.3",
29
- "@auto-engineer/file-store": "1.104.0",
30
- "@auto-engineer/id": "1.104.0",
31
- "@auto-engineer/message-bus": "1.104.0"
29
+ "@auto-engineer/file-store": "1.106.0",
30
+ "@auto-engineer/id": "1.106.0",
31
+ "@auto-engineer/message-bus": "1.106.0"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@types/node": "^20.0.0",
@@ -38,7 +38,7 @@
38
38
  "publishConfig": {
39
39
  "access": "public"
40
40
  },
41
- "version": "1.104.0",
41
+ "version": "1.106.0",
42
42
  "scripts": {
43
43
  "build": "tsx scripts/build.ts",
44
44
  "test": "vitest run --reporter=dot",
@@ -0,0 +1,32 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import { target } from './data-narrative-builders';
3
+
4
+ describe('target() builder', () => {
5
+ it('builds a target-only event item without id', () => {
6
+ const result = target().event('TodoAdded');
7
+
8
+ expect(result).toEqual({
9
+ target: { type: 'Event', name: 'TodoAdded' },
10
+ __type: 'target',
11
+ });
12
+ });
13
+
14
+ it('builds a target-only event item with id', () => {
15
+ const result = target('dt-1').event('OrderPlaced');
16
+
17
+ expect(result).toEqual({
18
+ id: 'dt-1',
19
+ target: { type: 'Event', name: 'OrderPlaced' },
20
+ __type: 'target',
21
+ });
22
+ });
23
+
24
+ it('omits id when empty string', () => {
25
+ const result = target('').event('TodoAdded');
26
+
27
+ expect(result).toEqual({
28
+ target: { type: 'Event', name: 'TodoAdded' },
29
+ __type: 'target',
30
+ });
31
+ });
32
+ });
@@ -1,7 +1,12 @@
1
1
  import { integrationExportRegistry } from './integration-export-registry';
2
+ import type { DataTarget } from './schema';
2
3
  import type { DataSinkItem, DataSourceItem, DefaultRecord, Integration, MessageTarget } from './types';
3
4
  import { createIntegrationOrigin } from './types';
4
5
 
6
+ export interface DataTargetItem extends DataTarget {
7
+ readonly __type: 'target';
8
+ }
9
+
5
10
  // Extended interfaces that add chainable methods
6
11
  export interface ChainableSinkMethods {
7
12
  additionalInstructions(instructions: string): ChainableSink;
@@ -439,9 +444,26 @@ export class DataSourceBuilder {
439
444
  }
440
445
  }
441
446
 
447
+ export class DataTargetBuilder {
448
+ private readonly builderId?: string;
449
+
450
+ constructor(id?: string) {
451
+ this.builderId = id;
452
+ }
453
+
454
+ event(name: string): DataTargetItem {
455
+ return {
456
+ ...(this.builderId != null && this.builderId !== '' && { id: this.builderId }),
457
+ target: { type: 'Event' as const, name },
458
+ __type: 'target' as const,
459
+ };
460
+ }
461
+ }
462
+
442
463
  // Factory functions for cleaner API
443
464
  export const sink = (id?: string) => new DataSinkBuilder(id);
444
465
  export const source = (id?: string) => new DataSourceBuilder(id);
466
+ export const target = (id?: string) => new DataTargetBuilder(id);
445
467
 
446
468
  // Type-safe sink function that accepts builder results
447
469
  export function typedSink(
package/src/index.ts CHANGED
@@ -24,9 +24,9 @@ export const get = (strings: TemplateStringsArray, ...values: unknown[]) => {
24
24
  }, '');
25
25
  };
26
26
 
27
- export type { FieldSelector } from './data-narrative-builders';
27
+ export type { DataTargetItem, FieldSelector } from './data-narrative-builders';
28
28
 
29
- export { sink, source } from './data-narrative-builders';
29
+ export { sink, source, target } from './data-narrative-builders';
30
30
  export type {
31
31
  FluentCommandSliceBuilder,
32
32
  FluentExperienceSliceBuilder,
@@ -3157,6 +3157,84 @@ narrative('All Projection Types', 'ALL-PROJ', () => {
3157
3157
  expect(code).not.toContain("sink('')");
3158
3158
  });
3159
3159
 
3160
+ it('should generate target-only event items without destination', async () => {
3161
+ const modelWithTarget: Model = {
3162
+ variant: 'specs',
3163
+ narratives: [
3164
+ {
3165
+ name: 'React Flow',
3166
+ id: 'REACT-FLOW',
3167
+ slices: [
3168
+ {
3169
+ name: 'reacts to event',
3170
+ id: 'REACT-SLICE',
3171
+ type: 'react',
3172
+ server: {
3173
+ description: 'React handler',
3174
+ data: {
3175
+ items: [{ target: { type: 'Event', name: 'TodoAdded' } }],
3176
+ },
3177
+ specs: [],
3178
+ },
3179
+ },
3180
+ ],
3181
+ },
3182
+ ],
3183
+ messages: [
3184
+ {
3185
+ type: 'event',
3186
+ source: 'internal',
3187
+ name: 'TodoAdded',
3188
+ fields: [{ name: 'todoId', type: 'string', required: true }],
3189
+ },
3190
+ ],
3191
+ integrations: [],
3192
+ modules: [],
3193
+ };
3194
+
3195
+ const code = getCode(await modelToNarrative(modelWithTarget));
3196
+ expect(code).toContain("target().event('TodoAdded')");
3197
+ });
3198
+
3199
+ it('should generate target-only event items with id', async () => {
3200
+ const modelWithTargetId: Model = {
3201
+ variant: 'specs',
3202
+ narratives: [
3203
+ {
3204
+ name: 'React Flow',
3205
+ id: 'REACT-FLOW',
3206
+ slices: [
3207
+ {
3208
+ name: 'reacts to event',
3209
+ id: 'REACT-SLICE',
3210
+ type: 'react',
3211
+ server: {
3212
+ description: 'React handler',
3213
+ data: {
3214
+ items: [{ id: 'TGT-1', target: { type: 'Event', name: 'TodoAdded' } }],
3215
+ },
3216
+ specs: [],
3217
+ },
3218
+ },
3219
+ ],
3220
+ },
3221
+ ],
3222
+ messages: [
3223
+ {
3224
+ type: 'event',
3225
+ source: 'internal',
3226
+ name: 'TodoAdded',
3227
+ fields: [{ name: 'todoId', type: 'string', required: true }],
3228
+ },
3229
+ ],
3230
+ integrations: [],
3231
+ modules: [],
3232
+ };
3233
+
3234
+ const code = getCode(await modelToNarrative(modelWithTargetId));
3235
+ expect(code).toContain("target('TGT-1').event('TodoAdded')");
3236
+ });
3237
+
3160
3238
  it('should generate _additionalInstructions on source items', async () => {
3161
3239
  const modelWithSourceInstructions: Model = {
3162
3240
  variant: 'specs',
@@ -2,8 +2,8 @@ import createDebug from 'debug';
2
2
  import type { z } from 'zod';
3
3
  import type { CommandSlice, ExperienceSlice, Narrative, QuerySlice, Slice } from './index';
4
4
  import type { GivenTypeInfo } from './loader/ts-utils';
5
- import type { ClientSpecNode, ExampleSchema, RuleSchema, SpecSchema, StepSchema } from './schema';
6
- import type { Data, DataItem, DataSink, DataSource } from './types';
5
+ import type { ClientSpecNode, DataTarget, ExampleSchema, RuleSchema, SpecSchema, StepSchema } from './schema';
6
+ import type { Data, DataSink, DataSource } from './types';
7
7
 
8
8
  type Step = z.infer<typeof StepSchema>;
9
9
  type Example = z.infer<typeof ExampleSchema>;
@@ -301,14 +301,14 @@ export function setSliceData(data: Data): void {
301
301
  };
302
302
  }
303
303
 
304
- function stripTypeDiscriminator(items: (DataSink | DataSource | DataItem)[]): (DataSink | DataSource)[] {
304
+ function stripTypeDiscriminator(items: (DataSink | DataSource | DataTarget)[]): (DataSink | DataSource | DataTarget)[] {
305
305
  return items.map((item) => {
306
306
  if ('__type' in item) {
307
307
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
308
308
  const { __type, ...rest } = item;
309
- return rest as DataSink | DataSource;
309
+ return rest;
310
310
  }
311
- return item as DataSink | DataSource;
311
+ return item;
312
312
  });
313
313
  }
314
314
 
package/src/narrative.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import createDebug from 'debug';
2
+ import type { DataTargetItem } from './data-narrative-builders';
2
3
  import {
3
4
  clearCurrentNarrative,
4
5
  endClientBlock,
@@ -214,7 +215,7 @@ export interface SliceTypeValueInterface {
214
215
  readonly value: 'command' | 'query' | 'react';
215
216
  }
216
217
 
217
- export function data(config: Data | DataItem[]): void {
218
+ export function data(config: Data | (DataItem | DataTargetItem)[]): void {
218
219
  const slice = getCurrentSlice();
219
220
  if (!slice) throw new Error('No active slice for data configuration');
220
221
 
@@ -2,6 +2,8 @@ import { describe, expect, it } from 'vitest';
2
2
  import type { Journey, JourneyPlanning, SceneClassification, SceneRoute } from './schema';
3
3
  import {
4
4
  CommandSliceSchema,
5
+ DataSchema,
6
+ DataTargetSchema,
5
7
  JourneyPlanningSchema,
6
8
  JourneySchema,
7
9
  modelSchema,
@@ -411,6 +413,92 @@ describe('exported types', () => {
411
413
  });
412
414
  });
413
415
 
416
+ describe('DataTargetSchema', () => {
417
+ it('should accept a target-only event item', () => {
418
+ const result = DataTargetSchema.safeParse({
419
+ target: { type: 'Event', name: 'TodoAdded' },
420
+ });
421
+
422
+ expect(result.success).toBe(true);
423
+ if (result.success) {
424
+ expect(result.data).toEqual({
425
+ target: { type: 'Event', name: 'TodoAdded' },
426
+ });
427
+ }
428
+ });
429
+
430
+ it('should accept optional id, transform, and _additionalInstructions', () => {
431
+ const result = DataTargetSchema.safeParse({
432
+ id: 'dt-1',
433
+ target: { type: 'Event', name: 'OrderPlaced' },
434
+ transform: 'mapOrder',
435
+ _additionalInstructions: 'extra info',
436
+ });
437
+
438
+ expect(result.success).toBe(true);
439
+ if (result.success) {
440
+ expect(result.data).toEqual({
441
+ id: 'dt-1',
442
+ target: { type: 'Event', name: 'OrderPlaced' },
443
+ transform: 'mapOrder',
444
+ _additionalInstructions: 'extra info',
445
+ });
446
+ }
447
+ });
448
+
449
+ it('should reject Command target type', () => {
450
+ const result = DataTargetSchema.safeParse({
451
+ target: { type: 'Command', name: 'CreateTodo' },
452
+ });
453
+
454
+ expect(result.success).toBe(false);
455
+ });
456
+
457
+ it('should reject State target type', () => {
458
+ const result = DataTargetSchema.safeParse({
459
+ target: { type: 'State', name: 'TodoState' },
460
+ });
461
+
462
+ expect(result.success).toBe(false);
463
+ });
464
+ });
465
+
466
+ describe('DataSchema with target-only items', () => {
467
+ it('should accept target-only items in the items array', () => {
468
+ const result = DataSchema.safeParse({
469
+ items: [{ target: { type: 'Event', name: 'TodoAdded' } }],
470
+ });
471
+
472
+ expect(result.success).toBe(true);
473
+ if (result.success) {
474
+ expect(result.data.items).toEqual([{ target: { type: 'Event', name: 'TodoAdded' } }]);
475
+ }
476
+ });
477
+
478
+ it('should accept mixed sink and target items', () => {
479
+ const result = DataSchema.safeParse({
480
+ items: [
481
+ {
482
+ target: { type: 'Event', name: 'TodoAdded' },
483
+ destination: { type: 'stream', pattern: 'todo-${id}' },
484
+ },
485
+ { target: { type: 'Event', name: 'TodoRemoved' } },
486
+ ],
487
+ });
488
+
489
+ expect(result.success).toBe(true);
490
+ if (result.success) {
491
+ expect(result.data.items).toEqual([
492
+ {
493
+ target: { type: 'Event', name: 'TodoAdded' },
494
+ destination: { type: 'stream', pattern: 'todo-${id}' },
495
+ },
496
+ { target: { type: 'Event', name: 'TodoRemoved' } },
497
+ ]);
498
+ }
499
+ });
500
+ });
501
+
414
502
  describe('QuerySliceSchema', () => {
415
503
  it('should accept optional mappings field with structured entries', () => {
416
504
  const slice = {
package/src/schema.ts CHANGED
@@ -133,12 +133,27 @@ const DataSourceSchema = z
133
133
  })
134
134
  .describe('Data source configuration for inbound data flow');
135
135
 
136
+ const EventTargetSchema = MessageTargetSchema.extend({
137
+ type: z.enum(['Event']),
138
+ }).describe('Event-only message target');
139
+
140
+ const DataTargetSchema = z
141
+ .object({
142
+ id: z.string().optional().describe('Optional unique identifier for the data target'),
143
+ target: EventTargetSchema,
144
+ transform: z.string().optional().describe('Optional transformation function name'),
145
+ _additionalInstructions: z.string().optional().describe('Additional instructions'),
146
+ })
147
+ .describe('Target-only data item for event declaration without routing');
148
+
136
149
  export const DataSchema = z
137
150
  .object({
138
151
  id: z.string().optional().describe('Optional unique identifier for the data configuration'),
139
- items: z.array(z.union([DataSinkSchema, DataSourceSchema])).describe('Array of data sinks and sources'),
152
+ items: z
153
+ .array(z.union([DataSinkSchema, DataSourceSchema, DataTargetSchema]))
154
+ .describe('Array of data sinks, sources, and targets'),
140
155
  })
141
- .describe('Data configuration containing sinks and sources');
156
+ .describe('Data configuration containing sinks, sources, and targets');
142
157
 
143
158
  const MessageFieldSchema = z
144
159
  .object({
@@ -512,6 +527,7 @@ export {
512
527
  SpecSchema,
513
528
  DataSinkSchema,
514
529
  DataSourceSchema,
530
+ DataTargetSchema,
515
531
  StepSchema,
516
532
  StepErrorSchema,
517
533
  StepWithDocStringSchema,
@@ -536,6 +552,7 @@ export type MessageRef = z.infer<typeof MessageRefSchema>;
536
552
  export type MappingFieldRef = z.infer<typeof MappingFieldRefSchema>;
537
553
  export type MappingEntry = z.infer<typeof MappingEntrySchema>;
538
554
  export type Journey = z.infer<typeof JourneySchema>;
555
+ export type DataTarget = z.infer<typeof DataTargetSchema>;
539
556
  export type SceneClassification = z.infer<typeof SceneClassificationSchema>;
540
557
  export type SceneRoute = z.infer<typeof SceneRouteSchema>;
541
558
  export type JourneyPlanning = z.infer<typeof JourneyPlanningSchema>;
@@ -11,6 +11,7 @@ import type {
11
11
  ClientSpecNode,
12
12
  DataSinkSchema,
13
13
  DataSourceSchema,
14
+ DataTargetSchema,
14
15
  DestinationSchema,
15
16
  ExampleSchema,
16
17
  OriginSchema,
@@ -25,6 +26,7 @@ type ExperienceSlice = ExperienceSliceType;
25
26
  type Example = z.infer<typeof ExampleSchema>;
26
27
  type DataSinkItem = z.infer<typeof DataSinkSchema>;
27
28
  type DataSourceItem = z.infer<typeof DataSourceSchema>;
29
+ type DataTargetItem = z.infer<typeof DataTargetSchema>;
28
30
  type Destination = z.infer<typeof DestinationSchema>;
29
31
  type Origin = z.infer<typeof OriginSchema>;
30
32
  type Slice = CommandSlice | QuerySlice | ReactSlice | ExperienceSlice;
@@ -305,8 +307,20 @@ function getOriginMethodName(origin: Origin): string {
305
307
  function buildSingleDataItem(
306
308
  ts: typeof import('typescript'),
307
309
  f: tsNS.NodeFactory,
308
- it: DataSinkItem | DataSourceItem,
310
+ it: DataSinkItem | DataSourceItem | DataTargetItem,
309
311
  ): tsNS.Expression {
312
+ if (!('destination' in it) && !('origin' in it)) {
313
+ const args: tsNS.Expression[] = it.id != null && it.id !== '' ? [f.createStringLiteral(it.id)] : [];
314
+ return f.createCallExpression(
315
+ f.createPropertyAccessExpression(
316
+ f.createCallExpression(f.createIdentifier('target'), undefined, args),
317
+ f.createIdentifier('event'),
318
+ ),
319
+ undefined,
320
+ [f.createStringLiteral(it.target.name)],
321
+ );
322
+ }
323
+
310
324
  let chain = buildInitialChain(ts, f, it.target, it.id);
311
325
 
312
326
  if ('destination' in it) {
@@ -364,7 +378,7 @@ function buildSingleDataItem(
364
378
  function buildDataItems(
365
379
  ts: typeof import('typescript'),
366
380
  f: tsNS.NodeFactory,
367
- data: { id?: string; items: Array<DataSinkItem | DataSourceItem> },
381
+ data: { id?: string; items: Array<DataSinkItem | DataSourceItem | DataTargetItem> },
368
382
  ) {
369
383
  const calls = data.items.map((it) => buildSingleDataItem(ts, f, it));
370
384
 
@@ -674,7 +688,13 @@ function buildServerStatements(
674
688
  const statements: tsNS.Statement[] = [];
675
689
 
676
690
  if (server.data?.items && server.data.items.length > 0) {
677
- statements.push(buildDataItems(ts, f, server.data as { id?: string; items: Array<DataSinkItem | DataSourceItem> }));
691
+ statements.push(
692
+ buildDataItems(
693
+ ts,
694
+ f,
695
+ server.data as { id?: string; items: Array<DataSinkItem | DataSourceItem | DataTargetItem> },
696
+ ),
697
+ );
678
698
  }
679
699
 
680
700
  if (server.specs !== null && server.specs !== undefined) {
@@ -15,6 +15,7 @@ export const ALL_FLOW_FUNCTION_NAMES = [
15
15
  'sink',
16
16
  'source',
17
17
  'specs',
18
+ 'target',
18
19
  ] as const;
19
20
 
20
21
  export function buildImports(