@auto-engineer/narrative 1.150.0 → 1.152.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/.turbo/turbo-build.log +1 -1
- package/.turbo/turbo-test.log +5 -5
- package/.turbo/turbo-type-check.log +1 -1
- package/CHANGELOG.md +75 -0
- package/dist/src/fluent-builder.d.ts +4 -0
- package/dist/src/fluent-builder.d.ts.map +1 -1
- package/dist/src/fluent-builder.js +15 -0
- package/dist/src/fluent-builder.js.map +1 -1
- package/dist/src/schema.d.ts +1234 -0
- package/dist/src/schema.d.ts.map +1 -1
- package/dist/src/schema.js +15 -0
- package/dist/src/schema.js.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.d.ts.map +1 -1
- package/dist/src/transformers/model-to-narrative/generators/flow.js +9 -0
- package/dist/src/transformers/model-to-narrative/generators/flow.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/fluent-builder.specs.ts +48 -2
- package/src/fluent-builder.ts +22 -1
- package/src/model-to-narrative.specs.ts +38 -0
- package/src/schema.specs.ts +33 -0
- package/src/schema.ts +17 -0
- package/src/transformers/model-to-narrative/generators/flow.ts +15 -0
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/
|
|
30
|
-
"@auto-engineer/
|
|
31
|
-
"@auto-engineer/
|
|
29
|
+
"@auto-engineer/id": "1.152.0",
|
|
30
|
+
"@auto-engineer/message-bus": "1.152.0",
|
|
31
|
+
"@auto-engineer/file-store": "1.152.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.
|
|
41
|
+
"version": "1.152.0",
|
|
42
42
|
"scripts": {
|
|
43
43
|
"build": "tsx scripts/build.ts",
|
|
44
44
|
"test": "vitest run --reporter=dot",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import { command, react } from './fluent-builder';
|
|
3
|
-
import { clearCurrentScene, startScene } from './narrative-context';
|
|
2
|
+
import { command, experience, query, react } from './fluent-builder';
|
|
3
|
+
import { clearCurrentScene, getCurrentScene, startScene } from './narrative-context';
|
|
4
4
|
import { createIntegration } from './types';
|
|
5
5
|
|
|
6
6
|
// Test integrations
|
|
@@ -34,3 +34,49 @@ describe('via method', () => {
|
|
|
34
34
|
expect(slice).toBeDefined();
|
|
35
35
|
});
|
|
36
36
|
});
|
|
37
|
+
|
|
38
|
+
describe('ui method', () => {
|
|
39
|
+
beforeEach(() => {
|
|
40
|
+
startScene('test-flow');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
afterEach(() => {
|
|
44
|
+
clearCurrentScene();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it('sets client.ui on a command moment', () => {
|
|
48
|
+
const uiBlock = { layoutId: 'centered-narrow', surface: 'route' as const };
|
|
49
|
+
command('Submit Order').ui(uiBlock);
|
|
50
|
+
|
|
51
|
+
const scene = getCurrentScene();
|
|
52
|
+
const moment = scene!.moments[0];
|
|
53
|
+
expect(moment.type).toBe('command');
|
|
54
|
+
if (moment.type === 'command') {
|
|
55
|
+
expect(moment.client.ui).toEqual(uiBlock);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('sets client.ui on a query moment', () => {
|
|
60
|
+
const uiBlock = { layoutId: 'two-column', surface: 'route' as const };
|
|
61
|
+
query('View Items').ui(uiBlock);
|
|
62
|
+
|
|
63
|
+
const scene = getCurrentScene();
|
|
64
|
+
const moment = scene!.moments[0];
|
|
65
|
+
expect(moment.type).toBe('query');
|
|
66
|
+
if (moment.type === 'query') {
|
|
67
|
+
expect(moment.client.ui).toEqual(uiBlock);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('sets client.ui on an experience moment', () => {
|
|
72
|
+
const uiBlock = { layoutId: 'full-width', surface: 'ephemeral' as const };
|
|
73
|
+
experience('Welcome Tour').ui(uiBlock);
|
|
74
|
+
|
|
75
|
+
const scene = getCurrentScene();
|
|
76
|
+
const moment = scene!.moments[0];
|
|
77
|
+
expect(moment.type).toBe('experience');
|
|
78
|
+
if (moment.type === 'experience') {
|
|
79
|
+
expect(moment.client.ui).toEqual(uiBlock);
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
});
|
package/src/fluent-builder.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import createDebug from 'debug';
|
|
2
2
|
import { type ASTNode, print } from 'graphql';
|
|
3
|
-
import type { CommandMoment, ExperienceMoment, QueryMoment, ReactMoment } from './index';
|
|
3
|
+
import type { CommandMoment, ExperienceMoment, QueryMoment, ReactMoment, UiBlock } from './index';
|
|
4
4
|
import {
|
|
5
5
|
addMoment,
|
|
6
6
|
endClientBlock,
|
|
@@ -38,6 +38,7 @@ export interface FluentCommandMomentBuilder {
|
|
|
38
38
|
client(description: string, fn: () => void): FluentCommandMomentBuilder;
|
|
39
39
|
server(fn: () => void): FluentCommandMomentBuilder;
|
|
40
40
|
server(description: string, fn: () => void): FluentCommandMomentBuilder;
|
|
41
|
+
ui(spec: UiBlock): FluentCommandMomentBuilder;
|
|
41
42
|
via(integration: Integration | Integration[]): FluentCommandMomentBuilder;
|
|
42
43
|
retries(count: number): FluentCommandMomentBuilder;
|
|
43
44
|
request(mutation: unknown): FluentCommandMomentBuilder;
|
|
@@ -48,6 +49,7 @@ export interface FluentQueryMomentBuilder {
|
|
|
48
49
|
client(description: string, fn: () => void): FluentQueryMomentBuilder;
|
|
49
50
|
server(fn: () => void): FluentQueryMomentBuilder;
|
|
50
51
|
server(description: string, fn: () => void): FluentQueryMomentBuilder;
|
|
52
|
+
ui(spec: UiBlock): FluentQueryMomentBuilder;
|
|
51
53
|
request(query: unknown): FluentQueryMomentBuilder;
|
|
52
54
|
}
|
|
53
55
|
|
|
@@ -61,6 +63,7 @@ export interface FluentReactionMomentBuilder {
|
|
|
61
63
|
export interface FluentExperienceMomentBuilder {
|
|
62
64
|
client(fn: () => void): FluentExperienceMomentBuilder;
|
|
63
65
|
client(description: string, fn: () => void): FluentExperienceMomentBuilder;
|
|
66
|
+
ui(spec: UiBlock): FluentExperienceMomentBuilder;
|
|
64
67
|
}
|
|
65
68
|
|
|
66
69
|
class CommandMomentBuilderImpl implements FluentCommandMomentBuilder {
|
|
@@ -132,6 +135,12 @@ class CommandMomentBuilderImpl implements FluentCommandMomentBuilder {
|
|
|
132
135
|
return this;
|
|
133
136
|
}
|
|
134
137
|
|
|
138
|
+
ui(spec: UiBlock): FluentCommandMomentBuilder {
|
|
139
|
+
debugCommand('Setting client.ui for moment %s', this.moment.name);
|
|
140
|
+
this.moment.client.ui = spec;
|
|
141
|
+
return this;
|
|
142
|
+
}
|
|
143
|
+
|
|
135
144
|
via(integration: Integration | Integration[]): FluentCommandMomentBuilder {
|
|
136
145
|
const integrations = Array.isArray(integration) ? integration : [integration];
|
|
137
146
|
this.moment.via = integrations.map((i) => i.name);
|
|
@@ -232,6 +241,12 @@ class QueryMomentBuilderImpl implements FluentQueryMomentBuilder {
|
|
|
232
241
|
return this;
|
|
233
242
|
}
|
|
234
243
|
|
|
244
|
+
ui(spec: UiBlock): FluentQueryMomentBuilder {
|
|
245
|
+
debugQuery('Setting client.ui for moment %s', this.moment.name);
|
|
246
|
+
this.moment.client.ui = spec;
|
|
247
|
+
return this;
|
|
248
|
+
}
|
|
249
|
+
|
|
235
250
|
request(query: unknown): FluentQueryMomentBuilder {
|
|
236
251
|
debugQuery('Setting request for moment %s', this.moment.name);
|
|
237
252
|
if (typeof query === 'string') {
|
|
@@ -346,6 +361,12 @@ class ExperienceMomentBuilderImpl implements FluentExperienceMomentBuilder {
|
|
|
346
361
|
|
|
347
362
|
return this;
|
|
348
363
|
}
|
|
364
|
+
|
|
365
|
+
ui(spec: UiBlock): FluentExperienceMomentBuilder {
|
|
366
|
+
debugExperience('Setting client.ui for moment %s', this.moment.name);
|
|
367
|
+
this.moment.client.ui = spec;
|
|
368
|
+
return this;
|
|
369
|
+
}
|
|
349
370
|
}
|
|
350
371
|
|
|
351
372
|
export const command = (name: string, id?: string): FluentCommandMomentBuilder => {
|
|
@@ -3569,4 +3569,42 @@ scene('All Projection Types', 'ALL-PROJ', () => {
|
|
|
3569
3569
|
expect(code).toContain("type FitnessGoalCreated = Event<'FitnessGoalCreated'");
|
|
3570
3570
|
expect(code).toContain("type FitnessGoalsView = State<'FitnessGoalsView'");
|
|
3571
3571
|
});
|
|
3572
|
+
|
|
3573
|
+
it('emits .ui() when moment has client.ui', async () => {
|
|
3574
|
+
const model: Model = {
|
|
3575
|
+
variant: 'specs',
|
|
3576
|
+
scenes: [
|
|
3577
|
+
{
|
|
3578
|
+
name: 'Checkout',
|
|
3579
|
+
moments: [
|
|
3580
|
+
{
|
|
3581
|
+
type: 'command',
|
|
3582
|
+
name: 'Submit Order',
|
|
3583
|
+
client: {
|
|
3584
|
+
specs: [],
|
|
3585
|
+
ui: {
|
|
3586
|
+
layoutId: 'centered-narrow',
|
|
3587
|
+
surface: 'route',
|
|
3588
|
+
spec: { root: 'layout-root', elements: {}, state: {} },
|
|
3589
|
+
},
|
|
3590
|
+
},
|
|
3591
|
+
server: { description: '', specs: [] },
|
|
3592
|
+
},
|
|
3593
|
+
],
|
|
3594
|
+
},
|
|
3595
|
+
],
|
|
3596
|
+
messages: [],
|
|
3597
|
+
integrations: [],
|
|
3598
|
+
modules: [],
|
|
3599
|
+
narratives: [],
|
|
3600
|
+
};
|
|
3601
|
+
|
|
3602
|
+
const result = await modelToNarrative(model);
|
|
3603
|
+
const code = getCode(result);
|
|
3604
|
+
|
|
3605
|
+
expect(code).toContain('.ui(');
|
|
3606
|
+
expect(code).toContain("layoutId: 'centered-narrow'");
|
|
3607
|
+
expect(code).toContain("surface: 'route'");
|
|
3608
|
+
expect(code).toContain("root: 'layout-root'");
|
|
3609
|
+
});
|
|
3572
3610
|
});
|
package/src/schema.specs.ts
CHANGED
|
@@ -57,6 +57,39 @@ describe('CommandMomentSchema', () => {
|
|
|
57
57
|
});
|
|
58
58
|
});
|
|
59
59
|
|
|
60
|
+
describe('CommandMomentSchema client.ui', () => {
|
|
61
|
+
it('accepts ui block in client alongside specs', () => {
|
|
62
|
+
const moment = {
|
|
63
|
+
type: 'command' as const,
|
|
64
|
+
name: 'Submit Order',
|
|
65
|
+
client: {
|
|
66
|
+
specs: [],
|
|
67
|
+
ui: {
|
|
68
|
+
layoutId: 'centered-narrow',
|
|
69
|
+
mode: 'as-is',
|
|
70
|
+
regions: { main: [{ id: 'r1', name: 'hero' }] },
|
|
71
|
+
spec: { root: 'layout-root', elements: {}, state: {} },
|
|
72
|
+
surface: 'route',
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
server: { description: 'Submits order', specs: [] },
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const result = CommandMomentSchema.safeParse(moment);
|
|
79
|
+
|
|
80
|
+
expect(result.success).toBe(true);
|
|
81
|
+
if (result.success) {
|
|
82
|
+
expect(result.data.client.ui).toEqual({
|
|
83
|
+
layoutId: 'centered-narrow',
|
|
84
|
+
mode: 'as-is',
|
|
85
|
+
regions: { main: [{ id: 'r1', name: 'hero' }] },
|
|
86
|
+
spec: { root: 'layout-root', elements: {}, state: {} },
|
|
87
|
+
surface: 'route',
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
60
93
|
describe('SceneRouteSchema', () => {
|
|
61
94
|
it('should accept valid route types', () => {
|
|
62
95
|
for (const type of ['dedicated', 'nested', 'no-route'] as const) {
|
package/src/schema.ts
CHANGED
|
@@ -351,10 +351,24 @@ export const ClientSpecNodeSchema: z.ZodType<ClientSpecNode> = z.lazy(() =>
|
|
|
351
351
|
|
|
352
352
|
export const ClientSpecSchema = z.array(ClientSpecNodeSchema).default([]);
|
|
353
353
|
|
|
354
|
+
export const UiBlockSchema = z
|
|
355
|
+
.object({
|
|
356
|
+
layoutId: z.string().optional().describe('Layout template identifier'),
|
|
357
|
+
mode: z.string().optional().describe('Rendering mode'),
|
|
358
|
+
regions: z
|
|
359
|
+
.record(z.array(z.object({ id: z.string(), name: z.string(), slots: z.record(z.unknown()).optional() })))
|
|
360
|
+
.optional()
|
|
361
|
+
.describe('Layout regions with placed components'),
|
|
362
|
+
spec: z.record(z.unknown()).optional().describe('UI specification tree'),
|
|
363
|
+
surface: z.enum(['route', 'overlay', 'ephemeral']).optional().describe('Presentation context'),
|
|
364
|
+
})
|
|
365
|
+
.describe('UI composition block for a moment');
|
|
366
|
+
|
|
354
367
|
const CommandMomentSchema = BaseMomentSchema.extend({
|
|
355
368
|
type: z.literal('command'),
|
|
356
369
|
client: z.object({
|
|
357
370
|
specs: ClientSpecSchema,
|
|
371
|
+
ui: UiBlockSchema.optional().describe('UI composition for this moment'),
|
|
358
372
|
}),
|
|
359
373
|
request: z.string().describe('Command request (GraphQL, REST endpoint, or other query format)').optional(),
|
|
360
374
|
mappings: z.array(MappingEntrySchema).optional().describe('Field mappings between Command/Event/State messages'),
|
|
@@ -369,6 +383,7 @@ const QueryMomentSchema = BaseMomentSchema.extend({
|
|
|
369
383
|
type: z.literal('query'),
|
|
370
384
|
client: z.object({
|
|
371
385
|
specs: ClientSpecSchema,
|
|
386
|
+
ui: UiBlockSchema.optional().describe('UI composition for this moment'),
|
|
372
387
|
}),
|
|
373
388
|
request: z.string().describe('Query request (GraphQL, REST endpoint, or other query format)').optional(),
|
|
374
389
|
mappings: z.array(MappingEntrySchema).optional().describe('Field mappings between Command/Event/State messages'),
|
|
@@ -392,6 +407,7 @@ const ExperienceMomentSchema = BaseMomentSchema.extend({
|
|
|
392
407
|
type: z.literal('experience'),
|
|
393
408
|
client: z.object({
|
|
394
409
|
specs: ClientSpecSchema,
|
|
410
|
+
ui: UiBlockSchema.optional().describe('UI composition for this moment'),
|
|
395
411
|
}),
|
|
396
412
|
}).describe('Experience moment for user interactions and UI behavior');
|
|
397
413
|
|
|
@@ -628,3 +644,4 @@ export type UIElement = z.infer<typeof UIElementSchema>;
|
|
|
628
644
|
export type UISpec = z.infer<typeof UISpecSchema>;
|
|
629
645
|
export type UI = z.infer<typeof UISchema>;
|
|
630
646
|
export type ComponentDefinition = z.infer<typeof ComponentDefinitionSchema>;
|
|
647
|
+
export type UiBlock = z.infer<typeof UiBlockSchema>;
|
|
@@ -424,6 +424,20 @@ function addClientToChain(
|
|
|
424
424
|
return chain;
|
|
425
425
|
}
|
|
426
426
|
|
|
427
|
+
function addUiToChain(
|
|
428
|
+
ts: typeof import('typescript'),
|
|
429
|
+
f: tsNS.NodeFactory,
|
|
430
|
+
chain: tsNS.Expression,
|
|
431
|
+
slice: Moment,
|
|
432
|
+
): tsNS.Expression {
|
|
433
|
+
if ('client' in slice && slice.client?.ui !== undefined) {
|
|
434
|
+
return f.createCallExpression(f.createPropertyAccessExpression(chain, f.createIdentifier('ui')), undefined, [
|
|
435
|
+
jsonToExpr(ts, f, slice.client.ui),
|
|
436
|
+
]);
|
|
437
|
+
}
|
|
438
|
+
return chain;
|
|
439
|
+
}
|
|
440
|
+
|
|
427
441
|
function addRequestToChain(
|
|
428
442
|
f: tsNS.NodeFactory,
|
|
429
443
|
chain: tsNS.Expression,
|
|
@@ -767,6 +781,7 @@ function buildMoment(
|
|
|
767
781
|
let chain: tsNS.Expression = f.createCallExpression(f.createIdentifier(sliceCtor), undefined, args);
|
|
768
782
|
|
|
769
783
|
chain = addClientToChain(ts, f, chain, slice);
|
|
784
|
+
chain = addUiToChain(ts, f, chain, slice);
|
|
770
785
|
chain = addRequestToChain(f, chain, slice);
|
|
771
786
|
chain = addServerToChain(ts, f, chain, slice, messages);
|
|
772
787
|
|