@soleri/core 9.9.0 → 9.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/brain/intelligence.d.ts.map +1 -1
- package/dist/brain/intelligence.js +4 -0
- package/dist/brain/intelligence.js.map +1 -1
- package/dist/brain/types.d.ts +1 -1
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/paths.d.ts +10 -0
- package/dist/paths.d.ts.map +1 -1
- package/dist/paths.js +41 -2
- package/dist/paths.js.map +1 -1
- package/dist/planning/plan-lifecycle.d.ts.map +1 -1
- package/dist/planning/plan-lifecycle.js +4 -2
- package/dist/planning/plan-lifecycle.js.map +1 -1
- package/dist/planning/planner-types.d.ts +1 -1
- package/dist/planning/planner-types.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.d.ts.map +1 -1
- package/dist/runtime/admin-setup-ops.js +29 -4
- package/dist/runtime/admin-setup-ops.js.map +1 -1
- package/dist/runtime/capture-ops.js +2 -2
- package/dist/runtime/capture-ops.js.map +1 -1
- package/dist/runtime/claude-md-helpers.d.ts +11 -0
- package/dist/runtime/claude-md-helpers.d.ts.map +1 -1
- package/dist/runtime/claude-md-helpers.js +18 -0
- package/dist/runtime/claude-md-helpers.js.map +1 -1
- package/dist/runtime/facades/memory-facade.d.ts.map +1 -1
- package/dist/runtime/facades/memory-facade.js +2 -1
- package/dist/runtime/facades/memory-facade.js.map +1 -1
- package/dist/runtime/orchestrate-ops.d.ts.map +1 -1
- package/dist/runtime/orchestrate-ops.js +6 -3
- package/dist/runtime/orchestrate-ops.js.map +1 -1
- package/dist/runtime/quality-signals.d.ts +6 -1
- package/dist/runtime/quality-signals.d.ts.map +1 -1
- package/dist/runtime/quality-signals.js +41 -5
- package/dist/runtime/quality-signals.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/paths.test.ts +31 -0
- package/src/brain/intelligence.ts +4 -0
- package/src/brain/types.ts +6 -1
- package/src/index.ts +1 -0
- package/src/paths.ts +42 -2
- package/src/planning/plan-lifecycle.ts +5 -2
- package/src/planning/planner-types.ts +1 -1
- package/src/planning/planner.test.ts +71 -0
- package/src/runtime/admin-setup-ops.ts +31 -3
- package/src/runtime/capture-ops.ts +2 -2
- package/src/runtime/claude-md-helpers.test.ts +81 -0
- package/src/runtime/claude-md-helpers.ts +25 -0
- package/src/runtime/facades/memory-facade.ts +2 -1
- package/src/runtime/orchestrate-ops.test.ts +51 -2
- package/src/runtime/orchestrate-ops.ts +12 -3
- package/src/runtime/quality-signals.test.ts +182 -8
- package/src/runtime/quality-signals.ts +44 -5
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
composeIntegrationSection,
|
|
13
13
|
buildInjectionContent,
|
|
14
14
|
injectEngineRulesBlock,
|
|
15
|
+
removeEngineRulesFromGlobal,
|
|
15
16
|
} from './claude-md-helpers.js';
|
|
16
17
|
import type { AgentRuntimeConfig } from './types.js';
|
|
17
18
|
|
|
@@ -186,4 +187,84 @@ describe('injectEngineRulesBlock', () => {
|
|
|
186
187
|
expect(result).toContain('AFTER');
|
|
187
188
|
expect(result).toContain('REPLACED');
|
|
188
189
|
});
|
|
190
|
+
|
|
191
|
+
it('handles empty content by appending rules', () => {
|
|
192
|
+
const result = injectEngineRulesBlock('', 'RULES');
|
|
193
|
+
expect(result).toContain('RULES');
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('is idempotent — double injection replaces cleanly', () => {
|
|
197
|
+
const first = injectEngineRulesBlock('# File', `${RULES_START}\nV1\n${RULES_END}`);
|
|
198
|
+
const second = injectEngineRulesBlock(first, `${RULES_START}\nV2\n${RULES_END}`);
|
|
199
|
+
expect(second).toContain('V2');
|
|
200
|
+
expect(second).not.toContain('V1');
|
|
201
|
+
// Should have exactly one start marker
|
|
202
|
+
const startCount = (second.match(/<!-- soleri:engine-rules -->/g) || []).length;
|
|
203
|
+
expect(startCount).toBe(1);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe('removeEngineRulesFromGlobal', () => {
|
|
208
|
+
const RULES_START = '<!-- soleri:engine-rules -->';
|
|
209
|
+
const RULES_END = '<!-- /soleri:engine-rules -->';
|
|
210
|
+
|
|
211
|
+
it('returns unchanged content when no engine rules present', () => {
|
|
212
|
+
const content = '# Global CLAUDE.md\n\nSome user content.';
|
|
213
|
+
const { cleaned, removed } = removeEngineRulesFromGlobal(content);
|
|
214
|
+
expect(removed).toBe(false);
|
|
215
|
+
expect(cleaned).toBe(content);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
it('removes engine rules block from content', () => {
|
|
219
|
+
const content = `# Global\n\n${RULES_START}\nEngine rules here\n${RULES_END}\n\nUser content`;
|
|
220
|
+
const { cleaned, removed } = removeEngineRulesFromGlobal(content);
|
|
221
|
+
expect(removed).toBe(true);
|
|
222
|
+
expect(cleaned).not.toContain('Engine rules here');
|
|
223
|
+
expect(cleaned).not.toContain(RULES_START);
|
|
224
|
+
expect(cleaned).not.toContain(RULES_END);
|
|
225
|
+
expect(cleaned).toContain('# Global');
|
|
226
|
+
expect(cleaned).toContain('User content');
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it('handles engine rules at end of file', () => {
|
|
230
|
+
const content = `# Global\n\n${RULES_START}\nRules\n${RULES_END}`;
|
|
231
|
+
const { cleaned, removed } = removeEngineRulesFromGlobal(content);
|
|
232
|
+
expect(removed).toBe(true);
|
|
233
|
+
expect(cleaned).toContain('# Global');
|
|
234
|
+
expect(cleaned).not.toContain('Rules');
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('handles engine rules at start of file', () => {
|
|
238
|
+
const content = `${RULES_START}\nRules\n${RULES_END}\n\n# Global`;
|
|
239
|
+
const { cleaned, removed } = removeEngineRulesFromGlobal(content);
|
|
240
|
+
expect(removed).toBe(true);
|
|
241
|
+
expect(cleaned).toContain('# Global');
|
|
242
|
+
expect(cleaned).not.toContain(RULES_START);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('preserves agent blocks when removing engine rules', () => {
|
|
246
|
+
const content = [
|
|
247
|
+
'# Global',
|
|
248
|
+
'',
|
|
249
|
+
'<!-- agent:mybot:mode -->',
|
|
250
|
+
'Agent content',
|
|
251
|
+
'<!-- /agent:mybot:mode -->',
|
|
252
|
+
'',
|
|
253
|
+
RULES_START,
|
|
254
|
+
'Engine rules',
|
|
255
|
+
RULES_END,
|
|
256
|
+
].join('\n');
|
|
257
|
+
const { cleaned, removed } = removeEngineRulesFromGlobal(content);
|
|
258
|
+
expect(removed).toBe(true);
|
|
259
|
+
expect(cleaned).toContain('<!-- agent:mybot:mode -->');
|
|
260
|
+
expect(cleaned).toContain('Agent content');
|
|
261
|
+
expect(cleaned).not.toContain('Engine rules');
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('returns non-empty string for content that is only engine rules', () => {
|
|
265
|
+
const content = `${RULES_START}\nOnly rules\n${RULES_END}`;
|
|
266
|
+
const { cleaned, removed } = removeEngineRulesFromGlobal(content);
|
|
267
|
+
expect(removed).toBe(true);
|
|
268
|
+
expect(cleaned.length).toBeGreaterThan(0);
|
|
269
|
+
});
|
|
189
270
|
});
|
|
@@ -216,3 +216,28 @@ export function injectEngineRulesBlock(content: string, engineRulesContent: stri
|
|
|
216
216
|
// Append
|
|
217
217
|
return content.trimEnd() + '\n\n' + engineRulesContent + '\n';
|
|
218
218
|
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Remove engine rules block from content.
|
|
222
|
+
* Used during self-healing to strip engine rules from global CLAUDE.md
|
|
223
|
+
* when they were incorrectly injected there (they belong in _engine.md).
|
|
224
|
+
*
|
|
225
|
+
* Returns the content without the engine rules block, or unchanged if no block found.
|
|
226
|
+
*/
|
|
227
|
+
export function removeEngineRulesFromGlobal(content: string): {
|
|
228
|
+
cleaned: string;
|
|
229
|
+
removed: boolean;
|
|
230
|
+
} {
|
|
231
|
+
if (!hasEngineRules(content)) {
|
|
232
|
+
return { cleaned: content, removed: false };
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const startIdx = content.indexOf(ENGINE_RULES_START);
|
|
236
|
+
const endIdx = content.indexOf(ENGINE_RULES_END);
|
|
237
|
+
|
|
238
|
+
const before = content.slice(0, startIdx).trimEnd();
|
|
239
|
+
const after = content.slice(endIdx + ENGINE_RULES_END.length).trimStart();
|
|
240
|
+
|
|
241
|
+
const cleaned = before + (before && after ? '\n\n' : '') + after;
|
|
242
|
+
return { cleaned: cleaned || '\n', removed: true };
|
|
243
|
+
}
|
|
@@ -151,7 +151,8 @@ export function createMemoryFacadeOps(runtime: AgentRuntime): OpDefinition[] {
|
|
|
151
151
|
}),
|
|
152
152
|
handler: async (params) => {
|
|
153
153
|
const { resolve } = await import('node:path');
|
|
154
|
-
const
|
|
154
|
+
const { findProjectRoot } = await import('../../paths.js');
|
|
155
|
+
const projectPath = findProjectRoot(resolve((params.projectPath as string) ?? '.'));
|
|
155
156
|
const summary = (params.summary ?? params.conversationContext) as string;
|
|
156
157
|
if (!summary) {
|
|
157
158
|
return { captured: false, error: 'Either summary or conversationContext is required.' };
|
|
@@ -363,10 +363,10 @@ describe('createOrchestrateOps', () => {
|
|
|
363
363
|
outcome: 'completed',
|
|
364
364
|
})) as Record<string, unknown>;
|
|
365
365
|
|
|
366
|
-
// Should complete successfully
|
|
366
|
+
// Should complete successfully with evidenceReport: null
|
|
367
367
|
expect(result).toHaveProperty('plan');
|
|
368
368
|
expect(result).toHaveProperty('session');
|
|
369
|
-
expect(result).
|
|
369
|
+
expect(result.evidenceReport).toBeNull();
|
|
370
370
|
});
|
|
371
371
|
|
|
372
372
|
it('adds warning when evidence accuracy is below 50%', async () => {
|
|
@@ -395,6 +395,55 @@ describe('createOrchestrateOps', () => {
|
|
|
395
395
|
const warnings = result.warnings as string[];
|
|
396
396
|
expect(warnings.some((w) => w.includes('Low evidence accuracy (30%)'))).toBe(true);
|
|
397
397
|
});
|
|
398
|
+
|
|
399
|
+
it('runs evidence collection for abandoned plans too', async () => {
|
|
400
|
+
const { collectGitEvidence } = await import('../planning/evidence-collector.js');
|
|
401
|
+
vi.mocked(collectGitEvidence).mockReturnValueOnce({
|
|
402
|
+
planId: 'plan-1',
|
|
403
|
+
planObjective: 'test',
|
|
404
|
+
accuracy: 60,
|
|
405
|
+
evidenceSources: ['git'],
|
|
406
|
+
taskEvidence: [
|
|
407
|
+
{
|
|
408
|
+
taskId: 't1',
|
|
409
|
+
taskTitle: 'Task 1',
|
|
410
|
+
plannedStatus: 'pending',
|
|
411
|
+
matchedFiles: [],
|
|
412
|
+
verdict: 'MISSING',
|
|
413
|
+
},
|
|
414
|
+
],
|
|
415
|
+
unplannedChanges: [],
|
|
416
|
+
missingWork: [],
|
|
417
|
+
verificationGaps: [],
|
|
418
|
+
summary: '0/1 tasks verified by git evidence',
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
422
|
+
const result = (await op.handler({
|
|
423
|
+
planId: 'plan-1',
|
|
424
|
+
sessionId: 'session-1',
|
|
425
|
+
outcome: 'abandoned',
|
|
426
|
+
projectPath: '.',
|
|
427
|
+
})) as Record<string, unknown>;
|
|
428
|
+
|
|
429
|
+
expect(collectGitEvidence).toHaveBeenCalled();
|
|
430
|
+
expect(result).toHaveProperty('evidenceReport');
|
|
431
|
+
const report = result.evidenceReport as Record<string, unknown>;
|
|
432
|
+
expect(report.accuracy).toBe(60);
|
|
433
|
+
expect(Array.isArray(report.taskEvidence)).toBe(true);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
it('returns evidenceReport as null when no plan is provided', async () => {
|
|
437
|
+
const op = findOp(ops, 'orchestrate_complete');
|
|
438
|
+
const result = (await op.handler({
|
|
439
|
+
sessionId: 'session-1',
|
|
440
|
+
outcome: 'completed',
|
|
441
|
+
summary: 'Direct task without a plan',
|
|
442
|
+
})) as Record<string, unknown>;
|
|
443
|
+
|
|
444
|
+
expect(result).toHaveProperty('evidenceReport');
|
|
445
|
+
expect(result.evidenceReport).toBeNull();
|
|
446
|
+
});
|
|
398
447
|
});
|
|
399
448
|
|
|
400
449
|
// ─── orchestrate_status ───────────────────────────────────────
|
|
@@ -43,7 +43,11 @@ import type { ImpactReport } from '../planning/impact-analyzer.js';
|
|
|
43
43
|
import { collectGitEvidence } from '../planning/evidence-collector.js';
|
|
44
44
|
import type { EvidenceReport } from '../planning/evidence-collector.js';
|
|
45
45
|
import { recordPlanFeedback } from './plan-feedback-helper.js';
|
|
46
|
-
import {
|
|
46
|
+
import {
|
|
47
|
+
analyzeQualitySignals,
|
|
48
|
+
captureQualitySignals,
|
|
49
|
+
buildFixTrailSummary,
|
|
50
|
+
} from './quality-signals.js';
|
|
47
51
|
|
|
48
52
|
// ---------------------------------------------------------------------------
|
|
49
53
|
// Intent detection — keyword-based mapping from prompt to intent
|
|
@@ -832,7 +836,7 @@ export function createOrchestrateOps(
|
|
|
832
836
|
|
|
833
837
|
// Evidence-based reconciliation: cross-reference plan tasks against git diff
|
|
834
838
|
let evidenceReport: EvidenceReport | null = null;
|
|
835
|
-
if (planObj
|
|
839
|
+
if (planObj) {
|
|
836
840
|
try {
|
|
837
841
|
evidenceReport = collectGitEvidence(
|
|
838
842
|
planObj,
|
|
@@ -840,6 +844,9 @@ export function createOrchestrateOps(
|
|
|
840
844
|
'main',
|
|
841
845
|
);
|
|
842
846
|
if (evidenceReport.accuracy < 50) {
|
|
847
|
+
console.error(
|
|
848
|
+
`[soleri] Evidence accuracy ${evidenceReport.accuracy}% — significant drift detected between plan and git state`,
|
|
849
|
+
);
|
|
843
850
|
warnings.push(
|
|
844
851
|
`Low evidence accuracy (${evidenceReport.accuracy}%) — plan tasks may not match git changes.`,
|
|
845
852
|
);
|
|
@@ -873,6 +880,7 @@ export function createOrchestrateOps(
|
|
|
873
880
|
}
|
|
874
881
|
|
|
875
882
|
// End brain session — runs regardless of plan existence
|
|
883
|
+
const fixTrail = evidenceReport ? buildFixTrailSummary(evidenceReport) : undefined;
|
|
876
884
|
const session = brainIntelligence.lifecycle({
|
|
877
885
|
action: 'end',
|
|
878
886
|
sessionId,
|
|
@@ -880,6 +888,7 @@ export function createOrchestrateOps(
|
|
|
880
888
|
planOutcome: outcome,
|
|
881
889
|
toolsUsed,
|
|
882
890
|
filesModified,
|
|
891
|
+
...(fixTrail ? { context: `Fix trail: ${fixTrail}` } : {}),
|
|
883
892
|
});
|
|
884
893
|
|
|
885
894
|
// Record brain feedback for vault entries referenced in plan decisions
|
|
@@ -974,7 +983,7 @@ export function createOrchestrateOps(
|
|
|
974
983
|
extraction,
|
|
975
984
|
epilogue: epilogueResult,
|
|
976
985
|
...(impactReport ? { impactAnalysis: impactReport } : {}),
|
|
977
|
-
|
|
986
|
+
evidenceReport,
|
|
978
987
|
...(warnings.length > 0 ? { warnings } : {}),
|
|
979
988
|
};
|
|
980
989
|
},
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
analyzeQualitySignals,
|
|
4
|
+
captureQualitySignals,
|
|
5
|
+
buildFixTrailSummary,
|
|
6
|
+
} from './quality-signals.js';
|
|
3
7
|
import type { EvidenceReport } from '../planning/evidence-collector.js';
|
|
4
8
|
|
|
5
9
|
// ---------------------------------------------------------------------------
|
|
@@ -104,7 +108,7 @@ describe('analyzeQualitySignals', () => {
|
|
|
104
108
|
expect(result.antiPatterns).toHaveLength(0);
|
|
105
109
|
});
|
|
106
110
|
|
|
107
|
-
it('
|
|
111
|
+
it('flags task with fixIterations === 2 (at threshold)', () => {
|
|
108
112
|
const report = makeReport({
|
|
109
113
|
taskEvidence: [
|
|
110
114
|
{
|
|
@@ -120,7 +124,28 @@ describe('analyzeQualitySignals', () => {
|
|
|
120
124
|
|
|
121
125
|
const result = analyzeQualitySignals(report);
|
|
122
126
|
|
|
127
|
+
expect(result.antiPatterns).toHaveLength(1);
|
|
128
|
+
expect(result.antiPatterns[0].fixIterations).toBe(2);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('does not flag task with fixIterations === 1 (below threshold)', () => {
|
|
132
|
+
const report = makeReport({
|
|
133
|
+
taskEvidence: [
|
|
134
|
+
{
|
|
135
|
+
taskId: 't4b',
|
|
136
|
+
taskTitle: 'Single retry task',
|
|
137
|
+
plannedStatus: 'completed',
|
|
138
|
+
matchedFiles: [],
|
|
139
|
+
verdict: 'DONE',
|
|
140
|
+
fixIterations: 1,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const result = analyzeQualitySignals(report);
|
|
146
|
+
|
|
123
147
|
expect(result.antiPatterns).toHaveLength(0);
|
|
148
|
+
expect(result.cleanTasks).toHaveLength(0);
|
|
124
149
|
});
|
|
125
150
|
|
|
126
151
|
it('detects scope creep from unplanned changes', () => {
|
|
@@ -199,9 +224,14 @@ describe('captureQualitySignals', () => {
|
|
|
199
224
|
expect(entry.tags).toContain('auto-captured');
|
|
200
225
|
|
|
201
226
|
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
227
|
+
expect.objectContaining({
|
|
228
|
+
query: 'Fix login',
|
|
229
|
+
entryId: 'plan-1',
|
|
230
|
+
action: 'dismissed',
|
|
231
|
+
confidence: 0.7,
|
|
232
|
+
source: 'evidence-quality',
|
|
233
|
+
reason: 'Task needed 3 fix iterations — high rework',
|
|
234
|
+
}),
|
|
205
235
|
);
|
|
206
236
|
|
|
207
237
|
expect(result.captured).toBe(1);
|
|
@@ -226,9 +256,14 @@ describe('captureQualitySignals', () => {
|
|
|
226
256
|
const result = captureQualitySignals(analysis, vault, brain, 'plan-1');
|
|
227
257
|
|
|
228
258
|
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
259
|
+
expect.objectContaining({
|
|
260
|
+
query: 'Add feature',
|
|
261
|
+
entryId: 'plan-1',
|
|
262
|
+
action: 'accepted',
|
|
263
|
+
confidence: 0.9,
|
|
264
|
+
source: 'evidence-quality',
|
|
265
|
+
reason: 'Clean first-try completion — no rework needed',
|
|
266
|
+
}),
|
|
232
267
|
);
|
|
233
268
|
expect(result.feedback).toBe(1);
|
|
234
269
|
expect(result.captured).toBe(0);
|
|
@@ -281,6 +316,83 @@ describe('captureQualitySignals', () => {
|
|
|
281
316
|
expect(entry.severity).toBe('critical');
|
|
282
317
|
});
|
|
283
318
|
|
|
319
|
+
it('records positive feedback with evidence-quality source for clean first-try tasks', () => {
|
|
320
|
+
const analysis = {
|
|
321
|
+
antiPatterns: [],
|
|
322
|
+
cleanTasks: [
|
|
323
|
+
{
|
|
324
|
+
taskId: 'clean-1',
|
|
325
|
+
taskTitle: 'Smooth task',
|
|
326
|
+
kind: 'clean' as const,
|
|
327
|
+
fixIterations: 0,
|
|
328
|
+
verdict: 'DONE',
|
|
329
|
+
},
|
|
330
|
+
],
|
|
331
|
+
scopeCreep: [],
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
captureQualitySignals(analysis, vault, brain, 'plan-99');
|
|
335
|
+
|
|
336
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
337
|
+
expect.objectContaining({
|
|
338
|
+
action: 'accepted',
|
|
339
|
+
confidence: 0.9,
|
|
340
|
+
source: 'evidence-quality',
|
|
341
|
+
entryId: 'plan-99',
|
|
342
|
+
}),
|
|
343
|
+
);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('records negative feedback with evidence-quality source for high-rework tasks', () => {
|
|
347
|
+
const analysis = {
|
|
348
|
+
antiPatterns: [
|
|
349
|
+
{
|
|
350
|
+
taskId: 'rework-1',
|
|
351
|
+
taskTitle: 'Painful task',
|
|
352
|
+
kind: 'anti-pattern' as const,
|
|
353
|
+
fixIterations: 3,
|
|
354
|
+
verdict: 'DONE',
|
|
355
|
+
},
|
|
356
|
+
],
|
|
357
|
+
cleanTasks: [],
|
|
358
|
+
scopeCreep: [],
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
captureQualitySignals(analysis, vault, brain, 'plan-99');
|
|
362
|
+
|
|
363
|
+
expect(brain.recordFeedback).toHaveBeenCalledWith(
|
|
364
|
+
expect.objectContaining({
|
|
365
|
+
action: 'dismissed',
|
|
366
|
+
confidence: 0.7,
|
|
367
|
+
source: 'evidence-quality',
|
|
368
|
+
reason: 'Task needed 3 fix iterations — high rework',
|
|
369
|
+
context: JSON.stringify({ taskId: 'rework-1', reworkCount: 3, verdict: 'DONE' }),
|
|
370
|
+
}),
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('does not record evidence-quality feedback for tasks with 1 fix iteration', () => {
|
|
375
|
+
// 1 fix iteration = neither clean (fixIterations !== 0) nor anti-pattern (< 2)
|
|
376
|
+
const analysis = analyzeQualitySignals(
|
|
377
|
+
makeReport({
|
|
378
|
+
taskEvidence: [
|
|
379
|
+
{
|
|
380
|
+
taskId: 't-mid',
|
|
381
|
+
taskTitle: 'Single retry',
|
|
382
|
+
plannedStatus: 'completed',
|
|
383
|
+
matchedFiles: [],
|
|
384
|
+
verdict: 'DONE',
|
|
385
|
+
fixIterations: 1,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
}),
|
|
389
|
+
);
|
|
390
|
+
|
|
391
|
+
captureQualitySignals(analysis, vault, brain, 'plan-99');
|
|
392
|
+
|
|
393
|
+
expect(brain.recordFeedback).not.toHaveBeenCalled();
|
|
394
|
+
});
|
|
395
|
+
|
|
284
396
|
it('handles mixed signals correctly', () => {
|
|
285
397
|
const analysis = {
|
|
286
398
|
antiPatterns: [
|
|
@@ -310,3 +422,65 @@ describe('captureQualitySignals', () => {
|
|
|
310
422
|
expect(result.feedback).toBe(2); // 1 dismissed + 1 accepted
|
|
311
423
|
});
|
|
312
424
|
});
|
|
425
|
+
|
|
426
|
+
// ---------------------------------------------------------------------------
|
|
427
|
+
// buildFixTrailSummary
|
|
428
|
+
// ---------------------------------------------------------------------------
|
|
429
|
+
|
|
430
|
+
describe('buildFixTrailSummary', () => {
|
|
431
|
+
it('returns summary string for tasks with rework iterations', () => {
|
|
432
|
+
const report = makeReport({
|
|
433
|
+
taskEvidence: [
|
|
434
|
+
{
|
|
435
|
+
taskId: 'a',
|
|
436
|
+
taskTitle: 'Task A',
|
|
437
|
+
plannedStatus: 'completed',
|
|
438
|
+
matchedFiles: [],
|
|
439
|
+
verdict: 'DONE',
|
|
440
|
+
fixIterations: 2,
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
taskId: 'b',
|
|
444
|
+
taskTitle: 'Task B',
|
|
445
|
+
plannedStatus: 'completed',
|
|
446
|
+
matchedFiles: [],
|
|
447
|
+
verdict: 'DONE',
|
|
448
|
+
fixIterations: 0,
|
|
449
|
+
},
|
|
450
|
+
{
|
|
451
|
+
taskId: 'c',
|
|
452
|
+
taskTitle: 'Task C',
|
|
453
|
+
plannedStatus: 'completed',
|
|
454
|
+
matchedFiles: [],
|
|
455
|
+
verdict: 'DONE',
|
|
456
|
+
fixIterations: 3,
|
|
457
|
+
},
|
|
458
|
+
],
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
const summary = buildFixTrailSummary(report);
|
|
462
|
+
expect(summary).toBe('Task A: 2 fix iterations; Task C: 3 fix iterations');
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
it('returns undefined when no tasks have rework', () => {
|
|
466
|
+
const report = makeReport({
|
|
467
|
+
taskEvidence: [
|
|
468
|
+
{
|
|
469
|
+
taskId: 'a',
|
|
470
|
+
taskTitle: 'Clean',
|
|
471
|
+
plannedStatus: 'completed',
|
|
472
|
+
matchedFiles: [],
|
|
473
|
+
verdict: 'DONE',
|
|
474
|
+
fixIterations: 0,
|
|
475
|
+
},
|
|
476
|
+
],
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
expect(buildFixTrailSummary(report)).toBeUndefined();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
it('returns undefined for empty task evidence', () => {
|
|
483
|
+
const report = makeReport({ taskEvidence: [] });
|
|
484
|
+
expect(buildFixTrailSummary(report)).toBeUndefined();
|
|
485
|
+
});
|
|
486
|
+
});
|
|
@@ -34,8 +34,12 @@ export interface QualityAnalysis {
|
|
|
34
34
|
// Thresholds
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
36
36
|
|
|
37
|
-
/** Tasks with
|
|
37
|
+
/** Tasks with this many or more fix iterations are flagged as anti-patterns. */
|
|
38
38
|
const REWORK_THRESHOLD = 2;
|
|
39
|
+
/** Brain feedback confidence for clean first-try tasks. */
|
|
40
|
+
const CLEAN_TASK_CONFIDENCE = 0.9;
|
|
41
|
+
/** Brain feedback confidence for high-rework anti-pattern tasks. */
|
|
42
|
+
const REWORK_TASK_CONFIDENCE = 0.7;
|
|
39
43
|
|
|
40
44
|
// ---------------------------------------------------------------------------
|
|
41
45
|
// Analysis
|
|
@@ -44,7 +48,7 @@ const REWORK_THRESHOLD = 2;
|
|
|
44
48
|
/**
|
|
45
49
|
* Analyze an evidence report for quality signals.
|
|
46
50
|
*
|
|
47
|
-
* - fixIterations
|
|
51
|
+
* - fixIterations >= 2 → anti-pattern (rework)
|
|
48
52
|
* - fixIterations === 0 + verdict DONE → clean (first-pass success)
|
|
49
53
|
* - unplannedChanges → scope-creep signals
|
|
50
54
|
*/
|
|
@@ -59,7 +63,7 @@ export function analyzeQualitySignals(
|
|
|
59
63
|
for (const te of report.taskEvidence) {
|
|
60
64
|
const iterations = te.fixIterations ?? 0;
|
|
61
65
|
|
|
62
|
-
if (iterations
|
|
66
|
+
if (iterations >= REWORK_THRESHOLD) {
|
|
63
67
|
antiPatterns.push({
|
|
64
68
|
taskId: te.taskId,
|
|
65
69
|
taskTitle: te.taskTitle,
|
|
@@ -148,7 +152,19 @@ export function captureQualitySignals(
|
|
|
148
152
|
// Record negative brain feedback for rework tasks
|
|
149
153
|
for (const ap of analysis.antiPatterns) {
|
|
150
154
|
try {
|
|
151
|
-
brain.recordFeedback(
|
|
155
|
+
brain.recordFeedback({
|
|
156
|
+
query: ap.taskTitle,
|
|
157
|
+
entryId: planId,
|
|
158
|
+
action: 'dismissed',
|
|
159
|
+
confidence: REWORK_TASK_CONFIDENCE,
|
|
160
|
+
source: 'evidence-quality',
|
|
161
|
+
reason: `Task needed ${ap.fixIterations} fix iterations — high rework`,
|
|
162
|
+
context: JSON.stringify({
|
|
163
|
+
taskId: ap.taskId,
|
|
164
|
+
reworkCount: ap.fixIterations,
|
|
165
|
+
verdict: ap.verdict,
|
|
166
|
+
}),
|
|
167
|
+
});
|
|
152
168
|
feedback++;
|
|
153
169
|
} catch {
|
|
154
170
|
// Best-effort
|
|
@@ -158,7 +174,14 @@ export function captureQualitySignals(
|
|
|
158
174
|
// Record positive brain feedback for clean tasks
|
|
159
175
|
for (const ct of analysis.cleanTasks) {
|
|
160
176
|
try {
|
|
161
|
-
brain.recordFeedback(
|
|
177
|
+
brain.recordFeedback({
|
|
178
|
+
query: ct.taskTitle,
|
|
179
|
+
entryId: planId,
|
|
180
|
+
action: 'accepted',
|
|
181
|
+
confidence: CLEAN_TASK_CONFIDENCE,
|
|
182
|
+
source: 'evidence-quality',
|
|
183
|
+
reason: 'Clean first-try completion — no rework needed',
|
|
184
|
+
});
|
|
162
185
|
feedback++;
|
|
163
186
|
} catch {
|
|
164
187
|
// Best-effort
|
|
@@ -167,3 +190,19 @@ export function captureQualitySignals(
|
|
|
167
190
|
|
|
168
191
|
return { captured, skipped, feedback };
|
|
169
192
|
}
|
|
193
|
+
|
|
194
|
+
// ---------------------------------------------------------------------------
|
|
195
|
+
// Fix-trail summary for knowledge extraction
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Build a human-readable fix-trail summary from an evidence report.
|
|
200
|
+
* Returns `undefined` when no tasks had rework iterations.
|
|
201
|
+
*/
|
|
202
|
+
export function buildFixTrailSummary(report: EvidenceReport): string | undefined {
|
|
203
|
+
const entries = report.taskEvidence
|
|
204
|
+
.filter((te) => (te.fixIterations ?? 0) > 0)
|
|
205
|
+
.map((te) => `${te.taskTitle}: ${te.fixIterations} fix iterations`);
|
|
206
|
+
|
|
207
|
+
return entries.length > 0 ? entries.join('; ') : undefined;
|
|
208
|
+
}
|