@auto-engineer/narrative 1.3.3 → 1.4.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 (54) 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 +31 -0
  5. package/dist/src/index.d.ts +3 -2
  6. package/dist/src/index.d.ts.map +1 -1
  7. package/dist/src/index.js +2 -1
  8. package/dist/src/index.js.map +1 -1
  9. package/dist/src/loader/ts-utils.d.ts +2 -2
  10. package/dist/src/loader/ts-utils.d.ts.map +1 -1
  11. package/dist/src/loader/ts-utils.js +7 -1
  12. package/dist/src/loader/ts-utils.js.map +1 -1
  13. package/dist/src/schema.d.ts +226 -20
  14. package/dist/src/schema.d.ts.map +1 -1
  15. package/dist/src/schema.js +6 -3
  16. package/dist/src/schema.js.map +1 -1
  17. package/dist/src/transformers/model-to-narrative/generators/imports.d.ts.map +1 -1
  18. package/dist/src/transformers/model-to-narrative/generators/imports.js +1 -0
  19. package/dist/src/transformers/model-to-narrative/generators/imports.js.map +1 -1
  20. package/dist/src/transformers/model-to-narrative/generators/module-code.js +10 -2
  21. package/dist/src/transformers/model-to-narrative/generators/module-code.js.map +1 -1
  22. package/dist/src/transformers/model-to-narrative/generators/types.d.ts +1 -1
  23. package/dist/src/transformers/model-to-narrative/generators/types.d.ts.map +1 -1
  24. package/dist/src/transformers/model-to-narrative/generators/types.js +2 -1
  25. package/dist/src/transformers/model-to-narrative/generators/types.js.map +1 -1
  26. package/dist/src/transformers/narrative-to-model/index.js.map +1 -1
  27. package/dist/src/transformers/narrative-to-model/messages.d.ts +1 -1
  28. package/dist/src/transformers/narrative-to-model/messages.d.ts.map +1 -1
  29. package/dist/src/transformers/narrative-to-model/messages.js +9 -1
  30. package/dist/src/transformers/narrative-to-model/messages.js.map +1 -1
  31. package/dist/src/transformers/narrative-to-model/spec-processors.d.ts +22 -1
  32. package/dist/src/transformers/narrative-to-model/spec-processors.d.ts.map +1 -1
  33. package/dist/src/transformers/narrative-to-model/spec-processors.js +81 -0
  34. package/dist/src/transformers/narrative-to-model/spec-processors.js.map +1 -1
  35. package/dist/src/transformers/narrative-to-model/type-inference.d.ts +1 -1
  36. package/dist/src/transformers/narrative-to-model/type-inference.d.ts.map +1 -1
  37. package/dist/src/transformers/narrative-to-model/type-inference.js.map +1 -1
  38. package/dist/src/types.d.ts +4 -0
  39. package/dist/src/types.d.ts.map +1 -1
  40. package/dist/tsconfig.tsbuildinfo +1 -1
  41. package/package.json +4 -4
  42. package/src/index.ts +4 -0
  43. package/src/loader/ts-utils.ts +16 -10
  44. package/src/model-to-narrative.specs.ts +123 -0
  45. package/src/schema.ts +7 -2
  46. package/src/transformers/model-to-narrative/generators/imports.ts +1 -0
  47. package/src/transformers/model-to-narrative/generators/module-code.ts +11 -2
  48. package/src/transformers/model-to-narrative/generators/types.ts +4 -5
  49. package/src/transformers/narrative-to-model/index.ts +5 -5
  50. package/src/transformers/narrative-to-model/messages.ts +12 -3
  51. package/src/transformers/narrative-to-model/spec-processors.specs.ts +241 -0
  52. package/src/transformers/narrative-to-model/spec-processors.ts +91 -4
  53. package/src/transformers/narrative-to-model/type-inference.ts +4 -4
  54. package/src/types.ts +5 -0
