@lumenflow/core 1.0.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.
Files changed (263) hide show
  1. package/LICENSE +190 -0
  2. package/README.md +119 -0
  3. package/dist/active-wu-detector.d.ts +33 -0
  4. package/dist/active-wu-detector.js +106 -0
  5. package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
  6. package/dist/adapters/filesystem-metrics.adapter.js +519 -0
  7. package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
  8. package/dist/adapters/terminal-renderer.adapter.js +337 -0
  9. package/dist/arg-parser.d.ts +63 -0
  10. package/dist/arg-parser.js +560 -0
  11. package/dist/backlog-editor.d.ts +98 -0
  12. package/dist/backlog-editor.js +179 -0
  13. package/dist/backlog-generator.d.ts +111 -0
  14. package/dist/backlog-generator.js +381 -0
  15. package/dist/backlog-parser.d.ts +45 -0
  16. package/dist/backlog-parser.js +102 -0
  17. package/dist/backlog-sync-validator.d.ts +78 -0
  18. package/dist/backlog-sync-validator.js +294 -0
  19. package/dist/branch-drift.d.ts +34 -0
  20. package/dist/branch-drift.js +51 -0
  21. package/dist/cleanup-install-config.d.ts +33 -0
  22. package/dist/cleanup-install-config.js +37 -0
  23. package/dist/cleanup-lock.d.ts +139 -0
  24. package/dist/cleanup-lock.js +313 -0
  25. package/dist/code-path-validator.d.ts +146 -0
  26. package/dist/code-path-validator.js +537 -0
  27. package/dist/code-paths-overlap.d.ts +55 -0
  28. package/dist/code-paths-overlap.js +245 -0
  29. package/dist/commands-logger.d.ts +77 -0
  30. package/dist/commands-logger.js +254 -0
  31. package/dist/commit-message-utils.d.ts +25 -0
  32. package/dist/commit-message-utils.js +41 -0
  33. package/dist/compliance-parser.d.ts +150 -0
  34. package/dist/compliance-parser.js +507 -0
  35. package/dist/constants/backlog-patterns.d.ts +20 -0
  36. package/dist/constants/backlog-patterns.js +23 -0
  37. package/dist/constants/dora-constants.d.ts +49 -0
  38. package/dist/constants/dora-constants.js +53 -0
  39. package/dist/constants/gate-constants.d.ts +15 -0
  40. package/dist/constants/gate-constants.js +15 -0
  41. package/dist/constants/linter-constants.d.ts +16 -0
  42. package/dist/constants/linter-constants.js +16 -0
  43. package/dist/constants/tokenizer-constants.d.ts +15 -0
  44. package/dist/constants/tokenizer-constants.js +15 -0
  45. package/dist/core/scope-checker.d.ts +97 -0
  46. package/dist/core/scope-checker.js +163 -0
  47. package/dist/core/tool-runner.d.ts +161 -0
  48. package/dist/core/tool-runner.js +393 -0
  49. package/dist/core/tool.constants.d.ts +105 -0
  50. package/dist/core/tool.constants.js +101 -0
  51. package/dist/core/tool.schemas.d.ts +226 -0
  52. package/dist/core/tool.schemas.js +226 -0
  53. package/dist/core/worktree-guard.d.ts +130 -0
  54. package/dist/core/worktree-guard.js +242 -0
  55. package/dist/coverage-gate.d.ts +108 -0
  56. package/dist/coverage-gate.js +196 -0
  57. package/dist/date-utils.d.ts +75 -0
  58. package/dist/date-utils.js +140 -0
  59. package/dist/dependency-graph.d.ts +142 -0
  60. package/dist/dependency-graph.js +550 -0
  61. package/dist/dependency-guard.d.ts +54 -0
  62. package/dist/dependency-guard.js +142 -0
  63. package/dist/dependency-validator.d.ts +105 -0
  64. package/dist/dependency-validator.js +154 -0
  65. package/dist/docs-path-validator.d.ts +36 -0
  66. package/dist/docs-path-validator.js +95 -0
  67. package/dist/domain/orchestration.constants.d.ts +99 -0
  68. package/dist/domain/orchestration.constants.js +97 -0
  69. package/dist/domain/orchestration.schemas.d.ts +280 -0
  70. package/dist/domain/orchestration.schemas.js +211 -0
  71. package/dist/domain/orchestration.types.d.ts +133 -0
  72. package/dist/domain/orchestration.types.js +12 -0
  73. package/dist/error-handler.d.ts +116 -0
  74. package/dist/error-handler.js +136 -0
  75. package/dist/file-classifiers.d.ts +62 -0
  76. package/dist/file-classifiers.js +108 -0
  77. package/dist/gates-agent-mode.d.ts +81 -0
  78. package/dist/gates-agent-mode.js +94 -0
  79. package/dist/generate-traceability.d.ts +107 -0
  80. package/dist/generate-traceability.js +411 -0
  81. package/dist/git-adapter.d.ts +395 -0
  82. package/dist/git-adapter.js +649 -0
  83. package/dist/git-staged-validator.d.ts +32 -0
  84. package/dist/git-staged-validator.js +48 -0
  85. package/dist/hardcoded-strings.d.ts +61 -0
  86. package/dist/hardcoded-strings.js +270 -0
  87. package/dist/incremental-lint.d.ts +78 -0
  88. package/dist/incremental-lint.js +129 -0
  89. package/dist/incremental-test.d.ts +39 -0
  90. package/dist/incremental-test.js +61 -0
  91. package/dist/index.d.ts +42 -0
  92. package/dist/index.js +61 -0
  93. package/dist/invariants/check-automated-tests.d.ts +50 -0
  94. package/dist/invariants/check-automated-tests.js +166 -0
  95. package/dist/invariants-runner.d.ts +103 -0
  96. package/dist/invariants-runner.js +527 -0
  97. package/dist/lane-checker.d.ts +50 -0
  98. package/dist/lane-checker.js +319 -0
  99. package/dist/lane-inference.d.ts +39 -0
  100. package/dist/lane-inference.js +195 -0
  101. package/dist/lane-lock.d.ts +211 -0
  102. package/dist/lane-lock.js +474 -0
  103. package/dist/lane-validator.d.ts +48 -0
  104. package/dist/lane-validator.js +114 -0
  105. package/dist/logs-lib.d.ts +104 -0
  106. package/dist/logs-lib.js +207 -0
  107. package/dist/lumenflow-config-schema.d.ts +272 -0
  108. package/dist/lumenflow-config-schema.js +207 -0
  109. package/dist/lumenflow-config.d.ts +95 -0
  110. package/dist/lumenflow-config.js +236 -0
  111. package/dist/manual-test-validator.d.ts +80 -0
  112. package/dist/manual-test-validator.js +200 -0
  113. package/dist/merge-lock.d.ts +115 -0
  114. package/dist/merge-lock.js +251 -0
  115. package/dist/micro-worktree.d.ts +159 -0
  116. package/dist/micro-worktree.js +427 -0
  117. package/dist/migration-deployer.d.ts +69 -0
  118. package/dist/migration-deployer.js +151 -0
  119. package/dist/orchestration-advisory-loader.d.ts +28 -0
  120. package/dist/orchestration-advisory-loader.js +87 -0
  121. package/dist/orchestration-advisory.d.ts +58 -0
  122. package/dist/orchestration-advisory.js +94 -0
  123. package/dist/orchestration-di.d.ts +48 -0
  124. package/dist/orchestration-di.js +57 -0
  125. package/dist/orchestration-rules.d.ts +57 -0
  126. package/dist/orchestration-rules.js +201 -0
  127. package/dist/orphan-detector.d.ts +131 -0
  128. package/dist/orphan-detector.js +226 -0
  129. package/dist/path-classifiers.d.ts +57 -0
  130. package/dist/path-classifiers.js +93 -0
  131. package/dist/piped-command-detector.d.ts +34 -0
  132. package/dist/piped-command-detector.js +64 -0
  133. package/dist/ports/dashboard-renderer.port.d.ts +112 -0
  134. package/dist/ports/dashboard-renderer.port.js +25 -0
  135. package/dist/ports/metrics-collector.port.d.ts +132 -0
  136. package/dist/ports/metrics-collector.port.js +26 -0
  137. package/dist/process-detector.d.ts +84 -0
  138. package/dist/process-detector.js +172 -0
  139. package/dist/prompt-linter.d.ts +72 -0
  140. package/dist/prompt-linter.js +312 -0
  141. package/dist/prompt-monitor.d.ts +15 -0
  142. package/dist/prompt-monitor.js +205 -0
  143. package/dist/rebase-artifact-cleanup.d.ts +145 -0
  144. package/dist/rebase-artifact-cleanup.js +433 -0
  145. package/dist/retry-strategy.d.ts +189 -0
  146. package/dist/retry-strategy.js +283 -0
  147. package/dist/risk-detector.d.ts +108 -0
  148. package/dist/risk-detector.js +252 -0
  149. package/dist/rollback-utils.d.ts +76 -0
  150. package/dist/rollback-utils.js +104 -0
  151. package/dist/section-headings.d.ts +43 -0
  152. package/dist/section-headings.js +49 -0
  153. package/dist/spawn-escalation.d.ts +90 -0
  154. package/dist/spawn-escalation.js +253 -0
  155. package/dist/spawn-monitor.d.ts +229 -0
  156. package/dist/spawn-monitor.js +672 -0
  157. package/dist/spawn-recovery.d.ts +82 -0
  158. package/dist/spawn-recovery.js +298 -0
  159. package/dist/spawn-registry-schema.d.ts +98 -0
  160. package/dist/spawn-registry-schema.js +108 -0
  161. package/dist/spawn-registry-store.d.ts +146 -0
  162. package/dist/spawn-registry-store.js +273 -0
  163. package/dist/spawn-tree.d.ts +121 -0
  164. package/dist/spawn-tree.js +285 -0
  165. package/dist/stamp-status-validator.d.ts +84 -0
  166. package/dist/stamp-status-validator.js +134 -0
  167. package/dist/stamp-utils.d.ts +100 -0
  168. package/dist/stamp-utils.js +229 -0
  169. package/dist/state-machine.d.ts +26 -0
  170. package/dist/state-machine.js +83 -0
  171. package/dist/system-map-validator.d.ts +80 -0
  172. package/dist/system-map-validator.js +272 -0
  173. package/dist/telemetry.d.ts +80 -0
  174. package/dist/telemetry.js +213 -0
  175. package/dist/token-counter.d.ts +51 -0
  176. package/dist/token-counter.js +145 -0
  177. package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
  178. package/dist/usecases/get-dashboard-data.usecase.js +61 -0
  179. package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
  180. package/dist/usecases/get-suggestions.usecase.js +153 -0
  181. package/dist/user-normalizer.d.ts +41 -0
  182. package/dist/user-normalizer.js +141 -0
  183. package/dist/validators/phi-constants.d.ts +97 -0
  184. package/dist/validators/phi-constants.js +152 -0
  185. package/dist/validators/phi-scanner.d.ts +58 -0
  186. package/dist/validators/phi-scanner.js +215 -0
  187. package/dist/worktree-ownership.d.ts +50 -0
  188. package/dist/worktree-ownership.js +74 -0
  189. package/dist/worktree-scanner.d.ts +103 -0
  190. package/dist/worktree-scanner.js +168 -0
  191. package/dist/worktree-symlink.d.ts +99 -0
  192. package/dist/worktree-symlink.js +359 -0
  193. package/dist/wu-backlog-updater.d.ts +17 -0
  194. package/dist/wu-backlog-updater.js +37 -0
  195. package/dist/wu-checkpoint.d.ts +124 -0
  196. package/dist/wu-checkpoint.js +233 -0
  197. package/dist/wu-claim-helpers.d.ts +26 -0
  198. package/dist/wu-claim-helpers.js +63 -0
  199. package/dist/wu-claim-resume.d.ts +106 -0
  200. package/dist/wu-claim-resume.js +276 -0
  201. package/dist/wu-consistency-checker.d.ts +95 -0
  202. package/dist/wu-consistency-checker.js +567 -0
  203. package/dist/wu-constants.d.ts +1275 -0
  204. package/dist/wu-constants.js +1382 -0
  205. package/dist/wu-create-validators.d.ts +42 -0
  206. package/dist/wu-create-validators.js +93 -0
  207. package/dist/wu-done-branch-only.d.ts +63 -0
  208. package/dist/wu-done-branch-only.js +191 -0
  209. package/dist/wu-done-messages.d.ts +119 -0
  210. package/dist/wu-done-messages.js +185 -0
  211. package/dist/wu-done-pr.d.ts +72 -0
  212. package/dist/wu-done-pr.js +174 -0
  213. package/dist/wu-done-retry-helpers.d.ts +85 -0
  214. package/dist/wu-done-retry-helpers.js +172 -0
  215. package/dist/wu-done-ui.d.ts +37 -0
  216. package/dist/wu-done-ui.js +69 -0
  217. package/dist/wu-done-validators.d.ts +411 -0
  218. package/dist/wu-done-validators.js +1229 -0
  219. package/dist/wu-done-worktree.d.ts +182 -0
  220. package/dist/wu-done-worktree.js +1097 -0
  221. package/dist/wu-helpers.d.ts +128 -0
  222. package/dist/wu-helpers.js +248 -0
  223. package/dist/wu-lint.d.ts +70 -0
  224. package/dist/wu-lint.js +234 -0
  225. package/dist/wu-paths.d.ts +171 -0
  226. package/dist/wu-paths.js +178 -0
  227. package/dist/wu-preflight-validators.d.ts +86 -0
  228. package/dist/wu-preflight-validators.js +251 -0
  229. package/dist/wu-recovery.d.ts +138 -0
  230. package/dist/wu-recovery.js +341 -0
  231. package/dist/wu-repair-core.d.ts +131 -0
  232. package/dist/wu-repair-core.js +669 -0
  233. package/dist/wu-schema-normalization.d.ts +17 -0
  234. package/dist/wu-schema-normalization.js +82 -0
  235. package/dist/wu-schema.d.ts +793 -0
  236. package/dist/wu-schema.js +881 -0
  237. package/dist/wu-spawn-helpers.d.ts +121 -0
  238. package/dist/wu-spawn-helpers.js +271 -0
  239. package/dist/wu-spawn.d.ts +158 -0
  240. package/dist/wu-spawn.js +1306 -0
  241. package/dist/wu-state-schema.d.ts +213 -0
  242. package/dist/wu-state-schema.js +156 -0
  243. package/dist/wu-state-store.d.ts +264 -0
  244. package/dist/wu-state-store.js +691 -0
  245. package/dist/wu-status-transition.d.ts +63 -0
  246. package/dist/wu-status-transition.js +382 -0
  247. package/dist/wu-status-updater.d.ts +25 -0
  248. package/dist/wu-status-updater.js +116 -0
  249. package/dist/wu-transaction-collectors.d.ts +116 -0
  250. package/dist/wu-transaction-collectors.js +272 -0
  251. package/dist/wu-transaction.d.ts +170 -0
  252. package/dist/wu-transaction.js +273 -0
  253. package/dist/wu-validation-constants.d.ts +60 -0
  254. package/dist/wu-validation-constants.js +66 -0
  255. package/dist/wu-validation.d.ts +118 -0
  256. package/dist/wu-validation.js +243 -0
  257. package/dist/wu-validator.d.ts +62 -0
  258. package/dist/wu-validator.js +325 -0
  259. package/dist/wu-yaml-fixer.d.ts +97 -0
  260. package/dist/wu-yaml-fixer.js +264 -0
  261. package/dist/wu-yaml.d.ts +86 -0
  262. package/dist/wu-yaml.js +222 -0
  263. package/package.json +114 -0
