@wrongstack/sdd 0.302.2 → 0.303.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { SpecParser } from './spec-parser.js';
2
2
  export { TaskGenerator, extractVerificationCommand, assessGeneratedTaskAtomicity, type TaskGeneratorOptions, type GeneratedTask, } from './task-generator.js';
3
+ export { assertSpecTaskGraphCoverage, assertTaskGraphExecutionIntegrity, assertTaskGraphRequirementCoverage, evaluateTaskGraphRequirementCoverage, type TaskGraphRequirementCoverage, } from './requirement-coverage.js';
3
4
  export { TaskTracker, DefaultTaskStore, type TaskStore, type TaskTrackerOptions, type TaskTransition, type TaskTrackerChange, type TaskTrackerListener, } from '@wrongstack/core/tasking';
4
5
  export { TaskFlow, SpecDrivenDev, type TaskFlowPhase, type TaskFlowOptions, type TaskFlowExecutionContext, type TaskFlowEventMap, type TaskFlowEventName, type SpecDrivenDevOptions, } from './task-flow.js';
5
6
  export { SpecStore, type SpecStoreOptions, type SpecIndexEntry } from './spec-store.js';
package/dist/index.js CHANGED
@@ -280,6 +280,11 @@ var TaskGenerator = class {
280
280
  }
