@lumenflow/core 2.2.2 → 2.3.2

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 (213) hide show
  1. package/dist/active-wu-detector.d.ts +1 -1
  2. package/dist/active-wu-detector.js +1 -1
  3. package/dist/arg-parser.js +51 -18
  4. package/dist/backlog-generator.d.ts +4 -4
  5. package/dist/backlog-generator.js +4 -4
  6. package/dist/backlog-sync-validator.js +1 -1
  7. package/dist/cleanup-lock.d.ts +9 -2
  8. package/dist/cleanup-lock.js +17 -7
  9. package/dist/code-path-validator.d.ts +3 -3
  10. package/dist/code-path-validator.js +3 -3
  11. package/dist/compliance-parser.d.ts +1 -1
  12. package/dist/compliance-parser.js +1 -1
  13. package/dist/constants/backlog-patterns.d.ts +1 -1
  14. package/dist/constants/backlog-patterns.js +1 -1
  15. package/dist/constants/dora-constants.d.ts +1 -1
  16. package/dist/constants/dora-constants.js +1 -1
  17. package/dist/constants/gate-constants.d.ts +1 -1
  18. package/dist/constants/gate-constants.js +1 -1
  19. package/dist/constants/linter-constants.d.ts +1 -1
  20. package/dist/constants/linter-constants.js +1 -1
  21. package/dist/constants/tokenizer-constants.d.ts +1 -1
  22. package/dist/constants/tokenizer-constants.js +1 -1
  23. package/dist/context/location-resolver.js +2 -1
  24. package/dist/context-validation-integration.d.ts +1 -0
  25. package/dist/core/scope-checker.d.ts +3 -3
  26. package/dist/core/scope-checker.js +3 -3
  27. package/dist/core/tool-runner.d.ts +5 -5
  28. package/dist/core/tool-runner.js +5 -5
  29. package/dist/core/tool.constants.d.ts +1 -1
  30. package/dist/core/tool.constants.js +1 -1
  31. package/dist/core/tool.schemas.d.ts +2 -2
  32. package/dist/core/tool.schemas.js +1 -1
  33. package/dist/core/worktree-guard.d.ts +1 -1
  34. package/dist/core/worktree-guard.js +1 -1
  35. package/dist/coverage-gate.d.ts +12 -3
  36. package/dist/coverage-gate.js +15 -8
  37. package/dist/date-utils.d.ts +4 -4
  38. package/dist/date-utils.js +4 -4
  39. package/dist/dependency-graph.d.ts +6 -0
  40. package/dist/dependency-graph.js +43 -2
  41. package/dist/dependency-guard.d.ts +2 -2
  42. package/dist/dependency-guard.js +3 -3
  43. package/dist/dependency-validator.d.ts +4 -4
  44. package/dist/dependency-validator.js +4 -7
  45. package/dist/domain/orchestration.constants.d.ts +31 -10
  46. package/dist/domain/orchestration.constants.js +45 -16
  47. package/dist/domain/orchestration.schemas.d.ts +54 -28
  48. package/dist/domain/orchestration.schemas.js +2 -2
  49. package/dist/domain/orchestration.types.d.ts +2 -2
  50. package/dist/domain/orchestration.types.js +2 -2
  51. package/dist/error-handler.d.ts +10 -10
  52. package/dist/error-handler.js +10 -10
  53. package/dist/file-classifiers.d.ts +6 -6
  54. package/dist/file-classifiers.js +6 -6
  55. package/dist/gates-config.d.ts +74 -0
  56. package/dist/gates-config.js +209 -2
  57. package/dist/git-adapter.d.ts +11 -11
  58. package/dist/git-adapter.js +11 -11
  59. package/dist/git-context-extractor.d.ts +112 -0
  60. package/dist/git-context-extractor.js +559 -0
  61. package/dist/hardcoded-strings.d.ts +1 -1
  62. package/dist/hardcoded-strings.js +1 -1
  63. package/dist/incremental-lint.d.ts +1 -1
  64. package/dist/incremental-lint.js +2 -2
  65. package/dist/incremental-test.d.ts +1 -1
  66. package/dist/incremental-test.js +1 -1
  67. package/dist/index.d.ts +13 -0
  68. package/dist/index.js +25 -0
  69. package/dist/invariants/check-automated-tests.d.ts +2 -2
  70. package/dist/invariants/check-automated-tests.js +3 -3
  71. package/dist/lane-checker.d.ts +28 -7
  72. package/dist/lane-checker.js +316 -159
  73. package/dist/lane-suggest-prompt.d.ts +108 -0
  74. package/dist/lane-suggest-prompt.js +359 -0
  75. package/dist/lane-validator.d.ts +3 -3
  76. package/dist/lane-validator.js +3 -3
  77. package/dist/logs-lib.d.ts +1 -1
  78. package/dist/logs-lib.js +1 -1
  79. package/dist/lumenflow-config-schema.d.ts +162 -0
  80. package/dist/lumenflow-config-schema.js +180 -0
  81. package/dist/manual-test-validator.d.ts +2 -2
  82. package/dist/manual-test-validator.js +3 -3
  83. package/dist/merge-lock.d.ts +8 -1
  84. package/dist/merge-lock.js +16 -7
  85. package/dist/micro-worktree.d.ts +81 -13
  86. package/dist/micro-worktree.js +98 -17
  87. package/dist/migration-deployer.d.ts +1 -1
  88. package/dist/migration-deployer.js +1 -1
  89. package/dist/orchestration-advisory-loader.d.ts +2 -2
  90. package/dist/orchestration-advisory-loader.js +10 -6
  91. package/dist/orchestration-advisory.d.ts +3 -3
  92. package/dist/orchestration-advisory.js +4 -4
  93. package/dist/orchestration-di.d.ts +4 -4
  94. package/dist/orchestration-di.js +4 -4
  95. package/dist/orchestration-rules.d.ts +4 -4
  96. package/dist/orchestration-rules.js +18 -10
  97. package/dist/orphan-detector.d.ts +3 -3
  98. package/dist/orphan-detector.js +3 -3
  99. package/dist/patrol-loop.d.ts +170 -0
  100. package/dist/patrol-loop.js +186 -0
  101. package/dist/process-detector.d.ts +5 -5
  102. package/dist/process-detector.js +5 -5
  103. package/dist/rebase-artifact-cleanup.d.ts +3 -3
  104. package/dist/rebase-artifact-cleanup.js +3 -3
  105. package/dist/resolve-policy.d.ts +195 -0
  106. package/dist/resolve-policy.js +203 -0
  107. package/dist/risk-detector.d.ts +2 -2
  108. package/dist/risk-detector.js +2 -2
  109. package/dist/rollback-utils.d.ts +1 -1
  110. package/dist/rollback-utils.js +1 -1
  111. package/dist/section-headings.d.ts +1 -1
  112. package/dist/section-headings.js +1 -1
  113. package/dist/spawn-escalation.d.ts +4 -4
  114. package/dist/spawn-escalation.js +3 -3
  115. package/dist/spawn-monitor.d.ts +4 -4
  116. package/dist/spawn-monitor.js +4 -4
  117. package/dist/spawn-recovery.d.ts +3 -3
  118. package/dist/spawn-recovery.js +3 -3
  119. package/dist/spawn-registry-schema.d.ts +2 -2
  120. package/dist/spawn-registry-schema.js +2 -2
  121. package/dist/spawn-registry-store.d.ts +2 -2
  122. package/dist/spawn-registry-store.js +2 -2
  123. package/dist/spawn-strategy.d.ts +17 -11
  124. package/dist/spawn-strategy.js +47 -44
  125. package/dist/spawn-tree.d.ts +3 -3
  126. package/dist/spawn-tree.js +3 -3
  127. package/dist/state-cleanup-core.d.ts +205 -0
  128. package/dist/state-cleanup-core.js +240 -0
  129. package/dist/state-doctor-core.d.ts +168 -0
  130. package/dist/state-doctor-core.js +251 -0
  131. package/dist/stream-error-handler.d.ts +67 -0
  132. package/dist/stream-error-handler.js +94 -0
  133. package/dist/telemetry.d.ts +1 -1
  134. package/dist/telemetry.js +1 -1
  135. package/dist/template-loader.d.ts +162 -0
  136. package/dist/template-loader.js +372 -0
  137. package/dist/test-baseline.d.ts +176 -0
  138. package/dist/test-baseline.js +282 -0
  139. package/dist/usecases/get-suggestions.usecase.d.ts +1 -1
  140. package/dist/validation/command-registry.js +37 -0
  141. package/dist/validators/backlog-sync.js +4 -2
  142. package/dist/worktree-scanner.d.ts +1 -1
  143. package/dist/worktree-scanner.js +1 -1
  144. package/dist/worktree-symlink.d.ts +3 -3
  145. package/dist/worktree-symlink.js +3 -3
  146. package/dist/wu-backlog-updater.d.ts +1 -1
  147. package/dist/wu-backlog-updater.js +1 -1
  148. package/dist/wu-claim-helpers.d.ts +1 -1
  149. package/dist/wu-claim-helpers.js +1 -1
  150. package/dist/wu-claim-resume.d.ts +1 -1
  151. package/dist/wu-claim-resume.js +1 -1
  152. package/dist/wu-consistency-checker.d.ts +1 -1
  153. package/dist/wu-consistency-checker.js +17 -11
  154. package/dist/wu-constants.d.ts +73 -21
  155. package/dist/wu-constants.js +65 -22
  156. package/dist/wu-done-branch-only.d.ts +1 -1
  157. package/dist/wu-done-branch-only.js +1 -1
  158. package/dist/wu-done-docs-generate.d.ts +1 -1
  159. package/dist/wu-done-docs-generate.js +1 -1
  160. package/dist/wu-done-messages.d.ts +2 -2
  161. package/dist/wu-done-messages.js +2 -2
  162. package/dist/wu-done-metadata.d.ts +3 -3
  163. package/dist/wu-done-metadata.js +3 -3
  164. package/dist/wu-done-pr.d.ts +1 -1
  165. package/dist/wu-done-pr.js +4 -2
  166. package/dist/wu-done-preflight.d.ts +8 -0
  167. package/dist/wu-done-preflight.js +18 -2
  168. package/dist/wu-done-ui.d.ts +3 -3
  169. package/dist/wu-done-ui.js +3 -3
  170. package/dist/wu-done-validation.d.ts +30 -0
  171. package/dist/wu-done-validation.js +106 -1
  172. package/dist/wu-done-worktree.d.ts +1 -1
  173. package/dist/wu-done-worktree.js +11 -1
  174. package/dist/wu-events-cleanup.d.ts +148 -0
  175. package/dist/wu-events-cleanup.js +401 -0
  176. package/dist/wu-helpers.d.ts +2 -2
  177. package/dist/wu-helpers.js +2 -2
  178. package/dist/wu-id-generator.d.ts +58 -0
  179. package/dist/wu-id-generator.js +103 -0
  180. package/dist/wu-lint.js +1 -1
  181. package/dist/wu-preflight-validators.d.ts +13 -1
  182. package/dist/wu-preflight-validators.js +56 -1
  183. package/dist/wu-recovery.d.ts +2 -2
  184. package/dist/wu-recovery.js +4 -4
  185. package/dist/wu-repair-core.d.ts +5 -5
  186. package/dist/wu-repair-core.js +6 -6
  187. package/dist/wu-schema-normalization.d.ts +1 -1
  188. package/dist/wu-schema-normalization.js +1 -1
  189. package/dist/wu-schema.d.ts +7 -7
  190. package/dist/wu-schema.js +8 -8
  191. package/dist/wu-spawn-context.d.ts +87 -0
  192. package/dist/wu-spawn-context.js +175 -0
  193. package/dist/wu-spawn-helpers.d.ts +1 -1
  194. package/dist/wu-spawn-helpers.js +1 -1
  195. package/dist/wu-spawn.d.ts +177 -4
  196. package/dist/wu-spawn.js +694 -72
  197. package/dist/wu-state-schema.d.ts +1 -1
  198. package/dist/wu-state-schema.js +1 -1
  199. package/dist/wu-state-store.d.ts +3 -3
  200. package/dist/wu-state-store.js +3 -3
  201. package/dist/wu-status-transition.d.ts +1 -1
  202. package/dist/wu-status-transition.js +1 -1
  203. package/dist/wu-status-updater.d.ts +3 -3
  204. package/dist/wu-status-updater.js +3 -3
  205. package/dist/wu-validation-constants.d.ts +2 -2
  206. package/dist/wu-validation-constants.js +2 -2
  207. package/dist/wu-validation.d.ts +3 -3
  208. package/dist/wu-validation.js +3 -3
  209. package/dist/wu-yaml-fixer.d.ts +2 -2
  210. package/dist/wu-yaml-fixer.js +3 -3
  211. package/dist/wu-yaml.d.ts +23 -0
  212. package/dist/wu-yaml.js +76 -2
  213. package/package.json +5 -2
