@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,567 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU Consistency Checker (WU-1276, WU-2412)
|
|
3
|
+
*
|
|
4
|
+
* Layer 2 defense-in-depth: Detect and repair WU state inconsistencies.
|
|
5
|
+
*
|
|
6
|
+
* Detects five types of inconsistencies:
|
|
7
|
+
* - YAML_DONE_STATUS_IN_PROGRESS: WU YAML done but in status.md In Progress
|
|
8
|
+
* - BACKLOG_DUAL_SECTION: WU in both Done and In Progress sections
|
|
9
|
+
* - YAML_DONE_NO_STAMP: WU YAML done but no stamp file
|
|
10
|
+
* - ORPHAN_WORKTREE_DONE: Done WU still has worktree
|
|
11
|
+
* - STAMP_EXISTS_YAML_NOT_DONE: Stamp exists but YAML status is not done (WU-2412)
|
|
12
|
+
*
|
|
13
|
+
* @see {@link ../wu-repair.mjs} CLI interface
|
|
14
|
+
*/
|
|
15
|
+
import { readFile, writeFile, readdir, mkdir, access } from 'node:fs/promises';
|
|
16
|
+
import { constants } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import yaml from 'js-yaml';
|
|
19
|
+
import { WU_PATHS } from './wu-paths.js';
|
|
20
|
+
import { CONSISTENCY_TYPES, LOG_PREFIX, REMOTES, STRING_LITERALS, toKebab, WU_STATUS, YAML_OPTIONS, } from './wu-constants.js';
|
|
21
|
+
import { todayISO } from './date-utils.js';
|
|
22
|
+
import { createGitForPath } from './git-adapter.js';
|
|
23
|
+
/**
|
|
24
|
+
* Check a single WU for state inconsistencies
|
|
25
|
+
*
|
|
26
|
+
* @param {string} id - WU ID (e.g., 'WU-123')
|
|
27
|
+
* @param {string} [projectRoot=process.cwd()] - Project root directory
|
|
28
|
+
* @returns {Promise<object>} Consistency report with valid, errors, and stats
|
|
29
|
+
*/
|
|
30
|
+
export async function checkWUConsistency(id, projectRoot = process.cwd()) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
const wuPath = path.join(projectRoot, WU_PATHS.WU(id));
|
|
33
|
+
const stampPath = path.join(projectRoot, WU_PATHS.STAMP(id));
|
|
34
|
+
const backlogPath = path.join(projectRoot, WU_PATHS.BACKLOG());
|
|
35
|
+
const statusPath = path.join(projectRoot, WU_PATHS.STATUS());
|
|
36
|
+
// Handle missing WU YAML gracefully
|
|
37
|
+
try {
|
|
38
|
+
await access(wuPath, constants.R_OK);
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return { valid: true, errors: [], stats: { wuExists: false } };
|
|
42
|
+
}
|
|
43
|
+
const wuContent = await readFile(wuPath, { encoding: 'utf-8' });
|
|
44
|
+
const wuDoc = yaml.load(wuContent);
|
|
45
|
+
const yamlStatus = wuDoc?.status || 'unknown';
|
|
46
|
+
const lane = wuDoc?.lane || '';
|
|
47
|
+
const title = wuDoc?.title || '';
|
|
48
|
+
// Check stamp existence
|
|
49
|
+
let hasStamp = false;
|
|
50
|
+
try {
|
|
51
|
+
await access(stampPath, constants.R_OK);
|
|
52
|
+
hasStamp = true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
hasStamp = false;
|
|
56
|
+
}
|
|
57
|
+
// Parse backlog sections
|
|
58
|
+
let backlogContent = '';
|
|
59
|
+
try {
|
|
60
|
+
backlogContent = await readFile(backlogPath, { encoding: 'utf-8' });
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
backlogContent = '';
|
|
64
|
+
}
|
|
65
|
+
const { inDone: backlogInDone, inProgress: backlogInProgress } = parseBacklogSections(backlogContent, id);
|
|
66
|
+
// Parse status.md sections
|
|
67
|
+
let statusContent = '';
|
|
68
|
+
try {
|
|
69
|
+
statusContent = await readFile(statusPath, { encoding: 'utf-8' });
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
statusContent = '';
|
|
73
|
+
}
|
|
74
|
+
const { inProgress: statusInProgress } = parseStatusSections(statusContent, id);
|
|
75
|
+
// Check for worktree
|
|
76
|
+
const hasWorktree = await checkWorktreeExists(id, projectRoot);
|
|
77
|
+
// Detection logic
|
|
78
|
+
// 1. YAML done but in status.md In Progress
|
|
79
|
+
if (yamlStatus === WU_STATUS.DONE && statusInProgress) {
|
|
80
|
+
errors.push({
|
|
81
|
+
type: CONSISTENCY_TYPES.YAML_DONE_STATUS_IN_PROGRESS,
|
|
82
|
+
wuId: id,
|
|
83
|
+
description: `WU ${id} has status '${WU_STATUS.DONE}' in YAML but still appears in status.md In Progress section`,
|
|
84
|
+
repairAction: 'Remove from status.md In Progress section',
|
|
85
|
+
canAutoRepair: true,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
// 2. Backlog dual section (Done AND In Progress)
|
|
89
|
+
if (backlogInDone && backlogInProgress) {
|
|
90
|
+
errors.push({
|
|
91
|
+
type: CONSISTENCY_TYPES.BACKLOG_DUAL_SECTION,
|
|
92
|
+
wuId: id,
|
|
93
|
+
description: `WU ${id} appears in both Done and In Progress sections of backlog.md`,
|
|
94
|
+
repairAction: 'Remove from In Progress section (Done wins)',
|
|
95
|
+
canAutoRepair: true,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
// 3. YAML done but no stamp
|
|
99
|
+
if (yamlStatus === WU_STATUS.DONE && !hasStamp) {
|
|
100
|
+
errors.push({
|
|
101
|
+
type: CONSISTENCY_TYPES.YAML_DONE_NO_STAMP,
|
|
102
|
+
wuId: id,
|
|
103
|
+
title,
|
|
104
|
+
description: `WU ${id} has status '${WU_STATUS.DONE}' but no stamp file exists`,
|
|
105
|
+
repairAction: 'Create stamp file',
|
|
106
|
+
canAutoRepair: true,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
// 4. Orphan worktree for done WU
|
|
110
|
+
if (yamlStatus === WU_STATUS.DONE && hasWorktree) {
|
|
111
|
+
errors.push({
|
|
112
|
+
type: CONSISTENCY_TYPES.ORPHAN_WORKTREE_DONE,
|
|
113
|
+
wuId: id,
|
|
114
|
+
lane,
|
|
115
|
+
description: `WU ${id} has status '${WU_STATUS.DONE}' but still has an associated worktree`,
|
|
116
|
+
repairAction: 'Remove orphan worktree and lane branch',
|
|
117
|
+
canAutoRepair: true,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
// 5. Stamp exists but YAML not done (inverse of YAML_DONE_NO_STAMP)
|
|
121
|
+
// This catches partial wu:done failures where stamp was created but YAML update failed
|
|
122
|
+
if (hasStamp && yamlStatus !== WU_STATUS.DONE) {
|
|
123
|
+
errors.push({
|
|
124
|
+
type: CONSISTENCY_TYPES.STAMP_EXISTS_YAML_NOT_DONE,
|
|
125
|
+
wuId: id,
|
|
126
|
+
title,
|
|
127
|
+
description: `WU ${id} has stamp file but YAML status is '${yamlStatus}' (not done)`,
|
|
128
|
+
repairAction: 'Update YAML to done+locked+completed',
|
|
129
|
+
canAutoRepair: true,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
valid: errors.length === 0,
|
|
134
|
+
errors,
|
|
135
|
+
stats: {
|
|
136
|
+
yamlStatus,
|
|
137
|
+
hasStamp,
|
|
138
|
+
backlogInDone,
|
|
139
|
+
backlogInProgress,
|
|
140
|
+
statusInProgress,
|
|
141
|
+
hasWorktree,
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Check all WUs for consistency
|
|
147
|
+
*
|
|
148
|
+
* @param {string} [projectRoot=process.cwd()] - Project root directory
|
|
149
|
+
* @returns {Promise<object>} Aggregated report with valid, errors, and checked count
|
|
150
|
+
*/
|
|
151
|
+
export async function checkAllWUConsistency(projectRoot = process.cwd()) {
|
|
152
|
+
const wuDir = path.join(projectRoot, 'docs/04-operations/tasks/wu');
|
|
153
|
+
try {
|
|
154
|
+
await access(wuDir, constants.R_OK);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return { valid: true, errors: [], checked: 0 };
|
|
158
|
+
}
|
|
159
|
+
const allErrors = [];
|
|
160
|
+
const wuFiles = (await readdir(wuDir)).filter((f) => /^WU-\d+\.yaml$/.test(f));
|
|
161
|
+
for (const file of wuFiles) {
|
|
162
|
+
const id = file.replace('.yaml', '');
|
|
163
|
+
const report = await checkWUConsistency(id, projectRoot);
|
|
164
|
+
allErrors.push(...report.errors);
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
valid: allErrors.length === 0,
|
|
168
|
+
errors: allErrors,
|
|
169
|
+
checked: wuFiles.length,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Check lane for orphan done WUs (pre-flight for wu:claim)
|
|
174
|
+
*
|
|
175
|
+
* @param {string} lane - Lane name to check
|
|
176
|
+
* @param {string} excludeId - WU ID to exclude from check (the one being claimed)
|
|
177
|
+
* @param {string} [projectRoot=process.cwd()] - Project root directory
|
|
178
|
+
* @returns {Promise<object>} Result with valid, orphans list, and reports
|
|
179
|
+
*/
|
|
180
|
+
export async function checkLaneForOrphanDoneWU(lane, excludeId, projectRoot = process.cwd()) {
|
|
181
|
+
const wuDir = path.join(projectRoot, 'docs/04-operations/tasks/wu');
|
|
182
|
+
try {
|
|
183
|
+
await access(wuDir, constants.R_OK);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
return { valid: true, orphans: [] };
|
|
187
|
+
}
|
|
188
|
+
const orphans = [];
|
|
189
|
+
const wuFiles = (await readdir(wuDir)).filter((f) => /^WU-\d+\.yaml$/.test(f));
|
|
190
|
+
for (const file of wuFiles) {
|
|
191
|
+
const id = file.replace('.yaml', '');
|
|
192
|
+
if (id === excludeId)
|
|
193
|
+
continue;
|
|
194
|
+
const wuPath = path.join(wuDir, file);
|
|
195
|
+
let wuContent;
|
|
196
|
+
try {
|
|
197
|
+
wuContent = await readFile(wuPath, { encoding: 'utf-8' });
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// Skip unreadable files
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
let wuDoc;
|
|
204
|
+
try {
|
|
205
|
+
wuDoc = yaml.load(wuContent);
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
// Skip malformed YAML files - they're a separate issue
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (wuDoc?.lane === lane && wuDoc?.status === WU_STATUS.DONE) {
|
|
212
|
+
const report = await checkWUConsistency(id, projectRoot);
|
|
213
|
+
if (!report.valid) {
|
|
214
|
+
orphans.push({ id, errors: report.errors });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return {
|
|
219
|
+
valid: orphans.length === 0,
|
|
220
|
+
orphans: orphans.map((o) => o.id),
|
|
221
|
+
reports: orphans,
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Repair WU inconsistencies
|
|
226
|
+
*
|
|
227
|
+
* @param {object} report - Report from checkWUConsistency()
|
|
228
|
+
* @param {RepairWUInconsistencyOptions} [options={}] - Repair options
|
|
229
|
+
* @returns {Promise<object>} Result with repaired, skipped, and failed counts
|
|
230
|
+
*/
|
|
231
|
+
export async function repairWUInconsistency(report, options = {}) {
|
|
232
|
+
const { dryRun = false, projectRoot = process.cwd() } = options;
|
|
233
|
+
if (report.valid) {
|
|
234
|
+
return { repaired: 0, skipped: 0, failed: 0 };
|
|
235
|
+
}
|
|
236
|
+
let repaired = 0;
|
|
237
|
+
let skipped = 0;
|
|
238
|
+
let failed = 0;
|
|
239
|
+
for (const error of report.errors) {
|
|
240
|
+
if (!error.canAutoRepair) {
|
|
241
|
+
skipped++;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (dryRun) {
|
|
245
|
+
repaired++;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
try {
|
|
249
|
+
const result = await repairSingleError(error, projectRoot);
|
|
250
|
+
if (result.success) {
|
|
251
|
+
repaired++;
|
|
252
|
+
}
|
|
253
|
+
else if (result.skipped) {
|
|
254
|
+
skipped++;
|
|
255
|
+
if (result.reason) {
|
|
256
|
+
console.warn(`${LOG_PREFIX.REPAIR} Skipped ${error.type}: ${result.reason}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
failed++;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
catch (err) {
|
|
264
|
+
const errMessage = err instanceof Error ? err.message : String(err);
|
|
265
|
+
console.error(`${LOG_PREFIX.REPAIR} Failed to repair ${error.type}: ${errMessage}`);
|
|
266
|
+
failed++;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
return { repaired, skipped, failed };
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Repair a single inconsistency error
|
|
273
|
+
*
|
|
274
|
+
* @param {object} error - Error object from checkWUConsistency()
|
|
275
|
+
* @param {string} projectRoot - Project root directory
|
|
276
|
+
* @returns {Promise<RepairResult>} Result with success, skipped, and reason
|
|
277
|
+
*/
|
|
278
|
+
async function repairSingleError(error, projectRoot) {
|
|
279
|
+
switch (error.type) {
|
|
280
|
+
case CONSISTENCY_TYPES.YAML_DONE_NO_STAMP:
|
|
281
|
+
await createStampInProject(error.wuId, error.title || `WU ${error.wuId}`, projectRoot);
|
|
282
|
+
return { success: true };
|
|
283
|
+
case CONSISTENCY_TYPES.YAML_DONE_STATUS_IN_PROGRESS:
|
|
284
|
+
await removeWUFromSection(path.join(projectRoot, WU_PATHS.STATUS()), error.wuId, '## In Progress');
|
|
285
|
+
return { success: true };
|
|
286
|
+
case CONSISTENCY_TYPES.BACKLOG_DUAL_SECTION:
|
|
287
|
+
await removeWUFromSection(path.join(projectRoot, WU_PATHS.BACKLOG()), error.wuId, '## 🔧 In progress');
|
|
288
|
+
return { success: true };
|
|
289
|
+
case CONSISTENCY_TYPES.ORPHAN_WORKTREE_DONE:
|
|
290
|
+
return await removeOrphanWorktree(error.wuId, error.lane, projectRoot);
|
|
291
|
+
case CONSISTENCY_TYPES.STAMP_EXISTS_YAML_NOT_DONE:
|
|
292
|
+
await updateYamlToDone(error.wuId, projectRoot);
|
|
293
|
+
return { success: true };
|
|
294
|
+
default:
|
|
295
|
+
return { skipped: true, reason: `Unknown error type: ${error.type}` };
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/**
|
|
299
|
+
* Create stamp file in a specific project root
|
|
300
|
+
*
|
|
301
|
+
* @param {string} id - WU ID
|
|
302
|
+
* @param {string} title - WU title
|
|
303
|
+
* @param {string} projectRoot - Project root directory
|
|
304
|
+
* @returns {Promise<void>}
|
|
305
|
+
*/
|
|
306
|
+
async function createStampInProject(id, title, projectRoot) {
|
|
307
|
+
const stampsDir = path.join(projectRoot, WU_PATHS.STAMPS_DIR());
|
|
308
|
+
const stampPath = path.join(projectRoot, WU_PATHS.STAMP(id));
|
|
309
|
+
// Ensure stamps directory exists
|
|
310
|
+
try {
|
|
311
|
+
await access(stampsDir, constants.R_OK);
|
|
312
|
+
}
|
|
313
|
+
catch {
|
|
314
|
+
await mkdir(stampsDir, { recursive: true });
|
|
315
|
+
}
|
|
316
|
+
// Don't overwrite existing stamp
|
|
317
|
+
try {
|
|
318
|
+
await access(stampPath, constants.R_OK);
|
|
319
|
+
return; // Stamp already exists
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
// Stamp doesn't exist, continue to create it
|
|
323
|
+
}
|
|
324
|
+
// Create stamp file
|
|
325
|
+
const body = `WU ${id} — ${title}\nCompleted: ${todayISO()}\n`;
|
|
326
|
+
await writeFile(stampPath, body, { encoding: 'utf-8' });
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* Update WU YAML to done+locked+completed state (WU-2412)
|
|
330
|
+
*
|
|
331
|
+
* Repairs STAMP_EXISTS_YAML_NOT_DONE by setting:
|
|
332
|
+
* - status: done
|
|
333
|
+
* - locked: true
|
|
334
|
+
* - completed: YYYY-MM-DD (today, unless already set)
|
|
335
|
+
*
|
|
336
|
+
* @param {string} id - WU ID
|
|
337
|
+
* @param {string} projectRoot - Project root directory
|
|
338
|
+
* @returns {Promise<void>}
|
|
339
|
+
*/
|
|
340
|
+
async function updateYamlToDone(id, projectRoot) {
|
|
341
|
+
const wuPath = path.join(projectRoot, WU_PATHS.WU(id));
|
|
342
|
+
// Read current YAML
|
|
343
|
+
const content = await readFile(wuPath, { encoding: 'utf-8' });
|
|
344
|
+
const wuDoc = yaml.load(content);
|
|
345
|
+
if (!wuDoc) {
|
|
346
|
+
throw new Error(`Failed to parse WU YAML: ${wuPath}`);
|
|
347
|
+
}
|
|
348
|
+
// Update fields
|
|
349
|
+
wuDoc.status = WU_STATUS.DONE;
|
|
350
|
+
wuDoc.locked = true;
|
|
351
|
+
// Preserve existing completed date if present, otherwise set to today
|
|
352
|
+
if (!wuDoc.completed) {
|
|
353
|
+
wuDoc.completed = todayISO();
|
|
354
|
+
}
|
|
355
|
+
// Write updated YAML
|
|
356
|
+
const updatedContent = yaml.dump(wuDoc, { lineWidth: YAML_OPTIONS.LINE_WIDTH });
|
|
357
|
+
await writeFile(wuPath, updatedContent, { encoding: 'utf-8' });
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* Remove WU entry from a specific section in a markdown file
|
|
361
|
+
*
|
|
362
|
+
* @param {string} filePath - Path to the markdown file
|
|
363
|
+
* @param {string} id - WU ID to remove
|
|
364
|
+
* @param {string} sectionHeading - Section heading to target
|
|
365
|
+
* @returns {Promise<void>}
|
|
366
|
+
*/
|
|
367
|
+
async function removeWUFromSection(filePath, id, sectionHeading) {
|
|
368
|
+
try {
|
|
369
|
+
await access(filePath, constants.R_OK);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return; // File doesn't exist
|
|
373
|
+
}
|
|
374
|
+
const content = await readFile(filePath, { encoding: 'utf-8' });
|
|
375
|
+
const lines = content.split(/\r?\n/);
|
|
376
|
+
let inTargetSection = false;
|
|
377
|
+
let nextSectionIdx = -1;
|
|
378
|
+
let sectionStartIdx = -1;
|
|
379
|
+
// Normalize heading for comparison (lowercase, trim)
|
|
380
|
+
const normalizedHeading = sectionHeading.toLowerCase().trim();
|
|
381
|
+
// Find section boundaries
|
|
382
|
+
for (let i = 0; i < lines.length; i++) {
|
|
383
|
+
const normalizedLine = lines[i].toLowerCase().trim();
|
|
384
|
+
if (normalizedLine === normalizedHeading || normalizedLine.startsWith(normalizedHeading)) {
|
|
385
|
+
inTargetSection = true;
|
|
386
|
+
sectionStartIdx = i;
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (inTargetSection && lines[i].trim().startsWith('## ')) {
|
|
390
|
+
nextSectionIdx = i;
|
|
391
|
+
break;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (sectionStartIdx === -1)
|
|
395
|
+
return;
|
|
396
|
+
const endIdx = nextSectionIdx === -1 ? lines.length : nextSectionIdx;
|
|
397
|
+
// Filter out lines containing the WU ID in the target section
|
|
398
|
+
const newLines = [];
|
|
399
|
+
for (let i = 0; i < lines.length; i++) {
|
|
400
|
+
if (i > sectionStartIdx && i < endIdx && lines[i].includes(id)) {
|
|
401
|
+
continue; // Skip this line
|
|
402
|
+
}
|
|
403
|
+
newLines.push(lines[i]);
|
|
404
|
+
}
|
|
405
|
+
await writeFile(filePath, newLines.join(STRING_LITERALS.NEWLINE));
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Remove orphan worktree for a done WU
|
|
409
|
+
*
|
|
410
|
+
* CRITICAL: This function includes safety guards to prevent data loss.
|
|
411
|
+
* See WU-1276 incident report for why these guards are essential.
|
|
412
|
+
*
|
|
413
|
+
* @param {string} id - WU ID
|
|
414
|
+
* @param {string} lane - Lane name
|
|
415
|
+
* @param {string} projectRoot - Project root directory
|
|
416
|
+
* @returns {Promise<object>} Result with success, skipped, and reason
|
|
417
|
+
*/
|
|
418
|
+
async function removeOrphanWorktree(id, lane, projectRoot) {
|
|
419
|
+
// Find worktree path
|
|
420
|
+
const laneKebab = toKebab(lane);
|
|
421
|
+
const worktreeName = `${laneKebab}-${id.toLowerCase()}`;
|
|
422
|
+
const worktreePath = path.join(projectRoot, 'worktrees', worktreeName);
|
|
423
|
+
// 🚨 SAFETY GUARD 1: Check if cwd is inside worktree
|
|
424
|
+
const cwd = process.cwd();
|
|
425
|
+
if (cwd.startsWith(worktreePath)) {
|
|
426
|
+
return { skipped: true, reason: 'Cannot delete worktree while inside it' };
|
|
427
|
+
}
|
|
428
|
+
// 🚨 SAFETY GUARD 2: Check for uncommitted changes (if worktree exists)
|
|
429
|
+
try {
|
|
430
|
+
await access(worktreePath, constants.R_OK);
|
|
431
|
+
// Worktree exists, check for uncommitted changes
|
|
432
|
+
try {
|
|
433
|
+
const gitWorktree = createGitForPath(worktreePath);
|
|
434
|
+
const status = await gitWorktree.getStatus();
|
|
435
|
+
if (status.trim().length > 0) {
|
|
436
|
+
return { skipped: true, reason: 'Worktree has uncommitted changes' };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
catch {
|
|
440
|
+
// Ignore errors checking status - proceed with other guards
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
// Worktree doesn't exist, that's fine
|
|
445
|
+
}
|
|
446
|
+
// 🚨 SAFETY GUARD 3: Check stamp exists (not rollback state)
|
|
447
|
+
const stampPath = path.join(projectRoot, WU_PATHS.STAMP(id));
|
|
448
|
+
try {
|
|
449
|
+
await access(stampPath, constants.R_OK);
|
|
450
|
+
}
|
|
451
|
+
catch {
|
|
452
|
+
return { skipped: true, reason: 'WU marked done but no stamp - possible rollback state' };
|
|
453
|
+
}
|
|
454
|
+
// Safe to proceed with cleanup
|
|
455
|
+
const git = createGitForPath(projectRoot);
|
|
456
|
+
try {
|
|
457
|
+
await access(worktreePath, constants.R_OK);
|
|
458
|
+
await git.worktreeRemove(worktreePath, { force: true });
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
// Worktree may not exist
|
|
462
|
+
}
|
|
463
|
+
// Delete lane branch
|
|
464
|
+
const branchName = `lane/${laneKebab}/${id.toLowerCase()}`;
|
|
465
|
+
try {
|
|
466
|
+
await git.deleteBranch(branchName, { force: true });
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Branch may not exist locally
|
|
470
|
+
}
|
|
471
|
+
try {
|
|
472
|
+
await git.raw(['push', REMOTES.ORIGIN, '--delete', branchName]);
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
// Remote branch may not exist
|
|
476
|
+
}
|
|
477
|
+
return { success: true };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Parse backlog.md to find which sections contain a WU ID
|
|
481
|
+
*
|
|
482
|
+
* @param {string} content - Backlog file content
|
|
483
|
+
* @param {string} id - WU ID to search for
|
|
484
|
+
* @returns {object} Object with inDone and inProgress booleans
|
|
485
|
+
*/
|
|
486
|
+
function parseBacklogSections(content, id) {
|
|
487
|
+
const lines = content.split(/\r?\n/);
|
|
488
|
+
let inDone = false;
|
|
489
|
+
let inProgress = false;
|
|
490
|
+
let currentSection = null;
|
|
491
|
+
// Match exact WU YAML filename to prevent substring false positives
|
|
492
|
+
// e.g., WU-208 should not match lines containing WU-2087
|
|
493
|
+
const exactPattern = `(wu/${id}.yaml)`;
|
|
494
|
+
for (const line of lines) {
|
|
495
|
+
if (line.trim() === '## ✅ Done') {
|
|
496
|
+
currentSection = WU_STATUS.DONE;
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
if (line.trim() === '## 🔧 In progress') {
|
|
500
|
+
currentSection = 'in_progress';
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (line.trim().startsWith('## ')) {
|
|
504
|
+
currentSection = null;
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
507
|
+
if (line.includes(exactPattern)) {
|
|
508
|
+
if (currentSection === WU_STATUS.DONE)
|
|
509
|
+
inDone = true;
|
|
510
|
+
if (currentSection === 'in_progress')
|
|
511
|
+
inProgress = true;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return { inDone, inProgress };
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Parse status.md to find if WU is in In Progress section
|
|
518
|
+
*
|
|
519
|
+
* @param {string} content - Status file content
|
|
520
|
+
* @param {string} id - WU ID to search for
|
|
521
|
+
* @returns {object} Object with inProgress boolean
|
|
522
|
+
*/
|
|
523
|
+
function parseStatusSections(content, id) {
|
|
524
|
+
const lines = content.split(/\r?\n/);
|
|
525
|
+
let inProgress = false;
|
|
526
|
+
let currentSection = null;
|
|
527
|
+
// Match exact WU YAML filename to prevent substring false positives
|
|
528
|
+
// e.g., WU-208 should not match lines containing WU-2087
|
|
529
|
+
const exactPattern = `(wu/${id}.yaml)`;
|
|
530
|
+
for (const line of lines) {
|
|
531
|
+
if (line.trim() === '## In Progress') {
|
|
532
|
+
currentSection = 'in_progress';
|
|
533
|
+
continue;
|
|
534
|
+
}
|
|
535
|
+
if (line.trim().startsWith('## ')) {
|
|
536
|
+
currentSection = null;
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
if (currentSection === 'in_progress' && line.includes(exactPattern)) {
|
|
540
|
+
inProgress = true;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
return { inProgress };
|
|
544
|
+
}
|
|
545
|
+
/**
|
|
546
|
+
* Check if a worktree exists for a given WU ID
|
|
547
|
+
*
|
|
548
|
+
* Uses word-boundary matching to avoid false positives where one WU ID
|
|
549
|
+
* is a prefix of another (e.g., WU-204 should not match wu-2049).
|
|
550
|
+
*
|
|
551
|
+
* @param {string} id - WU ID
|
|
552
|
+
* @param {string} projectRoot - Project root directory
|
|
553
|
+
* @returns {Promise<boolean>} True if worktree exists
|
|
554
|
+
*/
|
|
555
|
+
async function checkWorktreeExists(id, projectRoot) {
|
|
556
|
+
try {
|
|
557
|
+
const git = createGitForPath(projectRoot);
|
|
558
|
+
const output = await git.worktreeList();
|
|
559
|
+
// Match WU ID followed by non-digit or end of string to prevent
|
|
560
|
+
// false positives (e.g., wu-204 matching wu-2049)
|
|
561
|
+
const pattern = new RegExp(`${id.toLowerCase()}(?![0-9])`, 'i');
|
|
562
|
+
return pattern.test(output);
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
}
|