diff --git a/src/resources/extensions/gsd/db-writer.ts b/src/resources/extensions/gsd/db-writer.ts index 6fccd51da..b5165ff77 100644 --- a/src/resources/extensions/gsd/db-writer.ts +++ b/src/resources/extensions/gsd/db-writer.ts @@ -546,11 +546,35 @@ export async function updateRequirementInDb( try { const db = await import('./gsd-db.js'); - const existing = db.getRequirementById(id); + let existing = db.getRequirementById(id); + + // If requirement doesn't exist in DB, seed the entire requirements table + // from REQUIREMENTS.md first (#3346). This handles the standard workflow + // where requirements are authored in markdown during discussion but never + // imported into the database — making gsd_requirement_update always fail + // with "not_found" at milestone completion. + if (!existing) { + const reqFilePath = resolveGsdRootFile(basePath, 'REQUIREMENTS'); + try { + const content = readFileSync(reqFilePath, 'utf-8'); + const { parseRequirementsSections } = await import('./md-importer.js'); + const parsed = parseRequirementsSections(content); + if (parsed.length > 0) { + logWarning('manifest', `Seeding ${parsed.length} requirements from REQUIREMENTS.md into DB (first update triggers import)`, { fn: 'updateRequirementInDb' }); + for (const req of parsed) { + // Only seed if not already in DB (avoid overwriting concurrent inserts) + if (!db.getRequirementById(req.id)) { + db.upsertRequirement(req); + } + } + // Re-check after seeding + existing = db.getRequirementById(id); + } + } catch { + // REQUIREMENTS.md missing or unparseable — fall through to skeleton + } + } - // If requirement doesn't exist in DB, create a skeleton and merge updates. - // This handles the case where requirements were written to REQUIREMENTS.md - // but never imported into the database (see #2919). const base: Requirement = existing ?? { id, class: '', diff --git a/src/resources/extensions/gsd/tests/db-writer.test.ts b/src/resources/extensions/gsd/tests/db-writer.test.ts index 4125312a8..5a61bd131 100644 --- a/src/resources/extensions/gsd/tests/db-writer.test.ts +++ b/src/resources/extensions/gsd/tests/db-writer.test.ts @@ -476,6 +476,71 @@ describe('db-writer', () => { } }); + test('updateRequirementInDb — seeds from REQUIREMENTS.md when DB empty (#3346)', async () => { + const tmpDir = makeTmpDir(); + const dbPath = path.join(tmpDir, '.gsd', 'gsd.db'); + openDatabase(dbPath); + + try { + // Write a REQUIREMENTS.md with real content (simulating discussion phase output) + const reqContent = [ + '# Requirements', + '', + '## Active', + '', + '### R005 — User authentication', + '- Class: functional', + '- Why: Users need secure access', + '- Source: user-research', + '- Primary owner: M001/S02', + '', + '### R007 — API rate limiting', + '- Class: non-functional', + '- Why: Prevent abuse', + '- Source: architecture', + '- Primary owner: M001/S03', + '', + '## Validated', + '', + '### R001 — Database schema', + '- Class: functional', + '- Why: Foundation for storage', + '- Source: design', + '- Validation: S01 verified', + ].join('\n'); + fs.writeFileSync(path.join(tmpDir, '.gsd', 'REQUIREMENTS.md'), reqContent); + + // DB is empty — no requirements seeded. Update R005 to "validated". + // Before #3346 fix: this would create a skeleton with empty fields. + // After fix: this seeds all 3 requirements from REQUIREMENTS.md first. + await updateRequirementInDb('R005', { + status: 'validated', + validation: 'S02 — auth flow verified', + }, tmpDir); + + // R005 should have the update AND the original content from markdown + const r005 = getRequirementById('R005'); + assert.ok(r005, 'R005 should exist'); + assert.equal(r005!.status, 'validated', 'status should be updated'); + assert.equal(r005!.validation, 'S02 — auth flow verified', 'validation should be updated'); + assert.equal(r005!.class, 'functional', 'class should be preserved from REQUIREMENTS.md'); + assert.ok(r005!.description?.includes('authentication') || r005!.full_content?.includes('authentication'), + 'original content should be preserved'); + + // R007 and R001 should also be seeded (not just the one being updated) + const r007 = getRequirementById('R007'); + assert.ok(r007, 'R007 should be seeded from REQUIREMENTS.md'); + assert.equal(r007!.status, 'active', 'R007 status should be active'); + + const r001 = getRequirementById('R001'); + assert.ok(r001, 'R001 should be seeded from REQUIREMENTS.md'); + assert.equal(r001!.status, 'validated', 'R001 status should be validated (from section heading)'); + } finally { + closeDatabase(); + cleanupDir(tmpDir); + } + }); + // ═══════════════════════════════════════════════════════════════════════════ // saveArtifactToDb Tests // ═══════════════════════════════════════════════════════════════════════════