@auto-engineer/narrative 1.144.0 → 1.145.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.144.0",
30
- "@auto-engineer/id": "1.144.0",
31
- "@auto-engineer/message-bus": "1.144.0"
29
+ "@auto-engineer/file-store": "1.145.0",
30
+ "@auto-engineer/id": "1.145.0",
31
+ "@auto-engineer/message-bus": "1.145.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.144.0",
41
+ "version": "1.145.0",
42
42
  "scripts": {
43
43
  "build": "tsx scripts/build.ts",
44
44
  "test": "vitest run --reporter=dot",
@@ -3,10 +3,11 @@ import type {
3
3
  ComponentDefinition,
4
4
  Narrative,
5
5
  NarrativePlanning,
6
- RegionEntry,
7
6
  SceneClassification,
8
7
  SceneRoute,
9
8
  UI,
9
+ UIElement,
10
+ UISpec,
10
11
  } from './schema';
11
12
  import {
12
13
  CommandMomentSchema,
@@ -18,12 +19,13 @@ import {
18
19
  NarrativePlanningSchema,
19
20
  NarrativeSchema,
20
21
  QueryMomentSchema,
21
- RegionEntrySchema,
22
22
  SceneClassificationSchema,
23
23
  SceneNamesOnlySchema,
24
24
  SceneRouteSchema,
25
25
  SceneSchema,
26
+ UIElementSchema,
26
27
  UISchema,
28
+ UISpecSchema,
27
29
  } from './schema';
28
30
 
29
31
  describe('CommandMomentSchema', () => {
@@ -547,89 +549,70 @@ describe('QueryMomentSchema', () => {
547
549
  });
548
550
  });
549
551
 
550
- describe('RegionEntrySchema', () => {
551
- it('should accept valid region entry with required fields', () => {
552
- const result = RegionEntrySchema.safeParse({ id: 'hero-1', name: 'HeroBanner' });
552
+ describe('UIElementSchema', () => {
553
+ it('should accept a minimal element with type only', () => {
554
+ const result = UIElementSchema.safeParse({ type: 'Stack' });
553
555
  expect(result.success).toBe(true);
554
- if (result.success) {
555
- expect(result.data).toEqual({ id: 'hero-1', name: 'HeroBanner' });
556
- }
557
- });
558
-
559
- it('should accept optional slots', () => {
560
- const result = RegionEntrySchema.safeParse({ id: 'card-1', name: 'Card', slots: { title: 'Hello' } });
561
- expect(result.success).toBe(true);
562
- if (result.success) {
563
- expect(result.data.slots).toEqual({ title: 'Hello' });
564
- }
565
556
  });
566
557
 
567
- it('should reject missing id', () => {
568
- const result = RegionEntrySchema.safeParse({ name: 'Card' });
569
- expect(result.success).toBe(false);
558
+ it('should accept an element with all fields', () => {
559
+ const element = {
560
+ type: 'Heading',
561
+ props: { text: { $state: '/title' }, level: 'h1' },
562
+ children: ['subtitle'],
563
+ visible: { $state: '/showTitle' },
564
+ repeat: { statePath: '/items', key: 'id' },
565
+ watch: { '/count': { action: 'refresh' } },
566
+ };
567
+ expect(UIElementSchema.parse(element)).toEqual(element);
570
568
  });
571
569
 
572
- it('should reject missing name', () => {
573
- const result = RegionEntrySchema.safeParse({ id: 'card-1' });
574
- expect(result.success).toBe(false);
570
+ it('should accept repeat with object statePath for nested repeats', () => {
571
+ const element = {
572
+ type: 'Stack',
573
+ children: [],
574
+ repeat: { statePath: { $item: 'fields' }, key: 'name' },
575
+ };
576
+ expect(UIElementSchema.parse(element)).toEqual(element);
575
577
  });
576
578
  });
577
579
 
578
- describe('UISchema', () => {
579
- it('should accept valid UI with required fields', () => {
580
- const result = UISchema.safeParse({
581
- layoutId: 'two-column',
582
- mode: 'as-is',
583
- regions: { main: [{ id: 'c-1', name: 'List' }] },
584
- });
585
- expect(result.success).toBe(true);
586
- if (result.success) {
587
- expect(result.data).toEqual({
588
- layoutId: 'two-column',
589
- mode: 'as-is',
590
- regions: { main: [{ id: 'c-1', name: 'List' }] },
591
- });
592
- }
593
- });
594
-
595
- it('should accept all valid mode values', () => {
596
- for (const mode of ['as-is', 'modify', 'custom'] as const) {
597
- const result = UISchema.safeParse({ layoutId: 'l', mode, regions: {} });
598
- expect(result.success).toBe(true);
599
- }
600
- });
601
-
602
- it('should reject invalid mode', () => {
603
- const result = UISchema.safeParse({ layoutId: 'l', mode: 'invalid', regions: {} });
604
- expect(result.success).toBe(false);
580
+ describe('UISpecSchema', () => {
581
+ it('should accept a valid spec with root, elements, and state', () => {
582
+ const spec = {
583
+ root: 'page',
584
+ elements: {
585
+ page: { type: 'Stack', props: { direction: 'vertical' }, children: ['heading'] },
586
+ heading: { type: 'Heading', props: { text: 'Hello' }, children: [] },
587
+ },
588
+ state: { title: 'Hello' },
589
+ };
590
+ expect(UISpecSchema.parse(spec)).toEqual(spec);
605
591
  });
606
592
 
607
- it('should accept optional customizationNotes', () => {
608
- const result = UISchema.safeParse({
609
- layoutId: 'l',
610
- mode: 'modify',
611
- regions: {},
612
- customizationNotes: 'Make header sticky',
613
- });
614
- expect(result.success).toBe(true);
615
- if (result.success) {
616
- expect(result.data.customizationNotes).toBe('Make header sticky');
617
- }
593
+ it('should reject a spec missing root', () => {
594
+ expect(UISpecSchema.safeParse({ elements: {} }).success).toBe(false);
618
595
  });
619
596
 
620
- it('should reject missing layoutId', () => {
621
- const result = UISchema.safeParse({ mode: 'as-is', regions: {} });
622
- expect(result.success).toBe(false);
597
+ it('should reject a spec missing elements', () => {
598
+ expect(UISpecSchema.safeParse({ root: 'x' }).success).toBe(false);
623
599
  });
600
+ });
624
601
 
625
- it('should reject missing mode', () => {
626
- const result = UISchema.safeParse({ layoutId: 'l', regions: {} });
627
- expect(result.success).toBe(false);
602
+ describe('UISchema', () => {
603
+ it('should accept an empty object since spec is optional', () => {
604
+ expect(UISchema.parse({})).toEqual({});
628
605
  });
629
606
 
630
- it('should reject missing regions', () => {
631
- const result = UISchema.safeParse({ layoutId: 'l', mode: 'as-is' });
632
- expect(result.success).toBe(false);
607
+ it('should accept a UI with a spec', () => {
608
+ const ui = {
609
+ spec: {
610
+ root: 'page',
611
+ elements: { page: { type: 'Stack', children: [] } },
612
+ state: { title: 'Dashboard' },
613
+ },
614
+ };
615
+ expect(UISchema.parse(ui)).toEqual(ui);
633
616
  });
634
617
  });
635
618
 
@@ -663,26 +646,21 @@ describe('ComponentDefinitionSchema', () => {
663
646
  });
664
647
 
665
648
  describe('DesignSchema ui field', () => {
666
- it('should accept design with optional ui', () => {
667
- const result = DesignSchema.safeParse({
649
+ it('should accept design with ui containing a spec', () => {
650
+ const design = {
668
651
  ui: {
669
- layoutId: 'sidebar',
670
- mode: 'as-is',
671
- regions: { sidebar: [{ id: 'nav', name: 'NavMenu' }] },
652
+ spec: {
653
+ root: 'page',
654
+ elements: { page: { type: 'Stack', children: [] } },
655
+ state: { title: 'Dashboard' },
656
+ },
672
657
  },
673
- });
674
- expect(result.success).toBe(true);
675
- if (result.success) {
676
- expect(result.data.ui?.layoutId).toBe('sidebar');
677
- }
658
+ };
659
+ expect(DesignSchema.parse(design)).toEqual(design);
678
660
  });
679
661
 
680
662
  it('should accept design without ui', () => {
681
- const result = DesignSchema.safeParse({});
682
- expect(result.success).toBe(true);
683
- if (result.success) {
684
- expect(result.data.ui).toBeUndefined();
685
- }
663
+ expect(DesignSchema.parse({})).toEqual({});
686
664
  });
687
665
  });
688
666
 
@@ -731,16 +709,23 @@ describe('modelSchema design.components field', () => {
731
709
  });
732
710
 
733
711
  describe('exported UI types', () => {
734
- it('RegionEntry type matches RegionEntrySchema inference', () => {
735
- const parsed = RegionEntrySchema.parse({ id: 'r-1', name: 'Header' });
736
- const typed: RegionEntry = parsed;
737
- expect(typed).toEqual({ id: 'r-1', name: 'Header' });
712
+ it('UIElement type matches UIElementSchema inference', () => {
713
+ const parsed = UIElementSchema.parse({ type: 'Stack', children: [] });
714
+ const typed: UIElement = parsed;
715
+ expect(typed).toEqual({ type: 'Stack', children: [] });
716
+ });
717
+
718
+ it('UISpec type matches UISpecSchema inference', () => {
719
+ const spec = { root: 'page', elements: { page: { type: 'Stack', children: [] } } };
720
+ const parsed = UISpecSchema.parse(spec);
721
+ const typed: UISpec = parsed;
722
+ expect(typed).toEqual(spec);
738
723
  });
739
724
 
740
725
  it('UI type matches UISchema inference', () => {
741
- const parsed = UISchema.parse({ layoutId: 'l', mode: 'as-is', regions: {} });
726
+ const parsed = UISchema.parse({});
742
727
  const typed: UI = parsed;
743
- expect(typed).toEqual({ layoutId: 'l', mode: 'as-is', regions: {} });
728
+ expect(typed).toEqual({});
744
729
  });
745
730
 
746
731
  it('ComponentDefinition type matches ComponentDefinitionSchema inference', () => {
@@ -753,6 +738,6 @@ describe('exported UI types', () => {
753
738
  template: '<button/>',
754
739
  });
755
740
  const typed: ComponentDefinition = parsed;
756
- expect(typed.name).toBe('Btn');
741
+ expect(typed).toEqual(parsed);
757
742
  });
758
743
  });
package/src/schema.ts CHANGED
@@ -218,20 +218,32 @@ export const ImageAssetSchema = z
218
218
  })
219
219
  .describe('Image asset with optional generation metadata');
220
220
 
221
- const RegionEntrySchema = z
221
+ export const UIElementSchema = z
222
+ .object({
223
+ type: z.string(),
224
+ props: z.record(z.unknown()).optional(),
225
+ children: z.array(z.string()).optional(),
226
+ visible: z.unknown().optional(),
227
+ repeat: z
228
+ .object({ statePath: z.union([z.string(), z.record(z.unknown())]) })
229
+ .passthrough()
230
+ .optional(),
231
+ watch: z.record(z.unknown()).optional(),
232
+ })
233
+ .passthrough()
234
+ .describe('Single UI element in a UI spec');
235
+
236
+ export const UISpecSchema = z
222
237
  .object({
223
- id: z.string(),
224
- name: z.string(),
225
- slots: z.record(z.unknown()).optional(),
238
+ root: z.string(),
239
+ elements: z.record(UIElementSchema),
240
+ state: z.record(z.unknown()).optional(),
226
241
  })
227
- .describe('Component placed in a layout region');
242
+ .describe('Flat element-map UI specification');
228
243
 
229
244
  const UISchema = z
230
245
  .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(),
246
+ spec: UISpecSchema.optional().describe('The rendered UI spec — source of truth for rendering'),
235
247
  })
236
248
  .describe('UI composition for a moment');
237
249
 
@@ -584,7 +596,6 @@ export {
584
596
  StepErrorSchema,
585
597
  StepWithDocStringSchema,
586
598
  StepWithErrorSchema,
587
- RegionEntrySchema,
588
599
  UISchema,
589
600
  ComponentDefinitionSchema,
590
601
  };
@@ -613,6 +624,7 @@ export type SceneRoute = z.infer<typeof SceneRouteSchema>;
613
624
  export type NarrativePlanning = z.infer<typeof NarrativePlanningSchema>;
614
625
  export type ImageAsset = z.infer<typeof ImageAssetSchema>;
615
626
  export type Design = z.infer<typeof DesignSchema>;
616
- export type RegionEntry = z.infer<typeof RegionEntrySchema>;
627
+ export type UIElement = z.infer<typeof UIElementSchema>;
628
+ export type UISpec = z.infer<typeof UISpecSchema>;
617
629
  export type UI = z.infer<typeof UISchema>;
618
630
  export type ComponentDefinition = z.infer<typeof ComponentDefinitionSchema>;