@mmnto/cli 1.105.0 → 1.107.0

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.
Files changed (43) hide show
  1. package/dist/commands/doctor-qbd.d.ts +40 -0
  2. package/dist/commands/doctor-qbd.d.ts.map +1 -0
  3. package/dist/commands/doctor-qbd.js +156 -0
  4. package/dist/commands/doctor-qbd.js.map +1 -0
  5. package/dist/commands/doctor-qbd.test.d.ts +13 -0
  6. package/dist/commands/doctor-qbd.test.d.ts.map +1 -0
  7. package/dist/commands/doctor-qbd.test.js +548 -0
  8. package/dist/commands/doctor-qbd.test.js.map +1 -0
  9. package/dist/commands/mail-degraded-e4.test.d.ts +43 -0
  10. package/dist/commands/mail-degraded-e4.test.d.ts.map +1 -0
  11. package/dist/commands/mail-degraded-e4.test.js +317 -0
  12. package/dist/commands/mail-degraded-e4.test.js.map +1 -0
  13. package/dist/commands/orient.d.ts.map +1 -1
  14. package/dist/commands/orient.js +23 -0
  15. package/dist/commands/orient.js.map +1 -1
  16. package/dist/commands/qbd-seam.d.ts +77 -0
  17. package/dist/commands/qbd-seam.d.ts.map +1 -0
  18. package/dist/commands/qbd-seam.js +150 -0
  19. package/dist/commands/qbd-seam.js.map +1 -0
  20. package/dist/commands/qbd-seam.test.d.ts +16 -0
  21. package/dist/commands/qbd-seam.test.d.ts.map +1 -0
  22. package/dist/commands/qbd-seam.test.js +339 -0
  23. package/dist/commands/qbd-seam.test.js.map +1 -0
  24. package/dist/commands/review-fan.d.ts.map +1 -1
  25. package/dist/commands/review-fan.js +26 -0
  26. package/dist/commands/review-fan.js.map +1 -1
  27. package/dist/commands/run-compiled-rules.d.ts.map +1 -1
  28. package/dist/commands/run-compiled-rules.js +137 -5
  29. package/dist/commands/run-compiled-rules.js.map +1 -1
  30. package/dist/commands/run-compiled-rules.test.js +359 -1
  31. package/dist/commands/run-compiled-rules.test.js.map +1 -1
  32. package/dist/commands/search.d.ts.map +1 -1
  33. package/dist/commands/search.js +15 -0
  34. package/dist/commands/search.js.map +1 -1
  35. package/dist/commands/shield.d.ts.map +1 -1
  36. package/dist/commands/shield.js +34 -0
  37. package/dist/commands/shield.js.map +1 -1
  38. package/dist/commands/spec.d.ts.map +1 -1
  39. package/dist/commands/spec.js +16 -0
  40. package/dist/commands/spec.js.map +1 -1
  41. package/dist/index.js +8 -1
  42. package/dist/index.js.map +1 -1
  43. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as os from 'node:os';
3
3
  import * as path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
4
5
  import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
6
  import * as totem from '@mmnto/totem';
6
7
  import { readLedgerEvents, saveCompiledRules, TotemError, TotemParseError } from '@mmnto/totem';
@@ -1120,10 +1121,18 @@ describe('--ast-parse-mode lenient', () => {
1120
1121
  });
1121
1122
  // totem-context: helper has no OrExit suffix; the rule misclassifies test-fixture builders that return literal objects
