@lumenflow/core 2.2.2 → 2.3.1
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/dist/active-wu-detector.d.ts +1 -1
- package/dist/active-wu-detector.js +1 -1
- package/dist/arg-parser.js +51 -18
- package/dist/backlog-generator.d.ts +4 -4
- package/dist/backlog-generator.js +4 -4
- package/dist/backlog-sync-validator.js +1 -1
- package/dist/cleanup-lock.d.ts +9 -2
- package/dist/cleanup-lock.js +17 -7
- package/dist/code-path-validator.d.ts +3 -3
- package/dist/code-path-validator.js +3 -3
- package/dist/compliance-parser.d.ts +1 -1
- package/dist/compliance-parser.js +1 -1
- package/dist/constants/backlog-patterns.d.ts +1 -1
- package/dist/constants/backlog-patterns.js +1 -1
- package/dist/constants/dora-constants.d.ts +1 -1
- package/dist/constants/dora-constants.js +1 -1
- package/dist/constants/gate-constants.d.ts +1 -1
- package/dist/constants/gate-constants.js +1 -1
- package/dist/constants/linter-constants.d.ts +1 -1
- package/dist/constants/linter-constants.js +1 -1
- package/dist/constants/tokenizer-constants.d.ts +1 -1
- package/dist/constants/tokenizer-constants.js +1 -1
- package/dist/context/location-resolver.js +2 -1
- package/dist/context-validation-integration.d.ts +1 -0
- package/dist/core/scope-checker.d.ts +3 -3
- package/dist/core/scope-checker.js +3 -3
- package/dist/core/tool-runner.d.ts +5 -5
- package/dist/core/tool-runner.js +5 -5
- package/dist/core/tool.constants.d.ts +1 -1
- package/dist/core/tool.constants.js +1 -1
- package/dist/core/tool.schemas.d.ts +2 -2
- package/dist/core/tool.schemas.js +1 -1
- package/dist/core/worktree-guard.d.ts +1 -1
- package/dist/core/worktree-guard.js +1 -1
- package/dist/coverage-gate.d.ts +12 -3
- package/dist/coverage-gate.js +15 -8
- package/dist/date-utils.d.ts +4 -4
- package/dist/date-utils.js +4 -4
- package/dist/dependency-graph.d.ts +6 -0
- package/dist/dependency-graph.js +43 -2
- package/dist/dependency-guard.d.ts +2 -2
- package/dist/dependency-guard.js +3 -3
- package/dist/dependency-validator.d.ts +4 -4
- package/dist/dependency-validator.js +4 -7
- package/dist/domain/orchestration.constants.d.ts +31 -10
- package/dist/domain/orchestration.constants.js +45 -16
- package/dist/domain/orchestration.schemas.d.ts +54 -28
- package/dist/domain/orchestration.schemas.js +2 -2
- package/dist/domain/orchestration.types.d.ts +2 -2
- package/dist/domain/orchestration.types.js +2 -2
- package/dist/error-handler.d.ts +10 -10
- package/dist/error-handler.js +10 -10
- package/dist/file-classifiers.d.ts +6 -6
- package/dist/file-classifiers.js +6 -6
- package/dist/gates-config.d.ts +74 -0
- package/dist/gates-config.js +209 -2
- package/dist/git-adapter.d.ts +11 -11
- package/dist/git-adapter.js +11 -11
- package/dist/git-context-extractor.d.ts +112 -0
- package/dist/git-context-extractor.js +559 -0
- package/dist/hardcoded-strings.d.ts +1 -1
- package/dist/hardcoded-strings.js +1 -1
- package/dist/incremental-lint.d.ts +1 -1
- package/dist/incremental-lint.js +2 -2
- package/dist/incremental-test.d.ts +1 -1
- package/dist/incremental-test.js +1 -1
- package/dist/index.d.ts +13 -0
- package/dist/index.js +25 -0
- package/dist/invariants/check-automated-tests.d.ts +2 -2
- package/dist/invariants/check-automated-tests.js +3 -3
- package/dist/lane-checker.d.ts +28 -7
- package/dist/lane-checker.js +316 -159
- package/dist/lane-suggest-prompt.d.ts +108 -0
- package/dist/lane-suggest-prompt.js +359 -0
- package/dist/lane-validator.d.ts +3 -3
- package/dist/lane-validator.js +3 -3
- package/dist/logs-lib.d.ts +1 -1
- package/dist/logs-lib.js +1 -1
- package/dist/lumenflow-config-schema.d.ts +162 -0
- package/dist/lumenflow-config-schema.js +180 -0
- package/dist/manual-test-validator.d.ts +2 -2
- package/dist/manual-test-validator.js +3 -3
- package/dist/merge-lock.d.ts +8 -1
- package/dist/merge-lock.js +16 -7
- package/dist/micro-worktree.d.ts +81 -13
- package/dist/micro-worktree.js +98 -17
- package/dist/migration-deployer.d.ts +1 -1
- package/dist/migration-deployer.js +1 -1
- package/dist/orchestration-advisory-loader.d.ts +2 -2
- package/dist/orchestration-advisory-loader.js +10 -6
- package/dist/orchestration-advisory.d.ts +3 -3
- package/dist/orchestration-advisory.js +4 -4
- package/dist/orchestration-di.d.ts +4 -4
- package/dist/orchestration-di.js +4 -4
- package/dist/orchestration-rules.d.ts +4 -4
- package/dist/orchestration-rules.js +18 -10
- package/dist/orphan-detector.d.ts +3 -3
- package/dist/orphan-detector.js +3 -3
- package/dist/patrol-loop.d.ts +170 -0
- package/dist/patrol-loop.js +186 -0
- package/dist/process-detector.d.ts +5 -5
- package/dist/process-detector.js +5 -5
- package/dist/rebase-artifact-cleanup.d.ts +3 -3
- package/dist/rebase-artifact-cleanup.js +3 -3
- package/dist/resolve-policy.d.ts +195 -0
- package/dist/resolve-policy.js +203 -0
- package/dist/risk-detector.d.ts +2 -2
- package/dist/risk-detector.js +2 -2
- package/dist/rollback-utils.d.ts +1 -1
- package/dist/rollback-utils.js +1 -1
- package/dist/section-headings.d.ts +1 -1
- package/dist/section-headings.js +1 -1
- package/dist/spawn-escalation.d.ts +4 -4
- package/dist/spawn-escalation.js +3 -3
- package/dist/spawn-monitor.d.ts +4 -4
- package/dist/spawn-monitor.js +4 -4
- package/dist/spawn-recovery.d.ts +3 -3
- package/dist/spawn-recovery.js +3 -3
- package/dist/spawn-registry-schema.d.ts +2 -2
- package/dist/spawn-registry-schema.js +2 -2
- package/dist/spawn-registry-store.d.ts +2 -2
- package/dist/spawn-registry-store.js +2 -2
- package/dist/spawn-strategy.d.ts +17 -11
- package/dist/spawn-strategy.js +47 -44
- package/dist/spawn-tree.d.ts +3 -3
- package/dist/spawn-tree.js +3 -3
- package/dist/state-cleanup-core.d.ts +205 -0
- package/dist/state-cleanup-core.js +240 -0
- package/dist/state-doctor-core.d.ts +168 -0
- package/dist/state-doctor-core.js +251 -0
- package/dist/stream-error-handler.d.ts +67 -0
- package/dist/stream-error-handler.js +94 -0
- package/dist/telemetry.d.ts +1 -1
- package/dist/telemetry.js +1 -1
- package/dist/template-loader.d.ts +162 -0
- package/dist/template-loader.js +372 -0
- package/dist/test-baseline.d.ts +176 -0
- package/dist/test-baseline.js +282 -0
- package/dist/usecases/get-suggestions.usecase.d.ts +1 -1
- package/dist/validation/command-registry.js +37 -0
- package/dist/validators/backlog-sync.js +4 -2
- package/dist/worktree-scanner.d.ts +1 -1
- package/dist/worktree-scanner.js +1 -1
- package/dist/worktree-symlink.d.ts +3 -3
- package/dist/worktree-symlink.js +3 -3
- package/dist/wu-backlog-updater.d.ts +1 -1
- package/dist/wu-backlog-updater.js +1 -1
- package/dist/wu-claim-helpers.d.ts +1 -1
- package/dist/wu-claim-helpers.js +1 -1
- package/dist/wu-claim-resume.d.ts +1 -1
- package/dist/wu-claim-resume.js +1 -1
- package/dist/wu-consistency-checker.d.ts +1 -1
- package/dist/wu-consistency-checker.js +17 -11
- package/dist/wu-constants.d.ts +73 -21
- package/dist/wu-constants.js +65 -22
- package/dist/wu-done-branch-only.d.ts +1 -1
- package/dist/wu-done-branch-only.js +1 -1
- package/dist/wu-done-docs-generate.d.ts +1 -1
- package/dist/wu-done-docs-generate.js +1 -1
- package/dist/wu-done-messages.d.ts +2 -2
- package/dist/wu-done-messages.js +2 -2
- package/dist/wu-done-metadata.d.ts +3 -3
- package/dist/wu-done-metadata.js +3 -3
- package/dist/wu-done-pr.d.ts +1 -1
- package/dist/wu-done-pr.js +4 -2
- package/dist/wu-done-preflight.d.ts +8 -0
- package/dist/wu-done-preflight.js +18 -2
- package/dist/wu-done-ui.d.ts +3 -3
- package/dist/wu-done-ui.js +3 -3
- package/dist/wu-done-validation.d.ts +30 -0
- package/dist/wu-done-validation.js +106 -1
- package/dist/wu-done-worktree.d.ts +1 -1
- package/dist/wu-done-worktree.js +11 -1
- package/dist/wu-events-cleanup.d.ts +148 -0
- package/dist/wu-events-cleanup.js +401 -0
- package/dist/wu-helpers.d.ts +2 -2
- package/dist/wu-helpers.js +2 -2
- package/dist/wu-id-generator.d.ts +58 -0
- package/dist/wu-id-generator.js +103 -0
- package/dist/wu-lint.js +1 -1
- package/dist/wu-preflight-validators.d.ts +13 -1
- package/dist/wu-preflight-validators.js +56 -1
- package/dist/wu-recovery.d.ts +2 -2
- package/dist/wu-recovery.js +4 -4
- package/dist/wu-repair-core.d.ts +5 -5
- package/dist/wu-repair-core.js +6 -6
- package/dist/wu-schema-normalization.d.ts +1 -1
- package/dist/wu-schema-normalization.js +1 -1
- package/dist/wu-schema.d.ts +7 -7
- package/dist/wu-schema.js +8 -8
- package/dist/wu-spawn-context.d.ts +87 -0
- package/dist/wu-spawn-context.js +175 -0
- package/dist/wu-spawn-helpers.d.ts +1 -1
- package/dist/wu-spawn-helpers.js +1 -1
- package/dist/wu-spawn.d.ts +177 -4
- package/dist/wu-spawn.js +694 -72
- package/dist/wu-state-schema.d.ts +1 -1
- package/dist/wu-state-schema.js +1 -1
- package/dist/wu-state-store.d.ts +3 -3
- package/dist/wu-state-store.js +3 -3
- package/dist/wu-status-transition.d.ts +1 -1
- package/dist/wu-status-transition.js +1 -1
- package/dist/wu-status-updater.d.ts +3 -3
- package/dist/wu-status-updater.js +3 -3
- package/dist/wu-validation-constants.d.ts +2 -2
- package/dist/wu-validation-constants.js +2 -2
- package/dist/wu-validation.d.ts +3 -3
- package/dist/wu-validation.js +3 -3
- package/dist/wu-yaml-fixer.d.ts +2 -2
- package/dist/wu-yaml-fixer.js +3 -3
- package/dist/wu-yaml.d.ts +23 -0
- package/dist/wu-yaml.js +76 -2
- package/package.json +5 -2
package/dist/lane-checker.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* Lane Occupancy Checker
|
|
4
4
|
*
|
|
5
5
|
* Enforces one-WU-per-lane rule by checking status.md for active WUs in a given lane.
|
|
6
|
-
* Used by wu-claim.
|
|
6
|
+
* Used by wu-claim.ts and wu-unblock.ts to prevent WIP violations.
|
|
7
7
|
*/
|
|
8
8
|
import { existsSync, readFileSync } from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
@@ -114,6 +114,77 @@ const SPACE = ' ';
|
|
|
114
114
|
* @property {boolean} [strict=true] - When true, throws error for parent-only lanes with taxonomy.
|
|
115
115
|
* When false, only warns (for existing WU validation).
|
|
116
116
|
*/
|
|
117
|
+
/**
|
|
118
|
+
* WU-1197: Validate colon format in lane string
|
|
119
|
+
* @throws {LumenflowError} If format is invalid
|
|
120
|
+
*/
|
|
121
|
+
function validateColonFormat(lane, trimmed, colonIndex) {
|
|
122
|
+
// Check for space before colon
|
|
123
|
+
if (colonIndex > 0 && trimmed[colonIndex - 1] === SPACE) {
|
|
124
|
+
throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" has space before colon. Expected format: "Parent: Subdomain" (space AFTER colon only)`, { lane });
|
|
125
|
+
}
|
|
126
|
+
// Check for space after colon
|
|
127
|
+
if (colonIndex + 1 >= trimmed.length || trimmed[colonIndex + 1] !== SPACE) {
|
|
128
|
+
throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" is missing space after colon. Expected format: "Parent: Subdomain"`, { lane });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* WU-1197: Validate sub-lane format (Parent: Subdomain)
|
|
133
|
+
* @throws {LumenflowError} If validation fails
|
|
134
|
+
*/
|
|
135
|
+
function validateSubLaneFormat(lane, trimmed, colonIndex, configPath) {
|
|
136
|
+
validateColonFormat(lane, trimmed, colonIndex);
|
|
137
|
+
// Extract parent and subdomain (colonIndex + 2 = skip colon and space)
|
|
138
|
+
const parent = trimmed.substring(0, colonIndex).trim();
|
|
139
|
+
const subdomain = trimmed.substring(colonIndex + LANE_SEPARATOR.length + SPACE.length).trim();
|
|
140
|
+
// Validate parent exists in config
|
|
141
|
+
if (!isValidParentLane(parent, configPath)) {
|
|
142
|
+
throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${parent}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { parent, lane });
|
|
143
|
+
}
|
|
144
|
+
// Validate sub-lane exists in taxonomy
|
|
145
|
+
if (hasSubLaneTaxonomy(parent)) {
|
|
146
|
+
validateSubLaneInTaxonomy(parent, subdomain);
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Parent has no taxonomy - reject sub-lane format
|
|
150
|
+
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 });
|
|
151
|
+
}
|
|
152
|
+
return { valid: true, parent, error: null };
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* WU-1197: Validate that sub-lane exists in taxonomy
|
|
156
|
+
* @throws {LumenflowError} If sub-lane is not valid
|
|
157
|
+
*/
|
|
158
|
+
function validateSubLaneInTaxonomy(parent, subdomain) {
|
|
159
|
+
if (!isValidSubLane(parent, subdomain)) {
|
|
160
|
+
const validSubLanes = getSubLanesForParent(parent);
|
|
161
|
+
throw createError(ErrorCodes.INVALID_LANE, `Unknown sub-lane: "${subdomain}" for parent lane "${parent}".\n\n` +
|
|
162
|
+
`Valid sub-lanes: ${validSubLanes.join(', ')}`, { parent, subdomain, validSubLanes });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* WU-1197: Validate parent-only lane format
|
|
167
|
+
* @throws {LumenflowError} If validation fails (in strict mode)
|
|
168
|
+
*/
|
|
169
|
+
function validateParentOnlyFormat(trimmed, configPath, strict) {
|
|
170
|
+
if (!isValidParentLane(trimmed, configPath)) {
|
|
171
|
+
throw createError(ErrorCodes.INVALID_LANE, `Unknown parent lane: "${trimmed}". Check ${CONFIG_FILES.LUMENFLOW_CONFIG} for valid lanes.`, { lane: trimmed });
|
|
172
|
+
}
|
|
173
|
+
// Block if parent has sub-lane taxonomy (sub-lane required)
|
|
174
|
+
if (hasSubLaneTaxonomy(trimmed)) {
|
|
175
|
+
const validSubLanes = getSubLanesForParent(trimmed);
|
|
176
|
+
const message = `Parent-only lane "${trimmed}" blocked. Sub-lane required. ` +
|
|
177
|
+
`Valid: ${validSubLanes.join(', ')}. ` +
|
|
178
|
+
`Format: "${trimmed}: <sublane>"`;
|
|
179
|
+
if (strict) {
|
|
180
|
+
throw createError(ErrorCodes.INVALID_LANE, message, { lane: trimmed, validSubLanes });
|
|
181
|
+
}
|
|
182
|
+
// Non-strict mode: warn only for existing WU validation
|
|
183
|
+
// eslint-disable-next-line no-console -- Intentional operational logging
|
|
184
|
+
console.warn(`${PREFIX} ⚠️ ${message}`);
|
|
185
|
+
}
|
|
186
|
+
return { valid: true, parent: trimmed, error: null };
|
|
187
|
+
}
|
|
117
188
|
/**
|
|
118
189
|
* Validate lane format and parent existence
|
|
119
190
|
* @param {string} lane - Lane name (e.g., "Operations: Tooling" or "Operations")
|
|
@@ -129,62 +200,55 @@ export function validateLaneFormat(lane, configPath = null, options = {}) {
|
|
|
129
200
|
if (colonCount > 1) {
|
|
130
201
|
throw createError(ErrorCodes.INVALID_LANE, `Invalid lane format: "${lane}" contains multiple colons. Expected format: "Parent: Subdomain" or "Parent"`, { lane });
|
|
131
202
|
}
|
|
132
|
-
// Check for colon
|
|
133
203
|
const colonIndex = trimmed.indexOf(LANE_SEPARATOR);
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 };
|
|
204
|
+
const isSubLaneFormat = colonIndex !== -1;
|
|
205
|
+
if (isSubLaneFormat) {
|
|
206
|
+
return validateSubLaneFormat(lane, trimmed, colonIndex, configPath);
|
|
165
207
|
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
208
|
+
return validateParentOnlyFormat(trimmed, configPath, strict);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* WU-1197: Extract lane names and parent lanes from config
|
|
212
|
+
* Handles flat array, definitions, and legacy nested formats
|
|
213
|
+
*/
|
|
214
|
+
function extractLanesForParentCheck(config) {
|
|
215
|
+
const allLanes = [];
|
|
216
|
+
const parentLanes = new Set();
|
|
217
|
+
if (!config.lanes) {
|
|
218
|
+
return { allLanes, parentLanes };
|
|
219
|
+
}
|
|
220
|
+
if (Array.isArray(config.lanes)) {
|
|
221
|
+
// Flat array format: lanes: [{name: "Core"}, {name: "CLI"}, ...]
|
|
222
|
+
allLanes.push(...config.lanes.map((l) => l.name));
|
|
223
|
+
return { allLanes, parentLanes };
|
|
224
|
+
}
|
|
225
|
+
// WU-1022: New format with lanes.definitions containing full "Parent: Sublane" names
|
|
226
|
+
if (config.lanes.definitions) {
|
|
227
|
+
for (const lane of config.lanes.definitions) {
|
|
228
|
+
allLanes.push(lane.name);
|
|
229
|
+
// Extract parent from full lane name for parent validation
|
|
230
|
+
const extracted = extractParent(lane.name);
|
|
231
|
+
parentLanes.add(extracted.toLowerCase().trim());
|
|
185
232
|
}
|
|
186
|
-
return { valid: true, parent: trimmed, error: null };
|
|
187
233
|
}
|
|
234
|
+
// Legacy nested format: lanes: {engineering: [...], business: [...]}
|
|
235
|
+
if (config.lanes.engineering) {
|
|
236
|
+
allLanes.push(...config.lanes.engineering.map((l) => l.name));
|
|
237
|
+
}
|
|
238
|
+
if (config.lanes.business) {
|
|
239
|
+
allLanes.push(...config.lanes.business.map((l) => l.name));
|
|
240
|
+
}
|
|
241
|
+
return { allLanes, parentLanes };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* WU-1197: Resolve config path, defaulting to project root if not provided
|
|
245
|
+
*/
|
|
246
|
+
function resolveConfigPath(configPath) {
|
|
247
|
+
if (configPath) {
|
|
248
|
+
return configPath;
|
|
249
|
+
}
|
|
250
|
+
const projectRoot = findProjectRoot();
|
|
251
|
+
return path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
|
|
188
252
|
}
|
|
189
253
|
/**
|
|
190
254
|
* Check if a parent lane exists in LumenFlow config
|
|
@@ -197,12 +261,7 @@ export function validateLaneFormat(lane, configPath = null, options = {}) {
|
|
|
197
261
|
* @returns {boolean} True if parent lane exists
|
|
198
262
|
*/
|
|
199
263
|
function isValidParentLane(parent, configPath = null) {
|
|
200
|
-
|
|
201
|
-
let resolvedConfigPath = configPath;
|
|
202
|
-
if (!resolvedConfigPath) {
|
|
203
|
-
const projectRoot = findProjectRoot();
|
|
204
|
-
resolvedConfigPath = path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
|
|
205
|
-
}
|
|
264
|
+
const resolvedConfigPath = resolveConfigPath(configPath);
|
|
206
265
|
// Read and parse config
|
|
207
266
|
if (!existsSync(resolvedConfigPath)) {
|
|
208
267
|
throw createError(ErrorCodes.FILE_NOT_FOUND, `Config file not found: ${resolvedConfigPath}`, {
|
|
@@ -211,34 +270,7 @@ function isValidParentLane(parent, configPath = null) {
|
|
|
211
270
|
}
|
|
212
271
|
const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
|
|
213
272
|
const config = parseYAML(configContent);
|
|
214
|
-
|
|
215
|
-
const allLanes = [];
|
|
216
|
-
const parentLanes = new Set();
|
|
217
|
-
if (config.lanes) {
|
|
218
|
-
if (Array.isArray(config.lanes)) {
|
|
219
|
-
// Flat array format: lanes: [{name: "Core"}, {name: "CLI"}, ...]
|
|
220
|
-
allLanes.push(...config.lanes.map((l) => l.name));
|
|
221
|
-
}
|
|
222
|
-
else {
|
|
223
|
-
// WU-1022: New format with lanes.definitions containing full "Parent: Sublane" names
|
|
224
|
-
if (config.lanes.definitions) {
|
|
225
|
-
for (const lane of config.lanes.definitions) {
|
|
226
|
-
allLanes.push(lane.name);
|
|
227
|
-
// Extract parent from full lane name for parent validation
|
|
228
|
-
const extractedParent = extractParent(lane.name);
|
|
229
|
-
parentLanes.add(extractedParent.toLowerCase().trim());
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
// Legacy nested format: lanes: {engineering: [...], business: [...]}
|
|
233
|
-
if (config.lanes.engineering) {
|
|
234
|
-
allLanes.push(...config.lanes.engineering.map((l) => l.name));
|
|
235
|
-
}
|
|
236
|
-
if (config.lanes.business) {
|
|
237
|
-
allLanes.push(...config.lanes.business.map((l) => l.name));
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
// Case-insensitive comparison
|
|
273
|
+
const { allLanes, parentLanes } = extractLanesForParentCheck(config);
|
|
242
274
|
const normalizedParent = parent.toLowerCase().trim();
|
|
243
275
|
// WU-1022: If we have extracted parent lanes (from full lane names), check against those
|
|
244
276
|
if (parentLanes.size > 0) {
|
|
@@ -285,7 +317,11 @@ export function getWipLimitForLane(lane, options = {}) {
|
|
|
285
317
|
allLanes = config.lanes;
|
|
286
318
|
}
|
|
287
319
|
else {
|
|
288
|
-
//
|
|
320
|
+
// New format with definitions
|
|
321
|
+
if (config.lanes.definitions) {
|
|
322
|
+
allLanes.push(...config.lanes.definitions);
|
|
323
|
+
}
|
|
324
|
+
// Legacy nested format: lanes: {engineering: [...], business: [...]}
|
|
289
325
|
if (config.lanes.engineering) {
|
|
290
326
|
allLanes.push(...config.lanes.engineering);
|
|
291
327
|
}
|
|
@@ -306,6 +342,93 @@ export function getWipLimitForLane(lane, options = {}) {
|
|
|
306
342
|
return DEFAULT_WIP_LIMIT;
|
|
307
343
|
}
|
|
308
344
|
}
|
|
345
|
+
/** WU-1197: Section heading marker for H2 headings */
|
|
346
|
+
const SECTION_HEADING_PREFIX = '## ';
|
|
347
|
+
/**
|
|
348
|
+
* WU-1197: Create an empty lane result (no WUs in progress)
|
|
349
|
+
*/
|
|
350
|
+
function createEmptyLaneResult(wipLimit) {
|
|
351
|
+
return {
|
|
352
|
+
free: true,
|
|
353
|
+
occupiedBy: null,
|
|
354
|
+
error: null,
|
|
355
|
+
inProgressWUs: [],
|
|
356
|
+
wipLimit,
|
|
357
|
+
currentCount: 0,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
/**
|
|
361
|
+
* WU-1197: Extract In Progress section from status.md lines
|
|
362
|
+
* @returns Section content or null if not found
|
|
363
|
+
*/
|
|
364
|
+
function extractInProgressSection(lines) {
|
|
365
|
+
const inProgressIdx = lines.findIndex((l) => isInProgressHeader(l));
|
|
366
|
+
if (inProgressIdx === -1) {
|
|
367
|
+
return { section: '', error: 'Could not find "## In Progress" section in status.md' };
|
|
368
|
+
}
|
|
369
|
+
// Find end of In Progress section (next ## heading or end of file)
|
|
370
|
+
let endIdx = lines
|
|
371
|
+
.slice(inProgressIdx + 1)
|
|
372
|
+
.findIndex((l) => l.startsWith(SECTION_HEADING_PREFIX));
|
|
373
|
+
if (endIdx === -1) {
|
|
374
|
+
endIdx = lines.length - inProgressIdx - 1;
|
|
375
|
+
}
|
|
376
|
+
else {
|
|
377
|
+
endIdx = inProgressIdx + 1 + endIdx;
|
|
378
|
+
}
|
|
379
|
+
const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
|
|
380
|
+
return { section, error: null };
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* WU-1197: Check if a WU belongs to the target lane
|
|
384
|
+
* @returns The WU ID if it matches the target lane, null otherwise
|
|
385
|
+
*/
|
|
386
|
+
function checkWuLaneMatch(activeWuid, wuid, projectRoot, targetLane) {
|
|
387
|
+
// Skip if it's the same WU we're trying to claim
|
|
388
|
+
if (activeWuid === wuid) {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const wuPath = path.join(projectRoot, WU_PATHS.WU(activeWuid));
|
|
392
|
+
if (!existsSync(wuPath)) {
|
|
393
|
+
// eslint-disable-next-line no-console -- Intentional operational logging
|
|
394
|
+
console.warn(`${PREFIX} Warning: ${activeWuid} referenced in status.md but ${wuPath} not found`);
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const wuContent = readFileSync(wuPath, { encoding: 'utf-8' });
|
|
399
|
+
const wuDoc = parseYAML(wuContent);
|
|
400
|
+
if (!wuDoc || !wuDoc.lane) {
|
|
401
|
+
// eslint-disable-next-line no-console -- Intentional operational logging
|
|
402
|
+
console.warn(`${PREFIX} Warning: ${activeWuid} has no lane field`);
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
// Normalize lane names for comparison (case-insensitive, trim whitespace)
|
|
406
|
+
const activeLane = wuDoc.lane.toString().trim().toLowerCase();
|
|
407
|
+
if (activeLane === targetLane) {
|
|
408
|
+
return activeWuid;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
const errMessage = e instanceof Error ? e.message : String(e);
|
|
413
|
+
// eslint-disable-next-line no-console -- Intentional operational logging
|
|
414
|
+
console.warn(`${PREFIX} Warning: Failed to parse ${activeWuid} YAML: ${errMessage}`);
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* WU-1197: Collect WUs in the target lane from matched WU links
|
|
420
|
+
*/
|
|
421
|
+
function collectInProgressWUsForLane(matches, wuid, projectRoot, targetLane) {
|
|
422
|
+
const inProgressWUs = [];
|
|
423
|
+
for (const match of matches) {
|
|
424
|
+
const activeWuid = match[1]; // e.g., "WU-334"
|
|
425
|
+
const matchedWu = checkWuLaneMatch(activeWuid, wuid, projectRoot, targetLane);
|
|
426
|
+
if (matchedWu) {
|
|
427
|
+
inProgressWUs.push(matchedWu);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return inProgressWUs;
|
|
431
|
+
}
|
|
309
432
|
/**
|
|
310
433
|
* Check if a lane is free (in_progress WU count is below wip_limit)
|
|
311
434
|
*
|
|
@@ -320,8 +443,6 @@ export function getWipLimitForLane(lane, options = {}) {
|
|
|
320
443
|
* @returns {{ free: boolean, occupiedBy: string | null, error: string | null, inProgressWUs?: string[], wipLimit?: number, currentCount?: number }}
|
|
321
444
|
*/
|
|
322
445
|
export function checkLaneFree(statusPath, lane, wuid, options = {}) {
|
|
323
|
-
/** Section heading marker for H2 headings */
|
|
324
|
-
const SECTION_HEADING_PREFIX = '## ';
|
|
325
446
|
try {
|
|
326
447
|
// Read status.md
|
|
327
448
|
if (!existsSync(statusPath)) {
|
|
@@ -329,88 +450,26 @@ export function checkLaneFree(statusPath, lane, wuid, options = {}) {
|
|
|
329
450
|
}
|
|
330
451
|
const content = readFileSync(statusPath, { encoding: 'utf-8' });
|
|
331
452
|
const lines = content.split(/\r?\n/);
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
return {
|
|
336
|
-
free: false,
|
|
337
|
-
occupiedBy: null,
|
|
338
|
-
error: 'Could not find "## In Progress" section in status.md',
|
|
339
|
-
};
|
|
453
|
+
const { section, error } = extractInProgressSection(lines);
|
|
454
|
+
if (error) {
|
|
455
|
+
return { free: false, occupiedBy: null, error };
|
|
340
456
|
}
|
|
341
|
-
// Find end of In Progress section (next ## heading or end of file)
|
|
342
|
-
let endIdx = lines
|
|
343
|
-
.slice(inProgressIdx + 1)
|
|
344
|
-
.findIndex((l) => l.startsWith(SECTION_HEADING_PREFIX));
|
|
345
|
-
if (endIdx === -1)
|
|
346
|
-
endIdx = lines.length - inProgressIdx - 1;
|
|
347
|
-
else
|
|
348
|
-
endIdx = inProgressIdx + 1 + endIdx;
|
|
349
|
-
// Extract WU links from In Progress section
|
|
350
|
-
const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
|
|
351
457
|
// WU-1016: Get WIP limit for this lane from config
|
|
352
458
|
const wipLimit = getWipLimitForLane(lane, { configPath: options.configPath });
|
|
353
|
-
// Check for "No items" marker
|
|
459
|
+
// Check for "No items" marker or no WU links
|
|
354
460
|
if (section.includes(NO_ITEMS_MARKER)) {
|
|
355
|
-
return
|
|
356
|
-
free: true,
|
|
357
|
-
occupiedBy: null,
|
|
358
|
-
error: null,
|
|
359
|
-
inProgressWUs: [],
|
|
360
|
-
wipLimit,
|
|
361
|
-
currentCount: 0,
|
|
362
|
-
};
|
|
461
|
+
return createEmptyLaneResult(wipLimit);
|
|
363
462
|
}
|
|
364
463
|
// Extract WU IDs from links like [WU-334 — Title](wu/WU-334.yaml)
|
|
365
464
|
WU_LINK_PATTERN.lastIndex = 0; // Reset global regex state
|
|
366
465
|
const matches = [...section.matchAll(WU_LINK_PATTERN)];
|
|
367
466
|
if (matches.length === 0) {
|
|
368
|
-
return
|
|
369
|
-
free: true,
|
|
370
|
-
occupiedBy: null,
|
|
371
|
-
error: null,
|
|
372
|
-
inProgressWUs: [],
|
|
373
|
-
wipLimit,
|
|
374
|
-
currentCount: 0,
|
|
375
|
-
};
|
|
467
|
+
return createEmptyLaneResult(wipLimit);
|
|
376
468
|
}
|
|
377
469
|
// Get project root from statusPath (docs/04-operations/tasks/status.md)
|
|
378
|
-
// Use path.dirname 4 times: status.md -> tasks -> 04-operations -> docs -> root
|
|
379
470
|
const projectRoot = path.dirname(path.dirname(path.dirname(path.dirname(statusPath))));
|
|
380
|
-
// WU-1016: Collect all WUs in the target lane
|
|
381
|
-
const inProgressWUs = [];
|
|
382
471
|
const targetLane = lane.toString().trim().toLowerCase();
|
|
383
|
-
|
|
384
|
-
const activeWuid = match[1]; // e.g., "WU-334"
|
|
385
|
-
// Skip if it's the same WU we're trying to claim (shouldn't happen, but be safe)
|
|
386
|
-
if (activeWuid === wuid)
|
|
387
|
-
continue;
|
|
388
|
-
// Use WU_PATHS to build the path consistently
|
|
389
|
-
const wuPath = path.join(projectRoot, WU_PATHS.WU(activeWuid));
|
|
390
|
-
if (!existsSync(wuPath)) {
|
|
391
|
-
console.warn(`${PREFIX} Warning: ${activeWuid} referenced in status.md but ${wuPath} not found`);
|
|
392
|
-
continue;
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
395
|
-
const wuContent = readFileSync(wuPath, { encoding: 'utf-8' });
|
|
396
|
-
const wuDoc = parseYAML(wuContent);
|
|
397
|
-
if (!wuDoc || !wuDoc.lane) {
|
|
398
|
-
console.warn(`${PREFIX} Warning: ${activeWuid} has no lane field`);
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
// Normalize lane names for comparison (case-insensitive, trim whitespace)
|
|
402
|
-
const activeLane = wuDoc.lane.toString().trim().toLowerCase();
|
|
403
|
-
if (activeLane === targetLane) {
|
|
404
|
-
// WU-1016: Add to list of in-progress WUs in this lane
|
|
405
|
-
inProgressWUs.push(activeWuid);
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
catch (e) {
|
|
409
|
-
const errMessage = e instanceof Error ? e.message : String(e);
|
|
410
|
-
console.warn(`${PREFIX} Warning: Failed to parse ${activeWuid} YAML: ${errMessage}`);
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
}
|
|
472
|
+
const inProgressWUs = collectInProgressWUsForLane(matches, wuid, projectRoot, targetLane);
|
|
414
473
|
// WU-1016: Check if lane is free based on WIP limit
|
|
415
474
|
const currentCount = inProgressWUs.length;
|
|
416
475
|
const isFree = currentCount < wipLimit;
|
|
@@ -428,3 +487,101 @@ export function checkLaneFree(statusPath, lane, wuid, options = {}) {
|
|
|
428
487
|
return { free: false, occupiedBy: null, error: `Unexpected error: ${errMessage}` };
|
|
429
488
|
}
|
|
430
489
|
}
|
|
490
|
+
/** WU-1187: Default result when no justification is required */
|
|
491
|
+
const NO_JUSTIFICATION_REQUIRED = {
|
|
492
|
+
valid: true,
|
|
493
|
+
warning: null,
|
|
494
|
+
requiresJustification: false,
|
|
495
|
+
};
|
|
496
|
+
/**
|
|
497
|
+
* WU-1187: Extract all lanes from config (handles all config formats)
|
|
498
|
+
* @param config - Parsed LumenFlow config
|
|
499
|
+
* @returns Array of lane configs with wip settings
|
|
500
|
+
*/
|
|
501
|
+
function extractAllLanesFromConfig(config) {
|
|
502
|
+
if (!config.lanes) {
|
|
503
|
+
return [];
|
|
504
|
+
}
|
|
505
|
+
if (Array.isArray(config.lanes)) {
|
|
506
|
+
// Flat array format: lanes: [{name: "Core", wip_limit: 2}, ...]
|
|
507
|
+
return config.lanes;
|
|
508
|
+
}
|
|
509
|
+
// Nested formats with definitions, engineering, business
|
|
510
|
+
const allLanes = [];
|
|
511
|
+
if (config.lanes.definitions) {
|
|
512
|
+
allLanes.push(...config.lanes.definitions);
|
|
513
|
+
}
|
|
514
|
+
if (config.lanes.engineering) {
|
|
515
|
+
allLanes.push(...config.lanes.engineering);
|
|
516
|
+
}
|
|
517
|
+
if (config.lanes.business) {
|
|
518
|
+
allLanes.push(...config.lanes.business);
|
|
519
|
+
}
|
|
520
|
+
return allLanes;
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* WU-1187: Check if a lane has WIP justification when required
|
|
524
|
+
*
|
|
525
|
+
* Philosophy: If you need WIP > 1, you need better lanes, not higher limits.
|
|
526
|
+
* This is soft enforcement: logs a warning at claim time, but doesn't block.
|
|
527
|
+
*
|
|
528
|
+
* @param {string} lane - Lane name to check
|
|
529
|
+
* @param {CheckWipJustificationOptions} options - Options including configPath for testing
|
|
530
|
+
* @returns {CheckWipJustificationResult} Result with valid=true (always) and optional warning
|
|
531
|
+
*/
|
|
532
|
+
export function checkWipJustification(lane, options = {}) {
|
|
533
|
+
// Determine config path
|
|
534
|
+
let resolvedConfigPath = options.configPath;
|
|
535
|
+
if (!resolvedConfigPath) {
|
|
536
|
+
const projectRoot = findProjectRoot();
|
|
537
|
+
resolvedConfigPath = path.join(projectRoot, CONFIG_FILES.LUMENFLOW_CONFIG);
|
|
538
|
+
}
|
|
539
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path is validated
|
|
540
|
+
if (!existsSync(resolvedConfigPath)) {
|
|
541
|
+
return NO_JUSTIFICATION_REQUIRED;
|
|
542
|
+
}
|
|
543
|
+
try {
|
|
544
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename -- Config path is validated
|
|
545
|
+
const configContent = readFileSync(resolvedConfigPath, { encoding: 'utf-8' });
|
|
546
|
+
const config = parseYAML(configContent);
|
|
547
|
+
const allLanes = extractAllLanesFromConfig(config);
|
|
548
|
+
if (allLanes.length === 0) {
|
|
549
|
+
return NO_JUSTIFICATION_REQUIRED;
|
|
550
|
+
}
|
|
551
|
+
// Find matching lane (case-insensitive)
|
|
552
|
+
const normalizedLane = lane.toLowerCase().trim();
|
|
553
|
+
const matchingLane = allLanes.find((l) => l.name.toLowerCase().trim() === normalizedLane);
|
|
554
|
+
if (!matchingLane) {
|
|
555
|
+
return NO_JUSTIFICATION_REQUIRED;
|
|
556
|
+
}
|
|
557
|
+
const wipLimit = matchingLane.wip_limit ?? DEFAULT_WIP_LIMIT;
|
|
558
|
+
// WIP <= 1 doesn't need justification
|
|
559
|
+
if (wipLimit <= 1) {
|
|
560
|
+
return NO_JUSTIFICATION_REQUIRED;
|
|
561
|
+
}
|
|
562
|
+
// WIP > 1 - check for justification
|
|
563
|
+
const justification = matchingLane.wip_justification;
|
|
564
|
+
if (justification && justification.trim().length > 0) {
|
|
565
|
+
// Has justification - all good
|
|
566
|
+
return {
|
|
567
|
+
valid: true,
|
|
568
|
+
warning: null,
|
|
569
|
+
requiresJustification: false,
|
|
570
|
+
justification: justification.trim(),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
// WIP > 1 without justification - emit warning
|
|
574
|
+
const warning = `Lane "${lane}" has WIP limit of ${wipLimit} but no wip_justification. ` +
|
|
575
|
+
`Philosophy: If you need WIP > 1, you need better lanes, not higher limits. ` +
|
|
576
|
+
`Add wip_justification to .lumenflow.config.yaml to suppress this warning.`;
|
|
577
|
+
return {
|
|
578
|
+
valid: true, // Soft enforcement - warning only, doesn't block
|
|
579
|
+
warning,
|
|
580
|
+
requiresJustification: true,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
catch {
|
|
584
|
+
// If config parsing fails, don't block
|
|
585
|
+
return NO_JUSTIFICATION_REQUIRED;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lane Suggestion Prompt Module (WU-1189)
|
|
3
|
+
*
|
|
4
|
+
* Generates prompts for LLM-driven lane suggestions based on codebase context.
|
|
5
|
+
* This module gathers project context and generates structured prompts for
|
|
6
|
+
* the LLM to analyze and suggest appropriate lane definitions.
|
|
7
|
+
*
|
|
8
|
+
* Design decisions:
|
|
9
|
+
* - Uses LLM for semantic understanding (not hardcoded heuristics)
|
|
10
|
+
* - Gathers context from docs, structure, and existing config
|
|
11
|
+
* - Returns structured JSON for easy parsing
|
|
12
|
+
*/
|
|
13
|
+
import { z } from 'zod';
|
|
14
|
+
/**
|
|
15
|
+
* Schema for a single lane suggestion from the LLM
|
|
16
|
+
*/
|
|
17
|
+
export declare const LaneSuggestionSchema: z.ZodObject<{
|
|
18
|
+
lane: z.ZodString;
|
|
19
|
+
description: z.ZodString;
|
|
20
|
+
rationale: z.ZodString;
|
|
21
|
+
code_paths: z.ZodArray<z.ZodString>;
|
|
22
|
+
keywords: z.ZodArray<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export type LaneSuggestion = z.infer<typeof LaneSuggestionSchema>;
|
|
25
|
+
/**
|
|
26
|
+
* Schema for the complete LLM response
|
|
27
|
+
*/
|
|
28
|
+
export declare const LaneSuggestResponseSchema: z.ZodObject<{
|
|
29
|
+
suggestions: z.ZodArray<z.ZodObject<{
|
|
30
|
+
lane: z.ZodString;
|
|
31
|
+
description: z.ZodString;
|
|
32
|
+
rationale: z.ZodString;
|
|
33
|
+
code_paths: z.ZodArray<z.ZodString>;
|
|
34
|
+
keywords: z.ZodArray<z.ZodString>;
|
|
35
|
+
}, z.core.$strip>>;
|
|
36
|
+
}, z.core.$strip>;
|
|
37
|
+
export type LaneSuggestResponse = z.infer<typeof LaneSuggestResponseSchema>;
|
|
38
|
+
/**
|
|
39
|
+
* Project context gathered for LLM analysis
|
|
40
|
+
*/
|
|
41
|
+
export interface ProjectContext {
|
|
42
|
+
/** Names of packages found (for monorepos) */
|
|
43
|
+
packageNames: string[];
|
|
44
|
+
/** Top-level directory structure */
|
|
45
|
+
directoryStructure: string[];
|
|
46
|
+
/** Content of README.md if present */
|
|
47
|
+
readme: string | null;
|
|
48
|
+
/** Content of package.json if present */
|
|
49
|
+
packageJson: {
|
|
50
|
+
name?: string;
|
|
51
|
+
description?: string;
|
|
52
|
+
workspaces?: string[];
|
|
53
|
+
} | null;
|
|
54
|
+
/** Existing lane definitions if any */
|
|
55
|
+
existingLanes: string[];
|
|
56
|
+
/** Whether docs directory exists */
|
|
57
|
+
hasDocsDir: boolean;
|
|
58
|
+
/** Whether apps directory exists (common in monorepos) */
|
|
59
|
+
hasAppsDir: boolean;
|
|
60
|
+
/** Whether packages directory exists (monorepo indicator) */
|
|
61
|
+
hasPackagesDir: boolean;
|
|
62
|
+
/** Whether it's a monorepo */
|
|
63
|
+
isMonorepo: boolean;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Result of lane suggestion including suggestions and metadata
|
|
67
|
+
*/
|
|
68
|
+
export interface LaneSuggestResult {
|
|
69
|
+
suggestions: LaneSuggestion[];
|
|
70
|
+
context: {
|
|
71
|
+
packageCount: number;
|
|
72
|
+
docsFound: boolean;
|
|
73
|
+
existingConfig: boolean;
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Gather project context from the filesystem
|
|
78
|
+
*/
|
|
79
|
+
export declare function gatherProjectContext(projectRoot: string): ProjectContext;
|
|
80
|
+
/**
|
|
81
|
+
* Generate the system prompt for lane suggestion
|
|
82
|
+
*/
|
|
83
|
+
export declare function generateSystemPrompt(): string;
|
|
84
|
+
/**
|
|
85
|
+
* Generate the user prompt with project context
|
|
86
|
+
*/
|
|
87
|
+
export declare function generateUserPrompt(context: ProjectContext): string;
|
|
88
|
+
/**
|
|
89
|
+
* Parse and validate LLM response
|
|
90
|
+
*/
|
|
91
|
+
export declare function parseLLMResponse(response: string): LaneSuggestResponse;
|
|
92
|
+
/**
|
|
93
|
+
* Validate that a lane name follows the "Parent: Sublane" format
|
|
94
|
+
*/
|
|
95
|
+
export declare function isValidLaneFormat(lane: string): boolean;
|
|
96
|
+
/**
|
|
97
|
+
* Generate a dry-run preview of what would be sent to the LLM
|
|
98
|
+
*/
|
|
99
|
+
export declare function generateDryRunPreview(projectRoot: string): {
|
|
100
|
+
context: ProjectContext;
|
|
101
|
+
systemPrompt: string;
|
|
102
|
+
userPrompt: string;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* Get default lane suggestions for a minimal project
|
|
106
|
+
* Used when LLM is not available or for dry-run
|
|
107
|
+
*/
|
|
108
|
+
export declare function getDefaultSuggestions(context: ProjectContext): LaneSuggestion[];
|