@kernlang/core 3.3.4 → 3.3.6

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.
@@ -29,6 +29,38 @@ export function generateDerive(node) {
29
29
  const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
30
30
  return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = ${expr};`];
31
31
  }
32
+ // ── Ground Layer: fmt ────────────────────────────────────────────────────
33
+ // `fmt name=label template="${count} files"` →
34
+ // const label = `${count} files`;
35
+ //
36
+ // Why a dedicated node: string interpolation is ~15-20% of handler-block
37
+ // volume in agon (2026-04-20 scan). Expressing it as a named primitive keeps
38
+ // the IR declarative and lets tooling (reviewers, decompiler, codegen)
39
+ // recognise "this is a formatted string" without parsing a handler body.
40
+ //
41
+ // The template body is spliced verbatim into a JS template literal, so
42
+ // `${expr}` placeholders work exactly as in JS. Raw backticks in the author
43
+ // input are escaped to `\`` so the emitted template literal cannot be closed
44
+ // accidentally — `${...}` is the contract, arbitrary JS injection is not.
45
+ export function generateFmt(node) {
46
+ const annotations = emitReasonAnnotations(node);
47
+ const props = propsOf(node);
48
+ const conf = props.confidence;
49
+ const todo = emitLowConfidenceTodo(node, conf);
50
+ const name = emitIdentifier(props.name, 'formatted', node);
51
+ const template = props.template;
52
+ if (template === undefined || template === null) {
53
+ throw new KernCodegenError("fmt node requires a 'template' prop", node);
54
+ }
55
+ const constType = props.type;
56
+ const exp = exportPrefix(node);
57
+ // Escape backticks so the emitted template literal can't be closed
58
+ // prematurely. `${...}` is intentionally passed through untouched — that's
59
+ // the whole reason fmt exists.
60
+ const escapedTemplate = String(template).replace(/\\/g, '\\\\').replace(/`/g, '\\`');
61
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
62
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = \`${escapedTemplate}\`;`];
63
+ }
32
64
  // ── Ground Layer: transform ──────────────────────────────────────────────
33
65
  export function generateTransform(node) {
34
66
  const annotations = emitReasonAnnotations(node);
@@ -292,6 +324,945 @@ export function generateExpect(node) {
292
324
  lines.push('}');
293
325
  return lines;
294
326
  }
327
+ // ── Ground Layer: array methods ──────────────────────────────────────────
328
+ // Declarative array-method primitives — each is its own named node type:
329
+ // filter / find / some / every / findIndex → predicate-over-collection
330
+ // map / flatMap → projection (arrow body)
331
+ // reduce → accumulation (acc + item)
332
+ // sort → optional compare function (immutable via spread)
333
+ // reverse / flat / slice / at → shape-preserving or range ops
334
+ // join / includes / indexOf / lastIndexOf → value-returning lookups
335
+ // concat → array concatenation
336
+ // forEach → side-effect loop (statement, no binding)
337
+ //
338
+ // Why distinct names instead of one generic: KERN is an LLM-authored
339
+ // language. Giving each method a named structural anchor lets tooling
340
+ // (decompiler, review, codegen) recognise author intent without grepping
341
+ // a method name out of a string. The `where` / `expr` prop split mirrors
342
+ // the semantic distinction: `where` = boolean predicate, `expr` = arrow
343
+ // body returning a value. `each` remains the render-block JSX iteration
344
+ // primitive; `map` is its expression-form sibling for data-transformation.
345
+ /** Unwrap an `{ __expr: true, code }` shape or pass through a plain string. */
346
+ function unwrapExpr(raw) {
347
+ if (raw == null)
348
+ return undefined;
349
+ if (typeof raw === 'object' && raw.__expr) {
350
+ return raw.code;
351
+ }
352
+ return typeof raw === 'string' ? raw : String(raw);
353
+ }
354
+ function generateArrayMethod(node, method) {
355
+ const annotations = emitReasonAnnotations(node);
356
+ const props = propsOf(node);
357
+ const conf = props.confidence;
358
+ const todo = emitLowConfidenceTodo(node, conf);
359
+ const name = emitIdentifier(props.name, method, node);
360
+ const item = emitIdentifier(props.item || 'item', 'item', node);
361
+ const collection = unwrapExpr(props.in);
362
+ if (!collection)
363
+ throw new KernCodegenError(`${method} node requires an 'in' prop`, node);
364
+ const predicate = unwrapExpr(props.where);
365
+ if (!predicate)
366
+ throw new KernCodegenError(`${method} node requires a 'where' prop`, node);
367
+ const constType = props.type;
368
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
369
+ const exp = exportPrefix(node);
370
+ return [
371
+ ...todo,
372
+ ...annotations,
373
+ `${exp}const ${name}${typeAnnotation} = (${collection}).${method}((${item}) => ${predicate});`,
374
+ ];
375
+ }
376
+ export function generateFilter(node) {
377
+ return generateArrayMethod(node, 'filter');
378
+ }
379
+ // ── Ground Layer: reduce ─────────────────────────────────────────────────
380
+ // `reduce name=total in=items initial="0" expr="acc + item.value"`
381
+ // → const total = items.reduce((acc, item) => acc + item.value, 0);
382
+ // Two bound names (acc, item) default to those identifiers; override with
383
+ // `acc=` / `item=`. Body (`expr`) and seed (`initial`) are both required.
384
+ export function generateReduce(node) {
385
+ const annotations = emitReasonAnnotations(node);
386
+ const props = propsOf(node);
387
+ const conf = props.confidence;
388
+ const todo = emitLowConfidenceTodo(node, conf);
389
+ const name = emitIdentifier(props.name, 'reduced', node);
390
+ const acc = emitIdentifier(props.acc || 'acc', 'acc', node);
391
+ const item = emitIdentifier(props.item || 'item', 'item', node);
392
+ const collection = unwrapExpr(props.in);
393
+ if (!collection)
394
+ throw new KernCodegenError("reduce node requires an 'in' prop", node);
395
+ const initial = unwrapExpr(props.initial);
396
+ if (!initial)
397
+ throw new KernCodegenError("reduce node requires an 'initial' prop", node);
398
+ const body = unwrapExpr(props.expr);
399
+ if (!body)
400
+ throw new KernCodegenError("reduce node requires an 'expr' prop", node);
401
+ const constType = props.type;
402
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
403
+ const exp = exportPrefix(node);
404
+ return [
405
+ ...todo,
406
+ ...annotations,
407
+ `${exp}const ${name}${typeAnnotation} = (${collection}).reduce((${acc}, ${item}) => ${body}, ${initial});`,
408
+ ];
409
+ }
410
+ // ── Ground Layer: flatMap ────────────────────────────────────────────────
411
+ // `flatMap name=tags in=posts expr="item.tags"`
412
+ // → const tags = posts.flatMap((item) => item.tags);
413
+ // `expr` is the arrow body (array/iterable), not a predicate. Use the same
414
+ // shape as `filter` etc., but with `expr` instead of `where`.
415
+ export function generateFlatMap(node) {
416
+ const annotations = emitReasonAnnotations(node);
417
+ const props = propsOf(node);
418
+ const conf = props.confidence;
419
+ const todo = emitLowConfidenceTodo(node, conf);
420
+ const name = emitIdentifier(props.name, 'flatMapped', node);
421
+ const item = emitIdentifier(props.item || 'item', 'item', node);
422
+ const collection = unwrapExpr(props.in);
423
+ if (!collection)
424
+ throw new KernCodegenError("flatMap node requires an 'in' prop", node);
425
+ const body = unwrapExpr(props.expr);
426
+ if (!body)
427
+ throw new KernCodegenError("flatMap node requires an 'expr' prop", node);
428
+ const constType = props.type;
429
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
430
+ const exp = exportPrefix(node);
431
+ return [
432
+ ...todo,
433
+ ...annotations,
434
+ `${exp}const ${name}${typeAnnotation} = (${collection}).flatMap((${item}) => ${body});`,
435
+ ];
436
+ }
437
+ // ── Ground Layer: slice ──────────────────────────────────────────────────
438
+ // `slice name=first5 in=items start=0 end=5`
439
+ // → const first5 = items.slice(0, 5);
440
+ // Both indices are optional — `.slice()` with no args copies the whole
441
+ // array, `.slice(2)` copies from index 2 onward. Emit exactly what was
442
+ // supplied, in that order.
443
+ export function generateSlice(node) {
444
+ const annotations = emitReasonAnnotations(node);
445
+ const props = propsOf(node);
446
+ const conf = props.confidence;
447
+ const todo = emitLowConfidenceTodo(node, conf);
448
+ const name = emitIdentifier(props.name, 'sliced', node);
449
+ const collection = unwrapExpr(props.in);
450
+ if (!collection)
451
+ throw new KernCodegenError("slice node requires an 'in' prop", node);
452
+ const start = unwrapExpr(props.start);
453
+ const end = unwrapExpr(props.end);
454
+ const args = [];
455
+ if (start !== undefined)
456
+ args.push(start);
457
+ if (end !== undefined) {
458
+ if (start === undefined)
459
+ args.push('0');
460
+ args.push(end);
461
+ }
462
+ const constType = props.type;
463
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
464
+ const exp = exportPrefix(node);
465
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).slice(${args.join(', ')});`];
466
+ }
467
+ export function generateFind(node) {
468
+ return generateArrayMethod(node, 'find');
469
+ }
470
+ export function generateSome(node) {
471
+ return generateArrayMethod(node, 'some');
472
+ }
473
+ export function generateEvery(node) {
474
+ return generateArrayMethod(node, 'every');
475
+ }
476
+ // ── Ground Layer: map ────────────────────────────────────────────────────
477
+ // `map name=names in=users expr="item.name"`
478
+ // → const names = (users).map((item) => item.name);
479
+ // Sibling to `each` (JSX form). Shape mirrors flatMap exactly — expr is the
480
+ // arrow body, not a predicate.
481
+ export function generateMap(node) {
482
+ const annotations = emitReasonAnnotations(node);
483
+ const props = propsOf(node);
484
+ const conf = props.confidence;
485
+ const todo = emitLowConfidenceTodo(node, conf);
486
+ const name = emitIdentifier(props.name, 'mapped', node);
487
+ const item = emitIdentifier(props.item || 'item', 'item', node);
488
+ const collection = unwrapExpr(props.in);
489
+ if (!collection)
490
+ throw new KernCodegenError("map node requires an 'in' prop", node);
491
+ const body = unwrapExpr(props.expr);
492
+ if (!body)
493
+ throw new KernCodegenError("map node requires an 'expr' prop", node);
494
+ const constType = props.type;
495
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
496
+ const exp = exportPrefix(node);
497
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).map((${item}) => ${body});`];
498
+ }
499
+ // ── Ground Layer: findIndex ──────────────────────────────────────────────
500
+ // `findIndex name=i in=users where="item.active"`
501
+ // → const i = (users).findIndex((item) => item.active);
502
+ // Predicate-shaped like find, but returns a number. Defaults the binding
503
+ // type suggestion to `number` in comments; author-supplied `type=` wins.
504
+ export function generateFindIndex(node) {
505
+ const annotations = emitReasonAnnotations(node);
506
+ const props = propsOf(node);
507
+ const conf = props.confidence;
508
+ const todo = emitLowConfidenceTodo(node, conf);
509
+ const name = emitIdentifier(props.name, 'index', node);
510
+ const item = emitIdentifier(props.item || 'item', 'item', node);
511
+ const collection = unwrapExpr(props.in);
512
+ if (!collection)
513
+ throw new KernCodegenError("findIndex node requires an 'in' prop", node);
514
+ const predicate = unwrapExpr(props.where);
515
+ if (!predicate)
516
+ throw new KernCodegenError("findIndex node requires a 'where' prop", node);
517
+ const constType = props.type;
518
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
519
+ const exp = exportPrefix(node);
520
+ return [
521
+ ...todo,
522
+ ...annotations,
523
+ `${exp}const ${name}${typeAnnotation} = (${collection}).findIndex((${item}) => ${predicate});`,
524
+ ];
525
+ }
526
+ // ── Ground Layer: sort ───────────────────────────────────────────────────
527
+ // `sort name=sorted in=items compare="a.age - b.age"`
528
+ // → const sorted = [...(items)].sort((a, b) => a.age - b.age);
529
+ // With no `compare`, emits `[...(items)].sort()` — JS lexicographic default.
530
+ // Immutable via spread so the source collection is not mutated.
531
+ export function generateSort(node) {
532
+ const annotations = emitReasonAnnotations(node);
533
+ const props = propsOf(node);
534
+ const conf = props.confidence;
535
+ const todo = emitLowConfidenceTodo(node, conf);
536
+ const name = emitIdentifier(props.name, 'sorted', node);
537
+ const a = emitIdentifier(props.a || 'a', 'a', node);
538
+ const b = emitIdentifier(props.b || 'b', 'b', node);
539
+ const collection = unwrapExpr(props.in);
540
+ if (!collection)
541
+ throw new KernCodegenError("sort node requires an 'in' prop", node);
542
+ const compare = unwrapExpr(props.compare);
543
+ const constType = props.type;
544
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
545
+ const exp = exportPrefix(node);
546
+ const call = compare ? `sort((${a}, ${b}) => ${compare})` : 'sort()';
547
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = [...(${collection})].${call};`];
548
+ }
549
+ // ── Ground Layer: reverse ────────────────────────────────────────────────
550
+ // `reverse name=reversed in=items` → const reversed = [...(items)].reverse();
551
+ // Immutable via spread; matches sort's shape for consistency.
552
+ export function generateReverse(node) {
553
+ const annotations = emitReasonAnnotations(node);
554
+ const props = propsOf(node);
555
+ const conf = props.confidence;
556
+ const todo = emitLowConfidenceTodo(node, conf);
557
+ const name = emitIdentifier(props.name, 'reversed', node);
558
+ const collection = unwrapExpr(props.in);
559
+ if (!collection)
560
+ throw new KernCodegenError("reverse node requires an 'in' prop", node);
561
+ const constType = props.type;
562
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
563
+ const exp = exportPrefix(node);
564
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = [...(${collection})].reverse();`];
565
+ }
566
+ // ── Ground Layer: flat ───────────────────────────────────────────────────
567
+ // `flat name=flattened in=nested depth=2` → const flattened = (nested).flat(2);
568
+ // `depth` omitted → bare `.flat()` (default depth 1).
569
+ export function generateFlat(node) {
570
+ const annotations = emitReasonAnnotations(node);
571
+ const props = propsOf(node);
572
+ const conf = props.confidence;
573
+ const todo = emitLowConfidenceTodo(node, conf);
574
+ const name = emitIdentifier(props.name, 'flattened', node);
575
+ const collection = unwrapExpr(props.in);
576
+ if (!collection)
577
+ throw new KernCodegenError("flat node requires an 'in' prop", node);
578
+ const depth = unwrapExpr(props.depth);
579
+ const constType = props.type;
580
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
581
+ const exp = exportPrefix(node);
582
+ const call = depth !== undefined ? `flat(${depth})` : 'flat()';
583
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).${call};`];
584
+ }
585
+ // ── Ground Layer: at ─────────────────────────────────────────────────────
586
+ // `at name=first in=items index=0` → const first = (items).at(0);
587
+ export function generateAt(node) {
588
+ const annotations = emitReasonAnnotations(node);
589
+ const props = propsOf(node);
590
+ const conf = props.confidence;
591
+ const todo = emitLowConfidenceTodo(node, conf);
592
+ const name = emitIdentifier(props.name, 'element', node);
593
+ const collection = unwrapExpr(props.in);
594
+ if (!collection)
595
+ throw new KernCodegenError("at node requires an 'in' prop", node);
596
+ const index = unwrapExpr(props.index);
597
+ if (index === undefined)
598
+ throw new KernCodegenError("at node requires an 'index' prop", node);
599
+ const constType = props.type;
600
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
601
+ const exp = exportPrefix(node);
602
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).at(${index});`];
603
+ }
604
+ // ── Ground Layer: join ───────────────────────────────────────────────────
605
+ // `join name=csv in=fields separator=","` → const csv = (fields).join(',');
606
+ // `separator` omitted → bare `.join()` (default "," per JS).
607
+ // The separator is emitted as a quoted string literal when plain, or as a
608
+ // raw expression when wrapped as `{{ expr }}`.
609
+ export function generateJoin(node) {
610
+ const annotations = emitReasonAnnotations(node);
611
+ const props = propsOf(node);
612
+ const conf = props.confidence;
613
+ const todo = emitLowConfidenceTodo(node, conf);
614
+ const name = emitIdentifier(props.name, 'joined', node);
615
+ const collection = unwrapExpr(props.in);
616
+ if (!collection)
617
+ throw new KernCodegenError("join node requires an 'in' prop", node);
618
+ const sepRaw = props.separator;
619
+ let sepArg = '';
620
+ if (sepRaw !== undefined && sepRaw !== null) {
621
+ if (typeof sepRaw === 'object' && sepRaw.__expr) {
622
+ sepArg = sepRaw.code;
623
+ }
624
+ else {
625
+ const s = String(sepRaw);
626
+ sepArg = `'${s.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`;
627
+ }
628
+ }
629
+ const constType = props.type;
630
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
631
+ const exp = exportPrefix(node);
632
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).join(${sepArg});`];
633
+ }
634
+ // ── Ground Layer: includes / indexOf / lastIndexOf ───────────────────────
635
+ // All three share shape: `<method> name=X in=Y value="..." [from=N]`.
636
+ // `value` is always a raw expression (no implicit quoting — authors write
637
+ // `value="'fatal'"` for a string literal, `value=target` for a variable).
638
+ function generateValueLookup(node, method) {
639
+ const annotations = emitReasonAnnotations(node);
640
+ const props = propsOf(node);
641
+ const conf = props.confidence;
642
+ const todo = emitLowConfidenceTodo(node, conf);
643
+ const fallback = method === 'includes' ? 'has' : 'idx';
644
+ const name = emitIdentifier(props.name, fallback, node);
645
+ const collection = unwrapExpr(props.in);
646
+ if (!collection)
647
+ throw new KernCodegenError(`${method} node requires an 'in' prop`, node);
648
+ const value = unwrapExpr(props.value);
649
+ if (value === undefined)
650
+ throw new KernCodegenError(`${method} node requires a 'value' prop`, node);
651
+ const from = unwrapExpr(props.from);
652
+ const args = from !== undefined ? `${value}, ${from}` : value;
653
+ const constType = props.type;
654
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
655
+ const exp = exportPrefix(node);
656
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).${method}(${args});`];
657
+ }
658
+ export function generateIncludes(node) {
659
+ return generateValueLookup(node, 'includes');
660
+ }
661
+ export function generateIndexOf(node) {
662
+ return generateValueLookup(node, 'indexOf');
663
+ }
664
+ export function generateLastIndexOf(node) {
665
+ return generateValueLookup(node, 'lastIndexOf');
666
+ }
667
+ // ── Ground Layer: concat ─────────────────────────────────────────────────
668
+ // `concat name=all in=items with="a, b"` → const all = (items).concat(a, b);
669
+ // `with` is a raw expression injected directly — supports one arg or
670
+ // comma-separated spread. For a single array arg, write `with="other"`.
671
+ export function generateConcat(node) {
672
+ const annotations = emitReasonAnnotations(node);
673
+ const props = propsOf(node);
674
+ const conf = props.confidence;
675
+ const todo = emitLowConfidenceTodo(node, conf);
676
+ const name = emitIdentifier(props.name, 'combined', node);
677
+ const collection = unwrapExpr(props.in);
678
+ if (!collection)
679
+ throw new KernCodegenError("concat node requires an 'in' prop", node);
680
+ const withArg = unwrapExpr(props.with);
681
+ if (!withArg)
682
+ throw new KernCodegenError("concat node requires a 'with' prop", node);
683
+ const constType = props.type;
684
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
685
+ const exp = exportPrefix(node);
686
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).concat(${withArg});`];
687
+ }
688
+ // ── Ground Layer: forEach ────────────────────────────────────────────────
689
+ // Statement primitive — no `name`, no `const` binding. Takes a handler child
690
+ // and emits `(in).forEach((item[, index]) => { handlerBody });`.
691
+ // Distinct from `each` (JSX composition) and `map` (value binding).
692
+ export function generateForEach(node) {
693
+ const annotations = emitReasonAnnotations(node);
694
+ const props = propsOf(node);
695
+ const conf = props.confidence;
696
+ const todo = emitLowConfidenceTodo(node, conf);
697
+ const item = emitIdentifier(props.item || 'item', 'item', node);
698
+ const indexName = props.index ? emitIdentifier(props.index, 'index', node) : undefined;
699
+ const collection = unwrapExpr(props.in);
700
+ if (!collection)
701
+ throw new KernCodegenError("forEach node requires an 'in' prop", node);
702
+ const handler = firstChild(node, 'handler');
703
+ if (!handler) {
704
+ throw new KernCodegenError('forEach node requires a `handler <<<>>>` child with the loop body', node);
705
+ }
706
+ const body = handlerCode(node);
707
+ const params = indexName ? `(${item}, ${indexName})` : `(${item})`;
708
+ const lines = [...todo, ...annotations];
709
+ lines.push(`(${collection}).forEach(${params} => {`);
710
+ for (const line of body.split('\n'))
711
+ lines.push(` ${line}`);
712
+ lines.push('});');
713
+ return lines;
714
+ }
715
+ // ── Ground Layer: compact ────────────────────────────────────────────────
716
+ // `compact name=truthy in=items` → `const truthy = (items).filter(Boolean);`
717
+ // Lodash-style named primitive for the very common `.filter(Boolean)` pattern.
718
+ // Agon scan: 36 call sites. Zero runtime cost vs the raw form; pure naming win.
719
+ export function generateCompact(node) {
720
+ const annotations = emitReasonAnnotations(node);
721
+ const props = propsOf(node);
722
+ const conf = props.confidence;
723
+ const todo = emitLowConfidenceTodo(node, conf);
724
+ const name = emitIdentifier(props.name, 'compacted', node);
725
+ const collection = unwrapExpr(props.in);
726
+ if (!collection)
727
+ throw new KernCodegenError("compact node requires an 'in' prop", node);
728
+ const constType = props.type;
729
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
730
+ const exp = exportPrefix(node);
731
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).filter(Boolean);`];
732
+ }
733
+ // ── Ground Layer: pluck ──────────────────────────────────────────────────
734
+ // `pluck name=names in=users prop=name`
735
+ // → const names = (users).map((item) => item.name);
736
+ // `prop` is a raw identifier path — `prop=user.profile.name` emits
737
+ // `item.user.profile.name`. Distinct from `map` because the author's intent is
738
+ // just "lift one field out of each item" and the shape is fixed.
739
+ export function generatePluck(node) {
740
+ const annotations = emitReasonAnnotations(node);
741
+ const props = propsOf(node);
742
+ const conf = props.confidence;
743
+ const todo = emitLowConfidenceTodo(node, conf);
744
+ const name = emitIdentifier(props.name, 'plucked', node);
745
+ const item = emitIdentifier(props.item || 'item', 'item', node);
746
+ const collection = unwrapExpr(props.in);
747
+ if (!collection)
748
+ throw new KernCodegenError("pluck node requires an 'in' prop", node);
749
+ const prop = unwrapExpr(props.prop);
750
+ if (!prop)
751
+ throw new KernCodegenError("pluck node requires a 'prop' prop", node);
752
+ const constType = props.type;
753
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
754
+ const exp = exportPrefix(node);
755
+ return [
756
+ ...todo,
757
+ ...annotations,
758
+ `${exp}const ${name}${typeAnnotation} = (${collection}).map((${item}) => ${item}.${prop});`,
759
+ ];
760
+ }
761
+ // ── Ground Layer: unique ─────────────────────────────────────────────────
762
+ // `unique name=distinct in=items` → `const distinct = [...new Set(items)];`
763
+ // Deduplicates by JS `Set` identity (triple-equals on primitives, reference
764
+ // equality on objects). For object arrays with a key selector, use
765
+ // `uniqueBy` (ships in the arrays-complete-pt2 PR).
766
+ export function generateUnique(node) {
767
+ const annotations = emitReasonAnnotations(node);
768
+ const props = propsOf(node);
769
+ const conf = props.confidence;
770
+ const todo = emitLowConfidenceTodo(node, conf);
771
+ const name = emitIdentifier(props.name, 'distinct', node);
772
+ const collection = unwrapExpr(props.in);
773
+ if (!collection)
774
+ throw new KernCodegenError("unique node requires an 'in' prop", node);
775
+ const constType = props.type;
776
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
777
+ const exp = exportPrefix(node);
778
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = [...new Set(${collection})];`];
779
+ }
780
+ // ── Ground Layer: uniqueBy ───────────────────────────────────────────────
781
+ // `uniqueBy name=distinct in=users by="item.id"`
782
+ // → const distinct = [...new Map((users).map((item) => [item.id, item])).values()];
783
+ export function generateUniqueBy(node) {
784
+ const annotations = emitReasonAnnotations(node);
785
+ const props = propsOf(node);
786
+ const conf = props.confidence;
787
+ const todo = emitLowConfidenceTodo(node, conf);
788
+ const name = emitIdentifier(props.name, 'distinct', node);
789
+ const item = emitIdentifier(props.item || 'item', 'item', node);
790
+ const collection = unwrapExpr(props.in);
791
+ if (!collection)
792
+ throw new KernCodegenError("uniqueBy node requires an 'in' prop", node);
793
+ const by = unwrapExpr(props.by);
794
+ if (!by)
795
+ throw new KernCodegenError("uniqueBy node requires a 'by' prop", node);
796
+ const constType = props.type;
797
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
798
+ const exp = exportPrefix(node);
799
+ // First-wins semantics to match Lodash `uniqBy`. Uses Set+filter rather
800
+ // than Map-constructor (which would keep the last occurrence).
801
+ return [
802
+ ...todo,
803
+ ...annotations,
804
+ `${exp}const ${name}${typeAnnotation} = ((__seen) => (${collection}).filter((${item}) => {`,
805
+ ` const __k = ${by};`,
806
+ ` if (__seen.has(__k)) return false;`,
807
+ ` __seen.add(__k);`,
808
+ ` return true;`,
809
+ `}))(new Set());`,
810
+ ];
811
+ }
812
+ // ── Ground Layer: groupBy ────────────────────────────────────────────────
813
+ // `groupBy name=byType in=items by="item.type"`
814
+ // → const byType = Object.groupBy(items, (item) => item.type);
815
+ // ES2024 Object.groupBy. Returns `Partial<Record<K, T[]>>`.
816
+ export function generateGroupBy(node) {
817
+ const annotations = emitReasonAnnotations(node);
818
+ const props = propsOf(node);
819
+ const conf = props.confidence;
820
+ const todo = emitLowConfidenceTodo(node, conf);
821
+ const name = emitIdentifier(props.name, 'grouped', node);
822
+ const item = emitIdentifier(props.item || 'item', 'item', node);
823
+ const collection = unwrapExpr(props.in);
824
+ if (!collection)
825
+ throw new KernCodegenError("groupBy node requires an 'in' prop", node);
826
+ const by = unwrapExpr(props.by);
827
+ if (!by)
828
+ throw new KernCodegenError("groupBy node requires a 'by' prop", node);
829
+ const constType = props.type || 'Record<string, unknown[]>';
830
+ const typeAnn = emitTypeAnnotation(constType, 'Record<string, unknown[]>', node);
831
+ const exp = exportPrefix(node);
832
+ return [
833
+ ...todo,
834
+ ...annotations,
835
+ `${exp}const ${name}: ${typeAnn} = (${collection}).reduce((acc, ${item}) => {`,
836
+ ` const __k = ${by};`,
837
+ ` (acc[__k] ??= []).push(${item});`,
838
+ ` return acc;`,
839
+ `}, Object.create(null) as ${typeAnn});`,
840
+ ];
841
+ }
842
+ // ── Ground Layer: partition ──────────────────────────────────────────────
843
+ // Two-output primitive: emits a destructured const. Dual-filter shape for
844
+ // clarity (two passes but readable).
845
+ export function generatePartition(node) {
846
+ const annotations = emitReasonAnnotations(node);
847
+ const props = propsOf(node);
848
+ const conf = props.confidence;
849
+ const todo = emitLowConfidenceTodo(node, conf);
850
+ const passName = emitIdentifier(props.pass, 'pass', node);
851
+ const failName = emitIdentifier(props.fail, 'fail', node);
852
+ const item = emitIdentifier(props.item || 'item', 'item', node);
853
+ const collection = unwrapExpr(props.in);
854
+ if (!collection)
855
+ throw new KernCodegenError("partition node requires an 'in' prop", node);
856
+ const predicate = unwrapExpr(props.where);
857
+ if (!predicate)
858
+ throw new KernCodegenError("partition node requires a 'where' prop", node);
859
+ const constType = props.type;
860
+ const elemType = constType ? emitTypeAnnotation(constType, 'unknown', node) : 'unknown';
861
+ const typeAnnotation = constType ? `: [${elemType}[], ${elemType}[]]` : '';
862
+ const exp = exportPrefix(node);
863
+ // Single-pass reduce so the collection and predicate each evaluate once
864
+ // per item — avoids double side effects from the dual-filter shape.
865
+ return [
866
+ ...todo,
867
+ ...annotations,
868
+ `${exp}const [${passName}, ${failName}]${typeAnnotation} = (${collection}).reduce<[${elemType}[], ${elemType}[]]>((acc, ${item}) => {`,
869
+ ` (${predicate} ? acc[0] : acc[1]).push(${item});`,
870
+ ` return acc;`,
871
+ `}, [[], []]);`,
872
+ ];
873
+ }
874
+ // ── Ground Layer: indexBy ────────────────────────────────────────────────
875
+ // `indexBy name=byId in=users by="item.id"`
876
+ // → const byId = Object.fromEntries((users).map((item) => [item.id, item]));
877
+ export function generateIndexBy(node) {
878
+ const annotations = emitReasonAnnotations(node);
879
+ const props = propsOf(node);
880
+ const conf = props.confidence;
881
+ const todo = emitLowConfidenceTodo(node, conf);
882
+ const name = emitIdentifier(props.name, 'indexed', node);
883
+ const item = emitIdentifier(props.item || 'item', 'item', node);
884
+ const collection = unwrapExpr(props.in);
885
+ if (!collection)
886
+ throw new KernCodegenError("indexBy node requires an 'in' prop", node);
887
+ const by = unwrapExpr(props.by);
888
+ if (!by)
889
+ throw new KernCodegenError("indexBy node requires a 'by' prop", node);
890
+ const constType = props.type;
891
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
892
+ const exp = exportPrefix(node);
893
+ return [
894
+ ...todo,
895
+ ...annotations,
896
+ `${exp}const ${name}${typeAnnotation} = Object.fromEntries((${collection}).map((${item}) => [${by}, ${item}]));`,
897
+ ];
898
+ }
899
+ // ── Ground Layer: countBy ────────────────────────────────────────────────
900
+ // Multi-line reduce — `Record<string, number>` default type.
901
+ export function generateCountBy(node) {
902
+ const annotations = emitReasonAnnotations(node);
903
+ const props = propsOf(node);
904
+ const conf = props.confidence;
905
+ const todo = emitLowConfidenceTodo(node, conf);
906
+ const name = emitIdentifier(props.name, 'counts', node);
907
+ const item = emitIdentifier(props.item || 'item', 'item', node);
908
+ const collection = unwrapExpr(props.in);
909
+ if (!collection)
910
+ throw new KernCodegenError("countBy node requires an 'in' prop", node);
911
+ const by = unwrapExpr(props.by);
912
+ if (!by)
913
+ throw new KernCodegenError("countBy node requires a 'by' prop", node);
914
+ const constType = props.type || 'Record<string, number>';
915
+ const typeAnn = emitTypeAnnotation(constType, 'Record<string, number>', node);
916
+ const exp = exportPrefix(node);
917
+ return [
918
+ ...todo,
919
+ ...annotations,
920
+ `${exp}const ${name}: ${typeAnn} = (${collection}).reduce((acc, ${item}) => {`,
921
+ ` const __k = ${by};`,
922
+ ` acc[__k] = (acc[__k] ?? 0) + 1;`,
923
+ ` return acc;`,
924
+ `}, Object.create(null) as ${typeAnn});`,
925
+ ];
926
+ }
927
+ // ── Ground Layer: chunk ──────────────────────────────────────────────────
928
+ // Fixed-size splitting.
929
+ export function generateChunk(node) {
930
+ const annotations = emitReasonAnnotations(node);
931
+ const props = propsOf(node);
932
+ const conf = props.confidence;
933
+ const todo = emitLowConfidenceTodo(node, conf);
934
+ const name = emitIdentifier(props.name, 'chunks', node);
935
+ const collection = unwrapExpr(props.in);
936
+ if (!collection)
937
+ throw new KernCodegenError("chunk node requires an 'in' prop", node);
938
+ const size = unwrapExpr(props.size);
939
+ if (!size)
940
+ throw new KernCodegenError("chunk node requires a 'size' prop", node);
941
+ const constType = props.type;
942
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
943
+ const exp = exportPrefix(node);
944
+ // IIFE so the collection and size expressions are evaluated once each —
945
+ // important if either is expensive or has side effects.
946
+ return [
947
+ ...todo,
948
+ ...annotations,
949
+ `${exp}const ${name}${typeAnnotation} = ((__src, __n) => Array.from({ length: Math.ceil(__src.length / __n) }, (_, i) => __src.slice(i * __n, (i + 1) * __n)))((${collection}), (${size}));`,
950
+ ];
951
+ }
952
+ // ── Ground Layer: zip ────────────────────────────────────────────────────
953
+ export function generateZip(node) {
954
+ const annotations = emitReasonAnnotations(node);
955
+ const props = propsOf(node);
956
+ const conf = props.confidence;
957
+ const todo = emitLowConfidenceTodo(node, conf);
958
+ const name = emitIdentifier(props.name, 'pairs', node);
959
+ const item = emitIdentifier(props.item || 'item', 'item', node);
960
+ const indexName = emitIdentifier(props.index || '__i', '__i', node);
961
+ const left = unwrapExpr(props.in);
962
+ if (!left)
963
+ throw new KernCodegenError("zip node requires an 'in' prop", node);
964
+ const right = unwrapExpr(props.with);
965
+ if (!right)
966
+ throw new KernCodegenError("zip node requires a 'with' prop", node);
967
+ const constType = props.type;
968
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
969
+ const exp = exportPrefix(node);
970
+ // Bind the right collection once via IIFE — without this, `with=getOther()`
971
+ // would call getOther() once per element inside the map callback.
972
+ return [
973
+ ...todo,
974
+ ...annotations,
975
+ `${exp}const ${name}${typeAnnotation} = ((__r) => (${left}).map((${item}, ${indexName}) => [${item}, __r[${indexName}]]))((${right}));`,
976
+ ];
977
+ }
978
+ // ── Ground Layer: range ──────────────────────────────────────────────────
979
+ export function generateRange(node) {
980
+ const annotations = emitReasonAnnotations(node);
981
+ const props = propsOf(node);
982
+ const conf = props.confidence;
983
+ const todo = emitLowConfidenceTodo(node, conf);
984
+ const name = emitIdentifier(props.name, 'range', node);
985
+ const end = unwrapExpr(props.end);
986
+ if (!end)
987
+ throw new KernCodegenError("range node requires an 'end' prop", node);
988
+ const start = unwrapExpr(props.start) ?? '0';
989
+ const constType = props.type;
990
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'number[]', node)}` : '';
991
+ const exp = exportPrefix(node);
992
+ return [
993
+ ...todo,
994
+ ...annotations,
995
+ `${exp}const ${name}${typeAnnotation} = Array.from({ length: (${end}) - (${start}) }, (_, i) => i + (${start}));`,
996
+ ];
997
+ }
998
+ // ── Ground Layer: take / drop ────────────────────────────────────────────
999
+ function generateTakeOrDrop(node, which) {
1000
+ const annotations = emitReasonAnnotations(node);
1001
+ const props = propsOf(node);
1002
+ const conf = props.confidence;
1003
+ const todo = emitLowConfidenceTodo(node, conf);
1004
+ const fallback = which === 'take' ? 'taken' : 'dropped';
1005
+ const name = emitIdentifier(props.name, fallback, node);
1006
+ const collection = unwrapExpr(props.in);
1007
+ if (!collection)
1008
+ throw new KernCodegenError(`${which} node requires an 'in' prop`, node);
1009
+ const n = unwrapExpr(props.n);
1010
+ if (!n)
1011
+ throw new KernCodegenError(`${which} node requires an 'n' prop`, node);
1012
+ const constType = props.type;
1013
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
1014
+ const exp = exportPrefix(node);
1015
+ const call = which === 'take' ? `slice(0, ${n})` : `slice(${n})`;
1016
+ return [...todo, ...annotations, `${exp}const ${name}${typeAnnotation} = (${collection}).${call};`];
1017
+ }
1018
+ export function generateTake(node) {
1019
+ return generateTakeOrDrop(node, 'take');
1020
+ }
1021
+ export function generateDrop(node) {
1022
+ return generateTakeOrDrop(node, 'drop');
1023
+ }
1024
+ // ── Ground Layer: min / max ──────────────────────────────────────────────
1025
+ // Reduce-based (not `Math.min(...arr)` / `Math.max(...arr)`) to avoid:
1026
+ // 1. Stack overflow on huge arrays (spread blows the arg count limit).
1027
+ // 2. `Math.min()` returning `Infinity` and `Math.max()` returning
1028
+ // `-Infinity` on empty arrays — we return `undefined` instead.
1029
+ function generateMathAgg(node, which) {
1030
+ const annotations = emitReasonAnnotations(node);
1031
+ const props = propsOf(node);
1032
+ const conf = props.confidence;
1033
+ const todo = emitLowConfidenceTodo(node, conf);
1034
+ const fallback = which === 'min' ? 'lowest' : 'highest';
1035
+ const name = emitIdentifier(props.name, fallback, node);
1036
+ const collection = unwrapExpr(props.in);
1037
+ if (!collection)
1038
+ throw new KernCodegenError(`${which} node requires an 'in' prop`, node);
1039
+ const constType = props.type;
1040
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'number', node)} | undefined` : '';
1041
+ const exp = exportPrefix(node);
1042
+ const op = which === 'min' ? '<' : '>';
1043
+ return [
1044
+ ...todo,
1045
+ ...annotations,
1046
+ `${exp}const ${name}${typeAnnotation} = ((__src) => __src.length === 0 ? undefined : __src.reduce((__a: number, __b: number) => __b ${op} __a ? __b : __a))((${collection}));`,
1047
+ ];
1048
+ }
1049
+ export function generateMin(node) {
1050
+ return generateMathAgg(node, 'min');
1051
+ }
1052
+ export function generateMax(node) {
1053
+ return generateMathAgg(node, 'max');
1054
+ }
1055
+ // ── Ground Layer: minBy / maxBy ──────────────────────────────────────────
1056
+ // Emits a closure `__key = (item) => by` so the author's `by` expression is
1057
+ // evaluated as an arrow body — no fragile regex over the raw expression
1058
+ // text that would corrupt string literals like `by="item.tags.includes('item')"`.
1059
+ // The collection is bound once, so expensive or side-effecting `in=` only
1060
+ // runs one time. Returns `undefined` for empty collections.
1061
+ function generateByReducer(node, which) {
1062
+ const annotations = emitReasonAnnotations(node);
1063
+ const props = propsOf(node);
1064
+ const conf = props.confidence;
1065
+ const todo = emitLowConfidenceTodo(node, conf);
1066
+ const fallback = which === 'min' ? 'youngest' : 'oldest';
1067
+ const name = emitIdentifier(props.name, fallback, node);
1068
+ const item = emitIdentifier(props.item || 'item', 'item', node);
1069
+ const collection = unwrapExpr(props.in);
1070
+ if (!collection)
1071
+ throw new KernCodegenError(`${which}By node requires an 'in' prop`, node);
1072
+ const by = unwrapExpr(props.by);
1073
+ if (!by)
1074
+ throw new KernCodegenError(`${which}By node requires a 'by' prop`, node);
1075
+ const constType = props.type;
1076
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)} | undefined` : '';
1077
+ const exp = exportPrefix(node);
1078
+ const op = which === 'min' ? '<' : '>';
1079
+ return [
1080
+ ...todo,
1081
+ ...annotations,
1082
+ `${exp}const ${name}${typeAnnotation} = ((__src) => {`,
1083
+ ` if (__src.length === 0) return undefined;`,
1084
+ ` const __key = (${item}: typeof __src[number]) => ${by};`,
1085
+ ` return __src.reduce((__best, __cur) => __key(__cur) ${op} __key(__best) ? __cur : __best);`,
1086
+ `})((${collection}));`,
1087
+ ];
1088
+ }
1089
+ export function generateMinBy(node) {
1090
+ return generateByReducer(node, 'min');
1091
+ }
1092
+ export function generateMaxBy(node) {
1093
+ return generateByReducer(node, 'max');
1094
+ }
1095
+ // ── Ground Layer: sum / avg / sumBy ──────────────────────────────────────
1096
+ export function generateSum(node) {
1097
+ const annotations = emitReasonAnnotations(node);
1098
+ const props = propsOf(node);
1099
+ const conf = props.confidence;
1100
+ const todo = emitLowConfidenceTodo(node, conf);
1101
+ const name = emitIdentifier(props.name, 'total', node);
1102
+ const collection = unwrapExpr(props.in);
1103
+ if (!collection)
1104
+ throw new KernCodegenError("sum node requires an 'in' prop", node);
1105
+ const constType = props.type || 'number';
1106
+ const typeAnnotation = `: ${emitTypeAnnotation(constType, 'number', node)}`;
1107
+ const exp = exportPrefix(node);
1108
+ return [
1109
+ ...todo,
1110
+ ...annotations,
1111
+ `${exp}const ${name}${typeAnnotation} = (${collection}).reduce((acc, n) => acc + n, 0);`,
1112
+ ];
1113
+ }
1114
+ export function generateAvg(node) {
1115
+ const annotations = emitReasonAnnotations(node);
1116
+ const props = propsOf(node);
1117
+ const conf = props.confidence;
1118
+ const todo = emitLowConfidenceTodo(node, conf);
1119
+ const name = emitIdentifier(props.name, 'mean', node);
1120
+ const collection = unwrapExpr(props.in);
1121
+ if (!collection)
1122
+ throw new KernCodegenError("avg node requires an 'in' prop", node);
1123
+ const constType = props.type || 'number';
1124
+ const typeAnnotation = `: ${emitTypeAnnotation(constType, 'number', node)}`;
1125
+ const exp = exportPrefix(node);
1126
+ // Returns `NaN` on empty input (matches Lodash `_.mean([])` and math
1127
+ // convention — "no data" signal rather than a fake 0). Bound via IIFE so
1128
+ // the `in=` expression is evaluated exactly once.
1129
+ return [
1130
+ ...todo,
1131
+ ...annotations,
1132
+ `${exp}const ${name}${typeAnnotation} = ((__src) => __src.length === 0 ? Number.NaN : __src.reduce((acc, n) => acc + n, 0) / __src.length)((${collection}));`,
1133
+ ];
1134
+ }
1135
+ export function generateSumBy(node) {
1136
+ const annotations = emitReasonAnnotations(node);
1137
+ const props = propsOf(node);
1138
+ const conf = props.confidence;
1139
+ const todo = emitLowConfidenceTodo(node, conf);
1140
+ const name = emitIdentifier(props.name, 'total', node);
1141
+ const item = emitIdentifier(props.item || 'item', 'item', node);
1142
+ const collection = unwrapExpr(props.in);
1143
+ if (!collection)
1144
+ throw new KernCodegenError("sumBy node requires an 'in' prop", node);
1145
+ const by = unwrapExpr(props.by);
1146
+ if (!by)
1147
+ throw new KernCodegenError("sumBy node requires a 'by' prop", node);
1148
+ const constType = props.type || 'number';
1149
+ const typeAnnotation = `: ${emitTypeAnnotation(constType, 'number', node)}`;
1150
+ const exp = exportPrefix(node);
1151
+ return [
1152
+ ...todo,
1153
+ ...annotations,
1154
+ `${exp}const ${name}${typeAnnotation} = (${collection}).reduce((acc, ${item}) => acc + (${by}), 0);`,
1155
+ ];
1156
+ }
1157
+ // ── Ground Layer: intersect ──────────────────────────────────────────────
1158
+ // Uses `new Set(right)` for O(N+M) lookup instead of the naive O(N×M)
1159
+ // `.filter(...includes...)` pair. The right collection is bound once so
1160
+ // expensive `with=` expressions don't re-run per element.
1161
+ export function generateIntersect(node) {
1162
+ const annotations = emitReasonAnnotations(node);
1163
+ const props = propsOf(node);
1164
+ const conf = props.confidence;
1165
+ const todo = emitLowConfidenceTodo(node, conf);
1166
+ const name = emitIdentifier(props.name, 'shared', node);
1167
+ const item = emitIdentifier(props.item || 'item', 'item', node);
1168
+ const left = unwrapExpr(props.in);
1169
+ if (!left)
1170
+ throw new KernCodegenError("intersect node requires an 'in' prop", node);
1171
+ const right = unwrapExpr(props.with);
1172
+ if (!right)
1173
+ throw new KernCodegenError("intersect node requires a 'with' prop", node);
1174
+ const constType = props.type;
1175
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
1176
+ const exp = exportPrefix(node);
1177
+ return [
1178
+ ...todo,
1179
+ ...annotations,
1180
+ `${exp}const ${name}${typeAnnotation} = ((__r) => (${left}).filter((${item}) => __r.has(${item})))(new Set((${right})));`,
1181
+ ];
1182
+ }
1183
+ // ── Ground Layer: findLast / findLastIndex (ES2023) ──────────────────────
1184
+ function generateFindLastPair(node, which) {
1185
+ const annotations = emitReasonAnnotations(node);
1186
+ const props = propsOf(node);
1187
+ const conf = props.confidence;
1188
+ const todo = emitLowConfidenceTodo(node, conf);
1189
+ const fallback = which === 'findLast' ? 'lastMatch' : 'lastIndex';
1190
+ const name = emitIdentifier(props.name, fallback, node);
1191
+ const item = emitIdentifier(props.item || 'item', 'item', node);
1192
+ const collection = unwrapExpr(props.in);
1193
+ if (!collection)
1194
+ throw new KernCodegenError(`${which} node requires an 'in' prop`, node);
1195
+ const predicate = unwrapExpr(props.where);
1196
+ if (!predicate)
1197
+ throw new KernCodegenError(`${which} node requires a 'where' prop`, node);
1198
+ const constType = props.type;
1199
+ const typeAnnotation = constType ? `: ${emitTypeAnnotation(constType, 'unknown', node)}` : '';
1200
+ const exp = exportPrefix(node);
1201
+ return [
1202
+ ...todo,
1203
+ ...annotations,
1204
+ `${exp}const ${name}${typeAnnotation} = (${collection}).${which}((${item}) => ${predicate});`,
1205
+ ];
1206
+ }
1207
+ export function generateFindLast(node) {
1208
+ return generateFindLastPair(node, 'findLast');
1209
+ }
1210
+ export function generateFindLastIndex(node) {
1211
+ return generateFindLastPair(node, 'findLastIndex');
1212
+ }
1213
+ // ── Ground Layer: async ──────────────────────────────────────────────────
1214
+ // `async name=loadUser` with a `handler` child runs its body inside an IIFE.
1215
+ // With an optional trailing `recover` child, delegates recovery to the
1216
+ // existing `recover`/`strategy` machinery (see `generateRecover` below) —
1217
+ // the emitted `<name>WithRecovery<T>` wrapper is invoked with the body as
1218
+ // its Promise-returning `fn`.
1219
+ //
1220
+ // Design: the `async` primitive reuses existing recover/strategy semantics
1221
+ // rather than inventing a new error-handling path. `derive` and `set` are
1222
+ // intentionally NOT made awaitable — their identity as direct bindings /
1223
+ // state updates stays pure.
1224
+ export function generateAsync(node) {
1225
+ const annotations = emitReasonAnnotations(node);
1226
+ const props = propsOf(node);
1227
+ const conf = props.confidence;
1228
+ const todo = emitLowConfidenceTodo(node, conf);
1229
+ const name = emitIdentifier(props.name, 'asyncBlock', node);
1230
+ const handler = firstChild(node, 'handler');
1231
+ if (!handler) {
1232
+ throw new KernCodegenError('async block requires a `handler <<<>>>` child with the body', node);
1233
+ }
1234
+ // handlerCode() takes the PARENT that has a handler child — pass `node`, not `handler`.
1235
+ const body = handlerCode(node);
1236
+ const recover = firstChild(node, 'recover');
1237
+ const lines = [...todo, ...annotations];
1238
+ if (!recover) {
1239
+ // Bare IIFE — fire-and-forget. Parent context decides whether to await it.
1240
+ lines.push(`(async () => {`);
1241
+ if (body) {
1242
+ for (const line of body.split('\n')) {
1243
+ lines.push(` ${line}`);
1244
+ }
1245
+ }
1246
+ lines.push(`})();`);
1247
+ return lines;
1248
+ }
1249
+ // With recovery: emit the reusable wrapper, then invoke it. The recover
1250
+ // node inherits the async block's name so generateRecover emits
1251
+ // `<name>WithRecovery<T>(...)` — one symbol that ties the two together.
1252
+ const namedRecover = {
1253
+ ...recover,
1254
+ props: { ...(recover.props || {}), name },
1255
+ };
1256
+ lines.push(...generateRecover(namedRecover));
1257
+ lines.push(`${name}WithRecovery(async () => {`);
1258
+ if (body) {
1259
+ for (const line of body.split('\n')) {
1260
+ lines.push(` ${line}`);
1261
+ }
1262
+ }
1263
+ lines.push(`});`);
1264
+ return lines;
1265
+ }
295
1266
  // ── Ground Layer: recover / strategy ─────────────────────────────────────
296
1267
  export function generateRecover(node) {
297
1268
  const annotations = emitReasonAnnotations(node);