@auto-engineer/narrative 1.134.0 → 1.135.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.
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.134.0",
30
- "@auto-engineer/id": "1.134.0",
31
- "@auto-engineer/message-bus": "1.134.0"
29
+ "@auto-engineer/file-store": "1.135.0",
30
+ "@auto-engineer/id": "1.135.0",
31
+ "@auto-engineer/message-bus": "1.135.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.134.0",
41
+ "version": "1.135.0",
42
42
  "scripts": {
43
43
  "build": "tsx scripts/build.ts",
44
44
  "test": "vitest run --reporter=dot",
@@ -1,17 +1,29 @@
1
1
  import { describe, expect, it } from 'vitest';
2
- import type { Journey, JourneyPlanning, SceneClassification, SceneRoute } from './schema';
2
+ import type {
3
+ ComponentDefinition,
4
+ Journey,
5
+ JourneyPlanning,
6
+ RegionEntry,
7
+ SceneClassification,
8
+ SceneRoute,
9
+ UI,
10
+ } from './schema';
3
11
  import {
4
12
  CommandSliceSchema,
13
+ ComponentDefinitionSchema,
5
14
  DataSchema,
6
15
  DataTargetSchema,
16
+ DesignSchema,
7
17
  JourneyPlanningSchema,
8
18
  JourneySchema,
9
19
  modelSchema,
10
20
  NarrativeNamesOnlySchema,
11
21
  NarrativeSchema,
12
22
  QuerySliceSchema,
23
+ RegionEntrySchema,
13
24
  SceneClassificationSchema,
14
25
  SceneRouteSchema,
26
+ UISchema,
15
27
  } from './schema';
16
28
 
17
29
  describe('CommandSliceSchema', () => {
@@ -527,3 +539,212 @@ describe('QuerySliceSchema', () => {
527
539
  }
528
540
  });
529
541
  });
