agent-ecosystem/src/main/services/team/cache/taskChangeSummaryCacheSchema.ts
iliya 946ccb692c feat: persist safe task change summaries
Restore terminal task-change badges quickly across restarts without trusting stale snapshots, and keep kanban card review actions compact so important controls fit reliably on one row.

Made-with: Cursor
2026-03-11 21:37:08 +02:00

159 lines
4.8 KiB
TypeScript

import { TASK_CHANGE_SUMMARY_CACHE_SCHEMA_VERSION } from './taskChangeSummaryCacheTypes';
import type { FileChangeSummary, TaskChangeSetV2 } from '@shared/types';
import type { PersistedTaskChangeSummaryEntry } from './taskChangeSummaryCacheTypes';
function normalizeIsoString(value: unknown): string | null {
if (typeof value !== 'string' || value.trim() === '') return null;
const date = new Date(value);
if (Number.isNaN(date.getTime())) return null;
return date.toISOString();
}
function normalizeString(value: unknown): string | null {
return typeof value === 'string' && value.trim() !== '' ? value : null;
}
function normalizeFileSummary(value: unknown): FileChangeSummary | null {
if (!value || typeof value !== 'object') return null;
const candidate = value as Partial<FileChangeSummary>;
if (typeof candidate.filePath !== 'string' || typeof candidate.relativePath !== 'string') {
return null;
}
return {
filePath: candidate.filePath,
relativePath: candidate.relativePath,
snippets: [],
linesAdded: Number.isFinite(candidate.linesAdded) ? Number(candidate.linesAdded) : 0,
linesRemoved: Number.isFinite(candidate.linesRemoved) ? Number(candidate.linesRemoved) : 0,
isNewFile: candidate.isNewFile === true,
};
}
function normalizeSummary(
value: unknown,
teamName: string,
taskId: string
): TaskChangeSetV2 | null {
if (!value || typeof value !== 'object') return null;
const candidate = value as Partial<TaskChangeSetV2>;
const files = Array.isArray(candidate.files)
? candidate.files
.map(normalizeFileSummary)
.filter((file): file is FileChangeSummary => file !== null)
: null;
const confidence =
candidate.confidence === 'high' || candidate.confidence === 'medium'
? candidate.confidence
: null;
const computedAt = normalizeIsoString(candidate.computedAt);
if (
!files ||
!confidence ||
!computedAt ||
!candidate.scope ||
!Array.isArray(candidate.warnings)
) {
return null;
}
return {
teamName,
taskId,
files,
totalFiles: Number.isFinite(candidate.totalFiles) ? Number(candidate.totalFiles) : files.length,
totalLinesAdded: Number.isFinite(candidate.totalLinesAdded)
? Number(candidate.totalLinesAdded)
: files.reduce((sum, file) => sum + file.linesAdded, 0),
totalLinesRemoved: Number.isFinite(candidate.totalLinesRemoved)
? Number(candidate.totalLinesRemoved)
: files.reduce((sum, file) => sum + file.linesRemoved, 0),
confidence,
computedAt,
scope: candidate.scope,
warnings: candidate.warnings.filter(
(warning): warning is string => typeof warning === 'string'
),
};
}
export function toPersistedSummary(
entry: PersistedTaskChangeSummaryEntry
): PersistedTaskChangeSummaryEntry {
return {
...entry,
version: TASK_CHANGE_SUMMARY_CACHE_SCHEMA_VERSION,
summary: {
...entry.summary,
files: entry.summary.files.map((file) => ({
...file,
snippets: [],
timeline: undefined,
})),
},
};
}
export function normalizePersistedTaskChangeSummaryEntry(
value: unknown
): PersistedTaskChangeSummaryEntry | null {
if (!value || typeof value !== 'object') return null;
const candidate = value as Partial<PersistedTaskChangeSummaryEntry>;
if (candidate.version !== TASK_CHANGE_SUMMARY_CACHE_SCHEMA_VERSION) {
return null;
}
const teamName = normalizeString(candidate.teamName);
const taskId = normalizeString(candidate.taskId);
const taskSignature = normalizeString(candidate.taskSignature);
const sourceFingerprint = normalizeString(candidate.sourceFingerprint);
const projectFingerprint = normalizeString(candidate.projectFingerprint);
const writtenAt = normalizeIsoString(candidate.writtenAt);
const expiresAt = normalizeIsoString(candidate.expiresAt);
const stateBucket =
candidate.stateBucket === 'approved' || candidate.stateBucket === 'completed'
? candidate.stateBucket
: null;
const extractorConfidence =
candidate.extractorConfidence === 'high' || candidate.extractorConfidence === 'medium'
? candidate.extractorConfidence
: null;
if (
!teamName ||
!taskId ||
!taskSignature ||
!sourceFingerprint ||
!projectFingerprint ||
!writtenAt ||
!expiresAt ||
!stateBucket ||
!extractorConfidence
) {
return null;
}
const summary = normalizeSummary(candidate.summary, teamName, taskId);
if (!summary) {
return null;
}
return {
version: TASK_CHANGE_SUMMARY_CACHE_SCHEMA_VERSION,
teamName,
taskId,
stateBucket,
taskSignature,
sourceFingerprint,
projectFingerprint,
writtenAt,
expiresAt,
extractorConfidence,
summary,
debugMeta:
candidate.debugMeta && typeof candidate.debugMeta === 'object'
? candidate.debugMeta
: undefined,
};
}