@kernlang/core 3.4.4 → 3.4.5-canary.15.1.9efc3d4b
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/codegen/body-ts.d.ts +1 -0
- package/dist/codegen/body-ts.js +128 -2
- package/dist/codegen/body-ts.js.map +1 -1
- package/dist/codegen-core.js +18 -3
- package/dist/codegen-core.js.map +1 -1
- package/dist/codegen-expression.js +46 -6
- package/dist/codegen-expression.js.map +1 -1
- package/dist/concepts.d.ts +13 -0
- package/dist/concepts.js.map +1 -1
- package/dist/decompiler.js +16 -1
- package/dist/decompiler.js.map +1 -1
- package/dist/native-eligibility-ast.js +71 -3
- package/dist/native-eligibility-ast.js.map +1 -1
- package/dist/parser-expression.d.ts +5 -4
- package/dist/parser-expression.js +108 -4
- package/dist/parser-expression.js.map +1 -1
- package/dist/parser-validate-body-statements.d.ts +8 -6
- package/dist/parser-validate-body-statements.js +10 -6
- package/dist/parser-validate-body-statements.js.map +1 -1
- package/dist/schema.js +178 -17
- package/dist/schema.js.map +1 -1
- package/dist/semantic-validator.js +18 -0
- package/dist/semantic-validator.js.map +1 -1
- package/dist/spec.d.ts +1 -1
- package/dist/spec.js +1 -1
- package/dist/spec.js.map +1 -1
- package/dist/value-ir.d.ts +10 -0
- package/dist/value-ir.js +2 -0
- package/dist/value-ir.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
/** @internal Native KERN body-statement context validator — slice 5b-pre.
|
|
2
2
|
*
|
|
3
|
-
* Body-statement nodes (`return`, `throw`, `do`,
|
|
4
|
-
* body-form `try`) are valid only inside a
|
|
5
|
-
* (or nested inside another body-statement
|
|
6
|
-
* this rule, the parser silently accepts
|
|
7
|
-
* that then crash codegen with confusing
|
|
3
|
+
* Body-statement nodes (`return`, `throw`, `do`, `continue`, `break`,
|
|
4
|
+
* body-form `if`/`else`, body-form `try`) are valid only inside a
|
|
5
|
+
* `handler lang="kern"` scope (or nested inside another body-statement
|
|
6
|
+
* under such a handler). Without this rule, the parser silently accepts
|
|
7
|
+
* orphan `return`/`throw` lines that then crash codegen with confusing
|
|
8
|
+
* errors deep in the body emitter.
|
|
8
9
|
*
|
|
9
10
|
* Rules:
|
|
10
|
-
* - `return`, `throw`,
|
|
11
|
+
* - `return`, `throw`, `do`, `continue`, `break` are rejected outside
|
|
12
|
+
* a native-body scope.
|
|
11
13
|
* - `if` with a `cond` prop is body-statement form (vs `conditional`'s
|
|
12
14
|
* `if=` prop); rejected outside native-body scope.
|
|
13
15
|
* - `else` whose parent is not `conditional` is body-statement form
|
|
@@ -51,6 +53,8 @@ function isBodyStatementMisplaced(node, ctx) {
|
|
|
51
53
|
case 'return':
|
|
52
54
|
case 'throw':
|
|
53
55
|
case 'do':
|
|
56
|
+
case 'continue':
|
|
57
|
+
case 'break':
|
|
54
58
|
return true;
|
|
55
59
|
case 'if':
|
|
56
60
|
// Body-statement `if` carries a `cond` prop. `conditional` and route-
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"parser-validate-body-statements.js","sourceRoot":"","sources":["../src/parser-validate-body-statements.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"parser-validate-body-statements.js","sourceRoot":"","sources":["../src/parser-validate-body-statements.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,OAAO,EAAE,cAAc,EAAmB,MAAM,yBAAyB,CAAC;AAU1E,MAAM,QAAQ,GAAgB,EAAE,YAAY,EAAE,KAAK,EAAE,UAAU,EAAE,IAAI,EAAE,CAAC;AAExE,MAAM,UAAU,sBAAsB,CAAC,KAAiB,EAAE,IAAY;IACpE,IAAI,CAAC,KAAK,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC;AAC9B,CAAC;AAED,SAAS,IAAI,CAAC,KAAiB,EAAE,IAAY,EAAE,GAAgB;IAC7D,IAAI,wBAAwB,CAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;QACxC,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,EAAE,IAAI,EAAE,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,CAAC;QACvD,cAAc,CACZ,KAAK,EACL,uCAAuC,EACvC,OAAO,EACP,KAAK,IAAI,CAAC,IAAI,sJAAsJ,EACpK,GAAG,CAAC,IAAI,EACR,GAAG,CAAC,GAAG,EACP,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,CACtC,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAgB;QAC5B,YAAY,EAAE,GAAG,CAAC,YAAY,IAAI,mBAAmB,CAAC,IAAI,CAAC;QAC3D,UAAU,EAAE,IAAI,CAAC,IAAI;KACtB,CAAC;IACF,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAClB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;YAAE,IAAI,CAAC,KAAK,EAAE,KAAK,EAAE,QAAQ,CAAC,CAAC;IAClE,CAAC;AACH,CAAC;AAED,SAAS,mBAAmB,CAAC,IAAY;IACvC,OAAO,IAAI,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,CAAC,KAAK,EAAE,IAAI,KAAK,MAAM,CAAC;AAChE,CAAC;AAED,SAAS,wBAAwB,CAAC,IAAY,EAAE,GAAgB;IAC9D,IAAI,GAAG,CAAC,YAAY;QAAE,OAAO,KAAK,CAAC;IAEnC,QAAQ,IAAI,CAAC,IAAI,EAAE,CAAC;QAClB,KAAK,QAAQ,CAAC;QACd,KAAK,OAAO,CAAC;QACb,KAAK,IAAI,CAAC;QACV,KAAK,UAAU,CAAC;QAChB,KAAK,OAAO;YACV,OAAO,IAAI,CAAC;QACd,KAAK,IAAI;YACP,sEAAsE;YACtE,mEAAmE;YACnE,kEAAkE;YAClE,kBAAkB;YAClB,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,KAAK,SAAS,CAAC;QACxC,KAAK,MAAM;YACT,oEAAoE;YACpE,kEAAkE;YAClE,wBAAwB;YACxB,OAAO,GAAG,CAAC,UAAU,KAAK,aAAa,CAAC;QAC1C,KAAK,KAAK;YACR,kEAAkE;YAClE,gEAAgE;YAChE,+DAA+D;YAC/D,4DAA4D;YAC5D,OAAO,IAAI,CAAC,KAAK,EAAE,IAAI,KAAK,SAAS,CAAC;QACxC;YACE,OAAO,KAAK,CAAC;IACjB,CAAC;AACH,CAAC"}
|
package/dist/schema.js
CHANGED
|
@@ -482,7 +482,22 @@ export const NODE_SCHEMAS = {
|
|
|
482
482
|
// plus a required `catch` child. Schema permits both child sets;
|
|
483
483
|
// body-ts.ts disambiguates by inspecting the children, and validateBodyStatements
|
|
484
484
|
// enforces the body-statement-only constraints when the enclosing handler is `lang="kern"`.
|
|
485
|
-
allowedChildren: [
|
|
485
|
+
allowedChildren: [
|
|
486
|
+
'step',
|
|
487
|
+
'handler',
|
|
488
|
+
'catch',
|
|
489
|
+
'let',
|
|
490
|
+
'do',
|
|
491
|
+
'return',
|
|
492
|
+
'if',
|
|
493
|
+
'else',
|
|
494
|
+
'each',
|
|
495
|
+
'try',
|
|
496
|
+
'throw',
|
|
497
|
+
'continue',
|
|
498
|
+
'break',
|
|
499
|
+
'branch',
|
|
500
|
+
],
|
|
486
501
|
},
|
|
487
502
|
step: {
|
|
488
503
|
description: 'Sequential awaited step inside a `try` block — `step name=X await="expr"` emits `const X = await (expr);` in order with its siblings. Must be a direct child of `try`. Earlier step names are in scope for later ones.',
|
|
@@ -499,7 +514,20 @@ export const NODE_SCHEMAS = {
|
|
|
499
514
|
props: {
|
|
500
515
|
name: { kind: 'identifier' },
|
|
501
516
|
},
|
|
502
|
-
allowedChildren: [
|
|
517
|
+
allowedChildren: [
|
|
518
|
+
'handler',
|
|
519
|
+
'let',
|
|
520
|
+
'do',
|
|
521
|
+
'return',
|
|
522
|
+
'if',
|
|
523
|
+
'else',
|
|
524
|
+
'each',
|
|
525
|
+
'try',
|
|
526
|
+
'throw',
|
|
527
|
+
'continue',
|
|
528
|
+
'break',
|
|
529
|
+
'branch',
|
|
530
|
+
],
|
|
503
531
|
},
|
|
504
532
|
filter: {
|
|
505
533
|
description: 'Declarative `.filter` binding — `filter name=active in=items where="item.active"` lowers to `const active = items.filter(item => item.active);`. Use `item=x` to rename the per-item binding.',
|
|
@@ -1080,13 +1108,21 @@ export const NODE_SCHEMAS = {
|
|
|
1080
1108
|
},
|
|
1081
1109
|
},
|
|
1082
1110
|
each: {
|
|
1083
|
-
description: 'Iteration — renders children for each item in a collection. Inside a render block emits `items.map(...)` with auto-key; elsewhere emits `for...of`. `let` children become iteration-scoped `const` bindings inside the callback (hook-safe, unlike `derive`).',
|
|
1111
|
+
description: 'Iteration — renders children for each item in a collection. Inside a render block emits `items.map(...)` with auto-key; elsewhere emits `for...of`. `let` children become iteration-scoped `const` bindings inside the callback (hook-safe, unlike `derive`). Three forms in body-statement position: (1) `each name=x in=xs` → `for (const x of xs)`; (2) `each name=x index=i in=xs` → `for (const [i, x] of xs.entries())`; (3) `each pairKey=k pairValue=v in=map` → `for (const [k, v] of map)` (TS) / `for k, v in map.items():` (Python). In pair-mode `name` is optional. `key=` (render-only) is the React render key, distinct from `pairKey=`.',
|
|
1084
1112
|
example: 'each name=f in=files index=i key="f.path"\n let name=isSel expr="focused && i === selIdx"\n handler <<<\n <Text bold={isSel}>{f.path}</Text>\n >>>',
|
|
1085
1113
|
props: {
|
|
1114
|
+
// `name` is required schema-side for the array-iteration forms; the
|
|
1115
|
+
// pair-mode (`pairKey` + `pairValue`) form relaxes this via a
|
|
1116
|
+
// conditional-required exemption inside `checkRequiredProps` in this
|
|
1117
|
+
// same file. (The `each-pair-mode-body-stmt-only` semantic rule in
|
|
1118
|
+
// semantic-validator.ts is unrelated — it just blocks pair-mode in
|
|
1119
|
+
// render/group ancestor scope.)
|
|
1086
1120
|
name: { required: true, kind: 'identifier' },
|
|
1087
1121
|
in: { required: true, kind: 'rawExpr' },
|
|
1088
1122
|
index: { kind: 'identifier' },
|
|
1089
1123
|
key: { kind: 'rawExpr' },
|
|
1124
|
+
pairKey: { kind: 'identifier' },
|
|
1125
|
+
pairValue: { kind: 'identifier' },
|
|
1090
1126
|
},
|
|
1091
1127
|
// Intentionally unrestricted — statement-form `each` composes with `derive`,
|
|
1092
1128
|
// `transform`, etc. in fn/handler contexts. The `let` node is constrained
|
|
@@ -1124,9 +1160,11 @@ export const NODE_SCHEMAS = {
|
|
|
1124
1160
|
},
|
|
1125
1161
|
},
|
|
1126
1162
|
branch: {
|
|
1127
|
-
description: 'Pattern-match/switch on an expression — contains path children',
|
|
1128
|
-
example: 'branch name=route on=path\n path value="/home"\n path value="/about"',
|
|
1163
|
+
description: 'Pattern-match/switch on an expression — contains `path` children. Top-level form (statement context) emits TS `switch` with `case` blocks. Body-statement form (child of `handler lang="kern"` / `try` / `catch`) emits the same TS `switch` plus a Python `if/elif/else` chain on the fastapi target. Each `path value=X` is a case; `path default=true` is the trailing default case (parallels JS `switch`/`default`). Identifier values like `path value=Status.Active` (unquoted) emit raw refs; quoted strings emit JSON-quoted literals.',
|
|
1164
|
+
example: 'branch name=route on=path\n path value="/home"\n path value="/about"\n path default=true',
|
|
1129
1165
|
props: {
|
|
1166
|
+
// `name` is required for the top-level branch shape; body-statement
|
|
1167
|
+
// branches inherit the same shape for diagnostic clarity.
|
|
1130
1168
|
name: { required: true, kind: 'identifier' },
|
|
1131
1169
|
on: { required: true, kind: 'rawExpr' },
|
|
1132
1170
|
},
|
|
@@ -1338,7 +1376,7 @@ export const NODE_SCHEMAS = {
|
|
|
1338
1376
|
},
|
|
1339
1377
|
// ── Cross-target nodes ────────────────────────────────────────────────
|
|
1340
1378
|
handler: {
|
|
1341
|
-
description: 'Code block — the body of a function, method, route, tool, or event handler. Use <<<...>>> for raw multiline code, or `lang="kern"` with body-statement children (`let`/`do`/`return`/`if`/`else`/`each`/`try`/`catch`/`throw`) for cross-target structured bodies.',
|
|
1379
|
+
description: 'Code block — the body of a function, method, route, tool, or event handler. Use <<<...>>> for raw multiline code, or `lang="kern"` with body-statement children (`let`/`do`/`return`/`if`/`else`/`each`/`try`/`catch`/`throw`/`continue`/`break`/`branch`) for cross-target structured bodies. Use `continue` inside `each` to skip the current iteration; use `break` inside `each` to exit the innermost loop. Use `branch` for switch-style structural matching (TS `switch`, Python `if/elif/else`). Prefer these over raw handlers for loop-control and dispatch bodies.',
|
|
1342
1380
|
example: 'handler <<<\n const result = await doWork();\n return result;\n>>>',
|
|
1343
1381
|
props: {
|
|
1344
1382
|
code: { kind: 'rawBlock' },
|
|
@@ -1348,7 +1386,21 @@ export const NODE_SCHEMAS = {
|
|
|
1348
1386
|
// body statements are rejected by validateBodyStatements (the schema list
|
|
1349
1387
|
// is intentionally permissive so the validator can produce a clearer
|
|
1350
1388
|
// context-aware error).
|
|
1351
|
-
allowedChildren: [
|
|
1389
|
+
allowedChildren: [
|
|
1390
|
+
'let',
|
|
1391
|
+
'destructure',
|
|
1392
|
+
'do',
|
|
1393
|
+
'return',
|
|
1394
|
+
'if',
|
|
1395
|
+
'else',
|
|
1396
|
+
'each',
|
|
1397
|
+
'try',
|
|
1398
|
+
'catch',
|
|
1399
|
+
'throw',
|
|
1400
|
+
'continue',
|
|
1401
|
+
'break',
|
|
1402
|
+
'branch',
|
|
1403
|
+
],
|
|
1352
1404
|
},
|
|
1353
1405
|
return: {
|
|
1354
1406
|
description: 'Body-statement return — emits `return value` (or bare `return;` when `value` is omitted) inside a `lang="kern"` handler body. Only valid as a child of `handler lang="kern"` or another body-statement (if/else/each/try/catch).',
|
|
@@ -1371,6 +1423,16 @@ export const NODE_SCHEMAS = {
|
|
|
1371
1423
|
value: { kind: 'expression' },
|
|
1372
1424
|
},
|
|
1373
1425
|
},
|
|
1426
|
+
continue: {
|
|
1427
|
+
description: 'Body-statement loop-continue — emits `continue;` (TS) or `continue` (Python). Only valid inside a `lang="kern"` handler body, and the surrounding TS/Python compiler still rejects use outside an enclosing loop. Pair with `each` to express skip-this-iteration logic without dropping into a raw handler.',
|
|
1428
|
+
example: 'each name=item in=items\n if cond="item.skip"\n continue\n do value="process(item)"',
|
|
1429
|
+
props: {},
|
|
1430
|
+
},
|
|
1431
|
+
break: {
|
|
1432
|
+
description: 'Body-statement loop-break — emits `break;` (TS) or `break` (Python). Only valid inside a `lang="kern"` handler body, and the surrounding TS/Python compiler still rejects use outside an enclosing loop. Pair with `each` to express early-exit search/find loops without dropping into a raw handler.',
|
|
1433
|
+
example: 'each name=item in=items\n if cond="item.matches"\n let name=found value="item"\n break',
|
|
1434
|
+
props: {},
|
|
1435
|
+
},
|
|
1374
1436
|
if: {
|
|
1375
1437
|
description: 'Body-statement if — emits `if (cond) { ... }` inside a `lang="kern"` handler body. Optional `else` SIBLING (not child) emits `} else { ... }`. Distinct from the `if=` prop on `conditional` and route-guard nodes.',
|
|
1376
1438
|
example: 'if cond="user.active"\n return value="true"',
|
|
@@ -2233,9 +2295,15 @@ export const NODE_SCHEMAS = {
|
|
|
2233
2295
|
},
|
|
2234
2296
|
// Ground layer — semantic reasoning
|
|
2235
2297
|
path: {
|
|
2236
|
-
description: 'Decision path — a named branch
|
|
2237
|
-
example: 'path value="/api/users"',
|
|
2238
|
-
props: {
|
|
2298
|
+
description: 'Decision path — a named branch inside a `branch` (or resolve) tree. Provide exactly one of `value=` (the case literal: quoted → string compare, unquoted → identifier reference such as `Status.Active`) or `default=true` (the trailing fallback case, parallels JS `switch`/`default`). The validator rejects paths that supply neither or both. Body-statement child of `branch` admits the same body-statements as `handler lang="kern"`.',
|
|
2299
|
+
example: 'path value="/api/users"\npath value=Status.Active\npath default=true',
|
|
2300
|
+
props: {
|
|
2301
|
+
// `value` is no longer schema-required because `path default=true` is a
|
|
2302
|
+
// legal alternative shape. `path-shape` semantic rule enforces
|
|
2303
|
+
// exactly-one-of(value, default) at validation time.
|
|
2304
|
+
value: { kind: 'string' },
|
|
2305
|
+
default: { kind: 'boolean' },
|
|
2306
|
+
},
|
|
2239
2307
|
},
|
|
2240
2308
|
resolve: {
|
|
2241
2309
|
description: 'Resolution node — selects among candidates using a discriminator',
|
|
@@ -2431,14 +2499,33 @@ const UNIVERSAL_CHILDREN = new Set(['handler', 'cleanup', 'reason', 'evidence',
|
|
|
2431
2499
|
function checkRequiredProps(node, schema, violations) {
|
|
2432
2500
|
const props = node.props || {};
|
|
2433
2501
|
for (const [propName, propSchema] of Object.entries(schema.props)) {
|
|
2434
|
-
if (propSchema.required
|
|
2435
|
-
|
|
2436
|
-
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2502
|
+
if (!propSchema.required)
|
|
2503
|
+
continue;
|
|
2504
|
+
if (propName in props)
|
|
2505
|
+
continue;
|
|
2506
|
+
// each-pair-mode (2026-05-06): `name` becomes optional when both
|
|
2507
|
+
// `pairKey` and `pairValue` are present (Map / iterable-of-pairs form).
|
|
2508
|
+
// The schema can't express conditional-required, so suppress the
|
|
2509
|
+
// `name`-required violation here under that exact shape. Other props
|
|
2510
|
+
// (notably `in=`) still error if missing.
|
|
2511
|
+
// Codex review-fix (mid-build, confidence 0.91): require BOTH props to
|
|
2512
|
+
// be non-empty strings — accepting `null`/`0`/`false` here would let
|
|
2513
|
+
// malformed source bypass the `name=` requirement and emit silently-wrong
|
|
2514
|
+
// loop bindings (codegen does strict-truthy detection later).
|
|
2515
|
+
if (node.type === 'each' &&
|
|
2516
|
+
propName === 'name' &&
|
|
2517
|
+
typeof props.pairKey === 'string' &&
|
|
2518
|
+
props.pairKey.length > 0 &&
|
|
2519
|
+
typeof props.pairValue === 'string' &&
|
|
2520
|
+
props.pairValue.length > 0) {
|
|
2521
|
+
continue;
|
|
2441
2522
|
}
|
|
2523
|
+
violations.push({
|
|
2524
|
+
nodeType: node.type,
|
|
2525
|
+
message: `'${node.type}' requires prop '${propName}'`,
|
|
2526
|
+
line: node.loc?.line,
|
|
2527
|
+
col: node.loc?.col,
|
|
2528
|
+
});
|
|
2442
2529
|
}
|
|
2443
2530
|
}
|
|
2444
2531
|
function checkCrossProps(node, violations) {
|
|
@@ -2699,6 +2786,80 @@ function checkCrossProps(node, violations) {
|
|
|
2699
2786
|
// is only valid inside `render`/`group` — the positional check lives in
|
|
2700
2787
|
// the semantic validator, which has ancestry context.
|
|
2701
2788
|
}
|
|
2789
|
+
if (node.type === 'path') {
|
|
2790
|
+
// path-shape (2026-05-06): exactly one of `value=` (case literal) or
|
|
2791
|
+
// `default=true` (trailing fallback). Both → ambiguous; neither → empty
|
|
2792
|
+
// case clause that codegen can't emit. Schema dropped `value: required`
|
|
2793
|
+
// so this rule replaces the requirement with a context-aware shape check.
|
|
2794
|
+
const hasValue = 'value' in props;
|
|
2795
|
+
const hasDefault = isTruthyProp(props.default);
|
|
2796
|
+
if (!hasValue && !hasDefault) {
|
|
2797
|
+
violations.push({
|
|
2798
|
+
nodeType: 'path',
|
|
2799
|
+
message: "'path' requires either 'value=' (case literal) or 'default=true' (trailing fallback)",
|
|
2800
|
+
line: node.loc?.line,
|
|
2801
|
+
col: node.loc?.col,
|
|
2802
|
+
});
|
|
2803
|
+
}
|
|
2804
|
+
if (hasValue && hasDefault) {
|
|
2805
|
+
violations.push({
|
|
2806
|
+
nodeType: 'path',
|
|
2807
|
+
message: "'path' must not combine 'value=' and 'default=true' — choose one",
|
|
2808
|
+
line: node.loc?.line,
|
|
2809
|
+
col: node.loc?.col,
|
|
2810
|
+
});
|
|
2811
|
+
}
|
|
2812
|
+
}
|
|
2813
|
+
if (node.type === 'branch') {
|
|
2814
|
+
// At most one default `path` per branch. More than one is a structural
|
|
2815
|
+
// bug — codegen would emit unreachable trailing default clauses.
|
|
2816
|
+
const defaultCount = (node.children ?? []).filter((c) => c.type === 'path' && isTruthyProp(c.props?.default)).length;
|
|
2817
|
+
if (defaultCount > 1) {
|
|
2818
|
+
violations.push({
|
|
2819
|
+
nodeType: 'branch',
|
|
2820
|
+
message: `'branch' must contain at most one 'path default=true' (found ${defaultCount})`,
|
|
2821
|
+
line: node.loc?.line,
|
|
2822
|
+
col: node.loc?.col,
|
|
2823
|
+
});
|
|
2824
|
+
}
|
|
2825
|
+
}
|
|
2826
|
+
if (node.type === 'each') {
|
|
2827
|
+
// each-pair-mode (2026-05-06): the Map / iterable-of-pairs form uses
|
|
2828
|
+
// `pairKey=` + `pairValue=` for `for (const [k, v] of m)`. Three rules:
|
|
2829
|
+
// 1. pairKey and pairValue come as a pair — neither alone is meaningful.
|
|
2830
|
+
// 2. pair-mode is incompatible with `index=` (entries-with-index form).
|
|
2831
|
+
// 3. `name=` becomes optional in pair-mode (relaxes schema `required`).
|
|
2832
|
+
// Codex review-fix (2026-05-06, mid-build, confidence 0.91): use a strict
|
|
2833
|
+
// string check so malformed source like `pairKey: null` / `pairValue: 0`
|
|
2834
|
+
// is treated as ABSENT rather than as a truthy pair-mode declaration.
|
|
2835
|
+
// The previous `!== '' && !== undefined` check accepted `null`/`0`/`false`,
|
|
2836
|
+
// which allowed validator bypass when codegen later does a strict-truthy
|
|
2837
|
+
// check (codegen would fall back to plain `each name=item` and silently
|
|
2838
|
+
// emit a wrong loop binding).
|
|
2839
|
+
const isPairProp = (raw) => typeof raw === 'string' && raw.length > 0;
|
|
2840
|
+
const hasPairKey = isPairProp(props.pairKey);
|
|
2841
|
+
const hasPairValue = isPairProp(props.pairValue);
|
|
2842
|
+
const hasIndex = isPairProp(props.index);
|
|
2843
|
+
if (hasPairKey !== hasPairValue) {
|
|
2844
|
+
violations.push({
|
|
2845
|
+
nodeType: 'each',
|
|
2846
|
+
message: "'each' pair-mode requires both 'pairKey=' AND 'pairValue=' (or neither)",
|
|
2847
|
+
line: node.loc?.line,
|
|
2848
|
+
col: node.loc?.col,
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
if (hasPairKey && hasPairValue && hasIndex) {
|
|
2852
|
+
violations.push({
|
|
2853
|
+
nodeType: 'each',
|
|
2854
|
+
message: "'each' pair-mode ('pairKey'+'pairValue') is mutually exclusive with 'index='",
|
|
2855
|
+
line: node.loc?.line,
|
|
2856
|
+
col: node.loc?.col,
|
|
2857
|
+
});
|
|
2858
|
+
}
|
|
2859
|
+
// Pair-mode relaxes `name` requirement — handled by checkRequiredProps
|
|
2860
|
+
// (suppresses the `name`-required violation when pairKey+pairValue are
|
|
2861
|
+
// both present).
|
|
2862
|
+
}
|
|
2702
2863
|
}
|
|
2703
2864
|
function isTruthyProp(raw) {
|
|
2704
2865
|
return raw === true || raw === 'true';
|