@@ -0,0 +1,241 @@
1
+ import { describe, expect, it } from 'vitest';
2
+ import type { Message } from '../../index';
3
+ import type { ExampleShapeHints } from './example-shapes';
4
+ import { detectQueryAction, extractQueryNameFromRequest, processWhen } from './spec-processors';
5
+
6
+ describe('spec-processors', () => {
7
+ describe('extractQueryNameFromRequest', () => {
8
+ it('should extract query name from simple GraphQL query', () => {
9
+ const request = 'query ViewWorkoutPlan { workoutPlan { id } }';
10
+ expect(extractQueryNameFromRequest(request)).toBe('ViewWorkoutPlan');
11
+ });
12
+
13
+ it('should extract query name from GraphQL query with variables', () => {
14
+ const request = 'query ViewWorkoutPlan($workoutId: ID!) { workoutPlan(id: $workoutId) { id name } }';
15
+ expect(extractQueryNameFromRequest(request)).toBe('ViewWorkoutPlan');
16
+ });
17
+
18
+ it('should extract query name case-insensitively', () => {
19
+ const request = 'QUERY GetUserProfile { user { id } }';
20
+ expect(extractQueryNameFromRequest(request)).toBe('GetUserProfile');
21
+ });
22
+
23
+ it('should return null for undefined request', () => {
24
+ expect(extractQueryNameFromRequest(undefined)).toBe(null);
25
+ });
26
+
27
+ it('should return null for empty string', () => {
28
+ expect(extractQueryNameFromRequest('')).toBe(null);
29
+ });
30
+
31
+ it('should return null for mutation', () => {
32
+ const request = 'mutation CreateWorkout { createWorkout { id } }';
33
+ expect(extractQueryNameFromRequest(request)).toBe(null);
34
+ });
35
+
36
+ it('should extract query name from JSON AST format', () => {
37
+ const ast = {
38
+ kind: 'Document',
39
+ definitions: [
40
+ {
41
+ kind: 'OperationDefinition',
42
+ operation: 'query',
43
+ name: { value: 'ListWorkouts' },
44
+ },
45
+ ],
46
+ };
47
+ expect(extractQueryNameFromRequest(JSON.stringify(ast))).toBe('ListWorkouts');
48
+ });
49
+ });
50
+
51
+ describe('detectQueryAction', () => {
52
+ it('should return true when whenText exactly matches query name from request', () => {
53
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan($id: ID!) { workoutPlan { id } }' };
54
+ expect(detectQueryAction('ViewWorkoutPlan', slice)).toBe(true);
55
+ });
56
+
57
+ it('should return true when whenText matches query name case-insensitively', () => {
58
+ const slice = { type: 'query', request: 'query viewWorkoutPlan { workoutPlan { id } }' };
59
+ expect(detectQueryAction('ViewWorkoutPlan', slice)).toBe(true);
60
+ });
61
+
62
+ it('should return false for command slices', () => {
63
+ const slice = { type: 'command', request: 'mutation CreateWorkout { createWorkout { id } }' };
64
+ expect(detectQueryAction('ViewWorkoutPlan', slice)).toBe(false);
65
+ });
66
+
67
+ it('should return false when no request field is provided (no naming convention fallback)', () => {
68
+ const slice = { type: 'query' };
69
+ // Without request field, cannot determine if it's a query action
70
+ expect(detectQueryAction('ViewWorkoutPlan', slice)).toBe(false);
71
+ expect(detectQueryAction('GetUserProfile', slice)).toBe(false);
72
+ expect(detectQueryAction('ListWorkouts', slice)).toBe(false);
73
+ expect(detectQueryAction('FindUserById', slice)).toBe(false);
74
+ expect(detectQueryAction('SearchProducts', slice)).toBe(false);
75
+ expect(detectQueryAction('FetchOrders', slice)).toBe(false);
76
+ });
77
+
78
+ it('should return false for event names in query slices', () => {
79
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan { workoutPlan { id } }' };
80
+ expect(detectQueryAction('WorkoutPlanCreated', slice)).toBe(false);
81
+ });
82
+
83
+ it('should return true for case-insensitive match to query name from request', () => {
84
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan { workoutPlan { id } }' };
85
+ // Case-insensitive matching works when request field is present
86
+ expect(detectQueryAction('viewWorkoutPlan', slice)).toBe(true);
87
+ });
88
+
89
+ it('should return false for empty whenText', () => {
90
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan { workoutPlan { id } }' };
91
+ expect(detectQueryAction('', slice)).toBe(false);
92
+ });
93
+ });
94
+
95
+ describe('processWhen', () => {
96
+ const mockTypeResolver = (t: string) => ({
97
+ resolvedName: t,
98
+ typeInfo: undefined,
99
+ });
100
+
101
+ const createEmptyHints = (): ExampleShapeHints => new Map();
102
+
103
+ describe('query action detection', () => {
104
+ it('should create a query message when When is a query action matching request', () => {
105
+ const messages: Map<string, Message> = new Map();
106
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan($id: ID!) { workoutPlan { id } }' };
107
+ const when = { commandRef: 'ViewWorkoutPlan', exampleData: { workoutId: 'wrk_123' } };
108
+
109
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
110
+
111
+ // Query message should be created for query actions
112
+ expect(messages.size).toBe(1);
113
+ expect(messages.has('ViewWorkoutPlan')).toBe(true);
114
+ expect(messages.get('ViewWorkoutPlan')?.type).toBe('query');
115
+ });
116
+
117
+ it('should create a message when no request field (cannot detect query action)', () => {
118
+ const messages: Map<string, Message> = new Map();
119
+ const slice = { type: 'query' }; // No request field
120
+ const when = { commandRef: 'ViewWorkoutHistory', exampleData: { userId: 'user_456' } };
121
+
122
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
123
+
124
+ // Without request field, cannot detect query action - treats as event
125
+ expect(messages.size).toBe(1);
126
+ expect(messages.has('ViewWorkoutHistory')).toBe(true);
127
+ });
128
+
129
+ it('should create a query message when When matches query name from request', () => {
130
+ const messages: Map<string, Message> = new Map();
131
+ const slice = { type: 'query', request: 'query GetUserProfile { user { id } }' };
132
+ const when = { commandRef: 'GetUserProfile', exampleData: { userId: 'user_123' } };
133
+
134
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
135
+
136
+ // Query action detected via request field - query message created
137
+ expect(messages.size).toBe(1);
138
+ expect(messages.has('GetUserProfile')).toBe(true);
139
+ expect(messages.get('GetUserProfile')?.type).toBe('query');
140
+ });
141
+
142
+ it('should create a message when When is an event name in query slice', () => {
143
+ const messages: Map<string, Message> = new Map();
144
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan { workoutPlan { id } }' };
145
+ const when = { commandRef: 'WorkoutPlanUpdated', exampleData: { workoutId: 'wrk_123' } };
146
+
147
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
148
+
149
+ // Event messages SHOULD be created
150
+ expect(messages.size).toBe(1);
151
+ expect(messages.has('WorkoutPlanUpdated')).toBe(true);
152
+ });
153
+
154
+ it('should create a message for command slices regardless of naming', () => {
155
+ const messages: Map<string, Message> = new Map();
156
+ const slice = { type: 'command' };
157
+ const when = { commandRef: 'ViewWorkoutPlan', exampleData: { workoutId: 'wrk_123' } };
158
+
159
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
160
+
161
+ // Command messages SHOULD be created even if it looks like a query action name
162
+ expect(messages.size).toBe(1);
163
+ expect(messages.has('ViewWorkoutPlan')).toBe(true);
164
+ });
165
+ });
166
+
167
+ describe('array of When items', () => {
168
+ it('should create query messages for query action items matching request', () => {
169
+ const messages: Map<string, Message> = new Map();
170
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan { workoutPlan { id } }' };
171
+ const when = [{ commandRef: 'ViewWorkoutPlan', exampleData: { workoutId: 'wrk_123' } }];
172
+
173
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
174
+
175
+ // Query action matches request - query message created
176
+ expect(messages.size).toBe(1);
177
+ expect(messages.has('ViewWorkoutPlan')).toBe(true);
178
+ expect(messages.get('ViewWorkoutPlan')?.type).toBe('query');
179
+ });
180
+
181
+ it('should create messages when no request field (cannot detect query action)', () => {
182
+ const messages: Map<string, Message> = new Map();
183
+ const slice = { type: 'query' }; // No request field
184
+ const when = [{ commandRef: 'ViewWorkoutPlan', exampleData: { workoutId: 'wrk_123' } }];
185
+
186
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
187
+
188
+ // Without request field, cannot detect query action - message created
189
+ expect(messages.size).toBe(1);
190
+ expect(messages.has('ViewWorkoutPlan')).toBe(true);
191
+ });
192
+
193
+ it('should create messages for event items in array', () => {
194
+ const messages: Map<string, Message> = new Map();
195
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan { workoutPlan { id } }' };
196
+ const when = [
197
+ { commandRef: 'WorkoutPlanCreated', exampleData: { workoutId: 'wrk_123' } },
198
+ { commandRef: 'WorkoutPlanUpdated', exampleData: { workoutId: 'wrk_123' } },
199
+ ];
200
+
201
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
202
+
203
+ expect(messages.size).toBe(2);
204
+ expect(messages.has('WorkoutPlanCreated')).toBe(true);
205
+ expect(messages.has('WorkoutPlanUpdated')).toBe(true);
206
+ });
207
+
208
+ it('should create query message for query action and event message in mixed array', () => {
209
+ const messages: Map<string, Message> = new Map();
210
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan { workoutPlan { id } }' };
211
+ const when = [
212
+ { commandRef: 'ViewWorkoutPlan', exampleData: { workoutId: 'wrk_123' } }, // Query action - create query message
213
+ { commandRef: 'WorkoutPlanUpdated', exampleData: { workoutId: 'wrk_123' } }, // Event - create event message
214
+ ];
215
+
216
+ processWhen(when, slice, mockTypeResolver, messages, createEmptyHints());
217
+
218
+ expect(messages.size).toBe(2);
219
+ expect(messages.has('ViewWorkoutPlan')).toBe(true);
220
+ expect(messages.get('ViewWorkoutPlan')?.type).toBe('query');
221
+ expect(messages.has('WorkoutPlanUpdated')).toBe(true);
222
+ });
223
+ });
224
+
225
+ describe('example shape hints collection', () => {
226
+ it('should collect example hints for query actions', () => {
227
+ const messages: Map<string, Message> = new Map();
228
+ const slice = { type: 'query', request: 'query ViewWorkoutPlan { workoutPlan { id } }' };
229
+ const when = { commandRef: 'ViewWorkoutPlan', exampleData: { workoutId: 'wrk_123', userId: 'user_456' } };
230
+ const hints = createEmptyHints();
231
+
232
+ processWhen(when, slice, mockTypeResolver, messages, hints);
233
+
234
+ // Query message created and hints should be collected
235
+ expect(messages.size).toBe(1);
236
+ expect(messages.get('ViewWorkoutPlan')?.type).toBe('query');
237
+ expect(hints.has('ViewWorkoutPlan')).toBe(true);
238
+ });
239
+ });
240
+ });
241
+ });
@@ -7,9 +7,62 @@ import { preferNewFields } from './normalize';
7
7
 
