From 3c44e3d4e2b690e8ebaea78914a798f9039e137e Mon Sep 17 00:00:00 2001 From: mastertyko <11311479+mastertyko@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:09:51 +0200 Subject: [PATCH] fix(gsd): tolerate corrupt task arrays (#4056) --- src/resources/extensions/gsd/gsd-db.ts | 21 ++++++- .../extensions/gsd/tests/gsd-db.test.ts | 60 ++++++++++++++++++- 2 files changed, 78 insertions(+), 3 deletions(-) diff --git a/src/resources/extensions/gsd/gsd-db.ts b/src/resources/extensions/gsd/gsd-db.ts index 2ece198ed..3224831aa 100644 --- a/src/resources/extensions/gsd/gsd-db.ts +++ b/src/resources/extensions/gsd/gsd-db.ts @@ -1564,6 +1564,23 @@ export interface TaskRow { sequence: number; } +function parseTaskArrayColumn(raw: unknown): string[] { + if (typeof raw !== "string" || raw.trim() === "") return []; + + try { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed)) return parsed.map((value) => String(value)); + if (parsed === null || parsed === undefined || parsed === "") return []; + return [String(parsed)]; + } catch { + // Older/corrupt rows may contain comma-separated strings instead of JSON. + return raw + .split(",") + .map((value) => value.trim()) + .filter(Boolean); + } +} + function rowToTask(row: Record): TaskRow { const parseTaskArray = (value: unknown): string[] => { if (Array.isArray(value)) { @@ -1603,8 +1620,8 @@ function rowToTask(row: Record): TaskRow { blocker_discovered: (row["blocker_discovered"] as number) === 1, deviations: row["deviations"] as string, known_issues: row["known_issues"] as string, - key_files: JSON.parse((row["key_files"] as string) || "[]"), - key_decisions: JSON.parse((row["key_decisions"] as string) || "[]"), + key_files: parseTaskArrayColumn(row["key_files"]), + key_decisions: parseTaskArrayColumn(row["key_decisions"]), full_summary_md: row["full_summary_md"] as string, description: (row["description"] as string) ?? "", estimate: (row["estimate"] as string) ?? "", diff --git a/src/resources/extensions/gsd/tests/gsd-db.test.ts b/src/resources/extensions/gsd/tests/gsd-db.test.ts index 097ea7151..4685b6dcc 100644 --- a/src/resources/extensions/gsd/tests/gsd-db.test.ts +++ b/src/resources/extensions/gsd/tests/gsd-db.test.ts @@ -15,10 +15,14 @@ import { getRequirementById, getActiveDecisions, getActiveRequirements, - getTask, transaction, _getAdapter, _resetProvider, + insertMilestone, + insertSlice, + insertTask, + getTask, + getSliceTasks, } from '../gsd-db.ts'; // ═══════════════════════════════════════════════════════════════════════════ @@ -460,6 +464,60 @@ describe('gsd-db', () => { assert.ok(!wasDbOpenAttempted(), 'wasDbOpenAttempted should reset after closeDatabase'); }); + test('gsd-db: rowToTask tolerates corrupt comma-separated task arrays', () => { + openDatabase(':memory:'); + insertMilestone({ id: 'M001', status: 'active' }); + insertSlice({ milestoneId: 'M001', id: 'S01', status: 'active' }); + insertTask({ + milestoneId: 'M001', + sliceId: 'S01', + id: 'T01', + title: 'Recover corrupt arrays', + planning: { + description: 'desc', + estimate: 'small', + files: ['src/original.ts'], + verify: 'npm test', + inputs: ['docs/original.md'], + expectedOutput: ['dist/original.md'], + observabilityImpact: '', + }, + }); + + const adapter = _getAdapter()!; + adapter.prepare( + `UPDATE tasks + SET files = ?, inputs = ?, expected_output = ?, key_files = ?, key_decisions = ? + WHERE milestone_id = ? AND slice_id = ? AND id = ?`, + ).run( + 'src-erf/Models/foo.cs, src-erf/Models/bar.cs', + 'docs/input-a.md, docs/input-b.md', + 'dist/out-a.md, dist/out-b.md', + 'src/resources/extensions/gsd/gsd-db.ts, src/resources/extensions/gsd/state.ts', + '"decision-1"', + 'M001', + 'S01', + 'T01', + ); + + const task = getTask('M001', 'S01', 'T01'); + assert.ok(task, 'getTask should still return the corrupt row'); + assert.deepStrictEqual(task!.files, ['src-erf/Models/foo.cs', 'src-erf/Models/bar.cs']); + assert.deepStrictEqual(task!.inputs, ['docs/input-a.md', 'docs/input-b.md']); + assert.deepStrictEqual(task!.expected_output, ['dist/out-a.md', 'dist/out-b.md']); + assert.deepStrictEqual( + task!.key_files, + ['src/resources/extensions/gsd/gsd-db.ts', 'src/resources/extensions/gsd/state.ts'], + ); + assert.deepStrictEqual(task!.key_decisions, ['decision-1']); + + const sliceTasks = getSliceTasks('M001', 'S01'); + assert.equal(sliceTasks.length, 1, 'getSliceTasks should also survive corrupt rows'); + assert.deepStrictEqual(sliceTasks[0]!.files, task!.files); + + closeDatabase(); + }); + // ─── Final Report ────────────────────────────────────────────────────────── });