@mmnto/totem 1.14.11 → 1.14.13
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/adversarial.test.js +20 -15
- package/dist/adversarial.test.js.map +1 -1
- package/dist/compile-lesson.d.ts +58 -2
- package/dist/compile-lesson.d.ts.map +1 -1
- package/dist/compile-lesson.js +307 -29
- package/dist/compile-lesson.js.map +1 -1
- package/dist/compile-lesson.test.js +491 -19
- package/dist/compile-lesson.test.js.map +1 -1
- package/dist/compiler-schema.d.ts +154 -9
- package/dist/compiler-schema.d.ts.map +1 -1
- package/dist/compiler-schema.js +88 -10
- package/dist/compiler-schema.js.map +1 -1
- package/dist/compiler.d.ts +12 -4
- package/dist/compiler.d.ts.map +1 -1
- package/dist/compiler.js +25 -5
- package/dist/compiler.js.map +1 -1
- package/dist/compiler.test.js +194 -50
- package/dist/compiler.test.js.map +1 -1
- package/dist/config-schema.d.ts +45 -0
- package/dist/config-schema.d.ts.map +1 -1
- package/dist/config-schema.js +21 -0
- package/dist/config-schema.js.map +1 -1
- package/dist/config-schema.test.js +50 -1
- package/dist/config-schema.test.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -3
- package/dist/index.js.map +1 -1
- package/dist/rule-engine.d.ts +56 -13
- package/dist/rule-engine.d.ts.map +1 -1
- package/dist/rule-engine.js +62 -40
- package/dist/rule-engine.js.map +1 -1
- package/dist/rule-engine.test.js +87 -56
- package/dist/rule-engine.test.js.map +1 -1
- package/dist/rule-metrics.d.ts +40 -0
- package/dist/rule-metrics.d.ts.map +1 -1
- package/dist/rule-metrics.js +28 -0
- package/dist/rule-metrics.js.map +1 -1
- package/dist/rule-metrics.test.js +104 -3
- package/dist/rule-metrics.test.js.map +1 -1
- package/dist/rule-tester.d.ts.map +1 -1
- package/dist/rule-tester.js +9 -3
- package/dist/rule-tester.js.map +1 -1
- package/dist/test-utils.d.ts +9 -0
- package/dist/test-utils.d.ts.map +1 -1
- package/dist/test-utils.js +13 -0
- package/dist/test-utils.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { buildCompiledRule, buildManualRule, compileLesson, validateAstGrepPattern, verifyRuleExamples, } from './compile-lesson.js';
|
|
2
|
+
import { buildCompiledRule, buildManualRule, compileLesson, isSecurityContext, validateAstGrepPattern, verifyRuleExamples, } from './compile-lesson.js';
|
|
3
3
|
import { CompilerOutputSchema } from './compiler-schema.js';
|
|
4
4
|
// ─── Helpers ────────────────────────────────────────
|
|
5
5
|
const lesson = {
|
|
@@ -912,7 +912,11 @@ describe('compileLesson', () => {
|
|
|
912
912
|
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
913
913
|
expect(result.status).toBe('noop');
|
|
914
914
|
});
|
|
915
|
-
it('returns
|
|
915
|
+
it('returns skipped with pattern-syntax-invalid when parseCompilerResponse returns null', async () => {
|
|
916
|
+
// mmnto-ai/totem#1481: the Pipeline 2 parse-failure exit route was
|
|
917
|
+
// upgraded from 'failed' to 'skipped' with a machine-readable
|
|
918
|
+
// reasonCode so ADR-088 Layer 4 telemetry sees every LLM-output
|
|
919
|
+
// parse error rather than losing them to the 'failed' bucket.
|
|
916
920
|
const deps = {
|
|
917
921
|
parseCompilerResponse: vi.fn().mockReturnValue(null),
|
|
918
922
|
runOrchestrator: vi.fn().mockResolvedValue('bad response'),
|
|
@@ -920,7 +924,10 @@ describe('compileLesson', () => {
|
|
|
920
924
|
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
921
925
|
};
|
|
922
926
|
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
923
|
-
expect(result.status).toBe('
|
|
927
|
+
expect(result.status).toBe('skipped');
|
|
928
|
+
if (result.status === 'skipped') {
|
|
929
|
+
expect(result.reasonCode).toBe('pattern-syntax-invalid');
|
|
930
|
+
}
|
|
924
931
|
expect(deps.callbacks.onWarn).toHaveBeenCalled();
|
|
925
932
|
});
|
|
926
933
|
it('returns skipped for non-compilable lessons', async () => {
|
|
@@ -1228,7 +1235,10 @@ describe('compileLesson Pipeline 3 (Bad/Good snippets)', () => {
|
|
|
1228
1235
|
// Pattern matches neither Bad nor Good - smoke gate catches this
|
|
1229
1236
|
// before the old self-verification step ever runs. Pre-#1408 the
|
|
1230
1237
|
// test asserted on the self-verification message; post-#1408 the
|
|
1231
|
-
// smoke gate is the earlier (and stricter) filter.
|
|
1238
|
+
// smoke gate is the earlier (and stricter) filter. mmnto-ai/totem#1481
|
|
1239
|
+
// further promotes the smoke-gate zero-match branch from 'failed'
|
|
1240
|
+
// to 'skipped' with reasonCode 'pattern-syntax-invalid' so Layer 4
|
|
1241
|
+
// telemetry gets a machine-readable entry.
|
|
1232
1242
|
pattern: 'this_matches_nothing_at_all',
|
|
1233
1243
|
message: 'Wrong pattern',
|
|
1234
1244
|
engine: 'regex',
|
|
@@ -1238,7 +1248,11 @@ describe('compileLesson Pipeline 3 (Bad/Good snippets)', () => {
|
|
|
1238
1248
|
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1239
1249
|
};
|
|
1240
1250
|
const result = await compileLesson(pipeline3Lesson, 'system prompt', deps);
|
|
1241
|
-
expect(result.status).toBe('
|
|
1251
|
+
expect(result.status).toBe('skipped');
|
|
1252
|
+
if (result.status === 'skipped') {
|
|
1253
|
+
expect(result.reasonCode).toBe('pattern-zero-match');
|
|
1254
|
+
expect(result.reason).toContain('smoke gate');
|
|
1255
|
+
}
|
|
1242
1256
|
expect(deps.callbacks.onWarn).toHaveBeenCalledWith(pipeline3Lesson.heading, expect.stringContaining('smoke gate'));
|
|
1243
1257
|
});
|
|
1244
1258
|
it('passes self-verification when pattern matches Bad but not Good', async () => {
|
|
@@ -1302,7 +1316,9 @@ describe('compileLesson Pipeline 3 (Bad/Good snippets)', () => {
|
|
|
1302
1316
|
const result = await compileLesson(pipeline3Lesson, 'system prompt', deps);
|
|
1303
1317
|
expect(result.status).toBe('noop');
|
|
1304
1318
|
});
|
|
1305
|
-
it('returns
|
|
1319
|
+
it('returns skipped with pattern-syntax-invalid when Pipeline 3 LLM response cannot be parsed', async () => {
|
|
1320
|
+
// mmnto-ai/totem#1481: Pipeline 3 parse-failure now lands in the ledger
|
|
1321
|
+
// with a machine-readable reasonCode per ADR-088 Layer 4.
|
|
1306
1322
|
const deps = {
|
|
1307
1323
|
parseCompilerResponse: vi.fn().mockReturnValue(null),
|
|
1308
1324
|
runOrchestrator: vi.fn().mockResolvedValue('bad response'),
|
|
@@ -1310,7 +1326,10 @@ describe('compileLesson Pipeline 3 (Bad/Good snippets)', () => {
|
|
|
1310
1326
|
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1311
1327
|
};
|
|
1312
1328
|
const result = await compileLesson(pipeline3Lesson, 'system prompt', deps);
|
|
1313
|
-
expect(result.status).toBe('
|
|
1329
|
+
expect(result.status).toBe('skipped');
|
|
1330
|
+
if (result.status === 'skipped') {
|
|
1331
|
+
expect(result.reasonCode).toBe('pattern-syntax-invalid');
|
|
1332
|
+
}
|
|
1314
1333
|
expect(deps.callbacks.onWarn).toHaveBeenCalledWith(pipeline3Lesson.heading, expect.stringContaining('Pipeline 3'));
|
|
1315
1334
|
});
|
|
1316
1335
|
});
|
|
@@ -1394,7 +1413,21 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
|
|
|
1394
1413
|
it('rejects a security-context rule without retry when verify fails', async () => {
|
|
1395
1414
|
// securityContext: true + zero-match on attempt 1. Zero tolerance per
|
|
1396
1415
|
// ADR-088 Decision 3 means no retry. Expected outcome: skipped,
|
|
1397
|
-
// reasonCode 'security-
|
|
1416
|
+
// reasonCode 'security-rule-rejected', exactly 1 orchestrator call.
|
|
1417
|
+
//
|
|
1418
|
+
// mmnto-ai/totem#1480 added an upfront Example-Hit short-circuit for
|
|
1419
|
+
// security context, so this test uses a lesson body that includes a
|
|
1420
|
+
// non-empty Example Hit block. That keeps the test exercising the
|
|
1421
|
+
// verify-path security branch, not the new no-example-hit short-circuit.
|
|
1422
|
+
const securityLesson = {
|
|
1423
|
+
index: 0,
|
|
1424
|
+
heading: 'No shell injection in spawn',
|
|
1425
|
+
body: [
|
|
1426
|
+
'Do not pass untrusted input to spawn with shell: true.',
|
|
1427
|
+
'**Example Hit:** spawn("sh", ["-c", userInput])',
|
|
1428
|
+
].join('\n'),
|
|
1429
|
+
hash: 'h-security-verify',
|
|
1430
|
+
};
|
|
1398
1431
|
const parseMock = vi.fn().mockReturnValue({
|
|
1399
1432
|
compilable: true,
|
|
1400
1433
|
pattern: 'never_matches_xyz',
|
|
@@ -1410,14 +1443,14 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
|
|
|
1410
1443
|
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1411
1444
|
securityContext: true,
|
|
1412
1445
|
};
|
|
1413
|
-
const result = await compileLesson(
|
|
1446
|
+
const result = await compileLesson(securityLesson, 'system prompt', deps);
|
|
1414
1447
|
expect(result.status).toBe('skipped');
|
|
1415
1448
|
if (result.status === 'skipped') {
|
|
1416
|
-
expect(result.reasonCode).toBe('security-
|
|
1449
|
+
expect(result.reasonCode).toBe('security-rule-rejected');
|
|
1417
1450
|
expect(result.reason).toContain('zero matches');
|
|
1418
1451
|
}
|
|
1419
1452
|
expect(orchestratorMock).toHaveBeenCalledTimes(1);
|
|
1420
|
-
expect(deps.callbacks.onWarn).toHaveBeenCalledWith(
|
|
1453
|
+
expect(deps.callbacks.onWarn).toHaveBeenCalledWith(securityLesson.heading, expect.stringContaining('Security rule rejected on verify failure'));
|
|
1421
1454
|
});
|
|
1422
1455
|
it('does not retry when the smoke-gate rejection is missing badExample (short-circuit to skipped)', async () => {
|
|
1423
1456
|
// Missing badExample is a structural-output failure. Retrying cannot
|
|
@@ -1446,9 +1479,11 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
|
|
|
1446
1479
|
}
|
|
1447
1480
|
expect(orchestratorMock).toHaveBeenCalledTimes(1);
|
|
1448
1481
|
});
|
|
1449
|
-
it('propagates non-compilable LLM verdict with reasonCode
|
|
1482
|
+
it('propagates non-compilable LLM verdict with reasonCode out-of-scope', async () => {
|
|
1450
1483
|
// Conceptual/architectural lessons that the LLM classifies as
|
|
1451
1484
|
// non-compilable. ADR-088 Layer 4 requires a machine-readable reason.
|
|
1485
|
+
// mmnto-ai/totem#1481 renamed `'non-compilable'` to `'out-of-scope'`
|
|
1486
|
+
// to align the internal reason code with the persisted enum.
|
|
1452
1487
|
const parseMock = vi.fn().mockReturnValue({
|
|
1453
1488
|
compilable: false,
|
|
1454
1489
|
reason: 'Architectural principle, not a pattern.',
|
|
@@ -1463,7 +1498,7 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
|
|
|
1463
1498
|
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
1464
1499
|
expect(result.status).toBe('skipped');
|
|
1465
1500
|
if (result.status === 'skipped') {
|
|
1466
|
-
expect(result.reasonCode).toBe('
|
|
1501
|
+
expect(result.reasonCode).toBe('out-of-scope');
|
|
1467
1502
|
expect(result.reason).toBe('Architectural principle, not a pattern.');
|
|
1468
1503
|
}
|
|
1469
1504
|
expect(orchestratorMock).toHaveBeenCalledTimes(1);
|
|
@@ -1554,11 +1589,13 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
|
|
|
1554
1589
|
}
|
|
1555
1590
|
expect(orchestratorMock).toHaveBeenCalledTimes(3);
|
|
1556
1591
|
});
|
|
1557
|
-
it('returns
|
|
1558
|
-
// Validator-level rejection (ADR-088
|
|
1559
|
-
// an invalid regex produces more invalid regexes and
|
|
1560
|
-
//
|
|
1561
|
-
//
|
|
1592
|
+
it('returns skipped with pattern-syntax-invalid when the LLM emits an invalid regex (validator rejection)', async () => {
|
|
1593
|
+
// Validator-level rejection (ADR-088 Layer 4, mmnto-ai/totem#1481).
|
|
1594
|
+
// Retrying an invalid regex produces more invalid regexes and wastes
|
|
1595
|
+
// tokens, so no retry fires. Pre-#1481 this returned 'failed'; post-
|
|
1596
|
+
// #1481 it returns 'skipped' with reasonCode 'pattern-syntax-invalid'
|
|
1597
|
+
// so the ledger carries a machine-readable record per the Layer 4
|
|
1598
|
+
// explicit-failure contract.
|
|
1562
1599
|
const parseMock = vi.fn().mockReturnValue({
|
|
1563
1600
|
compilable: true,
|
|
1564
1601
|
pattern: '[unclosed-bracket-class',
|
|
@@ -1574,8 +1611,443 @@ describe('compileLesson Pipeline 2 verify-retry', () => {
|
|
|
1574
1611
|
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1575
1612
|
};
|
|
1576
1613
|
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
1577
|
-
expect(result.status).toBe('
|
|
1614
|
+
expect(result.status).toBe('skipped');
|
|
1615
|
+
if (result.status === 'skipped') {
|
|
1616
|
+
expect(result.reasonCode).toBe('pattern-syntax-invalid');
|
|
1617
|
+
expect(result.reason).toContain('Rejected regex');
|
|
1618
|
+
}
|
|
1578
1619
|
expect(orchestratorMock).toHaveBeenCalledTimes(1);
|
|
1579
1620
|
});
|
|
1580
1621
|
});
|
|
1622
|
+
// ─── ADR-088 Phase 1 Layer 3: unverified flag (mmnto-ai/totem#1480) ──
|
|
1623
|
+
describe('compileLesson unverified flag', () => {
|
|
1624
|
+
const lessonWithoutExampleHit = {
|
|
1625
|
+
index: 0,
|
|
1626
|
+
heading: 'Use err not error in catch blocks',
|
|
1627
|
+
body: 'Always use `err` as the variable name in catch blocks.',
|
|
1628
|
+
hash: 'h-no-example',
|
|
1629
|
+
};
|
|
1630
|
+
const lessonWithExampleHit = {
|
|
1631
|
+
index: 0,
|
|
1632
|
+
heading: 'No console.log in production',
|
|
1633
|
+
body: 'Do not ship console.log.\n**Example Hit:** console.log(x)',
|
|
1634
|
+
hash: 'h-with-example',
|
|
1635
|
+
};
|
|
1636
|
+
it('flags non-security Pipeline 2 rules as unverified when the lesson has no Example Hit', async () => {
|
|
1637
|
+
// Invariant #2: non-security rule without Example Hit ships with
|
|
1638
|
+
// `unverified: true`. Severity passes through from the LLM output —
|
|
1639
|
+
// no warning-downgrade happens here. A doctor advisory for
|
|
1640
|
+
// `unverified: true` + `severity: 'error'` combos ships with #1483.
|
|
1641
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
1642
|
+
compilable: true,
|
|
1643
|
+
pattern: 'console\\.log',
|
|
1644
|
+
message: 'No console.log',
|
|
1645
|
+
engine: 'regex',
|
|
1646
|
+
severity: 'error',
|
|
1647
|
+
badExample: 'console.log(x)',
|
|
1648
|
+
});
|
|
1649
|
+
const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
|
|
1650
|
+
const deps = {
|
|
1651
|
+
parseCompilerResponse: parseMock,
|
|
1652
|
+
runOrchestrator: orchestratorMock,
|
|
1653
|
+
existingByHash: new Map(),
|
|
1654
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1655
|
+
};
|
|
1656
|
+
const result = await compileLesson(lessonWithoutExampleHit, 'system prompt', deps);
|
|
1657
|
+
expect(result.status).toBe('compiled');
|
|
1658
|
+
if (result.status === 'compiled') {
|
|
1659
|
+
expect(result.rule.unverified).toBe(true);
|
|
1660
|
+
expect(result.rule.severity).toBe('error');
|
|
1661
|
+
}
|
|
1662
|
+
});
|
|
1663
|
+
it('applies the general omitted-severity default on an unverified rule', async () => {
|
|
1664
|
+
// Companion to the severity-pass-through invariant. When the LLM emits
|
|
1665
|
+
// no severity, buildCompiledRule applies the general `'warning'`
|
|
1666
|
+
// default (compile-lesson.ts line ~299: `parsed.severity ?? 'warning'`).
|
|
1667
|
+
// That default is not ADR-088 specific: it fires for any rule without
|
|
1668
|
+
// an emitted severity, unverified or not. Pinning it here guards
|
|
1669
|
+
// against a future ADR-088 change that tries to force severity in
|
|
1670
|
+
// addition to the existing default.
|
|
1671
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
1672
|
+
compilable: true,
|
|
1673
|
+
pattern: 'console\\.log',
|
|
1674
|
+
message: 'No console.log',
|
|
1675
|
+
engine: 'regex',
|
|
1676
|
+
badExample: 'console.log(x)',
|
|
1677
|
+
});
|
|
1678
|
+
const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
|
|
1679
|
+
const deps = {
|
|
1680
|
+
parseCompilerResponse: parseMock,
|
|
1681
|
+
runOrchestrator: orchestratorMock,
|
|
1682
|
+
existingByHash: new Map(),
|
|
1683
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1684
|
+
};
|
|
1685
|
+
const result = await compileLesson(lessonWithoutExampleHit, 'system prompt', deps);
|
|
1686
|
+
expect(result.status).toBe('compiled');
|
|
1687
|
+
if (result.status === 'compiled') {
|
|
1688
|
+
expect(result.rule.unverified).toBe(true);
|
|
1689
|
+
expect(result.rule.severity).toBe('warning');
|
|
1690
|
+
}
|
|
1691
|
+
});
|
|
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.
|
|
1697
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
1698
|
+
compilable: true,
|
|
1699
|
+
pattern: 'console\\.log',
|
|
1700
|
+
message: 'No console.log',
|
|
1701
|
+
engine: 'regex',
|
|
1702
|
+
badExample: 'console.log(x)',
|
|
1703
|
+
});
|
|
1704
|
+
const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
|
|
1705
|
+
const deps = {
|
|
1706
|
+
parseCompilerResponse: parseMock,
|
|
1707
|
+
runOrchestrator: orchestratorMock,
|
|
1708
|
+
existingByHash: new Map(),
|
|
1709
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1710
|
+
};
|
|
1711
|
+
const result = await compileLesson(lessonWithExampleHit, 'system prompt', deps);
|
|
1712
|
+
expect(result.status).toBe('compiled');
|
|
1713
|
+
if (result.status === 'compiled') {
|
|
1714
|
+
expect(result.rule.unverified).toBeUndefined();
|
|
1715
|
+
}
|
|
1716
|
+
});
|
|
1717
|
+
it('treats an Example Hit that is whitespace-only as absent for the unverified flag', async () => {
|
|
1718
|
+
// Edge case from spec: `**Example Hit:** ` (whitespace-only value) is
|
|
1719
|
+
// treated as no ground truth for the `unverified` signal. `trim()` on
|
|
1720
|
+
// every extracted line before counting guards against false-negatives
|
|
1721
|
+
// on whitespace-only examples. (The existing verify step still runs
|
|
1722
|
+
// against whatever extractRuleExamples returns, so the test pattern
|
|
1723
|
+
// matches the empty string to avoid tangling the test with verify
|
|
1724
|
+
// semantics — scope is the unverified flag only.)
|
|
1725
|
+
const lessonEmptyExample = {
|
|
1726
|
+
index: 0,
|
|
1727
|
+
heading: 'Edge case lesson',
|
|
1728
|
+
body: 'Body text.\n**Example Hit:** ',
|
|
1729
|
+
hash: 'h-empty-example',
|
|
1730
|
+
};
|
|
1731
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
1732
|
+
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: '.*',
|
|
1737
|
+
message: 'msg',
|
|
1738
|
+
engine: 'regex',
|
|
1739
|
+
badExample: 'anything',
|
|
1740
|
+
});
|
|
1741
|
+
const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": true}');
|
|
1742
|
+
const deps = {
|
|
1743
|
+
parseCompilerResponse: parseMock,
|
|
1744
|
+
runOrchestrator: orchestratorMock,
|
|
1745
|
+
existingByHash: new Map(),
|
|
1746
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1747
|
+
};
|
|
1748
|
+
const result = await compileLesson(lessonEmptyExample, 'system prompt', deps);
|
|
1749
|
+
expect(result.status).toBe('compiled');
|
|
1750
|
+
if (result.status === 'compiled') {
|
|
1751
|
+
expect(result.rule.unverified).toBe(true);
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
it('rejects security-context lessons that lack an Example Hit with security-rule-rejected', async () => {
|
|
1755
|
+
// Invariant #3: security-context lesson without Example Hit produces
|
|
1756
|
+
// no rule; ledger entry carries reasonCode 'security-rule-rejected'.
|
|
1757
|
+
// No orchestrator call fires (short-circuit before Pipeline 2).
|
|
1758
|
+
const orchestratorMock = vi.fn();
|
|
1759
|
+
const deps = {
|
|
1760
|
+
parseCompilerResponse: vi.fn(),
|
|
1761
|
+
runOrchestrator: orchestratorMock,
|
|
1762
|
+
existingByHash: new Map(),
|
|
1763
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1764
|
+
securityContext: true,
|
|
1765
|
+
};
|
|
1766
|
+
const result = await compileLesson(lessonWithoutExampleHit, 'system prompt', deps);
|
|
1767
|
+
expect(result.status).toBe('skipped');
|
|
1768
|
+
if (result.status === 'skipped') {
|
|
1769
|
+
expect(result.reasonCode).toBe('security-rule-rejected');
|
|
1770
|
+
expect(result.reason).toContain('Example Hit');
|
|
1771
|
+
}
|
|
1772
|
+
expect(orchestratorMock).not.toHaveBeenCalled();
|
|
1773
|
+
});
|
|
1774
|
+
it('flags Pipeline 1 manual rules as unverified when the lesson has no Example Hit', async () => {
|
|
1775
|
+
// Pipeline 1 consistency (design doc Open Question 2, resolution (a)):
|
|
1776
|
+
// manual rules authored without an Example Hit ship unverified too.
|
|
1777
|
+
// The #1414 backfill sweep will eliminate this population eventually.
|
|
1778
|
+
const manualLessonNoExample = {
|
|
1779
|
+
index: 0,
|
|
1780
|
+
heading: 'No console.log in production',
|
|
1781
|
+
body: '**Pattern:** `console\\.log`\n**Engine:** regex\n**Severity:** warning\n**Scope:** **/*.ts',
|
|
1782
|
+
hash: 'h-manual-no-example',
|
|
1783
|
+
};
|
|
1784
|
+
const deps = {
|
|
1785
|
+
parseCompilerResponse: vi.fn(),
|
|
1786
|
+
runOrchestrator: vi.fn(),
|
|
1787
|
+
existingByHash: new Map(),
|
|
1788
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1789
|
+
};
|
|
1790
|
+
const result = await compileLesson(manualLessonNoExample, 'system prompt', deps);
|
|
1791
|
+
expect(result.status).toBe('compiled');
|
|
1792
|
+
if (result.status === 'compiled') {
|
|
1793
|
+
expect(result.rule.unverified).toBe(true);
|
|
1794
|
+
}
|
|
1795
|
+
expect(deps.runOrchestrator).not.toHaveBeenCalled();
|
|
1796
|
+
});
|
|
1797
|
+
});
|
|
1798
|
+
// ─── ADR-088 Decision 3: security-context signal (mmnto-ai/totem#1480) ──
|
|
1799
|
+
describe('isSecurityContext', () => {
|
|
1800
|
+
// Covers both signals the helper accepts. The `deps.securityContext`
|
|
1801
|
+
// branch is exercised end-to-end by compileLesson tests above; the
|
|
1802
|
+
// `rule.immutable === true` branch is defense-in-depth for pack-merged
|
|
1803
|
+
// rules (ADR-089) and cannot reach compileLesson's public surface today
|
|
1804
|
+
// because CompilerOutput has no immutable field and the manual pattern
|
|
1805
|
+
// parser does not parse `**Immutable:**`. Unit tests on the helper lock
|
|
1806
|
+
// both signals so a future CompilerOutput or parser change cannot drop
|
|
1807
|
+
// the rule-based branch silently.
|
|
1808
|
+
const baseDeps = {
|
|
1809
|
+
parseCompilerResponse: vi.fn(),
|
|
1810
|
+
runOrchestrator: vi.fn(),
|
|
1811
|
+
existingByHash: new Map(),
|
|
1812
|
+
};
|
|
1813
|
+
it('returns true when deps.securityContext is true', () => {
|
|
1814
|
+
expect(isSecurityContext({ ...baseDeps, securityContext: true }, null)).toBe(true);
|
|
1815
|
+
});
|
|
1816
|
+
it('returns true when the built rule carries immutable: true', () => {
|
|
1817
|
+
expect(isSecurityContext(baseDeps, { immutable: true })).toBe(true);
|
|
1818
|
+
});
|
|
1819
|
+
it('returns true when both signals are present', () => {
|
|
1820
|
+
expect(isSecurityContext({ ...baseDeps, securityContext: true }, { immutable: true })).toBe(true);
|
|
1821
|
+
});
|
|
1822
|
+
it('returns false when neither signal is present', () => {
|
|
1823
|
+
expect(isSecurityContext(baseDeps, null)).toBe(false);
|
|
1824
|
+
expect(isSecurityContext(baseDeps, { immutable: false })).toBe(false);
|
|
1825
|
+
expect(isSecurityContext(baseDeps, {})).toBe(false);
|
|
1826
|
+
});
|
|
1827
|
+
});
|
|
1828
|
+
// ─── Layer-trace events (mmnto-ai/totem#1482) ────────────
|
|
1829
|
+
describe('compileLesson trace events', () => {
|
|
1830
|
+
it('Pipeline 1 manual compile emits a single layer-1 result event', async () => {
|
|
1831
|
+
// Invariant: Pipeline 1 is deterministic (no LLM, no retry). One event,
|
|
1832
|
+
// outcome 'compiled', layer 1.
|
|
1833
|
+
const deps = {
|
|
1834
|
+
parseCompilerResponse: vi.fn(),
|
|
1835
|
+
runOrchestrator: vi.fn(),
|
|
1836
|
+
existingByHash: new Map(),
|
|
1837
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1838
|
+
};
|
|
1839
|
+
const result = await compileLesson(manualLesson, 'system prompt', deps);
|
|
1840
|
+
expect(result.status).toBe('compiled');
|
|
1841
|
+
expect(result.trace).toBeDefined();
|
|
1842
|
+
expect(result.trace).toHaveLength(1);
|
|
1843
|
+
expect(result.trace[0]).toMatchObject({
|
|
1844
|
+
layer: 1,
|
|
1845
|
+
action: 'result',
|
|
1846
|
+
outcome: 'compiled',
|
|
1847
|
+
});
|
|
1848
|
+
expect(deps.runOrchestrator).not.toHaveBeenCalled();
|
|
1849
|
+
});
|
|
1850
|
+
it('Pipeline 2 first-try compile emits generate + verify(MATCH) + result(compiled)', async () => {
|
|
1851
|
+
const lessonOk = {
|
|
1852
|
+
index: 0,
|
|
1853
|
+
heading: 'No console.log in production',
|
|
1854
|
+
body: 'Do not use console.log.\n**Example Hit:** console.log(x)',
|
|
1855
|
+
hash: 'h-trace-first-try',
|
|
1856
|
+
};
|
|
1857
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
1858
|
+
compilable: true,
|
|
1859
|
+
pattern: 'console\\.log',
|
|
1860
|
+
message: 'No console.log',
|
|
1861
|
+
engine: 'regex',
|
|
1862
|
+
badExample: 'console.log("x")',
|
|
1863
|
+
});
|
|
1864
|
+
const orchestratorMock = vi.fn().mockResolvedValue('attempt-1');
|
|
1865
|
+
const deps = {
|
|
1866
|
+
parseCompilerResponse: parseMock,
|
|
1867
|
+
runOrchestrator: orchestratorMock,
|
|
1868
|
+
existingByHash: new Map(),
|
|
1869
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1870
|
+
};
|
|
1871
|
+
const result = await compileLesson(lessonOk, 'system prompt', deps);
|
|
1872
|
+
expect(result.status).toBe('compiled');
|
|
1873
|
+
expect(result.trace).toBeDefined();
|
|
1874
|
+
expect(result.trace).toHaveLength(3);
|
|
1875
|
+
const [gen, verify, res] = result.trace;
|
|
1876
|
+
expect(gen).toMatchObject({ layer: 3, action: 'generate', outcome: 'attempt-1' });
|
|
1877
|
+
expect(gen.patternHash).toMatch(/^[0-9a-f]{16}$/);
|
|
1878
|
+
expect(verify).toMatchObject({ layer: 3, action: 'verify', outcome: 'MATCH' });
|
|
1879
|
+
expect(res).toMatchObject({ layer: 3, action: 'result', outcome: 'compiled' });
|
|
1880
|
+
});
|
|
1881
|
+
it('Pipeline 2 verify-retry-exhausted emits generate+verify per attempt plus retry scheduling and terminal result', async () => {
|
|
1882
|
+
// Three attempts, each passing smoke gate but failing verifyRuleExamples.
|
|
1883
|
+
// Trace should contain (generate, verify, retry) for attempts 1 and 2, then
|
|
1884
|
+
// (generate, verify, result) for attempt 3.
|
|
1885
|
+
const lessonWithExample = {
|
|
1886
|
+
index: 0,
|
|
1887
|
+
heading: 'No console.log in production',
|
|
1888
|
+
body: 'Do not use console.log in production.\n**Example Hit:** console.log(x)',
|
|
1889
|
+
hash: 'h-trace-exhaust',
|
|
1890
|
+
};
|
|
1891
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
1892
|
+
compilable: true,
|
|
1893
|
+
pattern: 'zzz_only_matches_itself',
|
|
1894
|
+
message: 'No console.log',
|
|
1895
|
+
engine: 'regex',
|
|
1896
|
+
badExample: 'zzz_only_matches_itself',
|
|
1897
|
+
});
|
|
1898
|
+
const orchestratorMock = vi.fn().mockResolvedValue('always-misses');
|
|
1899
|
+
const deps = {
|
|
1900
|
+
parseCompilerResponse: parseMock,
|
|
1901
|
+
runOrchestrator: orchestratorMock,
|
|
1902
|
+
existingByHash: new Map(),
|
|
1903
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1904
|
+
};
|
|
1905
|
+
const result = await compileLesson(lessonWithExample, 'system prompt', deps);
|
|
1906
|
+
expect(result.status).toBe('skipped');
|
|
1907
|
+
expect(result.trace).toBeDefined();
|
|
1908
|
+
// 3 attempts × (generate + verify) = 6 events, plus 2 retry events between
|
|
1909
|
+
// the first two attempts, plus 1 terminal result = 9 events total.
|
|
1910
|
+
const actions = result.trace.map((e) => e.action);
|
|
1911
|
+
expect(actions).toEqual([
|
|
1912
|
+
'generate',
|
|
1913
|
+
'verify',
|
|
1914
|
+
'retry',
|
|
1915
|
+
'generate',
|
|
1916
|
+
'verify',
|
|
1917
|
+
'retry',
|
|
1918
|
+
'generate',
|
|
1919
|
+
'verify',
|
|
1920
|
+
'result',
|
|
1921
|
+
]);
|
|
1922
|
+
const terminal = result.trace[result.trace.length - 1];
|
|
1923
|
+
expect(terminal).toMatchObject({
|
|
1924
|
+
layer: 3,
|
|
1925
|
+
action: 'result',
|
|
1926
|
+
outcome: 'skipped',
|
|
1927
|
+
reasonCode: 'verify-retry-exhausted',
|
|
1928
|
+
});
|
|
1929
|
+
});
|
|
1930
|
+
it('Pipeline 2 out-of-scope skip emits a single layer-3 result event with reasonCode', async () => {
|
|
1931
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
1932
|
+
compilable: false,
|
|
1933
|
+
reason: 'Architectural principle, not a pattern.',
|
|
1934
|
+
});
|
|
1935
|
+
const orchestratorMock = vi.fn().mockResolvedValue('{"compilable": false}');
|
|
1936
|
+
const deps = {
|
|
1937
|
+
parseCompilerResponse: parseMock,
|
|
1938
|
+
runOrchestrator: orchestratorMock,
|
|
1939
|
+
existingByHash: new Map(),
|
|
1940
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1941
|
+
};
|
|
1942
|
+
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
1943
|
+
expect(result.status).toBe('skipped');
|
|
1944
|
+
expect(result.trace).toHaveLength(1);
|
|
1945
|
+
expect(result.trace[0]).toMatchObject({
|
|
1946
|
+
layer: 3,
|
|
1947
|
+
action: 'result',
|
|
1948
|
+
outcome: 'skipped',
|
|
1949
|
+
reasonCode: 'out-of-scope',
|
|
1950
|
+
});
|
|
1951
|
+
});
|
|
1952
|
+
it('Pipeline 2 validator rejection emits generate + verify(validator-rejected) + result(skipped)', async () => {
|
|
1953
|
+
// Invalid regex is terminal. No retry fires. Trace should show the
|
|
1954
|
+
// generate event then the validator-rejected verify event then result.
|
|
1955
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
1956
|
+
compilable: true,
|
|
1957
|
+
pattern: '[unclosed-bracket',
|
|
1958
|
+
message: 'bad regex',
|
|
1959
|
+
engine: 'regex',
|
|
1960
|
+
badExample: 'anything',
|
|
1961
|
+
});
|
|
1962
|
+
const orchestratorMock = vi.fn().mockResolvedValue('invalid-regex-output');
|
|
1963
|
+
const deps = {
|
|
1964
|
+
parseCompilerResponse: parseMock,
|
|
1965
|
+
runOrchestrator: orchestratorMock,
|
|
1966
|
+
existingByHash: new Map(),
|
|
1967
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
1968
|
+
};
|
|
1969
|
+
const result = await compileLesson(lesson, 'system prompt', deps);
|
|
1970
|
+
expect(result.status).toBe('skipped');
|
|
1971
|
+
const actions = result.trace.map((e) => e.action);
|
|
1972
|
+
expect(actions).toEqual(['generate', 'verify', 'result']);
|
|
1973
|
+
expect(result.trace[1]).toMatchObject({
|
|
1974
|
+
layer: 3,
|
|
1975
|
+
action: 'verify',
|
|
1976
|
+
outcome: 'validator-rejected',
|
|
1977
|
+
});
|
|
1978
|
+
expect(result.trace[2]).toMatchObject({
|
|
1979
|
+
layer: 3,
|
|
1980
|
+
action: 'result',
|
|
1981
|
+
outcome: 'skipped',
|
|
1982
|
+
reasonCode: 'pattern-syntax-invalid',
|
|
1983
|
+
});
|
|
1984
|
+
});
|
|
1985
|
+
it('Pipeline 3 example-based compile emits generate + verify + result events at layer 2', async () => {
|
|
1986
|
+
const pipeline3Lesson = {
|
|
1987
|
+
index: 0,
|
|
1988
|
+
heading: 'Example-based lesson',
|
|
1989
|
+
body: [
|
|
1990
|
+
'Do not use console.log.',
|
|
1991
|
+
'',
|
|
1992
|
+
'**Bad:**',
|
|
1993
|
+
'```ts',
|
|
1994
|
+
'console.log("x")',
|
|
1995
|
+
'```',
|
|
1996
|
+
'',
|
|
1997
|
+
'**Good:**',
|
|
1998
|
+
'```ts',
|
|
1999
|
+
'log.info("x")',
|
|
2000
|
+
'```',
|
|
2001
|
+
].join('\n'),
|
|
2002
|
+
hash: 'h-trace-pipeline3',
|
|
2003
|
+
};
|
|
2004
|
+
const parseMock = vi.fn().mockReturnValue({
|
|
2005
|
+
compilable: true,
|
|
2006
|
+
pattern: 'console\\.log',
|
|
2007
|
+
message: 'No console.log',
|
|
2008
|
+
engine: 'regex',
|
|
2009
|
+
badExample: 'console.log("x")',
|
|
2010
|
+
});
|
|
2011
|
+
const orchestratorMock = vi.fn().mockResolvedValue('pipeline-3-response');
|
|
2012
|
+
const deps = {
|
|
2013
|
+
parseCompilerResponse: parseMock,
|
|
2014
|
+
runOrchestrator: orchestratorMock,
|
|
2015
|
+
existingByHash: new Map(),
|
|
2016
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
2017
|
+
};
|
|
2018
|
+
const result = await compileLesson(pipeline3Lesson, 'system prompt', deps);
|
|
2019
|
+
expect(result.status).toBe('compiled');
|
|
2020
|
+
const actions = result.trace.map((e) => e.action);
|
|
2021
|
+
expect(actions).toEqual(['generate', 'verify', 'result']);
|
|
2022
|
+
// Pipeline 3 emits at layer 2 per the design doc reservation.
|
|
2023
|
+
expect(result.trace[0].layer).toBe(2);
|
|
2024
|
+
expect(result.trace[0].patternHash).toMatch(/^[0-9a-f]{16}$/);
|
|
2025
|
+
});
|
|
2026
|
+
it('security-context short-circuit emits a single result event with security-rule-rejected', async () => {
|
|
2027
|
+
const securityLesson = {
|
|
2028
|
+
index: 0,
|
|
2029
|
+
heading: 'No shell injection',
|
|
2030
|
+
body: 'Never pass user input to spawn.',
|
|
2031
|
+
hash: 'h-trace-security-short-circuit',
|
|
2032
|
+
};
|
|
2033
|
+
const orchestratorMock = vi.fn();
|
|
2034
|
+
const deps = {
|
|
2035
|
+
parseCompilerResponse: vi.fn(),
|
|
2036
|
+
runOrchestrator: orchestratorMock,
|
|
2037
|
+
existingByHash: new Map(),
|
|
2038
|
+
callbacks: { onWarn: vi.fn(), onDim: vi.fn() },
|
|
2039
|
+
securityContext: true,
|
|
2040
|
+
};
|
|
2041
|
+
const result = await compileLesson(securityLesson, 'system prompt', deps);
|
|
2042
|
+
expect(result.status).toBe('skipped');
|
|
2043
|
+
expect(result.trace).toHaveLength(1);
|
|
2044
|
+
expect(result.trace[0]).toMatchObject({
|
|
2045
|
+
layer: 3,
|
|
2046
|
+
action: 'result',
|
|
2047
|
+
outcome: 'skipped',
|
|
2048
|
+
reasonCode: 'security-rule-rejected',
|
|
2049
|
+
});
|
|
2050
|
+
expect(orchestratorMock).not.toHaveBeenCalled();
|
|
2051
|
+
});
|
|
2052
|
+
});
|
|
1581
2053
|
//# sourceMappingURL=compile-lesson.test.js.map
|