@mmnto/totem 1.18.2 → 1.19.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/compile-lesson.d.ts +42 -0
- package/dist/compile-lesson.d.ts.map +1 -1
- package/dist/compile-lesson.js +189 -0
- package/dist/compile-lesson.js.map +1 -1
- package/dist/compile-lesson.test.js +539 -0
- package/dist/compile-lesson.test.js.map +1 -1
- package/dist/compiler-schema.d.ts +110 -30
- package/dist/compiler-schema.d.ts.map +1 -1
- package/dist/compiler-schema.js +48 -2
- package/dist/compiler-schema.js.map +1 -1
- package/dist/compiler-schema.test.js +80 -0
- package/dist/compiler-schema.test.js.map +1 -1
- package/dist/compiler.d.ts +13 -6
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +14 -7
- package/dist/compiler.js.map +1 -1
- package/dist/compiler.test.js +33 -0
- package/dist/compiler.test.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/recurrence-stats.d.ts +10 -10
- package/dist/retrospect.d.ts +42 -42
- package/dist/stage4-verifier.d.ts +133 -0
- package/dist/stage4-verifier.d.ts.map +1 -0
- package/dist/stage4-verifier.js +355 -0
- package/dist/stage4-verifier.js.map +1 -0
- package/dist/stage4-verifier.test.d.ts +2 -0
- package/dist/stage4-verifier.test.d.ts.map +1 -0
- package/dist/stage4-verifier.test.js +372 -0
- package/dist/stage4-verifier.test.js.map +1 -0
- package/package.json +1 -1
|
@@ -1583,6 +1583,184 @@ describe('compileLesson', () => {
|
|
|
1583
1583
|
expect(deps.callbacks.onWarn).toHaveBeenCalledWith(lesson.heading, expect.stringContaining('parse'));
|
|
1584
1584
|
});
|
|
1585
1585
|
});
|
|
1586
|
+
// ─── Test-scope wording mismatch classifier (mmnto-ai/totem#1752) ──
|
|
1587
|
+
describe('compileLesson test-scope mismatch classifier (mmnto-ai/totem#1752)', () => {
|
|
1588
|
+
function makeClassifierDeps() {
|
|
1589
|
+
return {
|
|
1590
|
+
parseCompilerResponse: vi.fn().mockReturnValue({
|
|
1591
|
+
compilable: true,
|
|
1592
|
+
pattern: 'console\\.log',
|
|
1593
|
+
message: 'placeholder',
|
|
1594
|
+
engine: 'regex',
|
|
1595
|
+
badExample: 'console.log("debug")',
|
|
1596
|
+
goodExample: '// placeholder\n',
|
|
1597
|
+
}),
|
|
1598
|
+
runOrchestrator: vi.fn().mockResolvedValue('{"compilable": true}'),
|
|
1599
|
+
existingByHash: new Map(),
|
|
1600
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1601
|
+
};
|
|
1602
|
+
}
|
|
1603
|
+
function getMismatchWarnCalls(deps) {
|
|
1604
|
+
const calls = deps.callbacks.onWarn.mock.calls;
|
|
1605
|
+
return calls.filter((c) => typeof c[1] === 'string' && c[1].includes('Heading suggests test-contract intent'));
|
|
1606
|
+
}
|
|
1607
|
+
it('warns when heading carries test vocabulary AND scope excludes .test.* (n=1 anchor lesson-1d7283b3)', async () => {
|
|
1608
|
+
const mismatchLesson = {
|
|
1609
|
+
index: 0,
|
|
1610
|
+
heading: 'Assert import boundaries in LLM-free tests',
|
|
1611
|
+
body: '**Tags:** testing, architecture\n**Scope:** packages/cli/src/**/*.ts, !**/*.test.*\n\nUse regex assertions to forbid heavy imports.',
|
|
1612
|
+
hash: 'mismatch-1d7283b3',
|
|
1613
|
+
};
|
|
1614
|
+
const deps = makeClassifierDeps();
|
|
1615
|
+
await compileLesson(mismatchLesson, 'system prompt', deps);
|
|
1616
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(1);
|
|
1617
|
+
expect(deps.callbacks.onWarn).toHaveBeenCalledWith(mismatchLesson.heading, expect.stringContaining('Heading suggests test-contract intent'));
|
|
1618
|
+
});
|
|
1619
|
+
it('warns on .spec.* exclusion shape', async () => {
|
|
1620
|
+
const mismatchLesson = {
|
|
1621
|
+
index: 0,
|
|
1622
|
+
heading: 'Spy on logger contracts in tests',
|
|
1623
|
+
body: '**Scope:** packages/**/*.ts, !**/*.spec.*\n\nBody.',
|
|
1624
|
+
hash: 'mismatch-spec',
|
|
1625
|
+
};
|
|
1626
|
+
const deps = makeClassifierDeps();
|
|
1627
|
+
await compileLesson(mismatchLesson, 'system prompt', deps);
|
|
1628
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(1);
|
|
1629
|
+
});
|
|
1630
|
+
it('warns on /__tests__/ directory exclusion shape', async () => {
|
|
1631
|
+
const mismatchLesson = {
|
|
1632
|
+
index: 0,
|
|
1633
|
+
heading: 'Assertion contract for emitter',
|
|
1634
|
+
body: '**Scope:** packages/**/*.ts, !**/__tests__/**\n\nBody.',
|
|
1635
|
+
hash: 'mismatch-dunder',
|
|
1636
|
+
};
|
|
1637
|
+
const deps = makeClassifierDeps();
|
|
1638
|
+
await compileLesson(mismatchLesson, 'system prompt', deps);
|
|
1639
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(1);
|
|
1640
|
+
});
|
|
1641
|
+
it('warns on /tests/ directory exclusion shape', async () => {
|
|
1642
|
+
const mismatchLesson = {
|
|
1643
|
+
index: 0,
|
|
1644
|
+
heading: 'Test fixture isolation',
|
|
1645
|
+
body: '**Scope:** packages/**/*.ts, !**/tests/**\n\nBody.',
|
|
1646
|
+
hash: 'mismatch-tests-dir',
|
|
1647
|
+
};
|
|
1648
|
+
const deps = makeClassifierDeps();
|
|
1649
|
+
await compileLesson(mismatchLesson, 'system prompt', deps);
|
|
1650
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(1);
|
|
1651
|
+
});
|
|
1652
|
+
it('warns on root-relative !tests/** exclusion (no leading **/ glob)', async () => {
|
|
1653
|
+
const mismatchLesson = {
|
|
1654
|
+
index: 0,
|
|
1655
|
+
heading: 'Spec coverage for emitter',
|
|
1656
|
+
body: '**Scope:** src/**/*.ts, !tests/**\n\nBody.',
|
|
1657
|
+
hash: 'mismatch-root-tests',
|
|
1658
|
+
};
|
|
1659
|
+
const deps = makeClassifierDeps();
|
|
1660
|
+
await compileLesson(mismatchLesson, 'system prompt', deps);
|
|
1661
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(1);
|
|
1662
|
+
});
|
|
1663
|
+
it('warns on root-relative !__tests__/** exclusion (no leading **/ glob)', async () => {
|
|
1664
|
+
const mismatchLesson = {
|
|
1665
|
+
index: 0,
|
|
1666
|
+
heading: 'Assertion contract for emitter',
|
|
1667
|
+
body: '**Scope:** src/**/*.ts, !__tests__/**\n\nBody.',
|
|
1668
|
+
hash: 'mismatch-root-dunder',
|
|
1669
|
+
};
|
|
1670
|
+
const deps = makeClassifierDeps();
|
|
1671
|
+
await compileLesson(mismatchLesson, 'system prompt', deps);
|
|
1672
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(1);
|
|
1673
|
+
});
|
|
1674
|
+
it('does not warn when heading carries test vocabulary AND scope includes test patterns', async () => {
|
|
1675
|
+
const alignedLesson = {
|
|
1676
|
+
index: 0,
|
|
1677
|
+
heading: 'Spy on logger contracts in tests',
|
|
1678
|
+
body: '**Scope:** packages/**/*.test.ts\n\nBody.',
|
|
1679
|
+
hash: 'aligned-1',
|
|
1680
|
+
};
|
|
1681
|
+
const deps = makeClassifierDeps();
|
|
1682
|
+
await compileLesson(alignedLesson, 'system prompt', deps);
|
|
1683
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(0);
|
|
1684
|
+
});
|
|
1685
|
+
it('does not warn when heading lacks test vocabulary even if scope excludes tests', async () => {
|
|
1686
|
+
const nonTestLesson = {
|
|
1687
|
+
index: 0,
|
|
1688
|
+
heading: 'Use err not error in catch blocks',
|
|
1689
|
+
body: '**Scope:** packages/**/*.ts, !**/*.test.*\n\nBody.',
|
|
1690
|
+
hash: 'non-test-heading',
|
|
1691
|
+
};
|
|
1692
|
+
const deps = makeClassifierDeps();
|
|
1693
|
+
await compileLesson(nonTestLesson, 'system prompt', deps);
|
|
1694
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(0);
|
|
1695
|
+
});
|
|
1696
|
+
it('does not warn when source declares no Scope', async () => {
|
|
1697
|
+
const noScopeLesson = {
|
|
1698
|
+
index: 0,
|
|
1699
|
+
heading: 'Test the public API surface',
|
|
1700
|
+
body: 'No Scope field declared in this body.',
|
|
1701
|
+
hash: 'no-scope-1',
|
|
1702
|
+
};
|
|
1703
|
+
const deps = makeClassifierDeps();
|
|
1704
|
+
await compileLesson(noScopeLesson, 'system prompt', deps);
|
|
1705
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(0);
|
|
1706
|
+
});
|
|
1707
|
+
it('respects word boundaries — substring traps like "latest" do not trigger', async () => {
|
|
1708
|
+
const substringLesson = {
|
|
1709
|
+
index: 0,
|
|
1710
|
+
heading: 'Update latest manifest entries',
|
|
1711
|
+
body: '**Scope:** packages/**/*.ts, !**/*.test.*\n\nBody.',
|
|
1712
|
+
hash: 'substring-trap',
|
|
1713
|
+
};
|
|
1714
|
+
const deps = makeClassifierDeps();
|
|
1715
|
+
await compileLesson(substringLesson, 'system prompt', deps);
|
|
1716
|
+
expect(getMismatchWarnCalls(deps)).toHaveLength(0);
|
|
1717
|
+
});
|
|
1718
|
+
it('does not throw when callbacks are omitted entirely', async () => {
|
|
1719
|
+
const mismatchLesson = {
|
|
1720
|
+
index: 0,
|
|
1721
|
+
heading: 'Assert tests pass',
|
|
1722
|
+
body: '**Scope:** packages/**/*.ts, !**/*.test.*\n\nBody.',
|
|
1723
|
+
hash: 'no-callbacks',
|
|
1724
|
+
};
|
|
1725
|
+
const deps = {
|
|
1726
|
+
parseCompilerResponse: vi.fn().mockReturnValue({
|
|
1727
|
+
compilable: true,
|
|
1728
|
+
pattern: 'console\\.log',
|
|
1729
|
+
message: 'placeholder',
|
|
1730
|
+
engine: 'regex',
|
|
1731
|
+
badExample: 'console.log("debug")',
|
|
1732
|
+
goodExample: '// placeholder\n',
|
|
1733
|
+
}),
|
|
1734
|
+
runOrchestrator: vi.fn().mockResolvedValue('{"compilable": true}'),
|
|
1735
|
+
existingByHash: new Map(),
|
|
1736
|
+
// No callbacks
|
|
1737
|
+
};
|
|
1738
|
+
await expect(compileLesson(mismatchLesson, 'system prompt', deps)).resolves.toBeDefined();
|
|
1739
|
+
});
|
|
1740
|
+
it('does not warn when onWarn callback is omitted (other callbacks present)', async () => {
|
|
1741
|
+
const mismatchLesson = {
|
|
1742
|
+
index: 0,
|
|
1743
|
+
heading: 'Assert tests pass',
|
|
1744
|
+
body: '**Scope:** packages/**/*.ts, !**/*.test.*\n\nBody.',
|
|
1745
|
+
hash: 'no-onwarn',
|
|
1746
|
+
};
|
|
1747
|
+
const onDim = vi.fn();
|
|
1748
|
+
const deps = {
|
|
1749
|
+
parseCompilerResponse: vi.fn().mockReturnValue({
|
|
1750
|
+
compilable: true,
|
|
1751
|
+
pattern: 'console\\.log',
|
|
1752
|
+
message: 'placeholder',
|
|
1753
|
+
engine: 'regex',
|
|
1754
|
+
badExample: 'console.log("debug")',
|
|
1755
|
+
goodExample: '// placeholder\n',
|
|
1756
|
+
}),
|
|
1757
|
+
runOrchestrator: vi.fn().mockResolvedValue('{"compilable": true}'),
|
|
1758
|
+
existingByHash: new Map(),
|
|
1759
|
+
callbacks: { onDim },
|
|
1760
|
+
};
|
|
1761
|
+
await expect(compileLesson(mismatchLesson, 'system prompt', deps)).resolves.toBeDefined();
|
|
1762
|
+
});
|
|
1763
|
+
});
|
|
1586
1764
|
// ─── verifyRuleExamples ──────────────────────────────
|
|
1587
1765
|
describe('verifyRuleExamples', () => {
|
|
1588
1766
|
it('returns null when lesson has no examples', () => {
|
|
@@ -2769,4 +2947,365 @@ describe('compileLesson trace events', () => {
|
|
|
2769
2947
|
expect(orchestratorMock).not.toHaveBeenCalled();
|
|
2770
2948
|
});
|
|
2771
2949
|
});
|
|
2950
|
+
// ─── ADR-091 Stage 4 integration (mmnto-ai/totem#1682) ──
|
|
2951
|
+
describe('compileLesson Stage 4 integration', () => {
|
|
2952
|
+
function makePipeline2Deps(stage4Result) {
|
|
2953
|
+
const onWarn = vi.fn();
|
|
2954
|
+
const onDim = vi.fn();
|
|
2955
|
+
const onStage4Outcome = vi.fn();
|
|
2956
|
+
const verifyStage4 = stage4Result ? vi.fn().mockResolvedValue(stage4Result) : undefined;
|
|
2957
|
+
const deps = {
|
|
2958
|
+
parseCompilerResponse: vi.fn().mockReturnValue({
|
|
2959
|
+
compilable: true,
|
|
2960
|
+
pattern: 'console\\.log',
|
|
2961
|
+
message: 'No console.log',
|
|
2962
|
+
engine: 'regex',
|
|
2963
|
+
badExample: 'console.log("debug")',
|
|
2964
|
+
goodExample: '// noop\n',
|
|
2965
|
+
}),
|
|
2966
|
+
runOrchestrator: vi.fn().mockResolvedValue('{"compilable": true}'),
|
|
2967
|
+
existingByHash: new Map(),
|
|
2968
|
+
callbacks: { onWarn, onDim, onStage4Outcome },
|
|
2969
|
+
...(verifyStage4 ? { verifyStage4 } : {}),
|
|
2970
|
+
};
|
|
2971
|
+
return { deps, onWarn, onStage4Outcome, verifyStage4 };
|
|
2972
|
+
}
|
|
2973
|
+
function makePipeline2Lesson() {
|
|
2974
|
+
return {
|
|
2975
|
+
index: 0,
|
|
2976
|
+
heading: 'Pipeline 2 stage 4 lesson',
|
|
2977
|
+
body: 'Body without manual pattern, no Bad/Good snippets — Pipeline 2 path.',
|
|
2978
|
+
hash: 'h-stage4-p2',
|
|
2979
|
+
};
|
|
2980
|
+
}
|
|
2981
|
+
it('does NOT invoke Stage 4 when deps.verifyStage4 is absent (existing behavior preserved)', async () => {
|
|
2982
|
+
const { deps } = makePipeline2Deps();
|
|
2983
|
+
const result = await compileLesson(makePipeline2Lesson(), 'system prompt', deps);
|
|
2984
|
+
expect(result.status).toBe('compiled');
|
|
2985
|
+
if (result.status === 'compiled') {
|
|
2986
|
+
expect(result.rule.status).toBeUndefined();
|
|
2987
|
+
expect(result.rule.confidence).toBeUndefined();
|
|
2988
|
+
}
|
|
2989
|
+
expect(deps.callbacks.onStage4Outcome).not.toHaveBeenCalled();
|
|
2990
|
+
});
|
|
2991
|
+
it("Pipeline 2: 'no-matches' outcome sets status='untested-against-codebase'", async () => {
|
|
2992
|
+
const { deps, onStage4Outcome } = makePipeline2Deps({
|
|
2993
|
+
outcome: 'no-matches',
|
|
2994
|
+
baselineMatches: [],
|
|
2995
|
+
inScopeMatches: [],
|
|
2996
|
+
candidateDebtLines: [],
|
|
2997
|
+
});
|
|
2998
|
+
const result = await compileLesson(makePipeline2Lesson(), 'system prompt', deps);
|
|
2999
|
+
expect(result.status).toBe('compiled');
|
|
3000
|
+
if (result.status === 'compiled') {
|
|
3001
|
+
expect(result.rule.status).toBe('untested-against-codebase');
|
|
3002
|
+
expect(result.rule.confidence).toBeUndefined();
|
|
3003
|
+
expect(result.rule.archivedReason).toBeUndefined();
|
|
3004
|
+
}
|
|
3005
|
+
expect(onStage4Outcome).toHaveBeenCalledTimes(1);
|
|
3006
|
+
expect(result.trace).toContainEqual(expect.objectContaining({ layer: 4, action: 'verify', outcome: 'no-matches' }));
|
|
3007
|
+
});
|
|
3008
|
+
it("Pipeline 2: 'in-scope-bad-example' outcome sets confidence='high'", async () => {
|
|
3009
|
+
const { deps, onStage4Outcome } = makePipeline2Deps({
|
|
3010
|
+
outcome: 'in-scope-bad-example',
|
|
3011
|
+
baselineMatches: [],
|
|
3012
|
+
inScopeMatches: ['packages/cli/src/foo.ts'],
|
|
3013
|
+
candidateDebtLines: [],
|
|
3014
|
+
});
|
|
3015
|
+
const result = await compileLesson(makePipeline2Lesson(), 'system prompt', deps);
|
|
3016
|
+
expect(result.status).toBe('compiled');
|
|
3017
|
+
if (result.status === 'compiled') {
|
|
3018
|
+
expect(result.rule.confidence).toBe('high');
|
|
3019
|
+
expect(result.rule.status).toBeUndefined();
|
|
3020
|
+
}
|
|
3021
|
+
expect(onStage4Outcome).toHaveBeenCalledTimes(1);
|
|
3022
|
+
expect(result.trace).toContainEqual(expect.objectContaining({ layer: 4, action: 'verify', outcome: 'in-scope-bad-example' }));
|
|
3023
|
+
});
|
|
3024
|
+
it("Pipeline 2: 'candidate-debt' outcome forces severity='warning' and emits onWarn", async () => {
|
|
3025
|
+
const { deps, onWarn, onStage4Outcome } = makePipeline2Deps({
|
|
3026
|
+
outcome: 'candidate-debt',
|
|
3027
|
+
baselineMatches: [],
|
|
3028
|
+
inScopeMatches: ['packages/cli/src/foo.ts', 'packages/cli/src/bar.ts'],
|
|
3029
|
+
candidateDebtLines: [
|
|
3030
|
+
'console.log(`${env.X}`)',
|
|
3031
|
+
'console.log(req.body.id)',
|
|
3032
|
+
"console.log('a')",
|
|
3033
|
+
"console.log('b')",
|
|
3034
|
+
],
|
|
3035
|
+
});
|
|
3036
|
+
// Make the rule's declared severity 'error' to verify the force-downgrade.
|
|
3037
|
+
deps.parseCompilerResponse.mockReturnValue({
|
|
3038
|
+
compilable: true,
|
|
3039
|
+
pattern: 'console\\.log',
|
|
3040
|
+
message: 'No console.log',
|
|
3041
|
+
engine: 'regex',
|
|
3042
|
+
badExample: 'console.log("debug")',
|
|
3043
|
+
goodExample: '// noop\n',
|
|
3044
|
+
severity: 'error',
|
|
3045
|
+
});
|
|
3046
|
+
const result = await compileLesson(makePipeline2Lesson(), 'system prompt', deps);
|
|
3047
|
+
expect(result.status).toBe('compiled');
|
|
3048
|
+
if (result.status === 'compiled') {
|
|
3049
|
+
expect(result.rule.severity).toBe('warning');
|
|
3050
|
+
}
|
|
3051
|
+
expect(onStage4Outcome).toHaveBeenCalledTimes(1);
|
|
3052
|
+
expect(result.trace).toContainEqual(expect.objectContaining({ layer: 4, action: 'verify', outcome: 'candidate-debt' }));
|
|
3053
|
+
// Sample of debt sites should be emitted via onWarn.
|
|
3054
|
+
expect(onWarn).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('Stage 4: candidate debt'));
|
|
3055
|
+
expect(onWarn).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('+ 1 more'));
|
|
3056
|
+
});
|
|
3057
|
+
it("Pipeline 2: 'out-of-scope' outcome archives the rule with reasonCode + paths in archivedReason", async () => {
|
|
3058
|
+
const { deps, onWarn, onStage4Outcome } = makePipeline2Deps({
|
|
3059
|
+
outcome: 'out-of-scope',
|
|
3060
|
+
baselineMatches: ['packages/core/src/transport.ts', 'packages/cli/src/foo.test.ts'],
|
|
3061
|
+
inScopeMatches: ['packages/cli/src/foo.ts'],
|
|
3062
|
+
candidateDebtLines: [],
|
|
3063
|
+
});
|
|
3064
|
+
const result = await compileLesson(makePipeline2Lesson(), 'system prompt', deps);
|
|
3065
|
+
expect(result.status).toBe('compiled');
|
|
3066
|
+
if (result.status === 'compiled') {
|
|
3067
|
+
expect(result.rule.status).toBe('archived');
|
|
3068
|
+
expect(result.rule.archivedAt).toBeDefined();
|
|
3069
|
+
expect(result.rule.archivedReason).toContain('Stage 4');
|
|
3070
|
+
expect(result.rule.archivedReason).toContain('stage4-out-of-scope-match');
|
|
3071
|
+
expect(result.rule.archivedReason).toContain('packages/core/src/transport.ts');
|
|
3072
|
+
expect(result.rule.archivedReason).toContain('packages/cli/src/foo.test.ts');
|
|
3073
|
+
}
|
|
3074
|
+
expect(onStage4Outcome).toHaveBeenCalledTimes(1);
|
|
3075
|
+
expect(result.trace).toContainEqual(expect.objectContaining({
|
|
3076
|
+
layer: 4,
|
|
3077
|
+
action: 'verify',
|
|
3078
|
+
outcome: 'out-of-scope',
|
|
3079
|
+
reasonCode: 'stage4-out-of-scope-match',
|
|
3080
|
+
}));
|
|
3081
|
+
expect(onWarn).toHaveBeenCalledWith(expect.any(String), expect.stringContaining('Stage 4: archived'));
|
|
3082
|
+
});
|
|
3083
|
+
it('Pipeline 3 (Bad/Good example-based) wires Stage 4 — outcome mutates the rule (CR mmnto-ai/totem#1757 R2)', async () => {
|
|
3084
|
+
// Stage 4 is wired into both Pipeline 2 and Pipeline 3 success
|
|
3085
|
+
// branches. The other tests in this block pin Pipeline 2; this case
|
|
3086
|
+
// covers Pipeline 3 so a regression in the Pipeline 3 hook can't
|
|
3087
|
+
// bypass verification while the Pipeline 2 cases stay green.
|
|
3088
|
+
const verifyStage4 = vi.fn().mockResolvedValue({
|
|
3089
|
+
outcome: 'in-scope-bad-example',
|
|
3090
|
+
baselineMatches: [],
|
|
3091
|
+
inScopeMatches: ['packages/cli/src/foo.ts'],
|
|
3092
|
+
candidateDebtLines: [],
|
|
3093
|
+
});
|
|
3094
|
+
const onStage4Outcome = vi.fn();
|
|
3095
|
+
const onWarn = vi.fn();
|
|
3096
|
+
const deps = {
|
|
3097
|
+
parseCompilerResponse: vi.fn().mockReturnValue({
|
|
3098
|
+
compilable: true,
|
|
3099
|
+
pattern: 'console\\.log',
|
|
3100
|
+
message: 'No console.log',
|
|
3101
|
+
engine: 'regex',
|
|
3102
|
+
badExample: "console.log('debug')",
|
|
3103
|
+
goodExample: '// noop',
|
|
3104
|
+
}),
|
|
3105
|
+
runOrchestrator: vi.fn().mockResolvedValue('{"compilable": true}'),
|
|
3106
|
+
existingByHash: new Map(),
|
|
3107
|
+
callbacks: { onWarn, onDim: vi.fn(), onStage4Outcome },
|
|
3108
|
+
verifyStage4,
|
|
3109
|
+
};
|
|
3110
|
+
// Pipeline 3 dispatches when `extractBadGoodSnippets` returns
|
|
3111
|
+
// snippets — body needs explicit Bad/Good code blocks AND no
|
|
3112
|
+
// manual `**Pattern:**` (else Pipeline 1 wins).
|
|
3113
|
+
const pipeline3Lesson = {
|
|
3114
|
+
index: 0,
|
|
3115
|
+
heading: 'No console.log in production',
|
|
3116
|
+
body: [
|
|
3117
|
+
'**Bad:**',
|
|
3118
|
+
'',
|
|
3119
|
+
'```ts',
|
|
3120
|
+
"console.log('debug')",
|
|
3121
|
+
'```',
|
|
3122
|
+
'',
|
|
3123
|
+
'**Good:**',
|
|
3124
|
+
'',
|
|
3125
|
+
'```ts',
|
|
3126
|
+
'// noop',
|
|
3127
|
+
'```',
|
|
3128
|
+
].join('\n'),
|
|
3129
|
+
hash: 'h-stage4-p3',
|
|
3130
|
+
};
|
|
3131
|
+
const result = await compileLesson(pipeline3Lesson, 'system prompt', deps);
|
|
3132
|
+
expect(result.status).toBe('compiled');
|
|
3133
|
+
if (result.status === 'compiled') {
|
|
3134
|
+
expect(result.rule.confidence).toBe('high');
|
|
3135
|
+
}
|
|
3136
|
+
expect(verifyStage4).toHaveBeenCalledTimes(1);
|
|
3137
|
+
expect(onStage4Outcome).toHaveBeenCalledTimes(1);
|
|
3138
|
+
expect(result.trace).toContainEqual(expect.objectContaining({ layer: 4, action: 'verify', outcome: 'in-scope-bad-example' }));
|
|
3139
|
+
});
|
|
3140
|
+
it('Pipeline 1 (manual rule) bypasses Stage 4 — verifyStage4 not invoked', async () => {
|
|
3141
|
+
// Pipeline 1 manual rules are human-authored and self-evidencing per the
|
|
3142
|
+
// Pipeline 1 / unverified semantics; Stage 4 is a safety net for LLM-
|
|
3143
|
+
// generated patterns. The integration site only invokes Stage 4 from
|
|
3144
|
+
// Pipeline 2 / Pipeline 3 success branches.
|
|
3145
|
+
const verifyStage4 = vi.fn().mockResolvedValue({
|
|
3146
|
+
outcome: 'no-matches',
|
|
3147
|
+
baselineMatches: [],
|
|
3148
|
+
inScopeMatches: [],
|
|
3149
|
+
candidateDebtLines: [],
|
|
3150
|
+
});
|
|
3151
|
+
const deps = {
|
|
3152
|
+
parseCompilerResponse: vi.fn(),
|
|
3153
|
+
runOrchestrator: vi.fn(),
|
|
3154
|
+
existingByHash: new Map(),
|
|
3155
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
3156
|
+
verifyStage4,
|
|
3157
|
+
};
|
|
3158
|
+
const result = await compileLesson(manualLesson, 'system prompt', deps);
|
|
3159
|
+
expect(result.status).toBe('compiled');
|
|
3160
|
+
expect(verifyStage4).not.toHaveBeenCalled();
|
|
3161
|
+
});
|
|
3162
|
+
it('preserves the layer-3 MATCH trace event alongside the new layer-4 verify event', async () => {
|
|
3163
|
+
// Stage 4 appends to the trace; it does not replace existing events.
|
|
3164
|
+
// The CLI --verbose renderer relies on the full sequence.
|
|
3165
|
+
const { deps } = makePipeline2Deps({
|
|
3166
|
+
outcome: 'no-matches',
|
|
3167
|
+
baselineMatches: [],
|
|
3168
|
+
inScopeMatches: [],
|
|
3169
|
+
candidateDebtLines: [],
|
|
3170
|
+
});
|
|
3171
|
+
const result = await compileLesson(makePipeline2Lesson(), 'system prompt', deps);
|
|
3172
|
+
expect(result.trace).toContainEqual(expect.objectContaining({ layer: 3, action: 'verify', outcome: 'MATCH' }));
|
|
3173
|
+
expect(result.trace).toContainEqual(expect.objectContaining({ layer: 3, action: 'result', outcome: 'compiled' }));
|
|
3174
|
+
expect(result.trace).toContainEqual(expect.objectContaining({ layer: 4, action: 'verify', outcome: 'no-matches' }));
|
|
3175
|
+
});
|
|
3176
|
+
it("'in-scope-bad-example' promotes a carry-forward 'untested-against-codebase' rule to 'active' — CR mmnto-ai/totem#1757 R2", async () => {
|
|
3177
|
+
// F6 filters `'untested-against-codebase'` out of the lint path, so
|
|
3178
|
+
// a recompile that produces positive Stage 4 evidence MUST clear the
|
|
3179
|
+
// stale status or the rule stays inert despite high-confidence
|
|
3180
|
+
// matches. Tested for both in-scope-bad-example (this case) and
|
|
3181
|
+
// candidate-debt (next case).
|
|
3182
|
+
const lesson = makePipeline2Lesson();
|
|
3183
|
+
const { deps } = makePipeline2Deps({
|
|
3184
|
+
outcome: 'in-scope-bad-example',
|
|
3185
|
+
baselineMatches: [],
|
|
3186
|
+
inScopeMatches: ['packages/cli/src/foo.ts'],
|
|
3187
|
+
candidateDebtLines: [],
|
|
3188
|
+
});
|
|
3189
|
+
deps.existingByHash = new Map([
|
|
3190
|
+
[
|
|
3191
|
+
lesson.hash,
|
|
3192
|
+
{
|
|
3193
|
+
lessonHash: lesson.hash,
|
|
3194
|
+
message: 'previously-untested',
|
|
3195
|
+
pattern: 'console\\.log',
|
|
3196
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
3197
|
+
status: 'untested-against-codebase',
|
|
3198
|
+
},
|
|
3199
|
+
],
|
|
3200
|
+
]);
|
|
3201
|
+
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
3202
|
+
expect(result.status).toBe('compiled');
|
|
3203
|
+
if (result.status === 'compiled') {
|
|
3204
|
+
expect(result.rule.status).toBe('active');
|
|
3205
|
+
expect(result.rule.confidence).toBe('high');
|
|
3206
|
+
}
|
|
3207
|
+
});
|
|
3208
|
+
it("'candidate-debt' promotes a carry-forward 'untested-against-codebase' rule to 'active' — CR mmnto-ai/totem#1757 R2", async () => {
|
|
3209
|
+
const lesson = makePipeline2Lesson();
|
|
3210
|
+
const { deps } = makePipeline2Deps({
|
|
3211
|
+
outcome: 'candidate-debt',
|
|
3212
|
+
baselineMatches: [],
|
|
3213
|
+
inScopeMatches: ['packages/cli/src/foo.ts'],
|
|
3214
|
+
candidateDebtLines: ['console.log(req.body.id)'],
|
|
3215
|
+
});
|
|
3216
|
+
deps.existingByHash = new Map([
|
|
3217
|
+
[
|
|
3218
|
+
lesson.hash,
|
|
3219
|
+
{
|
|
3220
|
+
lessonHash: lesson.hash,
|
|
3221
|
+
message: 'previously-untested',
|
|
3222
|
+
pattern: 'console\\.log',
|
|
3223
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
3224
|
+
status: 'untested-against-codebase',
|
|
3225
|
+
},
|
|
3226
|
+
],
|
|
3227
|
+
]);
|
|
3228
|
+
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
3229
|
+
expect(result.status).toBe('compiled');
|
|
3230
|
+
if (result.status === 'compiled') {
|
|
3231
|
+
expect(result.rule.status).toBe('active');
|
|
3232
|
+
expect(result.rule.severity).toBe('warning');
|
|
3233
|
+
}
|
|
3234
|
+
});
|
|
3235
|
+
it("'no-matches' preserves a previously archived rule's status (carry-forward) — CR mmnto-ai/totem#1757 R1", async () => {
|
|
3236
|
+
// `preserveLifecycleFields` carries `status: 'archived'` (and its
|
|
3237
|
+
// `archivedReason`/`archivedAt`) forward on `--force` recompile.
|
|
3238
|
+
// Setting `status = 'untested-against-codebase'` unconditionally on
|
|
3239
|
+
// a `'no-matches'` outcome would silently un-archive a rule that
|
|
3240
|
+
// postmerge curation explicitly silenced.
|
|
3241
|
+
const lesson = makePipeline2Lesson();
|
|
3242
|
+
const { deps } = makePipeline2Deps({
|
|
3243
|
+
outcome: 'no-matches',
|
|
3244
|
+
baselineMatches: [],
|
|
3245
|
+
inScopeMatches: [],
|
|
3246
|
+
candidateDebtLines: [],
|
|
3247
|
+
});
|
|
3248
|
+
deps.existingByHash = new Map([
|
|
3249
|
+
[
|
|
3250
|
+
lesson.hash,
|
|
3251
|
+
{
|
|
3252
|
+
lessonHash: lesson.hash,
|
|
3253
|
+
message: 'previously-archived',
|
|
3254
|
+
pattern: 'console\\.log',
|
|
3255
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
3256
|
+
status: 'archived',
|
|
3257
|
+
archivedReason: 'manual archive (postmerge curation)',
|
|
3258
|
+
archivedAt: '2026-02-01T00:00:00.000Z',
|
|
3259
|
+
},
|
|
3260
|
+
],
|
|
3261
|
+
]);
|
|
3262
|
+
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
3263
|
+
expect(result.status).toBe('compiled');
|
|
3264
|
+
if (result.status === 'compiled') {
|
|
3265
|
+
expect(result.rule.status).toBe('archived');
|
|
3266
|
+
expect(result.rule.archivedReason).toBe('manual archive (postmerge curation)');
|
|
3267
|
+
expect(result.rule.archivedAt).toBe('2026-02-01T00:00:00.000Z');
|
|
3268
|
+
}
|
|
3269
|
+
});
|
|
3270
|
+
it("'candidate-debt' sanitizes CSI bytes in debtLines before onWarn — CR mmnto-ai/totem#1757 R1", async () => {
|
|
3271
|
+
// Repository code can carry CSI / control bytes from a tampered
|
|
3272
|
+
// file. `onWarn` lands in terminal output; raw text would let a
|
|
3273
|
+
// hostile pattern spoof cursor moves or color resets. Mirrors the
|
|
3274
|
+
// #1743 R4-R7 sanitization wave on the agent-rendered surface.
|
|
3275
|
+
const { deps, onWarn } = makePipeline2Deps({
|
|
3276
|
+
outcome: 'candidate-debt',
|
|
3277
|
+
baselineMatches: [],
|
|
3278
|
+
inScopeMatches: ['packages/cli/src/foo.ts'],
|
|
3279
|
+
candidateDebtLines: ['console.log(\x1b[31m"red"\x1b[0m)', 'console.log("\x07bell")'],
|
|
3280
|
+
});
|
|
3281
|
+
await compileLesson(makePipeline2Lesson(), 'system prompt', deps);
|
|
3282
|
+
const warnCall = onWarn.mock.calls.find((call) => typeof call[1] === 'string' && call[1].includes('candidate debt'));
|
|
3283
|
+
expect(warnCall).toBeDefined();
|
|
3284
|
+
const message = warnCall[1];
|
|
3285
|
+
// No raw ESC, no raw BEL after sanitization.
|
|
3286
|
+
expect(message).not.toMatch(/\x1b/);
|
|
3287
|
+
expect(message).not.toMatch(/\x07/);
|
|
3288
|
+
// Visible code shape preserved (CSI sequence stripped, payload kept).
|
|
3289
|
+
expect(message).toContain('"red"');
|
|
3290
|
+
});
|
|
3291
|
+
it("'out-of-scope' sanitizes CSI bytes in baselineMatches before archivedReason — CR mmnto-ai/totem#1757 R1", async () => {
|
|
3292
|
+
// Path text persists into compiled-rules.json (`archivedReason`)
|
|
3293
|
+
// and surfaces in `onWarn`. A hostile filename with CSI bytes would
|
|
3294
|
+
// re-emerge whenever the manifest is tailed in a terminal.
|
|
3295
|
+
const { deps } = makePipeline2Deps({
|
|
3296
|
+
outcome: 'out-of-scope',
|
|
3297
|
+
baselineMatches: ['packages/cli/src/\x1b[31mhostile\x1b[0m.test.ts'],
|
|
3298
|
+
inScopeMatches: ['packages/cli/src/foo.ts'],
|
|
3299
|
+
candidateDebtLines: [],
|
|
3300
|
+
});
|
|
3301
|
+
const result = await compileLesson(makePipeline2Lesson(), 'system prompt', deps);
|
|
3302
|
+
expect(result.status).toBe('compiled');
|
|
3303
|
+
if (result.status === 'compiled') {
|
|
3304
|
+
expect(result.rule.archivedReason).toBeDefined();
|
|
3305
|
+
expect(result.rule.archivedReason).not.toMatch(/\x1b/);
|
|
3306
|
+
// Path payload still present, just stripped of escape bytes.
|
|
3307
|
+
expect(result.rule.archivedReason).toContain('hostile');
|
|
3308
|
+
}
|
|
3309
|
+
});
|
|
3310
|
+
});
|
|
2772
3311
|
//# sourceMappingURL=compile-lesson.test.js.map
|