fix(gsd): seed requirements table from REQUIREMENTS.md on first update
When requirements are authored in REQUIREMENTS.md during the discussion phase (the standard workflow), the DB requirements table stays empty. gsd_requirement_update then fails with not_found for every requirement at milestone completion, burning tokens on retries. When updateRequirementInDb encounters a requirement ID not in the DB, it now parses REQUIREMENTS.md via parseRequirementsSections() and seeds all requirements into the DB before retrying the lookup. This preserves the original content (class, description, why, source, validation) instead of creating an empty skeleton. The seeding is: - Lazy: only runs on first miss, not on every update - Collision-safe: skips IDs already in the DB - Non-blocking: falls through to skeleton if REQUIREMENTS.md is missing or unparseable Adds 1 regression test verifying that updating R005 when the DB is empty seeds all 3 requirements from REQUIREMENTS.md with their original content preserved. Closes #3346
This commit is contained in:
parent
261e2a6d5f
commit
58c19ed48d
2 changed files with 93 additions and 4 deletions
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue