@mmnto/totem 1.14.14 → 1.14.15

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.
@@ -488,6 +488,7 @@ describe('buildCompiledRule smoke gate (mmnto/totem#1408)', () => {
488
488
  engine: 'ast-grep',
489
489
  astGrepPattern: 'debugger',
490
490
  badExample: 'const x = 1;\n',
491
+ goodExample: '// placeholder\n',
491
492
  };
492
493
  const result = buildCompiledRule(parsed, lesson, existingByHash, {
493
494
  enforceSmokeGate: true,
@@ -517,6 +518,7 @@ describe('buildCompiledRule smoke gate (mmnto/totem#1408)', () => {
517
518
  engine: 'ast-grep',
518
519
  astGrepPattern: 'debugger',
519
520
  badExample: 'debugger;\n',
521
+ goodExample: '// placeholder\n',
520
522
  };
521
523
  const result = buildCompiledRule(parsed, lesson, existingByHash, {
522
524
  enforceSmokeGate: true,
@@ -531,6 +533,7 @@ describe('buildCompiledRule smoke gate (mmnto/totem#1408)', () => {
531
533
  engine: 'regex',
532
534
  pattern: 'console\\.log',
533
535
  badExample: 'console.log("debug")',
536
+ goodExample: '// placeholder\n',
534
537
  };
535
538
  const result = buildCompiledRule(parsed, lesson, existingByHash, {
536
539
  enforceSmokeGate: true,
@@ -549,9 +552,11 @@ describe('buildCompiledRule smoke gate (mmnto/totem#1408)', () => {
549
552
  const result = buildCompiledRule(parsed, lesson, existingByHash, {
550
553
  enforceSmokeGate: true,
551
554
  badExampleOverride: 'console.log("bad")',
555
+ goodExampleOverride: 'logger.info("good")',
552
556
  });
553
557
  expect(result.rule).not.toBeNull();
554
558
  expect(result.rule.badExample).toBe('console.log("bad")');
559
+ expect(result.rule.goodExample).toBe('logger.info("good")');
555
560
  });
556
561
  it('Pipeline 1 is unaffected (gate is opt-in via enforceSmokeGate)', () => {
557
562
  // Without the option flag, buildCompiledRule must behave exactly as before.
@@ -593,6 +598,7 @@ describe('compound rule smoke gate (mmnto-ai/totem#1409)', () => {
593
598
  },
594
599
  },
595
600
  badExample: 'for (let i = 0; i < 10; i++) {\n const inside = i * 2;\n}',
601
+ goodExample: '// placeholder\n',
596
602
  };
597
603
  const result = buildCompiledRule(parsed, compoundLesson, existingByHash, {
598
604
  enforceSmokeGate: true,
@@ -622,6 +628,7 @@ describe('compound rule smoke gate (mmnto-ai/totem#1409)', () => {
622
628
  },
623
629
  },
624
630
  badExample: 'for (let i = 0; i < 10; i++) {\n const inside = i * 2;\n}',
631
+ goodExample: '// placeholder\n',
625
632
  };
626
633
  const result = buildCompiledRule(parsed, compoundLesson, existingByHash, {
627
634
  enforceSmokeGate: true,
@@ -654,6 +661,183 @@ describe('compound rule smoke gate (mmnto-ai/totem#1409)', () => {
654
661
  expect(result.rejectReason).toContain('badExample');
655
662
  });
656
663
  });
664
+ // ─── over-matching check (mmnto-ai/totem#1580) ───────
665
+ describe('buildCompiledRule goodExample over-matching check', () => {
666
+ it('rejects a regex rule that fires on its goodExample', () => {
667
+ const parsed = {
668
+ compilable: true,
669
+ message: 'No console.log',
670
+ engine: 'regex',
671
+ pattern: 'console\\.log',
672
+ // badExample exercises the pattern (matches) — gate under-match passes.
673
+ badExample: 'console.log("debug")',
674
+ // goodExample also matches the pattern, which means the rule is
675
+ // over-broad and fires on known-correct code. The gate must reject.
676
+ goodExample: 'console.log("intentional system message")',
677
+ };
678
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
679
+ enforceSmokeGate: true,
680
+ });
681
+ expect(result.rule).toBeNull();
682
+ expect(result.rejectReason).toContain('smoke gate');
683
+ expect(result.rejectReason).toContain('matches goodExample');
684
+ expect(result.rejectReason).toContain('over-matching');
685
+ });
686
+ it('rejects an ast-grep rule that fires on its goodExample', () => {
687
+ const parsed = {
688
+ compilable: true,
689
+ message: 'No debugger',
690
+ engine: 'ast-grep',
691
+ astGrepPattern: 'debugger',
692
+ badExample: 'debugger;',
693
+ goodExample: 'debugger;\n// should have been removed before commit',
694
+ };
695
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
696
+ enforceSmokeGate: true,
697
+ });
698
+ expect(result.rule).toBeNull();
699
+ expect(result.rejectReason).toContain('matches goodExample');
700
+ });
701
+ it('accepts a regex rule whose pattern fires on badExample but not goodExample', () => {
702
+ const parsed = {
703
+ compilable: true,
704
+ message: 'No console.log',
705
+ engine: 'regex',
706
+ pattern: 'console\\.log',
707
+ badExample: 'console.log("debug")',
708
+ goodExample: 'logger.info("intentional")',
709
+ };
710
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
711
+ enforceSmokeGate: true,
712
+ });
713
+ expect(result.rule).not.toBeNull();
714
+ expect(result.rule.goodExample).toBe('logger.info("intentional")');
715
+ });
716
+ it('rejects a rule that is missing goodExample (required for Pipeline 2/3)', () => {
717
+ const parsed = {
718
+ compilable: true,
719
+ message: 'No console.log',
720
+ engine: 'regex',
721
+ pattern: 'console\\.log',
722
+ badExample: 'console.log("debug")',
723
+ // goodExample absent — caller did not supply it and schema
724
+ // layer was bypassed. Gate must still reject.
725
+ };
726
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
727
+ enforceSmokeGate: true,
728
+ });
729
+ expect(result.rule).toBeNull();
730
+ expect(result.rejectReason).toContain('smoke gate');
731
+ expect(result.rejectReason).toContain('missing goodExample');
732
+ });
733
+ it('honors goodExampleOverride so Pipeline 3 can reuse its Good snippet', () => {
734
+ const parsed = {
735
+ compilable: true,
736
+ message: 'No console.log',
737
+ engine: 'regex',
738
+ pattern: 'console\\.log',
739
+ badExample: 'console.log("bad")',
740
+ // No goodExample on parsed; caller supplies it via override.
741
+ };
742
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
743
+ enforceSmokeGate: true,
744
+ goodExampleOverride: 'logger.info("good")',
745
+ });
746
+ expect(result.rule).not.toBeNull();
747
+ expect(result.rule.goodExample).toBe('logger.info("good")');
748
+ });
749
+ it('persists goodExample on the CompiledRule when the gate accepts', () => {
750
+ const parsed = {
751
+ compilable: true,
752
+ message: 'No debugger',
753
+ engine: 'ast-grep',
754
+ astGrepPattern: 'debugger',
755
+ badExample: 'debugger;',
756
+ goodExample: 'const x = 1;',
757
+ };
758
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
759
+ enforceSmokeGate: true,
760
+ });
761
+ expect(result.rule).not.toBeNull();
762
+ expect(result.rule.goodExample).toBe('const x = 1;');
763
+ });
764
+ it('falls back to parsed.goodExample when goodExampleOverride is undefined', () => {
765
+ // Pins the Pipeline 3 contract: the call site passes `undefined`
766
+ // (not an empty string) when snippets.good is empty, so buildCompiledRule's
767
+ // `options.goodExampleOverride ?? parsed.goodExample` correctly resolves
768
+ // to the LLM-echoed value.
769
+ const parsed = {
770
+ compilable: true,
771
+ message: 'No console.log',
772
+ engine: 'regex',
773
+ pattern: 'console\\.log',
774
+ badExample: 'console.log("bad")',
775
+ goodExample: 'logger.info("from parsed")',
776
+ };
777
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
778
+ enforceSmokeGate: true,
779
+ goodExampleOverride: undefined,
780
+ });
781
+ expect(result.rule).not.toBeNull();
782
+ expect(result.rule.goodExample).toBe('logger.info("from parsed")');
783
+ });
784
+ it('rejects a whitespace-only goodExample with missing-goodexample (parity with schema refine)', () => {
785
+ // Shield flagged on mmnto-ai/totem#1591: `if (!effectiveGoodExample)`
786
+ // treats `' '` as truthy, and runSmokeGate's early-return on
787
+ // `trim().length === 0` then reports matched: false, so a
788
+ // whitespace-only goodExample would otherwise pass the gate with
789
+ // zero coverage. The `.trim().length > 0` guard closes the hole.
790
+ const parsed = {
791
+ compilable: true,
792
+ message: 'No console.log',
793
+ engine: 'regex',
794
+ pattern: 'console\\.log',
795
+ badExample: 'console.log("bad")',
796
+ goodExample: ' \t\n ',
797
+ };
798
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
799
+ enforceSmokeGate: true,
800
+ });
801
+ expect(result.rule).toBeNull();
802
+ expect(result.rejectReason).toContain('missing goodExample');
803
+ });
804
+ it('rejects a whitespace-only badExample with missing-badexample (symmetric guard)', () => {
805
+ const parsed = {
806
+ compilable: true,
807
+ message: 'No console.log',
808
+ engine: 'regex',
809
+ pattern: 'console\\.log',
810
+ badExample: ' \t\n ',
811
+ goodExample: 'logger.info("good")',
812
+ };
813
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
814
+ enforceSmokeGate: true,
815
+ });
816
+ expect(result.rule).toBeNull();
817
+ expect(result.rejectReason).toContain('missing badExample');
818
+ });
819
+ it('empty-string goodExampleOverride clobbers parsed.goodExample (the trap the Pipeline 3 guard exists to prevent)', () => {
820
+ // Nullish coalescing (`??`) treats `''` as defined, so passing an
821
+ // empty string override suppresses the parsed value. This test pins
822
+ // that behavior so the Pipeline 3 call site's .length guard in
823
+ // compile-lesson.ts (snippets.good.length > 0 ? ... : undefined)
824
+ // stays load-bearing.
825
+ const parsed = {
826
+ compilable: true,
827
+ message: 'No console.log',
828
+ engine: 'regex',
829
+ pattern: 'console\\.log',
830
+ badExample: 'console.log("bad")',
831
+ goodExample: 'logger.info("from parsed")',
832
+ };
833
+ const result = buildCompiledRule(parsed, lesson, existingByHash, {
834
+ enforceSmokeGate: true,
835
+ goodExampleOverride: '',
836
+ });
837
+ expect(result.rule).toBeNull();
838
+ expect(result.rejectReason).toContain('missing goodExample');
839
+ });
840
+ });
657
841
  // ─── buildManualRule ────────────────────────────────
658
842
  describe('buildManualRule', () => {
659
843
  it('returns null rule for lessons without manual patterns', () => {
@@ -750,6 +934,7 @@ describe('compileLesson', () => {
750
934
  // wants the rule to compile, so give the helper a known-good
751
935
  // snippet that matches the pattern.
752
936
  badExample: 'console.log("debug")',
937
+ goodExample: '// placeholder\n',
753
938
  }
754
939
  : null),
755
940
  runOrchestrator: vi.fn().mockResolvedValue(response),
@@ -803,6 +988,7 @@ describe('compileLesson', () => {
803
988
  message: 'No console.log',
804
989
  engine: 'regex',
805
990
  badExample: 'console.log("debug")',
991
+ goodExample: '// placeholder\n',
806
992
  }),
807
993
  runOrchestrator: vi.fn().mockResolvedValue('{"compilable": true}'),
808
994
  existingByHash: new Map(),
@@ -863,6 +1049,7 @@ describe('compileLesson', () => {
863
1049
  message: 'No console.log',
864
1050
  engine: 'regex',
865
1051
  badExample: 'console.log("debug")',
1052
+ goodExample: '// placeholder\n',
866
1053
  }),
867
1054
  runOrchestrator: vi.fn().mockResolvedValue('{"compilable": true}'),
868
1055
  existingByHash: new Map(),
@@ -1108,6 +1295,7 @@ describe('compileLesson with inline examples', () => {
1108
1295
  message: 'No console.log',
1109
1296
  engine: 'regex',
1110
1297
  badExample: 'console.log("debug")',
1298
+ goodExample: '// placeholder\n',
1111
1299
  }
1112
1300
  : null),
1113
1301
  runOrchestrator: vi.fn().mockResolvedValue(response),
@@ -1353,6 +1541,7 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
1353
1541
  message: 'No console.log',
1354
1542
  engine: 'regex',
1355
1543
  badExample: 'console.log("debug")',
1544
+ goodExample: '// placeholder\n',
1356
1545
  })
1357
1546
  .mockReturnValueOnce({
1358
1547
  compilable: true,
@@ -1360,6 +1549,7 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
1360
1549
  message: 'No console.log',
1361
1550
  engine: 'regex',
1362
1551
  badExample: 'console.log("debug")',
1552
+ goodExample: '// placeholder\n',
1363
1553
  });
1364
1554
  const orchestratorMock = vi
1365
1555
  .fn()
@@ -1394,6 +1584,7 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
1394
1584
  message: 'No console.log',
1395
1585
  engine: 'regex',
1396
1586
  badExample: 'console.log("debug")',
1587
+ goodExample: '// placeholder\n',
1397
1588
  });
1398
1589
  const orchestratorMock = vi.fn().mockResolvedValue('always-bad');
1399
1590
  const deps = {
@@ -1524,6 +1715,7 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
1524
1715
  message: 'No console.log',
1525
1716
  engine: 'regex',
1526
1717
  badExample: 'zzz_only_matches_itself',
1718
+ goodExample: '// placeholder\n',
1527
1719
  })
1528
1720
  .mockReturnValueOnce({
1529
1721
  compilable: true,
@@ -1531,6 +1723,7 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
1531
1723
  message: 'No console.log',
1532
1724
  engine: 'regex',
1533
1725
  badExample: 'console.log("x")',
1726
+ goodExample: '// placeholder\n',
1534
1727
  });
1535
1728
  const orchestratorMock = vi
1536
1729
  .fn()
@@ -1574,6 +1767,7 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
1574
1767
  message: 'No console.log',
1575
1768
  engine: 'regex',
1576
1769
  badExample: 'zzz_only_matches_itself',
1770
+ goodExample: '// placeholder\n',
1577
1771
  });
1578
1772
  const orchestratorMock = vi.fn().mockResolvedValue('always-misses');
1579
1773
  const deps = {
@@ -1602,6 +1796,7 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
1602
1796
  message: 'Invalid regex test',
1603
1797
  engine: 'regex',
1604
1798
  badExample: 'any string',
1799
+ goodExample: '// placeholder\n',
1605
1800
  });
