@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,527 @@
1
+ /**
2
+ * Invariants Runner (WU-2252)
3
+ *
4
+ * Validates durable repo invariants from invariants.yml.
5
+ * Runs as the first gate check and also inside wu:done even when --skip-gates is used.
6
+ *
7
+ * Supported invariant types:
8
+ * - required-file: File must exist
9
+ * - forbidden-file: File must NOT exist
10
+ * - mutual-exclusivity: Only one of the listed files may exist
11
+ * - forbidden-pattern: Pattern must not appear in scoped files
12
+ * - required-pattern: Pattern MUST appear at least once in scoped files (WU-2254)
13
+ * - forbidden-import: Files must not import forbidden modules (WU-2254)
14
+ * - wu-automated-tests: WUs with code files must have automated tests (WU-2333)
15
+ *
16
+ * Performance constraints:
17
+ * - Excludes node_modules/, worktrees/, .next/, dist/, .git/ from scanning
18
+ * - For forbidden-pattern rules, scans only the specified scope paths
19
+ *
20
+ * @module tools/lib/invariants-runner
21
+ */
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import path from 'node:path';
24
+ import { globSync } from 'glob';
25
+ import { parseYAML } from './wu-yaml.js';
26
+ // WU-2333: Import automated tests invariant check
27
+ import { checkAutomatedTestsInvariant } from './invariants/check-automated-tests.js';
28
+ /**
29
+ * Invariant type constants
30
+ */
31
+ export const INVARIANT_TYPES = {
32
+ REQUIRED_FILE: 'required-file',
33
+ FORBIDDEN_FILE: 'forbidden-file',
34
+ MUTUAL_EXCLUSIVITY: 'mutual-exclusivity',
35
+ FORBIDDEN_PATTERN: 'forbidden-pattern',
36
+ // WU-2254: New invariant types
37
+ REQUIRED_PATTERN: 'required-pattern',
38
+ FORBIDDEN_IMPORT: 'forbidden-import',
39
+ // WU-2333: WU automated tests invariant
40
+ WU_AUTOMATED_TESTS: 'wu-automated-tests',
41
+ };
42
+ /**
43
+ * Directories to exclude from pattern scanning
44
+ */
45
+ const EXCLUDED_DIRS = ['node_modules', 'worktrees', '.next', 'dist', '.git'];
46
+ /**
47
+ * Custom error class for invariant violations
48
+ */
49
+ export class InvariantError extends Error {
50
+ /** The invariant ID */
51
+ invariantId;
52
+ /**
53
+ * @param {string} invariantId - The invariant ID (e.g., 'INV-001')
54
+ * @param {string} message - Error message
55
+ */
56
+ constructor(invariantId, message) {
57
+ super(message);
58
+ this.name = 'InvariantError';
59
+ this.invariantId = invariantId;
60
+ }
61
+ }
62
+ /**
63
+ * Load invariants from a YAML file
64
+ *
65
+ * @param {string} filePath - Path to invariants.yml
66
+ * @returns {Array<object>} Array of invariant definitions
67
+ * @throws {Error} If file doesn't exist or has invalid YAML
68
+ */
69
+ export function loadInvariants(filePath) {
70
+ if (!existsSync(filePath)) {
71
+ throw new Error(`Invariants file not found: ${filePath}`);
72
+ }
73
+ const content = readFileSync(filePath, 'utf-8');
74
+ let doc;
75
+ try {
76
+ doc = parseYAML(content);
77
+ }
78
+ catch (e) {
79
+ throw new Error(`Invalid YAML in ${filePath}: ${e.message}`);
80
+ }
81
+ if (!doc || !Array.isArray(doc.invariants)) {
82
+ throw new Error(`Invalid invariants.yml: expected 'invariants' array at root`);
83
+ }
84
+ return doc.invariants;
85
+ }
86
+ /**
87
+ * Validate a required-file invariant
88
+ *
89
+ * @param {object} invariant - Invariant definition
90
+ * @param {string} baseDir - Base directory for path resolution
91
+ * @returns {object|null} Violation object if invalid, null if valid
92
+ */
93
+ function validateRequiredFile(invariant, baseDir) {
94
+ const fullPath = path.join(baseDir, invariant.path);
95
+ if (!existsSync(fullPath)) {
96
+ return {
97
+ ...invariant,
98
+ valid: false,
99
+ path: invariant.path,
100
+ };
101
+ }
102
+ return null;
103
+ }
104
+ /**
105
+ * Validate a forbidden-file invariant
106
+ *
107
+ * @param {object} invariant - Invariant definition
108
+ * @param {string} baseDir - Base directory for path resolution
109
+ * @returns {object|null} Violation object if invalid, null if valid
110
+ */
111
+ function validateForbiddenFile(invariant, baseDir) {
112
+ const fullPath = path.join(baseDir, invariant.path);
113
+ if (existsSync(fullPath)) {
114
+ return {
115
+ ...invariant,
116
+ valid: false,
117
+ path: invariant.path,
118
+ };
119
+ }
120
+ return null;
121
+ }
122
+ /**
123
+ * Validate a mutual-exclusivity invariant
124
+ *
125
+ * @param {object} invariant - Invariant definition with paths array
126
+ * @param {string} baseDir - Base directory for path resolution
127
+ * @returns {object|null} Violation object if invalid, null if valid
128
+ */
129
+ function validateMutualExclusivity(invariant, baseDir) {
130
+ const existingPaths = invariant.paths.filter((p) => {
131
+ const fullPath = path.join(baseDir, p);
132
+ return existsSync(fullPath);
133
+ });
134
+ if (existingPaths.length > 1) {
135
+ return {
136
+ ...invariant,
137
+ valid: false,
138
+ existingPaths,
139
+ };
140
+ }
141
+ return null;
142
+ }
143
+ /**
144
+ * Validate a forbidden-pattern invariant
145
+ *
146
+ * @param {object} invariant - Invariant definition with pattern and scope
147
+ * @param {string} baseDir - Base directory for path resolution
148
+ * @returns {object|null} Violation object if invalid, null if valid
149
+ */
150
+ function validateForbiddenPattern(invariant, baseDir) {
151
+ const { pattern, scope } = invariant;
152
+ if (!pattern || !scope || !Array.isArray(scope)) {
153
+ return null; // Skip if misconfigured
154
+ }
155
+ // Build ignore patterns for excluded directories
156
+ const ignorePatterns = EXCLUDED_DIRS.map((dir) => `**/${dir}/**`);
157
+ // Find all files matching the scope
158
+ const matchingFiles = [];
159
+ for (const scopePattern of scope) {
160
+ const files = globSync(scopePattern, {
161
+ cwd: baseDir,
162
+ ignore: ignorePatterns,
163
+ nodir: true,
164
+ });
165
+ // Check each file for the forbidden pattern
166
+ const regex = new RegExp(pattern);
167
+ for (const file of files) {
168
+ const fullPath = path.join(baseDir, file);
169
+ try {
170
+ const content = readFileSync(fullPath, 'utf-8');
171
+ if (regex.test(content)) {
172
+ matchingFiles.push(file);
173
+ }
174
+ }
175
+ catch {
176
+ // Skip files that can't be read (e.g., binary files)
177
+ }
178
+ }
179
+ }
180
+ if (matchingFiles.length > 0) {
181
+ return {
182
+ ...invariant,
183
+ valid: false,
184
+ matchingFiles,
185
+ };
186
+ }
187
+ return null;
188
+ }
189
+ /**
190
+ * WU-2254: Validate a required-pattern invariant
191
+ *
192
+ * Semantics: PASS if the regex matches at least once across the scoped files.
193
+ * This is the inverse of forbidden-pattern - we WANT to find the pattern.
194
+ *
195
+ * @param {object} invariant - Invariant definition with pattern and scope
196
+ * @param {string} baseDir - Base directory for path resolution
197
+ * @returns {object|null} Violation object if pattern NOT found, null if found
198
+ */
199
+ function validateRequiredPattern(invariant, baseDir) {
200
+ const { pattern, scope } = invariant;
201
+ if (!pattern || !scope || !Array.isArray(scope)) {
202
+ return null; // Skip if misconfigured
203
+ }
204
+ // Build ignore patterns for excluded directories
205
+ const ignorePatterns = EXCLUDED_DIRS.map((dir) => `**/${dir}/**`);
206
+ // Check if pattern exists in any file matching the scope
207
+ const regex = new RegExp(pattern);
208
+ for (const scopePattern of scope) {
209
+ const files = globSync(scopePattern, {
210
+ cwd: baseDir,
211
+ ignore: ignorePatterns,
212
+ nodir: true,
213
+ });
214
+ for (const file of files) {
215
+ const fullPath = path.join(baseDir, file);
216
+ try {
217
+ const content = readFileSync(fullPath, 'utf-8');
218
+ if (regex.test(content)) {
219
+ // Pattern found - invariant passes
220
+ return null;
221
+ }
222
+ }
223
+ catch {
224
+ // Skip files that can't be read (e.g., binary files)
225
+ }
226
+ }
227
+ }
228
+ // Pattern not found in any file - invariant fails
229
+ return {
230
+ ...invariant,
231
+ valid: false,
232
+ patternNotFound: true,
233
+ };
234
+ }
235
+ /**
236
+ * WU-2254: Validate a forbidden-import invariant
237
+ *
238
+ * Detects import/require/re-export statements referencing forbidden modules.
239
+ * Supports:
240
+ * - ESM static import: import { x } from 'module'
241
+ * - ESM dynamic import: await import('module')
242
+ * - ESM re-export: export { x } from 'module'
243
+ * - CommonJS require: require('module')
244
+ *
245
+ * @param {object} invariant - Invariant definition with from glob and cannot_import array
246
+ * @param {string} baseDir - Base directory for path resolution
247
+ * @returns {object|null} Violation object if forbidden imports found, null otherwise
248
+ */
249
+ function validateForbiddenImport(invariant, baseDir) {
250
+ const { from, cannot_import } = invariant;
251
+ if (!from || !cannot_import || !Array.isArray(cannot_import)) {
252
+ return null; // Skip if misconfigured
253
+ }
254
+ // Build ignore patterns for excluded directories
255
+ const ignorePatterns = EXCLUDED_DIRS.map((dir) => `**/${dir}/**`);
256
+ // Find all files matching the 'from' glob
257
+ const files = globSync(from, {
258
+ cwd: baseDir,
259
+ ignore: ignorePatterns,
260
+ nodir: true,
261
+ });
262
+ const violatingFiles = [];
263
+ const violatingImports = {};
264
+ // Build regex patterns for detecting imports of forbidden modules
265
+ // We escape special regex characters in module names
266
+ const forbiddenModulePatterns = cannot_import.map((mod) => {
267
+ const escapedMod = mod.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
268
+ // Match:
269
+ // - import ... from 'module' or "module"
270
+ // - export ... from 'module' or "module"
271
+ // - require('module') or require("module")
272
+ // - import('module') or import("module") for dynamic imports
273
+ return new RegExp(`(?:` +
274
+ `import\\s+[^;]*from\\s*['"]${escapedMod}['"]|` + // static import
275
+ `export\\s+[^;]*from\\s*['"]${escapedMod}['"]|` + // re-export
276
+ `require\\s*\\(\\s*['"]${escapedMod}['"]\\s*\\)|` + // require()
277
+ `import\\s*\\(\\s*['"]${escapedMod}['"]\\s*\\)` + // dynamic import()
278
+ `)`);
279
+ });
280
+ for (const file of files) {
281
+ const fullPath = path.join(baseDir, file);
282
+ try {
283
+ const content = readFileSync(fullPath, 'utf-8');
284
+ // Check each forbidden module pattern
285
+ for (let i = 0; i < forbiddenModulePatterns.length; i++) {
286
+ const pattern = forbiddenModulePatterns[i];
287
+ const moduleName = cannot_import[i];
288
+ if (pattern.test(content)) {
289
+ if (!violatingFiles.includes(file)) {
290
+ violatingFiles.push(file);
291
+ }
292
+ violatingImports[moduleName] = (violatingImports[moduleName] || 0) + 1;
293
+ }
294
+ }
295
+ }
296
+ catch {
297
+ // Skip files that can't be read
298
+ }
299
+ }
300
+ if (violatingFiles.length > 0) {
301
+ return {
302
+ ...invariant,
303
+ valid: false,
304
+ violatingFiles,
305
+ violatingImports,
306
+ };
307
+ }
308
+ return null;
309
+ }
310
+ function validateWUAutomatedTests(invariant, baseDir, context = {}) {
311
+ const { wuId } = context;
312
+ const result = checkAutomatedTestsInvariant({ baseDir, wuId });
313
+ if (!result.valid && result.violations.length > 0) {
314
+ // Return first violation with invariant metadata merged in
315
+ // (checkAutomatedTestsInvariant returns array, we merge with registry invariant)
316
+ return {
317
+ ...invariant,
318
+ valid: false,
319
+ wuViolations: result.violations,
320
+ };
321
+ }
322
+ return null;
323
+ }
324
+ export function validateInvariants(invariants, options = {}) {
325
+ const { baseDir = process.cwd(), wuId } = options;
326
+ const violations = [];
327
+ for (const invariant of invariants) {
328
+ let violation = null;
329
+ switch (invariant.type) {
330
+ case INVARIANT_TYPES.REQUIRED_FILE:
331
+ violation = validateRequiredFile(invariant, baseDir);
332
+ break;
333
+ case INVARIANT_TYPES.FORBIDDEN_FILE:
334
+ violation = validateForbiddenFile(invariant, baseDir);
335
+ break;
336
+ case INVARIANT_TYPES.MUTUAL_EXCLUSIVITY:
337
+ violation = validateMutualExclusivity(invariant, baseDir);
338
+ break;
339
+ case INVARIANT_TYPES.FORBIDDEN_PATTERN:
340
+ violation = validateForbiddenPattern(invariant, baseDir);
341
+ break;
342
+ // WU-2254: New invariant types
343
+ case INVARIANT_TYPES.REQUIRED_PATTERN:
344
+ violation = validateRequiredPattern(invariant, baseDir);
345
+ break;
346
+ case INVARIANT_TYPES.FORBIDDEN_IMPORT:
347
+ violation = validateForbiddenImport(invariant, baseDir);
348
+ break;
349
+ // WU-2333: WU automated tests invariant
350
+ // WU-2425: Pass wuId for scoped validation
351
+ case INVARIANT_TYPES.WU_AUTOMATED_TESTS:
352
+ violation = validateWUAutomatedTests(invariant, baseDir, { wuId });
353
+ break;
354
+ default:
355
+ // Unknown invariant type - skip with warning
356
+ console.warn(`[invariants] Unknown invariant type: ${invariant.type} (${invariant.id})`);
357
+ }
358
+ if (violation) {
359
+ violations.push(violation);
360
+ }
361
+ }
362
+ return {
363
+ valid: violations.length === 0,
364
+ violations,
365
+ };
366
+ }
367
+ /**
368
+ * Format file-related violation details.
369
+ * @param {object} violation - Violation object
370
+ * @returns {string[]} Formatted lines
371
+ */
372
+ function formatFileViolationDetails(violation) {
373
+ const lines = [];
374
+ if (violation.path) {
375
+ lines.push(`Path: ${violation.path}`);
376
+ }
377
+ if (violation.existingPaths) {
378
+ lines.push(`Conflicting files: ${violation.existingPaths.join(', ')}`);
379
+ }
380
+ if (violation.matchingFiles) {
381
+ lines.push(`Files with forbidden pattern: ${violation.matchingFiles.join(', ')}`);
382
+ }
383
+ return lines;
384
+ }
385
+ /**
386
+ * Format forbidden-import violation details.
387
+ * @param {object} violation - Violation object
388
+ * @returns {string[]} Formatted lines
389
+ */
390
+ function formatImportViolationDetails(violation) {
391
+ const lines = [];
392
+ if (violation.from) {
393
+ lines.push(`From: ${violation.from}`);
394
+ }
395
+ if (violation.cannot_import) {
396
+ lines.push(`Cannot import: ${violation.cannot_import.join(', ')}`);
397
+ }
398
+ if (violation.violatingFiles) {
399
+ lines.push(`Files with forbidden imports: ${violation.violatingFiles.join(', ')}`);
400
+ }
401
+ if (violation.violatingImports) {
402
+ const imports = Object.entries(violation.violatingImports)
403
+ .map(([mod, count]) => `${mod} (${count} occurrence${count > 1 ? 's' : ''})`)
404
+ .join(', ');
405
+ lines.push(`Forbidden imports found: ${imports}`);
406
+ }
407
+ return lines;
408
+ }
409
+ /**
410
+ * Format pattern-related violation details.
411
+ * @param {object} violation - Violation object
412
+ * @returns {string[]} Formatted lines
413
+ */
414
+ function formatPatternViolationDetails(violation) {
415
+ const lines = [];
416
+ if (violation.patternNotFound) {
417
+ lines.push(`Required pattern not found: ${violation.pattern}`);
418
+ if (violation.scope) {
419
+ lines.push(`Searched in: ${violation.scope.join(', ')}`);
420
+ }
421
+ }
422
+ return lines;
423
+ }
424
+ /**
425
+ * WU-2333: Format wu-automated-tests violation details.
426
+ * @param {object} violation - Violation object
427
+ * @returns {string[]} Formatted lines
428
+ */
429
+ function formatWUAutomatedTestsViolationDetails(violation) {
430
+ const lines = [];
431
+ if (violation.wuViolations) {
432
+ lines.push(`WUs missing automated tests:`);
433
+ for (const wuViolation of violation.wuViolations) {
434
+ lines.push(` - ${wuViolation.wuId}`);
435
+ if (wuViolation.codeFiles && wuViolation.codeFiles.length > 0) {
436
+ lines.push(` Code files: ${wuViolation.codeFiles.join(', ')}`);
437
+ }
438
+ }
439
+ }
440
+ return lines;
441
+ }
442
+ /**
443
+ * Format type-specific details for a violation.
444
+ * Extracted to reduce cognitive complexity of formatInvariantError.
445
+ *
446
+ * @param {object} violation - Violation object
447
+ * @returns {string[]} Array of formatted detail lines
448
+ */
449
+ function formatViolationDetails(violation) {
450
+ return [
451
+ ...formatFileViolationDetails(violation),
452
+ ...formatImportViolationDetails(violation),
453
+ ...formatPatternViolationDetails(violation),
454
+ ...formatWUAutomatedTestsViolationDetails(violation),
455
+ ];
456
+ }
457
+ /**
458
+ * Format an invariant violation for display
459
+ *
460
+ * @param {object} violation - Violation object from validateInvariants
461
+ * @returns {string} Formatted error message
462
+ */
463
+ export function formatInvariantError(violation) {
464
+ const lines = [
465
+ `INVARIANT VIOLATION: ${violation.id}`,
466
+ `Type: ${violation.type}`,
467
+ `Description: ${violation.description}`,
468
+ ...formatViolationDetails(violation),
469
+ ];
470
+ // Add the actionable message
471
+ if (violation.message) {
472
+ lines.push('');
473
+ lines.push(`Action: ${violation.message}`);
474
+ }
475
+ return lines.join('\n');
476
+ }
477
+ export function runInvariants(options = {}) {
478
+ const { configPath = 'tools/invariants.yml', baseDir = process.cwd(), silent = false, wuId, } = options;
479
+ const fullConfigPath = path.isAbsolute(configPath) ? configPath : path.join(baseDir, configPath);
480
+ // Check if config exists - if not, pass (no invariants defined)
481
+ if (!existsSync(fullConfigPath)) {
482
+ if (!silent) {
483
+ console.log('[invariants] No tools/invariants.yml found - skipping');
484
+ }
485
+ return { success: true, violations: [], formatted: '' };
486
+ }
487
+ try {
488
+ const invariants = loadInvariants(fullConfigPath);
489
+ if (invariants.length === 0) {
490
+ if (!silent) {
491
+ console.log('[invariants] No invariants defined - skipping');
492
+ }
493
+ return { success: true, violations: [], formatted: '' };
494
+ }
495
+ // WU-2425: Pass wuId for scoped validation
496
+ const result = validateInvariants(invariants, { baseDir, wuId });
497
+ if (result.valid) {
498
+ if (!silent) {
499
+ console.log(`[invariants] All ${invariants.length} invariants passed`);
500
+ }
501
+ return { success: true, violations: [], formatted: '' };
502
+ }
503
+ // Format violations
504
+ const formatted = result.violations.map(formatInvariantError).join('\n\n');
505
+ if (!silent) {
506
+ console.error('[invariants] FAILED - violations detected:');
507
+ console.error('');
508
+ console.error(formatted);
509
+ }
510
+ return {
511
+ success: false,
512
+ violations: result.violations,
513
+ formatted,
514
+ };
515
+ }
516
+ catch (e) {
517
+ const error = `[invariants] Error: ${e.message}`;
518
+ if (!silent) {
519
+ console.error(error);
520
+ }
521
+ return {
522
+ success: false,
523
+ violations: [],
524
+ formatted: error,
525
+ };
526
+ }
527
+ }
@@ -0,0 +1,50 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Lane Occupancy Checker
4
+ *
5
+ * Enforces one-WU-per-lane rule by checking status.md for active WUs in a given lane.
6
+ * Used by wu-claim.mjs and wu-unblock.mjs to prevent WIP violations.
7
+ */
8
+ import { getSubLanesForParent } from './lane-inference.js';
9
+ interface ValidateLaneOptions {
10
+ strict?: boolean;
11
+ }
12
+ interface ValidateLaneResult {
13
+ valid: boolean;
14
+ parent: string;
15
+ error: string | null;
16
+ }
17
+ interface CheckLaneFreeResult {
18
+ free: boolean;
19
+ occupiedBy: string | null;
20
+ error: string | null;
21
+ }
22
+ export { getSubLanesForParent };
23
+ /**
24
+ * Extract parent lane from sub-lane or parent-only format
25
+ * @param {string} lane - Lane name (e.g., "Operations: Tooling" or "Operations")
26
+ * @returns {string} Parent lane name
27
+ */
28
+ export declare function extractParent(lane: string): string;
29
+ /**
30
+ * Validation mode options
31
+ * @typedef {Object} ValidateLaneOptions
32
+ * @property {boolean} [strict=true] - When true, throws error for parent-only lanes with taxonomy.
33
+ * When false, only warns (for existing WU validation).
34
+ */
35
+ /**
36
+ * Validate lane format and parent existence
37
+ * @param {string} lane - Lane name (e.g., "Operations: Tooling" or "Operations")
38
+ * @param {string} configPath - Path to config file (optional, defaults to project root)
39
+ * @param {ValidateLaneOptions} options - Validation options
40
+ * @returns {{ valid: boolean, parent: string, error: string | null }}
41
+ */
42
+ export declare function validateLaneFormat(lane: string, configPath?: string | null, options?: ValidateLaneOptions): ValidateLaneResult;
43
+ /**
44
+ * Check if a lane is free (no in_progress WU currently in that lane)
45
+ * @param {string} statusPath - Path to status.md
46
+ * @param {string} lane - Lane name (e.g., "Operations", "Intelligence")
47
+ * @param {string} wuid - WU ID being claimed (e.g., "WU-419")
48
+ * @returns {{ free: boolean, occupiedBy: string | null, error: string | null }}
49
+ */
50
+ export declare function checkLaneFree(statusPath: string, lane: string, wuid: string): CheckLaneFreeResult;