@@ -0,0 +1,179 @@
1
+ /* eslint-disable security/detect-non-literal-fs-filename, security/detect-object-injection */
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import matter from 'gray-matter';
4
+ import { createError, ErrorCodes } from './error-handler.js';
5
+ import { STRING_LITERALS } from './wu-constants.js';
6
+ /**
7
+ * Backlog/Status file editor module.
8
+ *
9
+ * Abstracts section movements and bullet manipulation to eliminate ~350 duplicate lines
10
+ * across wu-claim, wu-done, wu-block, wu-unblock, wu-cleanup, and wu-create.
11
+ *
12
+ * Core primitives:
13
+ * - readBacklogFile: Read file with frontmatter parsing
14
+ * - writeBacklogFile: Write file preserving frontmatter
15
+ * - findSectionBounds: Locate section start/end indices
16
+ * - removeBulletFromSection: Remove bullet from section
17
+ * - addBulletToSection: Add bullet to section
18
+ * - moveBullet: Atomic move operation (remove + add)
19
+ *
20
+ * @example
21
+ * import { moveBullet } from './lib/backlog-editor.js';
22
+ *
23
+ * moveBullet('docs/04-operations/tasks/backlog.md', {
24
+ * fromSection: '## Ready',
25
+ * toSection: '## In Progress',
26
+ * bulletPattern: 'WU-123',
27
+ * newBullet: '- [WU-123 — Title](link)',
28
+ * });
29
+ */
30
+ /**
31
+ * Read backlog/status file and separate frontmatter from content.
32
+ *
33
+ * @param {string} filePath - Path to file
34
+ * @returns {{ frontmatter: string, lines: string[] }} Frontmatter and content lines
35
+ */
36
+ export function readBacklogFile(filePath) {
37
+ if (!existsSync(filePath)) {
38
+ throw createError(ErrorCodes.FILE_NOT_FOUND, `File not found: ${filePath}`, { path: filePath });
39
+ }
40
+ const raw = readFileSync(filePath, { encoding: 'utf-8' });
41
+ // WU-1242: Use gray-matter for robust frontmatter extraction instead of regex
42
+ const parsed = matter(raw);
43
+ // Reconstruct frontmatter string for writeBacklogFile compatibility
44
+ const frontmatter = parsed.matter ? `---\n${parsed.matter}\n---\n` : '';
45
+ const content = parsed.content;
46
+ const lines = content.split(STRING_LITERALS.NEWLINE);
47
+ return { frontmatter, lines };
48
+ }
49
+ /**
50
+ * Write backlog/status file with frontmatter and content.
51
+ *
52
+ * @param {string} filePath - Path to file
53
+ * @param {string} frontmatter - Frontmatter text (including --- markers)
54
+ * @param {string[]} lines - Content lines
55
+ */
56
+ export function writeBacklogFile(filePath, frontmatter, lines) {
57
+ const content = frontmatter + lines.join(STRING_LITERALS.NEWLINE);
58
+ writeFileSync(filePath, content, { encoding: 'utf-8' });
59
+ }
60
+ /**
61
+ * Find section boundaries in lines array.
62
+ *
63
+ * Finds the section starting with the given heading and returns its start/end indices.
64
+ * Section ends at the next ## heading or end of file.
65
+ *
66
+ * @param {string[]} lines - Content lines
67
+ * @param {string} heading - Section heading (e.g., '## Ready')
68
+ * @returns {{ start: number, end: number } | null} Section bounds or null if not found
69
+ */
70
+ export function findSectionBounds(lines, heading) {
71
+ // Find section header (case-insensitive match)
72
+ const normalizedHeading = heading.trim().toLowerCase();
73
+ const startIdx = lines.findIndex((l) => l.trim().toLowerCase() === normalizedHeading);
74
+ if (startIdx === -1) {
75
+ return null; // Section not found
76
+ }
77
+ // Find next section header (## but not ###)
78
+ let endIdx = lines
79
+ .slice(startIdx + 1)
80
+ .findIndex((l) => l.startsWith('## ') && !l.startsWith('### '));
81
+ if (endIdx === -1) {
82
+ // No next section, use end of file
83
+ endIdx = lines.length;
84
+ }
85
+ else {
86
+ // Convert relative index to absolute
87
+ endIdx = startIdx + 1 + endIdx;
88
+ }
89
+ return { start: startIdx, end: endIdx };
90
+ }
91
+ /**
92
+ * Remove bullet matching pattern from section.
93
+ *
94
+ * Modifies lines array in-place, removing all bullets that contain the given pattern.
95
+ *
96
+ * @param {string[]} lines - Content lines (modified in-place)
97
+ * @param {number} sectionStart - Section start index
98
+ * @param {number} sectionEnd - Section end index
99
+ * @param {string} bulletPattern - Pattern to match (e.g., 'WU-123' or link path)
100
+ */
101
+ export function removeBulletFromSection(lines, sectionStart, sectionEnd, bulletPattern) {
102
+ for (let i = sectionStart + 1; i < sectionEnd; i++) {
103
+ if (lines[i] && lines[i].includes(bulletPattern)) {
104
+ lines.splice(i, 1);
105
+ sectionEnd--; // Adjust end index after removal
106
+ i--; // Re-check current index (next element shifted down)
107
+ }
108
+ }
109
+ }
110
+ /**
111
+ * Add bullet to section.
112
+ *
113
+ * Inserts bullet after section header, replacing "(No items...)" marker if present.
114
+ * Modifies lines array in-place.
115
+ *
116
+ * @param {string[]} lines - Content lines (modified in-place)
117
+ * @param {number} sectionStart - Section start index
118
+ * @param {string} bullet - Bullet text to add (e.g., '- [WU-123 — Title](link)')
119
+ */
120
+ export function addBulletToSection(lines, sectionStart, bullet) {
121
+ // Insert position: after header + empty line (typically sectionStart + 2)
122
+ // But handle case where there's a "(No items...)" marker
123
+ const nextLineIdx = sectionStart + 1;
124
+ const bulletInsertIdx = nextLineIdx + 1;
125
+ // WU-1242: Use string includes instead of regex for "(No items...)" marker check
126
+ const isNoItemsMarker = lines[bulletInsertIdx] &&
127
+ lines[bulletInsertIdx].toLowerCase().includes('no items currently in progress');
128
+ if (isNoItemsMarker) {
129
+ // Replace "(No items...)" with bullet
130
+ lines.splice(bulletInsertIdx, 1, bullet);
131
+ }
132
+ else {
133
+ // Insert bullet at bulletInsertIdx
134
+ lines.splice(bulletInsertIdx, 0, bullet);
135
+ }
136
+ }
137
+ /**
138
+ * Move bullet from one section to another (atomic operation).
139
+ *
140
+ * Reads file, removes bullet from source section, adds bullet to target section,
141
+ * writes file back. Preserves frontmatter.
142
+ *
143
+ * @param {string} filePath - Path to backlog/status file
144
+ * @param {object} options - Move options
145
+ * @param {string} options.fromSection - Source section heading (e.g., '## Ready')
146
+ * @param {string} options.toSection - Target section heading (e.g., '## In Progress')
147
+ * @param {string} options.bulletPattern - Pattern to match for removal (e.g., 'WU-123')
148
+ * @param {string} options.newBullet - Bullet text to add (e.g., '- [WU-123 — Title](link)')
149
+ * @throws {Error} If file not found or sections not found
150
+ */
151
+ export function moveBullet(filePath, { fromSection, toSection, bulletPattern, newBullet }) {
152
+ const { frontmatter, lines } = readBacklogFile(filePath);
153
+ // Find source and target sections
154
+ const fromBounds = findSectionBounds(lines, fromSection);
155
+ const toBounds = findSectionBounds(lines, toSection);
156
+ if (!fromBounds) {
157
+ throw createError(ErrorCodes.SECTION_NOT_FOUND, `Source section not found: ${fromSection}`, {
158
+ section: fromSection,
159
+ file: filePath,
160
+ });
161
+ }
162
+ if (!toBounds) {
163
+ throw createError(ErrorCodes.SECTION_NOT_FOUND, `Target section not found: ${toSection}`, {
164
+ section: toSection,
165
+ file: filePath,
166
+ });
167
+ }
168
+ // Remove bullet from source section
169
+ removeBulletFromSection(lines, fromBounds.start, fromBounds.end, bulletPattern);
170
+ // Recalculate target bounds after removal (indices may have shifted)
171
+ const updatedToBounds = findSectionBounds(lines, toSection);
172
+ if (!updatedToBounds) {
173
+ throw createError(ErrorCodes.SECTION_NOT_FOUND, `Target section not found after removal: ${toSection}`, { section: toSection, file: filePath, context: 'after removal' });
174
+ }
175
+ // Add bullet to target section
176
+ addBulletToSection(lines, updatedToBounds.start, newBullet);
177
+ // Write file back
178
+ writeBacklogFile(filePath, frontmatter, lines);
179
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Backlog Generator (WU-1573, WU-2244)
3
+ *
4
+ * Generates backlog.md and status.md from WUStateStore (read-only).
5
+ * Never parses markdown back to state - single source of truth is wu-events.jsonl.
6
+ *
7
+ * Performance target: <100ms for full backlog generation.
8
+ *
9
+ * WU-2244 additions:
10
+ * - validateBacklogConsistency(): Validates generated backlog against store state
11
+ * - computeStoreChecksum(): Computes deterministic checksum of store state
12
+ * - getCompletionDate(): Retrieves completion date from event timestamp
13
+ *
14
+ * @see {@link tools/__tests__/backlog-generator.test.mjs} - Tests
15
+ * @see {@link tools/__tests__/backlog-checksum.test.mjs} - Checksum tests
16
+ * @see {@link tools/__tests__/status-date-from-event.test.mjs} - Date tests
17
+ * @see {@link tools/lib/wu-state-store.mjs} - State store
18
+ */
19
+ /**
20
+ * Generates backlog.md markdown from WUStateStore
21
+ *
22
+ * Format matches current backlog.md exactly:
23
+ * - YAML frontmatter with section headings
24
+ * - Section headings with emojis
25
+ * - Bullet format: - [WU-ID — Title](wu/WU-ID.yaml) — Lane
26
+ * - Placeholder text for empty sections
27
+ *
28
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store to read from
29
+ * @returns {Promise<string>} Markdown content for backlog.md
30
+ *
31
+ * @example
32
+ * const store = new WUStateStore('/path/to/state');
33
+ * await store.load();
34
+ * const markdown = await generateBacklog(store);
35
+ * await fs.writeFile('backlog.md', markdown, 'utf-8');
36
+ */
37
+ export declare function generateBacklog(store: any): Promise<string>;
38
+ /**
39
+ * Generates status.md markdown from WUStateStore
40
+ *
41
+ * Format matches current status.md exactly:
42
+ * - Header with last updated timestamp
43
+ * - In Progress section
44
+ * - Completed section with dates
45
+ * - Placeholder for empty sections
46
+ *
47
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store to read from
48
+ * @returns {Promise<string>} Markdown content for status.md
49
+ *
50
+ * @example
51
+ * const store = new WUStateStore('/path/to/state');
52
+ * await store.load();
53
+ * const markdown = await generateStatus(store);
54
+ * await fs.writeFile('status.md', markdown, 'utf-8');
55
+ */
56
+ export declare function generateStatus(store: any): Promise<string>;
57
+ /**
58
+ * WU-2244: Get completion date for a WU from state store
59
+ *
60
+ * Returns the completion date from the complete event timestamp.
61
+ * Falls back to current date if completedAt is not available (legacy data).
62
+ *
63
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store
64
+ * @param {string} wuId - WU ID to get completion date for
65
+ * @returns {string} Completion date in YYYY-MM-DD format
66
+ *
67
+ * @example
68
+ * const date = getCompletionDate(store, 'WU-100');
69
+ * // Returns '2025-01-15' if completedAt is set, or today's date otherwise
70
+ */
71
+ export declare function getCompletionDate(store: any, wuId: any): any;
72
+ /**
73
+ * WU-2244: Compute deterministic checksum of store state
74
+ *
75
+ * Creates a hash of the current store state that can be used to detect
76
+ * inconsistencies between the store and generated backlog.
77
+ *
78
+ * The checksum is based on:
79
+ * - All WU IDs
80
+ * - Their statuses
81
+ * - Their titles
82
+ * - Their lanes
83
+ *
84
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store
85
+ * @returns {string} SHA-256 checksum of store state
86
+ *
87
+ * @example
88
+ * const checksum = computeStoreChecksum(store);
89
+ * // Returns '3f4d5a6b...' (64 char hex string)
90
+ */
91
+ export declare function computeStoreChecksum(store: any): string;
92
+ /**
93
+ * WU-2244: Validate backlog consistency against store state
94
+ *
95
+ * Checks that a generated backlog markdown contains all WUs from the store
96
+ * in the correct sections, with no duplicates or missing entries.
97
+ *
98
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store
99
+ * @param {string} markdown - Generated backlog markdown
100
+ * @returns {Promise<{valid: boolean, errors: string[]}>} Validation result
101
+ *
102
+ * @example
103
+ * const result = await validateBacklogConsistency(store, backlogMarkdown);
104
+ * if (!result.valid) {
105
+ * console.error('Backlog inconsistencies:', result.errors);
106
+ * }
107
+ */
108
+ export declare function validateBacklogConsistency(store: any, markdown: any): Promise<{
109
+ valid: boolean;
110
+ errors: any[];
111
+ }>;
@@ -0,0 +1,381 @@
1
+ /**
2
+ * Backlog Generator (WU-1573, WU-2244)
3
+ *
4
+ * Generates backlog.md and status.md from WUStateStore (read-only).
5
+ * Never parses markdown back to state - single source of truth is wu-events.jsonl.
6
+ *
7
+ * Performance target: <100ms for full backlog generation.
8
+ *
9
+ * WU-2244 additions:
10
+ * - validateBacklogConsistency(): Validates generated backlog against store state
11
+ * - computeStoreChecksum(): Computes deterministic checksum of store state
12
+ * - getCompletionDate(): Retrieves completion date from event timestamp
13
+ *
14
+ * @see {@link tools/__tests__/backlog-generator.test.mjs} - Tests
15
+ * @see {@link tools/__tests__/backlog-checksum.test.mjs} - Checksum tests
16
+ * @see {@link tools/__tests__/status-date-from-event.test.mjs} - Date tests
17
+ * @see {@link tools/lib/wu-state-store.mjs} - State store
18
+ */
19
+ import { createHash } from 'node:crypto';
20
+ /**
21
+ * Generates backlog.md markdown from WUStateStore
22
+ *
23
+ * Format matches current backlog.md exactly:
24
+ * - YAML frontmatter with section headings
25
+ * - Section headings with emojis
26
+ * - Bullet format: - [WU-ID — Title](wu/WU-ID.yaml) — Lane
27
+ * - Placeholder text for empty sections
28
+ *
29
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store to read from
30
+ * @returns {Promise<string>} Markdown content for backlog.md
31
+ *
32
+ * @example
33
+ * const store = new WUStateStore('/path/to/state');
34
+ * await store.load();
35
+ * const markdown = await generateBacklog(store);
36
+ * await fs.writeFile('backlog.md', markdown, 'utf-8');
37
+ */
38
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
39
+ export async function generateBacklog(store) {
40
+ // Start with frontmatter
41
+ const frontmatter = `---
42
+ sections:
43
+ ready:
44
+ heading: '## 🚀 Ready (pull from here)'
45
+ insertion: after_heading_blank_line
46
+ in_progress:
47
+ heading: '## 🔧 In progress'
48
+ insertion: after_heading_blank_line
49
+ blocked:
50
+ heading: '## â›” Blocked'
51
+ insertion: after_heading_blank_line
52
+ done:
53
+ heading: '## ✅ Done'
54
+ insertion: after_heading_blank_line
55
+ ---
56
+
57
+ > Agent: Read **ai/onboarding/starting-prompt.md** first, then follow **docs/04-operations/\\_frameworks/lumenflow/lumenflow-complete.md** for execution.
58
+
59
+ # Backlog (single source of truth)
60
+
61
+ `;
62
+ // Generate sections
63
+ const sections = [];
64
+ // Ready section (WUs with status: ready)
65
+ sections.push('## 🚀 Ready (pull from here)');
66
+ sections.push('');
67
+ const ready = store.getByStatus('ready');
68
+ if (ready.size === 0) {
69
+ sections.push('(No items ready)');
70
+ }
71
+ else {
72
+ for (const wuId of ready) {
73
+ const state = store.wuState.get(wuId);
74
+ if (state) {
75
+ sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml) — ${state.lane}`);
76
+ }
77
+ }
78
+ }
79
+ // In Progress section
80
+ sections.push('');
81
+ sections.push('## 🔧 In progress');
82
+ sections.push('');
83
+ const inProgress = store.getByStatus('in_progress');
84
+ if (inProgress.size === 0) {
85
+ sections.push('(No items currently in progress)');
86
+ }
87
+ else {
88
+ for (const wuId of inProgress) {
89
+ const state = store.wuState.get(wuId);
90
+ if (state) {
91
+ sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml) — ${state.lane}`);
92
+ }
93
+ }
94
+ }
95
+ // Blocked section
96
+ sections.push('');
97
+ sections.push('## â›” Blocked');
98
+ sections.push('');
99
+ const blocked = store.getByStatus('blocked');
100
+ if (blocked.size === 0) {
101
+ sections.push('(No items currently blocked)');
102
+ }
103
+ else {
104
+ for (const wuId of blocked) {
105
+ const state = store.wuState.get(wuId);
106
+ if (state) {
107
+ sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml) — ${state.lane}`);
108
+ }
109
+ }
110
+ }
111
+ // Done section
112
+ sections.push('');
113
+ sections.push('## ✅ Done');
114
+ sections.push('');
115
+ const done = store.getByStatus('done');
116
+ if (done.size === 0) {
117
+ sections.push('(No completed items)');
118
+ }
119
+ else {
120
+ for (const wuId of done) {
121
+ const state = store.wuState.get(wuId);
122
+ if (state) {
123
+ sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml)`);
124
+ }
125
+ }
126
+ }
127
+ return frontmatter + sections.join('\n');
128
+ }
129
+ /**
130
+ * Generates status.md markdown from WUStateStore
131
+ *
132
+ * Format matches current status.md exactly:
133
+ * - Header with last updated timestamp
134
+ * - In Progress section
135
+ * - Completed section with dates
136
+ * - Placeholder for empty sections
137
+ *
138
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store to read from
139
+ * @returns {Promise<string>} Markdown content for status.md
140
+ *
141
+ * @example
142
+ * const store = new WUStateStore('/path/to/state');
143
+ * await store.load();
144
+ * const markdown = await generateStatus(store);
145
+ * await fs.writeFile('status.md', markdown, 'utf-8');
146
+ */
147
+ // eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing complexity, refactor tracked separately
148
+ export async function generateStatus(store) {
149
+ const today = new Date().toISOString().split('T')[0]; // YYYY-MM-DD
150
+ // Header
151
+ const header = `# Work Unit Status
152
+
153
+ _Last updated: ${today}_
154
+ `;
155
+ const sections = [];
156
+ // In Progress section
157
+ sections.push('');
158
+ sections.push('## In Progress');
159
+ sections.push('');
160
+ const inProgress = store.getByStatus('in_progress');
161
+ if (inProgress.size === 0) {
162
+ sections.push('(No items currently in progress)');
163
+ }
164
+ else {
165
+ for (const wuId of inProgress) {
166
+ const state = store.wuState.get(wuId);
167
+ if (state) {
168
+ sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml)`);
169
+ }
170
+ }
171
+ }
172
+ // Blocked section (only show if has WUs)
173
+ const blocked = store.getByStatus('blocked');
174
+ if (blocked.size > 0) {
175
+ sections.push('');
176
+ sections.push('## Blocked');
177
+ sections.push('');
178
+ for (const wuId of blocked) {
179
+ const state = store.wuState.get(wuId);
180
+ if (state) {
181
+ sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml)`);
182
+ }
183
+ }
184
+ }
185
+ // Completed section
186
+ sections.push('');
187
+ sections.push('## Completed');
188
+ sections.push('');
189
+ const done = store.getByStatus('done');
190
+ if (done.size === 0) {
191
+ sections.push('(No completed items)');
192
+ }
193
+ else {
194
+ for (const wuId of done) {
195
+ const state = store.wuState.get(wuId);
196
+ if (state) {
197
+ // WU-2244: Use completedAt from event, fall back to today if not available
198
+ const completionDate = getCompletionDate(store, wuId);
199
+ sections.push(`- [${wuId} — ${state.title}](wu/${wuId}.yaml) — ${completionDate}`);
200
+ }
201
+ }
202
+ }
203
+ return header + sections.join('\n');
204
+ }
205
+ /**
206
+ * WU-2244: Get completion date for a WU from state store
207
+ *
208
+ * Returns the completion date from the complete event timestamp.
209
+ * Falls back to current date if completedAt is not available (legacy data).
210
+ *
211
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store
212
+ * @param {string} wuId - WU ID to get completion date for
213
+ * @returns {string} Completion date in YYYY-MM-DD format
214
+ *
215
+ * @example
216
+ * const date = getCompletionDate(store, 'WU-100');
217
+ * // Returns '2025-01-15' if completedAt is set, or today's date otherwise
218
+ */
219
+ export function getCompletionDate(store, wuId) {
220
+ const state = store.wuState.get(wuId);
221
+ if (state && state.completedAt) {
222
+ // Extract date portion from ISO timestamp
223
+ return state.completedAt.split('T')[0];
224
+ }
225
+ // Fallback to current date for legacy data
226
+ return new Date().toISOString().split('T')[0];
227
+ }
228
+ /**
229
+ * WU-2244: Compute deterministic checksum of store state
230
+ *
231
+ * Creates a hash of the current store state that can be used to detect
232
+ * inconsistencies between the store and generated backlog.
233
+ *
234
+ * The checksum is based on:
235
+ * - All WU IDs
236
+ * - Their statuses
237
+ * - Their titles
238
+ * - Their lanes
239
+ *
240
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store
241
+ * @returns {string} SHA-256 checksum of store state
242
+ *
243
+ * @example
244
+ * const checksum = computeStoreChecksum(store);
245
+ * // Returns '3f4d5a6b...' (64 char hex string)
246
+ */
247
+ export function computeStoreChecksum(store) {
248
+ // Build deterministic state representation
249
+ const stateEntries = [];
250
+ for (const [wuId, state] of store.wuState.entries()) {
251
+ stateEntries.push({
252
+ wuId,
253
+ status: state.status,
254
+ title: state.title,
255
+ lane: state.lane,
256
+ });
257
+ }
258
+ // Sort by wuId for deterministic ordering
259
+ stateEntries.sort((a, b) => a.wuId.localeCompare(b.wuId));
260
+ // Create hash
261
+ const hash = createHash('sha256');
262
+ hash.update(JSON.stringify(stateEntries));
263
+ return hash.digest('hex');
264
+ }
265
+ /** @type {Record<string, string>} Section heading to status mapping */
266
+ const SECTION_STATUS_MAP = {
267
+ '## 🚀 Ready (pull from here)': 'ready',
268
+ '## 🔧 In progress': 'in_progress',
269
+ '## â›” Blocked': 'blocked',
270
+ '## ✅ Done': 'done',
271
+ };
272
+ /**
273
+ * Strip YAML frontmatter from markdown content
274
+ * @param {string} markdown - Markdown with potential frontmatter
275
+ * @returns {string} Content without frontmatter
276
+ */
277
+ function stripFrontmatter(markdown) {
278
+ if (!markdown.startsWith('---')) {
279
+ return markdown;
280
+ }
281
+ const secondMarker = markdown.indexOf('---', 3);
282
+ return secondMarker !== -1 ? markdown.slice(secondMarker + 3) : markdown;
283
+ }
284
+ /**
285
+ * Count WU ID occurrences in content
286
+ * @param {string} content - Markdown content
287
+ * @returns {Map<string, number>} WU ID to count mapping
288
+ */
289
+ function countWUReferences(content) {
290
+ const foundWUs = new Map();
291
+ const matches = content.matchAll(/WU-\d+/g);
292
+ for (const match of matches) {
293
+ const wuId = match[0];
294
+ foundWUs.set(wuId, (foundWUs.get(wuId) || 0) + 1);
295
+ }
296
+ return foundWUs;
297
+ }
298
+ /**
299
+ * Parse markdown into sections with their WU IDs
300
+ * @param {string} content - Markdown content
301
+ * @returns {Map<string, string[]>} Status to WU IDs mapping
302
+ */
303
+ function parseMarkdownSections(content) {
304
+ const sections = new Map();
305
+ let currentSection = null;
306
+ for (const line of content.split('\n')) {
307
+ // Check for section headings
308
+ for (const [heading, status] of Object.entries(SECTION_STATUS_MAP)) {
309
+ if (line.includes(heading)) {
310
+ currentSection = status;
311
+ sections.set(status, []);
312
+ break;
313
+ }
314
+ }
315
+ // Extract WU IDs from lines in current section
316
+ if (currentSection && line.includes('[WU-')) {
317
+ const wuMatch = line.match(/WU-\d+/);
318
+ if (wuMatch) {
319
+ sections.get(currentSection).push(wuMatch[0]);
320
+ }
321
+ }
322
+ }
323
+ return sections;
324
+ }
325
+ /**
326
+ * Find which section contains a WU ID
327
+ * @param {Map<string, string[]>} sections - Parsed sections
328
+ * @param {string} wuId - WU ID to find
329
+ * @returns {string|null} Section status or null if not found
330
+ */
331
+ function findWUSection(sections, wuId) {
332
+ for (const [section, wus] of sections.entries()) {
333
+ if (wus.includes(wuId)) {
334
+ return section;
335
+ }
336
+ }
337
+ return null;
338
+ }
339
+ /**
340
+ * WU-2244: Validate backlog consistency against store state
341
+ *
342
+ * Checks that a generated backlog markdown contains all WUs from the store
343
+ * in the correct sections, with no duplicates or missing entries.
344
+ *
345
+ * @param {import('./wu-state-store.js').WUStateStore} store - State store
346
+ * @param {string} markdown - Generated backlog markdown
347
+ * @returns {Promise<{valid: boolean, errors: string[]}>} Validation result
348
+ *
349
+ * @example
350
+ * const result = await validateBacklogConsistency(store, backlogMarkdown);
351
+ * if (!result.valid) {
352
+ * console.error('Backlog inconsistencies:', result.errors);
353
+ * }
354
+ */
355
+ export async function validateBacklogConsistency(store, markdown) {
356
+ const errors = [];
357
+ const content = stripFrontmatter(markdown);
358
+ const foundWUs = countWUReferences(content);
359
+ // Check for duplicates (each WU appears twice: link text + URL)
360
+ for (const [wuId, count] of foundWUs) {
361
+ if (count > 2) {
362
+ errors.push(`${wuId} appears ${count / 2} times (duplicate entry)`);
363
+ }
364
+ }
365
+ const sections = parseMarkdownSections(content);
366
+ // Check each WU in store is in correct section
367
+ for (const [wuId, state] of store.wuState.entries()) {
368
+ const expectedSection = state.status;
369
+ const sectionWUs = sections.get(expectedSection) || [];
370
+ if (!sectionWUs.includes(wuId)) {
371
+ const foundInSection = findWUSection(sections, wuId);
372
+ if (foundInSection) {
373
+ errors.push(`${wuId} in wrong section: expected ${expectedSection}, found ${foundInSection}`);
374
+ }
375
+ else {
376
+ errors.push(`${wuId} missing from backlog (status: ${expectedSection})`);
377
+ }
378
+ }
379
+ }
380
+ return { valid: errors.length === 0, errors };
381
+ }