@mmnto/totem 1.14.14 → 1.14.16

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 = {
@@ -1689,17 +1886,23 @@ describe('compileLesson unverified flag', () => {
1689
1886
  expect(result.rule.severity).toBe('warning');
1690
1887
  }
1691
1888
  });
1692
- it('leaves unverified absent when the lesson carries a non-empty Example Hit', async () => {
1693
- // Invariant #1: presence of Example Hit produces unverified: undefined.
1694
- // The serialized JSON omits the field — `canonicalStringify`
1695
- // (key-sorted JSON.stringify) writes nothing for undefined values so
1696
- // pre-#1480 manifest hashes stay stable.
1889
+ it('ships Pipeline 2 rules as unverified: true even when the lesson carries a non-empty Example Hit (ADR-089 zero-trust default, mmnto-ai/totem#1581)', async () => {
1890
+ // Pre-#1581 invariant: presence of Example Hit produced
1891
+ // unverified: undefined. Post-#1581: LLM-generated rules (Pipeline 2
1892
+ // and Pipeline 3) always ship unverified: true regardless of Example
1893
+ // Hit presence. The LLM cannot self-certify structural invariants;
1894
+ // Example Hit/Miss is an LLM-produced artifact of the compile process,
1895
+ // not a human sign-off. Activation requires `totem rule promote
1896
+ // <hash>` or the ADR-091 Stage 4 Codebase Verifier in 1.16.0.
1897
+ // Pipeline 1 (manual) keeps its pre-#1581 conditional semantics; see
1898
+ // the Pipeline 1 test below.
1697
1899
  const parseMock = vi.fn().mockReturnValue({
1698
1900
  compilable: true,
1699
1901
  pattern: 'console\\.log',
1700
1902
  message: 'No console.log',
1701
1903
  engine: 'regex',
1702
1904
  badExample: 'console.log(x)',
1905
+ goodExample: '// placeholder\n',
1703
1906
  });
1704
1907
  const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
1705
1908
  const deps = {
@@ -1711,7 +1914,7 @@ describe('compileLesson unverified flag', () => {
1711
1914
  const result = await compileLesson(lessonWithExampleHit, 'system prompt', deps);
1712
1915
  expect(result.status).toBe('compiled');
1713
1916
  if (result.status === 'compiled') {
1714
- expect(result.rule.unverified).toBeUndefined();
1917
+ expect(result.rule.unverified).toBe(true);
1715
1918
  }
1716
1919
  });
1717
1920
  it('treats an Example Hit that is whitespace-only as absent for the unverified flag', async () => {
@@ -1730,13 +1933,18 @@ describe('compileLesson unverified flag', () => {
1730
1933
  };
1731
1934
  const parseMock = vi.fn().mockReturnValue({
1732
1935
  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: '.*',
1936
+ // Pattern that matches both the trimmed-empty Example Hit (via
1937
+ // `^$`) AND the badExample "anything" (via `\banything\b`) so
1938
+ // verifyRuleExamples passes without interference. The goodExample
1939
+ // `// placeholder` is constructed to not satisfy either alternative
1940
+ // so the mmnto-ai/totem#1580 over-matching check also passes.
1941
+ // No trailing newline on goodExample so the line-split doesn't
1942
+ // produce an empty trailing line that would match `^$`.
1943
+ pattern: '^$|\\banything\\b',
1737
1944
  message: 'msg',
1738
1945
  engine: 'regex',
1739
1946
  badExample: 'anything',
1947
+ goodExample: '// placeholder',
1740
1948
  });
1741
1949
  const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
1742
1950
  const deps = {
@@ -1860,6 +2068,7 @@ describe('compileLesson trace events', () => {
1860
2068
  message: 'No console.log',
1861
2069
  engine: 'regex',
1862
2070
  badExample: 'console.log("x")',
2071
+ goodExample: '// placeholder\n',
1863
2072
  });
1864
2073
  const orchestratorMock = vi.fn().mockResolvedValue('attempt-1');
1865
2074
  const deps = {
@@ -1894,6 +2103,7 @@ describe('compileLesson trace events', () => {
1894
2103
  message: 'No console.log',
1895
2104
  engine: 'regex',
1896
2105
  badExample: 'zzz_only_matches_itself',
2106
+ goodExample: '// placeholder\n',
1897
2107
  });
1898
2108
  const orchestratorMock = vi.fn().mockResolvedValue('always-misses');
1899
2109
  const deps = {
@@ -1958,6 +2168,7 @@ describe('compileLesson trace events', () => {
1958
2168
  message: 'bad regex',
1959
2169
  engine: 'regex',
1960
2170
  badExample: 'anything',
2171
+ goodExample: '// placeholder\n',
1961
2172
  });
1962
2173
  const orchestratorMock = vi.fn().mockResolvedValue('invalid-regex-output');
1963
2174
  const deps = {
@@ -2007,6 +2218,7 @@ describe('compileLesson trace events', () => {
2007
2218
  message: 'No console.log',
2008
2219
  engine: 'regex',
2009
2220
  badExample: 'console.log("x")',
2221
+ goodExample: '// placeholder\n',
2010
2222
  });
2011
2223
  const orchestratorMock = vi.fn().mockResolvedValue('pipeline-3-response');
2012
2224
  const deps = {