542
+
543
+ describe('RegionEntrySchema', () => {
544
+ it('should accept valid region entry with required fields', () => {
545
+ const result = RegionEntrySchema.safeParse({ id: 'hero-1', name: 'HeroBanner' });
546
+ expect(result.success).toBe(true);
547
+ if (result.success) {
548
+ expect(result.data).toEqual({ id: 'hero-1', name: 'HeroBanner' });
549
+ }
550
+ });
551
+
552
+ it('should accept optional slots', () => {
553
+ const result = RegionEntrySchema.safeParse({ id: 'card-1', name: 'Card', slots: { title: 'Hello' } });
554
+ expect(result.success).toBe(true);
555
+ if (result.success) {
556
+ expect(result.data.slots).toEqual({ title: 'Hello' });
557
+ }
558
+ });
559
+
560
+ it('should reject missing id', () => {
561
+ const result = RegionEntrySchema.safeParse({ name: 'Card' });
562
+ expect(result.success).toBe(false);
563
+ });
564
+
565
+ it('should reject missing name', () => {
566
+ const result = RegionEntrySchema.safeParse({ id: 'card-1' });
567
+ expect(result.success).toBe(false);
568
+ });
569
+ });
570
+
571
+ describe('UISchema', () => {
572
+ it('should accept valid UI with required fields', () => {
573
+ const result = UISchema.safeParse({
574
+ layoutId: 'two-column',
575
+ mode: 'as-is',
576
+ regions: { main: [{ id: 'c-1', name: 'List' }] },
577
+ });
578
+ expect(result.success).toBe(true);
579
+ if (result.success) {
580
+ expect(result.data).toEqual({
581
+ layoutId: 'two-column',
582
+ mode: 'as-is',
583
+ regions: { main: [{ id: 'c-1', name: 'List' }] },
584
+ });
585
+ }
586
+ });
587
+
588
+ it('should accept all valid mode values', () => {
589
+ for (const mode of ['as-is', 'modify', 'custom'] as const) {
590
+ const result = UISchema.safeParse({ layoutId: 'l', mode, regions: {} });
591
+ expect(result.success).toBe(true);
592
+ }
593
+ });
594
+
595
+ it('should reject invalid mode', () => {
596
+ const result = UISchema.safeParse({ layoutId: 'l', mode: 'invalid', regions: {} });
597
+ expect(result.success).toBe(false);
598
+ });
599
+
600
+ it('should accept optional customizationNotes', () => {
601
+ const result = UISchema.safeParse({
602
+ layoutId: 'l',
603
+ mode: 'modify',
604
+ regions: {},
605
+ customizationNotes: 'Make header sticky',
606
+ });
607
+ expect(result.success).toBe(true);
608
+ if (result.success) {
609
+ expect(result.data.customizationNotes).toBe('Make header sticky');
610
+ }
611
+ });
612
+
613
+ it('should reject missing layoutId', () => {
614
+ const result = UISchema.safeParse({ mode: 'as-is', regions: {} });
615
+ expect(result.success).toBe(false);
616
+ });
617
+
618
+ it('should reject missing mode', () => {
619
+ const result = UISchema.safeParse({ layoutId: 'l', regions: {} });
620
+ expect(result.success).toBe(false);
621
+ });
622
+
623
+ it('should reject missing regions', () => {
624
+ const result = UISchema.safeParse({ layoutId: 'l', mode: 'as-is' });
625
+ expect(result.success).toBe(false);
626
+ });
627
+ });
628
+
629
+ describe('ComponentDefinitionSchema', () => {
630
+ it('should accept valid component definition', () => {
631
+ const result = ComponentDefinitionSchema.safeParse({
632
+ id: 'comp-1',
633
+ name: 'PriceTag',
634
+ category: 'commerce',
635
+ description: 'Displays a price',
636
+ slots: { price: { type: 'number' } },
637
+ template: '<span>{{price}}</span>',
638
+ });
639
+ expect(result.success).toBe(true);
640
+ if (result.success) {
641
+ expect(result.data).toEqual({
642
+ id: 'comp-1',
643
+ name: 'PriceTag',
644
+ category: 'commerce',
645
+ description: 'Displays a price',
646
+ slots: { price: { type: 'number' } },
647
+ template: '<span>{{price}}</span>',
648
+ });
649
+ }
650
+ });
651
+
652
+ it('should reject missing required fields', () => {
653
+ const result = ComponentDefinitionSchema.safeParse({ id: 'comp-1', name: 'PriceTag' });
654
+ expect(result.success).toBe(false);
655
+ });
656
+ });
657
+
658
+ describe('DesignSchema ui field', () => {
659
+ it('should accept design with optional ui', () => {
660
+ const result = DesignSchema.safeParse({
661
+ ui: {
662
+ layoutId: 'sidebar',
663
+ mode: 'as-is',
664
+ regions: { sidebar: [{ id: 'nav', name: 'NavMenu' }] },
665
+ },
666
+ });
667
+ expect(result.success).toBe(true);
668
+ if (result.success) {
669
+ expect(result.data.ui?.layoutId).toBe('sidebar');
670
+ }
671
+ });
672
+
673
+ it('should accept design without ui', () => {
674
+ const result = DesignSchema.safeParse({});
675
+ expect(result.success).toBe(true);
676
+ if (result.success) {
677
+ expect(result.data.ui).toBeUndefined();
678
+ }
679
+ });
680
+ });
681
+
682
+ describe('modelSchema design.components field', () => {
683
+ const minimalModel = {
684
+ variant: 'specs' as const,
685
+ narratives: [],
686
+ messages: [],
687
+ modules: [],
688
+ };
689
+
690
+ it('should accept model with design.components', () => {
691
+ const result = modelSchema.safeParse({
692
+ ...minimalModel,
693
+ design: {
694
+ components: [
695
+ {
696
+ id: 'comp-1',
697
+ name: 'PriceTag',
698
+ category: 'commerce',
699
+ description: 'Displays a price',
700
+ slots: { price: { type: 'number' } },
701
+ template: '<span>{{price}}</span>',
702
+ },
703
+ ],
704
+ },
705
+ });
706
+ expect(result.success).toBe(true);
707
+ if (result.success) {
708
+ expect(result.data.design?.components).toHaveLength(1);
709
+ expect(result.data.design?.components?.[0].name).toBe('PriceTag');
710
+ }
711
+ });
712
+
713
+ it('should accept model design without components', () => {
714
+ const result = modelSchema.safeParse({
715
+ ...minimalModel,
716
+ design: {},
717
+ });
718
+ expect(result.success).toBe(true);
719
+ if (result.success) {
720
+ expect(result.data.design?.components).toBeUndefined();
721
+ }
722
+ });
723
+ });
724
+
725
+ describe('exported UI types', () => {
726
+ it('RegionEntry type matches RegionEntrySchema inference', () => {
727
+ const parsed = RegionEntrySchema.parse({ id: 'r-1', name: 'Header' });
728
+ const typed: RegionEntry = parsed;
729
+ expect(typed).toEqual({ id: 'r-1', name: 'Header' });
730
+ });
731
+
732
+ it('UI type matches UISchema inference', () => {
733
+ const parsed = UISchema.parse({ layoutId: 'l', mode: 'as-is', regions: {} });
734
+ const typed: UI = parsed;
735
+ expect(typed).toEqual({ layoutId: 'l', mode: 'as-is', regions: {} });
736
+ });
737
+
738
+ it('ComponentDefinition type matches ComponentDefinitionSchema inference', () => {
739
+ const parsed = ComponentDefinitionSchema.parse({
740
+ id: 'c-1',
741
+ name: 'Btn',
742
+ category: 'ui',
743
+ description: 'A button',
744
+ slots: {},
745
+ template: '<button/>',
746
+ });
747
+ const typed: ComponentDefinition = parsed;
748
+ expect(typed.name).toBe('Btn');
749
+ });
750
+ });
package/src/schema.ts CHANGED
@@ -218,10 +218,39 @@ export const ImageAssetSchema = z
218
218
  })
