fix(gsd): address adversarial review findings for wave 1
1. Type guard on cmd normalization: non-string cmd values are now skipped with a warning instead of throwing, preventing replay from crashing on malformed event lines. 2. complete_milestone replay now validates all slices are closed before marking milestone complete. Prevents a reordered/partial event stream from closing a milestone with incomplete work. 3. Type guard on extractEntityKey cmd normalization for consistency.
This commit is contained in:
parent
03dc62308d
commit
40a37125fe
1 changed files with 16 additions and 2 deletions
|
|
@ -9,6 +9,7 @@ import {
|
|||
updateSliceStatus,
|
||||
updateMilestoneStatus,
|
||||
getSliceTasks,
|
||||
getMilestoneSlices,
|
||||
insertVerificationEvidence,
|
||||
upsertDecision,
|
||||
openDatabase,
|
||||
|
|
@ -77,6 +78,11 @@ function replayEvents(events: WorkflowEvent[]): void {
|
|||
const p = event.params;
|
||||
// Normalize cmd format: completion tools write hyphens ("complete-task"),
|
||||
// legacy logs use underscores ("complete_task"). Accept both formats.
|
||||
// Type guard: malformed event lines with non-string cmd are skipped.
|
||||
if (typeof event.cmd !== "string") {
|
||||
logWarning("reconcile", `Event with non-string cmd skipped: ${JSON.stringify(event.cmd)}`);
|
||||
continue;
|
||||
}
|
||||
const cmd = event.cmd.replace(/-/g, "_");
|
||||
switch (cmd) {
|
||||
case "complete_task": {
|
||||
|
|
@ -125,9 +131,16 @@ function replayEvents(events: WorkflowEvent[]): void {
|
|||
}
|
||||
case "complete_milestone": {
|
||||
const milestoneId = p["milestoneId"] as string;
|
||||
// Milestone completion via worktree replay — update status to complete
|
||||
if (milestoneId) {
|
||||
if (!milestoneId) break;
|
||||
// Invariant check: only mark complete if all slices are closed.
|
||||
// Without this guard, a reordered/partial event stream could close
|
||||
// a milestone while work is still incomplete.
|
||||
const mSlices = getMilestoneSlices(milestoneId);
|
||||
const allClosed = mSlices.length === 0 || mSlices.every(s => isClosedStatus(s.status));
|
||||
if (allClosed) {
|
||||
updateMilestoneStatus(milestoneId, "complete", event.ts);
|
||||
} else {
|
||||
logWarning("reconcile", `Skipping complete_milestone replay for ${milestoneId}: not all slices are closed`);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
|
@ -170,6 +183,7 @@ export function extractEntityKey(
|
|||
): { type: string; id: string } | null {
|
||||
const p = event.params;
|
||||
// Normalize cmd format: accept both hyphens and underscores
|
||||
if (typeof event.cmd !== "string") return null;
|
||||
const cmd = event.cmd.replace(/-/g, "_");
|
||||
|
||||
switch (cmd) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue