principles-disciple 1.28.0 → 1.28.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/openclaw.plugin.json +4 -4
  2. package/package.json +4 -4
  3. package/scripts/validate-live-path.ts +18 -18
  4. package/src/commands/context.ts +1 -0
  5. package/src/commands/disable-impl.ts +2 -0
  6. package/src/commands/evolution-status.ts +2 -0
  7. package/src/commands/focus.ts +2 -0
  8. package/src/commands/nocturnal-train.ts +4 -6
  9. package/src/commands/pain.ts +9 -11
  10. package/src/commands/pd-reflect.ts +1 -1
  11. package/src/commands/principle-rollback.ts +1 -0
  12. package/src/commands/rollback-impl.ts +1 -0
  13. package/src/core/adaptive-thresholds.ts +1 -0
  14. package/src/core/bootstrap-rules.ts +3 -3
  15. package/src/core/dictionary.ts +1 -0
  16. package/src/core/empathy-keyword-matcher.ts +1 -0
  17. package/src/core/event-log.ts +2 -0
  18. package/src/core/evolution-engine.ts +1 -0
  19. package/src/core/external-training-contract.ts +1 -0
  20. package/src/core/focus-history.ts +3 -0
  21. package/src/core/init.ts +1 -0
  22. package/src/core/merge-gate-audit.ts +1 -1
  23. package/src/core/nocturnal-arbiter.ts +3 -0
  24. package/src/core/nocturnal-candidate-scoring.ts +131 -0
  25. package/src/core/nocturnal-compliance.ts +1 -0
  26. package/src/core/nocturnal-dataset.ts +1 -0
  27. package/src/core/nocturnal-executability.ts +1 -0
  28. package/src/core/nocturnal-reasoning-deriver.ts +338 -0
  29. package/src/core/nocturnal-rule-implementation-validator.ts +1 -0
  30. package/src/core/nocturnal-trinity.ts +457 -18
  31. package/src/core/pain-context-extractor.ts +2 -3
  32. package/src/core/pain.ts +1 -0
  33. package/src/core/pd-task-reconciler.ts +1 -0
  34. package/src/core/pd-task-service.ts +1 -0
  35. package/src/core/principle-internalization/deprecated-readiness.ts +1 -0
  36. package/src/core/principle-internalization/principle-lifecycle-service.ts +1 -0
  37. package/src/core/principle-tree-migration.ts +3 -4
  38. package/src/core/replay-engine.ts +4 -0
  39. package/src/core/risk-calculator.ts +1 -0
  40. package/src/core/rule-host.ts +2 -0
  41. package/src/core/session-tracker.ts +2 -0
  42. package/src/core/thinking-models.ts +1 -0
  43. package/src/core/thinking-os-parser.ts +3 -3
  44. package/src/core/trajectory.ts +4 -0
  45. package/src/hooks/bash-risk.ts +1 -1
  46. package/src/hooks/gfi-gate.ts +1 -1
  47. package/src/hooks/lifecycle-routing.ts +1 -0
  48. package/src/hooks/pain.ts +2 -1
  49. package/src/hooks/prompt.ts +37 -2
  50. package/src/hooks/subagent.ts +1 -1
  51. package/src/hooks/trajectory-collector.ts +1 -0
  52. package/src/http/principles-console-route.ts +2 -0
  53. package/src/index.ts +1 -1
  54. package/src/service/central-database.ts +2 -0
  55. package/src/service/central-sync-service.ts +1 -0
  56. package/src/service/control-ui-query-service.ts +2 -0
  57. package/src/service/event-log-auditor.ts +2 -0
  58. package/src/service/evolution-worker.ts +2 -1
  59. package/src/service/health-query-service.ts +20 -6
  60. package/src/service/nocturnal-runtime.ts +4 -0
  61. package/src/service/runtime-summary-service.ts +5 -0
  62. package/src/service/subagent-workflow/deep-reflect-workflow-manager.ts +1 -0
  63. package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +2 -1
  64. package/src/service/subagent-workflow/subagent-error-utils.ts +1 -0
  65. package/src/service/subagent-workflow/workflow-manager-base.ts +1 -0
  66. package/src/tools/critique-prompt.ts +1 -0
  67. package/src/utils/io.ts +1 -0
  68. package/tests/core/nocturnal-candidate-scoring.test.ts +132 -0
  69. package/tests/core/nocturnal-reasoning-deriver.test.ts +372 -0
  70. package/tests/core/nocturnal-trinity.test.ts +791 -0
@@ -7,11 +7,22 @@ import {
7
7
  DEFAULT_TRINITY_CONFIG,
8
8
  OpenClawTrinityRuntimeAdapter,
9
9
  TrinityRuntimeContractError,
10
+ NOCTURNAL_DREAMER_PROMPT,
11
+ NOCTURNAL_PHILOSOPHER_PROMPT,
12
+ formatReasoningContext,
13
+ invokeStubDreamer,
14
+ invokeStubPhilosopher,
10
15
  type TrinityConfig,
11
16
  type DreamerOutput,
17
+ type DreamerCandidate,
12
18
  type PhilosopherOutput,
19
+ type PhilosopherJudgment,
13
20
  type TrinityDraftArtifact,
14
21
  type TrinityRuntimeAdapter,
22
+ type TrinityTelemetry,
23
+ type RejectedAnalysis,
24
+ type ChosenJustification,
25
+ type ContrastiveAnalysis,
15
26
  } from '../../src/core/nocturnal-trinity.js';
