@lwelliott/cortex-cli 1.0.6 → 1.0.8

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.
Files changed (114) hide show
  1. package/README.md +53 -26
  2. package/dist/cli/formatters/markdown.d.ts.map +1 -1
  3. package/dist/cli/formatters/markdown.js +0 -4
  4. package/dist/cli/formatters/markdown.js.map +1 -1
  5. package/dist/cli/formatters/quiet.d.ts.map +1 -1
  6. package/dist/cli/formatters/quiet.js +0 -2
  7. package/dist/cli/formatters/quiet.js.map +1 -1
  8. package/dist/cli/formatters/table.d.ts.map +1 -1
  9. package/dist/cli/formatters/table.js +0 -5
  10. package/dist/cli/formatters/table.js.map +1 -1
  11. package/dist/cli/project-resolver.js +1 -1
  12. package/dist/cli/project-resolver.js.map +1 -1
  13. package/dist/cli/project.d.ts +0 -1
  14. package/dist/cli/project.d.ts.map +1 -1
  15. package/dist/cli/project.js +2 -35
  16. package/dist/cli/project.js.map +1 -1
  17. package/dist/cli/task.d.ts.map +1 -1
  18. package/dist/cli/task.js +22 -7
  19. package/dist/cli/task.js.map +1 -1
  20. package/dist/config/config.d.ts +8 -2
  21. package/dist/config/config.d.ts.map +1 -1
  22. package/dist/config/config.js +25 -9
  23. package/dist/config/config.js.map +1 -1
  24. package/dist/config/paths.d.ts +13 -0
  25. package/dist/config/paths.d.ts.map +1 -0
  26. package/dist/config/paths.js +76 -0
  27. package/dist/config/paths.js.map +1 -0
  28. package/dist/config/registry.d.ts +10 -12
  29. package/dist/config/registry.d.ts.map +1 -1
  30. package/dist/config/registry.js +50 -44
  31. package/dist/config/registry.js.map +1 -1
  32. package/dist/config/resolver.d.ts +5 -9
  33. package/dist/config/resolver.d.ts.map +1 -1
  34. package/dist/config/resolver.js +12 -46
  35. package/dist/config/resolver.js.map +1 -1
  36. package/dist/db/schema.d.ts +1 -1
  37. package/dist/db/schema.d.ts.map +1 -1
  38. package/dist/db/schema.js +70 -4
  39. package/dist/db/schema.js.map +1 -1
  40. package/dist/models/common.d.ts +2 -2
  41. package/dist/models/common.d.ts.map +1 -1
  42. package/dist/models/project.d.ts +4 -6
  43. package/dist/models/project.d.ts.map +1 -1
  44. package/dist/models/project.js +0 -1
  45. package/dist/models/project.js.map +1 -1
  46. package/dist/services/project.d.ts +0 -10
  47. package/dist/services/project.d.ts.map +1 -1
  48. package/dist/services/project.js +6 -30
  49. package/dist/services/project.js.map +1 -1
  50. package/dist/services/stage-validator.d.ts +19 -18
  51. package/dist/services/stage-validator.d.ts.map +1 -1
  52. package/dist/services/stage-validator.js +41 -42
  53. package/dist/services/stage-validator.js.map +1 -1
  54. package/dist/services/task.d.ts +18 -21
  55. package/dist/services/task.d.ts.map +1 -1
  56. package/dist/services/task.js +96 -86
  57. package/dist/services/task.js.map +1 -1
  58. package/dist/state-machines/task-state.d.ts +8 -8
  59. package/dist/state-machines/task-state.d.ts.map +1 -1
  60. package/dist/state-machines/task-state.js +33 -31
  61. package/dist/state-machines/task-state.js.map +1 -1
  62. package/dist/utils/errors.d.ts +26 -55
  63. package/dist/utils/errors.d.ts.map +1 -1
  64. package/dist/utils/errors.js +73 -124
  65. package/dist/utils/errors.js.map +1 -1
  66. package/dist/utils/format.d.ts +0 -11
  67. package/dist/utils/format.d.ts.map +1 -1
  68. package/dist/utils/format.js +1 -10
  69. package/dist/utils/format.js.map +1 -1
  70. package/dist/utils/levenshtein.d.ts +10 -0
  71. package/dist/utils/levenshtein.d.ts.map +1 -0
  72. package/dist/utils/levenshtein.js +42 -0
  73. package/dist/utils/levenshtein.js.map +1 -0
  74. package/dist/validators/owner-validator.d.ts +20 -16
  75. package/dist/validators/owner-validator.d.ts.map +1 -1
  76. package/dist/validators/owner-validator.js +37 -38
  77. package/dist/validators/owner-validator.js.map +1 -1
  78. package/package.json +9 -2
  79. package/dist/cli/entity.d.ts +0 -3
  80. package/dist/cli/entity.d.ts.map +0 -1
  81. package/dist/cli/entity.js +0 -135
  82. package/dist/cli/entity.js.map +0 -1
  83. package/dist/cli/schema.d.ts +0 -3
  84. package/dist/cli/schema.d.ts.map +0 -1
  85. package/dist/cli/schema.js +0 -94
  86. package/dist/cli/schema.js.map +0 -1
  87. package/dist/db/builtin-schema-constants.d.ts +0 -109
  88. package/dist/db/builtin-schema-constants.d.ts.map +0 -1
  89. package/dist/db/builtin-schema-constants.js +0 -32
  90. package/dist/db/builtin-schema-constants.js.map +0 -1
  91. package/dist/models/entity-model.d.ts +0 -30
  92. package/dist/models/entity-model.d.ts.map +0 -1
  93. package/dist/models/entity-model.js +0 -99
  94. package/dist/models/entity-model.js.map +0 -1
  95. package/dist/services/audit-checks/mapping-check.d.ts +0 -4
  96. package/dist/services/audit-checks/mapping-check.d.ts.map +0 -1
  97. package/dist/services/audit-checks/mapping-check.js +0 -74
  98. package/dist/services/audit-checks/mapping-check.js.map +0 -1
  99. package/dist/services/audit-checks/orphan-entities-check.d.ts +0 -4
  100. package/dist/services/audit-checks/orphan-entities-check.d.ts.map +0 -1
  101. package/dist/services/audit-checks/orphan-entities-check.js +0 -39
  102. package/dist/services/audit-checks/orphan-entities-check.js.map +0 -1
  103. package/dist/services/audit-checks/sprint-consistency-check.d.ts +0 -4
  104. package/dist/services/audit-checks/sprint-consistency-check.d.ts.map +0 -1
  105. package/dist/services/audit-checks/sprint-consistency-check.js +0 -80
  106. package/dist/services/audit-checks/sprint-consistency-check.js.map +0 -1
  107. package/dist/services/audit-checks/type-mismatch-check.d.ts +0 -4
  108. package/dist/services/audit-checks/type-mismatch-check.d.ts.map +0 -1
  109. package/dist/services/audit-checks/type-mismatch-check.js +0 -55
  110. package/dist/services/audit-checks/type-mismatch-check.js.map +0 -1
  111. package/dist/services/doc-entity-mapping.d.ts +0 -19
  112. package/dist/services/doc-entity-mapping.d.ts.map +0 -1
  113. package/dist/services/doc-entity-mapping.js +0 -35
  114. package/dist/services/doc-entity-mapping.js.map +0 -1
