@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,245 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Code Paths Overlap Detection
|
|
3
|
+
*
|
|
4
|
+
* Two-step algorithm for detecting code path conflicts between Work Units:
|
|
5
|
+
* 1. Static glob containment check (fast pre-filter using pattern matching)
|
|
6
|
+
* 2. Concrete file intersection (authoritative using filesystem)
|
|
7
|
+
*
|
|
8
|
+
* @module code-paths-overlap
|
|
9
|
+
*/
|
|
10
|
+
import { readFileSync, existsSync } from 'fs';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import yaml from 'js-yaml';
|
|
13
|
+
import fg from 'fast-glob';
|
|
14
|
+
import micromatch from 'micromatch';
|
|
15
|
+
import { STATUS_SECTIONS, BACKLOG_SECTIONS, STRING_LITERALS } from './wu-constants.js';
|
|
16
|
+
/**
|
|
17
|
+
* Check for code path overlap between two sets of glob patterns
|
|
18
|
+
*
|
|
19
|
+
* Uses two-step algorithm:
|
|
20
|
+
* - Static check: Fast pattern analysis to detect obvious containment
|
|
21
|
+
* - Concrete check: Filesystem-based expansion to find actual file intersection
|
|
22
|
+
*
|
|
23
|
+
* @param {string[]} claimingPaths - Glob patterns from WU being claimed
|
|
24
|
+
* @param {string[]} existingPaths - Glob patterns from in-progress WU
|
|
25
|
+
* @returns {{
|
|
26
|
+
* overlaps: boolean,
|
|
27
|
+
* type: 'none'|'concrete'|'ambiguous',
|
|
28
|
+
* files: string[]
|
|
29
|
+
* }} Overlap detection result
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* checkOverlap(['apps/web/**'], ['apps/web/prompts/**'])
|
|
33
|
+
* // => { overlaps: true, type: 'concrete', files: [...] }
|
|
34
|
+
*/
|
|
35
|
+
export function checkOverlap(claimingPaths, existingPaths) {
|
|
36
|
+
// Handle empty inputs
|
|
37
|
+
if (!claimingPaths || claimingPaths.length === 0) {
|
|
38
|
+
return { overlaps: false, type: 'none', files: [] };
|
|
39
|
+
}
|
|
40
|
+
if (!existingPaths || existingPaths.length === 0) {
|
|
41
|
+
return { overlaps: false, type: 'none', files: [] };
|
|
42
|
+
}
|
|
43
|
+
// Step 1: Static check (fast pre-filter)
|
|
44
|
+
// Check if any pattern pair has static containment
|
|
45
|
+
let hasStaticOverlap = false;
|
|
46
|
+
for (const claiming of claimingPaths) {
|
|
47
|
+
for (const existing of existingPaths) {
|
|
48
|
+
// Bidirectional check: A contains B OR B contains A
|
|
49
|
+
if (staticGlobContainment(claiming, existing) || staticGlobContainment(existing, claiming)) {
|
|
50
|
+
hasStaticOverlap = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (hasStaticOverlap)
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
// Step 2: Concrete check (authoritative)
|
|
58
|
+
// Expand globs and find actual file intersection
|
|
59
|
+
const allFiles = new Set();
|
|
60
|
+
for (const claiming of claimingPaths) {
|
|
61
|
+
for (const existing of existingPaths) {
|
|
62
|
+
const result = concreteFileIntersection(claiming, existing);
|
|
63
|
+
if (result.overlaps) {
|
|
64
|
+
result.files.forEach((f) => allFiles.add(f));
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const hasConcreteOverlap = allFiles.size > 0;
|
|
69
|
+
// Decision logic:
|
|
70
|
+
// - Both static and concrete → BLOCK (concrete overlap)
|
|
71
|
+
// - Static only, no concrete → WARN (ambiguous)
|
|
72
|
+
// - Neither → ALLOW (none)
|
|
73
|
+
if (hasConcreteOverlap) {
|
|
74
|
+
return {
|
|
75
|
+
overlaps: true,
|
|
76
|
+
type: 'concrete',
|
|
77
|
+
files: [...allFiles].sort(),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
else if (hasStaticOverlap && !hasConcreteOverlap) {
|
|
81
|
+
return {
|
|
82
|
+
overlaps: false,
|
|
83
|
+
type: 'ambiguous',
|
|
84
|
+
files: [],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
return {
|
|
89
|
+
overlaps: false,
|
|
90
|
+
type: 'none',
|
|
91
|
+
files: [],
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Find all in-progress WUs with overlapping code paths
|
|
97
|
+
*
|
|
98
|
+
* Reads status.md to find in-progress WUs, loads their code_paths,
|
|
99
|
+
* and checks for overlaps with the claiming WU's paths.
|
|
100
|
+
*
|
|
101
|
+
* @param {string} statusPath - Path to status.md file
|
|
102
|
+
* @param {string[]} claimingPaths - Glob patterns from WU being claimed
|
|
103
|
+
* @param {string} claimingWU - WU ID being claimed (excluded from check)
|
|
104
|
+
* @returns {{
|
|
105
|
+
* conflicts: Array<{wuid: string, overlaps: string[]}>,
|
|
106
|
+
* hasBlocker: boolean
|
|
107
|
+
* }} List of conflicting WUs and whether to block claim
|
|
108
|
+
*
|
|
109
|
+
* @example
|
|
110
|
+
* detectConflicts('docs/04-operations/tasks/status.md', ['apps/**'], 'WU-901')
|
|
111
|
+
* // => { conflicts: [{wuid: 'WU-900', overlaps: ['apps/web/foo.ts']}], hasBlocker: true }
|
|
112
|
+
*/
|
|
113
|
+
export function detectConflicts(statusPath, claimingPaths, claimingWU) {
|
|
114
|
+
// Handle empty claiming paths
|
|
115
|
+
if (!claimingPaths || claimingPaths.length === 0) {
|
|
116
|
+
return { conflicts: [], hasBlocker: false };
|
|
117
|
+
}
|
|
118
|
+
// Read status.md
|
|
119
|
+
const content = readFileSync(statusPath, { encoding: 'utf-8' });
|
|
120
|
+
const lines = content.split(STRING_LITERALS.NEWLINE);
|
|
121
|
+
// Find "## In Progress" section (handles both status.md and backlog.md formats)
|
|
122
|
+
const inProgressIdx = lines.findIndex((l) => {
|
|
123
|
+
const normalized = l.trim().toLowerCase();
|
|
124
|
+
return (normalized === STATUS_SECTIONS.IN_PROGRESS.toLowerCase() ||
|
|
125
|
+
normalized === BACKLOG_SECTIONS.IN_PROGRESS.toLowerCase() ||
|
|
126
|
+
normalized.startsWith('## in progress'));
|
|
127
|
+
});
|
|
128
|
+
if (inProgressIdx === -1) {
|
|
129
|
+
return { conflicts: [], hasBlocker: false };
|
|
130
|
+
}
|
|
131
|
+
// Find end of In Progress section (next ## heading or end of file)
|
|
132
|
+
let endIdx = lines.slice(inProgressIdx + 1).findIndex((l) => l.startsWith('## '));
|
|
133
|
+
if (endIdx === -1)
|
|
134
|
+
endIdx = lines.length - inProgressIdx - 1;
|
|
135
|
+
else
|
|
136
|
+
endIdx = inProgressIdx + 1 + endIdx;
|
|
137
|
+
// Extract section content
|
|
138
|
+
const section = lines.slice(inProgressIdx + 1, endIdx).join(STRING_LITERALS.NEWLINE);
|
|
139
|
+
// Check for "No items" marker
|
|
140
|
+
if (section.includes('No items currently in progress')) {
|
|
141
|
+
return { conflicts: [], hasBlocker: false };
|
|
142
|
+
}
|
|
143
|
+
// Extract WU IDs from links like [WU-334 — Title](wu/WU-334.yaml)
|
|
144
|
+
const wuLinkPattern = /\[([A-Z]+-\d+)\s*—\s*[^\]]+\]\([^)]+\)/gi;
|
|
145
|
+
const matches = [...section.matchAll(wuLinkPattern)];
|
|
146
|
+
if (matches.length === 0) {
|
|
147
|
+
return { conflicts: [], hasBlocker: false };
|
|
148
|
+
}
|
|
149
|
+
// Compute project root from statusPath
|
|
150
|
+
// statusPath is at docs/04-operations/tasks/status.md
|
|
151
|
+
const tasksDir = path.dirname(statusPath); // docs/04-operations/tasks
|
|
152
|
+
const operationsDir = path.dirname(tasksDir); // docs/04-operations
|
|
153
|
+
const docsDir = path.dirname(operationsDir); // docs
|
|
154
|
+
const projectRoot = path.dirname(docsDir); // project root
|
|
155
|
+
// Check each in-progress WU for overlaps
|
|
156
|
+
const conflicts = [];
|
|
157
|
+
for (const match of matches) {
|
|
158
|
+
const activeWuid = match[1]; // e.g., "WU-334"
|
|
159
|
+
// Skip claiming WU (shouldn't conflict with itself)
|
|
160
|
+
if (activeWuid === claimingWU) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
// Read WU YAML
|
|
164
|
+
const wuPath = path.join(projectRoot, 'docs', '04-operations', 'tasks', 'wu', `${activeWuid}.yaml`);
|
|
165
|
+
if (!existsSync(wuPath)) {
|
|
166
|
+
continue; // Skip if YAML doesn't exist
|
|
167
|
+
}
|
|
168
|
+
const wuContent = readFileSync(wuPath, { encoding: 'utf-8' });
|
|
169
|
+
const wuDoc = yaml.load(wuContent);
|
|
170
|
+
// Extract code_paths (skip if not defined)
|
|
171
|
+
const existingPaths = wuDoc?.code_paths;
|
|
172
|
+
if (!existingPaths || existingPaths.length === 0) {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
// Check for overlap
|
|
176
|
+
const overlapResult = checkOverlap(claimingPaths, existingPaths);
|
|
177
|
+
// Only record concrete overlaps (block on real conflicts)
|
|
178
|
+
if (overlapResult.overlaps && overlapResult.type === 'concrete') {
|
|
179
|
+
conflicts.push({
|
|
180
|
+
wuid: activeWuid,
|
|
181
|
+
overlaps: overlapResult.files,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
conflicts,
|
|
187
|
+
hasBlocker: conflicts.length > 0,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Static glob containment check (internal helper)
|
|
192
|
+
*
|
|
193
|
+
* Tests if patternA contains patternB using glob semantics.
|
|
194
|
+
* Uses micromatch to convert globs to regex and test containment.
|
|
195
|
+
*
|
|
196
|
+
* @private
|
|
197
|
+
* @param {string} patternA - First glob pattern
|
|
198
|
+
* @param {string} patternB - Second glob pattern
|
|
199
|
+
* @returns {boolean} True if patternA contains patternB
|
|
200
|
+
*
|
|
201
|
+
* @example
|
|
202
|
+
* staticGlobContainment('apps/**', 'apps/web/**') // => true
|
|
203
|
+
* staticGlobContainment('apps/web/**', 'packages/**') // => false
|
|
204
|
+
*/
|
|
205
|
+
function staticGlobContainment(patternA, patternB) {
|
|
206
|
+
// Convert patternB to a test path by replacing wildcards
|
|
207
|
+
// Example: 'apps/web/**' → 'apps/web/test/file.ts'
|
|
208
|
+
const testPath = patternB.replace(/\*\*/g, 'test/nested').replace(/\*/g, 'testfile');
|
|
209
|
+
// Use micromatch to test if patternA would match this test path
|
|
210
|
+
// If patternA matches the test path, it contains patternB
|
|
211
|
+
return micromatch.isMatch(testPath, patternA);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Concrete file intersection using filesystem (internal helper)
|
|
215
|
+
*
|
|
216
|
+
* Expands glob patterns to real files using fast-glob,
|
|
217
|
+
* then computes Set intersection to find overlapping files.
|
|
218
|
+
*
|
|
219
|
+
* @private
|
|
220
|
+
* @param {string} patternA - First glob pattern
|
|
221
|
+
* @param {string} patternB - Second glob pattern
|
|
222
|
+
* @returns {{overlaps: boolean, files: string[]}} Intersection result
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* concreteFileIntersection('apps/web/**', 'apps/web/prompts/**')
|
|
226
|
+
* // => { overlaps: true, files: ['apps/web/prompts/base.yaml'] }
|
|
227
|
+
*/
|
|
228
|
+
function concreteFileIntersection(patternA, patternB) {
|
|
229
|
+
// Expand globs to real files using fast-glob
|
|
230
|
+
// Use sync for simplicity (wu:claim is not performance-critical)
|
|
231
|
+
const filesA = new Set(fg.sync(patternA, {
|
|
232
|
+
dot: true, // Include dotfiles
|
|
233
|
+
ignore: ['node_modules/**', '.git/**'], // Exclude common bloat
|
|
234
|
+
}));
|
|
235
|
+
const filesB = new Set(fg.sync(patternB, {
|
|
236
|
+
dot: true,
|
|
237
|
+
ignore: ['node_modules/**', '.git/**'],
|
|
238
|
+
}));
|
|
239
|
+
// Compute intersection: files in both sets
|
|
240
|
+
const intersection = [...filesA].filter((file) => filesB.has(file));
|
|
241
|
+
return {
|
|
242
|
+
overlaps: intersection.length > 0,
|
|
243
|
+
files: intersection,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Commands Logger
|
|
3
|
+
* Part of WU-630: Detective layer (Layer 3 of 4-layer defense)
|
|
4
|
+
* Part of WU-1552: Complete logging integration with user/outcome tracking
|
|
5
|
+
*
|
|
6
|
+
* Logs all git commands to .beacon/commands.log for post-execution analysis.
|
|
7
|
+
* Provides defense-in-depth even if git shim is bypassed.
|
|
8
|
+
*
|
|
9
|
+
* Log format (v2): timestamp | command | branch | worktree | user | outcome
|
|
10
|
+
* Example: 2025-10-24T10:30:00.000Z | git status | lane/operations/wu-630 | worktrees/operations-wu-630 | agent | allowed
|
|
11
|
+
*
|
|
12
|
+
* Legacy format (v1): timestamp | command | branch | worktree
|
|
13
|
+
* - Backward compatible: old entries are parsed with user='unknown' and outcome='unknown'
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Command logging constants (WU-1552)
|
|
17
|
+
*
|
|
18
|
+
* User types and outcome values for audit trail logging.
|
|
19
|
+
*/
|
|
20
|
+
export declare const COMMAND_LOG: {
|
|
21
|
+
/** User types: who initiated the command */
|
|
22
|
+
USER: {
|
|
23
|
+
AGENT: string;
|
|
24
|
+
HUMAN: string;
|
|
25
|
+
UNKNOWN: string;
|
|
26
|
+
};
|
|
27
|
+
/** Outcome values: what happened to the command */
|
|
28
|
+
OUTCOME: {
|
|
29
|
+
ALLOWED: string;
|
|
30
|
+
BLOCKED: string;
|
|
31
|
+
UNKNOWN: string;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Options for logging git commands (WU-1552)
|
|
36
|
+
*/
|
|
37
|
+
export interface LogGitCommandOptions {
|
|
38
|
+
/** User type: 'agent' | 'human' | 'unknown' */
|
|
39
|
+
user?: string;
|
|
40
|
+
/** Command outcome: 'allowed' | 'blocked' */
|
|
41
|
+
outcome?: string;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Log a git command to the commands log
|
|
45
|
+
* @param {string[]} args - Git command arguments (e.g., ['status'], ['add', '.'])
|
|
46
|
+
* @param {string} logPath - Path to log file (defaults to .beacon/commands.log)
|
|
47
|
+
* @param {LogGitCommandOptions} options - Additional logging options (WU-1552)
|
|
48
|
+
*/
|
|
49
|
+
export declare function logGitCommand(args: any, logPath?: string, options?: LogGitCommandOptions): void;
|
|
50
|
+
/**
|
|
51
|
+
* Parse a log entry line into structured data
|
|
52
|
+
* Supports both v1 (4 fields) and v2 (6 fields) formats.
|
|
53
|
+
*
|
|
54
|
+
* @param {string} line - Log line to parse
|
|
55
|
+
* @returns {{timestamp: string, command: string, branch: string, worktree: string, user: string, outcome: string} | null}
|
|
56
|
+
*/
|
|
57
|
+
export declare function parseLogEntry(line: any): {
|
|
58
|
+
timestamp: any;
|
|
59
|
+
command: any;
|
|
60
|
+
branch: any;
|
|
61
|
+
worktree: any;
|
|
62
|
+
user: any;
|
|
63
|
+
outcome: any;
|
|
64
|
+
};
|
|
65
|
+
/**
|
|
66
|
+
* Scan log for violations within a session window
|
|
67
|
+
* @param {string} logPath - Path to commands log
|
|
68
|
+
* @param {number} windowMinutes - Session window in minutes (default 60)
|
|
69
|
+
* @returns {Array<{timestamp: string, command: string, branch: string, worktree: string}>}
|
|
70
|
+
*/
|
|
71
|
+
export declare function scanLogForViolations(logPath?: string, windowMinutes?: number): any[];
|
|
72
|
+
/**
|
|
73
|
+
* Rotate log by removing entries older than retention period
|
|
74
|
+
* @param {string} logPath - Path to commands log
|
|
75
|
+
* @param {number} retentionDays - Number of days to keep (default 7)
|
|
76
|
+
*/
|
|
77
|
+
export declare function rotateLog(logPath?: string, retentionDays?: number): void;
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Commands Logger
|
|
3
|
+
* Part of WU-630: Detective layer (Layer 3 of 4-layer defense)
|
|
4
|
+
* Part of WU-1552: Complete logging integration with user/outcome tracking
|
|
5
|
+
*
|
|
6
|
+
* Logs all git commands to .beacon/commands.log for post-execution analysis.
|
|
7
|
+
* Provides defense-in-depth even if git shim is bypassed.
|
|
8
|
+
*
|
|
9
|
+
* Log format (v2): timestamp | command | branch | worktree | user | outcome
|
|
10
|
+
* Example: 2025-10-24T10:30:00.000Z | git status | lane/operations/wu-630 | worktrees/operations-wu-630 | agent | allowed
|
|
11
|
+
*
|
|
12
|
+
* Legacy format (v1): timestamp | command | branch | worktree
|
|
13
|
+
* - Backward compatible: old entries are parsed with user='unknown' and outcome='unknown'
|
|
14
|
+
*/
|
|
15
|
+
/* eslint-disable security/detect-non-literal-fs-filename */
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { getCurrentBranch, isMainWorktree } from './wu-helpers.js';
|
|
20
|
+
import { BEACON_PATHS, GIT_FLAGS, STRING_LITERALS } from './wu-constants.js';
|
|
21
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
22
|
+
const __dirname = path.dirname(__filename);
|
|
23
|
+
// Default log path (can be overridden for testing)
|
|
24
|
+
const DEFAULT_LOG_PATH = path.resolve(__dirname, '../..', BEACON_PATHS.COMMANDS_LOG);
|
|
25
|
+
// Banned patterns (same as git shim)
|
|
26
|
+
const BANNED_PATTERNS = [
|
|
27
|
+
{ command: 'reset', flags: [GIT_FLAGS.HARD] },
|
|
28
|
+
{ command: 'stash' },
|
|
29
|
+
{ command: 'clean', flags: [GIT_FLAGS.FD_SHORT, GIT_FLAGS.DF_SHORT] },
|
|
30
|
+
{ command: 'checkout', flags: [GIT_FLAGS.FORCE_SHORT, GIT_FLAGS.FORCE] },
|
|
31
|
+
{ command: 'push', flags: [GIT_FLAGS.FORCE, GIT_FLAGS.FORCE_SHORT] },
|
|
32
|
+
];
|
|
33
|
+
const BANNED_FLAGS = [GIT_FLAGS.NO_VERIFY, GIT_FLAGS.NO_GPG_SIGN];
|
|
34
|
+
/**
|
|
35
|
+
* Command logging constants (WU-1552)
|
|
36
|
+
*
|
|
37
|
+
* User types and outcome values for audit trail logging.
|
|
38
|
+
*/
|
|
39
|
+
export const COMMAND_LOG = {
|
|
40
|
+
/** User types: who initiated the command */
|
|
41
|
+
USER: {
|
|
42
|
+
AGENT: 'agent',
|
|
43
|
+
HUMAN: 'human',
|
|
44
|
+
UNKNOWN: 'unknown',
|
|
45
|
+
},
|
|
46
|
+
/** Outcome values: what happened to the command */
|
|
47
|
+
OUTCOME: {
|
|
48
|
+
ALLOWED: 'allowed',
|
|
49
|
+
BLOCKED: 'blocked',
|
|
50
|
+
UNKNOWN: 'unknown',
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Log a git command to the commands log
|
|
55
|
+
* @param {string[]} args - Git command arguments (e.g., ['status'], ['add', '.'])
|
|
56
|
+
* @param {string} logPath - Path to log file (defaults to .beacon/commands.log)
|
|
57
|
+
* @param {LogGitCommandOptions} options - Additional logging options (WU-1552)
|
|
58
|
+
*/
|
|
59
|
+
export function logGitCommand(args, logPath = DEFAULT_LOG_PATH, options = {}) {
|
|
60
|
+
try {
|
|
61
|
+
const timestamp = new Date().toISOString();
|
|
62
|
+
const command = `git ${args.join(' ')}`;
|
|
63
|
+
// Get current context
|
|
64
|
+
let branch, worktree;
|
|
65
|
+
if (process.env.TEST_MODE === 'true') {
|
|
66
|
+
branch = process.env.TEST_BRANCH || COMMAND_LOG.USER.UNKNOWN;
|
|
67
|
+
worktree = process.env.TEST_WORKTREE || COMMAND_LOG.USER.UNKNOWN;
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
branch = getCurrentBranch();
|
|
71
|
+
worktree = isMainWorktree() ? '.' : process.cwd();
|
|
72
|
+
}
|
|
73
|
+
// WU-1552: Extract user and outcome from options with defaults
|
|
74
|
+
const user = options.user || COMMAND_LOG.USER.UNKNOWN;
|
|
75
|
+
const outcome = options.outcome || COMMAND_LOG.OUTCOME.ALLOWED;
|
|
76
|
+
// New format (v2): timestamp | command | branch | worktree | user | outcome
|
|
77
|
+
const logEntry = `${timestamp} | ${command} | ${branch} | ${worktree} | ${user} | ${outcome}\n`;
|
|
78
|
+
// Ensure .beacon directory exists
|
|
79
|
+
const logDir = path.dirname(logPath);
|
|
80
|
+
if (!fs.existsSync(logDir)) {
|
|
81
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
// Append to log file
|
|
84
|
+
fs.appendFileSync(logPath, logEntry, { encoding: 'utf-8' });
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
// Don't fail git commands if logging fails
|
|
88
|
+
console.error(`[commands-logger] Warning: Failed to log command: ${error.message}`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Parse a log entry line into structured data
|
|
93
|
+
* Supports both v1 (4 fields) and v2 (6 fields) formats.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} line - Log line to parse
|
|
96
|
+
* @returns {{timestamp: string, command: string, branch: string, worktree: string, user: string, outcome: string} | null}
|
|
97
|
+
*/
|
|
98
|
+
export function parseLogEntry(line) {
|
|
99
|
+
if (!line || !line.trim()) {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
// Split on " | " (with spaces) to avoid splitting commands that contain pipes
|
|
103
|
+
const parts = line.split(' | ');
|
|
104
|
+
// Minimum 4 fields required (v1 format)
|
|
105
|
+
if (parts.length < 4) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
// v2 format: timestamp | command | branch | worktree | user | outcome (6 fields)
|
|
109
|
+
// v1 format: timestamp | command | branch | worktree (4 fields)
|
|
110
|
+
// Commands may contain pipes, so we need to handle variable field counts
|
|
111
|
+
const hasUserOutcome = parts.length >= 6;
|
|
112
|
+
if (hasUserOutcome) {
|
|
113
|
+
// v2 format: last 2 fields are user and outcome
|
|
114
|
+
const outcome = parts[parts.length - 1].trim();
|
|
115
|
+
const user = parts[parts.length - 2].trim();
|
|
116
|
+
const worktree = parts[parts.length - 3].trim();
|
|
117
|
+
const branch = parts[parts.length - 4].trim();
|
|
118
|
+
// Everything between timestamp and branch is the command (may contain pipes)
|
|
119
|
+
const command = parts
|
|
120
|
+
.slice(1, parts.length - 4)
|
|
121
|
+
.join(' | ')
|
|
122
|
+
.trim();
|
|
123
|
+
return {
|
|
124
|
+
timestamp: parts[0].trim(),
|
|
125
|
+
command,
|
|
126
|
+
branch,
|
|
127
|
+
worktree,
|
|
128
|
+
user,
|
|
129
|
+
outcome,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
else {
|
|
133
|
+
// v1 format (backward compatibility): no user/outcome fields
|
|
134
|
+
const worktree = parts[parts.length - 1].trim();
|
|
135
|
+
const branch = parts[parts.length - 2].trim();
|
|
136
|
+
const command = parts
|
|
137
|
+
.slice(1, parts.length - 2)
|
|
138
|
+
.join(' | ')
|
|
139
|
+
.trim();
|
|
140
|
+
return {
|
|
141
|
+
timestamp: parts[0].trim(),
|
|
142
|
+
command,
|
|
143
|
+
branch,
|
|
144
|
+
worktree,
|
|
145
|
+
user: COMMAND_LOG.USER.UNKNOWN,
|
|
146
|
+
outcome: COMMAND_LOG.OUTCOME.UNKNOWN,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Check if a command+context is a violation
|
|
152
|
+
* @param {string} command - The git command
|
|
153
|
+
* @param {string} branch - The branch it was run on
|
|
154
|
+
* @param {string} worktree - The worktree it was run in
|
|
155
|
+
* @returns {boolean}
|
|
156
|
+
*/
|
|
157
|
+
function isViolation(command, branch, worktree) {
|
|
158
|
+
// Protected context: main branch OR main worktree (.)
|
|
159
|
+
const isProtected = branch === 'main' || worktree === '.';
|
|
160
|
+
if (!isProtected) {
|
|
161
|
+
return false; // Allowed on lane branches in worktrees
|
|
162
|
+
}
|
|
163
|
+
// Parse command into args
|
|
164
|
+
const args = command.replace(/^git\s+/, '').split(/\s+/);
|
|
165
|
+
const commandName = args[0]?.toLowerCase();
|
|
166
|
+
const flags = args.slice(1).map((a) => a.toLowerCase());
|
|
167
|
+
// Check banned flags
|
|
168
|
+
for (const bannedFlag of BANNED_FLAGS) {
|
|
169
|
+
if (flags.includes(bannedFlag)) {
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Check banned command patterns
|
|
174
|
+
for (const pattern of BANNED_PATTERNS) {
|
|
175
|
+
if (commandName !== pattern.command)
|
|
176
|
+
continue;
|
|
177
|
+
// If no specific flags required, ban the command entirely
|
|
178
|
+
if (!pattern.flags) {
|
|
179
|
+
return true;
|
|
180
|
+
}
|
|
181
|
+
// Check if any required flag is present
|
|
182
|
+
const hasRequiredFlag = pattern.flags.some((reqFlag) => flags.includes(reqFlag));
|
|
183
|
+
if (hasRequiredFlag) {
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Scan log for violations within a session window
|
|
191
|
+
* @param {string} logPath - Path to commands log
|
|
192
|
+
* @param {number} windowMinutes - Session window in minutes (default 60)
|
|
193
|
+
* @returns {Array<{timestamp: string, command: string, branch: string, worktree: string}>}
|
|
194
|
+
*/
|
|
195
|
+
export function scanLogForViolations(logPath = DEFAULT_LOG_PATH, windowMinutes = 60) {
|
|
196
|
+
if (!fs.existsSync(logPath)) {
|
|
197
|
+
return [];
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const content = fs.readFileSync(logPath, { encoding: 'utf-8' });
|
|
201
|
+
const lines = content.trim().split(STRING_LITERALS.NEWLINE).filter(Boolean);
|
|
202
|
+
const now = new Date();
|
|
203
|
+
const windowStart = new Date(now.getTime() - windowMinutes * 60 * 1000);
|
|
204
|
+
const violations = [];
|
|
205
|
+
for (const line of lines) {
|
|
206
|
+
const entry = parseLogEntry(line);
|
|
207
|
+
if (!entry)
|
|
208
|
+
continue;
|
|
209
|
+
// Filter by session window
|
|
210
|
+
const entryTime = new Date(entry.timestamp);
|
|
211
|
+
if (entryTime < windowStart) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
// Check if this is a violation
|
|
215
|
+
if (isViolation(entry.command, entry.branch, entry.worktree)) {
|
|
216
|
+
violations.push(entry);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
return violations;
|
|
220
|
+
}
|
|
221
|
+
catch (error) {
|
|
222
|
+
console.error(`[commands-logger] Warning: Failed to scan log: ${error.message}`);
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Rotate log by removing entries older than retention period
|
|
228
|
+
* @param {string} logPath - Path to commands log
|
|
229
|
+
* @param {number} retentionDays - Number of days to keep (default 7)
|
|
230
|
+
*/
|
|
231
|
+
export function rotateLog(logPath = DEFAULT_LOG_PATH, retentionDays = 7) {
|
|
232
|
+
if (!fs.existsSync(logPath)) {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
try {
|
|
236
|
+
const content = fs.readFileSync(logPath, { encoding: 'utf-8' });
|
|
237
|
+
const lines = content.trim().split(STRING_LITERALS.NEWLINE).filter(Boolean);
|
|
238
|
+
const now = new Date();
|
|
239
|
+
const cutoffDate = new Date(now.getTime() - retentionDays * 24 * 60 * 60 * 1000);
|
|
240
|
+
const recentLines = lines.filter((line) => {
|
|
241
|
+
const entry = parseLogEntry(line);
|
|
242
|
+
if (!entry)
|
|
243
|
+
return false;
|
|
244
|
+
const entryTime = new Date(entry.timestamp);
|
|
245
|
+
return entryTime >= cutoffDate;
|
|
246
|
+
});
|
|
247
|
+
// Write back only recent entries
|
|
248
|
+
fs.writeFileSync(logPath, recentLines.join(STRING_LITERALS.NEWLINE) +
|
|
249
|
+
(recentLines.length > 0 ? STRING_LITERALS.NEWLINE : ''), { encoding: 'utf-8' });
|
|
250
|
+
}
|
|
251
|
+
catch (error) {
|
|
252
|
+
console.error(`[commands-logger] Warning: Failed to rotate log: ${error.message}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-2278: Commit Message Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for processing commit messages according to commitlint rules.
|
|
5
|
+
* Specifically handles lowercasing of commit subjects.
|
|
6
|
+
*
|
|
7
|
+
* Note: This is internal LumenFlow tooling - no external library applies.
|
|
8
|
+
*
|
|
9
|
+
* @module commit-message-utils
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Lowercase the entire subject of a conventional commit message
|
|
13
|
+
*
|
|
14
|
+
* commitlint requires lowercase subjects, but the prepare-commit-msg hook
|
|
15
|
+
* was only lowercasing the first character. This function lowercases
|
|
16
|
+
* the entire subject portion.
|
|
17
|
+
*
|
|
18
|
+
* Examples:
|
|
19
|
+
* "feat(wu-100): Add Supabase integration" -> "feat(wu-100): add supabase integration"
|
|
20
|
+
* "fix: Fix OpenAI API call" -> "fix: fix openai api call"
|
|
21
|
+
*
|
|
22
|
+
* @param {string} message - Commit message (first line only)
|
|
23
|
+
* @returns {string} Message with lowercased subject
|
|
24
|
+
*/
|
|
25
|
+
export declare function lowercaseCommitSubject(message: any): any;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WU-2278: Commit Message Utilities
|
|
3
|
+
*
|
|
4
|
+
* Utilities for processing commit messages according to commitlint rules.
|
|
5
|
+
* Specifically handles lowercasing of commit subjects.
|
|
6
|
+
*
|
|
7
|
+
* Note: This is internal LumenFlow tooling - no external library applies.
|
|
8
|
+
*
|
|
9
|
+
* @module commit-message-utils
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Lowercase the entire subject of a conventional commit message
|
|
13
|
+
*
|
|
14
|
+
* commitlint requires lowercase subjects, but the prepare-commit-msg hook
|
|
15
|
+
* was only lowercasing the first character. This function lowercases
|
|
16
|
+
* the entire subject portion.
|
|
17
|
+
*
|
|
18
|
+
* Examples:
|
|
19
|
+
* "feat(wu-100): Add Supabase integration" -> "feat(wu-100): add supabase integration"
|
|
20
|
+
* "fix: Fix OpenAI API call" -> "fix: fix openai api call"
|
|
21
|
+
*
|
|
22
|
+
* @param {string} message - Commit message (first line only)
|
|
23
|
+
* @returns {string} Message with lowercased subject
|
|
24
|
+
*/
|
|
25
|
+
export function lowercaseCommitSubject(message) {
|
|
26
|
+
if (!message || typeof message !== 'string') {
|
|
27
|
+
return message;
|
|
28
|
+
}
|
|
29
|
+
// Match conventional commit format: type(scope): subject
|
|
30
|
+
// or type: subject
|
|
31
|
+
// Using indexOf for simple parsing - safer than complex regex
|
|
32
|
+
const colonIndex = message.indexOf(': ');
|
|
33
|
+
if (colonIndex > 0) {
|
|
34
|
+
const prefix = message.slice(0, colonIndex);
|
|
35
|
+
const subject = message.slice(colonIndex + 2);
|
|
36
|
+
const lowercaseSubject = subject.toLowerCase();
|
|
37
|
+
return `${prefix.toLowerCase()}: ${lowercaseSubject}`;
|
|
38
|
+
}
|
|
39
|
+
// No conventional format - lowercase entire message
|
|
40
|
+
return message.toLowerCase();
|
|
41
|
+
}
|