fix(task-change-ledger): read long hashed task artifacts
This commit is contained in:
parent
e944e2c937
commit
95b62d6013
4 changed files with 107 additions and 14 deletions
|
|
@ -26,6 +26,7 @@ const TASK_CHANGE_SUMMARY_SCHEMA_VERSION = 2;
|
|||
const TASK_CHANGE_FRESHNESS_SCHEMA_VERSION = 2;
|
||||
const TASK_CHANGE_LEDGER_DIRNAME = '.board-task-changes';
|
||||
const TASK_CHANGE_FRESHNESS_DIRNAME = '.board-task-change-freshness';
|
||||
const MAX_TASK_ID_ARTIFACT_SEGMENT_LENGTH = 120;
|
||||
|
||||
function isWindowsReservedArtifactSegment(segment: string): boolean {
|
||||
const stem = segment.split('.')[0]?.toUpperCase() ?? '';
|
||||
|
|
@ -42,7 +43,8 @@ function isWindowsReservedArtifactSegment(segment: string): boolean {
|
|||
|
||||
function encodeTaskId(taskId: string): string {
|
||||
const encoded = encodeURIComponent(taskId);
|
||||
return isWindowsReservedArtifactSegment(encoded)
|
||||
return isWindowsReservedArtifactSegment(encoded) ||
|
||||
encoded.length > MAX_TASK_ID_ARTIFACT_SEGMENT_LENGTH
|
||||
? `task-id-${createHash('sha256').update(taskId).digest('hex').slice(0, 32)}`
|
||||
: encoded;
|
||||
}
|
||||
|
|
@ -424,7 +426,11 @@ export class TaskChangeLedgerReader {
|
|||
) {
|
||||
return {
|
||||
bundle,
|
||||
provenance: this.buildLedgerProvenance(journalStamp, bundle.integrity, bundle.schemaVersion),
|
||||
provenance: this.buildLedgerProvenance(
|
||||
journalStamp,
|
||||
bundle.integrity,
|
||||
bundle.schemaVersion
|
||||
),
|
||||
mode: 'validated',
|
||||
};
|
||||
}
|
||||
|
|
@ -625,7 +631,10 @@ export class TaskChangeLedgerReader {
|
|||
return { entries, recovered };
|
||||
}
|
||||
|
||||
private bundleMatchesFreshness(bundle: LedgerSummaryBundleV2, freshness: LedgerFreshnessV2): boolean {
|
||||
private bundleMatchesFreshness(
|
||||
bundle: LedgerSummaryBundleV2,
|
||||
freshness: LedgerFreshnessV2
|
||||
): boolean {
|
||||
return (
|
||||
JSON.stringify(bundle.journalStamp) === JSON.stringify(freshness.journalStamp) &&
|
||||
bundle.eventCount === freshness.eventCount &&
|
||||
|
|
@ -791,7 +800,10 @@ export class TaskChangeLedgerReader {
|
|||
scope = this.mapV2Scope(params.taskId, params.bundle.scope, params.bundle.files);
|
||||
diffStatCompleteness = params.bundle.diffStatCompleteness;
|
||||
} else {
|
||||
const fallback = this.buildFallbackFilesFromGroupedSnippets(groupedSnippets, params.projectPath);
|
||||
const fallback = this.buildFallbackFilesFromGroupedSnippets(
|
||||
groupedSnippets,
|
||||
params.projectPath
|
||||
);
|
||||
files = fallback.files;
|
||||
totalLinesAdded = fallback.totalLinesAdded;
|
||||
totalLinesRemoved = fallback.totalLinesRemoved;
|
||||
|
|
@ -801,11 +813,18 @@ export class TaskChangeLedgerReader {
|
|||
: params.journal.events.some((event) => event.confidence === 'medium')
|
||||
? 'medium'
|
||||
: 'high';
|
||||
scope = this.buildFallbackScope(params.taskId, files, params.journal.events, params.journal.notices);
|
||||
scope = this.buildFallbackScope(
|
||||
params.taskId,
|
||||
files,
|
||||
params.journal.events,
|
||||
params.journal.notices
|
||||
);
|
||||
diffStatCompleteness = fallback.files.every((file) => file.diffStatKnown !== false)
|
||||
? 'complete'
|
||||
: 'partial';
|
||||
warnings.push('Ledger detail view fell back to journal reconstruction because summary bundle v2 was unavailable.');
|
||||
warnings.push(
|
||||
'Ledger detail view fell back to journal reconstruction because summary bundle v2 was unavailable.'
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
@ -972,7 +991,11 @@ export class TaskChangeLedgerReader {
|
|||
return {
|
||||
taskId,
|
||||
memberName:
|
||||
scope.memberName || scope.primaryMemberName || scope.primaryAgentId || scope.primaryActorKey || '',
|
||||
scope.memberName ||
|
||||
scope.primaryMemberName ||
|
||||
scope.primaryAgentId ||
|
||||
scope.primaryActorKey ||
|
||||
'',
|
||||
startLine: 0,
|
||||
endLine: 0,
|
||||
startTimestamp: scope.startTimestamp,
|
||||
|
|
@ -1004,7 +1027,10 @@ export class TaskChangeLedgerReader {
|
|||
);
|
||||
}
|
||||
|
||||
private async readContentRef(projectDir: string, ref: LedgerContentRef | null): Promise<string | null> {
|
||||
private async readContentRef(
|
||||
projectDir: string,
|
||||
ref: LedgerContentRef | null
|
||||
): Promise<string | null> {
|
||||
if (!ref?.blobRef) {
|
||||
return null;
|
||||
}
|
||||
|
|
@ -1116,7 +1142,10 @@ export class TaskChangeLedgerReader {
|
|||
}
|
||||
|
||||
private buildFallbackFilesFromGroupedSnippets(
|
||||
grouped: Map<string, { filePath: string; relation?: LedgerChangeRelation; snippets: SnippetDiff[] }>,
|
||||
grouped: Map<
|
||||
string,
|
||||
{ filePath: string; relation?: LedgerChangeRelation; snippets: SnippetDiff[] }
|
||||
>,
|
||||
projectPath?: string
|
||||
): { files: FileChangeSummary[]; totalLinesAdded: number; totalLinesRemoved: number } {
|
||||
const files: FileChangeSummary[] = [];
|
||||
|
|
@ -1157,9 +1186,7 @@ export class TaskChangeLedgerReader {
|
|||
...(relation ? { relation } : {}),
|
||||
latestOperation:
|
||||
entry.snippets[entry.snippets.length - 1]?.ledger?.operation ??
|
||||
(entry.snippets[entry.snippets.length - 1]?.type === 'write-new'
|
||||
? 'create'
|
||||
: 'modify'),
|
||||
(entry.snippets[entry.snippets.length - 1]?.type === 'write-new' ? 'create' : 'modify'),
|
||||
},
|
||||
timeline: this.buildTimeline(displayPath, entry.snippets),
|
||||
});
|
||||
|
|
@ -1347,7 +1374,11 @@ export class TaskChangeLedgerReader {
|
|||
return `${normalizedAnchor.slice(0, normalizedAnchor.length - normalizedAnchorRelation.length)}${targetRelationPath.replace(/\\/g, '/')}`;
|
||||
}
|
||||
|
||||
private relativePath(filePath: string, projectPath?: string, explicitRelativePath?: string): string {
|
||||
private relativePath(
|
||||
filePath: string,
|
||||
projectPath?: string,
|
||||
explicitRelativePath?: string
|
||||
): string {
|
||||
if (explicitRelativePath) {
|
||||
return explicitRelativePath.replace(/\\/g, '/');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import type { TaskLogFreshnessSignal } from './TeamTaskStallTypes';
|
|||
|
||||
const BOARD_TASK_LOG_FRESHNESS_DIRNAME = '.board-task-log-freshness';
|
||||
const BOARD_TASK_LOG_FRESHNESS_FILE_SUFFIX = '.json';
|
||||
const MAX_TASK_ID_ARTIFACT_SEGMENT_LENGTH = 120;
|
||||
|
||||
interface ParsedFreshnessSignal {
|
||||
taskId: string;
|
||||
|
|
@ -30,7 +31,8 @@ function isWindowsReservedArtifactSegment(segment: string): boolean {
|
|||
|
||||
function encodeTaskId(taskId: string): string {
|
||||
const encoded = encodeURIComponent(taskId);
|
||||
return isWindowsReservedArtifactSegment(encoded)
|
||||
return isWindowsReservedArtifactSegment(encoded) ||
|
||||
encoded.length > MAX_TASK_ID_ARTIFACT_SEGMENT_LENGTH
|
||||
? `task-id-${createHash('sha256').update(taskId).digest('hex').slice(0, 32)}`
|
||||
: encoded;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,6 +96,41 @@ describe('TaskChangeLedgerReader', () => {
|
|||
expect(result?.warnings).toContain('reserved segment safe path');
|
||||
});
|
||||
|
||||
it('reads ledger artifacts stored under hashed long task id segments', async () => {
|
||||
tmpDir = await fsTempDir();
|
||||
const taskId = `task-${'x'.repeat(180)}`;
|
||||
const bundleDir = path.join(tmpDir, '.board-task-changes', 'bundles');
|
||||
await mkdir(bundleDir, { recursive: true });
|
||||
await writeFile(
|
||||
path.join(bundleDir, `${safeTaskIdSegment(taskId)}.json`),
|
||||
JSON.stringify({
|
||||
schemaVersion: 1,
|
||||
source: 'task-change-ledger',
|
||||
taskId,
|
||||
generatedAt: '2026-03-01T10:00:00.000Z',
|
||||
eventCount: 0,
|
||||
files: [],
|
||||
totalLinesAdded: 0,
|
||||
totalLinesRemoved: 0,
|
||||
totalFiles: 0,
|
||||
confidence: 'high',
|
||||
warnings: ['long task id safe path'],
|
||||
events: [],
|
||||
}),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const reader = new TaskChangeLedgerReader();
|
||||
const result = await reader.readTaskChanges({
|
||||
teamName: 'team',
|
||||
taskId,
|
||||
projectDir: tmpDir,
|
||||
includeDetails: true,
|
||||
});
|
||||
|
||||
expect(result?.warnings).toContain('long task id safe path');
|
||||
});
|
||||
|
||||
it('maps ledger state and rename relation into snippets', async () => {
|
||||
tmpDir = await makeLedgerBundle({
|
||||
events: [
|
||||
|
|
|
|||
|
|
@ -83,4 +83,29 @@ describe('TeamTaskLogFreshnessReader', () => {
|
|||
);
|
||||
expect(signals.get('CON')?.transcriptFileBasename).toBe('session-con.jsonl');
|
||||
});
|
||||
|
||||
it('reads hashed freshness files for very long task ids', async () => {
|
||||
const projectDir = await fs.mkdtemp(path.join(os.tmpdir(), 'stall-freshness-'));
|
||||
tempDirs.push(projectDir);
|
||||
const signalDir = path.join(projectDir, '.board-task-log-freshness');
|
||||
await fs.mkdir(signalDir, { recursive: true });
|
||||
const taskId = `task-${'x'.repeat(180)}`;
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(signalDir, `${safeTaskIdSegment(taskId)}.json`),
|
||||
JSON.stringify({
|
||||
taskId,
|
||||
updatedAt: '2026-04-19T12:00:00.000Z',
|
||||
transcriptFile: 'session-long.jsonl',
|
||||
}),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const signals = await new TeamTaskLogFreshnessReader().readSignals(projectDir, [taskId]);
|
||||
|
||||
expect(signals.get(taskId)?.filePath).toBe(
|
||||
path.join(signalDir, `${safeTaskIdSegment(taskId)}.json`)
|
||||
);
|
||||
expect(signals.get(taskId)?.transcriptFileBasename).toBe('session-long.jsonl');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue