feat(triage): GAP-29 persist enrichment results to enrichment.json

After runEnrichment completes, write enrichment sections and
completenessScore to local enrichment.json. After applyTriageResults
successfully applies labels, persist triageResult with category,
confidence, labels, and triagedAt. Both use readEnrichmentFile +
writeEnrichmentFile with createDefaultEnrichment fallback. Errors
are caught and logged (non-fatal). 2 new tests (15 total).

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Sondre Engebråten
2026-02-12 21:42:06 +01:00
co-authored by Claude Opus 4.6
parent 2120211f00
commit 34f4b41ee6
3 changed files with 129 additions and 7 deletions
@@ -229,6 +229,44 @@ describe('runEnrichment handler', () => {
expect(mockSendError).toHaveBeenCalledWith('GitHub module not installed');
});
it('persists enrichment result to enrichment.json', async () => {
const { readEnrichmentFile, writeEnrichmentFile } = await import('../enrichment-persistence');
mockRunPythonSubprocess.mockReturnValue({
promise: Promise.resolve({
success: true,
data: {
issueNumber: 42,
problem: 'Test problem',
goal: 'Test goal',
scopeIn: ['scope-in'],
scopeOut: ['scope-out'],
acceptanceCriteria: ['AC-1'],
technicalContext: 'Some context',
risksEdgeCases: ['Risk 1'],
confidence: 0.85,
},
}),
});
await trigger('test-project', 42);
expect(readEnrichmentFile).toHaveBeenCalledWith('/fake/project');
expect(writeEnrichmentFile).toHaveBeenCalledWith(
'/fake/project',
expect.objectContaining({
issues: expect.objectContaining({
'42': expect.objectContaining({
enrichment: expect.objectContaining({
problem: 'Test problem',
goal: 'Test goal',
}),
completenessScore: 0.85,
}),
}),
}),
);
});
});
// ============================================
@@ -390,6 +428,30 @@ describe('applyTriageResults handler', () => {
expect(result.succeeded).toBeGreaterThan(0);
});
it('persists triage result to enrichment.json after applying', async () => {
const { readEnrichmentFile, writeEnrichmentFile } = await import('../enrichment-persistence');
mockExecFileSync.mockReturnValue(Buffer.from(''));
const singleItem = [reviewItems[0]]; // accepted item with bug + priority:high
await trigger('test-project', singleItem);
expect(readEnrichmentFile).toHaveBeenCalled();
expect(writeEnrichmentFile).toHaveBeenCalledWith(
'/fake/project',
expect.objectContaining({
issues: expect.objectContaining({
'1': expect.objectContaining({
triageResult: expect.objectContaining({
category: 'bug',
labelsToAdd: ['bug', 'priority:high'],
triagedAt: '2026-01-01T00:00:00Z',
}),
}),
}),
}),
);
});
it('skips rejected items', async () => {
const allRejected = reviewItems.map(item => ({ ...item, status: 'rejected' as const }));
mockExecFileSync.mockReturnValue(Buffer.from(''));
@@ -31,6 +31,9 @@ import {
} from './utils/subprocess-runner';
import { MAX_SPLIT_SUB_ISSUES } from '../../../shared/constants/ai-triage';
import { createDefaultProgressiveTrust } from '../../../shared/types/ai-triage';
import { readEnrichmentFile, writeEnrichmentFile } from './enrichment-persistence';
import { createDefaultEnrichment } from '../../../shared/types/enrichment';
import type { TriageCategory } from '../../../shared/types/enrichment';
import type {
AIEnrichmentResult,
SplitSuggestion,
@@ -141,7 +144,35 @@ export function registerAITriageHandlers(
return;
}
sendComplete(result.data as AIEnrichmentResult);
const enrichmentResult = result.data as AIEnrichmentResult;
sendComplete(enrichmentResult);
// Persist enrichment data to local file
try {
const enrichmentFile = await readEnrichmentFile(project.path);
const key = String(issueNumber);
const existing = enrichmentFile.issues[key] ?? createDefaultEnrichment(issueNumber);
enrichmentFile.issues[key] = {
...existing,
enrichment: {
problem: enrichmentResult.problem,
goal: enrichmentResult.goal,
scopeIn: enrichmentResult.scopeIn,
scopeOut: enrichmentResult.scopeOut,
acceptanceCriteria: enrichmentResult.acceptanceCriteria,
technicalContext: enrichmentResult.technicalContext,
risksEdgeCases: enrichmentResult.risksEdgeCases,
},
completenessScore: enrichmentResult.confidence,
updatedAt: new Date().toISOString(),
};
await writeEnrichmentFile(project.path, enrichmentFile);
} catch (persistErr) {
debugLog('Failed to persist enrichment result', {
issueNumber,
error: persistErr instanceof Error ? persistErr.message : persistErr,
});
}
});
} catch (error) {
sendError(error instanceof Error ? error.message : 'Failed to run enrichment');
@@ -309,6 +340,35 @@ export function registerAITriageHandlers(
}
succeeded++;
// Persist triage result to enrichment file
try {
const enrichmentFile = await readEnrichmentFile(project.path);
const key = String(item.issueNumber);
const existing = enrichmentFile.issues[key] ?? createDefaultEnrichment(item.issueNumber);
enrichmentFile.issues[key] = {
...existing,
triageResult: {
category: item.result.category as TriageCategory,
confidence: item.result.confidence,
labelsToAdd: item.result.labelsToAdd,
labelsToRemove: item.result.labelsToRemove,
isDuplicate: item.result.isDuplicate,
duplicateOf: item.result.duplicateOf,
isSpam: item.result.isSpam,
suggestedBreakdown: item.result.suggestedBreakdown,
comment: item.result.comment,
triagedAt: item.result.triagedAt,
},
updatedAt: new Date().toISOString(),
};
await writeEnrichmentFile(project.path, enrichmentFile);
} catch (persistErr) {
debugLog('Failed to persist triage result', {
issueNumber: item.issueNumber,
error: persistErr instanceof Error ? persistErr.message : persistErr,
});
}
} catch (error) {
debugLog('Failed to apply results to issue', {
issueNumber: item.issueNumber,
+6 -6
View File
@@ -3,7 +3,7 @@
**Branch:** `terminal/enhancement-issues-tab`
**Created:** 2026-02-12
**Total Gaps:** 41 confirmed (from triple-verified audit)
**Status:** 19 / 41 complete
**Status:** 20 / 41 complete
---
@@ -357,16 +357,16 @@ Each gap has: ID, description, status, files to modify, doc reference, test stat
- **Commit:** —
### GAP-29: No enrichment persistence to local files after AI triage
- **Status:** `PENDING`
- **Status:** `DONE`
- **Priority:** MUST-FIX
- **Scope:** Medium
- **Doc ref:** Phase 3 PRD > US-1 > AC1.11; Phase 3 PRD > US-5 > AC5.9
- **Files to modify:** `main/ipc-handlers/github/ai-triage-handlers.ts`
- **Fix:** After sendComplete(), import readEnrichmentFile/writeEnrichmentFile from enrichment-persistence. Persist triageResult and lastTriagedAt to enrichment.json. Same in applyTriageResults.
- **Tests:** Run enrichment → enrichment.json contains triageResult + lastTriagedAt
- **Test status:** `PENDING`
- **Fix:** After sendComplete() in runEnrichment, persist enrichment sections + completenessScore to enrichment.json. After successful label apply in applyTriageResults, persist triageResult. Both use readEnrichmentFile/writeEnrichmentFile with createDefaultEnrichment fallback. Errors caught and logged (non-fatal).
- **Tests:** 2 new tests: runEnrichment persists to file, applyTriageResults persists triageResult. 15 total.
- **Test status:** `PASS`
- **Depends on:** None
- **Commit:** —
- **Commit:** pending
### GAP-30: No `actor: 'ai-triage'` audit trail
- **Status:** `PENDING`