1122
1123
  function makeAstRuleAndDiff() {
1124
+ // fileGlobs target a REGISTERED extension on purpose. These tests mock
1125
+ // `applyAstRulesToAdditions` wholesale — the fixture rule exists only to make
1126
+ // `astRules.length > 0` so the mocked pipeline is reached, and the rust
1127
+ // wording lives in the mocked error, not the rule. A `**/*.rs` glob would now
1128
+ // be condemned by the rule-load target-mismatch guard
1129
+ // (mmnto-ai/totem-strategy#971, Prop 309 Class 7) before the mock is ever
1130
+ // consulted, which would test the guard instead of the parse-mode routing
1131
+ // these cases exist for. The guard's own coverage lives in its describe block.
1123
1132
  const astRule = makeRule('', 'rust pattern', 'No unsafe', {
1124
1133
  engine: 'ast-grep',
1125
1134
  astGrepPattern: 'unsafe { $$$ }',
1126
- fileGlobs: ['**/*.rs'],
1135
+ fileGlobs: ['**/*.ts'],
1127
1136
  });
1128
1137
  saveCompiledRules(path.join(tmpDir, TOTEM_DIR, 'compiled-rules.json'), [astRule]);
1129
1138
  const diff = `diff --git a/src/lib.rs b/src/lib.rs
@@ -1535,4 +1544,353 @@ describe('corpus-bearing zero-rules hard-error (mmnto-ai/totem-strategy#971)', (
1535
1544
  })).rejects.toThrow(/enforcement disarmed/i);
1536
1545
  });
1537
1546
  });
1547
+ // ─── Target-mismatched rule hard-error (mmnto-ai/totem-strategy#971, Prop 309 Class 7) ──
1548
+ //
1549
+ // An AST rule whose declared `fileGlobs` ALL target extensions with no
1550
+ // registered Tree-sitter language can never execute. Worse, it does not fail
1551
+ // quietly: `rule-engine.ts` throws the moment a diff contains a file the rule's
1552
+ // globs claim, and that throw aborts the entire lint. Before this guard the
1553
+ // failure was diff-dependent — green on every run until the unlucky one. The
1554
+ // guard moves it to rule-LOAD time so it fires deterministically.
1555
+ describe('target-mismatched rule hard-error (mmnto-ai/totem-strategy#971, Prop 309 Class 7)', () => {
1556
+ let tmpDir;
1557
+ beforeEach(() => {
1558
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'totem-rcr-target-'));
1559
+ fs.mkdirSync(path.join(tmpDir, TOTEM_DIR), { recursive: true });
1560
+ });
1561
+ afterEach(() => {
1562
+ cleanTmpDir(tmpDir);
1563
+ });
1564
+ /** The observed specimen's shape: ast-grep scoped entirely to `.json` files. */
1565
+ function mismatchedAstGrepRule(overrides = {}) {
1566
+ return makeRule('', 'No inline secrets in agent config', 'No inline secrets', {
1567
+ lessonHash: 'target-mismatch-astgrep',
1568
+ engine: 'ast-grep',
1569
+ astGrepPattern: 'pair($KEY, $VALUE)',
1570
+ fileGlobs: ['**/.mcp.json', '**/.cursor/mcp.json'],
1571
+ ...overrides,
1572
+ });
1573
+ }
1574
+ const cleanDiff = () => makeDiff('src/app.ts', ' const x = 1;');
1575
+ // ── Induced failure ─────────────────────────────────
1576
+ it('hard-errors at load when every positive glob of an ast-grep rule targets an unregistered extension', async () => {
1577
+ writeRules(tmpDir, [mismatchedAstGrepRule()]);
1578
+ await expect(runCompiledRules({
1579
+ diff: cleanDiff(),
1580
+ cwd: tmpDir,
1581
+ totemDir: TOTEM_DIR,
1582
+ format: 'text',
1583
+ tag: 'Test',
1584
+ })).rejects.toThrow(/enforcement disarmed/i);
1585
+ });
1586
+ it('names the lessonHash, the engine, every glob, the unregistered extension, and the archive-or-fix remedy', async () => {
1587
+ writeRules(tmpDir, [mismatchedAstGrepRule()]);
1588
+ let message = '';
1589
+ let hint = '';
1590
+ try {
1591
+ await runCompiledRules({
1592
+ diff: cleanDiff(),
1593
+ cwd: tmpDir,
1594
+ totemDir: TOTEM_DIR,
1595
+ format: 'text',
1596
+ tag: 'Test',
1597
+ });
1598
+ }
1599
+ catch (err) {
1600
+ message = err instanceof Error ? err.message : String(err);
1601
+ hint = err instanceof TotemError ? err.recoveryHint : '';
1602
+ }
1603
+ // Everything the operator needs to act without opening the manifest.
1604
+ expect(message).toContain('target-mismatch-astgrep');
1605
+ expect(message).toContain('No inline secrets');
1606
+ expect(message).toContain("engine 'ast-grep'");
1607
+ expect(message).toContain('**/.mcp.json');
1608
+ expect(message).toContain('**/.cursor/mcp.json');
1609
+ expect(message).toContain('.json');
1610
+ // The detonation mechanism is named, not just the mismatch.
1611
+ expect(message).toContain('abort the entire lint');
1612
+ // Both remedies, plus the registry snapshot that makes "fix the globs" actionable.
1613
+ expect(hint).toContain("status: 'archived'");
1614
+ expect(hint).toContain('fileGlobs');
1615
+ expect(hint).toContain('.tsx');
1616
+ });
1617
+ it("hard-errors for the engine:'ast' variant — the abort seam unions both AST engines", async () => {
1618
+ // rule-engine.ts searches `allAstRules` (Tree-sitter 'ast' ∪ 'ast-grep') for
1619
+ // a rule claiming the unparseable file, so an 'ast' rule arms the identical
1620
+ // landmine and must be validated identically.
1621
+ writeRules(tmpDir, [
1622
+ makeRule('', 'No secrets in settings', 'No secrets in settings', {
1623
+ lessonHash: 'target-mismatch-ast',
1624
+ engine: 'ast',
1625
+ astQuery: '(pair) @p',
1626
+ fileGlobs: ['**/settings.json'],
1627
+ }),
1628
+ ]);
1629
+ let message = '';
1630
+ try {
1631
+ await runCompiledRules({
1632
+ diff: cleanDiff(),
1633
+ cwd: tmpDir,
1634
+ totemDir: TOTEM_DIR,
1635
+ format: 'text',
1636
+ tag: 'Test',
1637
+ });
1638
+ }
1639
+ catch (err) {
1640
+ message = err instanceof Error ? err.message : String(err);
1641
+ }
1642
+ expect(message).toMatch(/enforcement disarmed/i);
1643
+ expect(message).toContain('target-mismatch-ast');
1644
+ expect(message).toContain("engine 'ast'");
1645
+ });
1646
+ it("fires on a diff that touches NONE of the rule's globs — deterministic at load, not on first contact", async () => {
1647
+ // The whole point of the hardening. The diff is a .ts file; the broken rule
1648
+ // is scoped to .json. Pre-guard this run was green and the landmine stayed
1649
+ // armed for whichever future diff happened to touch a .json file.
1650
+ writeRules(tmpDir, [mismatchedAstGrepRule()]);
1651
+ await expect(runCompiledRules({
1652
+ diff: makeDiff('src/unrelated.ts', ' const unrelated = true;'),
1653
+ cwd: tmpDir,
1654
+ totemDir: TOTEM_DIR,
1655
+ format: 'json',
1656
+ tag: 'Test',
1657
+ })).rejects.toThrow(/enforcement disarmed/i);
1658
+ });
1659
+ it('reports every offender in one deterministic failure rather than stopping at the first', async () => {
1660
+ writeRules(tmpDir, [
1661
+ mismatchedAstGrepRule({ lessonHash: 'offender-one' }),
1662
+ mismatchedAstGrepRule({ lessonHash: 'offender-two', fileGlobs: ['config/*.yaml'] }),
1663
+ ]);
1664
+ let message = '';
1665
+ try {
1666
+ await runCompiledRules({
1667
+ diff: cleanDiff(),
1668
+ cwd: tmpDir,
1669
+ totemDir: TOTEM_DIR,
1670
+ format: 'text',
1671
+ tag: 'Test',
1672
+ });
1673
+ }
1674
+ catch (err) {
1675
+ message = err instanceof Error ? err.message : String(err);
1676
+ }
1677
+ expect(message).toContain('2 active AST rule(s)');
1678
+ expect(message).toContain('offender-one');
1679
+ expect(message).toContain('offender-two');
1680
+ expect(message).toContain('.yaml');
1681
+ });
1682
+ // ── Operator escape (mmnto-ai/totem#1982) ───────────
1683
+ it('degrades to a warning under --ast-parse-mode lenient instead of hard-erroring', async () => {
1684
+ // Without this, the guard would remove the ONLY escape for a repo whose
1685
+ // rules target a pack language it has not installed — `.rs` here, exactly
1686
+ // the shape @mmnto/pack-rust-architecture provides.
1687
+ writeRules(tmpDir, [
1688
+ mismatchedAstGrepRule({ lessonHash: 'pack-language-rule', fileGlobs: ['**/*.rs'] }),
1689
+ ]);
1690
+ const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
1691
+ try {
1692
+ const result = await runCompiledRules({
1693
+ diff: cleanDiff(),
1694
+ cwd: tmpDir,
1695
+ totemDir: TOTEM_DIR,
1696
+ format: 'json',
1697
+ tag: 'Test',
1698
+ astParseMode: 'lenient',
1699
+ });
1700
+ expect(result.rules).toHaveLength(1);
1701
+ expect(JSON.parse(result.output).pass).toBe(true);
1702
+ // The disclosure survives the exit-code escape: lenient covers the failure,
1703
+ // never the accounting.
1704
+ const messages = stderrSpy.mock.calls.map((c) => c.join(' '));
1705
+ const warning = messages.find((m) => m.includes('target-mismatched'));
1706
+ expect(warning).toBeDefined();
1707
+ expect(warning).toContain('pack-language-rule');
1708
+ expect(warning).toContain('.rs');
1709
+ expect(warning).toContain('totem sync --packs-only');
1710
+ }
1711
+ finally {
1712
+ stderrSpy.mockRestore();
1713
+ }
1714
+ });
1715
+ it('strict mode is the default — an omitted astParseMode still hard-errors', async () => {
1716
+ writeRules(tmpDir, [mismatchedAstGrepRule({ fileGlobs: ['**/*.rs'] })]);
1717
+ await expect(runCompiledRules({
1718
+ diff: cleanDiff(),
1719
+ cwd: tmpDir,
1720
+ totemDir: TOTEM_DIR,
1721
+ format: 'text',
1722
+ tag: 'Test',
1723
+ })).rejects.toThrow(/enforcement disarmed/i);
1724
+ });
1725
+ // ── Negative controls ───────────────────────────────
1726
+ it('control: the same rule with status:archived loads clean — archiving is the offered remedy', async () => {
1727
+ // Closes the loop on the offered fix: loadCompiledRules drops inert rules
1728
+ // before the guard ever sees them, so following the hint actually works.
1729
+ writeRules(tmpDir, [mismatchedAstGrepRule({ status: 'archived' })]);
1730
+ const stderrSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
1731
+ try {
1732
+ const result = await runCompiledRules({
1733
+ diff: cleanDiff(),
1734
+ cwd: tmpDir,
1735
+ totemDir: TOTEM_DIR,
1736
+ format: 'text',
1737
+ tag: 'Test',
1738
+ });
1739
+ expect(result.rules).toHaveLength(0);
1740
+ expect(result.violations).toHaveLength(0);
1741
+ }
1742
+ finally {
1743
+ stderrSpy.mockRestore();
1744
+ }
1745
+ });
1746
+ it('control: a globless ast-grep rule loads clean — the documented Lang.Tsx fallback stays permitted', async () => {
1747
+ // Unscoped pre-1.16 rules legitimately carry no fileGlobs, and
1748
+ // rule-engine.ts only throws for a rule with non-empty globs, so a globless
1749
+ // rule cannot arm the landmine.
1750
+ writeRules(tmpDir, [
1751
+ makeRule('', 'No foo log', 'No foo log', {
1752
+ engine: 'ast-grep',
1753
+ astGrepPattern: 'console.log("foo")',
1754
+ }),
1755
+ ]);
1756
+ const result = await runCompiledRules({
1757
+ diff: cleanDiff(),
1758
+ cwd: tmpDir,
1759
+ totemDir: TOTEM_DIR,
1760
+ format: 'json',
1761
+ tag: 'Test',
1762
+ });
1763
+ expect(result.rules).toHaveLength(1);
1764
+ expect(JSON.parse(result.output).pass).toBe(true);
1765
+ });
1766
+ it('control: one resolvable glob among unresolvable ones loads clean', async () => {
1767
+ // The rule CAN execute — the condemnation requires that not one positive
1768
+ // glob resolves.
1769
+ writeRules(tmpDir, [mismatchedAstGrepRule({ fileGlobs: ['**/*.json', '**/*.ts'] })]);
1770
+ const result = await runCompiledRules({
1771
+ diff: cleanDiff(),
1772
+ cwd: tmpDir,
1773
+ totemDir: TOTEM_DIR,
1774
+ format: 'json',
1775
+ tag: 'Test',
1776
+ });
1777
+ expect(result.rules).toHaveLength(1);
1778
+ expect(JSON.parse(result.output).pass).toBe(true);
1779
+ });
1780
+ it('control: extensionless positive globs (src/**) load clean — absence of evidence is not proof of mismatch', async () => {
1781
+ // TRAILING_EXT_RE extracts nothing from `src/**` or `**/*.{ts,json}`, but at
1782
+ // run time both match .ts files and dispatch fine. Condemning them would
1783
+ // hard-error legitimately broad rules.
1784
+ writeRules(tmpDir, [
1785
+ mismatchedAstGrepRule({ lessonHash: 'broad-one', fileGlobs: ['src/**'] }),
1786
+ mismatchedAstGrepRule({ lessonHash: 'broad-two', fileGlobs: ['**/*.{ts,json}'] }),
1787
+ mismatchedAstGrepRule({ lessonHash: 'broad-three', fileGlobs: ['packages/**/*'] }),
1788
+ ]);
1789
+ const result = await runCompiledRules({
1790
+ diff: cleanDiff(),
1791
+ cwd: tmpDir,
1792
+ totemDir: TOTEM_DIR,
1793
+ format: 'json',
1794
+ tag: 'Test',
1795
+ });
1796
+ expect(result.rules).toHaveLength(3);
1797
+ expect(JSON.parse(result.output).pass).toBe(true);
1798
+ });
1799
+ // ── .mts/.cts registration (mmnto-ai/totem#2513) ────
1800
+ it('control: an ast-grep rule scoped entirely to .mts/.cts loads clean — both are registered built-ins', async () => {
1801
+ // Before #2513 registered them, `.mts`/`.cts` were TOTAL mismatches and this
1802
+ // guard condemned an all-.mts rule at load. They resolve to the typescript
1803
+ // grammar now, so the guard must let them through.
1804
+ writeRules(tmpDir, [
1805
+ mismatchedAstGrepRule({ lessonHash: 'mts-only', fileGlobs: ['**/*.mts'] }),
1806
+ mismatchedAstGrepRule({ lessonHash: 'cts-only', fileGlobs: ['**/*.cts'] }),
1807
+ ]);
1808
+ const result = await runCompiledRules({
1809
+ diff: cleanDiff(),
1810
+ cwd: tmpDir,
1811
+ totemDir: TOTEM_DIR,
1812
+ format: 'json',
1813
+ tag: 'Test',
1814
+ });
1815
+ expect(result.rules).toHaveLength(2);
1816
+ expect(JSON.parse(result.output).pass).toBe(true);
1817
+ });
1818
+ it('still condemns a rule scoped entirely to a dead dummy extension — registration did not soften the guard', async () => {
1819
+ // The partial-permit semantics this guard ships with are unchanged by
1820
+ // #2513: an extension NO language covers is still a load-time hard error.
1821
+ writeRules(tmpDir, [
1822
+ mismatchedAstGrepRule({ lessonHash: 'dead-ext-only', fileGlobs: ['**/*.zzznotalang'] }),
1823
+ ]);
1824
+ let message = '';
1825
+ try {
1826
+ await runCompiledRules({
1827
+ diff: cleanDiff(),
1828
+ cwd: tmpDir,
1829
+ totemDir: TOTEM_DIR,
1830
+ format: 'text',
1831
+ tag: 'Test',
1832
+ });
1833
+ }
1834
+ catch (err) {
1835
+ message = err instanceof Error ? err.message : String(err);
1836
+ }
1837
+ expect(message).toMatch(/enforcement disarmed/i);
1838
+ expect(message).toContain('dead-ext-only');
1839
+ expect(message).toContain('.zzznotalang');
1840
+ });
1841
+ it('control: a REGEX rule scoped entirely to .json is untouched — the guard is AST-only', async () => {
1842
+ // Regex rules never dispatch through the Tree-sitter language registry, so
1843
+ // .json scoping is completely legitimate for them.
1844
+ writeRules(tmpDir, [
1845
+ makeRule('"apiKey"', 'No inline keys', 'No inline keys', {
1846
+ fileGlobs: ['**/.mcp.json'],
1847
+ }),
1848
+ ]);
1849
+ const result = await runCompiledRules({
1850
+ diff: cleanDiff(),
1851
+ cwd: tmpDir,
1852
+ totemDir: TOTEM_DIR,
1853
+ format: 'json',
1854
+ tag: 'Test',
1855
+ });
1856
+ expect(result.rules).toHaveLength(1);
1857
+ expect(JSON.parse(result.output).pass).toBe(true);
1858
+ });
1859
+ it('control: an all-negation glob list loads clean — it matches every file, including resolvable ones', async () => {
1860
+ writeRules(tmpDir, [mismatchedAstGrepRule({ fileGlobs: ['!**/*.json'] })]);
1861
+ const result = await runCompiledRules({
1862
+ diff: cleanDiff(),
1863
+ cwd: tmpDir,
1864
+ totemDir: TOTEM_DIR,
1865
+ format: 'json',
1866
+ tag: 'Test',
1867
+ });
1868
+ expect(result.rules).toHaveLength(1);
1869
+ expect(JSON.parse(result.output).pass).toBe(true);
1870
+ });
1871
+ // ── The real corpus ─────────────────────────────────
1872
+ it("the repository's own .totem/compiled-rules.json loads clean through the guard", async () => {
1873
+ // Regression anchor for the corpus itself: both known specimens are archived,
1874
+ // so the live manifest must pass. Copied into the tmp dir so the run's metric
1875
+ // and ledger writes never touch the real .totem.
1876
+ const realManifest = fileURLToPath(new URL('../../../../.totem/compiled-rules.json', import.meta.url));
1877
+ // A missing fixture would make every assertion below vacuous — fail loudly
1878
+ // instead of silently skipping.
1879
+ expect(fs.existsSync(realManifest)).toBe(true);
1880
+ fs.copyFileSync(realManifest, path.join(tmpDir, TOTEM_DIR, 'compiled-rules.json'));
1881
+ // A .txt path no compiled rule scopes to, so the run exercises rule LOAD
1882
+ // (where the guard lives) without dispatching AST rules against fixtures
1883
+ // that do not exist in the tmp dir.
1884
+ const result = await runCompiledRules({
1885
+ diff: makeDiff('docs/example.txt', 'A plain note.'),
1886
+ cwd: tmpDir,
1887
+ totemDir: TOTEM_DIR,
1888
+ format: 'json',
1889
+ tag: 'Test',
1890
+ });
1891
+ // Non-vacuity: the guard actually had globbed AST rules to inspect.
1892
+ const globbedAstRules = result.rules.filter((r) => (r.engine === 'ast-grep' || r.engine === 'ast') && r.fileGlobs && r.fileGlobs.length > 0);
1893
+ expect(globbedAstRules.length).toBeGreaterThan(0);
1894
+ });
1895
+ });
1538
1896
  //# sourceMappingURL=run-compiled-rules.test.js.map