8
8
  const log = debug('auto:flow:spec-processors');
9
9
 
10
+ /**
11
+ * Extracts the query name from a GraphQL request string.
12
+ * Supports both SDL strings and JSON-serialized AST.
13
+ */
14
+ export function extractQueryNameFromRequest(request: string | undefined): string | null {
15
+ if (!request) return null;
16
+ const queryMatch = request.match(/query\s+(\w+)/i);
17
+ if (queryMatch) {
18
+ return queryMatch[1];
19
+ }
20
+ if (request.startsWith('{') && request.includes('"kind"')) {
21
+ try {
22
+ const ast = JSON.parse(request) as unknown;
23
+ if (
24
+ typeof ast === 'object' &&
25
+ ast !== null &&
26
+ 'definitions' in ast &&
27
+ Array.isArray((ast as { definitions: unknown[] }).definitions)
28
+ ) {
29
+ const definitions = (ast as { definitions: Array<{ kind?: string; name?: { value?: string } }> }).definitions;
30
+ const opDef = definitions.find((d) => d.kind === 'OperationDefinition');
31
+ if (opDef?.name?.value) {
32
+ return opDef.name.value;
33
+ }
34
+ }
35
+ } catch {}
36
+ }
37
+
38
+ return null;
39
+ }
40
+
41
+ /**
42
+ * Detects if the "When" text in a query slice represents a query action (query name)
43
+ * rather than an event name.
44
+ *
45
+ * Detection is based solely on matching the query name extracted from slice.request.
46
+ * If no query name can be extracted, returns false (treats as event - safe default).
47
+ *
48
+ * @param whenText - The text from the "When" step
49
+ * @param slice - The slice object containing type and optional request field
50
+ * @returns true if the "When" text matches the query name from slice.request
51
+ */
52
+ export function detectQueryAction(whenText: string, slice: { type: string; request?: string }): boolean {
53
+ if (slice.type !== 'query') return false;
54
+ if (!whenText) return false;
55
+
56
+ const queryName = extractQueryNameFromRequest(slice.request);
57
+ if (!queryName) return false;
58
+
59
+ // Exact match or case-insensitive match
60
+ return whenText === queryName || whenText.toLowerCase() === queryName.toLowerCase();
61
+ }
62
+
10
63
  type TypeResolver = (
11
64
  t: string,
12
- expected?: 'command' | 'event' | 'state',
65
+ expected?: 'command' | 'event' | 'state' | 'query',
13
66
  exampleData?: unknown,
14
67
  ) => { resolvedName: string; typeInfo: TypeInfo | undefined };
