@kernlang/core 3.3.9 → 3.4.1
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/dist/capability-matrix.d.ts +15 -0
- package/dist/capability-matrix.js +245 -0
- package/dist/capability-matrix.js.map +1 -0
- package/dist/codegen/body-ts.d.ts +68 -0
- package/dist/codegen/body-ts.js +214 -0
- package/dist/codegen/body-ts.js.map +1 -0
- package/dist/codegen/data-layer.d.ts +1 -1
- package/dist/codegen/data-layer.js +59 -23
- package/dist/codegen/data-layer.js.map +1 -1
- package/dist/codegen/events.js +1 -1
- package/dist/codegen/events.js.map +1 -1
- package/dist/codegen/functions.js +48 -7
- package/dist/codegen/functions.js.map +1 -1
- package/dist/codegen/ground-layer.js +10 -6
- package/dist/codegen/ground-layer.js.map +1 -1
- package/dist/codegen/helpers.d.ts +3 -1
- package/dist/codegen/helpers.js +5 -1
- package/dist/codegen/helpers.js.map +1 -1
- package/dist/codegen/kern-stdlib.d.ts +63 -0
- package/dist/codegen/kern-stdlib.js +160 -0
- package/dist/codegen/kern-stdlib.js.map +1 -0
- package/dist/codegen/machines.js +4 -3
- package/dist/codegen/machines.js.map +1 -1
- package/dist/codegen/modules.d.ts +1 -0
- package/dist/codegen/modules.js +52 -1
- package/dist/codegen/modules.js.map +1 -1
- package/dist/codegen/screens.js +31 -9
- package/dist/codegen/screens.js.map +1 -1
- package/dist/codegen/stdlib-preamble.d.ts +58 -0
- package/dist/codegen/stdlib-preamble.js +271 -0
- package/dist/codegen/stdlib-preamble.js.map +1 -0
- package/dist/codegen/type-system.d.ts +113 -1
- package/dist/codegen/type-system.js +404 -31
- package/dist/codegen/type-system.js.map +1 -1
- package/dist/codegen-core.d.ts +2 -2
- package/dist/codegen-core.js +65 -10
- package/dist/codegen-core.js.map +1 -1
- package/dist/codegen-expression.d.ts +11 -0
- package/dist/codegen-expression.js +199 -0
- package/dist/codegen-expression.js.map +1 -0
- package/dist/concepts.d.ts +3 -3
- package/dist/config.d.ts +16 -0
- package/dist/config.js +13 -0
- package/dist/config.js.map +1 -1
- package/dist/decompiler.js +575 -4
- package/dist/decompiler.js.map +1 -1
- package/dist/importer.d.ts +1 -0
- package/dist/importer.js +574 -34
- package/dist/importer.js.map +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.js +19 -3
- package/dist/index.js.map +1 -1
- package/dist/node-props.d.ts +181 -1
- package/dist/node-props.js.map +1 -1
- package/dist/parser-core.d.ts +7 -1
- package/dist/parser-core.js +33 -7
- package/dist/parser-core.js.map +1 -1
- package/dist/parser-diagnostics.js +8 -0
- package/dist/parser-diagnostics.js.map +1 -1
- package/dist/parser-expression.d.ts +22 -0
- package/dist/parser-expression.js +774 -0
- package/dist/parser-expression.js.map +1 -0
- package/dist/parser-keywords.js +16 -0
- package/dist/parser-keywords.js.map +1 -1
- package/dist/parser-tokenizer.d.ts +5 -3
- package/dist/parser-tokenizer.js +97 -16
- package/dist/parser-tokenizer.js.map +1 -1
- package/dist/parser-validate-effects.d.ts +21 -0
- package/dist/parser-validate-effects.js +188 -0
- package/dist/parser-validate-effects.js.map +1 -0
- package/dist/parser-validate-expressions.d.ts +6 -0
- package/dist/parser-validate-expressions.js +41 -0
- package/dist/parser-validate-expressions.js.map +1 -0
- package/dist/parser-validate-propagation.d.ts +105 -0
- package/dist/parser-validate-propagation.js +684 -0
- package/dist/parser-validate-propagation.js.map +1 -0
- package/dist/parser-validate-union-kind.d.ts +24 -0
- package/dist/parser-validate-union-kind.js +97 -0
- package/dist/parser-validate-union-kind.js.map +1 -0
- package/dist/parser.d.ts +10 -3
- package/dist/parser.js +11 -5
- package/dist/parser.js.map +1 -1
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +562 -30
- package/dist/schema.js.map +1 -1
- package/dist/semantic-validator.js +24 -13
- package/dist/semantic-validator.js.map +1 -1
- package/dist/spec.d.ts +5 -2
- package/dist/spec.js +36 -2
- package/dist/spec.js.map +1 -1
- package/dist/types.d.ts +7 -1
- package/dist/types.js +7 -1
- package/dist/types.js.map +1 -1
- package/dist/value-ir.d.ts +96 -0
- package/dist/value-ir.js +25 -0
- package/dist/value-ir.js.map +1 -0
- package/package.json +1 -1
package/dist/schema.js
CHANGED
|
@@ -29,24 +29,46 @@ export const NODE_SCHEMAS = {
|
|
|
29
29
|
},
|
|
30
30
|
},
|
|
31
31
|
type: {
|
|
32
|
-
description: 'TypeScript type alias — union of string literals or alias to another type',
|
|
32
|
+
description: 'TypeScript type alias — union of string literals, or alias to another type (including tuple types like [string, number]). Use generics="<T>" for parameterised aliases.',
|
|
33
33
|
example: 'type name=Status values="active|inactive|banned"',
|
|
34
34
|
props: {
|
|
35
35
|
name: { required: true, kind: 'identifier' },
|
|
36
36
|
values: { kind: 'string' },
|
|
37
37
|
alias: { kind: 'rawExpr' },
|
|
38
|
+
generics: { kind: 'rawExpr' },
|
|
38
39
|
export: { kind: 'boolean' },
|
|
39
40
|
},
|
|
40
41
|
},
|
|
41
42
|
interface: {
|
|
42
|
-
description: 'TypeScript interface with typed fields',
|
|
43
|
+
description: 'TypeScript interface with typed fields. Use generics="<T>" for parameterised interfaces.',
|
|
43
44
|
example: 'interface name=User export=true\n field name=id type=string\n field name=email type=string',
|
|
44
45
|
props: {
|
|
45
46
|
name: { required: true, kind: 'identifier' },
|
|
46
47
|
extends: { kind: 'typeAnnotation' },
|
|
48
|
+
generics: { kind: 'rawExpr' },
|
|
47
49
|
export: { kind: 'boolean' },
|
|
48
50
|
},
|
|
49
|
-
allowedChildren: ['field'],
|
|
51
|
+
allowedChildren: ['field', 'indexer'],
|
|
52
|
+
},
|
|
53
|
+
indexer: {
|
|
54
|
+
description: 'Index signature for an interface — [keyName: keyType]: type',
|
|
55
|
+
example: 'indexer keyName=key keyType=string type=Value',
|
|
56
|
+
props: {
|
|
57
|
+
keyName: { kind: 'identifier' },
|
|
58
|
+
keyType: { required: true, kind: 'typeAnnotation' },
|
|
59
|
+
type: { required: true, kind: 'typeAnnotation' },
|
|
60
|
+
readonly: { kind: 'boolean' },
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
overload: {
|
|
64
|
+
description: 'Function overload signature — declared as a child of fn. Each overload emits a TS overload declaration before the implementation signature.',
|
|
65
|
+
example: 'overload params="a:number,b:number" returns=number',
|
|
66
|
+
props: {
|
|
67
|
+
params: { kind: 'string' },
|
|
68
|
+
returns: { kind: 'typeAnnotation' },
|
|
69
|
+
generics: { kind: 'rawExpr' },
|
|
70
|
+
},
|
|
71
|
+
allowedChildren: ['param'],
|
|
50
72
|
},
|
|
51
73
|
union: {
|
|
52
74
|
description: 'Discriminated union type with variants, each having their own fields',
|
|
@@ -55,9 +77,33 @@ export const NODE_SCHEMAS = {
|
|
|
55
77
|
name: { required: true, kind: 'identifier' },
|
|
56
78
|
discriminant: { required: true, kind: 'identifier' },
|
|
57
79
|
export: { kind: 'boolean' },
|
|
80
|
+
// Slice 4 — `kind=result|option` opts the union into the Result/Option
|
|
81
|
+
// shape (see docs/language/result-option-spec.md). Default (unspecified)
|
|
82
|
+
// is a regular discriminated union, identical to the slice 3 behaviour.
|
|
83
|
+
// Validated by parser-validate-union-kind.ts.
|
|
84
|
+
kind: { kind: 'string' },
|
|
58
85
|
},
|
|
59
86
|
allowedChildren: ['variant'],
|
|
60
87
|
},
|
|
88
|
+
enum: {
|
|
89
|
+
description: 'TypeScript enum — numeric (auto-incremented) via values="A|B|C", or string-valued via member children',
|
|
90
|
+
example: 'enum name=Status values="Pending|Active|Done"',
|
|
91
|
+
props: {
|
|
92
|
+
name: { required: true, kind: 'identifier' },
|
|
93
|
+
values: { kind: 'string' },
|
|
94
|
+
const: { kind: 'boolean' },
|
|
95
|
+
export: { kind: 'boolean' },
|
|
96
|
+
},
|
|
97
|
+
allowedChildren: ['member'],
|
|
98
|
+
},
|
|
99
|
+
member: {
|
|
100
|
+
description: 'Enum member with explicit value (for string-valued or computed enums)',
|
|
101
|
+
example: 'member name=Up value="UP"',
|
|
102
|
+
props: {
|
|
103
|
+
name: { required: true, kind: 'identifier' },
|
|
104
|
+
value: { kind: 'expression' },
|
|
105
|
+
},
|
|
106
|
+
},
|
|
61
107
|
variant: {
|
|
62
108
|
description: 'A case within a discriminated union. Use name= for inline variants with fields, or type= to reference an existing interface.',
|
|
63
109
|
example: 'variant name=circle\n field name=radius type=number',
|
|
@@ -74,6 +120,12 @@ export const NODE_SCHEMAS = {
|
|
|
74
120
|
name: { required: true, kind: 'identifier' },
|
|
75
121
|
type: { kind: 'typeAnnotation' },
|
|
76
122
|
optional: { kind: 'boolean' },
|
|
123
|
+
// Slice 3b — `value` is the native ValueIR-canonicalised initializer;
|
|
124
|
+
// `default` remains as the rawExpr passthrough escape hatch. `value`
|
|
125
|
+
// takes precedence when both are set. Either marks the field as having
|
|
126
|
+
// an initializer (which makes the interface-side property optional in
|
|
127
|
+
// config emit).
|
|
128
|
+
value: { kind: 'expression' },
|
|
77
129
|
default: { kind: 'rawExpr' },
|
|
78
130
|
private: { kind: 'boolean' },
|
|
79
131
|
readonly: { kind: 'boolean' },
|
|
@@ -86,6 +138,7 @@ export const NODE_SCHEMAS = {
|
|
|
86
138
|
props: {
|
|
87
139
|
name: { required: true, kind: 'identifier' },
|
|
88
140
|
implements: { kind: 'typeAnnotation' },
|
|
141
|
+
generics: { kind: 'rawExpr' },
|
|
89
142
|
export: { kind: 'boolean' },
|
|
90
143
|
},
|
|
91
144
|
allowedChildren: ['field', 'method', 'constructor', 'singleton', 'getter', 'setter'],
|
|
@@ -98,6 +151,7 @@ export const NODE_SCHEMAS = {
|
|
|
98
151
|
extends: { kind: 'typeAnnotation' },
|
|
99
152
|
implements: { kind: 'typeAnnotation' },
|
|
100
153
|
abstract: { kind: 'boolean' },
|
|
154
|
+
generics: { kind: 'rawExpr' },
|
|
101
155
|
export: { kind: 'boolean' },
|
|
102
156
|
},
|
|
103
157
|
allowedChildren: ['field', 'method', 'constructor', 'singleton', 'getter', 'setter'],
|
|
@@ -113,8 +167,9 @@ export const NODE_SCHEMAS = {
|
|
|
113
167
|
stream: { kind: 'boolean' },
|
|
114
168
|
private: { kind: 'boolean' },
|
|
115
169
|
static: { kind: 'boolean' },
|
|
170
|
+
generics: { kind: 'rawExpr' },
|
|
116
171
|
},
|
|
117
|
-
allowedChildren: ['handler'],
|
|
172
|
+
allowedChildren: ['handler', 'param'],
|
|
118
173
|
},
|
|
119
174
|
getter: {
|
|
120
175
|
description: 'A getter accessor within a class or service — emits `get name(): T { body }`.',
|
|
@@ -136,7 +191,7 @@ export const NODE_SCHEMAS = {
|
|
|
136
191
|
private: { kind: 'boolean' },
|
|
137
192
|
static: { kind: 'boolean' },
|
|
138
193
|
},
|
|
139
|
-
allowedChildren: ['handler'],
|
|
194
|
+
allowedChildren: ['handler', 'param'],
|
|
140
195
|
},
|
|
141
196
|
fn: {
|
|
142
197
|
description: 'Standalone function — the most common code unit in KERN',
|
|
@@ -149,8 +204,12 @@ export const NODE_SCHEMAS = {
|
|
|
149
204
|
stream: { kind: 'boolean' },
|
|
150
205
|
export: { kind: 'boolean' },
|
|
151
206
|
expr: { kind: 'rawExpr' },
|
|
207
|
+
generics: { kind: 'rawExpr' },
|
|
208
|
+
// Slice 6 — effects=pure declares the body has no observable side effects.
|
|
209
|
+
// Validated by parser-validate-effects.ts; see docs/language/effects-pure-spec.md.
|
|
210
|
+
effects: { kind: 'string' },
|
|
152
211
|
},
|
|
153
|
-
allowedChildren: ['handler', 'signal', 'cleanup'],
|
|
212
|
+
allowedChildren: ['handler', 'signal', 'cleanup', 'overload', 'param'],
|
|
154
213
|
},
|
|
155
214
|
machine: {
|
|
156
215
|
description: 'State machine with states and guarded transitions — 12 lines of KERN generates 140+ lines of TypeScript',
|
|
@@ -193,7 +252,7 @@ export const NODE_SCHEMAS = {
|
|
|
193
252
|
params: { kind: 'string' },
|
|
194
253
|
guard: { kind: 'rawExpr' },
|
|
195
254
|
},
|
|
196
|
-
allowedChildren: ['handler'],
|
|
255
|
+
allowedChildren: ['handler', 'param'],
|
|
197
256
|
},
|
|
198
257
|
error: {
|
|
199
258
|
description: 'Custom error class extending a base error, with typed fields',
|
|
@@ -231,8 +290,10 @@ export const NODE_SCHEMAS = {
|
|
|
231
290
|
example: 'test name="AuthService"\n describe name="login"\n it name="rejects invalid email"',
|
|
232
291
|
props: {
|
|
233
292
|
name: { required: true, kind: 'string' },
|
|
293
|
+
target: { kind: 'string' },
|
|
294
|
+
coverage: { kind: 'boolean' },
|
|
234
295
|
},
|
|
235
|
-
allowedChildren: ['describe', 'it', 'handler'],
|
|
296
|
+
allowedChildren: ['describe', 'it', 'expect', 'fixture', 'mock', 'handler'],
|
|
236
297
|
},
|
|
237
298
|
event: {
|
|
238
299
|
description: 'Typed event with payload type children',
|
|
@@ -253,17 +314,101 @@ export const NODE_SCHEMAS = {
|
|
|
253
314
|
types: { kind: 'boolean' },
|
|
254
315
|
},
|
|
255
316
|
},
|
|
317
|
+
use: {
|
|
318
|
+
description: 'Cross-`.kern` symbol resolution. Parent of `from` children — one per imported binding. Compositional shape mirrors enum/member, class/method.',
|
|
319
|
+
example: 'use path="./helper.kern"\n from name=foo\n from name=bar as=baz',
|
|
320
|
+
props: {
|
|
321
|
+
path: { required: true, kind: 'importPath' },
|
|
322
|
+
},
|
|
323
|
+
allowedChildren: ['from'],
|
|
324
|
+
},
|
|
325
|
+
from: {
|
|
326
|
+
description: 'Single binding in a `use` block. `as=` aliases the local name; `export=true` re-exports.',
|
|
327
|
+
example: 'from name=foo as=bar export=true',
|
|
328
|
+
props: {
|
|
329
|
+
name: { required: true, kind: 'identifier' },
|
|
330
|
+
as: { kind: 'identifier' },
|
|
331
|
+
export: { kind: 'boolean' },
|
|
332
|
+
},
|
|
333
|
+
},
|
|
256
334
|
const: {
|
|
257
335
|
description: 'Constant declaration with optional type and value or handler body',
|
|
258
336
|
example: 'const name=MAX_RETRIES type=number value=3 export=true',
|
|
259
337
|
props: {
|
|
260
338
|
name: { required: true, kind: 'identifier' },
|
|
261
339
|
type: { kind: 'typeAnnotation' },
|
|
262
|
-
value: { kind: '
|
|
340
|
+
value: { kind: 'expression' },
|
|
263
341
|
export: { kind: 'boolean' },
|
|
264
342
|
},
|
|
265
343
|
allowedChildren: ['handler'],
|
|
266
344
|
},
|
|
345
|
+
destructure: {
|
|
346
|
+
description: 'Native destructuring statement — emits `const {a,b} = expr;` (object pattern with `binding` children) or `const [x,y] = expr;` (array pattern with `element` children). For complex patterns (rest `...`, defaults `=v`, nested `{a:{b}}`), use the `expr={{...}}` escape hatch which carries the raw TS statement verbatim. Slice 3d.',
|
|
347
|
+
example: 'destructure kind=const source=user\n binding name=id\n binding name=email key=mail',
|
|
348
|
+
props: {
|
|
349
|
+
kind: { kind: 'string' },
|
|
350
|
+
source: { kind: 'expression' },
|
|
351
|
+
type: { kind: 'typeAnnotation' },
|
|
352
|
+
export: { kind: 'boolean' },
|
|
353
|
+
expr: { kind: 'rawExpr' },
|
|
354
|
+
},
|
|
355
|
+
allowedChildren: ['binding', 'element'],
|
|
356
|
+
},
|
|
357
|
+
binding: {
|
|
358
|
+
description: 'Object-destructuring binding inside a `destructure` parent. `name` is the local binding; `key` is the optional property key when renaming, e.g. `{a: foo}` → `binding name=foo key=a`. Slice 3d.',
|
|
359
|
+
example: 'binding name=foo key=a',
|
|
360
|
+
props: {
|
|
361
|
+
name: { required: true, kind: 'identifier' },
|
|
362
|
+
key: { kind: 'identifier' },
|
|
363
|
+
},
|
|
364
|
+
},
|
|
365
|
+
element: {
|
|
366
|
+
description: 'Array-destructuring element inside a `destructure` parent. `index` is the ordered position (zero-based). Slice 3d.',
|
|
367
|
+
example: 'element name=first index=0',
|
|
368
|
+
props: {
|
|
369
|
+
name: { required: true, kind: 'identifier' },
|
|
370
|
+
index: { kind: 'string' },
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
mapLit: {
|
|
374
|
+
description: 'Native Map<K,V> literal — emits `const name: Type = new Map([[k1, v1], [k2, v2]]);` from `mapEntry` children. For complex shapes (computed keys, conditional entries, spread), use the `expr={{...}}` escape hatch which carries the raw TS statement verbatim. Slice 3e.',
|
|
375
|
+
example: 'mapLit name=cache type="Map<string, number>"\n mapEntry key="foo" value=1\n mapEntry key="bar" value=2',
|
|
376
|
+
props: {
|
|
377
|
+
name: { required: true, kind: 'identifier' },
|
|
378
|
+
type: { kind: 'typeAnnotation' },
|
|
379
|
+
kind: { kind: 'string' },
|
|
380
|
+
export: { kind: 'boolean' },
|
|
381
|
+
expr: { kind: 'rawExpr' },
|
|
382
|
+
},
|
|
383
|
+
allowedChildren: ['mapEntry'],
|
|
384
|
+
},
|
|
385
|
+
mapEntry: {
|
|
386
|
+
description: 'Map-literal entry inside a `mapLit` parent. `key` and `value` are both expression-typed and ValueIR-canonicalised. Slice 3e.',
|
|
387
|
+
example: 'mapEntry key="foo" value=1',
|
|
388
|
+
props: {
|
|
389
|
+
key: { required: true, kind: 'expression' },
|
|
390
|
+
value: { required: true, kind: 'expression' },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
setLit: {
|
|
394
|
+
description: 'Native Set<T> literal — emits `const name: Type = new Set([v1, v2]);` from `setItem` children. For complex shapes (conditional members, spread), use the `expr={{...}}` escape hatch which carries the raw TS statement verbatim. Slice 3e.',
|
|
395
|
+
example: 'setLit name=allowed type="Set<string>"\n setItem value="admin"\n setItem value="user"',
|
|
396
|
+
props: {
|
|
397
|
+
name: { required: true, kind: 'identifier' },
|
|
398
|
+
type: { kind: 'typeAnnotation' },
|
|
399
|
+
kind: { kind: 'string' },
|
|
400
|
+
export: { kind: 'boolean' },
|
|
401
|
+
expr: { kind: 'rawExpr' },
|
|
402
|
+
},
|
|
403
|
+
allowedChildren: ['setItem'],
|
|
404
|
+
},
|
|
405
|
+
setItem: {
|
|
406
|
+
description: 'Set-literal item inside a `setLit` parent. `value` is expression-typed and ValueIR-canonicalised. Slice 3e.',
|
|
407
|
+
example: 'setItem value="admin"',
|
|
408
|
+
props: {
|
|
409
|
+
value: { required: true, kind: 'expression' },
|
|
410
|
+
},
|
|
411
|
+
},
|
|
267
412
|
on: {
|
|
268
413
|
description: 'Event listener — binds a handler to a named event',
|
|
269
414
|
example: 'on event=click handler=handleClick',
|
|
@@ -293,6 +438,8 @@ export const NODE_SCHEMAS = {
|
|
|
293
438
|
expr: { required: true, kind: 'rawExpr' },
|
|
294
439
|
type: { kind: 'typeAnnotation' },
|
|
295
440
|
export: { kind: 'boolean' },
|
|
441
|
+
// Slice 6 — see fn schema above.
|
|
442
|
+
effects: { kind: 'string' },
|
|
296
443
|
},
|
|
297
444
|
},
|
|
298
445
|
fmt: {
|
|
@@ -858,7 +1005,7 @@ export const NODE_SCHEMAS = {
|
|
|
858
1005
|
reversible: { kind: 'boolean' },
|
|
859
1006
|
export: { kind: 'boolean' },
|
|
860
1007
|
},
|
|
861
|
-
allowedChildren: ['handler'],
|
|
1008
|
+
allowedChildren: ['handler', 'param'],
|
|
862
1009
|
},
|
|
863
1010
|
actionRegistry: {
|
|
864
1011
|
description: 'Calls an imported registration function with a map of string-keyed async action handlers. Emits `target({ key: async (...) => body, ... })` directly — no IIFE wrapper.',
|
|
@@ -878,6 +1025,9 @@ export const NODE_SCHEMAS = {
|
|
|
878
1025
|
confidence: { kind: 'number' },
|
|
879
1026
|
kind: { kind: 'identifier' },
|
|
880
1027
|
type: { kind: 'identifier' },
|
|
1028
|
+
covers: { kind: 'string' },
|
|
1029
|
+
over: { kind: 'identifier' },
|
|
1030
|
+
union: { kind: 'identifier' },
|
|
881
1031
|
param: { kind: 'identifier' },
|
|
882
1032
|
field: { kind: 'identifier' },
|
|
883
1033
|
target: { kind: 'identifier' },
|
|
@@ -936,11 +1086,12 @@ export const NODE_SCHEMAS = {
|
|
|
936
1086
|
// separately via the `let-must-be-inside-each` semantic rule.
|
|
937
1087
|
},
|
|
938
1088
|
let: {
|
|
939
|
-
description: 'Iteration-scoped binding — emits a plain `const` inside the containing `each` callback. Use for values that depend on the iteration variable or index. Unlike `derive` (which compiles to `useMemo` and violates Rules of Hooks inside `.map`), `let` is hook-safe by construction.',
|
|
940
|
-
example: 'let name=idx
|
|
1089
|
+
description: 'Iteration-scoped binding — emits a plain `const` inside the containing `each` callback. Use for values that depend on the iteration variable or index. Unlike `derive` (which compiles to `useMemo` and violates Rules of Hooks inside `.map`), `let` is hook-safe by construction. Provide either `value=` (native expression form, ValueIR-canonicalised — slice 3a) or `expr=` (raw passthrough escape hatch).',
|
|
1090
|
+
example: 'let name=idx value=i+1',
|
|
941
1091
|
props: {
|
|
942
1092
|
name: { required: true, kind: 'identifier' },
|
|
943
|
-
|
|
1093
|
+
value: { kind: 'expression' },
|
|
1094
|
+
expr: { kind: 'rawExpr' },
|
|
944
1095
|
type: { kind: 'typeAnnotation' },
|
|
945
1096
|
},
|
|
946
1097
|
},
|
|
@@ -1041,7 +1192,7 @@ export const NODE_SCHEMAS = {
|
|
|
1041
1192
|
params: { kind: 'string' },
|
|
1042
1193
|
returns: { kind: 'typeAnnotation' },
|
|
1043
1194
|
},
|
|
1044
|
-
allowedChildren: ['handler', 'memo', 'callback', 'ref', 'effect'],
|
|
1195
|
+
allowedChildren: ['handler', 'memo', 'callback', 'ref', 'effect', 'param'],
|
|
1045
1196
|
},
|
|
1046
1197
|
effect: {
|
|
1047
1198
|
description: 'React useEffect — side effect with dependency tracking',
|
|
@@ -1051,7 +1202,7 @@ export const NODE_SCHEMAS = {
|
|
|
1051
1202
|
deps: { kind: 'string' },
|
|
1052
1203
|
once: { kind: 'boolean' },
|
|
1053
1204
|
},
|
|
1054
|
-
allowedChildren: ['prop', 'handler', 'cleanup'],
|
|
1205
|
+
allowedChildren: ['prop', 'handler', 'cleanup', 'trigger', 'recover'],
|
|
1055
1206
|
},
|
|
1056
1207
|
// ── Web / UI node types ──────────────────────────────────────────────
|
|
1057
1208
|
page: {
|
|
@@ -1318,7 +1469,22 @@ export const NODE_SCHEMAS = {
|
|
|
1318
1469
|
props: {
|
|
1319
1470
|
name: { required: true, kind: 'identifier' },
|
|
1320
1471
|
},
|
|
1321
|
-
allowedChildren: [
|
|
1472
|
+
allowedChildren: [
|
|
1473
|
+
'param',
|
|
1474
|
+
'handler',
|
|
1475
|
+
'description',
|
|
1476
|
+
'guard',
|
|
1477
|
+
'sampling',
|
|
1478
|
+
'elicitation',
|
|
1479
|
+
'derive',
|
|
1480
|
+
'effect',
|
|
1481
|
+
'respond',
|
|
1482
|
+
'branch',
|
|
1483
|
+
'each',
|
|
1484
|
+
'collect',
|
|
1485
|
+
'destructure',
|
|
1486
|
+
'partition',
|
|
1487
|
+
],
|
|
1322
1488
|
},
|
|
1323
1489
|
resource: {
|
|
1324
1490
|
description: 'MCP resource — a data source exposed to AI agents via URI. Use {variables} for templated URIs.',
|
|
@@ -1330,18 +1496,42 @@ export const NODE_SCHEMAS = {
|
|
|
1330
1496
|
allowedChildren: ['param', 'handler', 'description', 'guard'],
|
|
1331
1497
|
},
|
|
1332
1498
|
param: {
|
|
1333
|
-
description: 'Parameter definition
|
|
1499
|
+
description: 'Parameter definition. Used in two contexts: (a) MCP tool/resource/prompt params (description/required/min/max apply); (b) fn/method/constructor/etc. parameter defaults via slice 3c — value flows through ValueIR canonicalisation (mirrors slice 1j const.value, 3a let.value, 3b field.value).',
|
|
1334
1500
|
example: 'param name=query type=string required=true description="Search query"',
|
|
1335
1501
|
props: {
|
|
1336
|
-
|
|
1337
|
-
|
|
1502
|
+
// Slice 3c-extension #3: `name` is required UNLESS the param carries
|
|
1503
|
+
// `binding`/`element` destructure children — destructured params encode
|
|
1504
|
+
// the LHS pattern in the children, not in `name`. The required-OR-children
|
|
1505
|
+
// invariant lives in `checkCrossProps` so `validateSchema` accepts both
|
|
1506
|
+
// forms. Keeping the schema-level `required: true` here would reject the
|
|
1507
|
+
// canonical destructured form emitted by the importer.
|
|
1508
|
+
name: { kind: 'identifier' },
|
|
1509
|
+
type: { kind: 'typeAnnotation' },
|
|
1510
|
+
value: { kind: 'expression' },
|
|
1338
1511
|
required: { kind: 'boolean' },
|
|
1512
|
+
// Slice 3c-extension: TS-style optional `?` on the LHS, distinct from MCP
|
|
1513
|
+
// `required`. When `optional=true`, codegen emits `name?: type` (with the
|
|
1514
|
+
// `?` inside the parameter list) so callers may omit the argument.
|
|
1515
|
+
optional: { kind: 'boolean' },
|
|
1516
|
+
// Slice 3c-extension: TS-style variadic `...rest`. When `variadic=true`,
|
|
1517
|
+
// codegen prepends `...` to the parameter name; the type should be an
|
|
1518
|
+
// array (e.g. `string[]`). Variadic params can't have defaults — that's
|
|
1519
|
+
// user error and TS will surface it at the call site.
|
|
1520
|
+
variadic: { kind: 'boolean' },
|
|
1339
1521
|
default: { kind: 'rawExpr' },
|
|
1340
1522
|
description: { kind: 'string' },
|
|
1341
1523
|
min: { kind: 'number' },
|
|
1342
1524
|
max: { kind: 'number' },
|
|
1343
1525
|
},
|
|
1344
|
-
|
|
1526
|
+
// Slice 3c-extension #3: TS-style destructured params via slice 3d's
|
|
1527
|
+
// `binding` (object pattern) / `element` (array pattern) children. When
|
|
1528
|
+
// present, codegen uses the pattern as the LHS instead of `name`, e.g.
|
|
1529
|
+
// param type="Point"
|
|
1530
|
+
// binding name=x
|
|
1531
|
+
// binding name=y
|
|
1532
|
+
// → `{x, y}: Point`. Same node types as slice 3d destructure — no new
|
|
1533
|
+
// node types needed. `name=` is omitted on destructured params.
|
|
1534
|
+
allowedChildren: ['guard', 'description', 'binding', 'element'],
|
|
1345
1535
|
},
|
|
1346
1536
|
prompt: {
|
|
1347
1537
|
description: 'MCP prompt template — a reusable system prompt exposed to AI agents',
|
|
@@ -1508,6 +1698,10 @@ export const NODE_SCHEMAS = {
|
|
|
1508
1698
|
kind: { kind: 'identifier' },
|
|
1509
1699
|
on: { kind: 'string' },
|
|
1510
1700
|
from: { kind: 'string' },
|
|
1701
|
+
expr: { kind: 'rawExpr' },
|
|
1702
|
+
query: { kind: 'string' },
|
|
1703
|
+
url: { kind: 'string' },
|
|
1704
|
+
call: { kind: 'rawExpr' },
|
|
1511
1705
|
},
|
|
1512
1706
|
allowedChildren: ['handler'],
|
|
1513
1707
|
},
|
|
@@ -1595,6 +1789,8 @@ export const NODE_SCHEMAS = {
|
|
|
1595
1789
|
props: {
|
|
1596
1790
|
name: { required: true, kind: 'identifier' },
|
|
1597
1791
|
deps: { kind: 'string' },
|
|
1792
|
+
// Slice 6 — see fn schema above.
|
|
1793
|
+
effects: { kind: 'string' },
|
|
1598
1794
|
},
|
|
1599
1795
|
allowedChildren: ['handler'],
|
|
1600
1796
|
},
|
|
@@ -1607,7 +1803,7 @@ export const NODE_SCHEMAS = {
|
|
|
1607
1803
|
deps: { kind: 'string' },
|
|
1608
1804
|
async: { kind: 'boolean' },
|
|
1609
1805
|
},
|
|
1610
|
-
allowedChildren: ['handler'],
|
|
1806
|
+
allowedChildren: ['handler', 'param'],
|
|
1611
1807
|
},
|
|
1612
1808
|
ref: {
|
|
1613
1809
|
description: 'React useRef — mutable ref object that persists across renders',
|
|
@@ -1961,8 +2157,11 @@ export const NODE_SCHEMAS = {
|
|
|
1961
2157
|
constructor: {
|
|
1962
2158
|
description: 'Constructor for a service — runs on instantiation',
|
|
1963
2159
|
example: 'constructor params="size:number"\n handler <<<\n this.data = new Map();\n >>>',
|
|
1964
|
-
props: {
|
|
1965
|
-
|
|
2160
|
+
props: {
|
|
2161
|
+
params: { kind: 'string' },
|
|
2162
|
+
generics: { kind: 'rawExpr' },
|
|
2163
|
+
},
|
|
2164
|
+
allowedChildren: ['handler', 'param'],
|
|
1966
2165
|
},
|
|
1967
2166
|
cleanup: {
|
|
1968
2167
|
description: 'Cleanup handler — runs on teardown (useEffect return, signal dispose)',
|
|
@@ -1984,13 +2183,13 @@ export const NODE_SCHEMAS = {
|
|
|
1984
2183
|
description: 'Test suite — groups related test cases',
|
|
1985
2184
|
example: 'describe name="UserService"\n it name="creates a user"\n handler <<<\n expect(createUser()).toBeDefined();\n >>>',
|
|
1986
2185
|
props: { name: { required: true, kind: 'string' } },
|
|
1987
|
-
allowedChildren: ['it', 'describe', 'handler'],
|
|
2186
|
+
allowedChildren: ['it', 'describe', 'expect', 'fixture', 'mock', 'handler'],
|
|
1988
2187
|
},
|
|
1989
2188
|
it: {
|
|
1990
2189
|
description: 'Test case — single test assertion',
|
|
1991
2190
|
example: 'it name="returns 200 on success"\n handler <<<\n expect(res.status).toBe(200);\n >>>',
|
|
1992
2191
|
props: { name: { required: true, kind: 'string' } },
|
|
1993
|
-
allowedChildren: ['handler'],
|
|
2192
|
+
allowedChildren: ['expect', 'case', 'fixture', 'mock', 'handler'],
|
|
1994
2193
|
},
|
|
1995
2194
|
// Ground layer — semantic reasoning
|
|
1996
2195
|
path: {
|
|
@@ -2028,14 +2227,110 @@ export const NODE_SCHEMAS = {
|
|
|
2028
2227
|
props: { fn: { kind: 'identifier' }, to: { kind: 'rawExpr' } },
|
|
2029
2228
|
},
|
|
2030
2229
|
expect: {
|
|
2031
|
-
description: 'Assertion — declare an expected condition
|
|
2032
|
-
example: 'expect expr={{items.length > 0}} message="Items must not be empty"',
|
|
2033
|
-
props: {
|
|
2230
|
+
description: 'Assertion — declare an expected runtime condition or KERN structural invariant',
|
|
2231
|
+
example: 'expect expr={{items.length > 0}} message="Items must not be empty"\nexpect route="GET /api/users" with={{({ query: { role: "admin" } })}} returns={{adminUsers}}\nexpect machine=Order reaches=paid via=confirm,capture\nexpect machine=Order transition=capture from=confirmed to=paid\nexpect node=interface name=User child=field count=3\nexpect no=deriveCycles',
|
|
2232
|
+
props: {
|
|
2233
|
+
expr: { kind: 'rawExpr' },
|
|
2234
|
+
fn: { kind: 'identifier' },
|
|
2235
|
+
derive: { kind: 'identifier' },
|
|
2236
|
+
route: { kind: 'string' },
|
|
2237
|
+
tool: { kind: 'identifier' },
|
|
2238
|
+
effect: { kind: 'identifier' },
|
|
2239
|
+
mock: { kind: 'identifier' },
|
|
2240
|
+
called: { kind: 'number' },
|
|
2241
|
+
import: { kind: 'string' },
|
|
2242
|
+
source: { kind: 'string' },
|
|
2243
|
+
unmapped: { kind: 'number' },
|
|
2244
|
+
allowWarnings: { kind: 'boolean' },
|
|
2245
|
+
args: { kind: 'rawExpr' },
|
|
2246
|
+
with: { kind: 'rawExpr' },
|
|
2247
|
+
input: { kind: 'rawExpr' },
|
|
2248
|
+
equals: { kind: 'rawExpr' },
|
|
2249
|
+
returns: { kind: 'rawExpr' },
|
|
2250
|
+
recovers: { kind: 'boolean' },
|
|
2251
|
+
fallback: { kind: 'rawExpr' },
|
|
2252
|
+
codegen: { kind: 'boolean' },
|
|
2253
|
+
decompile: { kind: 'boolean' },
|
|
2254
|
+
roundtrip: { kind: 'boolean' },
|
|
2255
|
+
contains: { kind: 'string' },
|
|
2256
|
+
notContains: { kind: 'string' },
|
|
2257
|
+
matches: { kind: 'string' },
|
|
2258
|
+
throws: { kind: 'string' },
|
|
2259
|
+
message: { kind: 'string' },
|
|
2260
|
+
preset: { kind: 'identifier' },
|
|
2261
|
+
severity: { kind: 'identifier' },
|
|
2262
|
+
node: { kind: 'identifier' },
|
|
2263
|
+
name: { kind: 'string' },
|
|
2264
|
+
within: { kind: 'string' },
|
|
2265
|
+
child: { kind: 'identifier' },
|
|
2266
|
+
childName: { kind: 'string' },
|
|
2267
|
+
prop: { kind: 'identifier' },
|
|
2268
|
+
is: { kind: 'string' },
|
|
2269
|
+
count: { kind: 'number' },
|
|
2270
|
+
machine: { kind: 'identifier' },
|
|
2271
|
+
transition: { kind: 'identifier' },
|
|
2272
|
+
from: { kind: 'identifier' },
|
|
2273
|
+
to: { kind: 'identifier' },
|
|
2274
|
+
guarded: { kind: 'boolean' },
|
|
2275
|
+
reaches: { kind: 'identifier' },
|
|
2276
|
+
through: { kind: 'string' },
|
|
2277
|
+
avoid: { kind: 'string' },
|
|
2278
|
+
avoids: { kind: 'string' },
|
|
2279
|
+
maxSteps: { kind: 'number' },
|
|
2280
|
+
via: { kind: 'string' },
|
|
2281
|
+
no: { kind: 'identifier' },
|
|
2282
|
+
guard: { kind: 'identifier' },
|
|
2283
|
+
exhaustive: { kind: 'boolean' },
|
|
2284
|
+
over: { kind: 'identifier' },
|
|
2285
|
+
union: { kind: 'identifier' },
|
|
2286
|
+
covers: { kind: 'string' },
|
|
2287
|
+
},
|
|
2288
|
+
},
|
|
2289
|
+
case: {
|
|
2290
|
+
description: 'Native test table row — supplies input and expected output for a parent expect assertion',
|
|
2291
|
+
example: 'case name=admin with={{({ query: { role: "admin" } })}} returns={{adminUsers}}\nexpect route="GET /api/users"',
|
|
2292
|
+
props: {
|
|
2293
|
+
name: { kind: 'string' },
|
|
2294
|
+
args: { kind: 'rawExpr' },
|
|
2295
|
+
with: { kind: 'rawExpr' },
|
|
2296
|
+
input: { kind: 'rawExpr' },
|
|
2297
|
+
equals: { kind: 'rawExpr' },
|
|
2298
|
+
returns: { kind: 'rawExpr' },
|
|
2299
|
+
recovers: { kind: 'boolean' },
|
|
2300
|
+
fallback: { kind: 'rawExpr' },
|
|
2301
|
+
matches: { kind: 'string' },
|
|
2302
|
+
throws: { kind: 'string' },
|
|
2303
|
+
called: { kind: 'number' },
|
|
2304
|
+
message: { kind: 'string' },
|
|
2305
|
+
severity: { kind: 'identifier' },
|
|
2306
|
+
},
|
|
2307
|
+
allowedChildren: ['fixture', 'mock'],
|
|
2308
|
+
},
|
|
2309
|
+
fixture: {
|
|
2310
|
+
description: 'Native test fixture — named runtime data available to scoped expect assertions',
|
|
2311
|
+
example: 'fixture name=paidOrder value={{({ id: "ord_1", status: "paid" })}}',
|
|
2312
|
+
props: {
|
|
2313
|
+
name: { required: true, kind: 'identifier' },
|
|
2314
|
+
value: { kind: 'rawExpr' },
|
|
2315
|
+
expr: { kind: 'rawExpr' },
|
|
2316
|
+
},
|
|
2317
|
+
},
|
|
2318
|
+
mock: {
|
|
2319
|
+
description: 'Native test mock — replaces a scoped KERN effect with deterministic runtime data',
|
|
2320
|
+
example: 'mock effect=fetchUsers returns={{users}}\nmock effect=fetchUsers throws=NetworkError',
|
|
2321
|
+
props: {
|
|
2322
|
+
effect: { required: true, kind: 'identifier' },
|
|
2323
|
+
returns: { kind: 'rawExpr' },
|
|
2324
|
+
throws: { kind: 'string' },
|
|
2325
|
+
},
|
|
2034
2326
|
},
|
|
2035
2327
|
recover: {
|
|
2036
2328
|
description: 'Recovery handler — runs when a parent node fails',
|
|
2037
2329
|
example: 'recover\n handler <<<\n return fallbackValue;\n >>>',
|
|
2038
|
-
props: {
|
|
2330
|
+
props: {
|
|
2331
|
+
retry: { kind: 'number' },
|
|
2332
|
+
fallback: { kind: 'rawExpr' },
|
|
2333
|
+
},
|
|
2039
2334
|
allowedChildren: ['handler'],
|
|
2040
2335
|
},
|
|
2041
2336
|
strategy: {
|
|
@@ -2124,6 +2419,232 @@ function checkCrossProps(node, violations) {
|
|
|
2124
2419
|
col: node.loc?.col,
|
|
2125
2420
|
});
|
|
2126
2421
|
}
|
|
2422
|
+
if (node.type === 'param') {
|
|
2423
|
+
// Slice 3c-extension #3: `param` requires `name=` UNLESS it carries
|
|
2424
|
+
// `binding`/`element` destructure children — destructured params encode
|
|
2425
|
+
// the LHS pattern in the children. Replaces the old prop-level
|
|
2426
|
+
// `required: true` constraint which rejected the canonical destructured
|
|
2427
|
+
// form emitted by importer/decompiler.
|
|
2428
|
+
const hasName = 'name' in props;
|
|
2429
|
+
const hasDestructure = (node.children ?? []).some((c) => c.type === 'binding' || c.type === 'element');
|
|
2430
|
+
if (!hasName && !hasDestructure) {
|
|
2431
|
+
violations.push({
|
|
2432
|
+
nodeType: 'param',
|
|
2433
|
+
message: "'param' requires either 'name' or destructure children ('binding'/'element')",
|
|
2434
|
+
line: node.loc?.line,
|
|
2435
|
+
col: node.loc?.col,
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
}
|
|
2439
|
+
if (node.type === 'expect') {
|
|
2440
|
+
const hasRuntimeAssertion = 'expr' in props;
|
|
2441
|
+
const hasRuntimeBehavior = 'fn' in props || 'derive' in props;
|
|
2442
|
+
const hasRuntimeWorkflow = 'route' in props || 'tool' in props || 'effect' in props;
|
|
2443
|
+
const hasMockCallAssertion = 'mock' in props || 'called' in props;
|
|
2444
|
+
const hasCodegenAssertion = 'codegen' in props;
|
|
2445
|
+
const hasDecompileAssertion = 'decompile' in props;
|
|
2446
|
+
const hasImportAssertion = 'import' in props;
|
|
2447
|
+
const hasRoundtripAssertion = 'roundtrip' in props;
|
|
2448
|
+
const hasPreset = 'preset' in props;
|
|
2449
|
+
const hasNodeShape = 'node' in props;
|
|
2450
|
+
const hasNegativeInvariant = 'no' in props;
|
|
2451
|
+
const hasPositiveInvariant = 'has' in props;
|
|
2452
|
+
const hasGuardExhaustiveness = 'guard' in props;
|
|
2453
|
+
const hasMachineTransition = 'transition' in props;
|
|
2454
|
+
const hasMachineReachability = 'reaches' in props ||
|
|
2455
|
+
('machine' in props && !hasNegativeInvariant && !hasPositiveInvariant && !hasMachineTransition);
|
|
2456
|
+
if (!hasRuntimeAssertion &&
|
|
2457
|
+
!hasRuntimeBehavior &&
|
|
2458
|
+
!hasRuntimeWorkflow &&
|
|
2459
|
+
!hasMockCallAssertion &&
|
|
2460
|
+
!hasCodegenAssertion &&
|
|
2461
|
+
!hasDecompileAssertion &&
|
|
2462
|
+
!hasImportAssertion &&
|
|
2463
|
+
!hasRoundtripAssertion &&
|
|
2464
|
+
!hasPreset &&
|
|
2465
|
+
!hasNodeShape &&
|
|
2466
|
+
!hasMachineTransition &&
|
|
2467
|
+
!hasMachineReachability &&
|
|
2468
|
+
!hasNegativeInvariant &&
|
|
2469
|
+
!hasPositiveInvariant &&
|
|
2470
|
+
!hasGuardExhaustiveness) {
|
|
2471
|
+
violations.push({
|
|
2472
|
+
nodeType: 'expect',
|
|
2473
|
+
message: "'expect' requires 'expr', 'fn', 'derive', 'route', 'tool', 'effect', 'mock' call count, 'codegen', 'decompile', 'import', 'roundtrip', 'preset', 'node', 'machine' reachability, machine transition, 'no', 'has', or 'guard'",
|
|
2474
|
+
line: node.loc?.line,
|
|
2475
|
+
col: node.loc?.col,
|
|
2476
|
+
});
|
|
2477
|
+
}
|
|
2478
|
+
if (hasCodegenAssertion && !('contains' in props) && !('notContains' in props) && !('matches' in props)) {
|
|
2479
|
+
violations.push({
|
|
2480
|
+
nodeType: 'expect',
|
|
2481
|
+
message: "'expect codegen' requires contains=, notContains=, or matches=",
|
|
2482
|
+
line: node.loc?.line,
|
|
2483
|
+
col: node.loc?.col,
|
|
2484
|
+
});
|
|
2485
|
+
}
|
|
2486
|
+
if (hasDecompileAssertion && !('contains' in props) && !('notContains' in props) && !('matches' in props)) {
|
|
2487
|
+
violations.push({
|
|
2488
|
+
nodeType: 'expect',
|
|
2489
|
+
message: "'expect decompile' requires contains=, notContains=, or matches=",
|
|
2490
|
+
line: node.loc?.line,
|
|
2491
|
+
col: node.loc?.col,
|
|
2492
|
+
});
|
|
2493
|
+
}
|
|
2494
|
+
if (hasImportAssertion) {
|
|
2495
|
+
const importValue = props.import === undefined ? '' : String(props.import);
|
|
2496
|
+
const hasPath = (importValue !== '' && importValue !== 'true') || 'from' in props || 'source' in props;
|
|
2497
|
+
const hasTextAssertion = 'contains' in props || 'notContains' in props || 'matches' in props;
|
|
2498
|
+
const hasUnmappedAssertion = 'unmapped' in props || props.no === 'unmapped';
|
|
2499
|
+
if (!hasPath) {
|
|
2500
|
+
violations.push({
|
|
2501
|
+
nodeType: 'expect',
|
|
2502
|
+
message: "'expect import' requires import=<ts-file>, from=<ts-file>, or source=<typescript>",
|
|
2503
|
+
line: node.loc?.line,
|
|
2504
|
+
col: node.loc?.col,
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
if (!hasTextAssertion && !hasRoundtripAssertion && !hasUnmappedAssertion) {
|
|
2508
|
+
violations.push({
|
|
2509
|
+
nodeType: 'expect',
|
|
2510
|
+
message: "'expect import' requires contains=, notContains=, matches=, roundtrip=true, unmapped=<count>, or no=unmapped",
|
|
2511
|
+
line: node.loc?.line,
|
|
2512
|
+
col: node.loc?.col,
|
|
2513
|
+
});
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
if (hasNegativeInvariant && hasPositiveInvariant) {
|
|
2517
|
+
violations.push({
|
|
2518
|
+
nodeType: 'expect',
|
|
2519
|
+
message: "'expect' cannot combine no=<invariant> and has=<invariant>",
|
|
2520
|
+
line: node.loc?.line,
|
|
2521
|
+
col: node.loc?.col,
|
|
2522
|
+
});
|
|
2523
|
+
}
|
|
2524
|
+
if (Number('fn' in props) +
|
|
2525
|
+
Number('derive' in props) +
|
|
2526
|
+
Number('route' in props) +
|
|
2527
|
+
Number('tool' in props) +
|
|
2528
|
+
Number('effect' in props) +
|
|
2529
|
+
Number('mock' in props) >
|
|
2530
|
+
1) {
|
|
2531
|
+
violations.push({
|
|
2532
|
+
nodeType: 'expect',
|
|
2533
|
+
message: "'expect' cannot combine fn=<name>, derive=<name>, route=<spec>, tool=<name>, effect=<name>, and mock=<name>",
|
|
2534
|
+
line: node.loc?.line,
|
|
2535
|
+
col: node.loc?.col,
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
if ((hasRuntimeBehavior || hasRuntimeWorkflow || hasMockCallAssertion) && hasRuntimeAssertion) {
|
|
2539
|
+
violations.push({
|
|
2540
|
+
nodeType: 'expect',
|
|
2541
|
+
message: "'expect' cannot combine fn/derive/route/tool/effect/mock behavioral assertions with expr={{...}}",
|
|
2542
|
+
line: node.loc?.line,
|
|
2543
|
+
col: node.loc?.col,
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
if (hasMockCallAssertion && (!('mock' in props) || !('called' in props))) {
|
|
2547
|
+
violations.push({
|
|
2548
|
+
nodeType: 'expect',
|
|
2549
|
+
message: "'expect' mock call assertions require both mock=<effect> and called=<count>",
|
|
2550
|
+
line: node.loc?.line,
|
|
2551
|
+
col: node.loc?.col,
|
|
2552
|
+
});
|
|
2553
|
+
}
|
|
2554
|
+
if ('called' in props) {
|
|
2555
|
+
const rawCalled = props.called;
|
|
2556
|
+
const called = Number(props.called);
|
|
2557
|
+
if (rawCalled === '' || !Number.isInteger(called) || called < 0) {
|
|
2558
|
+
violations.push({
|
|
2559
|
+
nodeType: 'expect',
|
|
2560
|
+
message: "'expect' mock call assertions require called=<non-negative integer>",
|
|
2561
|
+
line: node.loc?.line,
|
|
2562
|
+
col: node.loc?.col,
|
|
2563
|
+
});
|
|
2564
|
+
}
|
|
2565
|
+
}
|
|
2566
|
+
if (hasMockCallAssertion &&
|
|
2567
|
+
['args', 'with', 'input', 'equals', 'returns', 'recovers', 'fallback', 'matches', 'throws'].some((prop) => prop in props)) {
|
|
2568
|
+
violations.push({
|
|
2569
|
+
nodeType: 'expect',
|
|
2570
|
+
message: "'expect' mock call assertions cannot combine with runtime value/result props",
|
|
2571
|
+
line: node.loc?.line,
|
|
2572
|
+
col: node.loc?.col,
|
|
2573
|
+
});
|
|
2574
|
+
}
|
|
2575
|
+
if (hasMachineTransition && !('machine' in props)) {
|
|
2576
|
+
violations.push({
|
|
2577
|
+
nodeType: 'expect',
|
|
2578
|
+
message: "'expect' machine transition assertions require machine=<name>",
|
|
2579
|
+
line: node.loc?.line,
|
|
2580
|
+
col: node.loc?.col,
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
if (hasMachineTransition && 'reaches' in props) {
|
|
2584
|
+
violations.push({
|
|
2585
|
+
nodeType: 'expect',
|
|
2586
|
+
message: "'expect' cannot combine machine transition assertions with reaches=<state>",
|
|
2587
|
+
line: node.loc?.line,
|
|
2588
|
+
col: node.loc?.col,
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
if (hasMachineReachability && (!('machine' in props) || !('reaches' in props))) {
|
|
2592
|
+
violations.push({
|
|
2593
|
+
nodeType: 'expect',
|
|
2594
|
+
message: "'expect' machine reachability requires both 'machine' and 'reaches'",
|
|
2595
|
+
line: node.loc?.line,
|
|
2596
|
+
col: node.loc?.col,
|
|
2597
|
+
});
|
|
2598
|
+
}
|
|
2599
|
+
if (hasGuardExhaustiveness && props.exhaustive !== true && props.exhaustive !== 'true') {
|
|
2600
|
+
violations.push({
|
|
2601
|
+
nodeType: 'expect',
|
|
2602
|
+
message: "'expect' guard assertions require exhaustive=true",
|
|
2603
|
+
line: node.loc?.line,
|
|
2604
|
+
col: node.loc?.col,
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
}
|
|
2608
|
+
if (node.type === 'fixture') {
|
|
2609
|
+
const hasValue = 'value' in props;
|
|
2610
|
+
const hasExpr = 'expr' in props;
|
|
2611
|
+
if (!hasValue && !hasExpr) {
|
|
2612
|
+
violations.push({
|
|
2613
|
+
nodeType: 'fixture',
|
|
2614
|
+
message: "'fixture' requires either value={{...}} or expr={{...}}",
|
|
2615
|
+
line: node.loc?.line,
|
|
2616
|
+
col: node.loc?.col,
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
if (hasValue && hasExpr) {
|
|
2620
|
+
violations.push({
|
|
2621
|
+
nodeType: 'fixture',
|
|
2622
|
+
message: "'fixture' must not combine value={{...}} and expr={{...}}",
|
|
2623
|
+
line: node.loc?.line,
|
|
2624
|
+
col: node.loc?.col,
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
if (node.type === 'mock') {
|
|
2629
|
+
const hasReturns = 'returns' in props;
|
|
2630
|
+
const hasThrows = 'throws' in props;
|
|
2631
|
+
if (!hasReturns && !hasThrows) {
|
|
2632
|
+
violations.push({
|
|
2633
|
+
nodeType: 'mock',
|
|
2634
|
+
message: "'mock' requires either returns={{...}} or throws=<ErrorName>",
|
|
2635
|
+
line: node.loc?.line,
|
|
2636
|
+
col: node.loc?.col,
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
if (hasReturns && hasThrows) {
|
|
2640
|
+
violations.push({
|
|
2641
|
+
nodeType: 'mock',
|
|
2642
|
+
message: "'mock' must not combine returns={{...}} and throws=<ErrorName>",
|
|
2643
|
+
line: node.loc?.line,
|
|
2644
|
+
col: node.loc?.col,
|
|
2645
|
+
});
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2127
2648
|
if (node.type === 'fmt') {
|
|
2128
2649
|
const returnMode = isTruthyProp(props.return);
|
|
2129
2650
|
if (returnMode && 'name' in props) {
|
|
@@ -2191,7 +2712,18 @@ export function exportSchemaJSON(runtime) {
|
|
|
2191
2712
|
styleShorthands: { ...STYLE_SHORTHANDS },
|
|
2192
2713
|
valueShorthands: { ...VALUE_SHORTHANDS },
|
|
2193
2714
|
multilineBlockTypes: [...rt.multilineBlockTypes],
|
|
2194
|
-
propKinds: [
|
|
2715
|
+
propKinds: [
|
|
2716
|
+
'identifier',
|
|
2717
|
+
'typeAnnotation',
|
|
2718
|
+
'importPath',
|
|
2719
|
+
'rawExpr',
|
|
2720
|
+
'rawBlock',
|
|
2721
|
+
'string',
|
|
2722
|
+
'boolean',
|
|
2723
|
+
'number',
|
|
2724
|
+
'expression',
|
|
2725
|
+
'regex',
|
|
2726
|
+
],
|
|
2195
2727
|
...(evolvedTypes.length > 0 ? { evolvedTypes } : {}),
|
|
2196
2728
|
};
|
|
2197
2729
|
}
|