@cleocode/core 2026.3.57 → 2026.3.59

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 (136) hide show
  1. package/dist/agents/agent-registry.d.ts +206 -0
  2. package/dist/agents/agent-registry.d.ts.map +1 -0
  3. package/dist/agents/agent-schema.d.ts.map +1 -1
  4. package/dist/agents/execution-learning.d.ts +223 -0
  5. package/dist/agents/execution-learning.d.ts.map +1 -0
  6. package/dist/agents/health-monitor.d.ts +161 -0
  7. package/dist/agents/health-monitor.d.ts.map +1 -0
  8. package/dist/agents/index.d.ts +4 -1
  9. package/dist/agents/index.d.ts.map +1 -1
  10. package/dist/agents/retry.d.ts +57 -4
  11. package/dist/agents/retry.d.ts.map +1 -1
  12. package/dist/backfill/index.d.ts +83 -0
  13. package/dist/backfill/index.d.ts.map +1 -0
  14. package/dist/bootstrap.d.ts +1 -1
  15. package/dist/config.d.ts +47 -0
  16. package/dist/config.d.ts.map +1 -1
  17. package/dist/index.d.ts +2 -1
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +6985 -5068
  20. package/dist/index.js.map +4 -4
  21. package/dist/intelligence/adaptive-validation.d.ts +151 -0
  22. package/dist/intelligence/adaptive-validation.d.ts.map +1 -0
  23. package/dist/intelligence/impact.d.ts +34 -1
  24. package/dist/intelligence/impact.d.ts.map +1 -1
  25. package/dist/intelligence/index.d.ts +7 -2
  26. package/dist/intelligence/index.d.ts.map +1 -1
  27. package/dist/intelligence/types.d.ts +60 -0
  28. package/dist/intelligence/types.d.ts.map +1 -1
  29. package/dist/internal.d.ts +8 -4
  30. package/dist/internal.d.ts.map +1 -1
  31. package/dist/lib/index.d.ts +10 -0
  32. package/dist/lib/index.d.ts.map +1 -0
  33. package/dist/lib/retry.d.ts +128 -0
  34. package/dist/lib/retry.d.ts.map +1 -0
  35. package/dist/nexus/sharing/index.d.ts +48 -2
  36. package/dist/nexus/sharing/index.d.ts.map +1 -1
  37. package/dist/sessions/session-enforcement.d.ts.map +1 -1
  38. package/dist/stats/index.d.ts +1 -0
  39. package/dist/stats/index.d.ts.map +1 -1
  40. package/dist/stats/workflow-telemetry.d.ts +89 -0
  41. package/dist/stats/workflow-telemetry.d.ts.map +1 -0
  42. package/dist/store/brain-schema.d.ts.map +1 -1
  43. package/dist/store/converters.d.ts.map +1 -1
  44. package/dist/store/cross-db-cleanup.d.ts +93 -0
  45. package/dist/store/cross-db-cleanup.d.ts.map +1 -0
  46. package/dist/store/db-helpers.d.ts.map +1 -1
  47. package/dist/store/migration-sqlite.d.ts.map +1 -1
  48. package/dist/store/sqlite-data-accessor.d.ts.map +1 -1
  49. package/dist/store/sqlite.d.ts.map +1 -1
  50. package/dist/store/task-store.d.ts.map +1 -1
  51. package/dist/store/tasks-schema.d.ts +18 -3
  52. package/dist/store/tasks-schema.d.ts.map +1 -1
  53. package/dist/store/validation-schemas.d.ts +32 -0
  54. package/dist/store/validation-schemas.d.ts.map +1 -1
  55. package/dist/tasks/add.d.ts +10 -1
  56. package/dist/tasks/add.d.ts.map +1 -1
  57. package/dist/tasks/complete.d.ts.map +1 -1
  58. package/dist/tasks/enforcement.d.ts +22 -0
  59. package/dist/tasks/enforcement.d.ts.map +1 -0
  60. package/dist/tasks/epic-enforcement.d.ts +199 -0
  61. package/dist/tasks/epic-enforcement.d.ts.map +1 -0
  62. package/dist/tasks/index.d.ts +1 -1
  63. package/dist/tasks/index.d.ts.map +1 -1
  64. package/dist/tasks/pipeline-stage.d.ts +181 -0
  65. package/dist/tasks/pipeline-stage.d.ts.map +1 -0
  66. package/dist/tasks/update.d.ts +2 -0
  67. package/dist/tasks/update.d.ts.map +1 -1
  68. package/migrations/drizzle-brain/20260321000001_t033-brain-indexes/migration.sql +12 -0
  69. package/migrations/drizzle-brain/20260321000001_t033-brain-indexes/snapshot.json +1232 -0
  70. package/migrations/drizzle-tasks/20260321000000_t033-connection-health/migration.sql +518 -0
  71. package/migrations/drizzle-tasks/20260321000000_t033-connection-health/snapshot.json +4312 -0
  72. package/migrations/drizzle-tasks/20260321000002_t060-pipeline-stage-binding/migration.sql +82 -0
  73. package/migrations/drizzle-tasks/20260321000002_t060-pipeline-stage-binding/snapshot.json +9 -0
  74. package/package.json +5 -5
  75. package/schemas/config.schema.json +37 -1547
  76. package/src/__tests__/sharing.test.ts +24 -0
  77. package/src/agents/__tests__/agent-registry.test.ts +351 -0
  78. package/src/agents/__tests__/execution-learning.test.ts +684 -0
  79. package/src/agents/__tests__/health-monitor.test.ts +332 -0
  80. package/src/agents/__tests__/registry.test.ts +30 -2
  81. package/src/agents/agent-registry.ts +394 -0
  82. package/src/agents/agent-schema.ts +5 -0
  83. package/src/agents/execution-learning.ts +675 -0
  84. package/src/agents/health-monitor.ts +279 -0
  85. package/src/agents/index.ts +37 -1
  86. package/src/agents/retry.ts +57 -4
  87. package/src/backfill/index.ts +309 -0
  88. package/src/bootstrap.ts +1 -1
  89. package/src/config.ts +126 -0
  90. package/src/index.ts +8 -1
  91. package/src/intelligence/__tests__/adaptive-validation.test.ts +694 -0
  92. package/src/intelligence/__tests__/impact.test.ts +165 -1
  93. package/src/intelligence/adaptive-validation.ts +764 -0
  94. package/src/intelligence/impact.ts +203 -0
  95. package/src/intelligence/index.ts +19 -0
  96. package/src/intelligence/types.ts +76 -0
  97. package/src/internal.ts +39 -0
  98. package/src/lib/__tests__/retry.test.ts +321 -0
  99. package/src/lib/index.ts +16 -0
  100. package/src/lib/retry.ts +224 -0
  101. package/src/lifecycle/__tests__/chain-store.test.ts +7 -0
  102. package/src/lifecycle/__tests__/tessera-engine.test.ts +52 -0
  103. package/src/nexus/sharing/index.ts +142 -2
  104. package/src/sessions/__tests__/session-edge-cases.test.ts +24 -1
  105. package/src/sessions/session-enforcement.ts +13 -2
  106. package/src/stats/index.ts +7 -0
  107. package/src/stats/workflow-telemetry.ts +502 -0
  108. package/src/store/__tests__/migration-safety.test.ts +3 -0
  109. package/src/store/__tests__/session-store.test.ts +132 -1
  110. package/src/store/__tests__/task-store.test.ts +22 -1
  111. package/src/store/__tests__/test-db-helper.ts +29 -2
  112. package/src/store/brain-schema.ts +4 -1
  113. package/src/store/converters.ts +2 -0
  114. package/src/store/cross-db-cleanup.ts +192 -0
  115. package/src/store/db-helpers.ts +2 -0
  116. package/src/store/migration-sqlite.ts +6 -0
  117. package/src/store/sqlite-data-accessor.ts +20 -28
  118. package/src/store/sqlite.ts +14 -2
  119. package/src/store/task-store.ts +6 -0
  120. package/src/store/tasks-schema.ts +59 -20
  121. package/src/tasks/__tests__/add.test.ts +16 -0
  122. package/src/tasks/__tests__/complete-unblocks.test.ts +10 -1
  123. package/src/tasks/__tests__/complete.test.ts +11 -2
  124. package/src/tasks/__tests__/epic-enforcement.test.ts +909 -0
  125. package/src/tasks/__tests__/minimal-test.test.ts +28 -0
  126. package/src/tasks/__tests__/pipeline-stage.test.ts +403 -0
  127. package/src/tasks/__tests__/update.test.ts +40 -6
  128. package/src/tasks/add.ts +128 -2
  129. package/src/tasks/complete.ts +29 -17
  130. package/src/tasks/enforcement.ts +127 -0
  131. package/src/tasks/epic-enforcement.ts +364 -0
  132. package/src/tasks/index.ts +1 -0
  133. package/src/tasks/pipeline-stage.ts +293 -0
  134. package/src/tasks/update.ts +62 -0
  135. package/templates/config.template.json +34 -111
  136. package/templates/global-config.template.json +24 -40