1606
1801
  const orchestratorMock = vi.fn().mockResolvedValue('invalid-regex-output');
1607
1802
  const deps = {
@@ -1645,6 +1840,7 @@ describe('compileLesson unverified flag', () => {
1645
1840
  engine: 'regex',
1646
1841
  severity: 'error',
1647
1842
  badExample: 'console.log(x)',
1843
+ goodExample: '// placeholder\n',
1648
1844
  });
1649
1845
  const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
1650
1846
  const deps = {
@@ -1674,6 +1870,7 @@ describe('compileLesson unverified flag', () => {
1674
1870
  message: 'No console.log',
1675
1871
  engine: 'regex',
1676
1872
  badExample: 'console.log(x)',
1873
+ goodExample: '// placeholder\n',
1677
1874
  });
1678
1875
  const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
1679
1876
  const deps = {
@@ -1700,6 +1897,7 @@ describe('compileLesson unverified flag', () => {
1700
1897
  message: 'No console.log',
1701
1898
  engine: 'regex',
1702
1899
  badExample: 'console.log(x)',
1900
+ goodExample: '// placeholder\n',
1703
1901
  });
1704
1902
  const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
1705
1903
  const deps = {
@@ -1730,13 +1928,18 @@ describe('compileLesson unverified flag', () => {
1730
1928
  };
1731
1929
  const parseMock = vi.fn().mockReturnValue({
1732
1930
  compilable: true,
1733
- // Pattern that matches any string — including the trimmed-empty
1734
- // Example Hit so verifyRuleExamples passes and we can assert
1735
- // on the unverified flag without the verify branch interfering.
1736
- pattern: '.*',
1931
+ // Pattern that matches both the trimmed-empty Example Hit (via
1932
+ // `^$`) AND the badExample "anything" (via `\banything\b`) so
1933
+ // verifyRuleExamples passes without interference. The goodExample
1934
+ // `// placeholder` is constructed to not satisfy either alternative
1935
+ // so the mmnto-ai/totem#1580 over-matching check also passes.
1936
+ // No trailing newline on goodExample so the line-split doesn't
1937
+ // produce an empty trailing line that would match `^$`.
1938
+ pattern: '^$|\\banything\\b',
1737
1939
  message: 'msg',
1738
1940
  engine: 'regex',
1739
1941
  badExample: 'anything',
1942
+ goodExample: '// placeholder',
1740
1943
  });
1741
1944
  const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
1742
1945
  const deps = {
@@ -1860,6 +2063,7 @@ describe('compileLesson trace events', () => {
1860
2063
  message: 'No console.log',
1861
2064
  engine: 'regex',
1862
2065
  badExample: 'console.log("x")',
2066
+ goodExample: '// placeholder\n',
1863
2067
  });
1864
2068
  const orchestratorMock = vi.fn().mockResolvedValue('attempt-1');
1865
2069
  const deps = {
@@ -1894,6 +2098,7 @@ describe('compileLesson trace events', () => {
1894
2098
  message: 'No console.log',
1895
2099
  engine: 'regex',
1896
2100
  badExample: 'zzz_only_matches_itself',
2101
+ goodExample: '// placeholder\n',
1897
2102
  });
1898
2103
  const orchestratorMock = vi.fn().mockResolvedValue('always-misses');
1899
2104
  const deps = {
@@ -1958,6 +2163,7 @@ describe('compileLesson trace events', () => {
1958
2163
  message: 'bad regex',
1959
2164
  engine: 'regex',
1960
2165
  badExample: 'anything',
2166
+ goodExample: '// placeholder\n',
1961
2167
  });
1962
2168
  const orchestratorMock = vi.fn().mockResolvedValue('invalid-regex-output');
1963
2169
  const deps = {
@@ -2007,6 +2213,7 @@ describe('compileLesson trace events', () => {
2007
2213
  message: 'No console.log',
2008
2214
  engine: 'regex',
2009
2215
  badExample: 'console.log("x")',
2216
+ goodExample: '// placeholder\n',
2010
2217
  });
2011
2218
  const orchestratorMock = vi.fn().mockResolvedValue('pipeline-3-response');
2012
2219
  const deps = {