@lumenflow/core 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +119 -0
- package/dist/active-wu-detector.d.ts +33 -0
- package/dist/active-wu-detector.js +106 -0
- package/dist/adapters/filesystem-metrics.adapter.d.ts +108 -0
- package/dist/adapters/filesystem-metrics.adapter.js +519 -0
- package/dist/adapters/terminal-renderer.adapter.d.ts +106 -0
- package/dist/adapters/terminal-renderer.adapter.js +337 -0
- package/dist/arg-parser.d.ts +63 -0
- package/dist/arg-parser.js +560 -0
- package/dist/backlog-editor.d.ts +98 -0
- package/dist/backlog-editor.js +179 -0
- package/dist/backlog-generator.d.ts +111 -0
- package/dist/backlog-generator.js +381 -0
- package/dist/backlog-parser.d.ts +45 -0
- package/dist/backlog-parser.js +102 -0
- package/dist/backlog-sync-validator.d.ts +78 -0
- package/dist/backlog-sync-validator.js +294 -0
- package/dist/branch-drift.d.ts +34 -0
- package/dist/branch-drift.js +51 -0
- package/dist/cleanup-install-config.d.ts +33 -0
- package/dist/cleanup-install-config.js +37 -0
- package/dist/cleanup-lock.d.ts +139 -0
- package/dist/cleanup-lock.js +313 -0
- package/dist/code-path-validator.d.ts +146 -0
- package/dist/code-path-validator.js +537 -0
- package/dist/code-paths-overlap.d.ts +55 -0
- package/dist/code-paths-overlap.js +245 -0
- package/dist/commands-logger.d.ts +77 -0
- package/dist/commands-logger.js +254 -0
- package/dist/commit-message-utils.d.ts +25 -0
- package/dist/commit-message-utils.js +41 -0
- package/dist/compliance-parser.d.ts +150 -0
- package/dist/compliance-parser.js +507 -0
- package/dist/constants/backlog-patterns.d.ts +20 -0
- package/dist/constants/backlog-patterns.js +23 -0
- package/dist/constants/dora-constants.d.ts +49 -0
- package/dist/constants/dora-constants.js +53 -0
- package/dist/constants/gate-constants.d.ts +15 -0
- package/dist/constants/gate-constants.js +15 -0
- package/dist/constants/linter-constants.d.ts +16 -0
- package/dist/constants/linter-constants.js +16 -0
- package/dist/constants/tokenizer-constants.d.ts +15 -0
- package/dist/constants/tokenizer-constants.js +15 -0
- package/dist/core/scope-checker.d.ts +97 -0
- package/dist/core/scope-checker.js +163 -0
- package/dist/core/tool-runner.d.ts +161 -0
- package/dist/core/tool-runner.js +393 -0
- package/dist/core/tool.constants.d.ts +105 -0
- package/dist/core/tool.constants.js +101 -0
- package/dist/core/tool.schemas.d.ts +226 -0
- package/dist/core/tool.schemas.js +226 -0
- package/dist/core/worktree-guard.d.ts +130 -0
- package/dist/core/worktree-guard.js +242 -0
- package/dist/coverage-gate.d.ts +108 -0
- package/dist/coverage-gate.js +196 -0
- package/dist/date-utils.d.ts +75 -0
- package/dist/date-utils.js +140 -0
- package/dist/dependency-graph.d.ts +142 -0
- package/dist/dependency-graph.js +550 -0
- package/dist/dependency-guard.d.ts +54 -0
- package/dist/dependency-guard.js +142 -0
- package/dist/dependency-validator.d.ts +105 -0
- package/dist/dependency-validator.js +154 -0
- package/dist/docs-path-validator.d.ts +36 -0
- package/dist/docs-path-validator.js +95 -0
- package/dist/domain/orchestration.constants.d.ts +99 -0
- package/dist/domain/orchestration.constants.js +97 -0
- package/dist/domain/orchestration.schemas.d.ts +280 -0
- package/dist/domain/orchestration.schemas.js +211 -0
- package/dist/domain/orchestration.types.d.ts +133 -0
- package/dist/domain/orchestration.types.js +12 -0
- package/dist/error-handler.d.ts +116 -0
- package/dist/error-handler.js +136 -0
- package/dist/file-classifiers.d.ts +62 -0
- package/dist/file-classifiers.js +108 -0
- package/dist/gates-agent-mode.d.ts +81 -0
- package/dist/gates-agent-mode.js +94 -0
- package/dist/generate-traceability.d.ts +107 -0
- package/dist/generate-traceability.js +411 -0
- package/dist/git-adapter.d.ts +395 -0
- package/dist/git-adapter.js +649 -0
- package/dist/git-staged-validator.d.ts +32 -0
- package/dist/git-staged-validator.js +48 -0
- package/dist/hardcoded-strings.d.ts +61 -0
- package/dist/hardcoded-strings.js +270 -0
- package/dist/incremental-lint.d.ts +78 -0
- package/dist/incremental-lint.js +129 -0
- package/dist/incremental-test.d.ts +39 -0
- package/dist/incremental-test.js +61 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +61 -0
- package/dist/invariants/check-automated-tests.d.ts +50 -0
- package/dist/invariants/check-automated-tests.js +166 -0
- package/dist/invariants-runner.d.ts +103 -0
- package/dist/invariants-runner.js +527 -0
- package/dist/lane-checker.d.ts +50 -0
- package/dist/lane-checker.js +319 -0
- package/dist/lane-inference.d.ts +39 -0
- package/dist/lane-inference.js +195 -0
- package/dist/lane-lock.d.ts +211 -0
- package/dist/lane-lock.js +474 -0
- package/dist/lane-validator.d.ts +48 -0
- package/dist/lane-validator.js +114 -0
- package/dist/logs-lib.d.ts +104 -0
- package/dist/logs-lib.js +207 -0
- package/dist/lumenflow-config-schema.d.ts +272 -0
- package/dist/lumenflow-config-schema.js +207 -0
- package/dist/lumenflow-config.d.ts +95 -0
- package/dist/lumenflow-config.js +236 -0
- package/dist/manual-test-validator.d.ts +80 -0
- package/dist/manual-test-validator.js +200 -0
- package/dist/merge-lock.d.ts +115 -0
- package/dist/merge-lock.js +251 -0
- package/dist/micro-worktree.d.ts +159 -0
- package/dist/micro-worktree.js +427 -0
- package/dist/migration-deployer.d.ts +69 -0
- package/dist/migration-deployer.js +151 -0
- package/dist/orchestration-advisory-loader.d.ts +28 -0
- package/dist/orchestration-advisory-loader.js +87 -0
- package/dist/orchestration-advisory.d.ts +58 -0
- package/dist/orchestration-advisory.js +94 -0
- package/dist/orchestration-di.d.ts +48 -0
- package/dist/orchestration-di.js +57 -0
- package/dist/orchestration-rules.d.ts +57 -0
- package/dist/orchestration-rules.js +201 -0
- package/dist/orphan-detector.d.ts +131 -0
- package/dist/orphan-detector.js +226 -0
- package/dist/path-classifiers.d.ts +57 -0
- package/dist/path-classifiers.js +93 -0
- package/dist/piped-command-detector.d.ts +34 -0
- package/dist/piped-command-detector.js +64 -0
- package/dist/ports/dashboard-renderer.port.d.ts +112 -0
- package/dist/ports/dashboard-renderer.port.js +25 -0
- package/dist/ports/metrics-collector.port.d.ts +132 -0
- package/dist/ports/metrics-collector.port.js +26 -0
- package/dist/process-detector.d.ts +84 -0
- package/dist/process-detector.js +172 -0
- package/dist/prompt-linter.d.ts +72 -0
- package/dist/prompt-linter.js +312 -0
- package/dist/prompt-monitor.d.ts +15 -0
- package/dist/prompt-monitor.js +205 -0
- package/dist/rebase-artifact-cleanup.d.ts +145 -0
- package/dist/rebase-artifact-cleanup.js +433 -0
- package/dist/retry-strategy.d.ts +189 -0
- package/dist/retry-strategy.js +283 -0
- package/dist/risk-detector.d.ts +108 -0
- package/dist/risk-detector.js +252 -0
- package/dist/rollback-utils.d.ts +76 -0
- package/dist/rollback-utils.js +104 -0
- package/dist/section-headings.d.ts +43 -0
- package/dist/section-headings.js +49 -0
- package/dist/spawn-escalation.d.ts +90 -0
- package/dist/spawn-escalation.js +253 -0
- package/dist/spawn-monitor.d.ts +229 -0
- package/dist/spawn-monitor.js +672 -0
- package/dist/spawn-recovery.d.ts +82 -0
- package/dist/spawn-recovery.js +298 -0
- package/dist/spawn-registry-schema.d.ts +98 -0
- package/dist/spawn-registry-schema.js +108 -0
- package/dist/spawn-registry-store.d.ts +146 -0
- package/dist/spawn-registry-store.js +273 -0
- package/dist/spawn-tree.d.ts +121 -0
- package/dist/spawn-tree.js +285 -0
- package/dist/stamp-status-validator.d.ts +84 -0
- package/dist/stamp-status-validator.js +134 -0
- package/dist/stamp-utils.d.ts +100 -0
- package/dist/stamp-utils.js +229 -0
- package/dist/state-machine.d.ts +26 -0
- package/dist/state-machine.js +83 -0
- package/dist/system-map-validator.d.ts +80 -0
- package/dist/system-map-validator.js +272 -0
- package/dist/telemetry.d.ts +80 -0
- package/dist/telemetry.js +213 -0
- package/dist/token-counter.d.ts +51 -0
- package/dist/token-counter.js +145 -0
- package/dist/usecases/get-dashboard-data.usecase.d.ts +52 -0
- package/dist/usecases/get-dashboard-data.usecase.js +61 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +100 -0
- package/dist/usecases/get-suggestions.usecase.js +153 -0
- package/dist/user-normalizer.d.ts +41 -0
- package/dist/user-normalizer.js +141 -0
- package/dist/validators/phi-constants.d.ts +97 -0
- package/dist/validators/phi-constants.js +152 -0
- package/dist/validators/phi-scanner.d.ts +58 -0
- package/dist/validators/phi-scanner.js +215 -0
- package/dist/worktree-ownership.d.ts +50 -0
- package/dist/worktree-ownership.js +74 -0
- package/dist/worktree-scanner.d.ts +103 -0
- package/dist/worktree-scanner.js +168 -0
- package/dist/worktree-symlink.d.ts +99 -0
- package/dist/worktree-symlink.js +359 -0
- package/dist/wu-backlog-updater.d.ts +17 -0
- package/dist/wu-backlog-updater.js +37 -0
- package/dist/wu-checkpoint.d.ts +124 -0
- package/dist/wu-checkpoint.js +233 -0
- package/dist/wu-claim-helpers.d.ts +26 -0
- package/dist/wu-claim-helpers.js +63 -0
- package/dist/wu-claim-resume.d.ts +106 -0
- package/dist/wu-claim-resume.js +276 -0
- package/dist/wu-consistency-checker.d.ts +95 -0
- package/dist/wu-consistency-checker.js +567 -0
- package/dist/wu-constants.d.ts +1275 -0
- package/dist/wu-constants.js +1382 -0
- package/dist/wu-create-validators.d.ts +42 -0
- package/dist/wu-create-validators.js +93 -0
- package/dist/wu-done-branch-only.d.ts +63 -0
- package/dist/wu-done-branch-only.js +191 -0
- package/dist/wu-done-messages.d.ts +119 -0
- package/dist/wu-done-messages.js +185 -0
- package/dist/wu-done-pr.d.ts +72 -0
- package/dist/wu-done-pr.js +174 -0
- package/dist/wu-done-retry-helpers.d.ts +85 -0
- package/dist/wu-done-retry-helpers.js +172 -0
- package/dist/wu-done-ui.d.ts +37 -0
- package/dist/wu-done-ui.js +69 -0
- package/dist/wu-done-validators.d.ts +411 -0
- package/dist/wu-done-validators.js +1229 -0
- package/dist/wu-done-worktree.d.ts +182 -0
- package/dist/wu-done-worktree.js +1097 -0
- package/dist/wu-helpers.d.ts +128 -0
- package/dist/wu-helpers.js +248 -0
- package/dist/wu-lint.d.ts +70 -0
- package/dist/wu-lint.js +234 -0
- package/dist/wu-paths.d.ts +171 -0
- package/dist/wu-paths.js +178 -0
- package/dist/wu-preflight-validators.d.ts +86 -0
- package/dist/wu-preflight-validators.js +251 -0
- package/dist/wu-recovery.d.ts +138 -0
- package/dist/wu-recovery.js +341 -0
- package/dist/wu-repair-core.d.ts +131 -0
- package/dist/wu-repair-core.js +669 -0
- package/dist/wu-schema-normalization.d.ts +17 -0
- package/dist/wu-schema-normalization.js +82 -0
- package/dist/wu-schema.d.ts +793 -0
- package/dist/wu-schema.js +881 -0
- package/dist/wu-spawn-helpers.d.ts +121 -0
- package/dist/wu-spawn-helpers.js +271 -0
- package/dist/wu-spawn.d.ts +158 -0
- package/dist/wu-spawn.js +1306 -0
- package/dist/wu-state-schema.d.ts +213 -0
- package/dist/wu-state-schema.js +156 -0
- package/dist/wu-state-store.d.ts +264 -0
- package/dist/wu-state-store.js +691 -0
- package/dist/wu-status-transition.d.ts +63 -0
- package/dist/wu-status-transition.js +382 -0
- package/dist/wu-status-updater.d.ts +25 -0
- package/dist/wu-status-updater.js +116 -0
- package/dist/wu-transaction-collectors.d.ts +116 -0
- package/dist/wu-transaction-collectors.js +272 -0
- package/dist/wu-transaction.d.ts +170 -0
- package/dist/wu-transaction.js +273 -0
- package/dist/wu-validation-constants.d.ts +60 -0
- package/dist/wu-validation-constants.js +66 -0
- package/dist/wu-validation.d.ts +118 -0
- package/dist/wu-validation.js +243 -0
- package/dist/wu-validator.d.ts +62 -0
- package/dist/wu-validator.js +325 -0
- package/dist/wu-yaml-fixer.d.ts +97 -0
- package/dist/wu-yaml-fixer.js +264 -0
- package/dist/wu-yaml.d.ts +86 -0
- package/dist/wu-yaml.js +222 -0
- package/package.json +114 -0
|
@@ -0,0 +1,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 };
|