@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,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Helpers - Shared utilities for worktree and WU validation hooks
4
+ *
5
+ * Used by:
6
+ * - .husky/prepare-commit-msg
7
+ * - .husky/pre-commit
8
+ * - .husky/pre-push
9
+ */
10
+ /**
11
+ * Validate WU ID format
12
+ *
13
+ * WU-1593: Extracted from duplicate implementations in wu-create.mjs and wu-edit.mjs (DRY).
14
+ * Uses centralized PATTERNS.WU_ID regex from wu-constants.mjs.
15
+ *
16
+ * @param {string} id - WU ID to validate (e.g., 'WU-123')
17
+ * @throws {Error} If ID format is invalid
18
+ *
19
+ * @example
20
+ * validateWUIDFormat('WU-123'); // OK
21
+ * validateWUIDFormat('wu-123'); // throws Error
22
+ * validateWUIDFormat('TICKET-123'); // throws Error
23
+ */
24
+ export declare function validateWUIDFormat(id: any): void;
25
+ /**
26
+ * Execute a shell command and return the output
27
+ * @param {string} cmd - Command to execute
28
+ * @param {object} opts - Execution options
29
+ * @returns {string} Command output
30
+ */
31
+ export declare function run(cmd: any, opts?: {}): string;
32
+ /**
33
+ * Get the current Git branch name
34
+ * Uses REAL_GIT to bypass shim and prevent recursion.
35
+ * @returns {string|null} Branch name or null if not in a git repo
36
+ */
37
+ export declare function getCurrentBranch(): string;
38
+ /**
39
+ * Get the current working directory relative to repo root
40
+ * Uses REAL_GIT to bypass shim and prevent recursion.
41
+ * @returns {string} Current directory path
42
+ */
43
+ export declare function getCurrentDir(): string;
44
+ /**
45
+ * Check if we're in the main worktree (not a lane worktree)
46
+ * Uses REAL_GIT to bypass shim and prevent recursion.
47
+ * @returns {boolean} True if in main worktree
48
+ */
49
+ export declare function isMainWorktree(): boolean;
50
+ /**
51
+ * Extract WU ID from branch name
52
+ * Expected format: lane/<lane-name>/<wu-id>
53
+ * @param {string} branch - Branch name
54
+ * @returns {string|null} WU ID (e.g., 'WU-401') or null
55
+ */
56
+ export declare function extractWUFromBranch(branch: any): any;
57
+ /**
58
+ * Validate branch name follows lane convention
59
+ * Expected: lane/<lane-name>/<wu-id>
60
+ * @param {string} branch - Branch name to validate
61
+ * @returns {{valid: boolean, lane: string|null, wuid: string|null, error: string|null}}
62
+ */
63
+ export declare function validateBranchName(branch: any): {
64
+ valid: boolean;
65
+ lane: any;
66
+ wuid: any;
67
+ error: string;
68
+ } | {
69
+ valid: boolean;
70
+ lane: any;
71
+ wuid: any;
72
+ error: any;
73
+ };
74
+ /**
75
+ * Read WU YAML file and return parsed content
76
+ * Uses REAL_GIT to bypass shim and prevent recursion.
77
+ * @param {string} wuid - WU ID (e.g., 'WU-401')
78
+ * @returns {object|null} Parsed WU YAML or null if not found
79
+ */
80
+ export declare function readWUYaml(wuid: any): any;
81
+ /**
82
+ * Check if WU status allows the operation
83
+ * @param {string} wuid - WU ID
84
+ * @param {string[]} allowedStatuses - List of allowed statuses
85
+ * @returns {{allowed: boolean, status: string|null, error: string|null}}
86
+ */
87
+ export declare function checkWUStatus(wuid: any, allowedStatuses?: string[]): {
88
+ allowed: boolean;
89
+ status: any;
90
+ error: string;
91
+ };
92
+ /**
93
+ * Format an error message for hook output
94
+ * @param {string} hookName - Name of the hook
95
+ * @param {string} message - Error message
96
+ * @returns {string} Formatted error message
97
+ */
98
+ export declare function formatHookError(hookName: any, message: any): string;
99
+ /**
100
+ * Extract WU ID from commit message
101
+ * Supports formats:
102
+ * - wu(WU-401): message
103
+ * - chore(wu-401): message
104
+ * - feat(wu-401): message
105
+ * @param {string} message - Commit message
106
+ * @returns {string|null} WU ID or null
107
+ */
108
+ export declare function extractWUFromCommitMessage(message: any): any;
109
+ /**
110
+ * Ensure current branch is main. Throws if not.
111
+ *
112
+ * Centralized from duplicated ensureOnMain() functions across wu-* scripts (WU-1256).
113
+ * Async version - accepts a git adapter with async getCurrentBranch() method.
114
+ *
115
+ * @param {object} git - Git adapter with async getCurrentBranch() method
116
+ * @throws {Error} If not on main branch
117
+ */
118
+ export declare function ensureOnMain(git: any): Promise<void>;
119
+ /**
120
+ * Ensure main branch is up to date with origin.
121
+ *
122
+ * Centralized from duplicated ensureMainUpToDate() functions across wu-* scripts (WU-1256).
123
+ *
124
+ * @param {object} git - Git adapter with async fetch() and getCommitHash() methods
125
+ * @param {string} [scriptName='wu'] - Script name for logging
126
+ * @throws {Error} If main is out of sync with origin
127
+ */
128
+ export declare function ensureMainUpToDate(git: any, _scriptName?: string): Promise<void>;
@@ -0,0 +1,248 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Helpers - Shared utilities for worktree and WU validation hooks
4
+ *
5
+ * Used by:
6
+ * - .husky/prepare-commit-msg
7
+ * - .husky/pre-commit
8
+ * - .husky/pre-push
9
+ */
10
+ import { execSync } from 'node:child_process';
11
+ import { existsSync, readFileSync } from 'node:fs';
12
+ import path from 'node:path';
13
+ import { parse } from 'yaml';
14
+ import { BRANCHES, REMOTES, STDIO, REAL_GIT, PATTERNS } from './wu-constants.js';
15
+ import { die } from './error-handler.js';
16
+ /**
17
+ * Validate WU ID format
18
+ *
19
+ * WU-1593: Extracted from duplicate implementations in wu-create.mjs and wu-edit.mjs (DRY).
20
+ * Uses centralized PATTERNS.WU_ID regex from wu-constants.mjs.
21
+ *
22
+ * @param {string} id - WU ID to validate (e.g., 'WU-123')
23
+ * @throws {Error} If ID format is invalid
24
+ *
25
+ * @example
26
+ * validateWUIDFormat('WU-123'); // OK
27
+ * validateWUIDFormat('wu-123'); // throws Error
28
+ * validateWUIDFormat('TICKET-123'); // throws Error
29
+ */
30
+ export function validateWUIDFormat(id) {
31
+ if (!PATTERNS.WU_ID.test(id)) {
32
+ die(`Invalid WU ID format: "${id}"\n\nExpected format: WU-<number> (e.g., WU-706)`);
33
+ }
34
+ }
35
+ /**
36
+ * Execute a shell command and return the output
37
+ * @param {string} cmd - Command to execute
38
+ * @param {object} opts - Execution options
39
+ * @returns {string} Command output
40
+ */
41
+ export function run(cmd, opts = {}) {
42
+ try {
43
+ return execSync(cmd, { stdio: STDIO.PIPE, encoding: 'utf-8', ...opts }).trim();
44
+ }
45
+ catch {
46
+ return '';
47
+ }
48
+ }
49
+ /**
50
+ * Get the current Git branch name
51
+ * Uses REAL_GIT to bypass shim and prevent recursion.
52
+ * @returns {string|null} Branch name or null if not in a git repo
53
+ */
54
+ export function getCurrentBranch() {
55
+ return run(`${REAL_GIT} rev-parse --abbrev-ref HEAD`) || null;
56
+ }
57
+ /**
58
+ * Get the current working directory relative to repo root
59
+ * Uses REAL_GIT to bypass shim and prevent recursion.
60
+ * @returns {string} Current directory path
61
+ */
62
+ export function getCurrentDir() {
63
+ const repoRoot = run(`${REAL_GIT} rev-parse --show-toplevel`);
64
+ const cwd = process.cwd();
65
+ if (!repoRoot)
66
+ return cwd;
67
+ return path.relative(repoRoot, cwd) || '.';
68
+ }
69
+ /**
70
+ * Check if we're in the main worktree (not a lane worktree)
71
+ * Uses REAL_GIT to bypass shim and prevent recursion.
72
+ * @returns {boolean} True if in main worktree
73
+ */
74
+ export function isMainWorktree() {
75
+ const gitDir = run(`${REAL_GIT} rev-parse --git-dir`);
76
+ if (!gitDir)
77
+ return true;
78
+ const normalized = gitDir.replace(/\\/g, '/');
79
+ // Lane worktrees live under .git/worktrees/<name>
80
+ return !normalized.includes('/worktrees/');
81
+ }
82
+ /**
83
+ * Extract WU ID from branch name
84
+ * Expected format: lane/<lane-name>/<wu-id>
85
+ * @param {string} branch - Branch name
86
+ * @returns {string|null} WU ID (e.g., 'WU-401') or null
87
+ */
88
+ export function extractWUFromBranch(branch) {
89
+ if (!branch)
90
+ return null;
91
+ // Match lane/<lane>/<wu-id> format
92
+ const match = branch.match(/^lane\/[^/]+\/(wu-\d+)$/i);
93
+ if (match) {
94
+ return match[1].toUpperCase();
95
+ }
96
+ return null;
97
+ }
98
+ /**
99
+ * Validate branch name follows lane convention
100
+ * Expected: lane/<lane-name>/<wu-id>
101
+ * @param {string} branch - Branch name to validate
102
+ * @returns {{valid: boolean, lane: string|null, wuid: string|null, error: string|null}}
103
+ */
104
+ export function validateBranchName(branch) {
105
+ if (!branch || branch === BRANCHES.MAIN) {
106
+ return { valid: true, lane: null, wuid: null, error: null };
107
+ }
108
+ const match = branch.match(/^lane\/([^/]+)\/(wu-\d+)$/i);
109
+ if (!match) {
110
+ return {
111
+ valid: false,
112
+ lane: null,
113
+ wuid: null,
114
+ error: `Branch '${branch}' doesn't follow lane/<lane>/<wu-id> convention`,
115
+ };
116
+ }
117
+ return {
118
+ valid: true,
119
+ lane: match[1],
120
+ wuid: match[2].toUpperCase(),
121
+ error: null,
122
+ };
123
+ }
124
+ /**
125
+ * Read WU YAML file and return parsed content
126
+ * Uses REAL_GIT to bypass shim and prevent recursion.
127
+ * @param {string} wuid - WU ID (e.g., 'WU-401')
128
+ * @returns {object|null} Parsed WU YAML or null if not found
129
+ */
130
+ export function readWUYaml(wuid) {
131
+ const repoRoot = run(`${REAL_GIT} rev-parse --show-toplevel`);
132
+ if (!repoRoot)
133
+ return null;
134
+ const wuPath = path.join(repoRoot, 'docs', '04-operations', 'tasks', 'wu', `${wuid}.yaml`);
135
+ if (!existsSync(wuPath))
136
+ return null;
137
+ try {
138
+ const content = readFileSync(wuPath, { encoding: 'utf-8' });
139
+ return parse(content);
140
+ }
141
+ catch {
142
+ return null;
143
+ }
144
+ }
145
+ /**
146
+ * Check if WU status allows the operation
147
+ * @param {string} wuid - WU ID
148
+ * @param {string[]} allowedStatuses - List of allowed statuses
149
+ * @returns {{allowed: boolean, status: string|null, error: string|null}}
150
+ */
151
+ export function checkWUStatus(wuid, allowedStatuses = ['in_progress', 'waiting']) {
152
+ const wu = readWUYaml(wuid);
153
+ if (!wu) {
154
+ return {
155
+ allowed: false,
156
+ status: null,
157
+ error: `WU ${wuid} not found in docs/04-operations/tasks/wu/`,
158
+ };
159
+ }
160
+ const status = wu.status;
161
+ if (!status) {
162
+ return {
163
+ allowed: false,
164
+ status: null,
165
+ error: `WU ${wuid} has no status field`,
166
+ };
167
+ }
168
+ if (!allowedStatuses.includes(status)) {
169
+ return {
170
+ allowed: false,
171
+ status,
172
+ error: `WU ${wuid} status is '${status}' (expected one of: ${allowedStatuses.join(', ')})`,
173
+ };
174
+ }
175
+ return { allowed: true, status, error: null };
176
+ }
177
+ /**
178
+ * Format an error message for hook output
179
+ * @param {string} hookName - Name of the hook
180
+ * @param {string} message - Error message
181
+ * @returns {string} Formatted error message
182
+ */
183
+ export function formatHookError(hookName, message) {
184
+ return `
185
+ ╔═══════════════════════════════════════════════════════════════════╗
186
+ ║ ${hookName.toUpperCase()} HOOK ERROR
187
+ ╠═══════════════════════════════════════════════════════════════════╣
188
+ ║ ${message}
189
+ ╚═══════════════════════════════════════════════════════════════════╝
190
+ `;
191
+ }
192
+ /**
193
+ * Extract WU ID from commit message
194
+ * Supports formats:
195
+ * - wu(WU-401): message
196
+ * - chore(wu-401): message
197
+ * - feat(wu-401): message
198
+ * @param {string} message - Commit message
199
+ * @returns {string|null} WU ID or null
200
+ */
201
+ export function extractWUFromCommitMessage(message) {
202
+ if (!message)
203
+ return null;
204
+ // Match wu(WU-401) or type(wu-401) patterns
205
+ const patterns = [
206
+ /wu\((wu-\d+)\)/i, // wu(WU-401)
207
+ /\w+\((wu-\d+)\)/i, // chore(wu-401), feat(wu-401), etc.
208
+ ];
209
+ for (const pattern of patterns) {
210
+ const match = message.match(pattern);
211
+ if (match) {
212
+ return match[1].toUpperCase();
213
+ }
214
+ }
215
+ return null;
216
+ }
217
+ /**
218
+ * Ensure current branch is main. Throws if not.
219
+ *
220
+ * Centralized from duplicated ensureOnMain() functions across wu-* scripts (WU-1256).
221
+ * Async version - accepts a git adapter with async getCurrentBranch() method.
222
+ *
223
+ * @param {object} git - Git adapter with async getCurrentBranch() method
224
+ * @throws {Error} If not on main branch
225
+ */
226
+ export async function ensureOnMain(git) {
227
+ const branch = await git.getCurrentBranch();
228
+ if (branch !== BRANCHES.MAIN) {
229
+ throw new Error(`Run from shared checkout on '${BRANCHES.MAIN}' (found '${branch}')`);
230
+ }
231
+ }
232
+ /**
233
+ * Ensure main branch is up to date with origin.
234
+ *
235
+ * Centralized from duplicated ensureMainUpToDate() functions across wu-* scripts (WU-1256).
236
+ *
237
+ * @param {object} git - Git adapter with async fetch() and getCommitHash() methods
238
+ * @param {string} [scriptName='wu'] - Script name for logging
239
+ * @throws {Error} If main is out of sync with origin
240
+ */
241
+ export async function ensureMainUpToDate(git, _scriptName = 'wu') {
242
+ await git.fetch(REMOTES.ORIGIN, BRANCHES.MAIN);
243
+ const localMain = await git.getCommitHash(BRANCHES.MAIN);
244
+ const remoteMain = await git.getCommitHash(`${REMOTES.ORIGIN}/${BRANCHES.MAIN}`);
245
+ if (localMain !== remoteMain) {
246
+ throw new Error(`Main branch is out of sync with origin.\n\nRun: git pull ${REMOTES.ORIGIN} ${BRANCHES.MAIN}`);
247
+ }
248
+ }
@@ -0,0 +1,70 @@
1
+ /**
2
+ * WU Spec Linter (WU-2252)
3
+ *
4
+ * Validates WU specs against two critical rules:
5
+ * 1. Acceptance criteria cannot reference file paths absent from code_paths
6
+ * 2. Acceptance/code_paths cannot conflict with invariants.yml
7
+ *
8
+ * This prevents specs that create work contradicting prior fixes.
9
+ *
10
+ * @module tools/lib/wu-lint
11
+ */
12
+ /**
13
+ * Error type constants for WU spec linting
14
+ */
15
+ export declare const WU_LINT_ERROR_TYPES: {
16
+ ACCEPTANCE_PATH_NOT_IN_CODE_PATHS: string;
17
+ CODE_PATH_CONFLICTS_INVARIANT: string;
18
+ ACCEPTANCE_CONFLICTS_INVARIANT: string;
19
+ };
20
+ /**
21
+ * Validate that acceptance criteria only reference paths in code_paths
22
+ *
23
+ * @param {object} wu - WU spec object
24
+ * @param {string} wu.id - WU ID
25
+ * @param {string[]} wu.acceptance - Acceptance criteria
26
+ * @param {string[]} wu.code_paths - Code paths
27
+ * @returns {{valid: boolean, errors: Array<object>}} Validation result
28
+ */
29
+ export declare function validateAcceptanceCodePaths(wu: any): {
30
+ valid: boolean;
31
+ errors: any[];
32
+ };
33
+ /**
34
+ * Validate that code_paths and acceptance do not conflict with invariants
35
+ *
36
+ * @param {object} wu - WU spec object
37
+ * @param {Array<object>} invariants - Array of invariant definitions
38
+ * @returns {{valid: boolean, errors: Array<object>}} Validation result
39
+ */
40
+ export declare function validateInvariantsCompliance(wu: any, invariants: any): {
41
+ valid: boolean;
42
+ errors: any[];
43
+ };
44
+ /**
45
+ * Options for linting WU spec
46
+ */
47
+ export interface LintWUSpecOptions {
48
+ /** Pre-loaded invariants */
49
+ invariants?: unknown[];
50
+ /** Path to invariants.yml */
51
+ invariantsPath?: string;
52
+ }
53
+ /**
54
+ * Lint a WU spec against all rules
55
+ *
56
+ * @param {object} wu - WU spec object
57
+ * @param {LintWUSpecOptions} [options={}] - Options
58
+ * @returns {{valid: boolean, errors: Array<object>}} Lint result
59
+ */
60
+ export declare function lintWUSpec(wu: any, options?: LintWUSpecOptions): {
61
+ valid: boolean;
62
+ errors: any[];
63
+ };
64
+ /**
65
+ * Format lint errors for display
66
+ *
67
+ * @param {Array<object>} errors - Array of lint errors
68
+ * @returns {string} Formatted error message
69
+ */
70
+ export declare function formatLintErrors(errors: any): string;
@@ -0,0 +1,234 @@
1
+ /**
2
+ * WU Spec Linter (WU-2252)
3
+ *
4
+ * Validates WU specs against two critical rules:
5
+ * 1. Acceptance criteria cannot reference file paths absent from code_paths
6
+ * 2. Acceptance/code_paths cannot conflict with invariants.yml
7
+ *
8
+ * This prevents specs that create work contradicting prior fixes.
9
+ *
10
+ * @module tools/lib/wu-lint
11
+ */
12
+ import { existsSync } from 'node:fs';
13
+ import { minimatch } from 'minimatch';
14
+ import { loadInvariants, INVARIANT_TYPES } from './invariants-runner.js';
15
+ /**
16
+ * Error type constants for WU spec linting
17
+ */
18
+ export const WU_LINT_ERROR_TYPES = {
19
+ ACCEPTANCE_PATH_NOT_IN_CODE_PATHS: 'acceptance_path_not_in_code_paths',
20
+ CODE_PATH_CONFLICTS_INVARIANT: 'code_path_conflicts_invariant',
21
+ ACCEPTANCE_CONFLICTS_INVARIANT: 'acceptance_conflicts_invariant',
22
+ };
23
+ /**
24
+ * Regex to detect file paths in acceptance criteria text
25
+ * Matches patterns like: apps/web/src/file.ts, tools/lib/helper.mjs
26
+ * Uses explicit character sets to avoid regex backtracking issues
27
+ */
28
+ const FILE_PATH_PATTERN = /(?:^|[\s'"`])([a-zA-Z0-9_-]+\/[a-zA-Z0-9_./-]+\.[a-zA-Z0-9]+)/g;
29
+ /**
30
+ * Extract file paths from acceptance criteria text
31
+ *
32
+ * @param {string} text - Acceptance criterion text
33
+ * @returns {string[]} Array of file paths found
34
+ */
35
+ function extractFilePaths(text) {
36
+ const paths = [];
37
+ let match;
38
+ while ((match = FILE_PATH_PATTERN.exec(text)) !== null) {
39
+ paths.push(match[1]);
40
+ }
41
+ // Reset regex state
42
+ FILE_PATH_PATTERN.lastIndex = 0;
43
+ return paths;
44
+ }
45
+ /**
46
+ * Check if a file path matches any pattern in code_paths
47
+ * Supports glob patterns (e.g., apps/web/src/**\/*.ts)
48
+ *
49
+ * @param {string} filePath - File path to check
50
+ * @param {string[]} codePaths - Array of code_paths (may include globs)
51
+ * @returns {boolean} True if path matches any code_paths pattern
52
+ */
53
+ function pathMatchesCodePaths(filePath, codePaths) {
54
+ for (const pattern of codePaths) {
55
+ // Exact match
56
+ if (filePath === pattern) {
57
+ return true;
58
+ }
59
+ // Glob match
60
+ if (minimatch(filePath, pattern)) {
61
+ return true;
62
+ }
63
+ }
64
+ return false;
65
+ }
66
+ /**
67
+ * Validate that acceptance criteria only reference paths in code_paths
68
+ *
69
+ * @param {object} wu - WU spec object
70
+ * @param {string} wu.id - WU ID
71
+ * @param {string[]} wu.acceptance - Acceptance criteria
72
+ * @param {string[]} wu.code_paths - Code paths
73
+ * @returns {{valid: boolean, errors: Array<object>}} Validation result
74
+ */
75
+ export function validateAcceptanceCodePaths(wu) {
76
+ const { id, acceptance = [], code_paths = [] } = wu;
77
+ const errors = [];
78
+ for (const criterion of acceptance) {
79
+ const referencedPaths = extractFilePaths(criterion);
80
+ for (const referencedPath of referencedPaths) {
81
+ if (!pathMatchesCodePaths(referencedPath, code_paths)) {
82
+ errors.push({
83
+ type: WU_LINT_ERROR_TYPES.ACCEPTANCE_PATH_NOT_IN_CODE_PATHS,
84
+ wuId: id,
85
+ path: referencedPath,
86
+ criterion,
87
+ message: `Acceptance criterion references '${referencedPath}' which is not in code_paths`,
88
+ suggestion: `Add '${referencedPath}' to code_paths or remove the reference from acceptance criteria`,
89
+ });
90
+ }
91
+ }
92
+ }
93
+ return {
94
+ valid: errors.length === 0,
95
+ errors,
96
+ };
97
+ }
98
+ /**
99
+ * Check forbidden-file invariant for conflicts
100
+ *
101
+ * @param {object} invariant - Invariant definition
102
+ * @param {object} wu - WU spec object
103
+ * @returns {Array<object>} Array of errors (empty if no conflicts)
104
+ */
105
+ function checkForbiddenFileInvariant(invariant, wu) {
106
+ const { id, acceptance = [], code_paths = [] } = wu;
107
+ const errors = [];
108
+ // Check if code_paths includes the forbidden file
109
+ if (code_paths.includes(invariant.path)) {
110
+ errors.push({
111
+ type: WU_LINT_ERROR_TYPES.CODE_PATH_CONFLICTS_INVARIANT,
112
+ wuId: id,
113
+ invariantId: invariant.id,
114
+ path: invariant.path,
115
+ message: `code_paths includes '${invariant.path}' which conflicts with invariant ${invariant.id}: ${invariant.description}`,
116
+ suggestion: invariant.message || `Remove '${invariant.path}' from code_paths`,
117
+ });
118
+ }
119
+ // Check if acceptance mentions creating the forbidden file
120
+ for (const criterion of acceptance) {
121
+ if (criterion.includes(invariant.path)) {
122
+ errors.push({
123
+ type: WU_LINT_ERROR_TYPES.ACCEPTANCE_CONFLICTS_INVARIANT,
124
+ wuId: id,
125
+ invariantId: invariant.id,
126
+ path: invariant.path,
127
+ criterion,
128
+ message: `Acceptance criterion references forbidden file '${invariant.path}' (${invariant.id}: ${invariant.description})`,
129
+ suggestion: invariant.message,
130
+ });
131
+ }
132
+ }
133
+ return errors;
134
+ }
135
+ /**
136
+ * Check mutual-exclusivity invariant for conflicts
137
+ *
138
+ * @param {object} invariant - Invariant definition
139
+ * @param {object} wu - WU spec object
140
+ * @returns {Array<object>} Array of errors (empty if no conflicts)
141
+ */
142
+ function checkMutualExclusivityInvariant(invariant, wu) {
143
+ const { id, code_paths = [] } = wu;
144
+ const errors = [];
145
+ // Check if code_paths includes multiple files from the mutual-exclusivity set
146
+ const conflictingPaths = invariant.paths.filter((p) => code_paths.includes(p));
147
+ if (conflictingPaths.length > 1) {
148
+ errors.push({
149
+ type: WU_LINT_ERROR_TYPES.CODE_PATH_CONFLICTS_INVARIANT,
150
+ wuId: id,
151
+ invariantId: invariant.id,
152
+ paths: conflictingPaths,
153
+ message: `code_paths includes multiple mutually exclusive files (${invariant.id}): ${conflictingPaths.join(', ')}`,
154
+ suggestion: invariant.message || `Only one of these files should exist: ${invariant.paths.join(', ')}`,
155
+ });
156
+ }
157
+ return errors;
158
+ }
159
+ /**
160
+ * Validate that code_paths and acceptance do not conflict with invariants
161
+ *
162
+ * @param {object} wu - WU spec object
163
+ * @param {Array<object>} invariants - Array of invariant definitions
164
+ * @returns {{valid: boolean, errors: Array<object>}} Validation result
165
+ */
166
+ export function validateInvariantsCompliance(wu, invariants) {
167
+ const errors = [];
168
+ for (const invariant of invariants) {
169
+ if (invariant.type === INVARIANT_TYPES.FORBIDDEN_FILE) {
170
+ errors.push(...checkForbiddenFileInvariant(invariant, wu));
171
+ }
172
+ else if (invariant.type === INVARIANT_TYPES.MUTUAL_EXCLUSIVITY) {
173
+ errors.push(...checkMutualExclusivityInvariant(invariant, wu));
174
+ }
175
+ }
176
+ return {
177
+ valid: errors.length === 0,
178
+ errors,
179
+ };
180
+ }
181
+ /**
182
+ * Lint a WU spec against all rules
183
+ *
184
+ * @param {object} wu - WU spec object
185
+ * @param {LintWUSpecOptions} [options={}] - Options
186
+ * @returns {{valid: boolean, errors: Array<object>}} Lint result
187
+ */
188
+ export function lintWUSpec(wu, options = {}) {
189
+ const allErrors = [];
190
+ // 1. Validate acceptance/code_paths consistency
191
+ const acceptanceResult = validateAcceptanceCodePaths(wu);
192
+ allErrors.push(...acceptanceResult.errors);
193
+ // 2. Load invariants if not provided
194
+ let invariants = options.invariants || [];
195
+ if (!options.invariants && options.invariantsPath) {
196
+ try {
197
+ if (existsSync(options.invariantsPath)) {
198
+ invariants = loadInvariants(options.invariantsPath);
199
+ }
200
+ }
201
+ catch {
202
+ // If invariants can't be loaded, continue without them
203
+ }
204
+ }
205
+ // 3. Validate invariants compliance
206
+ if (invariants.length > 0) {
207
+ const invariantsResult = validateInvariantsCompliance(wu, invariants);
208
+ allErrors.push(...invariantsResult.errors);
209
+ }
210
+ return {
211
+ valid: allErrors.length === 0,
212
+ errors: allErrors,
213
+ };
214
+ }
215
+ /**
216
+ * Format lint errors for display
217
+ *
218
+ * @param {Array<object>} errors - Array of lint errors
219
+ * @returns {string} Formatted error message
220
+ */
221
+ export function formatLintErrors(errors) {
222
+ if (errors.length === 0) {
223
+ return '';
224
+ }
225
+ const lines = ['WU SPEC LINT ERRORS:', ''];
226
+ for (const error of errors) {
227
+ lines.push(`- ${error.message}`);
228
+ if (error.suggestion) {
229
+ lines.push(` Fix: ${error.suggestion}`);
230
+ }
231
+ lines.push('');
232
+ }
233
+ return lines.join('\n');
234
+ }