fix: persist live planning specs in db
This commit is contained in:
parent
8f5f33611a
commit
ffde54e05a
2 changed files with 172 additions and 0 deletions
|
|
@ -2362,9 +2362,43 @@ export function insertMilestone(m) {
|
|||
: "",
|
||||
":sequence": m.sequence ?? 0,
|
||||
});
|
||||
insertMilestoneSpecIfAbsent(m.id, m.planning ?? {});
|
||||
}
|
||||
function insertMilestoneSpecIfAbsent(milestoneId, planning = {}) {
|
||||
currentDb
|
||||
.prepare(`INSERT OR IGNORE INTO milestone_specs (
|
||||
id, vision, success_criteria, key_risks, proof_strategy,
|
||||
verification_contract, verification_integration, verification_operational, verification_uat,
|
||||
definition_of_done, requirement_coverage, boundary_map_markdown, vision_meeting_json,
|
||||
spec_version, created_at
|
||||
) VALUES (
|
||||
:id, :vision, :success_criteria, :key_risks, :proof_strategy,
|
||||
:verification_contract, :verification_integration, :verification_operational, :verification_uat,
|
||||
:definition_of_done, :requirement_coverage, :boundary_map_markdown, :vision_meeting_json,
|
||||
1, :created_at
|
||||
)`)
|
||||
.run({
|
||||
":id": milestoneId,
|
||||
":vision": planning.vision ?? "",
|
||||
":success_criteria": JSON.stringify(planning.successCriteria ?? []),
|
||||
":key_risks": JSON.stringify(planning.keyRisks ?? []),
|
||||
":proof_strategy": JSON.stringify(planning.proofStrategy ?? []),
|
||||
":verification_contract": planning.verificationContract ?? "",
|
||||
":verification_integration": planning.verificationIntegration ?? "",
|
||||
":verification_operational": planning.verificationOperational ?? "",
|
||||
":verification_uat": planning.verificationUat ?? "",
|
||||
":definition_of_done": JSON.stringify(planning.definitionOfDone ?? []),
|
||||
":requirement_coverage": planning.requirementCoverage ?? "",
|
||||
":boundary_map_markdown": planning.boundaryMapMarkdown ?? "",
|
||||
":vision_meeting_json": planning.visionMeeting
|
||||
? JSON.stringify(planning.visionMeeting)
|
||||
: "",
|
||||
":created_at": new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
export function upsertMilestonePlanning(milestoneId, planning) {
|
||||
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
|
||||
insertMilestoneSpecIfAbsent(milestoneId, planning);
|
||||
currentDb
|
||||
.prepare(`UPDATE milestones SET
|
||||
title = COALESCE(NULLIF(:title, ''), title),
|
||||
|
|
@ -2487,6 +2521,37 @@ export function insertSlice(s) {
|
|||
":raw_is_sketch": s.isSketch === undefined ? null : s.isSketch ? 1 : 0,
|
||||
":raw_sketch_scope": s.sketchScope === undefined ? null : s.sketchScope,
|
||||
});
|
||||
insertSliceSpecIfAbsent(s.milestoneId, s.id, s.planning ?? {});
|
||||
}
|
||||
function insertSliceSpecIfAbsent(milestoneId, sliceId, planning = {}) {
|
||||
currentDb
|
||||
.prepare(`INSERT OR IGNORE INTO slice_specs (
|
||||
milestone_id, slice_id, goal, success_criteria, proof_level,
|
||||
integration_closure, observability_impact,
|
||||
adversarial_partner, adversarial_combatant, adversarial_architect,
|
||||
planning_meeting_json, spec_version, created_at
|
||||
) VALUES (
|
||||
:milestone_id, :slice_id, :goal, :success_criteria, :proof_level,
|
||||
:integration_closure, :observability_impact,
|
||||
:adversarial_partner, :adversarial_combatant, :adversarial_architect,
|
||||
:planning_meeting_json, 1, :created_at
|
||||
)`)
|
||||
.run({
|
||||
":milestone_id": milestoneId,
|
||||
":slice_id": sliceId,
|
||||
":goal": planning.goal ?? "",
|
||||
":success_criteria": planning.successCriteria ?? "",
|
||||
":proof_level": planning.proofLevel ?? "",
|
||||
":integration_closure": planning.integrationClosure ?? "",
|
||||
":observability_impact": planning.observabilityImpact ?? "",
|
||||
":adversarial_partner": planning.adversarialReview?.partner ?? "",
|
||||
":adversarial_combatant": planning.adversarialReview?.combatant ?? "",
|
||||
":adversarial_architect": planning.adversarialReview?.architect ?? "",
|
||||
":planning_meeting_json": planning.planningMeeting
|
||||
? JSON.stringify(planning.planningMeeting)
|
||||
: "",
|
||||
":created_at": new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
/**
|
||||
* gsd-2 ADR-011: clear the is_sketch flag after refine-slice fills in the full plan.
|
||||
|
|
@ -2554,6 +2619,7 @@ export function listEscalationArtifacts(milestoneId, includeResolved = false) {
|
|||
}
|
||||
export function upsertSlicePlanning(milestoneId, sliceId, planning) {
|
||||
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
|
||||
insertSliceSpecIfAbsent(milestoneId, sliceId, planning);
|
||||
currentDb
|
||||
.prepare(`UPDATE slices SET
|
||||
goal = COALESCE(:goal, goal),
|
||||
|
|
@ -2649,6 +2715,26 @@ export function insertTask(t) {
|
|||
":observability_impact": t.planning?.observabilityImpact ?? "",
|
||||
":sequence": t.sequence ?? 0,
|
||||
});
|
||||
insertTaskSpecIfAbsent(t.milestoneId, t.sliceId, t.id, t.planning ?? {});
|
||||
}
|
||||
function insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning = {}) {
|
||||
currentDb
|
||||
.prepare(`INSERT OR IGNORE INTO task_specs (
|
||||
milestone_id, slice_id, task_id, verify, inputs, expected_output,
|
||||
spec_version, created_at
|
||||
) VALUES (
|
||||
:milestone_id, :slice_id, :task_id, :verify, :inputs, :expected_output,
|
||||
1, :created_at
|
||||
)`)
|
||||
.run({
|
||||
":milestone_id": milestoneId,
|
||||
":slice_id": sliceId,
|
||||
":task_id": taskId,
|
||||
":verify": planning.verify ?? "",
|
||||
":inputs": JSON.stringify(planning.inputs ?? []),
|
||||
":expected_output": JSON.stringify(planning.expectedOutput ?? []),
|
||||
":created_at": new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
export function updateTaskStatus(
|
||||
milestoneId,
|
||||
|
|
@ -2788,6 +2874,7 @@ export function setTaskBlockerDiscovered(
|
|||
}
|
||||
export function upsertTaskPlanning(milestoneId, sliceId, taskId, planning) {
|
||||
if (!currentDb) throw new SFError(SF_STALE_STATE, "sf-db: No database open");
|
||||
insertTaskSpecIfAbsent(milestoneId, sliceId, taskId, planning);
|
||||
currentDb
|
||||
.prepare(`UPDATE tasks SET
|
||||
title = COALESCE(:title, title),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
import assert from "node:assert/strict";
|
||||
import { afterEach, test } from "vitest";
|
||||
import {
|
||||
closeDatabase,
|
||||
getMilestoneSpec,
|
||||
getSliceSpec,
|
||||
getTaskSpec,
|
||||
insertMilestone,
|
||||
insertSlice,
|
||||
insertTask,
|
||||
openDatabase,
|
||||
upsertMilestonePlanning,
|
||||
upsertSlicePlanning,
|
||||
upsertTaskPlanning,
|
||||
} from "../sf-db.js";
|
||||
|
||||
afterEach(() => {
|
||||
closeDatabase();
|
||||
});
|
||||
|
||||
test("specTables_when_live_planning_written_persist_write_once_intent", () => {
|
||||
openDatabase(":memory:");
|
||||
|
||||
insertMilestone({
|
||||
id: "M001",
|
||||
title: "Spec source",
|
||||
status: "active",
|
||||
planning: {
|
||||
vision: "Initial milestone vision",
|
||||
successCriteria: ["initial criterion"],
|
||||
},
|
||||
});
|
||||
insertSlice({
|
||||
milestoneId: "M001",
|
||||
id: "S01",
|
||||
title: "Spec slice",
|
||||
planning: {
|
||||
goal: "Initial slice goal",
|
||||
successCriteria: "initial slice criterion",
|
||||
},
|
||||
});
|
||||
insertTask({
|
||||
milestoneId: "M001",
|
||||
sliceId: "S01",
|
||||
id: "T01",
|
||||
title: "Spec task",
|
||||
planning: {
|
||||
verify: "npm test",
|
||||
inputs: ["initial input"],
|
||||
expectedOutput: ["initial output"],
|
||||
},
|
||||
});
|
||||
|
||||
upsertMilestonePlanning("M001", {
|
||||
vision: "Changed milestone vision",
|
||||
successCriteria: ["changed criterion"],
|
||||
});
|
||||
upsertSlicePlanning("M001", "S01", {
|
||||
goal: "Changed slice goal",
|
||||
successCriteria: "changed slice criterion",
|
||||
});
|
||||
upsertTaskPlanning("M001", "S01", "T01", {
|
||||
verify: "npm run test:unit",
|
||||
inputs: ["changed input"],
|
||||
expectedOutput: ["changed output"],
|
||||
});
|
||||
|
||||
const milestoneSpec = getMilestoneSpec("M001");
|
||||
assert.equal(milestoneSpec.vision, "Initial milestone vision");
|
||||
assert.deepEqual(JSON.parse(milestoneSpec.success_criteria), [
|
||||
"initial criterion",
|
||||
]);
|
||||
assert.equal(milestoneSpec.spec_version, 1);
|
||||
|
||||
const sliceSpec = getSliceSpec("M001", "S01");
|
||||
assert.equal(sliceSpec.goal, "Initial slice goal");
|
||||
assert.equal(sliceSpec.success_criteria, "initial slice criterion");
|
||||
assert.equal(sliceSpec.spec_version, 1);
|
||||
|
||||
const taskSpec = getTaskSpec("M001", "S01", "T01");
|
||||
assert.equal(taskSpec.verify, "npm test");
|
||||
assert.deepEqual(JSON.parse(taskSpec.inputs), ["initial input"]);
|
||||
assert.deepEqual(JSON.parse(taskSpec.expected_output), ["initial output"]);
|
||||
assert.equal(taskSpec.spec_version, 1);
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue