@cleocode/core 2026.3.43 → 2026.3.44
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/admin/export-tasks.d.ts.map +1 -1
- package/dist/agents/agent-schema.d.ts +358 -0
- package/dist/agents/agent-schema.d.ts.map +1 -0
- package/dist/agents/capacity.d.ts +57 -0
- package/dist/agents/capacity.d.ts.map +1 -0
- package/dist/agents/index.d.ts +17 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/registry.d.ts +115 -0
- package/dist/agents/registry.d.ts.map +1 -0
- package/dist/agents/retry.d.ts +83 -0
- package/dist/agents/retry.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +4 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/payload-schemas.d.ts +214 -0
- package/dist/hooks/payload-schemas.d.ts.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +16443 -2160
- package/dist/index.js.map +4 -4
- package/dist/inject/index.d.ts.map +1 -1
- package/dist/intelligence/impact.d.ts +51 -0
- package/dist/intelligence/impact.d.ts.map +1 -0
- package/dist/intelligence/index.d.ts +15 -0
- package/dist/intelligence/index.d.ts.map +1 -0
- package/dist/intelligence/patterns.d.ts +66 -0
- package/dist/intelligence/patterns.d.ts.map +1 -0
- package/dist/intelligence/prediction.d.ts +51 -0
- package/dist/intelligence/prediction.d.ts.map +1 -0
- package/dist/intelligence/types.d.ts +221 -0
- package/dist/intelligence/types.d.ts.map +1 -0
- package/dist/internal.d.ts +9 -0
- package/dist/internal.d.ts.map +1 -1
- package/dist/issue/template-parser.d.ts +8 -2
- package/dist/issue/template-parser.d.ts.map +1 -1
- package/dist/lifecycle/pipeline.d.ts +2 -2
- package/dist/lifecycle/pipeline.d.ts.map +1 -1
- package/dist/lifecycle/state-machine.d.ts +1 -1
- package/dist/lifecycle/state-machine.d.ts.map +1 -1
- package/dist/memory/brain-lifecycle.d.ts.map +1 -1
- package/dist/memory/brain-retrieval.d.ts.map +1 -1
- package/dist/memory/brain-row-types.d.ts +40 -6
- package/dist/memory/brain-row-types.d.ts.map +1 -1
- package/dist/memory/brain-search.d.ts.map +1 -1
- package/dist/memory/brain-similarity.d.ts.map +1 -1
- package/dist/memory/claude-mem-migration.d.ts.map +1 -1
- package/dist/nexus/discover.d.ts.map +1 -1
- package/dist/orchestration/bootstrap.d.ts.map +1 -1
- package/dist/orchestration/skill-ops.d.ts +4 -4
- package/dist/orchestration/skill-ops.d.ts.map +1 -1
- package/dist/otel/index.d.ts +1 -1
- package/dist/otel/index.d.ts.map +1 -1
- package/dist/sessions/briefing.d.ts.map +1 -1
- package/dist/sessions/handoff.d.ts.map +1 -1
- package/dist/sessions/index.d.ts +1 -1
- package/dist/sessions/index.d.ts.map +1 -1
- package/dist/sessions/types.d.ts +8 -42
- package/dist/sessions/types.d.ts.map +1 -1
- package/dist/signaldock/signaldock-transport.d.ts +1 -1
- package/dist/signaldock/signaldock-transport.d.ts.map +1 -1
- package/dist/skills/injection/subagent.d.ts +3 -3
- package/dist/skills/injection/subagent.d.ts.map +1 -1
- package/dist/skills/manifests/contribution.d.ts +2 -2
- package/dist/skills/manifests/contribution.d.ts.map +1 -1
- package/dist/skills/orchestrator/spawn.d.ts +6 -6
- package/dist/skills/orchestrator/spawn.d.ts.map +1 -1
- package/dist/skills/orchestrator/startup.d.ts +1 -1
- package/dist/skills/orchestrator/startup.d.ts.map +1 -1
- package/dist/skills/orchestrator/validator.d.ts +2 -2
- package/dist/skills/orchestrator/validator.d.ts.map +1 -1
- package/dist/skills/precedence-types.d.ts +24 -1
- package/dist/skills/precedence-types.d.ts.map +1 -1
- package/dist/skills/types.d.ts +70 -4
- package/dist/skills/types.d.ts.map +1 -1
- package/dist/store/export.d.ts +5 -4
- package/dist/store/export.d.ts.map +1 -1
- package/dist/store/tasks-schema.d.ts +12 -2
- package/dist/store/tasks-schema.d.ts.map +1 -1
- package/dist/store/typed-query.d.ts +12 -0
- package/dist/store/typed-query.d.ts.map +1 -0
- package/dist/store/validation-schemas.d.ts +2422 -50
- package/dist/store/validation-schemas.d.ts.map +1 -1
- package/dist/system/inject-generate.d.ts.map +1 -1
- package/dist/validation/doctor/checks.d.ts +5 -0
- package/dist/validation/doctor/checks.d.ts.map +1 -1
- package/dist/validation/engine.d.ts +10 -10
- package/dist/validation/engine.d.ts.map +1 -1
- package/dist/validation/index.d.ts +6 -2
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/protocol-common.d.ts +10 -2
- package/dist/validation/protocol-common.d.ts.map +1 -1
- package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/migration.sql +84 -0
- package/migrations/drizzle-tasks/20260320013731_wave0-schema-hardening/snapshot.json +4060 -0
- package/migrations/drizzle-tasks/20260320020000_agent-dimension/migration.sql +35 -0
- package/migrations/drizzle-tasks/20260320020000_agent-dimension/snapshot.json +4312 -0
- package/package.json +2 -2
- package/src/admin/export-tasks.ts +2 -5
- package/src/agents/__tests__/capacity.test.ts +219 -0
- package/src/agents/__tests__/registry.test.ts +457 -0
- package/src/agents/__tests__/retry.test.ts +289 -0
- package/src/agents/agent-schema.ts +107 -0
- package/src/agents/capacity.ts +151 -0
- package/src/agents/index.ts +68 -0
- package/src/agents/registry.ts +449 -0
- package/src/agents/retry.ts +255 -0
- package/src/hooks/index.ts +20 -1
- package/src/hooks/payload-schemas.ts +199 -0
- package/src/index.ts +69 -0
- package/src/inject/index.ts +14 -14
- package/src/intelligence/__tests__/impact.test.ts +453 -0
- package/src/intelligence/__tests__/patterns.test.ts +450 -0
- package/src/intelligence/__tests__/prediction.test.ts +418 -0
- package/src/intelligence/impact.ts +638 -0
- package/src/intelligence/index.ts +47 -0
- package/src/intelligence/patterns.ts +621 -0
- package/src/intelligence/prediction.ts +621 -0
- package/src/intelligence/types.ts +273 -0
- package/src/internal.ts +82 -1
- package/src/issue/template-parser.ts +65 -4
- package/src/lifecycle/pipeline.ts +14 -7
- package/src/lifecycle/state-machine.ts +6 -2
- package/src/memory/brain-lifecycle.ts +5 -11
- package/src/memory/brain-retrieval.ts +44 -38
- package/src/memory/brain-row-types.ts +43 -6
- package/src/memory/brain-search.ts +53 -32
- package/src/memory/brain-similarity.ts +9 -8
- package/src/memory/claude-mem-migration.ts +4 -3
- package/src/nexus/__tests__/nexus-e2e.test.ts +1481 -0
- package/src/nexus/discover.ts +1 -0
- package/src/orchestration/bootstrap.ts +11 -17
- package/src/orchestration/skill-ops.ts +52 -32
- package/src/otel/index.ts +48 -4
- package/src/sessions/__tests__/briefing.test.ts +31 -2
- package/src/sessions/briefing.ts +27 -42
- package/src/sessions/handoff.ts +52 -86
- package/src/sessions/index.ts +5 -1
- package/src/sessions/types.ts +9 -43
- package/src/signaldock/signaldock-transport.ts +5 -2
- package/src/skills/injection/subagent.ts +10 -16
- package/src/skills/manifests/contribution.ts +5 -13
- package/src/skills/orchestrator/__tests__/spawn-tier.test.ts +44 -30
- package/src/skills/orchestrator/spawn.ts +18 -31
- package/src/skills/orchestrator/startup.ts +78 -65
- package/src/skills/orchestrator/validator.ts +26 -31
- package/src/skills/precedence-types.ts +24 -1
- package/src/skills/types.ts +72 -5
- package/src/store/__tests__/test-db-helper.d.ts +4 -4
- package/src/store/__tests__/test-db-helper.js +5 -16
- package/src/store/__tests__/test-db-helper.ts +5 -18
- package/src/store/chain-schema.ts +1 -1
- package/src/store/export.ts +22 -12
- package/src/store/tasks-schema.ts +65 -8
- package/src/store/typed-query.ts +17 -0
- package/src/store/validation-schemas.ts +347 -23
- package/src/system/inject-generate.ts +9 -23
- package/src/validation/doctor/checks.ts +24 -2
- package/src/validation/engine.ts +11 -11
- package/src/validation/index.ts +131 -3
- package/src/validation/protocol-common.ts +54 -3
- package/dist/tasks/reparent.d.ts +0 -38
- package/dist/tasks/reparent.d.ts.map +0 -1
- package/src/tasks/reparent.ts +0 -134
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the Pattern Extraction module.
|
|
3
|
+
*
|
|
4
|
+
* Tests pattern extraction from history, pattern matching against tasks,
|
|
5
|
+
* pattern storage, and stat updates. All external dependencies are mocked.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { Task } from '@cleocode/contracts';
|
|
9
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
10
|
+
import type { BrainDataAccessor } from '../../store/brain-accessor.js';
|
|
11
|
+
import type { BrainObservationRow, BrainPatternRow } from '../../store/brain-schema.js';
|
|
12
|
+
import type { DataAccessor } from '../../store/data-accessor.js';
|
|
13
|
+
import {
|
|
14
|
+
extractPatternsFromHistory,
|
|
15
|
+
matchPatterns,
|
|
16
|
+
storeDetectedPattern,
|
|
17
|
+
updatePatternStats,
|
|
18
|
+
} from '../patterns.js';
|
|
19
|
+
import type { DetectedPattern } from '../types.js';
|
|
20
|
+
|
|
21
|
+
// ---- helpers ----------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
function makeTask(overrides: Partial<Task> & { id: string; title: string }): Task {
|
|
24
|
+
return {
|
|
25
|
+
status: 'pending',
|
|
26
|
+
priority: 'medium',
|
|
27
|
+
description: `Description for ${overrides.id}`,
|
|
28
|
+
createdAt: new Date().toISOString(),
|
|
29
|
+
labels: [],
|
|
30
|
+
depends: [],
|
|
31
|
+
...overrides,
|
|
32
|
+
} as Task;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function makePattern(overrides: Partial<BrainPatternRow> = {}): BrainPatternRow {
|
|
36
|
+
return {
|
|
37
|
+
id: `P-${Math.random().toString(36).slice(2, 10)}`,
|
|
38
|
+
type: 'success',
|
|
39
|
+
pattern: 'test pattern',
|
|
40
|
+
context: 'test context',
|
|
41
|
+
frequency: 1,
|
|
42
|
+
successRate: null,
|
|
43
|
+
impact: null,
|
|
44
|
+
antiPattern: null,
|
|
45
|
+
mitigation: null,
|
|
46
|
+
examplesJson: '[]',
|
|
47
|
+
extractedAt: new Date().toISOString(),
|
|
48
|
+
updatedAt: null,
|
|
49
|
+
...overrides,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function makeObservation(overrides: Partial<BrainObservationRow> = {}): BrainObservationRow {
|
|
54
|
+
return {
|
|
55
|
+
id: `O-${Math.random().toString(36).slice(2, 10)}`,
|
|
56
|
+
type: 'feature',
|
|
57
|
+
title: 'Test observation',
|
|
58
|
+
subtitle: null,
|
|
59
|
+
narrative: null,
|
|
60
|
+
factsJson: null,
|
|
61
|
+
conceptsJson: null,
|
|
62
|
+
project: null,
|
|
63
|
+
filesReadJson: null,
|
|
64
|
+
filesModifiedJson: null,
|
|
65
|
+
sourceSessionId: null,
|
|
66
|
+
sourceType: 'agent',
|
|
67
|
+
contentHash: null,
|
|
68
|
+
discoveryTokens: null,
|
|
69
|
+
createdAt: new Date().toISOString(),
|
|
70
|
+
updatedAt: null,
|
|
71
|
+
...overrides,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function mockTaskAccessor(tasks: Task[]): DataAccessor {
|
|
76
|
+
return {
|
|
77
|
+
loadSingleTask: vi
|
|
78
|
+
.fn()
|
|
79
|
+
.mockImplementation((id: string) => Promise.resolve(tasks.find((t) => t.id === id) ?? null)),
|
|
80
|
+
queryTasks: vi.fn().mockResolvedValue({ tasks, total: tasks.length }),
|
|
81
|
+
countChildren: vi.fn().mockResolvedValue(0),
|
|
82
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
83
|
+
} as unknown as DataAccessor;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function mockBrainAccessor(
|
|
87
|
+
patterns: BrainPatternRow[] = [],
|
|
88
|
+
observations: BrainObservationRow[] = [],
|
|
89
|
+
): BrainDataAccessor {
|
|
90
|
+
return {
|
|
91
|
+
findPatterns: vi.fn().mockImplementation((params?: { type?: string; limit?: number }) => {
|
|
92
|
+
let filtered = patterns;
|
|
93
|
+
if (params?.type) {
|
|
94
|
+
filtered = filtered.filter((p) => p.type === params.type);
|
|
95
|
+
}
|
|
96
|
+
if (params?.limit) {
|
|
97
|
+
filtered = filtered.slice(0, params.limit);
|
|
98
|
+
}
|
|
99
|
+
return Promise.resolve(filtered);
|
|
100
|
+
}),
|
|
101
|
+
findObservations: vi.fn().mockResolvedValue(observations),
|
|
102
|
+
findLearnings: vi.fn().mockResolvedValue([]),
|
|
103
|
+
addPattern: vi.fn().mockImplementation((row: BrainPatternRow) => Promise.resolve(row)),
|
|
104
|
+
getPattern: vi
|
|
105
|
+
.fn()
|
|
106
|
+
.mockImplementation((id: string) =>
|
|
107
|
+
Promise.resolve(patterns.find((p) => p.id === id) ?? null),
|
|
108
|
+
),
|
|
109
|
+
updatePattern: vi.fn().mockResolvedValue(undefined),
|
|
110
|
+
} as unknown as BrainDataAccessor;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ---- tests ----------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
beforeEach(() => {
|
|
116
|
+
vi.clearAllMocks();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
describe('extractPatternsFromHistory', () => {
|
|
120
|
+
it('returns empty array when no tasks exist', async () => {
|
|
121
|
+
const taskAccessor = mockTaskAccessor([]);
|
|
122
|
+
const brainAccessor = mockBrainAccessor();
|
|
123
|
+
|
|
124
|
+
const result = await extractPatternsFromHistory(taskAccessor, brainAccessor);
|
|
125
|
+
|
|
126
|
+
expect(result).toEqual([]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('extracts blocker patterns from blocked tasks', async () => {
|
|
130
|
+
const tasks = [
|
|
131
|
+
makeTask({ id: 'T001', title: 'Task 1', status: 'blocked', blockedBy: 'API not ready' }),
|
|
132
|
+
makeTask({ id: 'T002', title: 'Task 2', status: 'blocked', blockedBy: 'API not ready' }),
|
|
133
|
+
makeTask({ id: 'T003', title: 'Task 3', status: 'active' }),
|
|
134
|
+
];
|
|
135
|
+
const taskAccessor = mockTaskAccessor(tasks);
|
|
136
|
+
const brainAccessor = mockBrainAccessor();
|
|
137
|
+
|
|
138
|
+
const result = await extractPatternsFromHistory(taskAccessor, brainAccessor);
|
|
139
|
+
|
|
140
|
+
const blockerPatterns = result.filter((p) => p.type === 'blocker');
|
|
141
|
+
expect(blockerPatterns.length).toBeGreaterThan(0);
|
|
142
|
+
expect(blockerPatterns.some((p) => p.pattern.includes('API not ready'))).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it('extracts success patterns from completed tasks with labels', async () => {
|
|
146
|
+
const tasks = [
|
|
147
|
+
makeTask({ id: 'T001', title: 'Fix A', status: 'done', labels: ['bugfix'] }),
|
|
148
|
+
makeTask({ id: 'T002', title: 'Fix B', status: 'done', labels: ['bugfix'] }),
|
|
149
|
+
makeTask({ id: 'T003', title: 'Fix C', status: 'done', labels: ['bugfix'] }),
|
|
150
|
+
];
|
|
151
|
+
const taskAccessor = mockTaskAccessor(tasks);
|
|
152
|
+
const brainAccessor = mockBrainAccessor();
|
|
153
|
+
|
|
154
|
+
const result = await extractPatternsFromHistory(taskAccessor, brainAccessor);
|
|
155
|
+
|
|
156
|
+
const successPatterns = result.filter((p) => p.type === 'success');
|
|
157
|
+
expect(successPatterns.length).toBeGreaterThan(0);
|
|
158
|
+
expect(successPatterns.some((p) => p.pattern.includes('bugfix'))).toBe(true);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('extracts workflow patterns from dependency hubs', async () => {
|
|
162
|
+
const tasks = [
|
|
163
|
+
makeTask({ id: 'T001', title: 'Base task' }),
|
|
164
|
+
makeTask({ id: 'T002', title: 'A', depends: ['T001'] }),
|
|
165
|
+
makeTask({ id: 'T003', title: 'B', depends: ['T001'] }),
|
|
166
|
+
makeTask({ id: 'T004', title: 'C', depends: ['T001'] }),
|
|
167
|
+
];
|
|
168
|
+
const taskAccessor = mockTaskAccessor(tasks);
|
|
169
|
+
const brainAccessor = mockBrainAccessor();
|
|
170
|
+
|
|
171
|
+
const result = await extractPatternsFromHistory(taskAccessor, brainAccessor, {
|
|
172
|
+
minFrequency: 2,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const workflowPatterns = result.filter((p) => p.type === 'workflow');
|
|
176
|
+
expect(workflowPatterns.some((p) => p.pattern.includes('T001'))).toBe(true);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
it('extracts observation patterns from brain observations', async () => {
|
|
180
|
+
const observations = [
|
|
181
|
+
makeObservation({ type: 'bugfix' }),
|
|
182
|
+
makeObservation({ type: 'bugfix' }),
|
|
183
|
+
makeObservation({ type: 'bugfix' }),
|
|
184
|
+
makeObservation({ type: 'feature' }),
|
|
185
|
+
];
|
|
186
|
+
const taskAccessor = mockTaskAccessor([]);
|
|
187
|
+
const brainAccessor = mockBrainAccessor([], observations);
|
|
188
|
+
|
|
189
|
+
const result = await extractPatternsFromHistory(taskAccessor, brainAccessor, {
|
|
190
|
+
minFrequency: 2,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(result.some((p) => p.pattern.includes('bugfix'))).toBe(true);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('filters by pattern type when specified', async () => {
|
|
197
|
+
const tasks = [
|
|
198
|
+
makeTask({ id: 'T001', title: 'Task 1', status: 'blocked', blockedBy: 'deps' }),
|
|
199
|
+
makeTask({ id: 'T002', title: 'Task 2', status: 'blocked', blockedBy: 'deps' }),
|
|
200
|
+
makeTask({ id: 'T003', title: 'Done 1', status: 'done', labels: ['feature'] }),
|
|
201
|
+
makeTask({ id: 'T004', title: 'Done 2', status: 'done', labels: ['feature'] }),
|
|
202
|
+
];
|
|
203
|
+
const taskAccessor = mockTaskAccessor(tasks);
|
|
204
|
+
const brainAccessor = mockBrainAccessor();
|
|
205
|
+
|
|
206
|
+
const result = await extractPatternsFromHistory(taskAccessor, brainAccessor, {
|
|
207
|
+
type: 'blocker',
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
for (const p of result) {
|
|
211
|
+
expect(p.type).toBe('blocker');
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('respects limit option', async () => {
|
|
216
|
+
const tasks = Array.from({ length: 20 }, (_, i) =>
|
|
217
|
+
makeTask({
|
|
218
|
+
id: `T${String(i).padStart(3, '0')}`,
|
|
219
|
+
title: `Task ${i}`,
|
|
220
|
+
status: 'done',
|
|
221
|
+
labels: [`label-${i % 3}`],
|
|
222
|
+
}),
|
|
223
|
+
);
|
|
224
|
+
const taskAccessor = mockTaskAccessor(tasks);
|
|
225
|
+
const brainAccessor = mockBrainAccessor();
|
|
226
|
+
|
|
227
|
+
const result = await extractPatternsFromHistory(taskAccessor, brainAccessor, {
|
|
228
|
+
limit: 3,
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
expect(result.length).toBeLessThanOrEqual(3);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('sorts by frequency descending', async () => {
|
|
235
|
+
const tasks = [
|
|
236
|
+
makeTask({ id: 'T001', title: 'A', status: 'done', labels: ['common'] }),
|
|
237
|
+
makeTask({ id: 'T002', title: 'B', status: 'done', labels: ['common'] }),
|
|
238
|
+
makeTask({ id: 'T003', title: 'C', status: 'done', labels: ['common'] }),
|
|
239
|
+
makeTask({ id: 'T004', title: 'D', status: 'done', labels: ['rare'] }),
|
|
240
|
+
makeTask({ id: 'T005', title: 'E', status: 'done', labels: ['rare'] }),
|
|
241
|
+
];
|
|
242
|
+
const taskAccessor = mockTaskAccessor(tasks);
|
|
243
|
+
const brainAccessor = mockBrainAccessor();
|
|
244
|
+
|
|
245
|
+
const result = await extractPatternsFromHistory(taskAccessor, brainAccessor);
|
|
246
|
+
|
|
247
|
+
for (let i = 1; i < result.length; i++) {
|
|
248
|
+
expect(result[i - 1].frequency).toBeGreaterThanOrEqual(result[i].frequency);
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('matchPatterns', () => {
|
|
254
|
+
it('returns empty array for not-found task', async () => {
|
|
255
|
+
const taskAccessor = mockTaskAccessor([]);
|
|
256
|
+
const brainAccessor = mockBrainAccessor();
|
|
257
|
+
|
|
258
|
+
const result = await matchPatterns('T999', taskAccessor, brainAccessor);
|
|
259
|
+
|
|
260
|
+
expect(result).toEqual([]);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('matches patterns by label overlap', async () => {
|
|
264
|
+
const task = makeTask({ id: 'T001', title: 'Auth task', labels: ['auth'] });
|
|
265
|
+
const patterns = [
|
|
266
|
+
makePattern({
|
|
267
|
+
id: 'P-001',
|
|
268
|
+
pattern: 'Auth modules require careful testing',
|
|
269
|
+
context: 'auth best practices',
|
|
270
|
+
}),
|
|
271
|
+
makePattern({
|
|
272
|
+
id: 'P-002',
|
|
273
|
+
pattern: 'Database migration steps',
|
|
274
|
+
context: 'database operations',
|
|
275
|
+
}),
|
|
276
|
+
];
|
|
277
|
+
const taskAccessor = mockTaskAccessor([task]);
|
|
278
|
+
const brainAccessor = mockBrainAccessor(patterns);
|
|
279
|
+
|
|
280
|
+
const result = await matchPatterns('T001', taskAccessor, brainAccessor);
|
|
281
|
+
|
|
282
|
+
expect(result.length).toBeGreaterThan(0);
|
|
283
|
+
expect(result[0].pattern.id).toBe('P-001');
|
|
284
|
+
expect(result[0].relevanceScore).toBeGreaterThan(0);
|
|
285
|
+
expect(result[0].matchReason).toContain('auth');
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it('matches patterns by title keywords', async () => {
|
|
289
|
+
const task = makeTask({ id: 'T001', title: 'Database migration refactor' });
|
|
290
|
+
const patterns = [
|
|
291
|
+
makePattern({
|
|
292
|
+
id: 'P-001',
|
|
293
|
+
pattern: 'Migration tasks need rollback plans',
|
|
294
|
+
context: 'database migration best practices',
|
|
295
|
+
}),
|
|
296
|
+
];
|
|
297
|
+
const taskAccessor = mockTaskAccessor([task]);
|
|
298
|
+
const brainAccessor = mockBrainAccessor(patterns);
|
|
299
|
+
|
|
300
|
+
const result = await matchPatterns('T001', taskAccessor, brainAccessor);
|
|
301
|
+
|
|
302
|
+
expect(result.length).toBeGreaterThan(0);
|
|
303
|
+
expect(result[0].matchReason).toContain('migration');
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('identifies anti-pattern matches', async () => {
|
|
307
|
+
const task = makeTask({ id: 'T001', title: 'Quick fix', labels: ['hotfix'] });
|
|
308
|
+
const patterns = [
|
|
309
|
+
makePattern({
|
|
310
|
+
id: 'P-001',
|
|
311
|
+
pattern: 'Hotfix without tests',
|
|
312
|
+
context: 'hotfix anti-pattern',
|
|
313
|
+
antiPattern: 'Deploying hotfixes without regression tests risks introducing new bugs',
|
|
314
|
+
mitigation: 'Always add regression tests for hotfixes',
|
|
315
|
+
}),
|
|
316
|
+
];
|
|
317
|
+
const taskAccessor = mockTaskAccessor([task]);
|
|
318
|
+
const brainAccessor = mockBrainAccessor(patterns);
|
|
319
|
+
|
|
320
|
+
const result = await matchPatterns('T001', taskAccessor, brainAccessor);
|
|
321
|
+
|
|
322
|
+
expect(result.length).toBeGreaterThan(0);
|
|
323
|
+
expect(result[0].isAntiPattern).toBe(true);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
it('sorts matches by relevance descending', async () => {
|
|
327
|
+
const task = makeTask({ id: 'T001', title: 'Auth migration', labels: ['auth'] });
|
|
328
|
+
const patterns = [
|
|
329
|
+
makePattern({
|
|
330
|
+
id: 'P-001',
|
|
331
|
+
pattern: 'Auth tasks need special handling',
|
|
332
|
+
context: 'auth context',
|
|
333
|
+
}),
|
|
334
|
+
makePattern({
|
|
335
|
+
id: 'P-002',
|
|
336
|
+
pattern: 'Migration requires rollback',
|
|
337
|
+
context: 'migration context',
|
|
338
|
+
}),
|
|
339
|
+
makePattern({
|
|
340
|
+
id: 'P-003',
|
|
341
|
+
pattern: 'Auth migration is complex',
|
|
342
|
+
context: 'auth migration combined',
|
|
343
|
+
}),
|
|
344
|
+
];
|
|
345
|
+
const taskAccessor = mockTaskAccessor([task]);
|
|
346
|
+
const brainAccessor = mockBrainAccessor(patterns);
|
|
347
|
+
|
|
348
|
+
const result = await matchPatterns('T001', taskAccessor, brainAccessor);
|
|
349
|
+
|
|
350
|
+
for (let i = 1; i < result.length; i++) {
|
|
351
|
+
expect(result[i - 1].relevanceScore).toBeGreaterThanOrEqual(result[i].relevanceScore);
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
describe('storeDetectedPattern', () => {
|
|
357
|
+
it('stores a pattern via brain accessor', async () => {
|
|
358
|
+
const brainAccessor = mockBrainAccessor();
|
|
359
|
+
const detected: DetectedPattern = {
|
|
360
|
+
type: 'success',
|
|
361
|
+
pattern: 'TDD approach leads to fewer bugs',
|
|
362
|
+
context: 'Development methodology analysis',
|
|
363
|
+
frequency: 5,
|
|
364
|
+
successRate: 0.9,
|
|
365
|
+
impact: 'high',
|
|
366
|
+
antiPattern: null,
|
|
367
|
+
mitigation: null,
|
|
368
|
+
examples: ['T001', 'T002', 'T003'],
|
|
369
|
+
confidence: 0.8,
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
const result = await storeDetectedPattern(detected, brainAccessor);
|
|
373
|
+
|
|
374
|
+
expect(brainAccessor.addPattern).toHaveBeenCalledTimes(1);
|
|
375
|
+
const call = (brainAccessor.addPattern as ReturnType<typeof vi.fn>).mock.calls[0][0];
|
|
376
|
+
expect(call.type).toBe('success');
|
|
377
|
+
expect(call.pattern).toBe('TDD approach leads to fewer bugs');
|
|
378
|
+
expect(call.context).toBe('Development methodology analysis');
|
|
379
|
+
expect(call.frequency).toBe(5);
|
|
380
|
+
expect(call.successRate).toBe(0.9);
|
|
381
|
+
expect(call.impact).toBe('high');
|
|
382
|
+
expect(call.examplesJson).toBe(JSON.stringify(['T001', 'T002', 'T003']));
|
|
383
|
+
expect(call.id).toMatch(/^P-/);
|
|
384
|
+
expect(result).toBeDefined();
|
|
385
|
+
});
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
describe('updatePatternStats', () => {
|
|
389
|
+
it('returns null for non-existent pattern', async () => {
|
|
390
|
+
const brainAccessor = mockBrainAccessor();
|
|
391
|
+
|
|
392
|
+
const result = await updatePatternStats('P-nonexistent', true, brainAccessor);
|
|
393
|
+
|
|
394
|
+
expect(result).toBeNull();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('increments frequency and recalculates success rate on success', async () => {
|
|
398
|
+
const existing = makePattern({
|
|
399
|
+
id: 'P-001',
|
|
400
|
+
frequency: 4,
|
|
401
|
+
successRate: 0.75,
|
|
402
|
+
});
|
|
403
|
+
const brainAccessor = mockBrainAccessor([existing]);
|
|
404
|
+
|
|
405
|
+
const result = await updatePatternStats('P-001', true, brainAccessor);
|
|
406
|
+
|
|
407
|
+
expect(result).not.toBeNull();
|
|
408
|
+
expect(result!.patternId).toBe('P-001');
|
|
409
|
+
expect(result!.newFrequency).toBe(5);
|
|
410
|
+
expect(result!.outcomeSuccess).toBe(true);
|
|
411
|
+
// New rate = (0.75 * 4 + 1) / 5 = 4.0 / 5 = 0.8
|
|
412
|
+
expect(result!.newSuccessRate).toBe(0.8);
|
|
413
|
+
|
|
414
|
+
expect(brainAccessor.updatePattern).toHaveBeenCalledWith('P-001', {
|
|
415
|
+
frequency: 5,
|
|
416
|
+
successRate: 0.8,
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
it('decreases success rate on failure outcome', async () => {
|
|
421
|
+
const existing = makePattern({
|
|
422
|
+
id: 'P-002',
|
|
423
|
+
frequency: 4,
|
|
424
|
+
successRate: 0.75,
|
|
425
|
+
});
|
|
426
|
+
const brainAccessor = mockBrainAccessor([existing]);
|
|
427
|
+
|
|
428
|
+
const result = await updatePatternStats('P-002', false, brainAccessor);
|
|
429
|
+
|
|
430
|
+
expect(result!.newFrequency).toBe(5);
|
|
431
|
+
// New rate = (0.75 * 4 + 0) / 5 = 3.0 / 5 = 0.6
|
|
432
|
+
expect(result!.newSuccessRate).toBe(0.6);
|
|
433
|
+
expect(result!.outcomeSuccess).toBe(false);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('handles null success rate by defaulting to 0.5', async () => {
|
|
437
|
+
const existing = makePattern({
|
|
438
|
+
id: 'P-003',
|
|
439
|
+
frequency: 1,
|
|
440
|
+
successRate: null,
|
|
441
|
+
});
|
|
442
|
+
const brainAccessor = mockBrainAccessor([existing]);
|
|
443
|
+
|
|
444
|
+
const result = await updatePatternStats('P-003', true, brainAccessor);
|
|
445
|
+
|
|
446
|
+
expect(result!.newFrequency).toBe(2);
|
|
447
|
+
// New rate = (0.5 * 1 + 1) / 2 = 1.5 / 2 = 0.75
|
|
448
|
+
expect(result!.newSuccessRate).toBe(0.75);
|
|
449
|
+
});
|
|
450
|
+
});
|