@kernlang/core 3.4.4 → 3.4.5-canary.16.1.649c3f23

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.
@@ -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`, body-form `if`/`else`,
4
- * body-form `try`) are valid only inside a `handler lang="kern"` scope
5
- * (or nested inside another body-statement under such a handler). Without
6
- * this rule, the parser silently accepts orphan `return`/`throw` lines
7
- * that then crash codegen with confusing errors deep in the body emitter.
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`, and `do` are rejected outside a native-body scope.
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
@@ -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`, body-form `if`/`else`,
4
- * body-form `try`) are valid only inside a `handler lang="kern"` scope
5
- * (or nested inside another body-statement under such a handler). Without
6
- * this rule, the parser silently accepts orphan `return`/`throw` lines
7
- * that then crash codegen with confusing errors deep in the body emitter.
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`, and `do` are rejected outside a native-body scope.
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;;;;;;;;;;;;;;;;;;;;;;;GAuBG;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;YACP,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"}
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: ['step', 'handler', 'catch', 'let', 'do', 'return', 'if', 'else', 'each', 'try', 'throw'],
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: ['handler', 'let', 'do', 'return', 'if', 'else', 'each', 'try', 'throw'],
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: ['let', 'do', 'return', 'if', 'else', 'each', 'try', 'catch', 'throw'],
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 in a resolve/branch tree',
2237
- example: 'path value="/api/users"',
2238
- props: { value: { required: true, kind: 'string' } },
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 && !(propName in props)) {
2435
- violations.push({
2436
- nodeType: node.type,
2437
- message: `'${node.type}' requires prop '${propName}'`,
2438
- line: node.loc?.line,
2439
- col: node.loc?.col,
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';