@@ -0,0 +1,694 @@
1
+ /**
2
+ * Unit tests for the Adaptive Validation module.
3
+ *
4
+ * Tests gate focus suggestions, confidence scoring, and prediction storage.
5
+ * All external dependencies are mocked.
6
+ *
7
+ * @task T035
8
+ * @epic T029
9
+ */
10
+
11
+ import type { Task, TaskVerification } from '@cleocode/contracts';
12
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
13
+ import type { BrainDataAccessor } from '../../store/brain-accessor.js';
14
+ import type {
15
+ BrainLearningRow,
16
+ BrainObservationRow,
17
+ BrainPatternRow,
18
+ } from '../../store/brain-schema.js';
19
+ import type { DataAccessor } from '../../store/data-accessor.js';
20
+ import {
21
+ predictAndStore,
22
+ scoreVerificationConfidence,
23
+ storePrediction,
24
+ suggestGateFocus,
25
+ } from '../adaptive-validation.js';
26
+
27
+ // ============================================================================
28
+ // Helpers
29
+ // ============================================================================
30
+
31
+ function makeTask(overrides: Partial<Task> & { id: string; title: string }): Task {
32
+ return {
33
+ status: 'pending',
34
+ priority: 'medium',
35
+ description: `Description for ${overrides.id}`,
36
+ createdAt: new Date().toISOString(),
37
+ labels: [],
38
+ depends: [],
39
+ ...overrides,
40
+ } as Task;
41
+ }
42
+
43
+ function makeVerification(overrides: Partial<TaskVerification> = {}): TaskVerification {
44
+ return {
45
+ passed: false,
46
+ round: 1,
47
+ gates: {},
48
+ lastAgent: null,
49
+ lastUpdated: null,
50
+ failureLog: [],
51
+ ...overrides,
52
+ };
53
+ }
54
+
55
+ function makePattern(overrides: Partial<BrainPatternRow> = {}): BrainPatternRow {
56
+ return {
57
+ id: `P-${Math.random().toString(36).slice(2, 10)}`,
58
+ type: 'failure',
59
+ pattern: 'test pattern',
60
+ context: 'test context',
61
+ frequency: 2,
62
+ successRate: 0.3,
63
+ impact: 'medium',
64
+ antiPattern: null,
65
+ mitigation: null,
66
+ examplesJson: '[]',
67
+ extractedAt: new Date().toISOString(),
68
+ updatedAt: null,
69
+ ...overrides,
70
+ };
71
+ }
72
+
73
+ function mockTaskAccessor(tasks: Task[]): DataAccessor {
74
+ return {
75
+ loadSingleTask: vi
76
+ .fn()
77
+ .mockImplementation((id: string) => Promise.resolve(tasks.find((t) => t.id === id) ?? null)),
78
+ queryTasks: vi.fn().mockResolvedValue({ tasks, total: tasks.length }),
79
+ countChildren: vi.fn().mockResolvedValue(0),
80
+ close: vi.fn().mockResolvedValue(undefined),
81
+ } as unknown as DataAccessor;
82
+ }
83
+
84
+ function mockBrainAccessor(
85
+ patterns: BrainPatternRow[] = [],
86
+ observations: BrainObservationRow[] = [],
87
+ learnings: BrainLearningRow[] = [],
88
+ ): BrainDataAccessor {
89
+ return {
90
+ findPatterns: vi.fn().mockImplementation((params?: { type?: string; limit?: number }) => {
91
+ let filtered = patterns;
92
+ if (params?.type) {
93
+ filtered = filtered.filter((p) => p.type === params.type);
94
+ }
95
+ if (params?.limit) {
96
+ filtered = filtered.slice(0, params.limit);
97
+ }
98
+ return Promise.resolve(filtered);
99
+ }),
100
+ findLearnings: vi.fn().mockResolvedValue(learnings),
101
+ findObservations: vi.fn().mockResolvedValue(observations),
102
+ addObservation: vi.fn().mockImplementation((row: BrainObservationRow) => Promise.resolve(row)),
103
+ addLearning: vi.fn().mockImplementation((row: BrainLearningRow) => Promise.resolve(row)),
104
+ getObservation: vi.fn().mockResolvedValue(null),
105
+ } as unknown as BrainDataAccessor;
106
+ }
107
+
108
+ // ============================================================================
109
+ // Tests: suggestGateFocus
110
+ // ============================================================================
111
+
112
+ beforeEach(() => {
113
+ vi.clearAllMocks();
114
+ });
115
+
116
+ describe('suggestGateFocus', () => {
117
+ it('returns empty gate focus for not-found task', async () => {
118
+ const taskAccessor = mockTaskAccessor([]);
119
+ const brainAccessor = mockBrainAccessor();
120
+
121
+ const result = await suggestGateFocus('T999', taskAccessor, brainAccessor);
122
+
123
+ expect(result.taskId).toBe('T999');
124
+ expect(result.gateFocus).toHaveLength(0);
125
+ expect(result.tips[0]).toContain('not found');
126
+ });
127
+
128
+ it('returns gate recommendations for a simple task', async () => {
129
+ const task = makeTask({ id: 'T001', title: 'Simple feature', size: 'small' });
130
+ const taskAccessor = mockTaskAccessor([task]);
131
+ const brainAccessor = mockBrainAccessor();
132
+
133
+ const result = await suggestGateFocus('T001', taskAccessor, brainAccessor);
134
+
135
+ expect(result.taskId).toBe('T001');
136
+ expect(result.gateFocus.length).toBeGreaterThan(0);
137
+ expect(result.overallConfidence).toBeGreaterThanOrEqual(0);
138
+ expect(result.overallConfidence).toBeLessThanOrEqual(1);
139
+ });
140
+
141
+ it('skips already-passed gates', async () => {
142
+ const task = makeTask({
143
+ id: 'T001',
144
+ title: 'Partial task',
145
+ verification: makeVerification({
146
+ gates: { implemented: true, testsPassed: true },
147
+ }),
148
+ });
149
+ const taskAccessor = mockTaskAccessor([task]);
150
+ const brainAccessor = mockBrainAccessor();
151
+
152
+ const result = await suggestGateFocus('T001', taskAccessor, brainAccessor);
153
+
154
+ const gateNames = result.gateFocus.map((r) => r.gate);
155
+ expect(gateNames).not.toContain('implemented');
156
+ expect(gateNames).not.toContain('testsPassed');
157
+ });
158
+
159
+ it('marks security gate as high priority for security-labeled tasks', async () => {
160
+ const task = makeTask({
161
+ id: 'T001',
162
+ title: 'Auth overhaul',
163
+ labels: ['auth', 'security'],
164
+ size: 'large',
165
+ });
166
+ const taskAccessor = mockTaskAccessor([task]);
167
+ const brainAccessor = mockBrainAccessor();
168
+
169
+ const result = await suggestGateFocus('T001', taskAccessor, brainAccessor);
170
+
171
+ const securityGate = result.gateFocus.find((r) => r.gate === 'securityPassed');
172
+ expect(securityGate).toBeDefined();
173
+ expect(securityGate!.priority).toBe('high');
174
+ });
175
+
176
+ it('incorporates historical failure patterns into gate risk', async () => {
177
+ const task = makeTask({ id: 'T001', title: 'Deploy service', labels: ['deploy'] });
178
+ const failurePatterns = [
179
+ makePattern({
180
+ type: 'failure',
181
+ pattern: 'testsPassed gate often fails during deploy tasks',
182
+ context: 'deploy testing failure',
183
+ successRate: 0.1,
184
+ }),
185
+ ];
186
+ const taskAccessor = mockTaskAccessor([task]);
187
+ const brainAccessor = mockBrainAccessor(failurePatterns);
188
+
189
+ const result = await suggestGateFocus('T001', taskAccessor, brainAccessor);
190
+
191
+ const testsGate = result.gateFocus.find((r) => r.gate === 'testsPassed');
192
+ expect(testsGate).toBeDefined();
193
+ // Historical failure data should push this gate to high priority
194
+ expect(testsGate!.priority).toBe('high');
195
+ });
196
+
197
+ it('includes mitigation from historical patterns in rationale', async () => {
198
+ const task = makeTask({ id: 'T001', title: 'Migration task', labels: ['migration'] });
199
+ const failurePatterns = [
200
+ makePattern({
201
+ type: 'failure',
202
+ pattern: 'qaPassed fails on migration tasks',
203
+ context: 'migration qa failure',
204
+ successRate: 0.2,
205
+ mitigation: 'Always test rollback before qa sign-off',
206
+ }),
207
+ ];
208
+ const taskAccessor = mockTaskAccessor([task]);
209
+ const brainAccessor = mockBrainAccessor(failurePatterns);
210
+
211
+ const result = await suggestGateFocus('T001', taskAccessor, brainAccessor);
212
+
213
+ const qaGate = result.gateFocus.find((r) => r.gate === 'qaPassed');
214
+ expect(qaGate).toBeDefined();
215
+ expect(qaGate!.rationale).toContain('rollback');
216
+ });
217
+
218
+ it('orders gates by priority (high before medium before low)', async () => {
219
+ const task = makeTask({
220
+ id: 'T001',
221
+ title: 'Auth service',
222
+ labels: ['auth', 'security'],
223
+ size: 'large',
224
+ priority: 'critical',
225
+ });
226
+ const taskAccessor = mockTaskAccessor([task]);
227
+ const brainAccessor = mockBrainAccessor();
228
+
229
+ const result = await suggestGateFocus('T001', taskAccessor, brainAccessor);
230
+
231
+ const priorityValues: Record<string, number> = { high: 0, medium: 1, low: 2 };
232
+ for (let i = 1; i < result.gateFocus.length; i++) {
233
+ const prevPriority = priorityValues[result.gateFocus[i - 1].priority];
234
+ const currPriority = priorityValues[result.gateFocus[i].priority];
235
+ expect(prevPriority).toBeLessThanOrEqual(currPriority);
236
+ }
237
+ });
238
+
239
+ it('generates tips about missing acceptance criteria', async () => {
240
+ const task = makeTask({ id: 'T001', title: 'Feature', acceptance: [] });
241
+ const taskAccessor = mockTaskAccessor([task]);
242
+ const brainAccessor = mockBrainAccessor();
243
+
244
+ const result = await suggestGateFocus('T001', taskAccessor, brainAccessor);
245
+
246
+ expect(result.tips.some((t) => t.toLowerCase().includes('acceptance'))).toBe(true);
247
+ });
248
+
249
+ it('mentions previously-failed gates in tips', async () => {
250
+ const task = makeTask({
251
+ id: 'T001',
252
+ title: 'Retry task',
253
+ verification: makeVerification({
254
+ round: 2,
255
+ gates: { testsPassed: false },
256
+ }),
257
+ });
258
+ const taskAccessor = mockTaskAccessor([task]);
259
+ const brainAccessor = mockBrainAccessor();
260
+
261
+ const result = await suggestGateFocus('T001', taskAccessor, brainAccessor);
262
+
263
+ expect(result.tips.some((t) => t.includes('previous'))).toBe(true);
264
+ });
265
+ });
266
+
267
+ // ============================================================================
268
+ // Tests: scoreVerificationConfidence
269
+ // ============================================================================
270
+
271
+ describe('scoreVerificationConfidence', () => {
272
+ it('returns confidence 0 when all tracked gates failed', async () => {
273
+ const task = makeTask({ id: 'T001', title: 'Failing task' });
274
+ const taskAccessor = mockTaskAccessor([task]);
275
+ const brainAccessor = mockBrainAccessor();
276
+
277
+ const verification = makeVerification({
278
+ passed: false,
279
+ round: 5,
280
+ gates: {
281
+ implemented: false,
282
+ testsPassed: false,
283
+ qaPassed: false,
284
+ },
285
+ failureLog: [
286
+ { round: 1, agent: 'qa', reason: 'fails', timestamp: new Date().toISOString() },
287
+ { round: 2, agent: 'qa', reason: 'still fails', timestamp: new Date().toISOString() },
288
+ { round: 3, agent: 'qa', reason: 'again', timestamp: new Date().toISOString() },
289
+ ],
290
+ });
291
+
292
+ const result = await scoreVerificationConfidence(
293
+ 'T001',
294
+ verification,
295
+ taskAccessor,
296
+ brainAccessor,
297
+ { dryRun: true },
298
+ );
299
+
300
+ expect(result.confidenceScore).toBeLessThan(0.3);
301
+ expect(result.passed).toBe(false);
302
+ expect(result.gatesFailed).toContain('implemented');
303
+ expect(result.observationId).toBeUndefined();
304
+ });
305
+
306
+ it('returns high confidence for first-round all-pass', async () => {
307
+ const task = makeTask({ id: 'T001', title: 'Green task' });
308
+ const taskAccessor = mockTaskAccessor([task]);
309
+ const brainAccessor = mockBrainAccessor();
310
+
311
+ const verification = makeVerification({
312
+ passed: true,
313
+ round: 1,
314
+ gates: {
315
+ implemented: true,
316
+ testsPassed: true,
317
+ qaPassed: true,
318
+ },
319
+ failureLog: [],
320
+ });
321
+
322
+ const result = await scoreVerificationConfidence(
323
+ 'T001',
324
+ verification,
325
+ taskAccessor,
326
+ brainAccessor,
327
+ { dryRun: true },
328
+ );
329
+
330
+ expect(result.confidenceScore).toBeGreaterThan(0.7);
331
+ expect(result.passed).toBe(true);
332
+ expect(result.gatesPassed).toContain('implemented');
333
+ expect(result.gatesPassed).toContain('testsPassed');
334
+ expect(result.gatesPassed).toContain('qaPassed');
335
+ });
336
+
337
+ it('correctly classifies gatesPassed and gatesFailed', async () => {
338
+ const task = makeTask({ id: 'T001', title: 'Mixed task' });
339
+ const taskAccessor = mockTaskAccessor([task]);
340
+ const brainAccessor = mockBrainAccessor();
341
+
342
+ const verification = makeVerification({
343
+ passed: false,
344
+ round: 1,
345
+ gates: {
346
+ implemented: true,
347
+ testsPassed: false,
348
+ qaPassed: null,
349
+ cleanupDone: true,
350
+ },
351
+ });
352
+
353
+ const result = await scoreVerificationConfidence(
354
+ 'T001',
355
+ verification,
356
+ taskAccessor,
357
+ brainAccessor,
358
+ { dryRun: true },
359
+ );
360
+
361
+ expect(result.gatesPassed).toContain('implemented');
362
+ expect(result.gatesPassed).toContain('cleanupDone');
363
+ expect(result.gatesFailed).toContain('testsPassed');
364
+ expect(result.gatesFailed).toContain('qaPassed');
365
+ });
366
+
367
+ it('persists observation when not dry-run', async () => {
368
+ const task = makeTask({ id: 'T001', title: 'Persist task' });
369
+ const taskAccessor = mockTaskAccessor([task]);
370
+ const brainAccessor = mockBrainAccessor();
371
+
372
+ const verification = makeVerification({
373
+ passed: true,
374
+ round: 1,
375
+ gates: { implemented: true, testsPassed: true },
376
+ });
377
+
378
+ const result = await scoreVerificationConfidence(
379
+ 'T001',
380
+ verification,
381
+ taskAccessor,
382
+ brainAccessor,
383
+ );
384
+
385
+ expect(brainAccessor.addObservation).toHaveBeenCalledTimes(1);
386
+ expect(result.observationId).toBeDefined();
387
+ expect(result.observationId).toMatch(/^O-vconf-/);
388
+ });
389
+
390
+ it('extracts learning for high-confidence first-round pass', async () => {
391
+ const task = makeTask({ id: 'T001', title: 'Clean task', size: 'small' });
392
+ const taskAccessor = mockTaskAccessor([task]);
393
+ const brainAccessor = mockBrainAccessor();
394
+
395
+ const verification = makeVerification({
396
+ passed: true,
397
+ round: 1,
398
+ gates: {
399
+ implemented: true,
400
+ testsPassed: true,
401
+ qaPassed: true,
402
+ cleanupDone: true,
403
+ documented: true,
404
+ },
405
+ failureLog: [],
406
+ });
407
+
408
+ const result = await scoreVerificationConfidence(
409
+ 'T001',
410
+ verification,
411
+ taskAccessor,
412
+ brainAccessor,
413
+ );
414
+
415
+ expect(brainAccessor.addLearning).toHaveBeenCalledTimes(1);
416
+ expect(result.learningId).toBeDefined();
417
+ expect(result.learningId).toMatch(/^L-vconf-/);
418
+ });
419
+
420
+ it('extracts actionable learning when multiple gates fail', async () => {
421
+ const task = makeTask({ id: 'T001', title: 'Troubled task', labels: ['auth'] });
422
+ const taskAccessor = mockTaskAccessor([task]);
423
+ const brainAccessor = mockBrainAccessor();
424
+
425
+ const verification = makeVerification({
426
+ passed: false,
427
+ round: 2,
428
+ gates: { testsPassed: false, qaPassed: false, securityPassed: false },
429
+ failureLog: [
430
+ { round: 1, agent: 'qa', reason: 'coverage', timestamp: new Date().toISOString() },
431
+ { round: 1, agent: 'security', reason: 'vuln', timestamp: new Date().toISOString() },
432
+ ],
433
+ });
434
+
435
+ const result = await scoreVerificationConfidence(
436
+ 'T001',
437
+ verification,
438
+ taskAccessor,
439
+ brainAccessor,
440
+ );
441
+
442
+ // Should extract learning: >= 2 gates failed
443
+ expect(brainAccessor.addLearning).toHaveBeenCalledTimes(1);
444
+ const learningCall = (brainAccessor.addLearning as ReturnType<typeof vi.fn>).mock.calls[0][0];
445
+ expect(learningCall.actionable).toBe(true);
446
+ expect(learningCall.insight).toContain('testsPassed');
447
+ expect(result.learningId).toBeDefined();
448
+ });
449
+
450
+ it('does not extract learning for moderate mid-round outcomes', async () => {
451
+ const task = makeTask({ id: 'T001', title: 'Average task' });
452
+ const taskAccessor = mockTaskAccessor([task]);
453
+ const brainAccessor = mockBrainAccessor();
454
+
455
+ const verification = makeVerification({
456
+ passed: true,
457
+ round: 3,
458
+ gates: { implemented: true, testsPassed: true },
459
+ failureLog: [{ round: 1, agent: 'qa', reason: 'minor', timestamp: new Date().toISOString() }],
460
+ });
461
+
462
+ const result = await scoreVerificationConfidence(
463
+ 'T001',
464
+ verification,
465
+ taskAccessor,
466
+ brainAccessor,
467
+ );
468
+
469
+ // Round 3 pass with moderate confidence — not notable enough
470
+ expect(result.learningId).toBeUndefined();
471
+ });
472
+ });
473
+
474
+ // ============================================================================
475
+ // Tests: storePrediction
476
+ // ============================================================================
477
+
478
+ describe('storePrediction', () => {
479
+ it('returns undefined on dry run', async () => {
480
+ const brainAccessor = mockBrainAccessor();
481
+
482
+ const observationId = await storePrediction(
483
+ {
484
+ taskId: 'T001',
485
+ stage: 'implementation',
486
+ passLikelihood: 0.8,
487
+ blockers: [],
488
+ suggestions: ['Review docs'],
489
+ },
490
+ brainAccessor,
491
+ { dryRun: true },
492
+ );
493
+
494
+ expect(observationId).toBeUndefined();
495
+ expect(brainAccessor.addObservation).not.toHaveBeenCalled();
496
+ });
497
+
498
+ it('persists prediction observation to brain', async () => {
499
+ const brainAccessor = mockBrainAccessor();
500
+
501
+ const observationId = await storePrediction(
502
+ {
503
+ taskId: 'T001',
504
+ stage: 'specification',
505
+ passLikelihood: 0.6,
506
+ blockers: ['Missing AC'],
507
+ suggestions: [],
508
+ },
509
+ brainAccessor,
510
+ { project: 'test-project', sessionId: 'S-001' },
511
+ );
512
+
513
+ expect(observationId).toMatch(/^O-pred-/);
514
+ expect(brainAccessor.addObservation).toHaveBeenCalledTimes(1);
515
+ const obs = (brainAccessor.addObservation as ReturnType<typeof vi.fn>).mock.calls[0][0];
516
+ expect(obs.title).toContain('T001');
517
+ expect(obs.title).toContain('specification');
518
+ expect(obs.narrative).toContain('Missing AC');
519
+ expect(obs.project).toBe('test-project');
520
+ expect(obs.sourceSessionId).toBe('S-001');
521
+ });
522
+
523
+ it('includes pass likelihood in subtitle', async () => {
524
+ const brainAccessor = mockBrainAccessor();
525
+
526
+ await storePrediction(
527
+ {
528
+ taskId: 'T002',
529
+ stage: 'verification',
530
+ passLikelihood: 0.75,
531
+ blockers: [],
532
+ suggestions: [],
533
+ },
534
+ brainAccessor,
535
+ );
536
+
537
+ const obs = (brainAccessor.addObservation as ReturnType<typeof vi.fn>).mock.calls[0][0];
538
+ expect(obs.subtitle).toContain('75%');
539
+ });
540
+ });
541
+
542
+ // ============================================================================
543
+ // Tests: predictAndStore
544
+ // ============================================================================
545
+
546
+ describe('predictAndStore', () => {
547
+ it('returns prediction with observationId when not dry-run', async () => {
548
+ const task = makeTask({ id: 'T001', title: 'Feature task', status: 'active' });
549
+ const taskAccessor = mockTaskAccessor([task]);
550
+ const brainAccessor = mockBrainAccessor();
551
+
552
+ const result = await predictAndStore('T001', 'implementation', taskAccessor, brainAccessor);
553
+
554
+ expect(result.taskId).toBe('T001');
555
+ expect(result.stage).toBe('implementation');
556
+ expect(result.passLikelihood).toBeGreaterThanOrEqual(0);
557
+ expect(result.passLikelihood).toBeLessThanOrEqual(1);
558
+ expect(result.observationId).toMatch(/^O-pred-/);
559
+ expect(brainAccessor.addObservation).toHaveBeenCalledTimes(1);
560
+ });
561
+
562
+ it('skips storage on dry run', async () => {
563
+ const task = makeTask({ id: 'T001', title: 'Feature task', status: 'active' });
564
+ const taskAccessor = mockTaskAccessor([task]);
565
+ const brainAccessor = mockBrainAccessor();
566
+
567
+ const result = await predictAndStore('T001', 'specification', taskAccessor, brainAccessor, {
568
+ dryRun: true,
569
+ });
570
+
571
+ expect(result.observationId).toBeUndefined();
572
+ expect(brainAccessor.addObservation).not.toHaveBeenCalled();
573
+ });
574
+
575
+ it('handles task not found gracefully', async () => {
576
+ const taskAccessor = mockTaskAccessor([]);
577
+ const brainAccessor = mockBrainAccessor();
578
+
579
+ const result = await predictAndStore('T999', 'verification', taskAccessor, brainAccessor, {
580
+ dryRun: true,
581
+ });
582
+
583
+ expect(result.taskId).toBe('T999');
584
+ expect(result.passLikelihood).toBe(0);
585
+ expect(result.blockers[0]).toContain('not found');
586
+ expect(result.observationId).toBeUndefined();
587
+ });
588
+ });
589
+
590
+ // ============================================================================
591
+ // Tests: confidence score computation boundary cases
592
+ // ============================================================================
593
+
594
+ describe('confidence score boundary cases', () => {
595
+ it('confidence approaches 1.0 for ideal verification', async () => {
596
+ const task = makeTask({ id: 'T001', title: 'Ideal' });
597
+ const taskAccessor = mockTaskAccessor([task]);
598
+ const brainAccessor = mockBrainAccessor();
599
+
600
+ const allGatesPassed: TaskVerification['gates'] = {
601
+ implemented: true,
602
+ testsPassed: true,
603
+ qaPassed: true,
604
+ cleanupDone: true,
605
+ securityPassed: true,
606
+ documented: true,
607
+ };
608
+
609
+ const verification = makeVerification({
610
+ passed: true,
611
+ round: 1,
612
+ gates: allGatesPassed,
613
+ failureLog: [],
614
+ });
615
+
616
+ const result = await scoreVerificationConfidence(
617
+ 'T001',
618
+ verification,
619
+ taskAccessor,
620
+ brainAccessor,
621
+ { dryRun: true },
622
+ );
623
+
624
+ // 6/6 gates passed (0.6) + no failures (0.2) + round 1 (0.2) = 1.0
625
+ expect(result.confidenceScore).toBe(1.0);
626
+ });
627
+
628
+ it('confidence is lower for multi-round verification even with pass', async () => {
629
+ const task = makeTask({ id: 'T001', title: 'Multi-round' });
630
+ const taskAccessor = mockTaskAccessor([task]);
631
+ const brainAccessor = mockBrainAccessor();
632
+
633
+ const singleRound = makeVerification({
634
+ passed: true,
635
+ round: 1,
636
+ gates: { implemented: true, testsPassed: true },
637
+ failureLog: [],
638
+ });
639
+
640
+ const multiRound = makeVerification({
641
+ passed: true,
642
+ round: 3,
643
+ gates: { implemented: true, testsPassed: true },
644
+ failureLog: [{ round: 1, agent: 'qa', reason: 'issue', timestamp: new Date().toISOString() }],
645
+ });
646
+
647
+ const [s1, m1] = await Promise.all([
648
+ scoreVerificationConfidence('T001', singleRound, taskAccessor, brainAccessor, {
649
+ dryRun: true,
650
+ }),
651
+ scoreVerificationConfidence('T001', multiRound, taskAccessor, brainAccessor, {
652
+ dryRun: true,
653
+ }),
654
+ ]);
655
+
656
+ expect(s1.confidenceScore).toBeGreaterThan(m1.confidenceScore);
657
+ });
658
+
659
+ it('confidence is clamped between 0 and 1', async () => {
660
+ const task = makeTask({ id: 'T001', title: 'Extreme' });
661
+ const taskAccessor = mockTaskAccessor([task]);
662
+ const brainAccessor = mockBrainAccessor();
663
+
664
+ const worstCase = makeVerification({
665
+ passed: false,
666
+ round: 10,
667
+ gates: {
668
+ implemented: false,
669
+ testsPassed: false,
670
+ qaPassed: false,
671
+ cleanupDone: false,
672
+ securityPassed: false,
673
+ documented: false,
674
+ },
675
+ failureLog: Array.from({ length: 10 }, (_, i) => ({
676
+ round: i + 1,
677
+ agent: 'qa',
678
+ reason: `failure ${i}`,
679
+ timestamp: new Date().toISOString(),
680
+ })),
681
+ });
682
+
683
+ const result = await scoreVerificationConfidence(
684
+ 'T001',
685
+ worstCase,
686
+ taskAccessor,
687
+ brainAccessor,
688
+ { dryRun: true },
689
+ );
690
+
691
+ expect(result.confidenceScore).toBeGreaterThanOrEqual(0);
692
+ expect(result.confidenceScore).toBeLessThanOrEqual(1);
693
+ });
694
+ });