@@ -1,32 +1,42 @@
1
1
  "use strict";
2
2
  // Stage progression validator
3
- // Pure function module — validates one-way forward stage progression
4
- // Implements AD-0031: Stage-Driven Workflow Engine
5
- // + FS-SS-030-0001 (stage progression engine)
6
- // + FS-SS-030-0003 (task stage assignment)
3
+ // Pure function module — validates stage values case-insensitively
4
+ // Implements AD-0031 v2: Consolidated Stage/Status Workflow Engine
5
+ // + FS-SS-030-0001 (stage progression engine v2)
6
+ // 6 consolidated stages, freely reversible, case-insensitive with closest-match suggestions
7
7
  Object.defineProperty(exports, "__esModule", { value: true });
8
8
  exports.CANONICAL_STAGES = void 0;
9
9
  exports.isValidStage = isValidStage;
10
+ exports.normalizeStage = normalizeStage;
10
11
  exports.getValidStages = getValidStages;
11
- exports.isForward = isForward;
12
+ exports.validateStage = validateStage;
13
+ exports.validateStageValue = validateStage;
12
14
  exports.validateProgression = validateProgression;
13
15
  const errors_1 = require("../utils/errors");
14
- /** Canonical stage sequence in forward order */
16
+ const levenshtein_1 = require("../utils/levenshtein");
17
+ /** Canonical stage sequence — 6 consolidated stages */
15
18
  exports.CANONICAL_STAGES = [
16
19
  'Planning',
17
- 'Requirement',
18
- 'Specification',
20
+ 'Requirement+Specification',
19
21
  'Design',
20
- 'Test Plan',
21
22
  'Implementation',
22
23
  'Testing',
23
24
  'Release',
24
25
  ];
26
+ /** Lowercase mapping for case-insensitive lookup */
27
+ const STAGE_LOWERCASE_MAP = new Map(exports.CANONICAL_STAGES.map(s => [s.toLowerCase(), s]));
25
28
  /**
26
- * Check if a given string is a valid stage value.
29
+ * Check if a given string is a valid stage value (case-insensitive).
27
30
  */
28
31
  function isValidStage(stage) {
29
- return exports.CANONICAL_STAGES.includes(stage);
32
+ return STAGE_LOWERCASE_MAP.has(stage.toLowerCase());
33
+ }
34
+ /**
35
+ * Normalize a stage value to canonical case.
36
+ * Returns null if the value is not a valid stage.
37
+ */
38
+ function normalizeStage(stage) {
39
+ return STAGE_LOWERCASE_MAP.get(stage.toLowerCase()) ?? null;
30
40
  }
31
41
  /**
32
42
  * Return the canonical stage sequence.
@@ -35,44 +45,33 @@ function getValidStages() {
35
45
  return [...exports.CANONICAL_STAGES];
36
46
  }
37
47
  /**
38
- * Check if target stage is strictly forward of current stage in the canonical sequence.
39
- * Returns true if target position > current position (allows skipping).
48
+ * Validate a stage value case-insensitively with closest-match suggestions.
49
+ *
50
+ * @param stage - The stage value to validate
51
+ * @returns The canonical stage value if valid
52
+ * @throws StageInvalidError with closest-match suggestions if invalid
40
53
  */
