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