281
281
  async generateFromSpec(spec) {
282
282
  const graph = await this.opts.taskTracker.createGraph(spec.id, spec.title);
283
+ graph.requiredRequirementIds = Array.from(
284
+ new Set(
285
+ (spec.requirements ?? []).map((requirement) => requirement.id.trim()).filter(Boolean)
286
+ )
287
+ );
283
288
  const overviewSection = spec.sections?.find((s) => s.type === "overview");
284
289
  if (overviewSection?.content) {
285
290
  const overview = {
@@ -303,41 +308,7 @@ var TaskGenerator = class {
303
308
  (a, b) => (priorityOrder[a.priority] ?? 9) - (priorityOrder[b.priority] ?? 9)
304
309
  );
305
310
  for (const req of sorted) {
306
- const estimateHours = REQUIREMENT_ESTIMATE_HOURS[req.priority] ?? 1;
307
- const tags = [req.type, req.priority];
308
- const acLines = (req.acceptanceCriteria ?? []).map((ac) => `- ${ac}`).join("\n");
309
- const blockedLine = req.blockedBy?.length ? `
310
-
311
- **Blocked by:** ${req.blockedBy.join(", ")}` : "";
312
- const description = `${req.description}
313
-
314
- **Type:** ${req.type}` + (acLines ? `
315
-
316
- **Acceptance Criteria:**
317
- ${acLines}` : "") + blockedLine;
318
- const metadata = {
319
- ...this.atomicityMetadata({
320
- title: req.description,
321
- description,
322
- estimateHours,
323
- acceptanceCriteria: req.acceptanceCriteria ?? []
324
- })
325
- };
326
- if (this.opts.verificationFromAcceptance) {
327
- const cmd = extractVerificationCommand(req.acceptanceCriteria ?? []);
328
- if (cmd) metadata.verificationCommand = cmd;
329
- }
330
- this.opts.taskTracker.addNode({
331
- title: req.description,
332
- description,
333
- type: "feature",
334
- priority: req.priority,
335
- status: "pending",
336
- estimateHours,
337
- tags,
338
- specRequirementId: req.id,
339
- ...Object.keys(metadata).length > 0 ? { metadata } : {}
340
- });
311
+ this.addRequirementTask(req);
341
312
  }
342
313
  if (spec.apiEndpoints?.length) {
343
314
  const apiParent = this.opts.taskTracker.addNode({
@@ -401,6 +372,78 @@ ${acLines}` : "") + blockedLine;
401
372
  });
402
373
  return graph;
403
374
  }
375
+ /**
376
+ * Repair an LLM-authored or resumed graph against the authoritative spec.
377
+ * Missing requirements become deterministic tasks, so omission is never
378
+ * rewarded by a smaller task list or an earlier "complete" result.
379
+ */
380
+ ensureRequirementCoverage(spec, graph) {
381
+ if (graph.specId !== spec.id) {
382
+ throw new Error(
383
+ `Task graph spec mismatch: graph targets "${graph.specId}" but spec is "${spec.id}".`
384
+ );
385
+ }
386
+ graph.requiredRequirementIds = Array.from(
387
+ new Set(
388
+ (spec.requirements ?? []).map((requirement) => requirement.id.trim()).filter(Boolean)
389
+ )
390
+ );
391
+ const required = new Set(graph.requiredRequirementIds);
392
+ for (const node of graph.nodes.values()) {
393
+ if (node.specRequirementId && !required.has(node.specRequirementId)) {
394
+ delete node.specRequirementId;
395
+ }
396
+ }
397
+ const covered = new Set(
398
+ Array.from(graph.nodes.values()).flatMap(
399
+ (node) => node.specRequirementId ? [node.specRequirementId] : []
400
+ )
401
+ );
402
+ const added = [];
403
+ for (const requirement of spec.requirements ?? []) {
404
+ if (covered.has(requirement.id)) continue;
405
+ added.push(this.addRequirementTask(requirement));
406
+ covered.add(requirement.id);
407
+ }
408
+ return added;
409
+ }
410
+ addRequirementTask(req) {
411
+ const estimateHours = REQUIREMENT_ESTIMATE_HOURS[req.priority] ?? 1;
412
+ const tags = [req.type, req.priority];
413
+ const acLines = (req.acceptanceCriteria ?? []).map((ac) => `- ${ac}`).join("\n");
414
+ const blockedLine = req.blockedBy?.length ? `
415
+
416
+ **Blocked by:** ${req.blockedBy.join(", ")}` : "";
417
+ const description = `${req.description}
418
+
419
+ **Type:** ${req.type}` + (acLines ? `
420
+
421
+ **Acceptance Criteria:**
422
+ ${acLines}` : "") + blockedLine;
423
+ const metadata = {
424
+ ...this.atomicityMetadata({
425
+ title: req.description,
426
+ description,
427
+ estimateHours,
428
+ acceptanceCriteria: req.acceptanceCriteria ?? []
429
+ })
430
+ };
431
+ if (this.opts.verificationFromAcceptance) {
432
+ const command = extractVerificationCommand(req.acceptanceCriteria ?? []);
433
+ if (command) metadata.verificationCommand = command;
434
+ }
435
+ return this.opts.taskTracker.addNode({
436
+ title: req.description,
437
+ description,
438
+ type: "feature",
439
+ priority: req.priority,
440
+ status: "pending",
441
+ estimateHours,
442
+ tags,
443
+ specRequirementId: req.id,
444
+ ...Object.keys(metadata).length > 0 ? { metadata } : {}
445
+ });
446
+ }
404
447
  async generateSubtasks(parentTaskId, spec) {
405
448
  const reqId = this.opts.taskTracker.getNode(parentTaskId)?.specRequirementId;
406
449
  if (!reqId) return;
@@ -421,6 +464,82 @@ ${acLines}` : "") + blockedLine;
421
464
  }
422
465
  };
423
466
 
467
+ // src/requirement-coverage.ts
468
+ function uniqueIds(ids) {
469
+ return Array.from(new Set(ids.map((id) => id.trim()).filter(Boolean)));
470
+ }
471
+ function evaluateTaskGraphRequirementCoverage(graph, requiredIds = graph.requiredRequirementIds ?? []) {
472
+ const requiredRequirementIds = uniqueIds(requiredIds);
473
+ const required = new Set(requiredRequirementIds);
474
+ const mapped = uniqueIds(
475
+ Array.from(graph.nodes.values()).flatMap(
476
+ (node) => node.specRequirementId ? [node.specRequirementId] : []
477
+ )
478
+ );
479
+ const mappedSet = new Set(mapped);
480
+ const missingRequirementIds = requiredRequirementIds.filter((id) => !mappedSet.has(id));
481
+ const unknownRequirementIds = mapped.filter((id) => !required.has(id));
482
+ return {
483
+ valid: missingRequirementIds.length === 0 && unknownRequirementIds.length === 0,
484
+ requiredRequirementIds,
485
+ missingRequirementIds,
486
+ unknownRequirementIds
487
+ };
488
+ }
489
+ function assertTaskGraphRequirementCoverage(graph, requiredIds = graph.requiredRequirementIds ?? []) {
490
+ const result = evaluateTaskGraphRequirementCoverage(graph, requiredIds);
491
+ if (result.valid) return;
492
+ const problems = [
493
+ result.missingRequirementIds.length ? `missing task coverage for ${result.missingRequirementIds.join(", ")}` : "",
494
+ result.unknownRequirementIds.length ? `tasks reference unknown requirements ${result.unknownRequirementIds.join(", ")}` : ""
495
+ ].filter(Boolean);
496
+ throw new Error(`Task graph requirement coverage is invalid: ${problems.join("; ")}.`);
497
+ }
498
+ function assertSpecTaskGraphCoverage(graph, spec) {
499
+ if (graph.specId !== spec.id) {
500
+ throw new Error(
501
+ `Task graph spec mismatch: graph targets "${graph.specId}" but execution received "${spec.id}".`
502
+ );
503
+ }
504
+ assertTaskGraphRequirementCoverage(
505
+ graph,
506
+ (spec.requirements ?? []).map((requirement) => requirement.id)
507
+ );
508
+ }
509
+ function assertTaskGraphExecutionIntegrity(graph) {
510
+ const dependencies = graph.edges.filter((edge) => edge.type === "depends_on");
511
+ for (const edge of dependencies) {
512
+ if (!graph.nodes.has(edge.from) || !graph.nodes.has(edge.to)) {
513
+ throw new Error(
514
+ `Task graph dependency ${edge.id} references a missing task (${edge.from} -> ${edge.to}).`
515
+ );
516
+ }
517
+ if (edge.from === edge.to) {
518
+ throw new Error(`Task graph dependency ${edge.id} is a self-cycle.`);
519
+ }
520
+ }
521
+ const indegree = new Map(Array.from(graph.nodes.keys(), (id) => [id, 0]));
522
+ const outgoing = /* @__PURE__ */ new Map();
523
+ for (const edge of dependencies) {
524
+ indegree.set(edge.to, (indegree.get(edge.to) ?? 0) + 1);
525
+ outgoing.set(edge.from, [...outgoing.get(edge.from) ?? [], edge.to]);
526
+ }
527
+ const ready = Array.from(indegree.entries()).filter(([, degree]) => degree === 0).map(([id]) => id);
528
+ let visited = 0;
529
+ while (ready.length > 0) {
530
+ const id = ready.shift();
531
+ visited += 1;
532
+ for (const dependent of outgoing.get(id) ?? []) {
533
+ const next = (indegree.get(dependent) ?? 0) - 1;
534
+ indegree.set(dependent, next);
535
+ if (next === 0) ready.push(dependent);
536
+ }
537
+ }
538
+ if (visited !== graph.nodes.size) {
539
+ throw new Error("Task graph dependency cycle prevents deterministic execution.");
540
+ }
541
+ }
542
+
424
543
  // src/index.ts
425
544
  import {
426
545
  TaskTracker as TaskTracker3,
@@ -2426,9 +2545,17 @@ var SddInterviewDriver = class {
2426
2545
  * emitted a parseable task array.
2427
2546
  */
2428
2547
  async ensureTaskGraph() {
2429
- if (this.graph) return this.graph;
2430
2548
  const spec = this.builder.getSession().spec;
2431
2549
  if (!spec) return null;
2550
+ if (this.graph && this.tracker) {
2551
+ const generator2 = new TaskGenerator({
2552
+ taskTracker: this.tracker,
2553
+ verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
2554
+ });
2555
+ generator2.ensureRequirementCoverage(spec, this.graph);
2556
+ await this.persistGraph(this.graph);
2557
+ return this.graph;
2558
+ }
2432
2559
  const tracker = new TaskTracker2({ store: this.o.graphStore });
2433
2560
  const generator = new TaskGenerator({
2434
2561
  taskTracker: tracker,
@@ -2550,6 +2677,10 @@ var SddInterviewDriver = class {
2550
2677
  if (depId && depId !== nodeId) tracker.addDependency(depId, nodeId);
2551
2678
  }
2552
2679
  }
2680
+ new TaskGenerator({
2681
+ taskTracker: tracker,
2682
+ verificationFromAcceptance: process.env["WRONGSTACK_SDD_VERIFY_FROM_ACCEPTANCE"] === "1"
2683
+ }).ensureRequirementCoverage(spec, graph);
2553
2684
  await this.persistGraph(graph);
2554
2685
  await this.builder.setTaskGraphId(graph.id);
2555
2686
  await this.builder.saveSession();
@@ -2562,6 +2693,8 @@ function normalizeTaskRef(ref) {
2562
2693
  return ref.trim().toLowerCase();
2563
2694
  }
2564
2695
  function addTaskToTracker(tracker, task) {
2696
+ const rawRequirementId = task.specRequirementId ?? task.requirementId;
2697
+ const specRequirementId = typeof rawRequirementId === "string" && rawRequirementId.trim() ? rawRequirementId.trim() : void 0;
2565
2698
  return tracker.addNode({
2566
2699
  title: String(task.title),
2567
2700
  description: String(task.description ?? ""),
@@ -2569,7 +2702,8 @@ function addTaskToTracker(tracker, task) {
2569
2702
  priority: TASK_PRIORITIES.includes(String(task.priority)) ? String(task.priority) : "medium",
2570
2703
  status: "pending",
2571
2704
  estimateHours: Number(task.estimateHours) || 2,
2572
- tags: Array.isArray(task.tags) ? task.tags.map(String) : []
2705
+ tags: Array.isArray(task.tags) ? task.tags.map(String) : [],
2706
+ ...specRequirementId ? { specRequirementId } : {}
2573
2707
  });
2574
2708
  }
2575
2709
  function isExplanatoryText(text) {
@@ -3772,6 +3906,10 @@ function applySddControlCommand(run, command) {
3772
3906
  });
3773
3907
  }
3774
3908
  function startSddRun(opts) {
3909
+ assertTaskGraphExecutionIntegrity(opts.graph);
3910
+ if (opts.graph.requiredRequirementIds !== void 0) {
3911
+ assertTaskGraphRequirementCoverage(opts.graph);
3912
+ }
3775
3913
  SddParallelRun.resetOrphans(opts.tracker);
3776
3914
  const run = new SddParallelRun({
3777
3915
  tracker: opts.tracker,
@@ -4912,6 +5050,8 @@ var AutoExecutor = class {
4912
5050
  * Execute all tasks in the graph, respecting dependencies.
4913
5051
  */
4914
5052
  async execute(graph, spec) {
5053
+ assertSpecTaskGraphCoverage(graph, spec);
5054
+ assertTaskGraphExecutionIntegrity(graph);
4915
5055
  this.stopped = false;
4916
5056
  this.retryMap.clear();
4917
5057
  const startTime = Date.now();
@@ -4980,7 +5120,7 @@ var AutoExecutor = class {
4980
5120
  const ready = [];
4981
5121
  for (const node of graph.nodes.values()) {
4982
5122
  if (node.status !== "pending") continue;
4983
- const blockers = graph.edges.filter((e) => e.type === "depends_on" && e.from === node.id).map((e) => graph.nodes.get(e.to)).filter(Boolean);
5123
+ const blockers = graph.edges.filter((e) => e.type === "depends_on" && e.to === node.id).map((e) => graph.nodes.get(e.from)).filter(Boolean);
4984
5124
  const allBlockersDone = blockers.every((b) => b.status === "completed");
4985
5125
  if (allBlockersDone) {
4986
5126
  ready.push(node);
@@ -5041,11 +5181,11 @@ var AutoExecutor = class {
5041
5181
  }
5042
5182
  /** Get tasks that this task depends on. */
5043
5183
  getTaskDependencies(taskId, graph) {
5044
- return graph.edges.filter((e) => e.type === "depends_on" && e.from === taskId).map((e) => graph.nodes.get(e.to)).filter(Boolean);
5184
+ return graph.edges.filter((e) => e.type === "depends_on" && e.to === taskId).map((e) => graph.nodes.get(e.from)).filter(Boolean);
5045
5185
  }
5046
5186
  /** Get tasks that depend on this task. */
5047
5187
  getTaskDependents(taskId, graph) {
5048
- return graph.edges.filter((e) => e.type === "depends_on" && e.to === taskId).map((e) => graph.nodes.get(e.from)).filter(Boolean);
5188
+ return graph.edges.filter((e) => e.type === "depends_on" && e.from === taskId).map((e) => graph.nodes.get(e.to)).filter(Boolean);
5049
5189
  }
5050
5190
  /** Detect deadlock: all remaining tasks are blocked by failed tasks. */
5051
5191
  detectDeadlock(graph) {
@@ -5054,7 +5194,7 @@ var AutoExecutor = class {
5054
5194
  );
5055
5195
  if (remaining.length === 0) return false;
5056
5196
  return remaining.every((node) => {
5057
- const blockers = graph.edges.filter((e) => e.type === "depends_on" && e.from === node.id).map((e) => graph.nodes.get(e.to)).filter(Boolean);
5197
+ const blockers = graph.edges.filter((e) => e.type === "depends_on" && e.to === node.id).map((e) => graph.nodes.get(e.from)).filter(Boolean);
5058
5198
  return blockers.some((b) => b.status === "failed");
5059
5199
  });
5060
5200
  }
@@ -5583,6 +5723,9 @@ export {
5583
5723
  TaskTracker3 as TaskTracker,
5584
5724
  analyzeCriticalPath,
5585
5725
  applySddLifecycle,
5726
+ assertSpecTaskGraphCoverage,
5727
+ assertTaskGraphExecutionIntegrity,
5728
+ assertTaskGraphRequirementCoverage,
5586
5729
  assessGeneratedTaskAtomicity,
5587
5730
  assessTaskNodeAtomicity,
5588
5731
  buildBoardSnapshot,
@@ -5594,6 +5737,7 @@ export {
5594
5737
  createKanbanSddSessionPersistence,
5595
5738
  decomposeNonAtomicTasks,
5596
5739
  destroySddProject,
5740
+ evaluateTaskGraphRequirementCoverage,
5597
5741
  extractVerificationCommand,
5598
5742
  gatherProjectContext,
5599
5743
  getTemplate,
@@ -0,0 +1,14 @@
1
+ import type { Specification, TaskGraph } from '@wrongstack/core/types';
2
+ export interface TaskGraphRequirementCoverage {
3
+ valid: boolean;
4
+ requiredRequirementIds: string[];
5
+ missingRequirementIds: string[];
6
+ unknownRequirementIds: string[];
7
+ }
8
+ /** Deterministic requirement-to-task coverage; no model judgment is involved. */
9
+ export declare function evaluateTaskGraphRequirementCoverage(graph: TaskGraph, requiredIds?: readonly string[]): TaskGraphRequirementCoverage;
10
+ export declare function assertTaskGraphRequirementCoverage(graph: TaskGraph, requiredIds?: readonly string[]): void;
11
+ export declare function assertSpecTaskGraphCoverage(graph: TaskGraph, spec: Specification): void;
12
+ /** Fail before dispatch when dependency edges cannot form an executable DAG. */
13
+ export declare function assertTaskGraphExecutionIntegrity(graph: TaskGraph): void;
14
+ //# sourceMappingURL=requirement-coverage.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import type { TaskStore, TaskTracker } from '@wrongstack/core/tasking';
2
- import type { Specification, TaskGraph, TaskPriority, TaskType } from '@wrongstack/core/types';
2
+ import type { Specification, TaskGraph, TaskNode, TaskPriority, TaskType } from '@wrongstack/core/types';
3
3
  import { type AtomicityRuleSetConfig } from '@wrongstack/kanban';
4
4
  /** Named estimate constants shared with the atomicity candidate mapping. */
5
5
  export declare const OVERVIEW_ESTIMATE_HOURS = 4;
@@ -62,6 +62,13 @@ export declare class TaskGenerator {
62
62
  /** metadata.atomicity payload when the option is enabled, else undefined. */
63
63
  private atomicityMetadata;
64
64
  generateFromSpec(spec: Specification): Promise<TaskGraph>;
65
+ /**
66
+ * Repair an LLM-authored or resumed graph against the authoritative spec.
67
+ * Missing requirements become deterministic tasks, so omission is never
68
+ * rewarded by a smaller task list or an earlier "complete" result.
69
+ */
70
+ ensureRequirementCoverage(spec: Specification, graph: TaskGraph): TaskNode[];
71
+ private addRequirementTask;
65
72
  generateSubtasks(parentTaskId: string, spec: Specification): Promise<void>;
66
73
  }
67
74
  export type { TaskStore };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wrongstack/sdd",
3
- "version": "0.302.2",
3
+ "version": "0.303.0",
4
4
  "license": "MIT",
5
5
  "description": "WrongStack Spec-Driven Development engine — standalone package extracted from @wrongstack/core. Task graph generation, tracking, execution, lifecycle management, and AI-driven spec building for SDD workflows.",
6
6
  "repository": {
@@ -27,9 +27,9 @@
27
27
  "!dist/**/*.map"
28
28
  ],
29
29
  "dependencies": {
30
- "@wrongstack/core": "0.302.2",
31
- "@wrongstack/requirement-intake": "0.302.2",
32
- "@wrongstack/kanban": "0.302.2"
30
+ "@wrongstack/core": "0.303.0",
31
+ "@wrongstack/kanban": "0.303.0",
32
+ "@wrongstack/requirement-intake": "0.303.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/node": "^26.1.2",