16
27
  import {
17
28
  validateDreamerOutput,
@@ -1024,3 +1035,783 @@ describe('runTrinityAsync — useStubs=true uses synchronous stubs', () => {
1024
1035
  expect(adapter.invokeScribe).not.toHaveBeenCalled();
1025
1036
  });
1026
1037
  });
1038
+
1039
+ // ---------------------------------------------------------------------------
1040
+ // Tests: NOCTURNAL_DREAMER_PROMPT — strategic perspective requirements (Task 1)
1041
+ // ---------------------------------------------------------------------------
1042
+
1043
+ describe('NOCTURNAL_DREAMER_PROMPT — strategic perspective requirements', () => {
1044
+ it('contains "## Strategic Perspective Requirements" section', () => {
1045
+ expect(NOCTURNAL_DREAMER_PROMPT).toContain('## Strategic Perspective Requirements');
1046
+ });
1047
+
1048
+ it('mentions all three strategic perspectives', () => {
1049
+ expect(NOCTURNAL_DREAMER_PROMPT).toContain('conservative_fix');
1050
+ expect(NOCTURNAL_DREAMER_PROMPT).toContain('structural_improvement');
1051
+ expect(NOCTURNAL_DREAMER_PROMPT).toContain('paradigm_shift');
1052
+ });
1053
+
1054
+ it('contains ANTI-PATTERN warning', () => {
1055
+ expect(NOCTURNAL_DREAMER_PROMPT).toContain('ANTI-PATTERN');
1056
+ });
1057
+
1058
+ it('references riskLevel as required candidate field', () => {
1059
+ expect(NOCTURNAL_DREAMER_PROMPT).toContain('riskLevel');
1060
+ });
1061
+
1062
+ it('references strategicPerspective as required candidate field', () => {
1063
+ expect(NOCTURNAL_DREAMER_PROMPT).toContain('strategicPerspective');
1064
+ });
1065
+ });
1066
+
1067
+ // ---------------------------------------------------------------------------
1068
+ // Tests: DreamerCandidate interface — optional fields (Task 1)
1069
+ // ---------------------------------------------------------------------------
1070
+
1071
+ describe('DreamerCandidate interface — optional fields', () => {
1072
+ it('accepts a candidate with riskLevel and strategicPerspective', () => {
1073
+ const candidate: DreamerCandidate = {
1074
+ candidateIndex: 0,
1075
+ badDecision: 'Did something wrong',
1076
+ betterDecision: 'Do it right',
1077
+ rationale: 'Because the principle says so',
1078
+ confidence: 0.9,
1079
+ riskLevel: 'medium',
1080
+ strategicPerspective: 'structural_improvement',
1081
+ };
1082
+ expect(candidate.riskLevel).toBe('medium');
1083
+ expect(candidate.strategicPerspective).toBe('structural_improvement');
1084
+ });
1085
+
1086
+ it('accepts a candidate without riskLevel or strategicPerspective (backward compat)', () => {
1087
+ const candidate: DreamerCandidate = {
1088
+ candidateIndex: 0,
1089
+ badDecision: 'Did something wrong',
1090
+ betterDecision: 'Do it right',
1091
+ rationale: 'Because the principle says so',
1092
+ confidence: 0.9,
1093
+ };
1094
+ expect(candidate.riskLevel).toBeUndefined();
1095
+ expect(candidate.strategicPerspective).toBeUndefined();
1096
+ });
1097
+
1098
+ it('accepts all valid riskLevel values', () => {
1099
+ const levels: Array<'low' | 'medium' | 'high'> = ['low', 'medium', 'high'];
1100
+ for (const level of levels) {
1101
+ const candidate: DreamerCandidate = {
1102
+ candidateIndex: 0,
1103
+ badDecision: 'Wrong',
1104
+ betterDecision: 'Right',
1105
+ rationale: 'Because',
1106
+ confidence: 0.8,
1107
+ riskLevel: level,
1108
+ };
1109
+ expect(candidate.riskLevel).toBe(level);
1110
+ }
1111
+ });
1112
+
1113
+ it('accepts all valid strategicPerspective values', () => {
1114
+ const perspectives: Array<'conservative_fix' | 'structural_improvement' | 'paradigm_shift'> = [
1115
+ 'conservative_fix',
1116
+ 'structural_improvement',
1117
+ 'paradigm_shift',
1118
+ ];
1119
+ for (const perspective of perspectives) {
1120
+ const candidate: DreamerCandidate = {
1121
+ candidateIndex: 0,
1122
+ badDecision: 'Wrong',
1123
+ betterDecision: 'Right',
1124
+ rationale: 'Because',
1125
+ confidence: 0.8,
1126
+ strategicPerspective: perspective,
1127
+ };
1128
+ expect(candidate.strategicPerspective).toBe(perspective);
1129
+ }
1130
+ });
1131
+ });
1132
+
1133
+ // ---------------------------------------------------------------------------
1134
+ // Tests: buildDreamerPrompt — reasoning context injection (Task 2)
1135
+ // ---------------------------------------------------------------------------
1136
+
1137
+ describe('buildDreamerPrompt — reasoning context injection', () => {
1138
+ // Helper to create a minimal snapshot for reasoning context tests
1139
+ function makeReasoningSnapshot(overrides: {
1140
+ assistantTurns?: any[];
1141
+ toolCalls?: any[];
1142
+ userTurns?: any[];
1143
+ } = {}) {
1144
+ return {
1145
+ sessionId: 'session-reasoning-test',
1146
+ startedAt: '2026-04-13T00:00:00.000Z',
1147
+ updatedAt: '2026-04-13T00:05:00.000Z',
1148
+ assistantTurns: overrides.assistantTurns ?? [],
1149
+ userTurns: overrides.userTurns ?? [],
1150
+ toolCalls: overrides.toolCalls ?? [],
1151
+ painEvents: [],
1152
+ gateBlocks: [],
1153
+ stats: {
1154
+ failureCount: 0,
1155
+ totalPainEvents: 0,
1156
+ totalGateBlocks: 0,
1157
+ totalAssistantTurns: overrides.assistantTurns?.length ?? 0,
1158
+ totalToolCalls: overrides.toolCalls?.length ?? 0,
1159
+ },
1160
+ };
1161
+ }
1162
+
1163
+ it('injects ## Reasoning Context section when assistant turns have thinking content', () => {
1164
+ const snapshot = makeReasoningSnapshot({
1165
+ assistantTurns: [
1166
+ {
1167
+ turnIndex: 0,
1168
+ sanitizedText: '<thinking>I need to consider the implications carefully</thinking>',
1169
+ createdAt: '2026-04-13T00:01:00.000Z',
1170
+ },
1171
+ ],
1172
+ });
1173
+
1174
+ const result = formatReasoningContext(snapshot as any);
1175
+ expect(result).toContain('## Reasoning Context');
1176
+ });
1177
+
1178
+ it('includes uncertainty markers in reasoning context', () => {
1179
+ const snapshot = makeReasoningSnapshot({
1180
+ assistantTurns: [
1181
+ {
1182
+ turnIndex: 0,
1183
+ sanitizedText: 'let me verify this first before proceeding with the change',
1184
+ createdAt: '2026-04-13T00:01:00.000Z',
1185
+ },
1186
+ ],
1187
+ });
1188
+
1189
+ const result = formatReasoningContext(snapshot as any);
1190
+ expect(result).toContain('Uncertainty detected');
1191
+ });
1192
+
1193
+ it('includes confidence signal when not high', () => {
1194
+ const snapshot = makeReasoningSnapshot({
1195
+ assistantTurns: [
1196
+ {
1197
+ turnIndex: 0,
1198
+ sanitizedText: 'I should probably check this more thoroughly before continuing',
1199
+ createdAt: '2026-04-13T00:01:00.000Z',
1200
+ },
1201
+ ],
1202
+ });
1203
+
1204
+ const result = formatReasoningContext(snapshot as any);
1205
+ // Low or medium confidence should be shown
1206
+ expect(result).toMatch(/Confidence:\s*(low|medium)/);
1207
+ });
1208
+
1209
+ it('includes contextual factors when present', () => {
1210
+ const snapshot = makeReasoningSnapshot({
1211
+ assistantTurns: [],
1212
+ toolCalls: [
1213
+ { toolName: 'Read', outcome: 'success', createdAt: '2026-04-13T00:01:00.000Z' },
1214
+ { toolName: 'Edit', outcome: 'success', createdAt: '2026-04-13T00:02:00.000Z' },
1215
+ ],
1216
+ });
1217
+
1218
+ const result = formatReasoningContext(snapshot as any);
1219
+ expect(result).toContain('File structure explored');
1220
+ });
1221
+
1222
+ it('omits ## Reasoning Context when no reasoning signals exist', () => {
1223
+ const snapshot = makeReasoningSnapshot({
1224
+ assistantTurns: [],
1225
+ toolCalls: [
1226
+ { toolName: 'Edit', outcome: 'success', createdAt: '2026-04-13T00:01:00.000Z' },
1227
+ ],
1228
+ });
1229
+
1230
+ const result = formatReasoningContext(snapshot as any);
1231
+ expect(result).toBeNull();
1232
+ });
1233
+
1234
+ it('does not inject decisionPoints', () => {
1235
+ const snapshot = makeReasoningSnapshot({
1236
+ assistantTurns: [
1237
+ {
1238
+ turnIndex: 0,
1239
+ sanitizedText: '<thinking>some thought</thinking>',
1240
+ createdAt: '2026-04-13T00:01:00.000Z',
1241
+ },
1242
+ ],
1243
+ });
1244
+
1245
+ const result = formatReasoningContext(snapshot as any);
1246
+ expect(result).not.toContain('decisionPoint');
1247
+ expect(result).not.toContain('DecisionPoint');
1248
+ });
1249
+ });
1250
+
1251
+ // ---------------------------------------------------------------------------
1252
+ // Tests: invokeStubDreamer — risk level and perspective mapping (D-07)
1253
+ // ---------------------------------------------------------------------------
1254
+
1255
+ describe('invokeStubDreamer — risk level and perspective mapping (D-07)', () => {
1256
+ it('gateBlocks candidates get conservative_fix/low', () => {
1257
+ const snapshot = makeSnapshot({ totalGateBlocks: 2 });
1258
+ const output = invokeStubDreamer(snapshot as any, 'T-03', 3);
1259
+ expect(output.valid).toBe(true);
1260
+ expect(output.candidates.length).toBeGreaterThan(0);
1261
+ for (const candidate of output.candidates) {
1262
+ expect(candidate.riskLevel).toBe('low');
1263
+ expect(candidate.strategicPerspective).toBe('conservative_fix');
1264
+ }
1265
+ });
1266
+
1267
+ it('pain candidates get structural_improvement/medium', () => {
1268
+ const snapshot = makeSnapshot({ totalPainEvents: 3 });
1269
+ const output = invokeStubDreamer(snapshot as any, 'T-08', 3);
1270
+ expect(output.valid).toBe(true);
1271
+ expect(output.candidates.length).toBeGreaterThan(0);
1272
+ for (const candidate of output.candidates) {
1273
+ expect(candidate.riskLevel).toBe('medium');
1274
+ expect(candidate.strategicPerspective).toBe('structural_improvement');
1275
+ }
1276
+ });
1277
+
1278
+ it('failure candidates get paradigm_shift/high', () => {
1279
+ const snapshot = makeSnapshot({ failureCount: 2 });
1280
+ const output = invokeStubDreamer(snapshot as any, 'T-08', 3);
1281
+ expect(output.valid).toBe(true);
1282
+ expect(output.candidates.length).toBeGreaterThan(0);
1283
+ for (const candidate of output.candidates) {
1284
+ expect(candidate.riskLevel).toBe('high');
1285
+ expect(candidate.strategicPerspective).toBe('paradigm_shift');
1286
+ }
1287
+ });
1288
+ });
1289
+
1290
+ // ---------------------------------------------------------------------------
1291
+ // Tests: runTrinity — diversity telemetry (DIVER-04)
1292
+ // ---------------------------------------------------------------------------
1293
+
1294
+ describe('runTrinity — diversity telemetry (DIVER-04)', () => {
1295
+ it('emits diversityCheckPassed=false when stub candidates all have same risk level', () => {
1296
+ // Failure signal produces all paradigm_shift/high candidates → not diverse
1297
+ const snapshot = makeSnapshot({ failureCount: 2 });
1298
+ const config: TrinityConfig = {
1299
+ useTrinity: true,
1300
+ maxCandidates: 3,
1301
+ useStubs: true,
1302
+ };
1303
+
1304
+ const result = runTrinity({ snapshot: snapshot as any, principleId: 'T-08', config });
1305
+
1306
+ expect(result.success).toBe(true);
1307
+ expect(result.telemetry.diversityCheckPassed).toBe(false);
1308
+ });
1309
+
1310
+ it('emits candidateRiskLevels array matching stub mapping', () => {
1311
+ const snapshot = makeSnapshot({ failureCount: 2 });
1312
+ const config: TrinityConfig = {
1313
+ useTrinity: true,
1314
+ maxCandidates: 3,
1315
+ useStubs: true,
1316
+ };
1317
+
1318
+ const result = runTrinity({ snapshot: snapshot as any, principleId: 'T-08', config });
1319
+
1320
+ expect(result.success).toBe(true);
1321
+ expect(result.telemetry.candidateRiskLevels).toBeDefined();
1322
+ expect(result.telemetry.candidateRiskLevels!.length).toBeGreaterThan(0);
1323
+ // All failure stub candidates should be 'high'
1324
+ for (const level of result.telemetry.candidateRiskLevels!) {
1325
+ expect(level).toBe('high');
1326
+ }
1327
+ });
1328
+
1329
+ it('pipeline completes even when diversity check fails (soft enforcement)', () => {
1330
+ // Failure signal: all candidates have same risk → diversity fails
1331
+ const snapshot = makeSnapshot({ failureCount: 2 });
1332
+ const config: TrinityConfig = {
1333
+ useTrinity: true,
1334
+ maxCandidates: 3,
1335
+ useStubs: true,
1336
+ };
1337
+
1338
+ const result = runTrinity({ snapshot: snapshot as any, principleId: 'T-08', config });
1339
+
1340
+ expect(result.telemetry.diversityCheckPassed).toBe(false);
1341
+ expect(result.success).toBe(true);
1342
+ expect(result.artifact).toBeDefined();
1343
+ });
1344
+ });
1345
+
1346
+ // ---------------------------------------------------------------------------
1347
+ // Tests: TrinityTelemetry — diversity fields
1348
+ // ---------------------------------------------------------------------------
1349
+
1350
+ describe('TrinityTelemetry — diversity fields', () => {
1351
+ it('accepts optional diversityCheckPassed field', () => {
1352
+ const telemetry: TrinityTelemetry = {
1353
+ chainMode: 'trinity',
1354
+ usedStubs: true,
1355
+ dreamerPassed: true,
1356
+ philosopherPassed: true,
1357
+ scribePassed: true,
1358
+ candidateCount: 2,
1359
+ selectedCandidateIndex: 0,
1360
+ stageFailures: [],
1361
+ diversityCheckPassed: true,
1362
+ };
1363
+ expect(telemetry.diversityCheckPassed).toBe(true);
1364
+ });
1365
+
1366
+ it('accepts optional candidateRiskLevels field', () => {
1367
+ const telemetry: TrinityTelemetry = {
1368
+ chainMode: 'trinity',
1369
+ usedStubs: true,
1370
+ dreamerPassed: true,
1371
+ philosopherPassed: true,
1372
+ scribePassed: true,
1373
+ candidateCount: 2,
1374
+ selectedCandidateIndex: 0,
1375
+ stageFailures: [],
1376
+ candidateRiskLevels: ['low', 'high'],
1377
+ };
1378
+ expect(telemetry.candidateRiskLevels).toEqual(['low', 'high']);
1379
+ });
1380
+ });
1381
+
1382
+ // ---------------------------------------------------------------------------
1383
+ // Tests: Philosopher 6D Evaluation (PHILO-01)
1384
+ // ---------------------------------------------------------------------------
1385
+
1386
+ describe('Philosopher 6D Evaluation (PHILO-01)', () => {
1387
+ it('NOCTURNAL_PHILOSOPHER_PROMPT contains 6 dimensions with calibrated weights', () => {
1388
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('Safety Impact');
1389
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('UX Impact');
1390
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('(weight: 0.20)'); // Principle Alignment
1391
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('(weight: 0.15)'); // Specificity
1392
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('(weight: 0.15)'); // Actionability
1393
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('(weight: 0.15)'); // Executability
1394
+ });
1395
+
1396
+ it('prompt output format includes scores and risks objects', () => {
1397
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('"scores"');
1398
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('"principleAlignment"');
1399
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('"safetyImpact"');
1400
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('"uxImpact"');
1401
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('"risks"');
1402
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('"falsePositiveEstimate"');
1403
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('"implementationComplexity"');
1404
+ expect(NOCTURNAL_PHILOSOPHER_PROMPT).toContain('"breakingChangeRisk"');
1405
+ });
1406
+ });
1407
+
1408
+ // ---------------------------------------------------------------------------
1409
+ // Tests: Philosopher Risk Assessment (PHILO-02)
1410
+ // ---------------------------------------------------------------------------
1411
+
1412
+ describe('Philosopher Risk Assessment (PHILO-02)', () => {
1413
+ it('invokeStubPhilosopher produces risk assessment per candidate', () => {
1414
+ const dreamerOutput: DreamerOutput = {
1415
+ valid: true,
1416
+ candidates: [
1417
+ {
1418
+ candidateIndex: 0,
1419
+ badDecision: 'Did something wrong',
1420
+ betterDecision: 'Read the file before editing to verify content',
1421
+ rationale: 'A good rationale that explains why this is better',
1422
+ confidence: 0.9,
1423
+ riskLevel: 'low',
1424
+ strategicPerspective: 'conservative_fix',
1425
+ },
1426
+ {
1427
+ candidateIndex: 1,
1428
+ badDecision: 'Ignored error messages',
1429
+ betterDecision: 'Challenge the original approach entirely',
1430
+ rationale: 'A paradigm shift rationale for fundamentally different approach',
1431
+ confidence: 0.6,
1432
+ riskLevel: 'high',
1433
+ strategicPerspective: 'paradigm_shift',
1434
+ },
1435
+ ],
1436
+ generatedAt: new Date().toISOString(),
1437
+ };
1438
+ const result = invokeStubPhilosopher(dreamerOutput, 'T-01', makeSnapshot() as any);
1439
+ expect(result.valid).toBe(true);
1440
+ for (const j of result.judgments) {
1441
+ expect(j.risks).toBeDefined();
1442
+ expect(j.risks!.falsePositiveEstimate).toBeGreaterThanOrEqual(0);
1443
+ expect(j.risks!.falsePositiveEstimate).toBeLessThanOrEqual(1);
1444
+ expect(['low', 'medium', 'high']).toContain(j.risks!.implementationComplexity);
1445
+ expect(typeof j.risks!.breakingChangeRisk).toBe('boolean');
1446
+ }
1447
+ });
1448
+ });
1449
+
1450
+ // ---------------------------------------------------------------------------
1451
+ // Tests: Philosopher Backward Compatibility (PHILO-03)
1452
+ // ---------------------------------------------------------------------------
1453
+
1454
+ describe('Philosopher Backward Compatibility (PHILO-03)', () => {
1455
+ it('PhilosopherJudgment without scores/risks is valid', () => {
1456
+ const judgment: PhilosopherJudgment = {
1457
+ candidateIndex: 0,
1458
+ critique: 'test',
1459
+ principleAligned: true,
1460
+ score: 0.8,
1461
+ rank: 1,
1462
+ };
1463
+ expect(judgment.score).toBe(0.8);
1464
+ expect(judgment.scores).toBeUndefined();
1465
+ expect(judgment.risks).toBeUndefined();
1466
+ });
1467
+
1468
+ it('runTrinity produces output with 6D scores when candidates have strategicPerspective', () => {
1469
+ const snapshot = makeSnapshot({ failureCount: 2, totalPainEvents: 1 });
1470
+ const config: TrinityConfig = {
1471
+ useTrinity: true,
1472
+ maxCandidates: 3,
1473
+ useStubs: true,
1474
+ };
1475
+
1476
+ const result = runTrinity({ snapshot: snapshot as any, principleId: 'T-08', config });
1477
+ expect(result.success).toBe(true);
1478
+ expect(result.artifact).toBeDefined();
1479
+
1480
+ // The stub philosopher should produce 6D scores for stub candidates
1481
+ // (stub dreamer assigns strategicPerspective based on principleId)
1482
+ if (result.telemetry.philosopher6D) {
1483
+ const avgScores = result.telemetry.philosopher6D.avgScores;
1484
+ expect(typeof avgScores.principleAlignment).toBe('number');
1485
+ expect(typeof avgScores.specificity).toBe('number');
1486
+ expect(typeof avgScores.actionability).toBe('number');
1487
+ expect(typeof avgScores.executability).toBe('number');
1488
+ expect(typeof avgScores.safetyImpact).toBe('number');
1489
+ expect(typeof avgScores.uxImpact).toBe('number');
1490
+ }
1491
+ });
1492
+ });
1493
+
1494
+ // ---------------------------------------------------------------------------
1495
+ // Tests: Stub Philosopher 6D Scoring (D-09)
1496
+ // ---------------------------------------------------------------------------
1497
+
1498
+ describe('Stub Philosopher 6D Scoring (D-09)', () => {
1499
+ it('conservative_fix candidates get high principleAlignment and low risk', () => {
1500
+ const dreamerOutput: DreamerOutput = {
1501
+ valid: true,
1502
+ candidates: [
1503
+ {
1504
+ candidateIndex: 0,
1505
+ badDecision: 'Did something wrong',
1506
+ betterDecision: 'Read the file before editing to verify current content',
1507
+ rationale: 'Following T-01 requires verifying content before making changes',
1508
+ confidence: 0.9,
1509
+ riskLevel: 'low',
1510
+ strategicPerspective: 'conservative_fix',
1511
+ },
1512
+ ],
1513
+ generatedAt: new Date().toISOString(),
1514
+ };
1515
+ const result = invokeStubPhilosopher(dreamerOutput, 'T-01', makeSnapshot() as any);
1516
+ expect(result.valid).toBe(true);
1517
+ const j = result.judgments[0];
1518
+ expect(j.scores).toBeDefined();
1519
+ expect(j.scores!.principleAlignment).toBeGreaterThanOrEqual(0.9);
1520
+ expect(j.scores!.safetyImpact).toBeGreaterThanOrEqual(0.9);
1521
+ expect(j.risks).toBeDefined();
1522
+ expect(j.risks!.breakingChangeRisk).toBe(false);
1523
+ expect(j.risks!.implementationComplexity).toBe('low');
1524
+ });
1525
+
1526
+ it('paradigm_shift candidates get high breakingChangeRisk', () => {
1527
+ const dreamerOutput: DreamerOutput = {
1528
+ valid: true,
1529
+ candidates: [
1530
+ {
1531
+ candidateIndex: 0,
1532
+ badDecision: 'Ignored all errors',
1533
+ betterDecision: 'Challenge the entire approach and redesign from scratch',
1534
+ rationale: 'A paradigm shift rationale for a fundamentally different approach',
1535
+ confidence: 0.5,
1536
+ riskLevel: 'high',
1537
+ strategicPerspective: 'paradigm_shift',
1538
+ },
1539
+ ],
1540
+ generatedAt: new Date().toISOString(),
1541
+ };
1542
+ const result = invokeStubPhilosopher(dreamerOutput, 'T-08', makeSnapshot() as any);
1543
+ expect(result.valid).toBe(true);
1544
+ const j = result.judgments[0];
1545
+ expect(j.scores).toBeDefined();
1546
+ expect(j.scores!.safetyImpact).toBeLessThan(0.5);
1547
+ expect(j.risks).toBeDefined();
1548
+ expect(j.risks!.breakingChangeRisk).toBe(true);
1549
+ expect(j.risks!.implementationComplexity).toBe('high');
1550
+ });
1551
+
1552
+ it('structural_improvement candidates get medium across all dimensions', () => {
1553
+ const dreamerOutput: DreamerOutput = {
1554
+ valid: true,
1555
+ candidates: [
1556
+ {
1557
+ candidateIndex: 0,
1558
+ badDecision: 'Rushed through steps',
1559
+ betterDecision: 'Reorder operations and introduce an intermediate checkpoint',
1560
+ rationale: 'Structural improvement rationale to reorder operations properly',
1561
+ confidence: 0.7,
1562
+ riskLevel: 'medium',
1563
+ strategicPerspective: 'structural_improvement',
1564
+ },
1565
+ ],
1566
+ generatedAt: new Date().toISOString(),
1567
+ };
1568
+ const result = invokeStubPhilosopher(dreamerOutput, 'T-03', makeSnapshot() as any);
1569
+ expect(result.valid).toBe(true);
1570
+ const j = result.judgments[0];
1571
+ expect(j.scores).toBeDefined();
1572
+ // Medium scores should be between conservative and paradigm
1573
+ expect(j.scores!.principleAlignment).toBeGreaterThanOrEqual(0.7);
1574
+ expect(j.scores!.principleAlignment).toBeLessThanOrEqual(0.8);
1575
+ expect(j.risks).toBeDefined();
1576
+ expect(j.risks!.breakingChangeRisk).toBe(false);
1577
+ expect(j.risks!.implementationComplexity).toBe('medium');
1578
+ });
1579
+ });
1580
+
1581
+ // ---------------------------------------------------------------------------
1582
+ // Tests: TrinityTelemetry — philosopher6D field
1583
+ // ---------------------------------------------------------------------------
1584
+
1585
+ describe('TrinityTelemetry — philosopher6D field', () => {
1586
+ it('accepts optional philosopher6D field', () => {
1587
+ const telemetry: TrinityTelemetry = {
1588
+ chainMode: 'trinity',
1589
+ usedStubs: true,
1590
+ dreamerPassed: true,
1591
+ philosopherPassed: true,
1592
+ scribePassed: true,
1593
+ candidateCount: 2,
1594
+ selectedCandidateIndex: 0,
1595
+ stageFailures: [],
1596
+ philosopher6D: {
1597
+ avgScores: {
1598
+ principleAlignment: 0.85,
1599
+ specificity: 0.75,
1600
+ actionability: 0.8,
1601
+ executability: 0.78,
1602
+ safetyImpact: 0.7,
1603
+ uxImpact: 0.72,
1604
+ },
1605
+ highRiskCount: 1,
1606
+ },
1607
+ };
1608
+ expect(telemetry.philosopher6D).toBeDefined();
1609
+ expect(telemetry.philosopher6D!.avgScores.principleAlignment).toBe(0.85);
1610
+ expect(telemetry.philosopher6D!.highRiskCount).toBe(1);
1611
+ });
1612
+ });
1613
+
1614
+ // ---------------------------------------------------------------------------
1615
+ // Tests: Scribe Contrastive Analysis (SCRIBE-01, SCRIBE-02, SCRIBE-03)
1616
+ // ---------------------------------------------------------------------------
1617
+
1618
+ describe('Scribe Contrastive Analysis (SCRIBE-01, SCRIBE-02, SCRIBE-03)', () => {
1619
+ function makeValidArtifact(overrides: Record<string, unknown> = {}): TrinityDraftArtifact {
1620
+ return {
1621
+ selectedCandidateIndex: 0,
1622
+ badDecision: 'Did something wrong',
1623
+ betterDecision: 'Do it right',
1624
+ rationale: 'Because the principle says so and this is the right approach',
1625
+ sessionId: 'session-test-123',
1626
+ principleId: 'T-01',
1627
+ sourceSnapshotRef: 'snapshot-test-001',
1628
+ telemetry: {
1629
+ chainMode: 'trinity',
1630
+ usedStubs: false,
1631
+ dreamerPassed: true,
1632
+ philosopherPassed: true,
1633
+ scribePassed: true,
1634
+ candidateCount: 2,
1635
+ selectedCandidateIndex: 0,
1636
+ stageFailures: [],
1637
+ },
1638
+ ...overrides,
1639
+ };
1640
+ }
1641
+
1642
+ it('TrinityDraftArtifact accepts optional rejectedAnalysis fields (SCRIBE-01)', () => {
1643
+ const artifact = makeValidArtifact({
1644
+ rejectedAnalysis: {
1645
+ whyRejected: 'Lower alignment score',
1646
+ warningSignals: ['missed pain signal', 'ignored gate block'],
1647
+ correctiveThinking: 'Should have verified the routing state before proceeding',
1648
+ },
1649
+ } as Record<string, unknown>);
1650
+ expect(artifact.rejectedAnalysis).toBeDefined();
1651
+ expect(artifact.rejectedAnalysis!.whyRejected).toBe('Lower alignment score');
1652
+ expect(artifact.rejectedAnalysis!.warningSignals).toHaveLength(2);
1653
+ expect(artifact.rejectedAnalysis!.correctiveThinking).toContain('Should have');
1654
+ });
1655
+
1656
+ it('TrinityDraftArtifact accepts optional chosenJustification fields (SCRIBE-02)', () => {
1657
+ const artifact = makeValidArtifact({
1658
+ chosenJustification: {
1659
+ whyChosen: 'Highest 6D composite score and low breakingChangeRisk',
1660
+ keyInsights: ['Verify routing state before file operations', 'Check pain signals early'],
1661
+ limitations: ['Does not apply when session has no pain history', 'Less relevant for conservative fixes'],
1662
+ },
1663
+ } as Record<string, unknown>);
1664
+ expect(artifact.chosenJustification).toBeDefined();
1665
+ expect(artifact.chosenJustification!.whyChosen).toContain('Highest');
1666
+ expect(artifact.chosenJustification!.keyInsights).toHaveLength(2);
1667
+ expect(artifact.chosenJustification!.limitations).toHaveLength(2);
1668
+ });
1669
+
1670
+ it('TrinityDraftArtifact accepts optional contrastiveAnalysis fields (SCRIBE-03)', () => {
1671
+ const artifact = makeValidArtifact({
1672
+ contrastiveAnalysis: {
1673
+ criticalDifference: 'Winner checked routing state; loser proceeded without verification',
1674
+ decisionTrigger: 'When session has pain events and gate blocks, verify infrastructure before file operations',
1675
+ preventionStrategy: 'Add a pre-flight check: read the routing status and confirm no pending failures',
1676
+ },
1677
+ } as Record<string, unknown>);
1678
+ expect(artifact.contrastiveAnalysis).toBeDefined();
1679
+ expect(artifact.contrastiveAnalysis!.criticalDifference).toContain('routing state');
1680
+ expect(artifact.contrastiveAnalysis!.decisionTrigger).toContain('When');
1681
+ expect(artifact.contrastiveAnalysis!.preventionStrategy).toContain('pre-flight');
1682
+ });
1683
+
1684
+ it('validateDraftArtifact passes when all three analysis sections are present', () => {
1685
+ const artifact = makeValidArtifact({
1686
+ rejectedAnalysis: {
1687
+ whyRejected: 'Lower score',
1688
+ warningSignals: ['missed signal'],
1689
+ correctiveThinking: 'Should have checked',
1690
+ },
1691
+ chosenJustification: {
1692
+ whyChosen: 'Best score',
1693
+ keyInsights: ['insight 1'],
1694
+ limitations: ['limitation 1'],
1695
+ },
1696
+ contrastiveAnalysis: {
1697
+ criticalDifference: 'key difference',
1698
+ decisionTrigger: 'When X, do Y',
1699
+ preventionStrategy: 'avoid the rejected path',
1700
+ },
1701
+ } as Record<string, unknown>);
1702
+ const result = validateDraftArtifact(artifact);
1703
+ expect(result.valid).toBe(true);
1704
+ expect(result.failures).toHaveLength(0);
1705
+ });
1706
+
1707
+ it('RejectedAnalysis interface accepts all required fields', () => {
1708
+ const analysis: RejectedAnalysis = {
1709
+ whyRejected: 'test reason',
1710
+ warningSignals: ['signal 1', 'signal 2'],
1711
+ correctiveThinking: 'correct path',
1712
+ };
1713
+ expect(analysis.whyRejected).toBe('test reason');
1714
+ expect(analysis.warningSignals).toHaveLength(2);
1715
+ expect(analysis.correctiveThinking).toBe('correct path');
1716
+ });
1717
+
1718
+ it('ChosenJustification interface accepts all required fields', () => {
1719
+ const justification: ChosenJustification = {
1720
+ whyChosen: 'test reason',
1721
+ keyInsights: ['insight 1', 'insight 2', 'insight 3'],
1722
+ limitations: ['limitation 1'],
1723
+ };
1724
+ expect(justification.whyChosen).toBe('test reason');
1725
+ expect(justification.keyInsights).toHaveLength(3);
1726
+ expect(justification.limitations).toHaveLength(1);
1727
+ });
1728
+
1729
+ it('ContrastiveAnalysis interface accepts all required fields', () => {
1730
+ const analysis: ContrastiveAnalysis = {
1731
+ criticalDifference: 'key insight',
1732
+ decisionTrigger: 'When X, do Y',
1733
+ preventionStrategy: 'avoid the rejected path',
1734
+ };
1735
+ expect(analysis.criticalDifference).toBe('key insight');
1736
+ expect(analysis.decisionTrigger).toBe('When X, do Y');
1737
+ expect(analysis.preventionStrategy).toBe('avoid the rejected path');
1738
+ });
1739
+ });
1740
+
1741
+ // ---------------------------------------------------------------------------
1742
+ // Tests: Scribe Backward Compatibility (SCRIBE-04)
1743
+ // ---------------------------------------------------------------------------
1744
+
1745
+ describe('Scribe Backward Compatibility (SCRIBE-04)', () => {
1746
+ function makeValidArtifact(): TrinityDraftArtifact {
1747
+ return {
1748
+ selectedCandidateIndex: 0,
1749
+ badDecision: 'Did something wrong',
1750
+ betterDecision: 'Do it right',
1751
+ rationale: 'Because the principle says so and this is the right approach',
1752
+ sessionId: 'session-test-123',
1753
+ principleId: 'T-01',
1754
+ sourceSnapshotRef: 'snapshot-test-001',
1755
+ telemetry: {
1756
+ chainMode: 'trinity',
1757
+ usedStubs: false,
1758
+ dreamerPassed: true,
1759
+ philosopherPassed: true,
1760
+ scribePassed: true,
1761
+ candidateCount: 2,
1762
+ selectedCandidateIndex: 0,
1763
+ stageFailures: [],
1764
+ },
1765
+ };
1766
+ }
1767
+
1768
+ it('TrinityDraftArtifact without contrastiveAnalysis fields is valid', () => {
1769
+ const artifact = makeValidArtifact();
1770
+ expect(artifact.contrastiveAnalysis).toBeUndefined();
1771
+ expect(artifact.rejectedAnalysis).toBeUndefined();
1772
+ expect(artifact.chosenJustification).toBeUndefined();
1773
+ const result = validateDraftArtifact(artifact);
1774
+ expect(result.valid).toBe(true);
1775
+ expect(result.failures).toHaveLength(0);
1776
+ });
1777
+
1778
+ it('artifact without new fields produces identical output via draftToArtifact', () => {
1779
+ const artifact = makeValidArtifact();
1780
+ const nocturnalArtifact = draftToArtifact(artifact);
1781
+ expect(nocturnalArtifact.badDecision).toBe('Did something wrong');
1782
+ expect(nocturnalArtifact.betterDecision).toBe('Do it right');
1783
+ expect(nocturnalArtifact.principleId).toBe('T-01');
1784
+ });
1785
+
1786
+ it('runTrinity produces artifact without contrastiveAnalysis when useStubs=true', () => {
1787
+ const snapshot = {
1788
+ sessionId: 'session-backward-compat',
1789
+ startedAt: '2026-04-13T00:00:00.000Z',
1790
+ updatedAt: '2026-04-13T00:05:00.000Z',
1791
+ assistantTurns: [],
1792
+ userTurns: [],
1793
+ toolCalls: [],
1794
+ painEvents: [],
1795
+ gateBlocks: [],
1796
+ stats: {
1797
+ failureCount: 1,
1798
+ totalPainEvents: 0,
1799
+ totalGateBlocks: 0,
1800
+ totalAssistantTurns: 5,
1801
+ totalToolCalls: 10,
1802
+ },
1803
+ };
1804
+ const config: TrinityConfig = {
1805
+ useTrinity: true,
1806
+ maxCandidates: 3,
1807
+ useStubs: true,
1808
+ };
1809
+
1810
+ const result = runTrinity({ snapshot: snapshot as any, principleId: 'T-08', config });
1811
+ expect(result.success).toBe(true);
1812
+ expect(result.artifact).toBeDefined();
1813
+ expect(result.artifact!.contrastiveAnalysis).toBeUndefined();
1814
+ expect(result.artifact!.rejectedAnalysis).toBeUndefined();
1815
+ expect(result.artifact!.chosenJustification).toBeUndefined();
1816
+ });
1817
+ });