@@ -0,0 +1,240 @@
1
+ /**
2
+ * State Cleanup Core (WU-1208)
3
+ *
4
+ * Unified orchestration of all state cleanup operations:
5
+ * - Signal cleanup (TTL-based, from @lumenflow/memory)
6
+ * - Memory cleanup (lifecycle-based, from @lumenflow/memory)
7
+ * - Event archival (age-based, from @lumenflow/core)
8
+ *
9
+ * Cleanup order: signals -> memory -> events (dependency order)
10
+ *
11
+ * Design principles:
12
+ * - Non-fatal errors: failures in one cleanup type don't block others
13
+ * - Consistent summary: aggregated counts for all cleanup types
14
+ * - Configurable: supports --dry-run and type-specific flags
15
+ *
16
+ * @see {@link packages/@lumenflow/cli/src/state-cleanup.ts} - CLI wrapper
17
+ * @see {@link packages/@lumenflow/core/src/__tests__/state-cleanup-core.test.ts} - Tests
18
+ */
19
+ /**
20
+ * All cleanup types in dependency order
21
+ */
22
+ const ALL_CLEANUP_TYPES = ['signals', 'memory', 'events'];
23
+ /**
24
+ * Determine which cleanup types to execute based on options
25
+ *
26
+ * @param options - State cleanup options
27
+ * @returns Array of cleanup types to execute
28
+ */
29
+ function getTypesToExecute(options) {
30
+ if (options.signalsOnly) {
31
+ return ['signals'];
32
+ }
33
+ if (options.memoryOnly) {
34
+ return ['memory'];
35
+ }
36
+ if (options.eventsOnly) {
37
+ return ['events'];
38
+ }
39
+ return ALL_CLEANUP_TYPES;
40
+ }
41
+ /**
42
+ * Execute signal cleanup and capture results or errors
43
+ *
44
+ * @param baseDir - Project base directory
45
+ * @param options - Cleanup options
46
+ * @returns Signal cleanup summary or undefined on error
47
+ */
48
+ async function executeSignalCleanup(baseDir, options) {
49
+ if (!options.cleanupSignals) {
50
+ return { error: { type: 'signals', message: 'No cleanupSignals function provided' } };
51
+ }
52
+ try {
53
+ const result = await options.cleanupSignals(baseDir, { dryRun: options.dryRun });
54
+ return {
55
+ summary: {
56
+ removedCount: result.removedIds.length,
57
+ retainedCount: result.retainedIds.length,
58
+ bytesFreed: result.bytesFreed,
59
+ breakdown: result.breakdown,
60
+ },
61
+ };
62
+ }
63
+ catch (err) {
64
+ const error = err;
65
+ return {
66
+ error: {
67
+ type: 'signals',
68
+ message: error.message,
69
+ error,
70
+ },
71
+ };
72
+ }
73
+ }
74
+ /**
75
+ * Execute memory cleanup and capture results or errors
76
+ *
77
+ * @param baseDir - Project base directory
78
+ * @param options - Cleanup options
79
+ * @returns Memory cleanup summary or undefined on error
80
+ */
81
+ async function executeMemoryCleanup(baseDir, options) {
82
+ if (!options.cleanupMemory) {
83
+ return { error: { type: 'memory', message: 'No cleanupMemory function provided' } };
84
+ }
85
+ try {
86
+ const result = await options.cleanupMemory(baseDir, { dryRun: options.dryRun });
87
+ return {
88
+ summary: {
89
+ removedCount: result.removedIds.length,
90
+ retainedCount: result.retainedIds.length,
91
+ bytesFreed: result.bytesFreed,
92
+ breakdown: result.breakdown,
93
+ },
94
+ };
95
+ }
96
+ catch (err) {
97
+ const error = err;
98
+ return {
99
+ error: {
100
+ type: 'memory',
101
+ message: error.message,
102
+ error,
103
+ },
104
+ };
105
+ }
106
+ }
107
+ /**
108
+ * Execute event archival and capture results or errors
109
+ *
110
+ * @param baseDir - Project base directory
111
+ * @param options - Cleanup options
112
+ * @returns Event archival summary or undefined on error
113
+ */
114
+ async function executeEventArchival(baseDir, options) {
115
+ if (!options.archiveEvents) {
116
+ return { error: { type: 'events', message: 'No archiveEvents function provided' } };
117
+ }
118
+ try {
119
+ const result = await options.archiveEvents(baseDir, { dryRun: options.dryRun });
120
+ return {
121
+ summary: {
122
+ archivedWuCount: result.archivedWuIds.length,
123
+ retainedWuCount: result.retainedWuIds.length,
124
+ archivedEventCount: result.archivedEventCount,
125
+ retainedEventCount: result.retainedEventCount,
126
+ bytesArchived: result.bytesArchived,
127
+ breakdown: result.breakdown,
128
+ },
129
+ };
130
+ }
131
+ catch (err) {
132
+ const error = err;
133
+ return {
134
+ error: {
135
+ type: 'events',
136
+ message: error.message,
137
+ error,
138
+ },
139
+ };
140
+ }
141
+ }
142
+ /**
143
+ * Calculate total bytes freed from all cleanup summaries
144
+ *
145
+ * @param signals - Signal cleanup summary
146
+ * @param memory - Memory cleanup summary
147
+ * @param events - Event archival summary
148
+ * @returns Total bytes freed
149
+ */
150
+ function calculateTotalBytesFreed(signals, memory, events) {
151
+ let total = 0;
152
+ if (signals) {
153
+ total += signals.bytesFreed;
154
+ }
155
+ if (memory) {
156
+ total += memory.bytesFreed;
157
+ }
158
+ if (events) {
159
+ total += events.bytesArchived;
160
+ }
161
+ return total;
162
+ }
163
+ /**
164
+ * Orchestrate all state cleanup operations in dependency order.
165
+ *
166
+ * Executes cleanups in order: signals -> memory -> events
167
+ *
168
+ * Non-fatal: failures in one cleanup type don't block others.
169
+ * All errors are collected and reported in the result.
170
+ *
171
+ * @param baseDir - Project base directory
172
+ * @param options - State cleanup options
173
+ * @returns Unified cleanup result with summaries and errors
174
+ *
175
+ * @example
176
+ * // Full cleanup with dry-run
177
+ * const result = await cleanupState(baseDir, { dryRun: true });
178
+ *
179
+ * @example
180
+ * // Signals only
181
+ * const result = await cleanupState(baseDir, { signalsOnly: true });
182
+ *
183
+ * @example
184
+ * // With injected cleanup functions (for testing or custom implementations)
185
+ * const result = await cleanupState(baseDir, {
186
+ * cleanupSignals: myCustomSignalCleanup,
187
+ * cleanupMemory: myCustomMemoryCleanup,
188
+ * archiveEvents: myCustomEventArchival,
189
+ * });
190
+ */
191
+ export async function cleanupState(baseDir, options = {}) {
192
+ const typesToExecute = getTypesToExecute(options);
193
+ const typesSkipped = ALL_CLEANUP_TYPES.filter((t) => !typesToExecute.includes(t));
194
+ const errors = [];
195
+ let signalsSummary;
196
+ let memorySummary;
197
+ let eventsSummary;
198
+ // Execute cleanups in dependency order: signals -> memory -> events
199
+ if (typesToExecute.includes('signals')) {
200
+ const result = await executeSignalCleanup(baseDir, options);
201
+ if (result.error) {
202
+ errors.push(result.error);
203
+ }
204
+ else {
205
+ signalsSummary = result.summary;
206
+ }
207
+ }
208
+ if (typesToExecute.includes('memory')) {
209
+ const result = await executeMemoryCleanup(baseDir, options);
210
+ if (result.error) {
211
+ errors.push(result.error);
212
+ }
213
+ else {
214
+ memorySummary = result.summary;
215
+ }
216
+ }
217
+ if (typesToExecute.includes('events')) {
218
+ const result = await executeEventArchival(baseDir, options);
219
+ if (result.error) {
220
+ errors.push(result.error);
221
+ }
222
+ else {
223
+ eventsSummary = result.summary;
224
+ }
225
+ }
226
+ const totalBytesFreed = calculateTotalBytesFreed(signalsSummary, memorySummary, eventsSummary);
227
+ return {
228
+ success: errors.length === 0,
229
+ dryRun: options.dryRun,
230
+ signals: signalsSummary,
231
+ memory: memorySummary,
232
+ events: eventsSummary,
233
+ errors,
234
+ summary: {
235
+ totalBytesFreed,
236
+ typesExecuted: typesToExecute,
237
+ typesSkipped,
238
+ },
239
+ };
240
+ }
@@ -0,0 +1,168 @@
1
+ /**
2
+ * State Doctor Core (WU-1209)
3
+ *
4
+ * Integrity checker for LumenFlow state that detects:
5
+ * - Orphaned WUs (done status but no stamp)
6
+ * - Dangling signals (reference non-existent WUs)
7
+ * - Broken memory relationships (events for missing WU specs)
8
+ *
9
+ * Inspired by Beads bd doctor command.
10
+ *
11
+ * Design principles:
12
+ * - Non-destructive by default (read-only diagnosis)
13
+ * - --fix flag for safe auto-repair of resolvable issues
14
+ * - Dependency injection for testability
15
+ * - Human-readable output with actionable suggestions
16
+ *
17
+ * @see {@link packages/@lumenflow/cli/src/state-doctor.ts} - CLI wrapper
18
+ * @see {@link packages/@lumenflow/core/src/__tests__/state-doctor-core.test.ts} - Tests
19
+ */
20
+ /**
21
+ * Issue type constants
22
+ */
23
+ export declare const ISSUE_TYPES: {
24
+ /** WU has done status but no stamp file */
25
+ readonly ORPHANED_WU: "orphaned_wu";
26
+ /** Signal references a WU that doesn't exist */
27
+ readonly DANGLING_SIGNAL: "dangling_signal";
28
+ /** Event references a WU that doesn't exist */
29
+ readonly BROKEN_EVENT: "broken_event";
30
+ };
31
+ /**
32
+ * Issue severity levels
33
+ */
34
+ export declare const ISSUE_SEVERITY: {
35
+ /** Critical issues that may cause data loss */
36
+ readonly ERROR: "error";
37
+ /** Issues that should be fixed but aren't blocking */
38
+ readonly WARNING: "warning";
39
+ /** Informational findings */
40
+ readonly INFO: "info";
41
+ };
42
+ /**
43
+ * Issue type (union)
44
+ */
45
+ export type IssueType = (typeof ISSUE_TYPES)[keyof typeof ISSUE_TYPES];
46
+ /**
47
+ * Issue severity (union)
48
+ */
49
+ export type IssueSeverity = (typeof ISSUE_SEVERITY)[keyof typeof ISSUE_SEVERITY];
50
+ /**
51
+ * Mock WU YAML content
52
+ */
53
+ export interface MockWU {
54
+ id: string;
55
+ status: string;
56
+ lane?: string;
57
+ title?: string;
58
+ }
59
+ /**
60
+ * Mock signal content
61
+ */
62
+ export interface MockSignal {
63
+ id: string;
64
+ wuId?: string;
65
+ timestamp?: string;
66
+ message?: string;
67
+ }
68
+ /**
69
+ * Mock event content
70
+ */
71
+ export interface MockEvent {
72
+ wuId: string;
73
+ type: string;
74
+ timestamp?: string;
75
+ }
76
+ /**
77
+ * Dependencies for state doctor (injectable for testing)
78
+ */
79
+ export interface StateDoctorDeps {
80
+ /** List all WU YAML files */
81
+ listWUs: () => Promise<MockWU[]>;
82
+ /** List all stamp file IDs */
83
+ listStamps: () => Promise<string[]>;
84
+ /** List all signals */
85
+ listSignals: () => Promise<MockSignal[]>;
86
+ /** List all events */
87
+ listEvents: () => Promise<MockEvent[]>;
88
+ /** Remove a signal by ID (for --fix) */
89
+ removeSignal?: (id: string) => Promise<void>;
90
+ /** Remove events for a WU (for --fix) */
91
+ removeEvent?: (wuId: string) => Promise<void>;
92
+ /** Create a stamp for a WU (for --fix) */
93
+ createStamp?: (wuId: string, title: string) => Promise<void>;
94
+ }
95
+ /**
96
+ * A detected issue in the state
97
+ */
98
+ export interface DiagnosisIssue {
99
+ /** Type of issue */
100
+ type: IssueType;
101
+ /** Severity level */
102
+ severity: IssueSeverity;
103
+ /** WU ID involved (if applicable) */
104
+ wuId?: string;
105
+ /** Signal ID involved (if applicable) */
106
+ signalId?: string;
107
+ /** Human-readable description */
108
+ description: string;
109
+ /** Suggested fix */
110
+ suggestion: string;
111
+ /** Whether this issue can be auto-fixed */
112
+ canAutoFix: boolean;
113
+ }
114
+ /**
115
+ * A fix error that occurred during auto-repair
116
+ */
117
+ export interface FixError {
118
+ /** Type of issue that failed to fix */
119
+ type: IssueType;
120
+ /** WU ID involved (if applicable) */
121
+ wuId?: string;
122
+ /** Signal ID involved (if applicable) */
123
+ signalId?: string;
124
+ /** Error message */
125
+ error: string;
126
+ }
127
+ /**
128
+ * Summary statistics
129
+ */
130
+ export interface DiagnosisSummary {
131
+ /** Number of orphaned WUs */
132
+ orphanedWUs: number;
133
+ /** Number of dangling signals */
134
+ danglingSignals: number;
135
+ /** Number of broken events */
136
+ brokenEvents: number;
137
+ /** Total number of issues */
138
+ totalIssues: number;
139
+ }
140
+ /**
141
+ * Options for diagnosis
142
+ */
143
+ export interface DiagnosisOptions {
144
+ /** Whether to attempt auto-fixes */
145
+ fix?: boolean;
146
+ /** Dry-run mode (report what would be fixed) */
147
+ dryRun?: boolean;
148
+ }
149
+ /**
150
+ * Result of state diagnosis
151
+ */
152
+ export interface StateDiagnosis {
153
+ /** Whether the state is healthy */
154
+ healthy: boolean;
155
+ /** List of detected issues */
156
+ issues: DiagnosisIssue[];
157
+ /** Summary statistics */
158
+ summary: DiagnosisSummary;
159
+ /** Issues that were fixed */
160
+ fixed: DiagnosisIssue[];
161
+ /** Errors that occurred during fixing */
162
+ fixErrors: FixError[];
163
+ /** Whether this was a dry-run */
164
+ dryRun?: boolean;
165
+ /** Issues that would be fixed (in dry-run mode) */
166
+ wouldFix?: DiagnosisIssue[];
167
+ }
168
+ export declare function diagnoseState(_baseDir: string, deps: StateDoctorDeps, options?: DiagnosisOptions): Promise<StateDiagnosis>;
@@ -0,0 +1,251 @@
1
+ /**
2
+ * State Doctor Core (WU-1209)
3
+ *
4
+ * Integrity checker for LumenFlow state that detects:
5
+ * - Orphaned WUs (done status but no stamp)
6
+ * - Dangling signals (reference non-existent WUs)
7
+ * - Broken memory relationships (events for missing WU specs)
8
+ *
9
+ * Inspired by Beads bd doctor command.
10
+ *
11
+ * Design principles:
12
+ * - Non-destructive by default (read-only diagnosis)
13
+ * - --fix flag for safe auto-repair of resolvable issues
14
+ * - Dependency injection for testability
15
+ * - Human-readable output with actionable suggestions
16
+ *
17
+ * @see {@link packages/@lumenflow/cli/src/state-doctor.ts} - CLI wrapper
18
+ * @see {@link packages/@lumenflow/core/src/__tests__/state-doctor-core.test.ts} - Tests
19
+ */
20
+ /**
21
+ * Issue type constants
22
+ */
23
+ export const ISSUE_TYPES = {
24
+ /** WU has done status but no stamp file */
25
+ ORPHANED_WU: 'orphaned_wu',
26
+ /** Signal references a WU that doesn't exist */
27
+ DANGLING_SIGNAL: 'dangling_signal',
28
+ /** Event references a WU that doesn't exist */
29
+ BROKEN_EVENT: 'broken_event',
30
+ };
31
+ /**
32
+ * Issue severity levels
33
+ */
34
+ export const ISSUE_SEVERITY = {
35
+ /** Critical issues that may cause data loss */
36
+ ERROR: 'error',
37
+ /** Issues that should be fixed but aren't blocking */
38
+ WARNING: 'warning',
39
+ /** Informational findings */
40
+ INFO: 'info',
41
+ };
42
+ /**
43
+ * Detect orphaned WUs (done status but no stamp)
44
+ */
45
+ function detectOrphanedWUs(wus, stamps) {
46
+ const issues = [];
47
+ for (const wu of wus) {
48
+ if (wu.status === 'done' && !stamps.has(wu.id)) {
49
+ issues.push({
50
+ type: ISSUE_TYPES.ORPHANED_WU,
51
+ severity: ISSUE_SEVERITY.WARNING,
52
+ wuId: wu.id,
53
+ description: `WU ${wu.id} has status 'done' but no stamp file exists`,
54
+ suggestion: `Create stamp file for ${wu.id} using: pnpm state:doctor --fix`,
55
+ canAutoFix: true,
56
+ });
57
+ }
58
+ }
59
+ return issues;
60
+ }
61
+ /**
62
+ * Detect dangling signals (reference non-existent WUs)
63
+ */
64
+ function detectDanglingSignals(signals, wuIds) {
65
+ const issues = [];
66
+ for (const signal of signals) {
67
+ // Skip signals without WU references
68
+ if (!signal.wuId) {
69
+ continue;
70
+ }
71
+ if (!wuIds.has(signal.wuId)) {
72
+ issues.push({
73
+ type: ISSUE_TYPES.DANGLING_SIGNAL,
74
+ severity: ISSUE_SEVERITY.WARNING,
75
+ wuId: signal.wuId,
76
+ signalId: signal.id,
77
+ description: `Signal ${signal.id} references non-existent WU ${signal.wuId}`,
78
+ suggestion: `Remove dangling signal using: pnpm state:doctor --fix`,
79
+ canAutoFix: true,
80
+ });
81
+ }
82
+ }
83
+ return issues;
84
+ }
85
+ /**
86
+ * Detect broken events (reference non-existent WUs)
87
+ */
88
+ function detectBrokenEvents(events, wuIds) {
89
+ const issues = [];
90
+ const seenWuIds = new Set();
91
+ for (const event of events) {
92
+ // Only report once per WU
93
+ if (seenWuIds.has(event.wuId)) {
94
+ continue;
95
+ }
96
+ if (!wuIds.has(event.wuId)) {
97
+ seenWuIds.add(event.wuId);
98
+ issues.push({
99
+ type: ISSUE_TYPES.BROKEN_EVENT,
100
+ severity: ISSUE_SEVERITY.WARNING,
101
+ wuId: event.wuId,
102
+ description: `Events exist for non-existent WU ${event.wuId}`,
103
+ suggestion: `Archive or remove events for missing WU using: pnpm state:doctor --fix`,
104
+ canAutoFix: true,
105
+ });
106
+ }
107
+ }
108
+ return issues;
109
+ }
110
+ /**
111
+ * Calculate summary statistics from issues
112
+ */
113
+ function calculateSummary(issues) {
114
+ let orphanedWUs = 0;
115
+ let danglingSignals = 0;
116
+ let brokenEvents = 0;
117
+ for (const issue of issues) {
118
+ switch (issue.type) {
119
+ case ISSUE_TYPES.ORPHANED_WU:
120
+ orphanedWUs++;
121
+ break;
122
+ case ISSUE_TYPES.DANGLING_SIGNAL:
123
+ danglingSignals++;
124
+ break;
125
+ case ISSUE_TYPES.BROKEN_EVENT:
126
+ brokenEvents++;
127
+ break;
128
+ }
129
+ }
130
+ return {
131
+ orphanedWUs,
132
+ danglingSignals,
133
+ brokenEvents,
134
+ totalIssues: issues.length,
135
+ };
136
+ }
137
+ /**
138
+ * Attempt to fix an issue
139
+ */
140
+ async function fixIssue(issue, deps, wus) {
141
+ try {
142
+ switch (issue.type) {
143
+ case ISSUE_TYPES.DANGLING_SIGNAL:
144
+ if (deps.removeSignal && issue.signalId) {
145
+ await deps.removeSignal(issue.signalId);
146
+ return { fixed: true };
147
+ }
148
+ return { fixed: false, error: 'No removeSignal function provided' };
149
+ case ISSUE_TYPES.BROKEN_EVENT:
150
+ if (deps.removeEvent && issue.wuId) {
151
+ await deps.removeEvent(issue.wuId);
152
+ return { fixed: true };
153
+ }
154
+ return { fixed: false, error: 'No removeEvent function provided' };
155
+ case ISSUE_TYPES.ORPHANED_WU:
156
+ if (deps.createStamp && issue.wuId) {
157
+ const wu = wus.find((w) => w.id === issue.wuId);
158
+ await deps.createStamp(issue.wuId, wu?.title || `WU ${issue.wuId}`);
159
+ return { fixed: true };
160
+ }
161
+ return { fixed: false, error: 'No createStamp function provided' };
162
+ default:
163
+ return { fixed: false, error: `Unknown issue type: ${issue.type}` };
164
+ }
165
+ }
166
+ catch (err) {
167
+ const error = err instanceof Error ? err.message : String(err);
168
+ return { fixed: false, error };
169
+ }
170
+ }
171
+ /**
172
+ * Diagnose state integrity issues.
173
+ *
174
+ * Detects:
175
+ * - Orphaned WUs (done status but no stamp)
176
+ * - Dangling signals (reference non-existent WUs)
177
+ * - Broken events (events for missing WU specs)
178
+ *
179
+ * @param baseDir - Project base directory
180
+ * @param deps - Dependency functions for reading/writing state
181
+ * @param options - Diagnosis options
182
+ * @returns Diagnosis result with issues and optional fixes
183
+ *
184
+ * @example
185
+ * // Diagnose without fixing
186
+ * const result = await diagnoseState(baseDir, deps);
187
+ *
188
+ * @example
189
+ * // Diagnose and fix
190
+ * const result = await diagnoseState(baseDir, deps, { fix: true });
191
+ *
192
+ * @example
193
+ * // Dry-run (report what would be fixed)
194
+ * const result = await diagnoseState(baseDir, deps, { fix: true, dryRun: true });
195
+ */
196
+ /**
197
+ * Apply fixes to auto-fixable issues
198
+ */
199
+ async function applyFixes(issues, deps, wus, result) {
200
+ const fixableIssues = issues.filter((i) => i.canAutoFix);
201
+ for (const issue of fixableIssues) {
202
+ const fixResult = await fixIssue(issue, deps, wus);
203
+ if (fixResult.fixed) {
204
+ result.fixed.push(issue);
205
+ }
206
+ else if (fixResult.error) {
207
+ result.fixErrors.push({
208
+ type: issue.type,
209
+ wuId: issue.wuId,
210
+ signalId: issue.signalId,
211
+ error: fixResult.error,
212
+ });
213
+ }
214
+ }
215
+ }
216
+ export async function diagnoseState(_baseDir, deps, options = {}) {
217
+ const { fix = false, dryRun = false } = options;
218
+ // Gather state data
219
+ const [wus, stamps, signals, events] = await Promise.all([
220
+ deps.listWUs(),
221
+ deps.listStamps(),
222
+ deps.listSignals(),
223
+ deps.listEvents(),
224
+ ]);
225
+ // Build lookup sets
226
+ const wuIds = new Set(wus.map((wu) => wu.id));
227
+ const stampIds = new Set(stamps);
228
+ // Detect issues
229
+ const orphanedWUissues = detectOrphanedWUs(wus, stampIds);
230
+ const danglingSignalIssues = detectDanglingSignals(signals, wuIds);
231
+ const brokenEventIssues = detectBrokenEvents(events, wuIds);
232
+ const issues = [...orphanedWUissues, ...danglingSignalIssues, ...brokenEventIssues];
233
+ const summary = calculateSummary(issues);
234
+ // Initialize result
235
+ const result = {
236
+ healthy: issues.length === 0,
237
+ issues,
238
+ summary,
239
+ fixed: [],
240
+ fixErrors: [],
241
+ };
242
+ // Handle fixing
243
+ if (fix && dryRun) {
244
+ result.dryRun = true;
245
+ result.wouldFix = issues.filter((i) => i.canAutoFix);
246
+ }
247
+ else if (fix) {
248
+ await applyFixes(issues, deps, wus, result);
249
+ }
250
+ return result;
251
+ }