@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.
- package/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- 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
|
+
}
|