@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,243 @@
1
+ /**
2
+ * WU Exposure Validation (WU-1999, WU-2022)
3
+ *
4
+ * WU-1999: Validates exposure field and UI pairing for wu:done.
5
+ * Provides warnings (not errors) to guide completion without blocking.
6
+ *
7
+ * WU-2022: Adds BLOCKING validation for feature accessibility.
8
+ * When exposure=ui, ensures the feature is actually accessible via navigation.
9
+ *
10
+ * Part of INIT-031 Phase 4: Prevent backend-without-UI pattern.
11
+ *
12
+ * @see {@link tools/wu-done.mjs} - Consumer
13
+ * @see {@link tools/lib/wu-schema.mjs} - WU schema with exposure field
14
+ * @see {@link tools/lib/wu-constants.mjs} - WU_EXPOSURE values
15
+ */
16
+ import { WU_EXPOSURE } from './wu-constants.js';
17
+ /**
18
+ * UI verification keywords to search for in acceptance criteria.
19
+ * Case-insensitive patterns that indicate the acceptance criteria
20
+ * mentions UI verification.
21
+ */
22
+ const UI_VERIFICATION_KEYWORDS = [
23
+ 'ui',
24
+ 'frontend',
25
+ 'component',
26
+ 'widget',
27
+ 'page',
28
+ 'displays',
29
+ 'shows',
30
+ 'renders',
31
+ 'user sees',
32
+ 'visible',
33
+ 'screen',
34
+ 'interface',
35
+ ];
36
+ /**
37
+ * Warning message templates with remediation guidance.
38
+ * All messages include the WU ID for context.
39
+ */
40
+ export const EXPOSURE_WARNING_MESSAGES = {
41
+ /**
42
+ * Warning when exposure field is missing entirely.
43
+ * @param {string} wuId - The WU identifier
44
+ * @returns {string} Warning message with remediation
45
+ */
46
+ MISSING_EXPOSURE: (wuId) => `${wuId}: exposure field is missing. ` +
47
+ `Add 'exposure: ui|api|backend-only|documentation' to the WU YAML. ` +
48
+ `This helps ensure user-facing features have corresponding UI coverage.`,
49
+ /**
50
+ * Warning when API exposure lacks UI pairing WUs.
51
+ * @param {string} wuId - The WU identifier
52
+ * @returns {string} Warning message with remediation
53
+ */
54
+ MISSING_UI_PAIRING: (wuId) => `${wuId}: exposure=api but ui_pairing_wus not specified. ` +
55
+ `Add 'ui_pairing_wus: [WU-XXX]' listing UI WUs that consume this API, ` +
56
+ `or set exposure to 'backend-only' if no UI is planned.`,
57
+ /**
58
+ * Warning when API exposure lacks UI verification in acceptance criteria.
59
+ * @param {string} wuId - The WU identifier
60
+ * @returns {string} Warning message with remediation
61
+ */
62
+ MISSING_UI_VERIFICATION: (wuId) => `${wuId}: exposure=api but acceptance criteria lacks UI verification mention. ` +
63
+ `Consider adding a criterion like 'UI displays the data correctly' to ensure end-to-end coverage.`,
64
+ /**
65
+ * Recommendation for user_journey when exposure is UI.
66
+ * @param {string} wuId - The WU identifier
67
+ * @returns {string} Warning message with remediation
68
+ */
69
+ MISSING_USER_JOURNEY: (wuId) => `${wuId}: exposure=ui but user_journey field not present. ` +
70
+ `Adding 'user_journey: "<description>"' is recommended to document the user flow.`,
71
+ };
72
+ /**
73
+ * Check if acceptance criteria mentions UI verification.
74
+ *
75
+ * Searches acceptance criteria (array or nested object) for keywords
76
+ * that indicate UI verification is mentioned.
77
+ *
78
+ * @param {string[]|Record<string, string[]>} acceptance - Acceptance criteria
79
+ * @returns {boolean} True if UI verification is mentioned
80
+ */
81
+ function hasUIVerificationInAcceptance(acceptance) {
82
+ // Flatten acceptance to array of strings
83
+ let criteria = [];
84
+ if (Array.isArray(acceptance)) {
85
+ criteria = acceptance;
86
+ }
87
+ else if (typeof acceptance === 'object' && acceptance !== null) {
88
+ // Nested object format: { category: [items] }
89
+ criteria = Object.values(acceptance).flat();
90
+ }
91
+ // Search for UI-related keywords (case-insensitive)
92
+ const lowerCriteria = criteria.map((c) => (typeof c === 'string' ? c.toLowerCase() : ''));
93
+ return lowerCriteria.some((criterion) => UI_VERIFICATION_KEYWORDS.some((keyword) => criterion.includes(keyword.toLowerCase())));
94
+ }
95
+ export function validateExposure(wu, options = {}) {
96
+ const warnings = [];
97
+ // Early return if skip flag is set
98
+ if (options.skipExposureCheck) {
99
+ return { valid: true, warnings: [] };
100
+ }
101
+ const wuId = wu.id || 'WU-???';
102
+ const exposure = wu.exposure;
103
+ // Check 1: exposure field presence
104
+ if (!exposure) {
105
+ warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_EXPOSURE(wuId));
106
+ // Can't check further without exposure
107
+ return { valid: true, warnings };
108
+ }
109
+ // Check 2 & 3: API exposure checks
110
+ if (exposure === WU_EXPOSURE.API) {
111
+ // Check for ui_pairing_wus
112
+ const uiPairingWus = wu.ui_pairing_wus;
113
+ if (!uiPairingWus || uiPairingWus.length === 0) {
114
+ warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_UI_PAIRING(wuId));
115
+ }
116
+ // Check acceptance criteria for UI verification mention
117
+ const acceptance = wu.acceptance;
118
+ if (acceptance && !hasUIVerificationInAcceptance(acceptance)) {
119
+ warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_UI_VERIFICATION(wuId));
120
+ }
121
+ }
122
+ // Check 4: UI exposure checks
123
+ if (exposure === WU_EXPOSURE.UI) {
124
+ // Recommend user_journey if not present
125
+ if (!wu.user_journey) {
126
+ warnings.push(EXPOSURE_WARNING_MESSAGES.MISSING_USER_JOURNEY(wuId));
127
+ }
128
+ }
129
+ // backend-only and documentation exposures: no additional checks
130
+ return { valid: true, warnings };
131
+ }
132
+ // =============================================================================
133
+ // WU-2022: Feature Accessibility Validation (BLOCKING)
134
+ // =============================================================================
135
+ /**
136
+ * Navigation keywords to search for in tests.manual.
137
+ * Case-insensitive patterns that indicate manual navigation testing.
138
+ */
139
+ const NAVIGATION_KEYWORDS = [
140
+ 'navigate',
141
+ 'navigation',
142
+ 'accessible',
143
+ 'access',
144
+ 'visible',
145
+ 'reachable',
146
+ 'go to',
147
+ 'visit',
148
+ 'open',
149
+ 'click',
150
+ 'link',
151
+ 'route',
152
+ 'url',
153
+ 'path',
154
+ '/space',
155
+ '/dashboard',
156
+ '/settings',
157
+ ];
158
+ /**
159
+ * Pattern to detect Next.js page files in code_paths.
160
+ * Matches: app/.../page.tsx, pages/.../index.tsx, pages/.../*.tsx
161
+ */
162
+ const PAGE_FILE_PATTERNS = [
163
+ /app\/.*\/page\.tsx$/,
164
+ /app\/.*\/page\.ts$/,
165
+ /pages\/.*\.tsx$/,
166
+ /pages\/.*\.ts$/,
167
+ ];
168
+ /**
169
+ * Error message templates for accessibility validation (WU-2022).
170
+ * These are BLOCKING errors, not warnings.
171
+ */
172
+ export const ACCESSIBILITY_ERROR_MESSAGES = {
173
+ /**
174
+ * Error when UI exposure lacks navigation accessibility proof.
175
+ * @param {string} wuId - The WU identifier
176
+ * @returns {string} Error message with remediation guidance
177
+ */
178
+ UI_NOT_ACCESSIBLE: (wuId) => `${wuId}: exposure=ui but feature accessibility not verified. ` +
179
+ `Add one of the following:\n` +
180
+ ` 1. navigation_path: '/your-route' - specify the route where feature is accessible\n` +
181
+ ` 2. code_paths: [..., 'apps/web/src/app/.../page.tsx'] - include a page file\n` +
182
+ ` 3. tests.manual: ['Navigate to /path and verify feature is accessible'] - add navigation test\n\n` +
183
+ `This prevents "orphaned code" - features that exist but users cannot access. ` +
184
+ `Use --skip-accessibility-check to bypass (not recommended).`,
185
+ };
186
+ /**
187
+ * Check if code_paths includes a page file (Next.js page).
188
+ *
189
+ * @param {string[]} codePaths - Array of code paths
190
+ * @returns {boolean} True if any code path matches a page file pattern
191
+ */
192
+ function hasPageFileInCodePaths(codePaths) {
193
+ if (!codePaths || !Array.isArray(codePaths)) {
194
+ return false;
195
+ }
196
+ return codePaths.some((codePath) => PAGE_FILE_PATTERNS.some((pattern) => pattern.test(codePath)));
197
+ }
198
+ /**
199
+ * Check if tests.manual includes navigation verification.
200
+ *
201
+ * @param {object} tests - Tests object from WU YAML
202
+ * @returns {boolean} True if manual tests mention navigation
203
+ */
204
+ function hasNavigationInManualTests(tests) {
205
+ if (!tests || typeof tests !== 'object') {
206
+ return false;
207
+ }
208
+ const manualTests = tests.manual;
209
+ if (!manualTests || !Array.isArray(manualTests)) {
210
+ return false;
211
+ }
212
+ const lowerTests = manualTests.map((t) => (typeof t === 'string' ? t.toLowerCase() : ''));
213
+ return lowerTests.some((test) => NAVIGATION_KEYWORDS.some((keyword) => test.includes(keyword.toLowerCase())));
214
+ }
215
+ export function validateFeatureAccessibility(wu, options = {}) {
216
+ const errors = [];
217
+ // Early return if skip flag is set
218
+ if (options.skipAccessibilityCheck) {
219
+ return { valid: true, errors: [] };
220
+ }
221
+ const exposure = wu.exposure;
222
+ // Skip validation for non-UI exposures
223
+ if (!exposure || exposure !== WU_EXPOSURE.UI) {
224
+ return { valid: true, errors: [] };
225
+ }
226
+ // For exposure=ui, verify accessibility via one of three methods
227
+ const wuId = wu.id || 'WU-???';
228
+ // Method 1: navigation_path is specified
229
+ if (wu.navigation_path && wu.navigation_path.trim().length > 0) {
230
+ return { valid: true, errors: [] };
231
+ }
232
+ // Method 2: code_paths includes a page file
233
+ if (hasPageFileInCodePaths(wu.code_paths)) {
234
+ return { valid: true, errors: [] };
235
+ }
236
+ // Method 3: tests.manual includes navigation verification
237
+ if (hasNavigationInManualTests(wu.tests)) {
238
+ return { valid: true, errors: [] };
239
+ }
240
+ // No accessibility proof found - this is a blocking error
241
+ errors.push(ACCESSIBILITY_ERROR_MESSAGES.UI_NOT_ACCESSIBLE(wuId));
242
+ return { valid: false, errors };
243
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Validator - Enforces code quality rules from Definition of Done
4
+ *
5
+ * Validates WU completion requirements:
6
+ * - No TODO/FIXME/HACK/XXX comments in production code
7
+ * - No Mock/Stub/Fake classes in production code
8
+ * - Excludes test files from scans
9
+ * - Excludes markdown files from TODO scans (documentation prose)
10
+ *
11
+ * Used by wu:done before creating stamp.
12
+ */
13
+ /**
14
+ * Check if a file path is a test file
15
+ * @param {string} filePath - Path to check
16
+ * @returns {boolean} True if file is a test file
17
+ */
18
+ export declare function isTestFile(filePath: any): boolean;
19
+ /**
20
+ * Check if a file path is a markdown file
21
+ * @param {string} filePath - Path to check
22
+ * @returns {boolean} True if file is a markdown file
23
+ */
24
+ export declare function isMarkdownFile(filePath: any): boolean;
25
+ /**
26
+ * Scan a file for TODO/FIXME/HACK/XXX comments
27
+ * @param {string} filePath - Path to file to scan
28
+ * @returns {{found: boolean, matches: Array<{line: number, text: string, pattern: string}>}}
29
+ */
30
+ export declare function scanFileForTODOs(filePath: any): {
31
+ found: boolean;
32
+ matches: any[];
33
+ };
34
+ /**
35
+ * Scan a file for Mock/Stub/Fake class/function names
36
+ * @param {string} filePath - Path to file to scan
37
+ * @returns {{found: boolean, matches: Array<{line: number, text: string, type: string}>}}
38
+ */
39
+ export declare function scanFileForMocks(filePath: any): {
40
+ found: boolean;
41
+ matches: any[];
42
+ };
43
+ /**
44
+ * Options for validating WU code paths
45
+ */
46
+ export interface ValidateWUCodePathsOptions {
47
+ /** Allow TODO comments (with warning) */
48
+ allowTodos?: boolean;
49
+ /** Optional worktree path to validate files from */
50
+ worktreePath?: string | null;
51
+ }
52
+ /**
53
+ * Validate all code paths for a WU
54
+ * @param {Array<string>} codePaths - Array of file/directory paths from WU YAML
55
+ * @param {ValidateWUCodePathsOptions} options - Validation options
56
+ * @returns {{valid: boolean, errors: Array<string>, warnings: Array<string>}}
57
+ */
58
+ export declare function validateWUCodePaths(codePaths: any, options?: ValidateWUCodePathsOptions): {
59
+ valid: boolean;
60
+ errors: any[];
61
+ warnings: any[];
62
+ };
@@ -0,0 +1,325 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * WU Validator - Enforces code quality rules from Definition of Done
4
+ *
5
+ * Validates WU completion requirements:
6
+ * - No TODO/FIXME/HACK/XXX comments in production code
7
+ * - No Mock/Stub/Fake classes in production code
8
+ * - Excludes test files from scans
9
+ * - Excludes markdown files from TODO scans (documentation prose)
10
+ *
11
+ * Used by wu:done before creating stamp.
12
+ */
13
+ import { readFileSync, existsSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { execSync } from 'node:child_process';
16
+ import { STDIO } from './wu-constants.js';
17
+ /**
18
+ * Check if a file path is a test file
19
+ * @param {string} filePath - Path to check
20
+ * @returns {boolean} True if file is a test file
21
+ */
22
+ export function isTestFile(filePath) {
23
+ const normalized = filePath.replace(/\\/g, '/');
24
+ // Test file patterns
25
+ const testPatterns = [
26
+ /\.test\.(ts|tsx|js|jsx|mjs)$/,
27
+ /\.spec\.(ts|tsx|js|jsx|mjs)$/,
28
+ /__tests__\//,
29
+ /\.test-utils\./,
30
+ /\.mock\./,
31
+ ];
32
+ return testPatterns.some((pattern) => pattern.test(normalized));
33
+ }
34
+ /**
35
+ * Check if a file path is a markdown file
36
+ * @param {string} filePath - Path to check
37
+ * @returns {boolean} True if file is a markdown file
38
+ */
39
+ export function isMarkdownFile(filePath) {
40
+ const normalized = filePath.replace(/\\/g, '/');
41
+ return /\.md$/i.test(normalized);
42
+ }
43
+ /**
44
+ * Scan a file for TODO/FIXME/HACK/XXX comments
45
+ * @param {string} filePath - Path to file to scan
46
+ * @returns {{found: boolean, matches: Array<{line: number, text: string, pattern: string}>}}
47
+ */
48
+ export function scanFileForTODOs(filePath) {
49
+ if (!existsSync(filePath)) {
50
+ return { found: false, matches: [] };
51
+ }
52
+ // Skip test files
53
+ if (isTestFile(filePath)) {
54
+ return { found: false, matches: [] };
55
+ }
56
+ // Skip markdown files (documentation prose often mentions TODO in workflow explanations)
57
+ if (isMarkdownFile(filePath)) {
58
+ return { found: false, matches: [] };
59
+ }
60
+ try {
61
+ const content = readFileSync(filePath, { encoding: 'utf-8' });
62
+ const lines = content.split(/\r?\n/);
63
+ const matches = [];
64
+ // Match TODO/FIXME/HACK/XXX as actionable markers in comments
65
+ // Covers: // TODO:, /* TODO */, * TODO, <!-- TODO -->, # TODO, @todo, etc.
66
+ //
67
+ // WU-1807: Tightened detection to prevent false positives:
68
+ // - Only matches markers at the START of comment text (after comment symbol)
69
+ // - Excludes slash-separated keyword lists like "TODO/FIXME/HACK" (documentation)
70
+ // - Excludes WU-XXX placeholders (not an XXX marker)
71
+ // - Excludes keywords appearing mid-sentence in prose
72
+ //
73
+ // Pattern explanation:
74
+ // - Comment start: ^[\s]*(\/\/|\/\*+|\*|<!--|#)[\s]* captures comment prefixes
75
+ // - Keyword at start: TODO/FIXME/HACK/XXX immediately after comment start
76
+ // - Or @-prefixed: @todo, @fixme, @hack, @xxx anywhere in comment
77
+ /**
78
+ * Check if a line contains an actionable TODO/FIXME/HACK/XXX marker
79
+ * @param {string} line - Line to check
80
+ * @returns {{found: boolean, pattern: string|null}} Match result
81
+ */
82
+ const checkForActionableMarker = (line) => {
83
+ const trimmed = line.trim();
84
+ // Skip lines that are documentation about the patterns themselves
85
+ // These contain keywords as examples, not actionable markers
86
+ if (trimmed.includes('// TODO:,') || trimmed.includes('/* TODO */')) {
87
+ return { found: false, pattern: null };
88
+ }
89
+ if (trimmed.includes('@todo,') || trimmed.includes('@-prefixed:')) {
90
+ return { found: false, pattern: null };
91
+ }
92
+ // Pattern 1: @-prefixed tags at start of JSDoc comment line
93
+ // Matches: * @todo Implement this
94
+ // Does NOT match: // mentions @todo in documentation
95
+ const atTagMatch = trimmed.match(/^\*\s+@(todo|fixme|hack|xxx)\b/i);
96
+ if (atTagMatch) {
97
+ return { found: true, pattern: atTagMatch[1].toUpperCase() };
98
+ }
99
+ // Pattern 2: Keyword at start of comment content
100
+ // Matches: // TODO:, /* TODO, * TODO, # TODO, <!-- TODO
101
+ // Does NOT match: // mentions TODO in workflow, // TODO/FIXME/HACK list
102
+ const commentStartMatch = trimmed.match(/^(?:\/\/|\/\*+|\*|<!--|#)\s*(TODO|FIXME|HACK|XXX)(?::|[\s]|$)/i);
103
+ if (commentStartMatch) {
104
+ // Check it is not followed by / (slash-separated list)
105
+ const afterKeyword = trimmed.slice(trimmed.indexOf(commentStartMatch[1]) + commentStartMatch[1].length);
106
+ if (!afterKeyword.startsWith('/')) {
107
+ return { found: true, pattern: commentStartMatch[1].toUpperCase() };
108
+ }
109
+ }
110
+ // Pattern 3: Keyword in inline comment after code
111
+ // Matches: someCode(); // TODO: fix this
112
+ // Does NOT match: someCode(); // mentions TODO in prose
113
+ // Does NOT match: error('// TODO'); (// inside string literal)
114
+ const inlineCommentMatch = line.match(/\/\/\s*(TODO|FIXME|HACK|XXX)(?::|[\s]|$)/i);
115
+ if (inlineCommentMatch && !line.match(/\/\/\s*(TODO|FIXME|HACK|XXX)\//i)) {
116
+ // Verify the // is not inside a string literal
117
+ const doubleSlashIndex = line.indexOf('//');
118
+ const beforeSlash = line.slice(0, doubleSlashIndex);
119
+ // Count unescaped quotes - if odd number, we're inside a string
120
+ const singleQuotes = (beforeSlash.match(/(?<!\\)'/g) || []).length;
121
+ const doubleQuotes = (beforeSlash.match(/(?<!\\)"/g) || []).length;
122
+ const backticks = (beforeSlash.match(/(?<!\\)`/g) || []).length;
123
+ if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0) {
124
+ // The // is inside a string literal, not a real comment
125
+ return { found: false, pattern: null };
126
+ }
127
+ // Verify the keyword is right after // (not buried in prose)
128
+ const commentPart = line.slice(doubleSlashIndex);
129
+ const keywordIndex = commentPart.search(/\b(TODO|FIXME|HACK|XXX)\b/i);
130
+ // Only flag if keyword appears within first 10 chars of comment
131
+ if (keywordIndex >= 0 && keywordIndex <= 10) {
132
+ return { found: true, pattern: inlineCommentMatch[1].toUpperCase() };
133
+ }
134
+ }
135
+ // Pattern 4: Special check for XXX - must not be preceded by WU-
136
+ // This is handled by patterns above, but add explicit WU-XXX exclusion
137
+ if (trimmed.match(/\bWU-XXX\b/i)) {
138
+ return { found: false, pattern: null };
139
+ }
140
+ return { found: false, pattern: null };
141
+ };
142
+ lines.forEach((line, index) => {
143
+ const lineNumber = index + 1;
144
+ const trimmed = line.trim();
145
+ // Check if line contains a comment marker
146
+ const isComment = /^(\/\/|\/\*|\*|<!--|#)/.test(trimmed) || line.includes('//') || line.includes('/*');
147
+ if (isComment) {
148
+ const result = checkForActionableMarker(line);
149
+ if (result.found) {
150
+ matches.push({
151
+ line: lineNumber,
152
+ text: trimmed,
153
+ pattern: result.pattern,
154
+ });
155
+ }
156
+ }
157
+ });
158
+ return {
159
+ found: matches.length > 0,
160
+ matches,
161
+ };
162
+ }
163
+ catch {
164
+ // If file can't be read, skip it
165
+ return { found: false, matches: [] };
166
+ }
167
+ }
168
+ /**
169
+ * Scan a file for Mock/Stub/Fake class/function names
170
+ * @param {string} filePath - Path to file to scan
171
+ * @returns {{found: boolean, matches: Array<{line: number, text: string, type: string}>}}
172
+ */
173
+ export function scanFileForMocks(filePath) {
174
+ if (!existsSync(filePath)) {
175
+ return { found: false, matches: [] };
176
+ }
177
+ // Skip test files
178
+ if (isTestFile(filePath)) {
179
+ return { found: false, matches: [] };
180
+ }
181
+ try {
182
+ const content = readFileSync(filePath, { encoding: 'utf-8' });
183
+ const lines = content.split(/\r?\n/);
184
+ const matches = [];
185
+ // Match Mock/Stub/Fake/Placeholder in class/function/const names
186
+ const mockPatterns = [
187
+ // Classes: class MockService, export class StubAdapter
188
+ { name: 'Mock', regex: /\b(class|export\s+class)\s+(\w*Mock\w*)/i },
189
+ { name: 'Stub', regex: /\b(class|export\s+class)\s+(\w*Stub\w*)/i },
190
+ { name: 'Fake', regex: /\b(class|export\s+class)\s+(\w*Fake\w*)/i },
191
+ { name: 'Placeholder', regex: /\b(class|export\s+class)\s+(\w*Placeholder\w*)/i },
192
+ // Functions: function mockService, const stubAdapter =
193
+ { name: 'Mock', regex: /\b(function|const|let|var)\s+(\w*mock\w*)/i },
194
+ { name: 'Stub', regex: /\b(function|const|let|var)\s+(\w*stub\w*)/i },
195
+ { name: 'Fake', regex: /\b(function|const|let|var)\s+(\w*fake\w*)/i },
196
+ { name: 'Placeholder', regex: /\b(function|const|let|var)\s+(\w*placeholder\w*)/i },
197
+ ];
198
+ lines.forEach((line, index) => {
199
+ const lineNumber = index + 1;
200
+ mockPatterns.forEach(({ name, regex }) => {
201
+ const match = regex.exec(line);
202
+ if (match) {
203
+ matches.push({
204
+ line: lineNumber,
205
+ text: line.trim(),
206
+ type: name,
207
+ });
208
+ }
209
+ });
210
+ });
211
+ return {
212
+ found: matches.length > 0,
213
+ matches,
214
+ };
215
+ }
216
+ catch {
217
+ // If file can't be read, skip it
218
+ return { found: false, matches: [] };
219
+ }
220
+ }
221
+ /**
222
+ * Get the repo root directory
223
+ * @returns {string} Absolute path to repo root
224
+ */
225
+ function getRepoRoot() {
226
+ try {
227
+ return execSync('git rev-parse --show-toplevel', {
228
+ encoding: 'utf-8',
229
+ stdio: [STDIO.PIPE, STDIO.PIPE, STDIO.IGNORE],
230
+ }).trim();
231
+ }
232
+ catch {
233
+ return process.cwd();
234
+ }
235
+ }
236
+ /**
237
+ * Validate all code paths for a WU
238
+ * @param {Array<string>} codePaths - Array of file/directory paths from WU YAML
239
+ * @param {ValidateWUCodePathsOptions} options - Validation options
240
+ * @returns {{valid: boolean, errors: Array<string>, warnings: Array<string>}}
241
+ */
242
+ export function validateWUCodePaths(codePaths, options = {}) {
243
+ const { allowTodos = false, worktreePath = null } = options;
244
+ const errors = [];
245
+ const warnings = [];
246
+ const repoRoot = worktreePath || getRepoRoot();
247
+ if (!codePaths || codePaths.length === 0) {
248
+ return { valid: true, errors, warnings };
249
+ }
250
+ const todoFindings = [];
251
+ const mockFindings = [];
252
+ // Scan each code path
253
+ for (const codePath of codePaths) {
254
+ const absolutePath = path.join(repoRoot, codePath);
255
+ if (!existsSync(absolutePath)) {
256
+ errors.push(`\n❌ Code path validation failed: File does not exist: ${codePath}\n\n` +
257
+ `This indicates the WU claims to have created/modified a file that doesn't exist.\n` +
258
+ `Either create the file, or remove it from code_paths in the WU YAML.\n`);
259
+ continue;
260
+ }
261
+ // Scan for TODOs
262
+ const todoResult = scanFileForTODOs(absolutePath);
263
+ if (todoResult.found) {
264
+ todoFindings.push({ path: codePath, ...todoResult });
265
+ }
266
+ // Scan for Mocks
267
+ const mockResult = scanFileForMocks(absolutePath);
268
+ if (mockResult.found) {
269
+ mockFindings.push({ path: codePath, ...mockResult });
270
+ }
271
+ }
272
+ // Report TODO findings
273
+ if (todoFindings.length > 0) {
274
+ const message = formatTODOFindings(todoFindings);
275
+ if (allowTodos) {
276
+ warnings.push(message);
277
+ }
278
+ else {
279
+ errors.push(message);
280
+ }
281
+ }
282
+ // Report Mock findings (always warnings, not errors)
283
+ if (mockFindings.length > 0) {
284
+ warnings.push(formatMockFindings(mockFindings));
285
+ }
286
+ return {
287
+ valid: errors.length === 0,
288
+ errors,
289
+ warnings,
290
+ };
291
+ }
292
+ /**
293
+ * Format TODO findings for display
294
+ * @param {Array} findings - TODO findings
295
+ * @returns {string} Formatted message
296
+ */
297
+ function formatTODOFindings(findings) {
298
+ let msg = '\n❌ TODO/FIXME/HACK/XXX comments found in production code:\n';
299
+ findings.forEach(({ path, matches }) => {
300
+ msg += `\n ${path}:\n`;
301
+ matches.forEach(({ line, text }) => {
302
+ msg += ` Line ${line}: ${text}\n`;
303
+ });
304
+ });
305
+ msg += '\nThese indicate incomplete work and must be resolved before WU completion.';
306
+ msg += '\nEither complete the work or use --allow-todo with justification in WU notes.';
307
+ return msg;
308
+ }
309
+ /**
310
+ * Format Mock findings for display
311
+ * @param {Array} findings - Mock findings
312
+ * @returns {string} Formatted message
313
+ */
314
+ function formatMockFindings(findings) {
315
+ let msg = '\n⚠️ Mock/Stub/Fake/Placeholder classes found in production code:\n';
316
+ findings.forEach(({ path, matches }) => {
317
+ msg += `\n ${path}:\n`;
318
+ matches.forEach(({ line, text }) => {
319
+ msg += ` Line ${line}: ${text}\n`;
320
+ });
321
+ });
322
+ msg += '\nThese suggest incomplete implementation (interface ≠ implementation).';
323
+ msg += '\nVerify these are actual implementations, not placeholder code.';
324
+ return msg;
325
+ }