@mmnto/cli 1.122.0 → 1.123.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.
- package/dist/commands/config-drift.test.js +73 -1
- package/dist/commands/config-drift.test.js.map +1 -1
- package/dist/commands/init-templates.d.ts +6 -6
- package/dist/commands/init-templates.d.ts.map +1 -1
- package/dist/commands/init-templates.js +8 -7
- package/dist/commands/init-templates.js.map +1 -1
- package/dist/commands/init.test.js +18 -5
- package/dist/commands/init.test.js.map +1 -1
- package/dist/commands/install-hooks.d.ts.map +1 -1
- package/dist/commands/install-hooks.js +58 -0
- package/dist/commands/install-hooks.js.map +1 -1
- package/dist/commands/install-hooks.test.js +460 -5
- package/dist/commands/install-hooks.test.js.map +1 -1
- package/dist/commands/legs.d.ts +259 -0
- package/dist/commands/legs.d.ts.map +1 -0
- package/dist/commands/legs.js +623 -0
- package/dist/commands/legs.js.map +1 -0
- package/dist/commands/legs.test.d.ts +20 -0
- package/dist/commands/legs.test.d.ts.map +1 -0
- package/dist/commands/legs.test.js +1004 -0
- package/dist/commands/legs.test.js.map +1 -0
- package/dist/commands/pre-push-gate-matrix.test.js +14 -0
- package/dist/commands/pre-push-gate-matrix.test.js.map +1 -1
- package/dist/commands/review-fan.d.ts +40 -1
- package/dist/commands/review-fan.d.ts.map +1 -1
- package/dist/commands/review-fan.js +108 -3
- package/dist/commands/review-fan.js.map +1 -1
- package/dist/commands/review-fan.test.js +444 -6
- package/dist/commands/review-fan.test.js.map +1 -1
- package/dist/commands/shield-covariate.test.js +19 -2
- package/dist/commands/shield-covariate.test.js.map +1 -1
- package/dist/commands/shield-nonreview.test.js +140 -1
- package/dist/commands/shield-nonreview.test.js.map +1 -1
- package/dist/commands/shield.d.ts.map +1 -1
- package/dist/commands/shield.js +53 -1
- package/dist/commands/shield.js.map +1 -1
- package/dist/git.d.ts +19 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +13 -4
- package/dist/git.js.map +1 -1
- package/dist/index.js +40 -0
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
1
2
|
import { createHash } from 'node:crypto';
|
|
2
3
|
import * as fs from 'node:fs';
|
|
3
4
|
import * as os from 'node:os';
|
|
4
5
|
import * as path from 'node:path';
|
|
5
6
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
6
|
-
import { computeLineageKey, computeVerdictArtifactContentHash, deriveCacheEligible, deriveSettled, findLatestVerdictForLineage, listVerdictArtifacts, readLedgerEvents, renderCovariateLine, TotemConfigError, VERDICT_ARTIFACT_SCHEMA_VERSION, } from '@mmnto/totem';
|
|
7
|
+
import { computeLineageKey, computeVerdictArtifactContentHash, deriveCacheEligible, deriveSettled, findLatestVerdictForLineage, LEG_DEPOSIT_SCHEMA_VERSION, listVerdictArtifacts, readLedgerEvents, renderCovariateLine, saveLegDeposit, TotemConfigError, VERDICT_ARTIFACT_SCHEMA_VERSION, } from '@mmnto/totem';
|
|
7
8
|
import { EMPTY_SHARED } from '../exemptions/exemption-schema.js';
|
|
8
9
|
import { OrchestratorInvokeError } from '../orchestrators/orchestrator.js';
|
|
9
10
|
import { cleanTmpDir } from '../test-utils.js';
|
|
10
|
-
import { assembleVerdict, assertFanFlagsSupported, buildDiffScope, classifyRejectedLane, printCovariateLine, resolveLineage, runLane, runReviewFan, validateReviewLanes, } from './review-fan.js';
|
|
11
|
+
import { assembleVerdict, assertFanFlagsSupported, buildDiffScope, classifyRejectedLane, printCovariateLine, resolveLegFieldForHead, resolveLineage, runLane, runReviewFan, validateReviewLanes, } from './review-fan.js';
|
|
11
12
|
import { MAX_DIFF_CHARS } from './shield-templates.js';
|
|
12
13
|
// ─── Test fixtures ────────────────────────────────────────────────────────────
|
|
13
14
|
const hex = (seed) => createHash('sha256').update(seed).digest('hex');
|
|
@@ -1418,13 +1419,16 @@ describe('printCovariateLine (rev-5 item 4)', () => {
|
|
|
1418
1419
|
diffMeta: { source: 'branch-vs-base', base: 'main' },
|
|
1419
1420
|
totemDirAbs: tmpDir,
|
|
1420
1421
|
cwd: tmpDir,
|
|
1422
|
+
globs: [],
|
|
1421
1423
|
gitExec: git,
|
|
1422
1424
|
});
|
|
1423
|
-
// EXACTLY the core renderer's line, on stdout
|
|
1425
|
+
// EXACTLY the core renderer's line, on stdout, with the format-v1.2 leg
|
|
1426
|
+
// field appended — the v1 text is byte-for-byte unchanged BEFORE the field,
|
|
1427
|
+
// and an empty deposit store renders `leg: none` (mmnto-ai/totem#2698).
|
|
1424
1428
|
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
1425
|
-
expect(logSpy).toHaveBeenCalledWith(renderCovariateLine(verdict));
|
|
1429
|
+
expect(logSpy).toHaveBeenCalledWith(`${renderCovariateLine(verdict)} leg: none`);
|
|
1426
1430
|
const line = String(logSpy.mock.calls[0][0]);
|
|
1427
|
-
expect(line).toMatch(/^local-lane: [0-9a-f]{8} round=0 settled=true lanes=2\/2$/);
|
|
1431
|
+
expect(line).toMatch(/^local-lane: [0-9a-f]{8} round=0 settled=true lanes=2\/2 leg: none$/);
|
|
1428
1432
|
});
|
|
1429
1433
|
it('resolves the LATEST verdict for the lineage (round chain respected)', async () => {
|
|
1430
1434
|
const git = fakeGit('feature-cov', 'covbase');
|
|
@@ -1450,6 +1454,7 @@ describe('printCovariateLine (rev-5 item 4)', () => {
|
|
|
1450
1454
|
diffMeta: { source: 'branch-vs-base', base: 'main' },
|
|
1451
1455
|
totemDirAbs: tmpDir,
|
|
1452
1456
|
cwd: tmpDir,
|
|
1457
|
+
globs: [],
|
|
1453
1458
|
gitExec: git,
|
|
1454
1459
|
});
|
|
1455
1460
|
expect(String(logSpy.mock.calls[0][0])).toContain('round=1');
|
|
@@ -1461,6 +1466,7 @@ describe('printCovariateLine (rev-5 item 4)', () => {
|
|
|
1461
1466
|
diffMeta: { source: 'branch-vs-base', base: 'main' },
|
|
1462
1467
|
totemDirAbs: tmpDir,
|
|
1463
1468
|
cwd: tmpDir,
|
|
1469
|
+
globs: [],
|
|
1464
1470
|
gitExec: fakeGit('feature-none', 'nobase'),
|
|
1465
1471
|
})).resolves.toBeUndefined();
|
|
1466
1472
|
expect(logSpy).not.toHaveBeenCalled();
|
|
@@ -1470,10 +1476,442 @@ describe('printCovariateLine (rev-5 item 4)', () => {
|
|
|
1470
1476
|
it('no diff scope (diffMeta null) ⇒ loud sensor message, NO stdout line, clean return', async () => {
|
|
1471
1477
|
const errSpy = vi.mocked(console.error);
|
|
1472
1478
|
const logSpy = vi.mocked(console.log);
|
|
1473
|
-
await expect(printCovariateLine({ diffMeta: null, totemDirAbs: tmpDir, cwd: tmpDir })).resolves.toBeUndefined();
|
|
1479
|
+
await expect(printCovariateLine({ diffMeta: null, totemDirAbs: tmpDir, cwd: tmpDir, globs: [] })).resolves.toBeUndefined();
|
|
1474
1480
|
expect(logSpy).not.toHaveBeenCalled();
|
|
1475
1481
|
const out = errSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
1476
1482
|
expect(out).toMatch(/no diff detected/i);
|
|
1477
1483
|
});
|
|
1478
1484
|
});
|
|
1485
|
+
// ─── Covariate format v1.2 — the leg field (mmnto-ai/totem#2698) ──────────────
|
|
1486
|
+
describe('covariate format v1.2: the leg field (mmnto-ai/totem#2698)', () => {
|
|
1487
|
+
/** Restored after each case — see the cross-spawn note in `legs.test.ts`. */
|
|
1488
|
+
const realCwd = process.cwd();
|
|
1489
|
+
let tmpDir;
|
|
1490
|
+
beforeEach(() => {
|
|
1491
|
+
tmpDir = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'covariate-leg-')));
|
|
1492
|
+
// A REAL repo, because since mmnto-ai/totem#2698 fold 4 the leg field
|
|
1493
|
+
// derives its coverage inputs from HEAD's own branch scope on every review
|
|
1494
|
+
// scope. These cases declare no owed globs, so that scope resolves, owes
|
|
1495
|
+
// nothing, and coverage is vacuously satisfied — which is what keeps them
|
|
1496
|
+
// about the FIELD's shape rather than about the predicate.
|
|
1497
|
+
execFileSync('git', ['init', '-b', 'main'], { cwd: tmpDir, stdio: 'ignore' });
|
|
1498
|
+
execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '--allow-empty', '-m', 'base'], { cwd: tmpDir, stdio: 'ignore' });
|
|
1499
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
1500
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
1501
|
+
});
|
|
1502
|
+
afterEach(() => {
|
|
1503
|
+
vi.restoreAllMocks();
|
|
1504
|
+
process.chdir(realCwd);
|
|
1505
|
+
cleanTmpDir(tmpDir);
|
|
1506
|
+
});
|
|
1507
|
+
/** A 40-char lowercase hex commit name derived from a seed (the schema's shape). */
|
|
1508
|
+
const sha40 = (seed) => hex(seed).slice(0, 40);
|
|
1509
|
+
/**
|
|
1510
|
+
* A git runner over an IN-MEMORY commit graph. It answers the four lineage
|
|
1511
|
+
* probes `fakeGit` does, plus the three the leg adapter makes — and RECORDS
|
|
1512
|
+
* every ancestry probe, so a test can assert that an exact-match deposit
|
|
1513
|
+
* spends none of them.
|
|
1514
|
+
*/
|
|
1515
|
+
function legGit(head, graph = {}) {
|
|
1516
|
+
const ancestryCalls = [];
|
|
1517
|
+
const commits = graph.commits ?? [];
|
|
1518
|
+
const ancestorOf = graph.ancestorOf ?? {};
|
|
1519
|
+
const run = ((args) => {
|
|
1520
|
+
if (args[0] === 'symbolic-ref')
|
|
1521
|
+
return 'feature-leg';
|
|
1522
|
+
if (args[0] === 'rev-parse' && args[1] === '--show-toplevel')
|
|
1523
|
+
return REPO_TOPLEVEL;
|
|
1524
|
+
if (args[0] === 'rev-parse')
|
|
1525
|
+
return head;
|
|
1526
|
+
if (args[0] === 'cat-file') {
|
|
1527
|
+
ancestryCalls.push([...args]);
|
|
1528
|
+
const sha = String(args[2]).replace('^{commit}', '');
|
|
1529
|
+
if (!commits.includes(sha))
|
|
1530
|
+
throw new Error(`fatal: not a commit: ${sha}`);
|
|
1531
|
+
return '';
|
|
1532
|
+
}
|
|
1533
|
+
if (args[0] === 'merge-base' && args[1] === '--is-ancestor') {
|
|
1534
|
+
ancestryCalls.push([...args]);
|
|
1535
|
+
if (!(ancestorOf[String(args[2])] ?? []).includes(String(args[3]))) {
|
|
1536
|
+
throw new Error('not an ancestor');
|
|
1537
|
+
}
|
|
1538
|
+
return '';
|
|
1539
|
+
}
|
|
1540
|
+
if (args[0] === 'merge-base')
|
|
1541
|
+
return 'legbase';
|
|
1542
|
+
if (args[0] === 'rev-list') {
|
|
1543
|
+
ancestryCalls.push([...args]);
|
|
1544
|
+
return String(graph.distance ?? 0);
|
|
1545
|
+
}
|
|
1546
|
+
return '';
|
|
1547
|
+
});
|
|
1548
|
+
run.ancestryCalls = ancestryCalls;
|
|
1549
|
+
return run;
|
|
1550
|
+
}
|
|
1551
|
+
/** Write a deposit through the REAL core writer (never a hand-rolled file). */
|
|
1552
|
+
function deposit(diffSha, findings = [], folded = []) {
|
|
1553
|
+
saveLegDeposit(tmpDir, {
|
|
1554
|
+
schemaVersion: LEG_DEPOSIT_SCHEMA_VERSION,
|
|
1555
|
+
diffSha,
|
|
1556
|
+
readAt: '2026-09-03T04:00:00.000Z',
|
|
1557
|
+
findings,
|
|
1558
|
+
folded,
|
|
1559
|
+
verdict: 'read the diff; two blocking claims folded',
|
|
1560
|
+
});
|
|
1561
|
+
}
|
|
1562
|
+
const finding = (id, severity) => ({
|
|
1563
|
+
id,
|
|
1564
|
+
severity,
|
|
1565
|
+
file: 'packages/cli/src/commands/review-fan.ts',
|
|
1566
|
+
line: 1,
|
|
1567
|
+
claim: `${id} claim`,
|
|
1568
|
+
counterexample: '',
|
|
1569
|
+
});
|
|
1570
|
+
const twoLaneInvoker = () => mapInvoker({
|
|
1571
|
+
'anthropic:claude-a': completedInvocation({
|
|
1572
|
+
content: wrapVerdict([]),
|
|
1573
|
+
provider: 'anthropic',
|
|
1574
|
+
model: 'claude-a',
|
|
1575
|
+
seed: 'lega',
|
|
1576
|
+
}),
|
|
1577
|
+
'gemini:g': completedInvocation({
|
|
1578
|
+
content: wrapVerdict([]),
|
|
1579
|
+
provider: 'gemini',
|
|
1580
|
+
model: 'g',
|
|
1581
|
+
seed: 'legb',
|
|
1582
|
+
}),
|
|
1583
|
+
});
|
|
1584
|
+
it('appends the leg field to the VERDICT shape, leaving the v1 text byte-unchanged', async () => {
|
|
1585
|
+
const head = sha40('head-exact');
|
|
1586
|
+
const git = legGit(head);
|
|
1587
|
+
await runReviewFan(makeCtx(tmpDir, ['anthropic:claude-a', 'gemini:g'], twoLaneInvoker(), { gitExec: git }));
|
|
1588
|
+
const verdict = listVerdictArtifacts(tmpDir, noWarn)[0];
|
|
1589
|
+
// Two BLOCKING (both folded) + one MATERIAL: the ruled semantic is that a
|
|
1590
|
+
// folded finding counts in BOTH its severity bucket and `folded`, so
|
|
1591
|
+
// `blocking=2 folded=2` reads "all addressed".
|
|
1592
|
+
deposit(head, [finding('b1', 'BLOCKING'), finding('b2', 'BLOCKING'), finding('m1', 'MATERIAL')], ['b1', 'b2']);
|
|
1593
|
+
const logSpy = vi.mocked(console.log);
|
|
1594
|
+
logSpy.mockClear();
|
|
1595
|
+
await printCovariateLine({
|
|
1596
|
+
diffMeta: { source: 'branch-vs-base', base: 'main' },
|
|
1597
|
+
totemDirAbs: tmpDir,
|
|
1598
|
+
cwd: tmpDir,
|
|
1599
|
+
globs: [],
|
|
1600
|
+
gitExec: git,
|
|
1601
|
+
});
|
|
1602
|
+
const line = String(logSpy.mock.calls[0][0]);
|
|
1603
|
+
const field = `leg: ${head.slice(0, 8)} blocking=2 material=1 folded=2`;
|
|
1604
|
+
expect(line).toBe(`${renderCovariateLine(verdict)} ${field}`);
|
|
1605
|
+
// The v1 prefix is byte-identical: everything before the field is exactly
|
|
1606
|
+
// what v1/v1.1 consumers already parse, and the second token still
|
|
1607
|
+
// discriminates the shape.
|
|
1608
|
+
expect(line.slice(0, line.length - field.length - 1)).toBe(renderCovariateLine(verdict));
|
|
1609
|
+
expect(line.split(' ')[1]).toMatch(/^[0-9a-f]{8}$/);
|
|
1610
|
+
});
|
|
1611
|
+
it('prints the deposit-only head shape `local-lane: none <field>` when no verdict artifact exists', async () => {
|
|
1612
|
+
const head = sha40('head-deposit-only');
|
|
1613
|
+
const git = legGit(head);
|
|
1614
|
+
deposit(head, [finding('b1', 'BLOCKING')], []);
|
|
1615
|
+
const logSpy = vi.mocked(console.log);
|
|
1616
|
+
const errSpy = vi.mocked(console.error);
|
|
1617
|
+
await printCovariateLine({
|
|
1618
|
+
diffMeta: { source: 'branch-vs-base', base: 'main' },
|
|
1619
|
+
totemDirAbs: tmpDir,
|
|
1620
|
+
cwd: tmpDir,
|
|
1621
|
+
globs: [],
|
|
1622
|
+
gitExec: git,
|
|
1623
|
+
});
|
|
1624
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
1625
|
+
expect(String(logSpy.mock.calls[0][0])).toBe(`local-lane: none leg: ${head.slice(0, 8)} blocking=1 material=0 folded=0`);
|
|
1626
|
+
// The head shape REPLACES the no-verdict sensor: there IS a line to carry.
|
|
1627
|
+
const out = errSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
1628
|
+
expect(out).not.toMatch(/no verdict artifact recorded for the current lineage/i);
|
|
1629
|
+
});
|
|
1630
|
+
it('keeps the no-verdict sensor (and prints nothing) when neither a verdict nor a deposit resolves', async () => {
|
|
1631
|
+
const logSpy = vi.mocked(console.log);
|
|
1632
|
+
const errSpy = vi.mocked(console.error);
|
|
1633
|
+
await printCovariateLine({
|
|
1634
|
+
diffMeta: { source: 'branch-vs-base', base: 'main' },
|
|
1635
|
+
totemDirAbs: tmpDir,
|
|
1636
|
+
cwd: tmpDir,
|
|
1637
|
+
globs: [],
|
|
1638
|
+
gitExec: legGit(sha40('head-empty')),
|
|
1639
|
+
});
|
|
1640
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
1641
|
+
const out = errSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
1642
|
+
expect(out).toMatch(/no verdict artifact recorded for the current lineage/i);
|
|
1643
|
+
});
|
|
1644
|
+
it('discloses a corrupt deposit sibling as a Sensor line without changing the field', async () => {
|
|
1645
|
+
const head = sha40('head-corrupt-sibling');
|
|
1646
|
+
const git = legGit(head);
|
|
1647
|
+
deposit(head, [finding('m1', 'MATERIAL')], []);
|
|
1648
|
+
// A sibling that is not a deposit at all. Its NAME is a valid address, so
|
|
1649
|
+
// the loader must reject it on content, not on the filename.
|
|
1650
|
+
const sibling = `${sha40('corrupt-sibling')}.json`;
|
|
1651
|
+
fs.writeFileSync(path.join(tmpDir, 'artifacts', 'legs', sibling), '{ not json', 'utf-8');
|
|
1652
|
+
const logSpy = vi.mocked(console.log);
|
|
1653
|
+
const errSpy = vi.mocked(console.error);
|
|
1654
|
+
await printCovariateLine({
|
|
1655
|
+
diffMeta: { source: 'branch-vs-base', base: 'main' },
|
|
1656
|
+
totemDirAbs: tmpDir,
|
|
1657
|
+
cwd: tmpDir,
|
|
1658
|
+
globs: [],
|
|
1659
|
+
gitExec: git,
|
|
1660
|
+
});
|
|
1661
|
+
const out = errSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
1662
|
+
expect(out).toContain(`Sensor: ignoring corrupt leg deposit ${sibling}`);
|
|
1663
|
+
// Loud, and NEVER masking the valid sibling: the field is the good one's.
|
|
1664
|
+
expect(String(logSpy.mock.calls[0][0])).toBe(`local-lane: none leg: ${head.slice(0, 8)} blocking=0 material=1 folded=0`);
|
|
1665
|
+
});
|
|
1666
|
+
it('resolveLegFieldForHead spends NO ancestry probe on an exact match', async () => {
|
|
1667
|
+
const head = sha40('head-no-probe');
|
|
1668
|
+
const git = legGit(head);
|
|
1669
|
+
deposit(head, [], []);
|
|
1670
|
+
const resolution = await resolveLegFieldForHead(tmpDir, tmpDir, git);
|
|
1671
|
+
expect(resolution.field).toBe(`leg: ${head.slice(0, 8)} blocking=0 material=0 folded=0`);
|
|
1672
|
+
expect(resolution.winner?.rank).toBe('exact');
|
|
1673
|
+
expect(resolution.winner?.distance).toBe(0);
|
|
1674
|
+
// `cat-file`, `merge-base --is-ancestor` and `rev-list --count` are the
|
|
1675
|
+
// three probes the adapter can make; an exact match answers the gate's
|
|
1676
|
+
// question without spawning any of them.
|
|
1677
|
+
expect(git.ancestryCalls).toEqual([]);
|
|
1678
|
+
});
|
|
1679
|
+
it('resolveLegFieldForHead accepts an ANCESTOR deposit and reports its distance', async () => {
|
|
1680
|
+
const head = sha40('head-descendant');
|
|
1681
|
+
const base = sha40('base-ancestor');
|
|
1682
|
+
const git = legGit(head, { commits: [base], ancestorOf: { [base]: [head] }, distance: 7 });
|
|
1683
|
+
deposit(base, [finding('b1', 'BLOCKING')], ['b1']);
|
|
1684
|
+
const resolution = await resolveLegFieldForHead(tmpDir, tmpDir, git);
|
|
1685
|
+
expect(resolution.field).toBe(`leg: ${base.slice(0, 8)} blocking=1 material=0 folded=1`);
|
|
1686
|
+
expect(resolution.winner?.rank).toBe('ancestor');
|
|
1687
|
+
expect(resolution.winner?.distance).toBe(7);
|
|
1688
|
+
});
|
|
1689
|
+
it('resolveLegFieldForHead is `leg: none` for a deposit that is not ancestor-or-equal, and for an unresolvable HEAD', async () => {
|
|
1690
|
+
const head = sha40('head-disjoint');
|
|
1691
|
+
const other = sha40('other-branch');
|
|
1692
|
+
const git = legGit(head, { commits: [other] });
|
|
1693
|
+
deposit(other, [], []);
|
|
1694
|
+
expect((await resolveLegFieldForHead(tmpDir, tmpDir, git)).field).toBe('leg: none');
|
|
1695
|
+
// A HEAD that does not resolve at all (not a repo / unborn branch) is
|
|
1696
|
+
// `leg: none`, never a throw on a verb contracted to exit 0.
|
|
1697
|
+
const noHead = (args) => {
|
|
1698
|
+
if (args[0] === 'rev-parse')
|
|
1699
|
+
throw new Error('fatal: not a git repository');
|
|
1700
|
+
return '';
|
|
1701
|
+
};
|
|
1702
|
+
const resolution = await resolveLegFieldForHead(tmpDir, tmpDir, noHead);
|
|
1703
|
+
expect(resolution.field).toBe('leg: none');
|
|
1704
|
+
expect(resolution.winner).toBeUndefined();
|
|
1705
|
+
expect(resolution.corrupt).toEqual([]);
|
|
1706
|
+
});
|
|
1707
|
+
});
|
|
1708
|
+
// ─── Fold 3: the covariate resolves through the GATE's predicate ────────────
|
|
1709
|
+
//
|
|
1710
|
+
// The field must never name a deposit the gate would reject. Both sites route
|
|
1711
|
+
// through the same core resolution with the same coverage inputs, and a scope
|
|
1712
|
+
// that cannot supply them says so instead of implying a check that never ran
|
|
1713
|
+
// (mmnto-ai/totem#2698 fold 3).
|
|
1714
|
+
describe('the leg field honors coverage (mmnto-ai/totem#2698 fold 3)', () => {
|
|
1715
|
+
let tmpDir;
|
|
1716
|
+
beforeEach(() => {
|
|
1717
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'legfield-coverage-'));
|
|
1718
|
+
vi.restoreAllMocks();
|
|
1719
|
+
vi.spyOn(console, 'error').mockImplementation(() => { });
|
|
1720
|
+
vi.spyOn(console, 'log').mockImplementation(() => { });
|
|
1721
|
+
});
|
|
1722
|
+
afterEach(() => {
|
|
1723
|
+
vi.restoreAllMocks();
|
|
1724
|
+
cleanTmpDir(tmpDir);
|
|
1725
|
+
});
|
|
1726
|
+
const HEAD_SHA = 'a'.repeat(40);
|
|
1727
|
+
const BASE_SHA = 'b'.repeat(40);
|
|
1728
|
+
/** A runner that answers HEAD, ancestry, and a candidate's own branch diff. */
|
|
1729
|
+
function coverageGit(reach) {
|
|
1730
|
+
return (args) => {
|
|
1731
|
+
if (args[0] === 'rev-parse' && args[1] === 'HEAD')
|
|
1732
|
+
return HEAD_SHA;
|
|
1733
|
+
if (args[0] === 'cat-file')
|
|
1734
|
+
return '';
|
|
1735
|
+
if (args[0] === 'merge-base')
|
|
1736
|
+
return '';
|
|
1737
|
+
if (args[0] === 'rev-list')
|
|
1738
|
+
return '1';
|
|
1739
|
+
if (args[0] === 'diff' && args[1] === '--name-only')
|
|
1740
|
+
return reach.join('\n');
|
|
1741
|
+
return '';
|
|
1742
|
+
};
|
|
1743
|
+
}
|
|
1744
|
+
function depositAt(sha) {
|
|
1745
|
+
saveLegDeposit(tmpDir, {
|
|
1746
|
+
schemaVersion: LEG_DEPOSIT_SCHEMA_VERSION,
|
|
1747
|
+
diffSha: sha,
|
|
1748
|
+
readAt: '2026-09-03T06:00:00.000Z',
|
|
1749
|
+
findings: [],
|
|
1750
|
+
folded: [],
|
|
1751
|
+
verdict: 'read the diff at this head',
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
it('a deposit the gate would reject as no-coverage resolves to `leg: none`', async () => {
|
|
1755
|
+
depositAt(BASE_SHA);
|
|
1756
|
+
const resolution = await resolveLegFieldForHead(tmpDir, tmpDir,
|
|
1757
|
+
// Its own branch diff touched nothing this push owes — the merge-base shape.
|
|
1758
|
+
coverageGit(['src/unrelated.ts']), { query: { base: 'main', owedFiles: ['docs/wiki/a.md'] } });
|
|
1759
|
+
expect(resolution.field).toBe('leg: none');
|
|
1760
|
+
expect(resolution.winner).toBeUndefined();
|
|
1761
|
+
});
|
|
1762
|
+
it('the same deposit names itself once it covers an owed path', async () => {
|
|
1763
|
+
depositAt(BASE_SHA);
|
|
1764
|
+
const resolution = await resolveLegFieldForHead(tmpDir, tmpDir, coverageGit(['docs/wiki/a.md']), { query: { base: 'main', owedFiles: ['docs/wiki/a.md'] } });
|
|
1765
|
+
expect(resolution.field).toBe(`leg: ${BASE_SHA.slice(0, 8)} blocking=0 material=0 folded=0`);
|
|
1766
|
+
});
|
|
1767
|
+
it('WITHOUT a coverage query the field is ancestry-only — the pre-fold behavior', async () => {
|
|
1768
|
+
depositAt(BASE_SHA);
|
|
1769
|
+
const resolution = await resolveLegFieldForHead(tmpDir, tmpDir, coverageGit(['src/other.ts']));
|
|
1770
|
+
expect(resolution.field).toBe(`leg: ${BASE_SHA.slice(0, 8)} blocking=0 material=0 folded=0`);
|
|
1771
|
+
});
|
|
1772
|
+
it('an UNDERIVABLE coverage answer is `leg: none`, never an ancestry-only name', async () => {
|
|
1773
|
+
// The fold-4 rule at the seam: an ancestor deposit exists and would have
|
|
1774
|
+
// been named on ancestry alone. Once coverage was ASKED FOR and could not
|
|
1775
|
+
// be derived, the honest answer is none — crediting it would be the field
|
|
1776
|
+
// disagreeing with the gate, which rejects it.
|
|
1777
|
+
depositAt(BASE_SHA);
|
|
1778
|
+
const resolution = await resolveLegFieldForHead(tmpDir, tmpDir, coverageGit(['docs/wiki/a.md']), {
|
|
1779
|
+
reason: 'coverage was not derivable — HEAD has no branch base',
|
|
1780
|
+
});
|
|
1781
|
+
expect(resolution.field).toBe('leg: none');
|
|
1782
|
+
expect(resolution.winner).toBeUndefined();
|
|
1783
|
+
});
|
|
1784
|
+
it('the no-base arm STILL discloses a corrupt deposit (fold 5, F7)', async () => {
|
|
1785
|
+
// The store is a fact about itself: a file that is not a deposit is worth
|
|
1786
|
+
// saying whatever coverage concludes. The first version of this arm
|
|
1787
|
+
// returned early with an empty corrupt list and silenced exactly the sensor
|
|
1788
|
+
// a hand-edited deposit needs.
|
|
1789
|
+
fs.mkdirSync(path.join(tmpDir, 'artifacts', 'legs'), { recursive: true });
|
|
1790
|
+
fs.writeFileSync(path.join(tmpDir, 'artifacts', 'legs', `${'e'.repeat(40)}.json`), 'not a deposit at all');
|
|
1791
|
+
const resolution = await resolveLegFieldForHead(tmpDir, tmpDir, coverageGit([]), {
|
|
1792
|
+
reason: 'coverage was not derivable — HEAD has no branch base',
|
|
1793
|
+
});
|
|
1794
|
+
expect(resolution.field).toBe('leg: none');
|
|
1795
|
+
expect(resolution.winner).toBeUndefined();
|
|
1796
|
+
expect(resolution.corrupt.map((entry) => entry.file)).toEqual([`${'e'.repeat(40)}.json`]);
|
|
1797
|
+
});
|
|
1798
|
+
it('HEAD with no branch base: the covariate prints ONE sensor and no leg name', async () => {
|
|
1799
|
+
// `tmpDir` is not a git repo, so HEAD's branch scope will not resolve.
|
|
1800
|
+
const errSpy = vi.mocked(console.error);
|
|
1801
|
+
depositAt(BASE_SHA);
|
|
1802
|
+
await expect(printCovariateLine({
|
|
1803
|
+
diffMeta: { source: 'staged' },
|
|
1804
|
+
totemDirAbs: tmpDir,
|
|
1805
|
+
cwd: tmpDir,
|
|
1806
|
+
globs: ['docs/**'],
|
|
1807
|
+
gitExec: coverageGit(['docs/wiki/a.md']),
|
|
1808
|
+
})).resolves.toBeUndefined();
|
|
1809
|
+
const out = errSpy.mock.calls.map((c) => c.join(' ')).join('\n');
|
|
1810
|
+
expect(out).toContain('coverage was not derivable — HEAD has no branch base');
|
|
1811
|
+
// Article-free, and it never claims an ancestry-only fallback happened.
|
|
1812
|
+
expect(out).not.toContain('for a staged scope');
|
|
1813
|
+
expect(out).not.toContain('ancestry alone');
|
|
1814
|
+
expect(vi.mocked(console.log)).not.toHaveBeenCalled();
|
|
1815
|
+
});
|
|
1816
|
+
// The reproduction, end to end: a REAL repo whose review scope is the default
|
|
1817
|
+
// dirty-tree one, and a deposit at the merge base. Before fold 4 the field
|
|
1818
|
+
// resolved on the review's scope, found no branch base, fell back to ancestry
|
|
1819
|
+
// and NAMED that deposit — the gate rejects it.
|
|
1820
|
+
describe('the field derives coverage from HEAD, whatever the review scope is', () => {
|
|
1821
|
+
/** Built, never authored as an escape (the banked decode trap). */
|
|
1822
|
+
const NL = String.fromCharCode(10);
|
|
1823
|
+
let repoDir;
|
|
1824
|
+
let baseSha;
|
|
1825
|
+
let headSha;
|
|
1826
|
+
const realCwd = process.cwd();
|
|
1827
|
+
beforeEach(() => {
|
|
1828
|
+
repoDir = fs.realpathSync.native(fs.mkdtempSync(path.join(os.tmpdir(), 'legfield-headscope-')));
|
|
1829
|
+
const git = (...args) => execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', '-c', 'commit.gpgsign=false', ...args], { cwd: repoDir, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
1830
|
+
git('init', '-b', 'main');
|
|
1831
|
+
fs.writeFileSync(path.join(repoDir, 'src.ts'), 'export const a = 1;');
|
|
1832
|
+
git('add', '.');
|
|
1833
|
+
git('commit', '-m', 'base');
|
|
1834
|
+
baseSha = git('rev-parse', 'HEAD');
|
|
1835
|
+
git('checkout', '-b', 'feat/owed');
|
|
1836
|
+
fs.mkdirSync(path.join(repoDir, 'docs'), { recursive: true });
|
|
1837
|
+
fs.writeFileSync(path.join(repoDir, 'docs', 'page.md'), '# a page');
|
|
1838
|
+
git('add', 'docs/page.md');
|
|
1839
|
+
git('commit', '-m', 'a docs change');
|
|
1840
|
+
headSha = git('rev-parse', 'HEAD');
|
|
1841
|
+
});
|
|
1842
|
+
afterEach(() => {
|
|
1843
|
+
process.chdir(realCwd);
|
|
1844
|
+
cleanTmpDir(repoDir);
|
|
1845
|
+
});
|
|
1846
|
+
function depositIn(dir, sha) {
|
|
1847
|
+
saveLegDeposit(path.join(dir, '.totem'), {
|
|
1848
|
+
schemaVersion: LEG_DEPOSIT_SCHEMA_VERSION,
|
|
1849
|
+
diffSha: sha,
|
|
1850
|
+
readAt: '2026-09-03T06:00:00.000Z',
|
|
1851
|
+
findings: [],
|
|
1852
|
+
folded: [],
|
|
1853
|
+
verdict: 'read the diff at this head',
|
|
1854
|
+
});
|
|
1855
|
+
}
|
|
1856
|
+
async function covariateOn(source) {
|
|
1857
|
+
vi.mocked(console.log).mockClear();
|
|
1858
|
+
await printCovariateLine({
|
|
1859
|
+
diffMeta: { source },
|
|
1860
|
+
totemDirAbs: path.join(repoDir, '.totem'),
|
|
1861
|
+
cwd: repoDir,
|
|
1862
|
+
globs: ['docs/**'],
|
|
1863
|
+
});
|
|
1864
|
+
return vi.mocked(console.log).mock.calls.map((c) => c.join(' '));
|
|
1865
|
+
}
|
|
1866
|
+
it('a MERGE-BASE deposit is `leg: none` on a dirty-tree review scope', async () => {
|
|
1867
|
+
depositIn(repoDir, baseSha);
|
|
1868
|
+
const printed = await covariateOn('uncommitted');
|
|
1869
|
+
// Nothing printed: with no verdict artifact AND no leg, there is no line
|
|
1870
|
+
// — where before the fold this printed `local-lane: none leg: <base8>`.
|
|
1871
|
+
expect(printed.join(' ')).not.toContain(baseSha.slice(0, 8));
|
|
1872
|
+
});
|
|
1873
|
+
it('a LARGE branch diff narrates nothing under [Legs] (fold 5, F2)', async () => {
|
|
1874
|
+
// The truncation warning is advice about an LLM review of this diff, and
|
|
1875
|
+
// a background probe is not going to run one. This branch's own diff is
|
|
1876
|
+
// ~370k chars, so before the guard every covariate run here printed it.
|
|
1877
|
+
const big = Array.from({ length: 4000 }, (_, i) => `line ${i} of a large page`).join(NL);
|
|
1878
|
+
fs.writeFileSync(path.join(repoDir, 'docs', 'big.md'), big);
|
|
1879
|
+
execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', 'add', '-A'], {
|
|
1880
|
+
cwd: repoDir,
|
|
1881
|
+
stdio: 'ignore',
|
|
1882
|
+
});
|
|
1883
|
+
execFileSync('git', ['-c', 'user.name=t', '-c', 'user.email=t@t', 'commit', '-m', 'a large page'], { cwd: repoDir, stdio: 'ignore' });
|
|
1884
|
+
const errSpy = vi.mocked(console.error);
|
|
1885
|
+
errSpy.mockClear();
|
|
1886
|
+
await printCovariateLine({
|
|
1887
|
+
diffMeta: { source: 'uncommitted' },
|
|
1888
|
+
totemDirAbs: path.join(repoDir, '.totem'),
|
|
1889
|
+
cwd: repoDir,
|
|
1890
|
+
globs: ['docs/**'],
|
|
1891
|
+
});
|
|
1892
|
+
const narration = errSpy.mock.calls.map((c) => c.join(' ')).join(NL);
|
|
1893
|
+
// Every one of the resolver's four narration lines, and this module's own.
|
|
1894
|
+
expect(narration).not.toContain('[Legs]');
|
|
1895
|
+
expect(narration).not.toContain('Diff exceeds');
|
|
1896
|
+
expect(narration).not.toContain('Diff source:');
|
|
1897
|
+
expect(narration).not.toContain('Changed files (');
|
|
1898
|
+
// The control that makes the assertion above non-vacuous: the same diff,
|
|
1899
|
+
// resolved WITHOUT the suppression, does narrate all of it — so the
|
|
1900
|
+
// fixture really does cross the 50k threshold and the option is what
|
|
1901
|
+
// silenced it. Called on the resolver directly, because the legs scope
|
|
1902
|
+
// resolver now suppresses it unconditionally and prints its own line.
|
|
1903
|
+
const { getDiffForReview } = await import('../git.js');
|
|
1904
|
+
errSpy.mockClear();
|
|
1905
|
+
await getDiffForReview({ branch: true }, { ignorePatterns: [], shieldIgnorePatterns: [] }, repoDir, 'Control');
|
|
1906
|
+
const loud = errSpy.mock.calls.map((c) => c.join(' ')).join(NL);
|
|
1907
|
+
expect(loud).toContain('Diff exceeds');
|
|
1908
|
+
expect(loud).toContain('Changed files (');
|
|
1909
|
+
});
|
|
1910
|
+
it('a COVERING deposit is named on the same review scope', async () => {
|
|
1911
|
+
depositIn(repoDir, headSha);
|
|
1912
|
+
const printed = await covariateOn('staged');
|
|
1913
|
+
expect(printed.join(' ')).toContain(`local-lane: none leg: ${headSha.slice(0, 8)}`);
|
|
1914
|
+
});
|
|
1915
|
+
});
|
|
1916
|
+
});
|
|
1479
1917
|
//# sourceMappingURL=review-fan.test.js.map
|