agent-ecosystem/src/shared/utils/taskChangeReviewability.ts

391 lines
12 KiB
TypeScript

import { TASK_CHANGE_DIAGNOSTIC_CODES } from '../types';
import type {
TaskChangeDiagnosticCode,
TaskChangeDiagnosticSeverity,
TaskChangeReviewabilityStatus,
TaskChangeReviewDiagnostic,
TaskChangeSetV2,
} from '../types';
export const EMPTY_INTERVAL_NO_EDITS_WARNING =
'No file edits found within persisted workIntervals.';
const MULTI_SCOPE_MESSAGES = [
'Task change ledger skipped attribution because multiple task scopes were active.',
'Ledger skipped attribution because multiple task scopes were active.',
] as const;
const TASK_CHANGE_DIAGNOSTIC_CODE_SET = new Set<string>(TASK_CHANGE_DIAGNOSTIC_CODES);
type ReviewabilityInput = Pick<
TaskChangeSetV2,
'files' | 'totalFiles' | 'confidence' | 'warnings' | 'scope'
> &
Partial<Pick<TaskChangeSetV2, 'diffStatCompleteness' | 'provenance' | 'reviewDiagnostics'>>;
interface DiagnosticTemplate {
code: TaskChangeDiagnosticCode;
severity: TaskChangeDiagnosticSeverity;
reviewBlocking: boolean;
message: string;
}
function templateForLegacyWarning(warning: string): DiagnosticTemplate {
const trimmed = warning.trim();
const normalized = trimmed.toLowerCase();
if (MULTI_SCOPE_MESSAGES.some((message) => message.toLowerCase() === normalized)) {
return {
code: 'multi_scope_no_safe_diff',
severity: 'info',
reviewBlocking: false,
message:
'Activity was observed while multiple task scopes were active, so file edits were not safely assigned to this task.',
};
}
if (normalized === EMPTY_INTERVAL_NO_EDITS_WARNING.toLowerCase()) {
return {
code: 'active_task_no_edits_yet',
severity: 'info',
reviewBlocking: false,
message: 'No file edits have been observed in the active task interval yet.',
};
}
if (normalized.includes('timed out')) {
return {
code: 'summary_timeout',
severity: 'warning',
reviewBlocking: true,
message: 'The changes scan timed out before it could finish.',
};
}
if (normalized.includes('fell back to journal reconstruction')) {
return {
code: 'summary_reconstructed',
severity: 'info',
reviewBlocking: false,
message: 'The change summary was reconstructed from the task-change journal.',
};
}
if (normalized.includes('journal was unavailable')) {
return {
code: 'journal_unavailable',
severity: 'warning',
reviewBlocking: true,
message: 'Detailed ledger entries were unavailable for this task.',
};
}
if (normalized.includes('recovered from malformed journal lines')) {
return {
code: 'ledger_integrity_recovered',
severity: 'warning',
reviewBlocking: true,
message: 'The task-change ledger was recovered from malformed journal lines.',
};
}
if (
normalized.includes('freshness did not match') ||
normalized.includes('partial') ||
normalized.includes('integrity')
) {
return {
code: 'ledger_integrity_partial',
severity: 'warning',
reviewBlocking: true,
message: 'The task-change ledger may be incomplete or stale.',
};
}
if (normalized.startsWith('tool ') && normalized.includes(' failed after changing files')) {
return {
code: 'tool_failed_after_edit',
severity: 'warning',
reviewBlocking: true,
message: 'A tool failed after changing files.',
};
}
if (
normalized.startsWith('background tool ') &&
normalized.includes(' was killed after changing files')
) {
return {
code: 'tool_killed_after_edit',
severity: 'warning',
reviewBlocking: true,
message: 'A background tool was killed after changing files.',
};
}
return {
code: 'legacy_warning',
severity: 'warning',
reviewBlocking: true,
message: trimmed || 'The change summary reported an unclassified warning.',
};
}
export function createTaskChangeDiagnosticFromWarning(
warning: string,
source: TaskChangeReviewDiagnostic['source'] = 'legacy'
): TaskChangeReviewDiagnostic {
const template = templateForLegacyWarning(warning);
return { ...template, source };
}
function diagnosticKey(diagnostic: TaskChangeReviewDiagnostic): string {
return `${diagnostic.code}:${diagnostic.message}`;
}
function diagnosticSeverityRank(severity: TaskChangeDiagnosticSeverity): number {
switch (severity) {
case 'error':
return 3;
case 'warning':
return 2;
case 'info':
return 1;
}
}
export function mergeTaskChangeReviewDiagnostics(
existing: TaskChangeReviewDiagnostic,
incoming: TaskChangeReviewDiagnostic
): TaskChangeReviewDiagnostic {
if (
incoming.reviewBlocking &&
(!existing.reviewBlocking ||
diagnosticSeverityRank(incoming.severity) > diagnosticSeverityRank(existing.severity))
) {
return incoming;
}
if (
existing.reviewBlocking === incoming.reviewBlocking &&
diagnosticSeverityRank(incoming.severity) > diagnosticSeverityRank(existing.severity)
) {
return incoming;
}
return existing;
}
function addDiagnostic(
diagnostics: Map<string, TaskChangeReviewDiagnostic>,
diagnostic: TaskChangeReviewDiagnostic
): void {
const key = diagnosticKey(diagnostic);
const existing = diagnostics.get(key);
if (existing) {
diagnostics.set(key, mergeTaskChangeReviewDiagnostics(existing, diagnostic));
} else {
diagnostics.set(key, diagnostic);
}
}
function getInputFiles(input: ReviewabilityInput): TaskChangeSetV2['files'] {
return Array.isArray(input.files) ? input.files : [];
}
function getInputWarnings(input: ReviewabilityInput): string[] {
return Array.isArray(input.warnings)
? input.warnings.filter((warning): warning is string => typeof warning === 'string')
: [];
}
function getInputReviewDiagnostics(input: ReviewabilityInput): TaskChangeReviewDiagnostic[] {
if (!Array.isArray(input.reviewDiagnostics)) {
return [];
}
return input.reviewDiagnostics.filter((diagnostic): diagnostic is TaskChangeReviewDiagnostic => {
if (!diagnostic || typeof diagnostic !== 'object' || Array.isArray(diagnostic)) {
return false;
}
const candidate = diagnostic as Partial<TaskChangeReviewDiagnostic>;
return (
typeof candidate.code === 'string' &&
TASK_CHANGE_DIAGNOSTIC_CODE_SET.has(candidate.code) &&
(candidate.severity === 'info' ||
candidate.severity === 'warning' ||
candidate.severity === 'error') &&
typeof candidate.reviewBlocking === 'boolean' &&
typeof candidate.message === 'string'
);
});
}
function getInputToolUseIds(input: ReviewabilityInput): string[] {
const scope = input.scope as Partial<TaskChangeSetV2['scope']> | undefined;
return Array.isArray(scope?.toolUseIds) ? scope.toolUseIds : [];
}
function getInputStartTimestamp(input: ReviewabilityInput): string {
const scope = input.scope as Partial<TaskChangeSetV2['scope']> | undefined;
return typeof scope?.startTimestamp === 'string' ? scope.startTimestamp : '';
}
function getInputEndTimestamp(input: ReviewabilityInput): string {
const scope = input.scope as Partial<TaskChangeSetV2['scope']> | undefined;
return typeof scope?.endTimestamp === 'string' ? scope.endTimestamp : '';
}
function getInputTotalFiles(input: ReviewabilityInput, fileCount: number): number {
const totalFiles = Number(input.totalFiles);
if (!Number.isFinite(totalFiles) || totalFiles < 0) {
return fileCount;
}
return Math.trunc(totalFiles);
}
function collectDiagnostics(input: ReviewabilityInput): TaskChangeReviewDiagnostic[] {
const diagnostics = new Map<string, TaskChangeReviewDiagnostic>();
for (const diagnostic of getInputReviewDiagnostics(input)) {
addDiagnostic(diagnostics, diagnostic);
}
for (const warning of getInputWarnings(input)) {
const diagnostic = createTaskChangeDiagnosticFromWarning(warning);
addDiagnostic(diagnostics, diagnostic);
}
if (input.diffStatCompleteness === 'partial') {
const diagnostic: TaskChangeReviewDiagnostic = {
code: 'diff_stat_partial',
severity: 'warning',
reviewBlocking: true,
message: 'Some file change statistics are incomplete.',
source: 'summary',
};
addDiagnostic(diagnostics, diagnostic);
}
if (input.provenance?.integrity === 'partial') {
const diagnostic: TaskChangeReviewDiagnostic = {
code: 'ledger_integrity_partial',
severity: 'warning',
reviewBlocking: true,
message: 'The task-change ledger is partially available.',
source: 'ledger',
};
addDiagnostic(diagnostics, diagnostic);
} else if (input.provenance?.integrity === 'recovered') {
const diagnostic: TaskChangeReviewDiagnostic = {
code: 'ledger_integrity_recovered',
severity: 'warning',
reviewBlocking: true,
message: 'The task-change ledger was recovered from malformed journal lines.',
source: 'ledger',
};
addDiagnostic(diagnostics, diagnostic);
}
const fileCount = getInputFiles(input).length;
const totalFiles = getInputTotalFiles(input, fileCount);
if (totalFiles > fileCount) {
const missingFileCount = totalFiles - fileCount;
const diagnostic: TaskChangeReviewDiagnostic = {
code: 'unsafe_or_untrusted_evidence',
severity: 'warning',
reviewBlocking: true,
message:
missingFileCount === 1
? 'The change summary reported one file without safe review details.'
: `The change summary reported ${missingFileCount} files without safe review details.`,
source: 'summary',
};
addDiagnostic(diagnostics, diagnostic);
}
return [...diagnostics.values()];
}
function isActiveIntervalWithoutFileEdits(
input: ReviewabilityInput,
diagnostics: TaskChangeReviewDiagnostic[]
): boolean {
return (
getInputFiles(input).length === 0 &&
diagnostics.some((diagnostic) => diagnostic.code === 'active_task_no_edits_yet') &&
Boolean(getInputStartTimestamp(input)) &&
!getInputEndTimestamp(input) &&
getInputToolUseIds(input).length === 0
);
}
export function classifyTaskChangeReviewability(
input: ReviewabilityInput
): TaskChangeReviewabilityStatus {
const diagnostics = collectDiagnostics(input);
const blockingDiagnostics = diagnostics.filter((diagnostic) => diagnostic.reviewBlocking);
const hasFiles = getInputFiles(input).length > 0;
if (blockingDiagnostics.length > 0) {
return {
reviewability: 'attention_required',
reasonCode: 'blocking_diagnostics',
userAction: hasFiles ? 'review_diff' : 'inspect_diagnostics',
severity: 'warning',
message: hasFiles ? 'Changes may be incomplete.' : 'Changes need attention.',
diagnostics,
};
}
if (isActiveIntervalWithoutFileEdits(input, diagnostics)) {
return {
reviewability: 'unknown',
reasonCode: 'pending_no_edits_yet',
userAction: 'wait_or_refresh',
severity: 'none',
message: 'No file edits have been observed yet.',
diagnostics,
};
}
if (hasFiles) {
return {
reviewability: 'reviewable',
reasonCode:
diagnostics.length > 0 ? 'files_changed_with_non_blocking_diagnostics' : 'files_changed',
userAction: 'review_diff',
severity: 'success',
message: 'Reviewable file changes are available.',
diagnostics,
};
}
if (diagnostics.length > 0) {
return {
reviewability: 'diagnostic_only',
reasonCode: 'diagnostic_only',
userAction: 'inspect_diagnostics',
severity: 'info',
message: 'No safe diff is available for this task.',
diagnostics,
};
}
if (input.confidence === 'high' || input.confidence === 'medium') {
return {
reviewability: 'none',
reasonCode: 'confirmed_no_changes',
userAction: 'nothing',
severity: 'none',
message: 'No reviewable file changes were found.',
diagnostics,
};
}
return {
reviewability: 'unknown',
reasonCode: 'low_confidence',
userAction: 'wait_or_refresh',
severity: 'none',
message: 'The change summary is not confident enough yet.',
diagnostics,
};
}