15
68
 
@@ -316,7 +369,7 @@ function processSingleWhen(
316
369
  stateRef?: string;
317
370
  exampleData?: unknown;
318
371
  },
319
- slice: { type: string },
372
+ slice: { type: string; request?: string },
320
373
  resolveTypeAndInfo: TypeResolver,
321
374
  messages: Map<string, Message>,
322
375
  exampleShapeHints: ExampleShapeHints,
@@ -326,6 +379,23 @@ function processSingleWhen(
326
379
  }
327
380
 
328
381
  const originalCommandRef = when.commandRef;
382
+
383
+ // Check if this is a query action
384
+ // Query actions represent the act of executing the query - create a query message for them
385
+ if (detectQueryAction(originalCommandRef, slice)) {
386
+ log('DEBUG processSingleWhen: detected query action, creating query message:', originalCommandRef);
387
+ const { typeInfo } = resolveTypeAndInfo(originalCommandRef, 'query', when.exampleData);
388
+ const msg = createMessage(originalCommandRef, typeInfo, 'query');
389
+ const existing = messages.get(originalCommandRef);
390
+ if (!existing || preferNewFields(msg.fields, existing.fields)) {
391
+ messages.set(originalCommandRef, msg);
392
+ }
393
+ if (when.exampleData !== undefined) {
394
+ collectExampleHintsForData(originalCommandRef, when.exampleData, exampleShapeHints);
395
+ }
396
+ return;
397
+ }
398
+
329
399
  const expected = slice.type === 'command' ? 'command' : 'event';
