@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,319 @@
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 { existsSync, readFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+ import yaml from 'js-yaml';
11
+ import { getSubLanesForParent } from './lane-inference.js';
12
+ import { createError, ErrorCodes } from './error-handler.js';
13
+ import { isInProgressHeader, WU_LINK_PATTERN } from './constants/backlog-patterns.js';
14
+ import { CONFIG_FILES, STRING_LITERALS } from './wu-constants.js';
15
+ import { WU_PATHS } from './wu-paths.js';
16
+ import { findProjectRoot } from './lumenflow-config.js';
17
+ // Re-export for test access
18
+ export { getSubLanesForParent };
19
+ /** Log prefix for lane-checker messages */
20
+ const PREFIX = '[lane-checker]';
21
+ /** Status.md marker for empty In Progress section */
22
+ const NO_ITEMS_MARKER = 'No items currently in progress';
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 function extractParent(lane) {
29
+ const trimmed = lane.trim();
30
+ const colonIndex = trimmed.indexOf(':');
31
+ if (colonIndex === -1) {
32
+ // Parent-only format
33
+ return trimmed;
34
+ }
35
+ // Sub-lane format: extract parent before colon
36
+ return trimmed.substring(0, colonIndex).trim();
37
+ }
38
+ /**
39
+ * Check if a parent lane has sub-lane taxonomy defined
40
+ * @param {string} parent - Parent lane name
41
+ * @returns {boolean} True if parent has sub-lanes in lane-inference config
42
+ */
43
+ function hasSubLaneTaxonomy(parent) {
44
+ const projectRoot = findProjectRoot();
45
+ const taxonomyPath = path.join(projectRoot, CONFIG_FILES.LANE_INFERENCE);
46
+ if (!existsSync(taxonomyPath)) {
47
+ return false;
48
+ }
49
+ try {
50
+ const taxonomyContent = readFileSync(taxonomyPath, { encoding: 'utf-8' });
51
+ const taxonomy = yaml.load(taxonomyContent);
52
+ // Check if parent exists as top-level key in taxonomy
53
+ const normalizedParent = parent.trim();
54
+ return Object.keys(taxonomy).some((key) => key.toLowerCase().trim() === normalizedParent.toLowerCase());
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /**
61
+ * Check if a sub-lane exists for a given parent in lane-inference config
62
+ * @param {string} parent - Parent lane name
63
+ * @param {string} subdomain - Sub-lane name
64
+ * @returns {boolean} True if sub-lane exists
65
+ */
66
+ function isValidSubLane(parent, subdomain) {
67
+ const projectRoot = findProjectRoot();
68
+ const taxonomyPath = path.join(projectRoot, CONFIG_FILES.LANE_INFERENCE);
69
+ if (!existsSync(taxonomyPath)) {
70
+ return false;
71
+ }
72
+ try {
73
+ const taxonomyContent = readFileSync(taxonomyPath, { encoding: 'utf-8' });
74
+ const taxonomy = yaml.load(taxonomyContent);
75
+ // Find parent key (case-insensitive)
76
+ const normalizedParent = parent.trim().toLowerCase();
77
+ const parentKey = Object.keys(taxonomy).find((key) => key.toLowerCase().trim() === normalizedParent);
78
+ if (!parentKey) {
79
+ return false;
80
+ }
81
+ // Check if subdomain exists under parent
82
+ const subLanes = taxonomy[parentKey];
83
+ if (!subLanes || typeof subLanes !== 'object') {
84
+ return false;
85
+ }
86
+ // Exact match on subdomain (case-sensitive per Codex spec)
87
+ return Object.keys(subLanes).includes(subdomain.trim());
88
+ }
89
+ catch {
90
+ return false;
91
+ }
92
+ }
93
+ /**
94
+ * Count occurrences of a character in a string
95
+ * @param {string} str - String to search
96
+ * @param {string} char - Character to count
97
+ * @returns {number} Number of occurrences
98
+ */
99
+ function countChar(str, char) {
100
+ let count = 0;
101
+ for (const c of str) {
102
+ if (c === char)
103
+ count++;
104
+ }
105
+ return count;
106
+ }
107
+ /** Lane format separator character */
108
+ const LANE_SEPARATOR = ':';
109
+ /** Space character for format validation */
110
+ const SPACE = ' ';
111
+ /**
112
+ * Validation mode options
113
+ * @typedef {Object} ValidateLaneOptions
114
+ * @property {boolean} [strict=true] - When true, throws error for parent-only lanes with taxonomy.
115
+ * When false, only warns (for existing WU validation).
116
+ */
117
+ /**
118
+ * Validate lane format and parent existence
119
+ * @param {string} lane - Lane name (e.g., "Operations: Tooling" or "Operations")
120
+ * @param {string} configPath - Path to config file (optional, defaults to project root)
121
+ * @param {ValidateLaneOptions} options - Validation options
122
+ * @returns {{ valid: boolean, parent: string, error: string | null }}
123
+ */
124
+ export function validateLaneFormat(lane, configPath = null, options = {}) {
125
+ const { strict = true } = options;
126
+ const trimmed = lane.trim();
127
+ // Check for multiple colons
128
+ const colonCount = countChar(trimmed, LANE_SEPARATOR);
129
+ if (colonCount > 1) {
130
+ throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" contains multiple colons. Expected format: "Parent: Subdomain" or "Parent"`, { lane });
131
+ }
132
+ // Check for colon
133
+ const colonIndex = trimmed.indexOf(LANE_SEPARATOR);
134
+ if (colonIndex !== -1) {
135
+ // Sub-lane format validation
136
+ // Check for space before colon
137
+ if (colonIndex > 0 && trimmed[colonIndex - 1] === SPACE) {
138
+ throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" has space before colon. Expected format: "Parent: Subdomain" (space AFTER colon only)`, { lane });
139
+ }
140
+ // Check for space after colon
141
+ if (colonIndex + 1 >= trimmed.length || trimmed[colonIndex + 1] !== SPACE) {
142
+ throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" is missing space after colon. Expected format: "Parent: Subdomain"`, { lane });
143
+ }
144
+ // Extract parent and subdomain (colonIndex + 2 = skip colon and space)
145
+ const parent = trimmed.substring(0, colonIndex).trim();
146
+ const subdomain = trimmed.substring(colonIndex + LANE_SEPARATOR.length + SPACE.length).trim();
147
+ // Validate parent exists in config
148
+ if (!isValidParentLane(parent, configPath)) {
149
+ throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${parent}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { parent, lane });
150
+ }
151
+ // Validate sub-lane exists in taxonomy
152
+ if (hasSubLaneTaxonomy(parent)) {
153
+ // Parent has taxonomy - validate sub-lane
154
+ if (!isValidSubLane(parent, subdomain)) {
155
+ const validSubLanes = getSubLanesForParent(parent);
156
+ throw createError(ErrorCodes.INVALID_LANE, `Unknown sub-lane: "${subdomain}" for parent lane "${parent}".\n\n` +
157
+ `Valid sub-lanes: ${validSubLanes.join(', ')}`, { parent, subdomain, validSubLanes });
158
+ }
159
+ }
160
+ else {
161
+ // Parent has no taxonomy - reject sub-lane format
162
+ throw createError(ErrorCodes.INVALID_LANE, `Parent lane "${parent}" does not support sub-lanes. Use parent-only format or extend ${CONFIG_FILES.LANE_INFERENCE}.`, { parent, lane });
163
+ }
164
+ return { valid: true, parent, error: null };
165
+ }
166
+ else {
167
+ // Parent-only format
168
+ if (!isValidParentLane(trimmed, configPath)) {
169
+ throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${trimmed}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { lane: trimmed });
170
+ }
171
+ // Block if parent has sub-lane taxonomy (sub-lane required)
172
+ if (hasSubLaneTaxonomy(trimmed)) {
173
+ const validSubLanes = getSubLanesForParent(trimmed);
174
+ const message = `Parent-only lane "${trimmed}" blocked. Sub-lane required. ` +
175
+ `Valid: ${validSubLanes.join(', ')}. ` +
176
+ `Format: "${trimmed}: <sublane>"`;
177
+ if (strict) {
178
+ // Strict mode (default): throw error for new WUs
179
+ throw createError(ErrorCodes.INVALID_LANE, message, { lane: trimmed, validSubLanes });
180
+ }
181
+ else {
182
+ // Non-strict mode: warn only for existing WU validation
183
+ console.warn(`${PREFIX} ⚠️ ${message}`);
184
+ }
185
+ }
186
+ return { valid: true, parent: trimmed, error: null };
187
+ }
188
+ }
189
+ /**
190
+ * Check if a parent lane exists in LumenFlow config
191
+ * @param {string} parent - Parent lane name to check
192
+ * @param {string} configPath - Path to config file (optional)
193
+ * @returns {boolean} True if parent lane exists
194
+ */
195
+ function isValidParentLane(parent, configPath = null) {
196
+ // Determine config path
197
+ let resolvedConfigPath = configPath;
198
+ if (!resolvedConfigPath) {
199
+ const projectRoot = findProjectRoot();
200
+ resolvedConfigPath = path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
201
+ }
202
+ // Read and parse config
203
+ if (!existsSync(resolvedConfigPath)) {
204
+ throw createError(ErrorCodes.FILE_NOT_FOUND, `Config file not found: ${resolvedConfigPath}`, {
205
+ path: resolvedConfigPath,
206
+ });
207
+ }
208
+ const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
209
+ const config = yaml.load(configContent);
210
+ // Extract all lane names - handle both flat array and nested engineering/business formats
211
+ const allLanes = [];
212
+ if (config.lanes) {
213
+ if (Array.isArray(config.lanes)) {
214
+ // Flat array format: lanes: [{name: "Core"}, {name: "CLI"}, ...]
215
+ allLanes.push(...config.lanes.map((l) => l.name));
216
+ }
217
+ else {
218
+ // Nested format: lanes: {engineering: [...], business: [...]}
219
+ if (config.lanes.engineering) {
220
+ allLanes.push(...config.lanes.engineering.map((l) => l.name));
221
+ }
222
+ if (config.lanes.business) {
223
+ allLanes.push(...config.lanes.business.map((l) => l.name));
224
+ }
225
+ }
226
+ }
227
+ // Case-insensitive comparison
228
+ const normalizedParent = parent.toLowerCase().trim();
229
+ return allLanes.some((lane) => lane.toLowerCase().trim() === normalizedParent);
230
+ }
231
+ /**
232
+ * Check if a lane is free (no in_progress WU currently in that lane)
233
+ * @param {string} statusPath - Path to status.md
234
+ * @param {string} lane - Lane name (e.g., "Operations", "Intelligence")
235
+ * @param {string} wuid - WU ID being claimed (e.g., "WU-419")
236
+ * @returns {{ free: boolean, occupiedBy: string | null, error: string | null }}
237
+ */
238
+ export function checkLaneFree(statusPath, lane, wuid) {
239
+ /** Section heading marker for H2 headings */
240
+ const SECTION_HEADING_PREFIX = '## ';
241
+ try {
242
+ // Read status.md
243
+ if (!existsSync(statusPath)) {
244
+ return { free: false, occupiedBy: null, error: `status.md not found: ${statusPath}` };
245
+ }
246
+ const content = readFileSync(statusPath, { encoding: 'utf-8' });
247
+ const lines = content.split(/\r?\n/);
248
+ // Find "## In Progress" section
249
+ const inProgressIdx = lines.findIndex((l) => isInProgressHeader(l));
250
+ if (inProgressIdx === -1) {
251
+ return {
252
+ free: false,
253
+ occupiedBy: null,
254
+ error: 'Could not find "## In Progress" section in status.md',
255
+ };
256
+ }
257
+ // Find end of In Progress section (next ## heading or end of file)
258
+ let endIdx = lines
259
+ .slice(inProgressIdx + 1)
260
+ .findIndex((l) => l.startsWith(SECTION_HEADING_PREFIX));
261
+ if (endIdx === -1)
262
+ endIdx = lines.length - inProgressIdx - 1;
263
+ else
264
+ endIdx = inProgressIdx + 1 + endIdx;
265
+ // Extract WU links from In Progress section
266
+ const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
267
+ // Check for "No items" marker
268
+ if (section.includes(NO_ITEMS_MARKER)) {
269
+ return { free: true, occupiedBy: null, error: null };
270
+ }
271
+ // Extract WU IDs from links like [WU-334 — Title](wu/WU-334.yaml)
272
+ WU_LINK_PATTERN.lastIndex = 0; // Reset global regex state
273
+ const matches = [...section.matchAll(WU_LINK_PATTERN)];
274
+ if (matches.length === 0) {
275
+ return { free: true, occupiedBy: null, error: null };
276
+ }
277
+ // Get project root from statusPath (docs/04-operations/tasks/status.md)
278
+ // Use path.dirname 4 times: status.md -> tasks -> 04-operations -> docs -> root
279
+ const projectRoot = path.dirname(path.dirname(path.dirname(path.dirname(statusPath))));
280
+ for (const match of matches) {
281
+ const activeWuid = match[1]; // e.g., "WU-334"
282
+ // Skip if it's the same WU we're trying to claim (shouldn't happen, but be safe)
283
+ if (activeWuid === wuid)
284
+ continue;
285
+ // Use WU_PATHS to build the path consistently
286
+ const wuPath = path.join(projectRoot, WU_PATHS.WU(activeWuid));
287
+ if (!existsSync(wuPath)) {
288
+ console.warn(`${PREFIX} Warning: ${activeWuid} referenced in status.md but ${wuPath} not found`);
289
+ continue;
290
+ }
291
+ try {
292
+ const wuContent = readFileSync(wuPath, { encoding: 'utf-8' });
293
+ const wuDoc = yaml.load(wuContent);
294
+ if (!wuDoc || !wuDoc.lane) {
295
+ console.warn(`${PREFIX} Warning: ${activeWuid} has no lane field`);
296
+ continue;
297
+ }
298
+ // Normalize lane names for comparison (case-insensitive, trim whitespace)
299
+ const activeLane = wuDoc.lane.toString().trim().toLowerCase();
300
+ const targetLane = lane.toString().trim().toLowerCase();
301
+ if (activeLane === targetLane) {
302
+ // Lane is occupied!
303
+ return { free: false, occupiedBy: activeWuid, error: null };
304
+ }
305
+ }
306
+ catch (e) {
307
+ const errMessage = e instanceof Error ? e.message : String(e);
308
+ console.warn(`${PREFIX} Warning: Failed to parse ${activeWuid} YAML: ${errMessage}`);
309
+ continue;
310
+ }
311
+ }
312
+ // No WUs found in target lane
313
+ return { free: true, occupiedBy: null, error: null };
314
+ }
315
+ catch (error) {
316
+ const errMessage = error instanceof Error ? error.message : String(error);
317
+ return { free: false, occupiedBy: null, error: `Unexpected error: ${errMessage}` };
318
+ }
319
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Lane Inference Module (WU-906)
3
+ *
4
+ * Provides automated sub-lane suggestion based on WU code_paths and description.
5
+ * Uses config-driven pattern matching with confidence scoring.
6
+ *
7
+ * Inference is suggestion only (not enforcement). Track accuracy for future tuning.
8
+ *
9
+ * Uses industry-standard libraries:
10
+ * - micromatch for robust glob matching (28x faster than minimatch)
11
+ * - yaml for modern YAML parsing (actively maintained, YAML 1.2 compliant)
12
+ */
13
+ import { extractParent } from './lane-checker.js';
14
+ /**
15
+ * Infer sub-lane from WU code paths and description
16
+ * @param {string[]} codePaths - Array of file paths modified/created by this WU
17
+ * @param {string} description - WU description/title text
18
+ * @param {string|null} configPath - Optional path to config file
19
+ * @returns {{ lane: string, confidence: number }} Suggested sub-lane and confidence (0-100)
20
+ * @throws {Error} If config cannot be loaded
21
+ */
22
+ export declare function inferSubLane(codePaths: any, description: any, configPath?: any): {
23
+ lane: any;
24
+ confidence: any;
25
+ };
26
+ /**
27
+ * Get all valid sub-lanes from config
28
+ * @param {string|null} configPath - Optional path to config file
29
+ * @returns {string[]} Array of all sub-lane names (format: "Parent: Subdomain")
30
+ */
31
+ export declare function getAllSubLanes(configPath?: any): any[];
32
+ /**
33
+ * Get valid sub-lanes for a specific parent lane
34
+ * @param {string} parent - Parent lane name (e.g., "Operations", "Core Systems")
35
+ * @param {string|null} configPath - Optional path to config file
36
+ * @returns {string[]} Array of sub-lane names for that parent (e.g., ["Tooling", "CI/CD", ...])
37
+ */
38
+ export declare function getSubLanesForParent(parent: any, configPath?: any): string[];
39
+ export { extractParent };
@@ -0,0 +1,195 @@
1
+ /**
2
+ * Lane Inference Module (WU-906)
3
+ *
4
+ * Provides automated sub-lane suggestion based on WU code_paths and description.
5
+ * Uses config-driven pattern matching with confidence scoring.
6
+ *
7
+ * Inference is suggestion only (not enforcement). Track accuracy for future tuning.
8
+ *
9
+ * Uses industry-standard libraries:
10
+ * - micromatch for robust glob matching (28x faster than minimatch)
11
+ * - yaml for modern YAML parsing (actively maintained, YAML 1.2 compliant)
12
+ */
13
+ import { readFileSync, existsSync } from 'node:fs';
14
+ import path from 'node:path';
15
+ import { fileURLToPath } from 'node:url';
16
+ import YAML from 'yaml'; // Modern YAML library (not js-yaml)
17
+ import micromatch from 'micromatch'; // Industry-standard glob matching (CommonJS)
18
+ import { extractParent } from './lane-checker.js'; // Shared utility (WU-1137: consolidation)
19
+ import { createError, ErrorCodes } from './error-handler.js';
20
+ import { WEIGHTS, CONFIDENCE } from './wu-validation-constants.js';
21
+ /**
22
+ * Load lane inference config from project root
23
+ * @param {string|null} configPath - Optional path to config file (defaults to project root)
24
+ * @returns {object} Parsed config object
25
+ * @throws {Error} If config file not found or YAML parsing fails
26
+ */
27
+ function loadConfig(configPath = null) {
28
+ if (!configPath) {
29
+ // Default to project root
30
+ const currentDir = path.dirname(fileURLToPath(import.meta.url));
31
+ const projectRoot = path.resolve(currentDir, '../..');
32
+ configPath = path.join(projectRoot, '.lumenflow.lane-inference.yaml');
33
+ }
34
+ if (!existsSync(configPath)) {
35
+ throw createError(ErrorCodes.FILE_NOT_FOUND, `Lane inference config not found: ${configPath}\n\nRun WU-906 to create infrastructure files.`, { path: configPath });
36
+ }
37
+ try {
38
+ const configContent = readFileSync(configPath, { encoding: 'utf-8' });
39
+ return YAML.parse(configContent);
40
+ }
41
+ catch (err) {
42
+ throw createError(ErrorCodes.YAML_PARSE_ERROR, `Failed to parse lane inference config: ${configPath}\n\n${err.message}\n\n` +
43
+ `Ensure config is valid YAML.`, { path: configPath, originalError: err.message });
44
+ }
45
+ }
46
+ /**
47
+ * Check if a code path matches a glob pattern using micromatch
48
+ * @param {string} codePath - Actual code path from WU
49
+ * @param {string} pattern - Glob pattern from config (e.g., "tools/**", "*.ts")
50
+ * @returns {boolean} True if path matches pattern
51
+ */
52
+ function matchesPattern(codePath, pattern) {
53
+ // Use micromatch for robust, fast glob matching (industry standard)
54
+ // 28x faster than minimatch, used by webpack/babel/jest
55
+ return micromatch.isMatch(codePath, pattern, { nocase: true });
56
+ }
57
+ /**
58
+ * Check if description contains a keyword (case-insensitive match)
59
+ * @param {string} description - WU description text
60
+ * @param {string} keyword - Keyword to search for
61
+ * @returns {boolean} True if keyword found in description
62
+ */
63
+ function containsKeyword(description, keyword) {
64
+ const normalizedDesc = description.toLowerCase().trim();
65
+ const normalizedKeyword = keyword.toLowerCase().trim();
66
+ // Simple substring match (sufficient for keyword detection)
67
+ return normalizedDesc.includes(normalizedKeyword);
68
+ }
69
+ /**
70
+ * Calculate confidence score for a sub-lane match
71
+ *
72
+ * WU-2438: Changed from percentage-based to absolute scoring.
73
+ * Previously, confidence = (score / maxPossibleScore) * 100, which penalized
74
+ * lanes with more patterns/keywords even when they had MORE matches.
75
+ *
76
+ * Now, confidence = raw score, so lanes with more matches win.
77
+ * This is more intuitive: 4 signals beats 1 signal, regardless of config size.
78
+ *
79
+ * @param {string[]} codePaths - WU code paths
80
+ * @param {string} description - WU description
81
+ * @param {object} subLaneConfig - Sub-lane config (code_paths, keywords)
82
+ * @returns {number} Confidence score (raw score, higher = better match)
83
+ */
84
+ function calculateConfidence(codePaths, description, subLaneConfig) {
85
+ let score = 0;
86
+ // Score code path matches
87
+ const patterns = subLaneConfig.code_paths || [];
88
+ for (const pattern of patterns) {
89
+ const hasMatch = codePaths.some((cp) => matchesPattern(cp, pattern));
90
+ if (hasMatch) {
91
+ score += WEIGHTS.CODE_PATH_MATCH;
92
+ }
93
+ }
94
+ // Score keyword matches
95
+ const keywords = subLaneConfig.keywords || [];
96
+ for (const keyword of keywords) {
97
+ if (containsKeyword(description, keyword)) {
98
+ score += WEIGHTS.KEYWORD_MATCH;
99
+ }
100
+ }
101
+ return score;
102
+ }
103
+ /**
104
+ * Infer sub-lane from WU code paths and description
105
+ * @param {string[]} codePaths - Array of file paths modified/created by this WU
106
+ * @param {string} description - WU description/title text
107
+ * @param {string|null} configPath - Optional path to config file
108
+ * @returns {{ lane: string, confidence: number }} Suggested sub-lane and confidence (0-100)
109
+ * @throws {Error} If config cannot be loaded
110
+ */
111
+ export function inferSubLane(codePaths, description, configPath = null) {
112
+ // Validate inputs
113
+ if (!Array.isArray(codePaths)) {
114
+ throw createError(ErrorCodes.VALIDATION_ERROR, 'codePaths must be an array of strings', {
115
+ codePaths,
116
+ type: typeof codePaths,
117
+ });
118
+ }
119
+ if (typeof description !== 'string') {
120
+ throw createError(ErrorCodes.VALIDATION_ERROR, 'description must be a string', {
121
+ description,
122
+ type: typeof description,
123
+ });
124
+ }
125
+ // Load config
126
+ const config = loadConfig(configPath);
127
+ // Score all sub-lanes
128
+ const scores = [];
129
+ for (const [parentLane, subLanes] of Object.entries(config)) {
130
+ for (const [subLane, subLaneConfig] of Object.entries(subLanes)) {
131
+ const confidence = calculateConfidence(codePaths, description, subLaneConfig);
132
+ const fullLaneName = `${parentLane}: ${subLane}`;
133
+ scores.push({
134
+ lane: fullLaneName,
135
+ confidence,
136
+ parent: parentLane,
137
+ subLane,
138
+ });
139
+ }
140
+ }
141
+ // Sort by confidence (descending)
142
+ scores.sort((a, b) => b.confidence - a.confidence);
143
+ // Return highest scoring sub-lane
144
+ const best = scores[0];
145
+ if (!best || best.confidence < CONFIDENCE.THRESHOLD) {
146
+ // No good matches found, return parent-only suggestion
147
+ // This shouldn't happen with CONFIDENCE.THRESHOLD=0, but keep for future tuning
148
+ return {
149
+ lane: best ? best.parent : 'Operations', // Default to Operations if all else fails
150
+ confidence: CONFIDENCE.MIN,
151
+ };
152
+ }
153
+ return {
154
+ lane: best.lane,
155
+ confidence: best.confidence,
156
+ };
157
+ }
158
+ /**
159
+ * Get all valid sub-lanes from config
160
+ * @param {string|null} configPath - Optional path to config file
161
+ * @returns {string[]} Array of all sub-lane names (format: "Parent: Subdomain")
162
+ */
163
+ export function getAllSubLanes(configPath = null) {
164
+ const config = loadConfig(configPath);
165
+ const subLanes = [];
166
+ for (const [parentLane, subLaneConfigs] of Object.entries(config)) {
167
+ for (const subLane of Object.keys(subLaneConfigs)) {
168
+ subLanes.push(`${parentLane}: ${subLane}`);
169
+ }
170
+ }
171
+ return subLanes.sort();
172
+ }
173
+ /**
174
+ * Get valid sub-lanes for a specific parent lane
175
+ * @param {string} parent - Parent lane name (e.g., "Operations", "Core Systems")
176
+ * @param {string|null} configPath - Optional path to config file
177
+ * @returns {string[]} Array of sub-lane names for that parent (e.g., ["Tooling", "CI/CD", ...])
178
+ */
179
+ export function getSubLanesForParent(parent, configPath = null) {
180
+ const config = loadConfig(configPath);
181
+ // Find parent key (case-insensitive)
182
+ const normalizedParent = parent.trim().toLowerCase();
183
+ const parentKey = Object.keys(config).find((key) => key.toLowerCase().trim() === normalizedParent);
184
+ if (!parentKey) {
185
+ return [];
186
+ }
187
+ // Return sub-lane names for this parent
188
+ const subLanes = config[parentKey];
189
+ if (!subLanes || typeof subLanes !== 'object') {
190
+ return [];
191
+ }
192
+ return Object.keys(subLanes);
193
+ }
194
+ // Re-export extractParent from lane-checker for backward compatibility (WU-1137: consolidation)
195
+ export { extractParent };