219
219
  .describe('Image asset with optional generation metadata');
220
220
 
221
+ const RegionEntrySchema = z
222
+ .object({
223
+ id: z.string(),
224
+ name: z.string(),
225
+ slots: z.record(z.unknown()).optional(),
226
+ })
227
+ .describe('Component placed in a layout region');
228
+
229
+ const UISchema = z
230
+ .object({
231
+ layoutId: z.string(),
232
+ mode: z.enum(['as-is', 'modify', 'custom']),
233
+ regions: z.record(z.array(RegionEntrySchema)),
234
+ customizationNotes: z.string().optional(),
235
+ })
236
+ .describe('UI composition for a slice');
237
+
238
+ const ComponentDefinitionSchema = z
239
+ .object({
240
+ id: z.string(),
241
+ name: z.string(),
242
+ category: z.string(),
243
+ description: z.string(),
244
+ slots: z.record(z.unknown()),
245
+ template: z.string(),
246
+ })
247
+ .describe('Custom reusable component definition');
248
+
221
249
  export const DesignSchema = z
222
250
  .object({
223
251
  imageAsset: ImageAssetSchema.optional().describe('Primary image asset for this entity'),
224
252
  metadata: z.record(z.unknown()).optional().describe('Flexible design metadata'),
253
+ ui: UISchema.optional().describe('UI composition for this entity'),
225
254
  })
226
255
  .describe('Design fields for visual representation');
227
256
 
@@ -513,6 +542,10 @@ export const ClientServerNamesSchema = z
513
542
  })
514
543
  .describe('System with client/server descriptions for behavior planning');
515
544
 
545
+ const ModelDesignSchema = DesignSchema.extend({
546
+ components: z.array(ComponentDefinitionSchema).optional().describe('Custom reusable component definitions'),
547
+ }).describe('Model-level design fields with component definitions');
548
+
516
549
  export const modelSchema = z
517
550
  .object({
518
551
  variant: z.literal('specs').describe('Full specification with all details'),
@@ -521,7 +554,7 @@ export const modelSchema = z
521
554
  integrations: z.array(IntegrationSchema).optional(),
522
555
  modules: z.array(ModuleSchema).describe('Modules for type ownership and file grouping'),
523
556
  journeys: z.array(JourneySchema).optional(),
524
- design: DesignSchema.optional().describe('Design fields for visual representation'),
557
+ design: ModelDesignSchema.optional().describe('Design fields for visual representation'),
525
558
  })
526
559
  .describe('Complete system specification with all implementation details');
527
560
 
@@ -551,6 +584,9 @@ export {
551
584
  StepErrorSchema,
552
585
  StepWithDocStringSchema,
553
586
  StepWithErrorSchema,
587
+ RegionEntrySchema,
588
+ UISchema,
589
+ ComponentDefinitionSchema,
554
590
  };
555
591
 
556
592
  export type Model = z.infer<typeof modelSchema>;
@@ -577,3 +613,6 @@ export type SceneRoute = z.infer<typeof SceneRouteSchema>;
577
613
  export type JourneyPlanning = z.infer<typeof JourneyPlanningSchema>;
578
614
  export type ImageAsset = z.infer<typeof ImageAssetSchema>;
579
615
  export type Design = z.infer<typeof DesignSchema>;
616
+ export type RegionEntry = z.infer<typeof RegionEntrySchema>;
617
+ export type UI = z.infer<typeof UISchema>;
618
+ export type ComponentDefinition = z.infer<typeof ComponentDefinitionSchema>;