330
400
 
331
401
  log('DEBUG processSingleWhen:', {
@@ -418,7 +488,7 @@ function processCommandRefInArray(
418
488
  stateRef?: string;
419
489
  exampleData?: unknown;
420
490
  },
421
- slice: { type: string },
491
+ slice: { type: string; request?: string },
422
492
  resolveTypeAndInfo: TypeResolver,
423
493
  messages: Map<string, Message>,
424
494
  exampleShapeHints: ExampleShapeHints,
@@ -428,6 +498,23 @@ function processCommandRefInArray(
428
498
  }
429
499
 
430
500
  const originalCommandRef = item.commandRef;
501
+
502
+ // Check if this is a query action (query name like ViewWorkoutPlan)
503
+ // Query actions represent the act of executing the query - create a query message for them
504
+ if (detectQueryAction(originalCommandRef, slice)) {
505
+ log('DEBUG processCommandRefInArray: detected query action, creating query message:', originalCommandRef);
506
+ const { typeInfo } = resolveTypeAndInfo(originalCommandRef, 'query', item.exampleData);
507
+ const msg = createMessage(originalCommandRef, typeInfo, 'query');
508
+ const existing = messages.get(originalCommandRef);
509
+ if (!existing || preferNewFields(msg.fields, existing.fields)) {
510
+ messages.set(originalCommandRef, msg);
511
+ }
512
+ if (item.exampleData !== undefined) {
513
+ collectExampleHintsForData(originalCommandRef, item.exampleData, exampleShapeHints);
514
+ }
515
+ return;
516
+ }
517
+
431
518
  const expected = slice.type === 'command' ? 'command' : 'event';
432
519
  const { resolvedName, typeInfo } = resolveTypeAndInfo(originalCommandRef, expected, item.exampleData);
433
520
 
@@ -497,7 +584,7 @@ export function processWhen(
497
584
  stateRef?: string;
498
585
  exampleData?: unknown;
499
586
  }>,
500
- slice: { type: string },
587
+ slice: { type: string; request?: string },
501
588
  resolveTypeAndInfo: TypeResolver,
502
589
  messages: Map<string, Message>,
503
590
  exampleShapeHints: ExampleShapeHints,
@@ -64,7 +64,7 @@ function tryMatchCandidates(candidates: TypeInfo[], dataKeys: Set<string>): stri
64
64
  function tryResolveByExampleData(
65
65
  candidates: TypeInfo[],
66
66
  all: TypeInfo[],
67
- expectedMessageType: 'command' | 'event' | 'state' | undefined,
67
+ expectedMessageType: 'command' | 'event' | 'state' | 'query' | undefined,
68
68
  exampleData: unknown,
69
69
  ): string | null {
70
70
  if (exampleData === null || typeof exampleData !== 'object' || exampleData === undefined) {
@@ -85,7 +85,7 @@ function tryResolveByExampleData(
85
85
 
86
86
  function tryResolveByExpectedType(
87
87
  candidates: TypeInfo[],
88
- expectedMessageType: 'command' | 'event' | 'state' | undefined,
88
+ expectedMessageType: 'command' | 'event' | 'state' | 'query' | undefined,
89
89
  ): string | null {
90
90
  if (!expectedMessageType || candidates.length === 0) {
91
91
  return null;
@@ -98,7 +98,7 @@ function tryResolveByExpectedType(
98
98
  function resolveFromCandidates(
99
99
  candidates: TypeInfo[],
100
100
  all: TypeInfo[],
101
- expectedMessageType?: 'command' | 'event' | 'state',
101
+ expectedMessageType?: 'command' | 'event' | 'state' | 'query',
102
102
  exampleData?: unknown,
103
103
  ): string | null {
104
104
  if (candidates.length === 1) return candidates[0].stringLiteral;
@@ -112,7 +112,7 @@ function resolveFromCandidates(
112
112
  export function resolveInferredType(
113
113
  typeName: string,
114
114
  flowTypeMap?: Map<string, TypeInfo>,
115
- expectedMessageType?: 'command' | 'event' | 'state',
115
+ expectedMessageType?: 'command' | 'event' | 'state' | 'query',
116
116
  exampleData?: unknown,
117
117
  ): string {
118
118
  if (typeName !== 'InferredType' || flowTypeMap === undefined) return typeName;
package/src/types.ts CHANGED
@@ -228,4 +228,9 @@ export type Event<
228
228
  readonly kind?: 'Event';
229
229
  };
230
230
 
231
+ export type Query<QueryType extends string, QueryData extends Record<string, unknown> = Record<string, unknown>> = {
232
+ type: QueryType;
233
+ data: QueryData;
234
+ };
235
+
231
236
  export type ExtractStateData<T> = T extends State<string, infer Data, DefaultRecord | undefined> ? Data : never;