41
- function isForward(current, target) {
42
- const currentIdx = exports.CANONICAL_STAGES.indexOf(current);
43
- const targetIdx = exports.CANONICAL_STAGES.indexOf(target);
44
- return targetIdx > currentIdx;
54
+ function validateStage(stage) {
55
+ const normalized = normalizeStage(stage);
56
+ if (normalized !== null) {
57
+ return normalized;
58
+ }
59
+ const suggestions = (0, levenshtein_1.getClosestMatches)(stage, exports.CANONICAL_STAGES);
60
+ throw new errors_1.StageInvalidError(stage, suggestions);
45
61
  }
46
62
  /**
47
63
  * Validate that a stage progression is valid.
48
64
  *
49
- * Rules (FS-SS-030-0001):
50
- * - SR-001: Only forward progression (never backward)
51
- * - SR-002: Intermediate stages may be skipped
52
- * - SR-003: Release is the final stage
53
- * - SR-004: Dropped status locks the task — no stage changes
54
- * - SR-005: Same-stage transitions are rejected
65
+ * v2: Free stage transitions. Forward, backward, and same-stage all allowed.
66
+ * No forward-only enforcement. No Dropped lock. No owner-for-stage check.
55
67
  *
56
- * @param currentStage - The task's current stage
68
+ * @param currentStage - The task's current stage (unused in v2, kept for API compatibility)
57
69
  * @param targetStage - The desired target stage
58
- * @param taskStatus - The task's current status (to check Dropped lock)
59
- * @throws StageInvalidError if target is not a valid stage
60
- * @throws StageLockedError if task is Dropped
61
- * @throws StageNoProgressError if target is same as current
62
- * @throws StageBackwardError if target is backward of current
70
+ * @param taskStatus - The task's current status (unused in v2, kept for API compatibility)
63
71
  */
64
- function validateProgression(currentStage, targetStage, taskStatus) {
65
- // Check Dropped lock first
66
- if (taskStatus === 'Dropped') {
67
- throw new errors_1.StageLockedError('(task)');
68
- }
69
- // Check same-stage no-op
70
- if (currentStage === targetStage) {
71
- throw new errors_1.StageNoProgressError('(task)', currentStage);
72
- }
73
- // Check forward progression
74
- if (!isForward(currentStage, targetStage)) {
75
- throw new errors_1.StageBackwardError(currentStage, targetStage);
76
- }
72
+ function validateProgression(_currentStage, _targetStage, _taskStatus) {
73
+ // v2: No validation restrictions. All stage transitions are freely allowed.
74
+ // No forward-only enforcement, no backward rejection, no same-stage rejection,
75
+ // no Dropped lock, no owner-for-stage check.
77
76
  }
78
77
  //# sourceMappingURL=stage-validator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"stage-validator.js","sourceRoot":"","sources":["../../src/services/stage-validator.ts"],"names":[],"mappings":";AAAA,8BAA8B;AAC9B,qEAAqE;AACrE,mDAAmD;AACnD,8CAA8C;AAC9C,2CAA2C;;;AAwB3C,oCAEC;AAKD,wCAEC;AAMD,8BAIC;AAoBD,kDAmBC;AA/ED,4CAIyB;AAEzB,gDAAgD;AACnC,QAAA,gBAAgB,GAAgB;IAC3C,UAAU;IACV,aAAa;IACb,eAAe;IACf,QAAQ;IACR,WAAW;IACX,gBAAgB;IAChB,SAAS;IACT,SAAS;CACV,CAAC;AAEF;;GAEG;AACH,SAAgB,YAAY,CAAC,KAAa;IACxC,OAAQ,wBAAsC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACjE,CAAC;AAED;;GAEG;AACH,SAAgB,cAAc;IAC5B,OAAO,CAAC,GAAG,wBAAgB,CAAC,CAAC;AAC/B,CAAC;AAED;;;GAGG;AACH,SAAgB,SAAS,CAAC,OAAkB,EAAE,MAAiB;IAC7D,MAAM,UAAU,GAAG,wBAAgB,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC;IACrD,MAAM,SAAS,GAAG,wBAAgB,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACnD,OAAO,SAAS,GAAG,UAAU,CAAC;AAChC,CAAC;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,mBAAmB,CACjC,YAAuB,EACvB,WAAsB,EACtB,UAAkB;IAElB,2BAA2B;IAC3B,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;QAC7B,MAAM,IAAI,yBAAgB,CAAC,QAAQ,CAAC,CAAC;IACvC,CAAC;IAED,yBAAyB;IACzB,IAAI,YAAY,KAAK,WAAW,EAAE,CAAC;QACjC,MAAM,IAAI,6BAAoB,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACzD,CAAC;IAED,4BAA4B;IAC5B,IAAI,CAAC,SAAS,CAAC,YAAY,EAAE,WAAW,CAAC,EAAE,CAAC;QAC1C,MAAM,IAAI,2BAAkB,CAAC,YAAY,EAAE,WAAW,CAAC,CAAC;IAC1D,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"stage-validator.js","sourceRoot":"","sources":["../../src/services/stage-validator.ts"],"names":[],"mappings":";AAAA,8BAA8B;AAC9B,mEAAmE;AACnE,mEAAmE;AACnE,iDAAiD;AACjD,4FAA4F;;;AAwB5F,oCAEC;AAMD,wCAEC;AAKD,wCAEC;AASD,sCAQC;AAuByB,2CAAkB;AAX5C,kDAQC;AA3ED,4CAAoD;AACpD,sDAAyD;AAEzD,uDAAuD;AAC1C,QAAA,gBAAgB,GAAgB;IAC3C,UAAU;IACV,2BAA2B;IAC3B,QAAQ;IACR,gBAAgB;IAChB,SAAS;IACT,SAAS;CACV,CAAC;AAEF,oDAAoD;AACpD,MAAM,mBAAmB,GAA2B,IAAI,GAAG,CACzD,wBAAgB,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,EAAE,CAAC,CAAC,CAAC,CAChD,CAAC;AAEF;;GAEG;AACH,SAAgB,YAAY,CAAC,KAAa;IACxC,OAAO,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;AACtD,CAAC;AAED;;;GAGG;AACH,SAAgB,cAAc,CAAC,KAAa;IAC1C,OAAO,mBAAmB,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,IAAI,IAAI,CAAC;AAC9D,CAAC;AAED;;GAEG;AACH,SAAgB,cAAc;IAC5B,OAAO,CAAC,GAAG,wBAAgB,CAAC,CAAC;AAC/B,CAAC;AAED;;;;;;GAMG;AACH,SAAgB,aAAa,CAAC,KAAa;IACzC,MAAM,UAAU,GAAG,cAAc,CAAC,KAAK,CAAC,CAAC;IACzC,IAAI,UAAU,KAAK,IAAI,EAAE,CAAC;QACxB,OAAO,UAAU,CAAC;IACpB,CAAC;IAED,MAAM,WAAW,GAAG,IAAA,+BAAiB,EAAC,KAAK,EAAE,wBAA4B,CAAC,CAAC;IAC3E,MAAM,IAAI,0BAAiB,CAAC,KAAK,EAAE,WAAW,CAAC,CAAC;AAClD,CAAC;AAED;;;;;;;;;GASG;AACH,SAAgB,mBAAmB,CACjC,aAAwB,EACxB,YAAuB,EACvB,WAAmB;IAEnB,4EAA4E;IAC5E,+EAA+E;IAC/E,6CAA6C;AAC/C,CAAC"}
@@ -1,30 +1,38 @@
1
1
  import type Database from 'better-sqlite3';
2
2
  import type { Task, TaskResult, TaskDetail, TaskAddOptions, TaskMoveContext, TaskUpdateFields } from '../models/task';
3
3
  import type { TaskStatus } from '../models/common';
4
- import { validateOwnerRole, isValidRole } from '../validators/owner-validator';
4
+ import { validateOwnerRole } from '../validators/owner-validator';
5
5
  /**
6
6
  * Add a new task to a sprint.
7
7
  *
8
+ * v2: No captain-only owner validation. Any valid role can own a task in any stage.
9
+ *
8
10
  * @param db - Database connection
9
11
  * @param sprintId - Sprint ID
10
12
  * @param content - Task content (nullable; can be populated via --content-file)
11
- * @param owner - Owner role (must be valid, captain-only for Backlog)
13
+ * @param owner - Owner role (must be valid)
12
14
  * @param opts - Optional task properties
13
15
  * @returns The created task result
14
16
  * @throws TaskBadSprintError if sprint doesn't exist or is closed
15
17
  * @throws TaskOwnerRequiredError if owner is empty
16
- * @throws various owner validation errors
18
+ * @throws various validation errors
17
19
  */
18
20
  export declare function addTask(db: Database.Database, sprintId: string, content: string | null, owner: string, opts?: TaskAddOptions): TaskResult;
19
21
  /**
20
- * Move a task: status transition, stage transition, or combined.
22
+ * Move a task: status transition, stage transition, owner change, or combined.
21
23
  *
22
- * Per AD-0039: stage validation runs before status validation in combined moves.
23
- * Per FR-URD-034-0004: active sprint is required — no --sprint override.
24
+ * v2 per AD-0039 / AD-0031:
25
+ * - Case-insensitive validation for status, stage, owner
26
+ * - Closest-match suggestions via Levenshtein distance
27
+ * - Free stage transitions (forward, backward, same-stage all allowed)
28
+ * - Non-terminal Dropped (can recover to Backlog/To Do)
29
+ * - Dry-run mode: validate without persisting
30
+ * - Compulsory parameters: at least one of --to, --stage, --owner
31
+ * - No --sprint override; move always operates on active sprint
24
32
  *
25
33
  * @param db - Database connection
26
34
  * @param taskId - Task ID (4-digit NNNN)
27
- * @param opts - Move options: targetStatus, targetStage, context
35
+ * @param opts - Move options: targetStatus, targetStage, context, dryRun, sprintId
28
36
  * @returns Updated task result
29
37
  */
30
38
  export declare function moveTask(db: Database.Database, taskId: string, opts: {
@@ -32,32 +40,21 @@ export declare function moveTask(db: Database.Database, taskId: string, opts: {
32
40
  targetStage?: string;
33
41
  context?: TaskMoveContext;
34
42
  sprintId?: string;
43
+ dryRun?: boolean;
35
44
  }): TaskResult;
36
45
  /**
37
46
  * Update task metadata (content/type/priority/points/scope/acceptance).
38
47
  * Does NOT change task status, stage, owner, or sprint — use moveTask for those.
39
48
  *
40
49
  * Per AD-0038: rejects stage, owner, sprint fields at the service layer.
41
- *
42
- * @throws TaskNotFoundError if task doesn't exist
43
- * @throws TaskInvalidFieldError if prohibited fields are present
44
50
  */
45
51
  export declare function updateTask(db: Database.Database, taskId: string, fields: TaskUpdateFields, sprintId?: string): TaskResult;
46
52
  /**
47
- * Remove a task (soft delete by default per AD-0011).
48
- *
49
- * @param db - Database connection
50
- * @param taskId - Task ID
51
- * @throws TaskNotFoundError if task doesn't exist
53
+ * Remove a task (permanent delete).
52
54
  */
53
55
  export declare function removeTask(db: Database.Database, taskId: string, sprintId?: string): void;
54
56
  /**
55
57
  * Show detailed task information including sprint name and parent content.
56
- *
57
- * @param db - Database connection
58
- * @param taskId - Task ID
59
- * @param sprintId - Sprint ID (required for sprint-scoped lookup per URD-026)
60
- * @throws TaskNotFoundError if task doesn't exist or is not in the specified sprint
61
58
  */
62
59
  export declare function showTask(db: Database.Database, taskId: string, sprintId?: string): TaskDetail;
63
60
  /**
@@ -71,5 +68,5 @@ export declare function listTasks(db: Database.Database, sprintId: string, filte
71
68
  * Get task status counts for a sprint.
72
69
  */
73
70
  export declare function getTaskStatusCounts(db: Database.Database, sprintId: string): Record<string, number>;
74
- export { isValidRole, validateOwnerRole };
71
+ export { validateOwnerRole };
75
72
  //# sourceMappingURL=task.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/services/task.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtH,OAAO,KAAK,EAAE,UAAU,EAA+C,MAAM,kBAAkB,CAAC;AAwBhG,OAAO,EAAyB,iBAAiB,EAAE,WAAW,EAAE,MAAM,+BAA+B,CAAC;AA4DtG;;;;;;;;;;;;GAYG;AACH,wBAAgB,OAAO,CACrB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,cAAc,GACpB,UAAU,CAoGZ;AAmBD;;;;;;;;;;GAUG;AACH,wBAAgB,QAAQ,CACtB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IACJ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GACA,UAAU,CAgIZ;AAED;;;;;;;;GAQG;AACH,wBAAgB,UAAU,CACxB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,gBAAgB,EACxB,QAAQ,CAAC,EAAE,MAAM,GAChB,UAAU,CAqHZ;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CACxB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CAgCN;AAED;;;;;;;GAOG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAwB7F;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,IAAI,EAAE,CAuBR;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxB;AAGD,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,CAAC"}
1
+ {"version":3,"file":"task.d.ts","sourceRoot":"","sources":["../../src/services/task.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,QAAQ,MAAM,gBAAgB,CAAC;AAC3C,OAAO,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,UAAU,EAAE,cAAc,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AACtH,OAAO,KAAK,EAAE,UAAU,EAA+C,MAAM,kBAAkB,CAAC;AAyBhG,OAAO,EAAE,iBAAiB,EAAE,MAAM,+BAA+B,CAAC;AA6DlE;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,OAAO,CACrB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,MAAM,GAAG,IAAI,EACtB,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,cAAc,GACpB,UAAU,CAqGZ;AAmBD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,QAAQ,CACtB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,MAAM,EACd,IAAI,EAAE;IACJ,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,OAAO,CAAC,EAAE,eAAe,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,GACA,UAAU,CA2JZ;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,MAAM,EACd,MAAM,EAAE,gBAAgB,EACxB,QAAQ,CAAC,EAAE,MAAM,GAChB,UAAU,CAkHZ;AAED;;GAEG;AACH,wBAAgB,UAAU,CACxB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,MAAM,EAAE,MAAM,EACd,QAAQ,CAAC,EAAE,MAAM,GAChB,IAAI,CA4BN;AAED;;GAEG;AACH,wBAAgB,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,UAAU,CAwB7F;AAED;;GAEG;AACH,wBAAgB,SAAS,CACvB,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,EAChB,OAAO,CAAC,EAAE;IACR,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,IAAI,EAAE,CAoBR;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,EAAE,EAAE,QAAQ,CAAC,QAAQ,EACrB,QAAQ,EAAE,MAAM,GACf,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAaxB;AAGD,OAAO,EAAE,iBAAiB,EAAE,CAAC"}
@@ -1,13 +1,12 @@
1
1
  "use strict";
2
2
  // Task service: orchestrates task/kanban lifecycle
3
- // Implements AD-0008: Centralized State Machines + Owner Validation
4
- // + AD-0011: Soft Delete for Task Removal → now permanent delete (FS-SS-005-0007 v3)
3
+ // Implements AD-0031 v2: Consolidated Stage/Status Workflow Engine
4
+ // + AD-0039 v2: Combined Move Validation
5
5
  // + AD-0014: Nullable Columns for State-Specific Fields
6
- // + AD-0031: Stage-Driven Workflow Engine
7
- // + FS-SS-005-0001..0012
8
- // + FS-SS-030-0001..0004
6
+ // + FS-SS-005-0001..0012 v2
7
+ // + FS-SS-030-0001..0004 v2
9
8
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.validateOwnerRole = exports.isValidRole = void 0;
9
+ exports.validateOwnerRole = void 0;
11
10
  exports.addTask = addTask;
12
11
  exports.moveTask = moveTask;
13
12
  exports.updateTask = updateTask;
@@ -19,12 +18,12 @@ const errors_1 = require("../utils/errors");
19
18
  const task_state_1 = require("../state-machines/task-state");
20
19
  const owner_validator_1 = require("../validators/owner-validator");
21
20
  Object.defineProperty(exports, "validateOwnerRole", { enumerable: true, get: function () { return owner_validator_1.validateOwnerRole; } });
22
- Object.defineProperty(exports, "isValidRole", { enumerable: true, get: function () { return owner_validator_1.isValidRole; } });
23
21
  const state_field_recorder_1 = require("../validators/state-field-recorder");
24
22
  const stage_validator_1 = require("./stage-validator");
25
23
  const sprint_1 = require("./sprint");
26
24
  const locks_1 = require("../db/locks");
27
25
  const retry_1 = require("../db/retry");
26
+ const levenshtein_1 = require("../utils/levenshtein");
28
27
  /** Valid task type values */
29
28
  const VALID_TASK_TYPES = ['Feature', 'Bug', 'Chore'];
30
29
  /** Valid task priority values */
@@ -71,15 +70,17 @@ function validatePriority(priority) {
71
70
  /**
72
71
  * Add a new task to a sprint.
73
72
  *
73
+ * v2: No captain-only owner validation. Any valid role can own a task in any stage.
74
+ *
74
75
  * @param db - Database connection
75
76
  * @param sprintId - Sprint ID
76
77
  * @param content - Task content (nullable; can be populated via --content-file)
77
- * @param owner - Owner role (must be valid, captain-only for Backlog)
78
+ * @param owner - Owner role (must be valid)
78
79
  * @param opts - Optional task properties
79
80
  * @returns The created task result
80
81
  * @throws TaskBadSprintError if sprint doesn't exist or is closed
81
82
  * @throws TaskOwnerRequiredError if owner is empty
82
- * @throws various owner validation errors
83
+ * @throws various validation errors
83
84
  */
84
85
  function addTask(db, sprintId, content, owner, opts) {
85
86
  // Validate sprint exists and is not closed
@@ -97,16 +98,17 @@ function addTask(db, sprintId, content, owner, opts) {
97
98
  if (sprintStatus === 'CLOSED') {
98
99
  throw new errors_1.TaskBadSprintError(sprintId, 'sprint is closed');
99
100
  }
100
- // Validate owner
101
+ // Validate owner (v2: case-insensitive, basic role-name only)
101
102
  if (!owner || owner.trim().length === 0) {
102
103
  throw new errors_1.TaskOwnerRequiredError();
103
104
  }
104
- // Default stage is Planning — captain-only for Planning stage (FS-SS-005-0010, FS-SS-030-0003)
105
- const stage = opts?.stage ?? 'Planning';
106
- if (!(0, stage_validator_1.isValidStage)(stage)) {
107
- throw new errors_1.StageInvalidError(stage);
105
+ const normalizedOwner = (0, owner_validator_1.validateOwnerRole)(owner.trim());
106
+ // Validate stage (v2: case-insensitive)
107
+ const stageInput = opts?.stage ?? 'Planning';
108
+ if (!(0, stage_validator_1.isValidStage)(stageInput)) {
109
+ throw new errors_1.StageInvalidError(stageInput);
108
110
  }
109
- (0, owner_validator_1.validateOwnerForStage)(owner.trim(), stage);
111
+ const stage = (0, stage_validator_1.validateStage)(stageInput);
110
112
  // Validate optional type
111
113
  const taskType = opts?.task_type ?? 'Feature';
112
114
  if (opts?.task_type) {
@@ -128,11 +130,10 @@ function addTask(db, sprintId, content, owner, opts) {
128
130
  const lockOwner = (0, locks_1.generateLockOwner)();
129
131
  return (0, locks_1.withLock)(db, { name: lockName, owner: lockOwner }, () => {
130
132
  return (0, retry_1.withTransaction)(db, () => {
131
- // Support explicit task ID (FS-SS-028-0001: positional <id> on task add)
133
+ // Support explicit task ID
132
134
  let taskId;
133
135
  if (opts?.task_id) {
134
136
  validateTaskIdFormat(opts.task_id);
135
- // Check for duplicate within the same sprint (per-sprint ID policy)
136
137
  const existing = db.prepare('SELECT id FROM tasks WHERE id = ? AND sprint_id = ?').get(opts.task_id, sprintId);
137
138
  if (existing) {
138
139
  throw new errors_1.TaskDuplicateIdError(opts.task_id);
@@ -146,7 +147,7 @@ function addTask(db, sprintId, content, owner, opts) {
146
147
  db.prepare(`
147
148
  INSERT INTO tasks (id, sprint_id, parent_id, content, task_type, priority, owner, points, status, stage, created_at, updated_at)
148
149
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'Backlog', ?, ?, ?)
149
- `).run(taskId, sprintId, opts?.parent_id ?? null, content?.trim() ?? null, taskType, priority, owner.trim(), opts?.points ?? 0, stage, now, now);
150
+ `).run(taskId, sprintId, opts?.parent_id ?? null, content?.trim() ?? null, taskType, priority, normalizedOwner, opts?.points ?? 0, stage, now, now);
150
151
  return {
151
152
  id: taskId,
152
153
  sprint_id: sprintId,
@@ -154,7 +155,7 @@ function addTask(db, sprintId, content, owner, opts) {
154
155
  content: content?.trim() ?? null,
155
156
  task_type: taskType,
156
157
  priority: priority,
157
- owner: owner.trim(),
158
+ owner: normalizedOwner,
158
159
  points: opts?.points ?? 0,
159
160
  status: 'Backlog',
160
161
  stage: stage,
@@ -176,67 +177,97 @@ function resolveActiveSprint(db) {
176
177
  return sprint;
177
178
  }
178
179
  /**
179
- * Move a task: status transition, stage transition, or combined.
180
+ * Move a task: status transition, stage transition, owner change, or combined.
180
181
  *
181
- * Per AD-0039: stage validation runs before status validation in combined moves.
182
- * Per FR-URD-034-0004: active sprint is required — no --sprint override.
182
+ * v2 per AD-0039 / AD-0031:
183
+ * - Case-insensitive validation for status, stage, owner
184
+ * - Closest-match suggestions via Levenshtein distance
185
+ * - Free stage transitions (forward, backward, same-stage all allowed)
186
+ * - Non-terminal Dropped (can recover to Backlog/To Do)
187
+ * - Dry-run mode: validate without persisting
188
+ * - Compulsory parameters: at least one of --to, --stage, --owner
189
+ * - No --sprint override; move always operates on active sprint
183
190
  *
184
191
  * @param db - Database connection
185
192
  * @param taskId - Task ID (4-digit NNNN)
186
- * @param opts - Move options: targetStatus, targetStage, context
193
+ * @param opts - Move options: targetStatus, targetStage, context, dryRun, sprintId
187
194
  * @returns Updated task result
188
195
  */
189
196
  function moveTask(db, taskId, opts) {
190
197
  // Validate task ID format
191
198
  validateTaskIdFormat(taskId);
192
- // Resolve sprint for task lookup (per-sprint ID policy)
199
+ // At least one of targetStatus, targetStage, or owner must be provided
200
+ const hasStatusTarget = opts.targetStatus !== undefined;
201
+ const hasStageTarget = opts.targetStage !== undefined;
202
+ const hasOwnerTarget = opts.context?.owner !== undefined;
203
+ if (!hasStatusTarget && !hasStageTarget && !hasOwnerTarget) {
204
+ throw new errors_1.TaskMoveNoTargetError();
205
+ }
206
+ const isDryRun = opts.dryRun === true;
207
+ // Resolve sprint for task lookup
193
208
  const sprintId = opts.sprintId || resolveActiveSprint(db).id;
194
209
  // Read task first (pre-lock, for validation)
195
- // First try with sprint_id filter (exact match)
196
210
  let task = db.prepare('SELECT * FROM tasks WHERE id = ? AND sprint_id = ? AND removed_at IS NULL').get(taskId, sprintId);
197
- // If not found in the resolved sprint, check if it exists in any sprint
198
- // to give a more specific error (TaskNotInActiveSprintError vs TaskNotFoundError)
211
+ // Cross-sprint fallback lookup
199
212
  if (!task) {
200
213
  const taskInAnySprint = db.prepare('SELECT * FROM tasks WHERE id = ? AND removed_at IS NULL').get(taskId);
201
214
  if (taskInAnySprint) {
202
- // Task exists but is in a different sprint → specific error
203
215
  throw new errors_1.TaskNotInActiveSprintError(taskId);
204
216
  }
205
- // Task truly doesn't exist → generic error
206
217
  throw new errors_1.TaskNotFoundError(taskId, sprintId);
207
218
  }
208
- // Active sprint enforcement: task must belong to the active sprint
219
+ // Active sprint enforcement
209
220
  const activeSprint = resolveActiveSprint(db);
210
221
  if (task.sprint_id !== activeSprint.id) {
211
222
  throw new errors_1.TaskNotInActiveSprintError(taskId);
212
223
  }
213
- const targetStatus = opts.targetStatus;
214
- const targetStage = opts.targetStage;
215
- // Phase 1: Validate stage progression (runs first per AD-0039)
216
- if (targetStage !== undefined) {
217
- if (!(0, stage_validator_1.isValidStage)(targetStage)) {
218
- throw new errors_1.StageInvalidError(targetStage);
219
- }
220
- (0, stage_validator_1.validateProgression)(task.stage, targetStage, task.status);
221
- // Validate owner for destination stage (captain-only for Planning/Release)
222
- (0, owner_validator_1.validateOwnerForStage)(task.owner, targetStage);
223
- }
224
- // Phase 2: Validate status transition (runs second per AD-0039)
225
- if (targetStatus !== undefined) {
226
- if (!(0, task_state_1.isValidTaskStatus)(targetStatus)) {
227
- throw new errors_1.TaskBadStatusError(targetStatus);
224
+ // Case-insensitive normalize and validate target status
225
+ let normalizedStatus;
226
+ if (hasStatusTarget) {
227
+ if (!(0, task_state_1.isValidTaskStatus)(opts.targetStatus)) {
228
+ const suggestions = (0, levenshtein_1.getClosestMatches)(opts.targetStatus, [
229
+ 'Backlog', 'To Do', 'In Progress', 'In Review', 'Done', 'Dropped',
230
+ ]);
231
+ throw new errors_1.TaskBadStatusError(opts.targetStatus, suggestions);
228
232
  }
229
- if (task.status === targetStatus) {
230
- throw new errors_1.TaskNoopError(taskId, task.status);
231
- }
232
- if (!(0, task_state_1.isValidTaskTransition)(task.status, targetStatus)) {
233
+ normalizedStatus = (0, task_state_1.normalizeTaskStatus)(opts.targetStatus);
234
+ // Validate transition
235
+ if (!(0, task_state_1.isValidTaskTransition)(task.status, normalizedStatus)) {
233
236
  const allowed = (0, task_state_1.getAllowedTaskTransitions)(task.status).map(s => s);
234
- throw new errors_1.TaskTransitionError(taskId, task.status, targetStatus, allowed);
237
+ throw new errors_1.TaskInvalidTransitionError(task.status, normalizedStatus, allowed);
235
238
  }
236
239
  }
237
- // At least one of targetStatus or targetStage must be provided
238
- if (targetStatus === undefined && targetStage === undefined) {
239
- throw new errors_1.TaskNoopError(taskId, task.status);
240
+ // Case-insensitive normalize and validate target stage
241
+ let normalizedStage;
242
+ if (hasStageTarget) {
243
+ normalizedStage = (0, stage_validator_1.validateStage)(opts.targetStage);
244
+ }
245
+ // Case-insensitive normalize and validate target owner
246
+ let normalizedOwner;
247
+ if (hasOwnerTarget) {
248
+ normalizedOwner = (0, owner_validator_1.validateOwnerRole)(opts.context.owner);
249
+ }
250
+ // Dry-run: validate everything but don't persist
251
+ if (isDryRun) {
252
+ const currentStatus = task.status;
253
+ const currentStage = task.stage;
254
+ const currentOwner = task.owner;
255
+ const targetStatusStr = normalizedStatus ?? currentStatus;
256
+ const targetStageStr = normalizedStage ?? currentStage;
257
+ const targetOwnerStr = normalizedOwner ?? currentOwner;
258
+ return {
259
+ id: taskId,
260
+ sprint_id: task.sprint_id,
261
+ parent_id: task.parent_id,
262
+ content: task.content,
263
+ task_type: task.task_type,
264
+ priority: task.priority,
265
+ owner: targetOwnerStr,
266
+ points: task.points,
267
+ status: targetStatusStr,
268
+ stage: targetStageStr,
269
+ created_at: task.created_at,
270
+ };
240
271
  }
241
272
  // Phase 3: Apply in atomic transaction
242
273
  const lockName = `kanban:${task.sprint_id}`;
@@ -252,26 +283,27 @@ function moveTask(db, taskId, opts) {
252
283
  const values = [];
253
284
  const now = new Date().toISOString();
254
285
  // Apply stage update if provided
255
- if (targetStage !== undefined) {
286
+ if (normalizedStage !== undefined) {
256
287
  columns.push('stage = ?');
257
- values.push(targetStage);
288
+ values.push(normalizedStage);
258
289
  }
259
290
  // Apply status update and record state-specific fields if provided
260
- if (targetStatus !== undefined) {
261
- // Record state-specific fields inline
262
- const stFields = (0, state_field_recorder_1.computeStateFields)(fresh.status, targetStatus, opts.context);
263
- // build column+value pairs: stFields already includes status + updated_at
291
+ if (normalizedStatus !== undefined) {
292
+ const stFields = (0, state_field_recorder_1.computeStateFields)(fresh.status, normalizedStatus, opts.context);
264
293
  for (let i = 0; i < stFields.columns.length; i++) {
265
- // Avoid duplicating updated_at
266
294
  const col = stFields.columns[i];
267
295
  if (col.startsWith('updated_at')) {
268
- // We'll add updated_at at the end
269
296
  continue;
270
297
  }
271
298
  columns.push(col);
272
299
  values.push(stFields.values[i]);
273
300
  }
274
301
  }
302
+ // Apply owner update if provided
303
+ if (normalizedOwner !== undefined) {
304
+ columns.push('owner = ?');
305
+ values.push(normalizedOwner);
306
+ }
275
307
  // Always update updated_at
276
308
  columns.push('updated_at = ?');
277
309
  values.push(now);
@@ -290,9 +322,6 @@ function moveTask(db, taskId, opts) {
290
322
  * Does NOT change task status, stage, owner, or sprint — use moveTask for those.
291
323
  *
292
324
  * Per AD-0038: rejects stage, owner, sprint fields at the service layer.
293
- *
294
- * @throws TaskNotFoundError if task doesn't exist
295
- * @throws TaskInvalidFieldError if prohibited fields are present
296
325
  */
297
326
  function updateTask(db, taskId, fields, sprintId) {
298
327
  // Guard clause: reject prohibited fields before any DB access (AD-0038)
@@ -302,9 +331,9 @@ function updateTask(db, taskId, fields, sprintId) {
302
331
  throw new errors_1.TaskInvalidFieldError('owner');
303
332
  if ('sprint' in fields)
304
333
  throw new errors_1.TaskInvalidFieldError('sprint');
305
- // Resolve sprint for task lookup (per-sprint ID policy)
334
+ // Resolve sprint for task lookup
306
335
  const resolvedSprintId = sprintId || resolveActiveSprint(db).id;
307
- // Read task first to resolve sprint ID for per-sprint locking (AD-0015)
336
+ // Read task first
308
337
  const task = db.prepare('SELECT * FROM tasks WHERE id = ? AND sprint_id = ? AND removed_at IS NULL').get(taskId, resolvedSprintId);
309
338
  if (!task) {
310
339
  throw new errors_1.TaskNotFoundError(taskId, resolvedSprintId);
@@ -313,14 +342,12 @@ function updateTask(db, taskId, fields, sprintId) {
313
342
  const lockOwner = (0, locks_1.generateLockOwner)();
314
343
  return (0, locks_1.withLock)(db, { name: lockName, owner: lockOwner }, () => {
315
344
  return (0, retry_1.withTransaction)(db, () => {
316
- // Re-read task inside transaction for consistency under lock
317
345
  const fresh = db.prepare('SELECT * FROM tasks WHERE id = ? AND sprint_id = ? AND removed_at IS NULL').get(taskId, resolvedSprintId);
318
346
  if (!fresh) {
319
347
  throw new errors_1.TaskNotFoundError(taskId, resolvedSprintId);
320
348
  }
321
349
  const updates = [];
322
350
  const params = [];
323
- // Handle content_file_content: store raw content from file in content column
324
351
  if (fields.content_file_content !== undefined) {
325
352
  updates.push('content = ?');
326
353
  params.push(fields.content_file_content);
@@ -383,33 +410,24 @@ function updateTask(db, taskId, fields, sprintId) {
383
410
  params.push(taskId);
384
411
  params.push(resolvedSprintId);
385
412
  db.prepare(`UPDATE tasks SET ${updates.join(', ')} WHERE id = ? AND sprint_id = ?`).run(...params);
386
- // Re-read
387
413
  const updated = db.prepare('SELECT id, sprint_id, parent_id, content, task_type, priority, owner, points, status, stage, created_at FROM tasks WHERE id = ? AND sprint_id = ?').get(taskId, resolvedSprintId);
388
414
  return updated;
389
415
  });
390
416
  });
391
417
  }
392
418
  /**
393
- * Remove a task (soft delete by default per AD-0011).
394
- *
395
- * @param db - Database connection
396
- * @param taskId - Task ID
397
- * @throws TaskNotFoundError if task doesn't exist
419
+ * Remove a task (permanent delete).
398
420
  */
399
421
  function removeTask(db, taskId, sprintId) {
400
- // Resolve sprint for task lookup (per-sprint ID policy)
401
422
  const resolvedSprintId = sprintId || resolveActiveSprint(db).id;
402
- // Read task first to resolve sprint ID for per-sprint locking (AD-0015)
403
423
  const task = db.prepare('SELECT id, sprint_id FROM tasks WHERE id = ? AND sprint_id = ?').get(taskId, resolvedSprintId);
404
424
  if (!task) {
405
425
  throw new errors_1.TaskNotFoundError(taskId, resolvedSprintId);
406
426
  }
407
- // FS-SS-005-0007 v3 / AD-0011: Always permanent delete
408
427
  const lockName = `kanban:${task.sprint_id}`;
409
428
  const owner = (0, locks_1.generateLockOwner)();
410
429
  (0, locks_1.withLock)(db, { name: lockName, owner }, () => {
411
430
  return (0, retry_1.withTransaction)(db, () => {
412
- // Re-read inside transaction for consistency under lock
413
431
  const fresh = db.prepare('SELECT id FROM tasks WHERE id = ? AND sprint_id = ?').get(taskId, resolvedSprintId);
414
432
  if (!fresh) {
415
433
  throw new errors_1.TaskNotFoundError(taskId, resolvedSprintId);
@@ -421,11 +439,6 @@ function removeTask(db, taskId, sprintId) {
421
439
  }
422
440
  /**
423
441
  * Show detailed task information including sprint name and parent content.
424
- *
425
- * @param db - Database connection
426
- * @param taskId - Task ID
427
- * @param sprintId - Sprint ID (required for sprint-scoped lookup per URD-026)
428
- * @throws TaskNotFoundError if task doesn't exist or is not in the specified sprint
429
442
  */
430
443
  function showTask(db, taskId, sprintId) {
431
444
  let sql = `SELECT t.*, s.name as sprint_name, p.content as parent_content
@@ -454,15 +467,12 @@ function showTask(db, taskId, sprintId) {
454
467
  function listTasks(db, sprintId, filters) {
455
468
  let sql = 'SELECT * FROM tasks WHERE removed_at IS NULL';
456
469
  const params = [];
457
- // Sprint filter
458
470
  sql += ' AND sprint_id = ?';
459
471
  params.push(sprintId);
460
- // Status filter
461
472
  if (filters?.status) {
462
473
  sql += ' AND status = ?';
463
474
  params.push(filters.status);
464
475
  }
465
- // Owner filter
466
476
  if (filters?.owner) {
467
477
  sql += ' AND owner = ?';
468
478
  params.push(filters.owner);