@lumenflow/cli 2.18.3 → 2.19.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/README.md +42 -41
- package/dist/delegation-list.js +140 -0
- package/dist/delegation-list.js.map +1 -0
- package/dist/doctor.js +35 -99
- package/dist/doctor.js.map +1 -1
- package/dist/gates-plan-resolvers.js +150 -0
- package/dist/gates-plan-resolvers.js.map +1 -0
- package/dist/gates-runners.js +533 -0
- package/dist/gates-runners.js.map +1 -0
- package/dist/gates-types.js +3 -0
- package/dist/gates-types.js.map +1 -1
- package/dist/gates-utils.js +316 -0
- package/dist/gates-utils.js.map +1 -0
- package/dist/gates.js +44 -1016
- package/dist/gates.js.map +1 -1
- package/dist/hooks/enforcement-generator.js +16 -880
- package/dist/hooks/enforcement-generator.js.map +1 -1
- package/dist/hooks/enforcement-sync.js +1 -4
- package/dist/hooks/enforcement-sync.js.map +1 -1
- package/dist/hooks/generators/auto-checkpoint.js +123 -0
- package/dist/hooks/generators/auto-checkpoint.js.map +1 -0
- package/dist/hooks/generators/enforce-worktree.js +188 -0
- package/dist/hooks/generators/enforce-worktree.js.map +1 -0
- package/dist/hooks/generators/index.js +16 -0
- package/dist/hooks/generators/index.js.map +1 -0
- package/dist/hooks/generators/pre-compact-checkpoint.js +134 -0
- package/dist/hooks/generators/pre-compact-checkpoint.js.map +1 -0
- package/dist/hooks/generators/require-wu.js +115 -0
- package/dist/hooks/generators/require-wu.js.map +1 -0
- package/dist/hooks/generators/session-start-recovery.js +101 -0
- package/dist/hooks/generators/session-start-recovery.js.map +1 -0
- package/dist/hooks/generators/signal-utils.js +52 -0
- package/dist/hooks/generators/signal-utils.js.map +1 -0
- package/dist/hooks/generators/warn-incomplete.js +65 -0
- package/dist/hooks/generators/warn-incomplete.js.map +1 -0
- package/dist/init-detection.js +228 -0
- package/dist/init-detection.js.map +1 -0
- package/dist/init-scaffolding.js +146 -0
- package/dist/init-scaffolding.js.map +1 -0
- package/dist/init-templates.js +1928 -0
- package/dist/init-templates.js.map +1 -0
- package/dist/init.js +136 -2425
- package/dist/init.js.map +1 -1
- package/dist/initiative-edit.js +42 -11
- package/dist/initiative-edit.js.map +1 -1
- package/dist/initiative-remove-wu.js +0 -0
- package/dist/initiative-status.js +29 -2
- package/dist/initiative-status.js.map +1 -1
- package/dist/mem-context.js +22 -9
- package/dist/mem-context.js.map +1 -1
- package/dist/orchestrate-init-status.js +32 -1
- package/dist/orchestrate-init-status.js.map +1 -1
- package/dist/orchestrate-monitor.js +38 -38
- package/dist/orchestrate-monitor.js.map +1 -1
- package/dist/public-manifest.js +12 -5
- package/dist/public-manifest.js.map +1 -1
- package/dist/shared-validators.js +1 -0
- package/dist/shared-validators.js.map +1 -1
- package/dist/spawn-list.js +0 -0
- package/dist/wu-claim-branch.js +121 -0
- package/dist/wu-claim-branch.js.map +1 -0
- package/dist/wu-claim-output.js +83 -0
- package/dist/wu-claim-output.js.map +1 -0
- package/dist/wu-claim-resume-handler.js +85 -0
- package/dist/wu-claim-resume-handler.js.map +1 -0
- package/dist/wu-claim-state.js +572 -0
- package/dist/wu-claim-state.js.map +1 -0
- package/dist/wu-claim-validation.js +439 -0
- package/dist/wu-claim-validation.js.map +1 -0
- package/dist/wu-claim-worktree.js +221 -0
- package/dist/wu-claim-worktree.js.map +1 -0
- package/dist/wu-claim.js +54 -1402
- package/dist/wu-claim.js.map +1 -1
- package/dist/wu-create-content.js +254 -0
- package/dist/wu-create-content.js.map +1 -0
- package/dist/wu-create-readiness.js +57 -0
- package/dist/wu-create-readiness.js.map +1 -0
- package/dist/wu-create-validation.js +149 -0
- package/dist/wu-create-validation.js.map +1 -0
- package/dist/wu-create.js +39 -441
- package/dist/wu-create.js.map +1 -1
- package/dist/wu-done.js +144 -249
- package/dist/wu-done.js.map +1 -1
- package/dist/wu-edit-operations.js +432 -0
- package/dist/wu-edit-operations.js.map +1 -0
- package/dist/wu-edit-validators.js +280 -0
- package/dist/wu-edit-validators.js.map +1 -0
- package/dist/wu-edit.js +27 -713
- package/dist/wu-edit.js.map +1 -1
- package/dist/wu-prep.js +32 -2
- package/dist/wu-prep.js.map +1 -1
- package/dist/wu-repair.js +1 -1
- package/dist/wu-repair.js.map +1 -1
- package/dist/wu-spawn-prompt-builders.js +1123 -0
- package/dist/wu-spawn-prompt-builders.js.map +1 -0
- package/dist/wu-spawn-strategy-resolver.js +314 -0
- package/dist/wu-spawn-strategy-resolver.js.map +1 -0
- package/dist/wu-spawn.js +9 -1398
- package/dist/wu-spawn.js.map +1 -1
- package/package.json +10 -7
- package/templates/core/LUMENFLOW.md.template +29 -99
- package/templates/core/ai/onboarding/agent-invocation-guide.md.template +1 -1
- package/templates/core/ai/onboarding/quick-ref-commands.md.template +29 -4
- package/templates/vendors/claude/.claude/skills/orchestration/SKILL.md.template +8 -8
package/dist/gates.js
CHANGED
|
@@ -4,12 +4,18 @@
|
|
|
4
4
|
*
|
|
5
5
|
* Runs quality gates with support for docs-only mode and incremental linting.
|
|
6
6
|
*
|
|
7
|
+
* WU-1647: Refactored into focused modules:
|
|
8
|
+
* - gates-utils.ts: Shell helpers, logging, git helpers, WU helpers
|
|
9
|
+
* - gates-plan-resolvers.ts: Pure plan resolution (format, lint, test, spec-linter)
|
|
10
|
+
* - gates-runners.ts: Gate runner functions (format, lint, test, safety, integration, etc.)
|
|
11
|
+
* - gates.ts (this file): Orchestrator, CLI options, main entry point
|
|
12
|
+
*
|
|
7
13
|
* WU-1304: Optimise ESLint gates performance
|
|
8
14
|
* - Uses incremental linting (only files changed since branching from main)
|
|
9
15
|
* - Full lint coverage maintained via CI workflow
|
|
10
16
|
*
|
|
11
17
|
* WU-1433: Coverage gate with mode flag
|
|
12
|
-
* - Checks coverage thresholds for hex core files (
|
|
18
|
+
* - Checks coverage thresholds for hex core files (>=90% for application layer)
|
|
13
19
|
* - Mode: block (default) fails the gate, warn logs warnings only
|
|
14
20
|
* WU-2334: Changed default from warn to block for TDD enforcement
|
|
15
21
|
*
|
|
@@ -18,8 +24,8 @@
|
|
|
18
24
|
* - Fails if any table is missing documentation
|
|
19
25
|
*
|
|
20
26
|
* For type:documentation WUs:
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
27
|
+
* - Run: format:check, spec:linter, backlog-sync
|
|
28
|
+
* - Skip: lint, typecheck, supabase-docs:linter, tests, coverage (no code changed)
|
|
23
29
|
*
|
|
24
30
|
* WU-1920: Incremental test execution
|
|
25
31
|
* - Uses Vitest's --changed flag to run only tests for changed files
|
|
@@ -38,45 +44,36 @@
|
|
|
38
44
|
* node tools/gates.ts --full-tests # Full tests (bypass incremental)
|
|
39
45
|
* node tools/gates.ts --coverage-mode=block # Coverage gate in block mode
|
|
40
46
|
*/
|
|
41
|
-
import {
|
|
42
|
-
import { closeSync, mkdirSync, openSync, readSync, statSync, writeSync } from 'node:fs';
|
|
43
|
-
import { access } from 'node:fs/promises';
|
|
44
|
-
import path from 'node:path';
|
|
47
|
+
import { writeSync } from 'node:fs';
|
|
45
48
|
import { emitGateEvent, getCurrentWU, getCurrentLane } from '@lumenflow/core/telemetry';
|
|
46
49
|
import { die } from '@lumenflow/core/error-handler';
|
|
47
|
-
|
|
48
|
-
import { readWURaw } from '@lumenflow/core/wu-yaml';
|
|
49
|
-
import { createWuPaths } from '@lumenflow/core/wu-paths';
|
|
50
|
-
import { getChangedLintableFiles, isLintableFile } from '@lumenflow/core/incremental-lint';
|
|
51
|
-
import { buildVitestChangedArgs, isCodeFilePath } from '@lumenflow/core/incremental-test';
|
|
52
|
-
import { createGitForPath } from '@lumenflow/core/git-adapter';
|
|
53
|
-
import { runCoverageGate, COVERAGE_GATE_MODES } from '@lumenflow/core/coverage-gate';
|
|
54
|
-
import { buildGatesLogPath, shouldUseGatesAgentMode, updateGatesLatestSymlink, } from '@lumenflow/core/gates-agent-mode';
|
|
50
|
+
import { shouldUseGatesAgentMode, updateGatesLatestSymlink, } from '@lumenflow/core/gates-agent-mode';
|
|
55
51
|
// WU-2062: Import risk detector for tiered test execution
|
|
56
52
|
import { detectRiskTier, RISK_TIERS } from '@lumenflow/core/risk-detector';
|
|
57
53
|
// WU-2252: Import invariants runner for first-check validation
|
|
58
54
|
import { runInvariants } from '@lumenflow/core/invariants-runner';
|
|
59
55
|
import { createWUParser } from '@lumenflow/core/arg-parser';
|
|
60
|
-
import {
|
|
61
|
-
|
|
62
|
-
import {
|
|
63
|
-
|
|
64
|
-
// WU-1191: Lane health gate configuration
|
|
65
|
-
// WU-1262: Coverage config from methodology policy
|
|
66
|
-
// WU-1280: Test policy for tests_required (warn vs block on test failures)
|
|
67
|
-
// WU-1356: Configurable package manager and test commands
|
|
68
|
-
import { loadLaneHealthConfig, resolveTestPolicy, resolveGatesCommands, resolveTestRunner, } from '@lumenflow/core/gates-config';
|
|
69
|
-
// WU-1191: Lane health check
|
|
70
|
-
import { runLaneHealthCheck } from './lane-health.js';
|
|
71
|
-
// WU-1315: Onboarding smoke test
|
|
72
|
-
import { runOnboardingSmokeTestGate } from './onboarding-smoke-test.js';
|
|
73
|
-
import { BRANCHES, PACKAGES, PKG_MANAGER, ESLINT_FLAGS, ESLINT_COMMANDS, ESLINT_DEFAULTS, SCRIPTS, CACHE_STRATEGIES, DIRECTORIES, GATE_NAMES, GATE_COMMANDS, EXIT_CODES, FILE_SYSTEM, PRETTIER_ARGS, PRETTIER_FLAGS, } from '@lumenflow/core/wu-constants';
|
|
56
|
+
import { runCoverageGate, COVERAGE_GATE_MODES } from '@lumenflow/core/coverage-gate';
|
|
57
|
+
// WU-1067: Config-driven gates support
|
|
58
|
+
import { loadLaneHealthConfig, resolveTestPolicy, resolveGatesCommands, } from '@lumenflow/core/gates-config';
|
|
59
|
+
import { GATE_NAMES, GATE_COMMANDS, EXIT_CODES } from '@lumenflow/core/wu-constants';
|
|
74
60
|
// WU-1520: Gates graceful degradation for missing optional scripts
|
|
75
61
|
import { buildMissingScriptWarning, loadPackageJsonScripts, resolveGateAction, formatGateSummary, } from './gates-graceful-degradation.js';
|
|
76
62
|
import { runCLI } from './cli-entry-point.js';
|
|
77
63
|
// WU-1550: Gate registry for declarative gate registration
|
|
78
64
|
import { GateRegistry } from './gate-registry.js';
|
|
79
65
|
import { registerDocsOnlyGates, registerCodeGates } from './gate-defaults.js';
|
|
66
|
+
// WU-1315: Onboarding smoke test
|
|
67
|
+
import { runOnboardingSmokeTestGate } from './onboarding-smoke-test.js';
|
|
68
|
+
// ── WU-1647: Import from extracted modules ─────────────────────────────
|
|
69
|
+
import { run, makeGateLogger, readLogTail, createAgentLogContext, emitFormatCheckGuidance, getAllChangedFiles, loadCurrentWUCodePaths, } from './gates-utils.js';
|
|
70
|
+
import { resolveDocsOnlyTestPlan, formatDocsOnlySkipMessage, } from './gates-plan-resolvers.js';
|
|
71
|
+
import { runFormatCheckGate, runIncrementalLint, runChangedTests, runSafetyCriticalTests, runIntegrationTests, runSpecLinterGate, runBacklogSyncGate, runSupabaseDocsGate, runSystemMapGate, runLaneHealthGate, runDocsOnlyFilteredTests, } from './gates-runners.js';
|
|
72
|
+
// ── Re-exports for backward compatibility ──────────────────────────────
|
|
73
|
+
// Tests and other modules import these from gates.ts; preserve the public API.
|
|
74
|
+
export { parsePrettierListOutput, buildPrettierWriteCommand, formatFormatCheckGuidance, parseWUFromBranchName, extractPackagesFromCodePaths, loadCurrentWUCodePaths, } from './gates-utils.js';
|
|
75
|
+
export { isPrettierConfigFile, isTestConfigFile, resolveFormatCheckPlan, resolveLintPlan, resolveTestPlan, resolveDocsOnlyTestPlan, formatDocsOnlySkipMessage, resolveSpecLinterPlan, } from './gates-plan-resolvers.js';
|
|
76
|
+
// ── CLI options ────────────────────────────────────────────────────────
|
|
80
77
|
/**
|
|
81
78
|
* WU-1087: Gates-specific option definitions for createWUParser.
|
|
82
79
|
* Exported for testing and consistency with other CLI commands.
|
|
@@ -162,973 +159,7 @@ export function parseGatesOptions() {
|
|
|
162
159
|
process.argv = originalArgv;
|
|
163
160
|
}
|
|
164
161
|
}
|
|
165
|
-
|
|
166
|
-
* @deprecated Use parseGatesOptions() instead (WU-1087)
|
|
167
|
-
* Kept for backward compatibility during migration.
|
|
168
|
-
*/
|
|
169
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Pre-existing: argv kept for backwards compatibility
|
|
170
|
-
function parseGatesArgs(argv = process.argv) {
|
|
171
|
-
return parseGatesOptions();
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Build a pnpm command string
|
|
175
|
-
*/
|
|
176
|
-
function pnpmCmd(...parts) {
|
|
177
|
-
return `${PKG_MANAGER} ${parts.join(' ')}`;
|
|
178
|
-
}
|
|
179
|
-
/**
|
|
180
|
-
* Build a pnpm run command string
|
|
181
|
-
*/
|
|
182
|
-
function pnpmRun(script, ...args) {
|
|
183
|
-
const argsStr = args.length > 0 ? ` ${args.join(' ')}` : '';
|
|
184
|
-
return `${PKG_MANAGER} ${SCRIPTS.RUN} ${script}${argsStr}`;
|
|
185
|
-
}
|
|
186
|
-
const PRETTIER_CONFIG_FILES = new Set([
|
|
187
|
-
'.prettierrc',
|
|
188
|
-
'.prettierrc.json',
|
|
189
|
-
'.prettierrc.yaml',
|
|
190
|
-
'.prettierrc.yml',
|
|
191
|
-
'.prettierrc.js',
|
|
192
|
-
'.prettierrc.cjs',
|
|
193
|
-
'.prettierrc.ts',
|
|
194
|
-
'prettier.config.js',
|
|
195
|
-
'prettier.config.cjs',
|
|
196
|
-
'prettier.config.ts',
|
|
197
|
-
'prettier.config.mjs',
|
|
198
|
-
'.prettierignore',
|
|
199
|
-
]);
|
|
200
|
-
// WU-1356: Extended to support multiple build tools and test runners
|
|
201
|
-
const TEST_CONFIG_BASENAMES = new Set([
|
|
202
|
-
'turbo.json', // Turborepo
|
|
203
|
-
'nx.json', // Nx
|
|
204
|
-
'lerna.json', // Lerna
|
|
205
|
-
'pnpm-lock.yaml',
|
|
206
|
-
'package-lock.json',
|
|
207
|
-
'yarn.lock',
|
|
208
|
-
'bun.lockb',
|
|
209
|
-
'package.json',
|
|
210
|
-
]);
|
|
211
|
-
// WU-1356: Extended to support vitest, jest, and mocha config patterns
|
|
212
|
-
const TEST_CONFIG_PATTERNS = [
|
|
213
|
-
/^vitest\.config\.(ts|mts|js|mjs|cjs)$/i,
|
|
214
|
-
/^jest\.config\.(ts|js|mjs|cjs|json)$/i,
|
|
215
|
-
/^\.mocharc\.(js|json|yaml|yml)$/i,
|
|
216
|
-
// eslint-disable-next-line security/detect-unsafe-regex -- static tsconfig pattern; no backtracking risk
|
|
217
|
-
/^tsconfig(\..+)?\.json$/i,
|
|
218
|
-
];
|
|
219
|
-
function normalizePath(filePath) {
|
|
220
|
-
return filePath.replace(/\\/g, '/');
|
|
221
|
-
}
|
|
222
|
-
function getBasename(filePath) {
|
|
223
|
-
const normalized = normalizePath(filePath);
|
|
224
|
-
const parts = normalized.split('/');
|
|
225
|
-
return parts[parts.length - 1] || normalized;
|
|
226
|
-
}
|
|
227
|
-
function quoteShellArgs(files) {
|
|
228
|
-
return files.map((file) => `"${file}"`).join(' ');
|
|
229
|
-
}
|
|
230
|
-
export function isPrettierConfigFile(filePath) {
|
|
231
|
-
if (!filePath)
|
|
232
|
-
return false;
|
|
233
|
-
const basename = getBasename(filePath);
|
|
234
|
-
return PRETTIER_CONFIG_FILES.has(basename);
|
|
235
|
-
}
|
|
236
|
-
export function isTestConfigFile(filePath) {
|
|
237
|
-
if (!filePath)
|
|
238
|
-
return false;
|
|
239
|
-
const basename = getBasename(filePath);
|
|
240
|
-
if (TEST_CONFIG_BASENAMES.has(basename)) {
|
|
241
|
-
return true;
|
|
242
|
-
}
|
|
243
|
-
return TEST_CONFIG_PATTERNS.some((pattern) => pattern.test(basename));
|
|
244
|
-
}
|
|
245
|
-
/* eslint-disable sonarjs/no-duplicate-string -- Pre-existing: format check reasons are intentionally distinct string literals */
|
|
246
|
-
export function resolveFormatCheckPlan({ changedFiles, fileListError = false, }) {
|
|
247
|
-
if (fileListError) {
|
|
248
|
-
return { mode: 'full', files: [], reason: 'file-list-error' };
|
|
249
|
-
}
|
|
250
|
-
if (changedFiles.some(isPrettierConfigFile)) {
|
|
251
|
-
return { mode: 'full', files: [], reason: 'prettier-config' };
|
|
252
|
-
}
|
|
253
|
-
if (changedFiles.length === 0) {
|
|
254
|
-
return { mode: 'skip', files: [] };
|
|
255
|
-
}
|
|
256
|
-
return { mode: 'incremental', files: changedFiles };
|
|
257
|
-
}
|
|
258
|
-
export function resolveLintPlan({ isMainBranch, changedFiles, }) {
|
|
259
|
-
if (isMainBranch) {
|
|
260
|
-
return { mode: 'full', files: [] };
|
|
261
|
-
}
|
|
262
|
-
const lintTargets = changedFiles.filter((filePath) => {
|
|
263
|
-
const normalized = normalizePath(filePath);
|
|
264
|
-
return ((normalized.startsWith('apps/') || normalized.startsWith('packages/')) &&
|
|
265
|
-
isLintableFile(normalized));
|
|
266
|
-
});
|
|
267
|
-
if (lintTargets.length === 0) {
|
|
268
|
-
return { mode: 'skip', files: [] };
|
|
269
|
-
}
|
|
270
|
-
return { mode: 'incremental', files: lintTargets };
|
|
271
|
-
}
|
|
272
|
-
/* eslint-enable sonarjs/no-duplicate-string */
|
|
273
|
-
export function resolveTestPlan({ isMainBranch, hasUntrackedCode, hasConfigChange, fileListError, }) {
|
|
274
|
-
if (fileListError) {
|
|
275
|
-
return { mode: 'full', reason: 'file-list-error' };
|
|
276
|
-
}
|
|
277
|
-
if (hasUntrackedCode) {
|
|
278
|
-
return { mode: 'full', reason: 'untracked-code' };
|
|
279
|
-
}
|
|
280
|
-
if (hasConfigChange) {
|
|
281
|
-
return { mode: 'full', reason: 'test-config' };
|
|
282
|
-
}
|
|
283
|
-
if (isMainBranch) {
|
|
284
|
-
return { mode: 'full' };
|
|
285
|
-
}
|
|
286
|
-
return { mode: 'incremental' };
|
|
287
|
-
}
|
|
288
|
-
/**
|
|
289
|
-
* WU-1299: Extract package name from a single code path
|
|
290
|
-
*
|
|
291
|
-
* @param codePath - Single code path to parse
|
|
292
|
-
* @returns Package name or null if not a package/app path
|
|
293
|
-
*/
|
|
294
|
-
function extractPackageFromPath(codePath) {
|
|
295
|
-
if (!codePath || typeof codePath !== 'string') {
|
|
296
|
-
return null;
|
|
297
|
-
}
|
|
298
|
-
const normalized = codePath.replace(/\\/g, '/');
|
|
299
|
-
// Handle packages/@scope/name/... or packages/name/...
|
|
300
|
-
if (normalized.startsWith('packages/')) {
|
|
301
|
-
const parts = normalized.slice('packages/'.length).split('/');
|
|
302
|
-
// Scoped package (@scope/name)
|
|
303
|
-
if (parts[0]?.startsWith('@') && parts[1]) {
|
|
304
|
-
return `${parts[0]}/${parts[1]}`;
|
|
305
|
-
}
|
|
306
|
-
// Unscoped package
|
|
307
|
-
if (parts[0]) {
|
|
308
|
-
return parts[0];
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
// WU-1415: Skip apps/ paths - they aren't valid turbo packages for test filtering
|
|
312
|
-
// apps/ directories (e.g., apps/docs, apps/github-app) don't have turbo test tasks
|
|
313
|
-
// and using directory names as --filter args causes "No package found" errors
|
|
314
|
-
return null;
|
|
315
|
-
}
|
|
316
|
-
/**
|
|
317
|
-
* WU-1299: Extract package/app names from code_paths
|
|
318
|
-
*
|
|
319
|
-
* Parses paths like:
|
|
320
|
-
* - packages/@lumenflow/cli/src/file.ts -> @lumenflow/cli
|
|
321
|
-
* - apps/web/src/file.ts -> web
|
|
322
|
-
*
|
|
323
|
-
* @param codePaths - Array of code paths from WU YAML
|
|
324
|
-
* @returns Array of unique package/app names
|
|
325
|
-
*/
|
|
326
|
-
export function extractPackagesFromCodePaths(codePaths) {
|
|
327
|
-
if (!codePaths || !Array.isArray(codePaths) || codePaths.length === 0) {
|
|
328
|
-
return [];
|
|
329
|
-
}
|
|
330
|
-
const packages = new Set();
|
|
331
|
-
for (const codePath of codePaths) {
|
|
332
|
-
const pkg = extractPackageFromPath(codePath);
|
|
333
|
-
if (pkg) {
|
|
334
|
-
packages.add(pkg);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return Array.from(packages);
|
|
338
|
-
}
|
|
339
|
-
/**
|
|
340
|
-
* WU-1299: Resolve test plan for docs-only mode
|
|
341
|
-
*
|
|
342
|
-
* When --docs-only is passed, this determines whether to:
|
|
343
|
-
* - Skip tests entirely (no code packages in code_paths)
|
|
344
|
-
* - Run tests only for packages mentioned in code_paths
|
|
345
|
-
*
|
|
346
|
-
* @param options - Options including code_paths from WU YAML
|
|
347
|
-
* @returns DocsOnlyTestPlan indicating how to handle tests
|
|
348
|
-
*/
|
|
349
|
-
export function resolveDocsOnlyTestPlan({ codePaths }) {
|
|
350
|
-
const packages = extractPackagesFromCodePaths(codePaths);
|
|
351
|
-
if (packages.length === 0) {
|
|
352
|
-
return {
|
|
353
|
-
mode: 'skip',
|
|
354
|
-
packages: [],
|
|
355
|
-
reason: 'no-code-packages',
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
return {
|
|
359
|
-
mode: 'filtered',
|
|
360
|
-
packages,
|
|
361
|
-
};
|
|
362
|
-
}
|
|
363
|
-
/**
|
|
364
|
-
* WU-1299: Format message for docs-only test skipping/filtering
|
|
365
|
-
*
|
|
366
|
-
* Provides clear messaging when tests are skipped or filtered in docs-only mode.
|
|
367
|
-
*
|
|
368
|
-
* @param plan - The docs-only test plan
|
|
369
|
-
* @returns Human-readable message explaining what's happening
|
|
370
|
-
*/
|
|
371
|
-
export function formatDocsOnlySkipMessage(plan) {
|
|
372
|
-
if (plan.mode === 'skip') {
|
|
373
|
-
return '📝 docs-only mode: skipping all tests (no code packages in code_paths)';
|
|
374
|
-
}
|
|
375
|
-
const packageList = plan.packages.join(', ');
|
|
376
|
-
return `📝 docs-only mode: running tests only for packages in code_paths: ${packageList}`;
|
|
377
|
-
}
|
|
378
|
-
/**
|
|
379
|
-
* WU-1299: Load code_paths from current WU YAML
|
|
380
|
-
*
|
|
381
|
-
* Attempts to read the WU YAML file for the current WU (detected from git branch)
|
|
382
|
-
* and return its code_paths. Returns empty array if WU cannot be determined or
|
|
383
|
-
* YAML file doesn't exist.
|
|
384
|
-
*
|
|
385
|
-
* @param options - Options including optional cwd
|
|
386
|
-
* @returns Array of code_paths from WU YAML, or empty array if unavailable
|
|
387
|
-
*/
|
|
388
|
-
export function loadCurrentWUCodePaths(options = {}) {
|
|
389
|
-
const cwd = options.cwd ?? process.cwd();
|
|
390
|
-
const wuId = getCurrentWU();
|
|
391
|
-
if (!wuId) {
|
|
392
|
-
return [];
|
|
393
|
-
}
|
|
394
|
-
try {
|
|
395
|
-
const wuPaths = createWuPaths({ projectRoot: cwd });
|
|
396
|
-
const wuYamlPath = wuPaths.WU(wuId);
|
|
397
|
-
const wuDoc = readWURaw(wuYamlPath);
|
|
398
|
-
if (wuDoc && Array.isArray(wuDoc.code_paths)) {
|
|
399
|
-
return wuDoc.code_paths.filter((p) => typeof p === 'string');
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
catch {
|
|
403
|
-
// WU YAML not found or unreadable - return empty array
|
|
404
|
-
}
|
|
405
|
-
return [];
|
|
406
|
-
}
|
|
407
|
-
/**
|
|
408
|
-
* WU-1299: Run filtered tests for docs-only mode
|
|
409
|
-
* WU-1356: Updated to use configured test command
|
|
410
|
-
*
|
|
411
|
-
* When --docs-only is passed and code_paths contains packages, this runs tests
|
|
412
|
-
* only for those specific packages. The filter syntax adapts to the configured
|
|
413
|
-
* build tool (turbo, nx, or plain package manager).
|
|
414
|
-
*
|
|
415
|
-
* @param options - Options including packages to test and agent log context
|
|
416
|
-
* @returns Result object with ok status and duration
|
|
417
|
-
*/
|
|
418
|
-
async function runDocsOnlyFilteredTests({ packages, agentLog, cwd = process.cwd(), }) {
|
|
419
|
-
const start = Date.now();
|
|
420
|
-
const logLine = makeGateLogger({ agentLog, useAgentMode: !!agentLog, cwd });
|
|
421
|
-
if (packages.length === 0) {
|
|
422
|
-
logLine('📝 docs-only mode: no packages to test, skipping');
|
|
423
|
-
return { ok: true, duration: Date.now() - start };
|
|
424
|
-
}
|
|
425
|
-
logLine(`\n> Tests (docs-only filtered: ${packages.join(', ')})\n`);
|
|
426
|
-
// WU-1356: Use configured test command with filter
|
|
427
|
-
const gatesCommands = resolveGatesCommands(cwd);
|
|
428
|
-
// If there's a configured test_docs_only command, use it
|
|
429
|
-
if (gatesCommands.test_docs_only) {
|
|
430
|
-
const result = run(gatesCommands.test_docs_only, { agentLog, cwd });
|
|
431
|
-
return { ok: result.ok, duration: Date.now() - start };
|
|
432
|
-
}
|
|
433
|
-
// Otherwise, use the full test command with filter args
|
|
434
|
-
// Build filter args for each package (works with turbo, nx, and pnpm/yarn workspaces)
|
|
435
|
-
const filterArgs = packages.map((pkg) => `--filter=${pkg}`);
|
|
436
|
-
const baseCmd = gatesCommands.test_full;
|
|
437
|
-
// Append filter args to the base command
|
|
438
|
-
const filteredCmd = `${baseCmd} ${filterArgs.join(' ')}`;
|
|
439
|
-
const result = run(filteredCmd, { agentLog });
|
|
440
|
-
return { ok: result.ok, duration: Date.now() - start };
|
|
441
|
-
}
|
|
442
|
-
export function parsePrettierListOutput(output) {
|
|
443
|
-
if (!output)
|
|
444
|
-
return [];
|
|
445
|
-
return output
|
|
446
|
-
.split(/\r?\n/)
|
|
447
|
-
.map((line) => line.trim())
|
|
448
|
-
.filter(Boolean)
|
|
449
|
-
.map((line) => line.replace(/^\[error\]\s*/i, '').trim())
|
|
450
|
-
.filter((line) => !line.toLowerCase().includes('code style issues found') &&
|
|
451
|
-
!line.toLowerCase().includes('all matched files use prettier') &&
|
|
452
|
-
!line.toLowerCase().includes('checking formatting'));
|
|
453
|
-
}
|
|
454
|
-
export function buildPrettierWriteCommand(files) {
|
|
455
|
-
const quotedFiles = files.map((file) => `"${file}"`).join(' ');
|
|
456
|
-
const base = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_FLAGS.WRITE);
|
|
457
|
-
return quotedFiles ? `${base} ${quotedFiles}` : base;
|
|
458
|
-
}
|
|
459
|
-
function buildPrettierCheckCommand(files) {
|
|
460
|
-
const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
|
|
461
|
-
return pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.CHECK, filesArg);
|
|
462
|
-
}
|
|
463
|
-
export function formatFormatCheckGuidance(files) {
|
|
464
|
-
if (!files.length)
|
|
465
|
-
return [];
|
|
466
|
-
const command = buildPrettierWriteCommand(files);
|
|
467
|
-
return [
|
|
468
|
-
'',
|
|
469
|
-
'❌ format:check failed',
|
|
470
|
-
'Fix with:',
|
|
471
|
-
` ${command}`,
|
|
472
|
-
'',
|
|
473
|
-
'Affected files:',
|
|
474
|
-
...files.map((file) => ` - ${file}`),
|
|
475
|
-
'',
|
|
476
|
-
];
|
|
477
|
-
}
|
|
478
|
-
function collectPrettierListDifferent(cwd, files = []) {
|
|
479
|
-
const filesArg = files.length > 0 ? quoteShellArgs(files) : '.';
|
|
480
|
-
const cmd = pnpmCmd(SCRIPTS.PRETTIER, PRETTIER_ARGS.LIST_DIFFERENT, filesArg);
|
|
481
|
-
const result = spawnSync(cmd, [], {
|
|
482
|
-
shell: true,
|
|
483
|
-
cwd,
|
|
484
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
485
|
-
});
|
|
486
|
-
const output = `${result.stdout || ''}\n${result.stderr || ''}`;
|
|
487
|
-
return parsePrettierListOutput(output);
|
|
488
|
-
}
|
|
489
|
-
function emitFormatCheckGuidance({ agentLog, useAgentMode, files, cwd, }) {
|
|
490
|
-
const formattedFiles = collectPrettierListDifferent(cwd, files ?? []);
|
|
491
|
-
if (!formattedFiles.length)
|
|
492
|
-
return;
|
|
493
|
-
const lines = formatFormatCheckGuidance(formattedFiles);
|
|
494
|
-
const logLine = useAgentMode && agentLog
|
|
495
|
-
? (line) => writeSync(agentLog.logFd, `${line}\n`)
|
|
496
|
-
: (line) => console.log(line);
|
|
497
|
-
for (const line of lines) {
|
|
498
|
-
logLine(line);
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
function readLogTail(logPath, { maxLines = 40, maxBytes = 64 * 1024 } = {}) {
|
|
502
|
-
try {
|
|
503
|
-
const stats = statSync(logPath);
|
|
504
|
-
const startPos = Math.max(0, stats.size - maxBytes);
|
|
505
|
-
const bytesToRead = stats.size - startPos;
|
|
506
|
-
const fd = openSync(logPath, 'r');
|
|
507
|
-
try {
|
|
508
|
-
const buffer = Buffer.alloc(bytesToRead);
|
|
509
|
-
readSync(fd, buffer, 0, bytesToRead, startPos);
|
|
510
|
-
const text = buffer.toString(FILE_SYSTEM.ENCODING);
|
|
511
|
-
const lines = text.split(/\r?\n/).filter(Boolean);
|
|
512
|
-
return lines.slice(-maxLines).join('\n');
|
|
513
|
-
}
|
|
514
|
-
finally {
|
|
515
|
-
closeSync(fd);
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
catch {
|
|
519
|
-
return '';
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
function createAgentLogContext({ wuId, lane, cwd, }) {
|
|
523
|
-
const logPath = buildGatesLogPath({ cwd, env: process.env, wuId, lane });
|
|
524
|
-
mkdirSync(path.dirname(logPath), { recursive: true });
|
|
525
|
-
const logFd = openSync(logPath, 'a');
|
|
526
|
-
const header = `# gates log\n# lane: ${lane || 'unknown'}\n# wu: ${wuId || 'unknown'}\n# started: ${new Date().toISOString()}\n\n`;
|
|
527
|
-
writeSync(logFd, header);
|
|
528
|
-
// Ensure we close the FD even if gates exit via die().
|
|
529
|
-
process.on('exit', () => {
|
|
530
|
-
try {
|
|
531
|
-
closeSync(logFd);
|
|
532
|
-
}
|
|
533
|
-
catch {
|
|
534
|
-
// ignore
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
return { logPath, logFd };
|
|
538
|
-
}
|
|
539
|
-
function run(cmd, { agentLog, cwd = process.cwd(), } = {}) {
|
|
540
|
-
const start = Date.now();
|
|
541
|
-
if (!agentLog) {
|
|
542
|
-
console.log(`\n> ${cmd}\n`);
|
|
543
|
-
try {
|
|
544
|
-
execSync(cmd, {
|
|
545
|
-
stdio: 'inherit',
|
|
546
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
547
|
-
cwd,
|
|
548
|
-
});
|
|
549
|
-
return { ok: true, duration: Date.now() - start };
|
|
550
|
-
}
|
|
551
|
-
catch {
|
|
552
|
-
return { ok: false, duration: Date.now() - start };
|
|
553
|
-
}
|
|
554
|
-
}
|
|
555
|
-
writeSync(agentLog.logFd, `\n> ${cmd}\n\n`);
|
|
556
|
-
const result = spawnSync(cmd, [], {
|
|
557
|
-
shell: true,
|
|
558
|
-
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
559
|
-
cwd,
|
|
560
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
561
|
-
});
|
|
562
|
-
return { ok: result.status === EXIT_CODES.SUCCESS, duration: Date.now() - start };
|
|
563
|
-
}
|
|
564
|
-
/**
|
|
565
|
-
* Parse a WU ID from a branch name.
|
|
566
|
-
* Returns canonical upper-case ID (e.g., WU-123) or null when not present.
|
|
567
|
-
*/
|
|
568
|
-
export function parseWUFromBranchName(branchName) {
|
|
569
|
-
if (!branchName) {
|
|
570
|
-
return null;
|
|
571
|
-
}
|
|
572
|
-
const match = branchName.match(/wu-(\d+)/i);
|
|
573
|
-
if (!match) {
|
|
574
|
-
return null;
|
|
575
|
-
}
|
|
576
|
-
return `WU-${match[1]}`.toUpperCase();
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Resolve spec-linter execution strategy.
|
|
580
|
-
* If current WU is known, run scoped validation only.
|
|
581
|
-
* If unknown, fall back to global validation.
|
|
582
|
-
*/
|
|
583
|
-
export function resolveSpecLinterPlan(wuId) {
|
|
584
|
-
if (wuId) {
|
|
585
|
-
return {
|
|
586
|
-
scopedWuId: wuId,
|
|
587
|
-
runGlobal: false,
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
return {
|
|
591
|
-
scopedWuId: null,
|
|
592
|
-
runGlobal: true,
|
|
593
|
-
};
|
|
594
|
-
}
|
|
595
|
-
async function detectCurrentWUForCwd(cwd) {
|
|
596
|
-
const workingDir = cwd ?? process.cwd();
|
|
597
|
-
try {
|
|
598
|
-
const branch = await createGitForPath(workingDir).getCurrentBranch();
|
|
599
|
-
const parsed = parseWUFromBranchName(branch);
|
|
600
|
-
if (parsed) {
|
|
601
|
-
return parsed;
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
catch {
|
|
605
|
-
// Fall back to legacy process-cwd based resolver below.
|
|
606
|
-
}
|
|
607
|
-
return getCurrentWU();
|
|
608
|
-
}
|
|
609
|
-
async function runSpecLinterGate({ agentLog, useAgentMode, cwd }) {
|
|
610
|
-
const start = Date.now();
|
|
611
|
-
const wuId = await detectCurrentWUForCwd(cwd);
|
|
612
|
-
const plan = resolveSpecLinterPlan(wuId);
|
|
613
|
-
if (plan.scopedWuId) {
|
|
614
|
-
const scopedCmd = pnpmCmd('wu:validate', '--id', plan.scopedWuId);
|
|
615
|
-
const scopedResult = run(scopedCmd, { agentLog, cwd });
|
|
616
|
-
if (!scopedResult.ok) {
|
|
617
|
-
return { ok: false, duration: Date.now() - start };
|
|
618
|
-
}
|
|
619
|
-
return { ok: true, duration: Date.now() - start };
|
|
620
|
-
}
|
|
621
|
-
if (!useAgentMode) {
|
|
622
|
-
console.log('⚠️ Unable to detect current WU; skipping scoped validation.');
|
|
623
|
-
}
|
|
624
|
-
else if (agentLog) {
|
|
625
|
-
writeSync(agentLog.logFd, '⚠️ Unable to detect current WU; skipping scoped validation.\n');
|
|
626
|
-
}
|
|
627
|
-
if (!plan.runGlobal) {
|
|
628
|
-
return { ok: true, duration: Date.now() - start };
|
|
629
|
-
}
|
|
630
|
-
const fallbackResult = run(pnpmRun(SCRIPTS.SPEC_LINTER), { agentLog, cwd });
|
|
631
|
-
return { ok: fallbackResult.ok, duration: Date.now() - start };
|
|
632
|
-
}
|
|
633
|
-
function makeGateLogger({ agentLog, useAgentMode }) {
|
|
634
|
-
return (line) => {
|
|
635
|
-
if (!useAgentMode) {
|
|
636
|
-
console.log(line);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
if (agentLog) {
|
|
640
|
-
writeSync(agentLog.logFd, `${line}\n`);
|
|
641
|
-
}
|
|
642
|
-
};
|
|
643
|
-
}
|
|
644
|
-
async function runBacklogSyncGate({ agentLog, useAgentMode, cwd }) {
|
|
645
|
-
const start = Date.now();
|
|
646
|
-
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
647
|
-
logLine('\n> Backlog sync\n');
|
|
648
|
-
const result = await validateBacklogSync({ cwd });
|
|
649
|
-
if (result.errors.length > 0) {
|
|
650
|
-
logLine('❌ Backlog sync errors:');
|
|
651
|
-
result.errors.forEach((error) => logLine(` - ${error}`));
|
|
652
|
-
}
|
|
653
|
-
if (result.warnings.length > 0) {
|
|
654
|
-
logLine('⚠️ Backlog sync warnings:');
|
|
655
|
-
result.warnings.forEach((warning) => logLine(` - ${warning}`));
|
|
656
|
-
}
|
|
657
|
-
logLine(`Backlog sync summary: WU files=${result.wuCount}, Backlog refs=${result.backlogCount}`);
|
|
658
|
-
return { ok: result.valid, duration: Date.now() - start };
|
|
659
|
-
}
|
|
660
|
-
async function runSupabaseDocsGate({ agentLog, useAgentMode, cwd }) {
|
|
661
|
-
const start = Date.now();
|
|
662
|
-
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
663
|
-
logLine('\n> Supabase docs linter\n');
|
|
664
|
-
const result = await runSupabaseDocsLinter({ cwd, logger: { log: logLine } });
|
|
665
|
-
if (result.skipped) {
|
|
666
|
-
logLine(`⚠️ ${result.message ?? 'Supabase docs linter skipped.'}`);
|
|
667
|
-
}
|
|
668
|
-
else if (!result.ok) {
|
|
669
|
-
logLine('❌ Supabase docs linter failed.');
|
|
670
|
-
(result.errors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
671
|
-
}
|
|
672
|
-
else {
|
|
673
|
-
logLine(result.message ?? 'Supabase docs linter passed.');
|
|
674
|
-
}
|
|
675
|
-
return { ok: result.ok, duration: Date.now() - start };
|
|
676
|
-
}
|
|
677
|
-
async function runSystemMapGate({ agentLog, useAgentMode, cwd }) {
|
|
678
|
-
const start = Date.now();
|
|
679
|
-
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
680
|
-
logLine('\n> System map validation\n');
|
|
681
|
-
const result = await runSystemMapValidation({
|
|
682
|
-
cwd,
|
|
683
|
-
logger: { log: logLine, warn: logLine, error: logLine },
|
|
684
|
-
});
|
|
685
|
-
if (!result.valid) {
|
|
686
|
-
logLine('❌ System map validation failed');
|
|
687
|
-
(result.pathErrors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
688
|
-
(result.orphanDocs ?? []).forEach((error) => logLine(` - ${error}`));
|
|
689
|
-
(result.audienceErrors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
690
|
-
(result.queryErrors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
691
|
-
(result.classificationErrors ?? []).forEach((error) => logLine(` - ${error}`));
|
|
692
|
-
}
|
|
693
|
-
else {
|
|
694
|
-
logLine('System map validation passed.');
|
|
695
|
-
}
|
|
696
|
-
return { ok: result.valid, duration: Date.now() - start };
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* WU-1191: Run lane health check gate
|
|
700
|
-
*
|
|
701
|
-
* Checks lane configuration for overlaps and coverage gaps.
|
|
702
|
-
* Mode is configurable via gates.lane_health in .lumenflow.config.yaml:
|
|
703
|
-
* - 'warn': Log warnings but don't fail (default)
|
|
704
|
-
* - 'error': Fail the gate if issues detected
|
|
705
|
-
* - 'off': Skip the check entirely
|
|
706
|
-
*/
|
|
707
|
-
async function runLaneHealthGate({ agentLog, useAgentMode, mode, cwd, }) {
|
|
708
|
-
const start = Date.now();
|
|
709
|
-
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
710
|
-
// Skip if mode is 'off'
|
|
711
|
-
if (mode === 'off') {
|
|
712
|
-
logLine('\n> Lane health check (skipped - mode: off)\n');
|
|
713
|
-
return { ok: true, duration: Date.now() - start };
|
|
714
|
-
}
|
|
715
|
-
logLine(`\n> Lane health check (mode: ${mode})\n`);
|
|
716
|
-
const report = runLaneHealthCheck({ projectRoot: cwd });
|
|
717
|
-
if (!report.healthy) {
|
|
718
|
-
logLine('⚠️ Lane health issues detected:');
|
|
719
|
-
if (report.overlaps.hasOverlaps) {
|
|
720
|
-
logLine(` - ${report.overlaps.overlaps.length} overlapping code_paths`);
|
|
721
|
-
}
|
|
722
|
-
if (report.gaps.hasGaps) {
|
|
723
|
-
logLine(` - ${report.gaps.uncoveredFiles.length} uncovered files`);
|
|
724
|
-
}
|
|
725
|
-
logLine(` Run 'pnpm lane:health' for full report.`);
|
|
726
|
-
if (mode === 'error') {
|
|
727
|
-
return { ok: false, duration: Date.now() - start };
|
|
728
|
-
}
|
|
729
|
-
// mode === 'warn': report but don't fail
|
|
730
|
-
logLine(' (mode: warn - not blocking)');
|
|
731
|
-
}
|
|
732
|
-
else {
|
|
733
|
-
logLine('Lane health check passed.');
|
|
734
|
-
}
|
|
735
|
-
return { ok: true, duration: Date.now() - start };
|
|
736
|
-
}
|
|
737
|
-
async function filterExistingFiles(files) {
|
|
738
|
-
const existingFiles = await Promise.all(files.map(async (file) => {
|
|
739
|
-
try {
|
|
740
|
-
await access(file);
|
|
741
|
-
return file;
|
|
742
|
-
}
|
|
743
|
-
catch {
|
|
744
|
-
return null;
|
|
745
|
-
}
|
|
746
|
-
}));
|
|
747
|
-
return existingFiles.filter((file) => Boolean(file));
|
|
748
|
-
}
|
|
749
|
-
async function runFormatCheckGate({ agentLog, useAgentMode, cwd }) {
|
|
750
|
-
const start = Date.now();
|
|
751
|
-
const logLine = makeGateLogger({ agentLog, useAgentMode });
|
|
752
|
-
let git;
|
|
753
|
-
let isMainBranch = false;
|
|
754
|
-
try {
|
|
755
|
-
git = createGitForPath(cwd);
|
|
756
|
-
const currentBranch = await git.getCurrentBranch();
|
|
757
|
-
isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
758
|
-
}
|
|
759
|
-
catch (error) {
|
|
760
|
-
logLine(`⚠️ Failed to determine branch for format check: ${error.message}`);
|
|
761
|
-
const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd });
|
|
762
|
-
return { ...result, duration: Date.now() - start, fileCount: -1 };
|
|
763
|
-
}
|
|
764
|
-
if (isMainBranch) {
|
|
765
|
-
logLine('📋 On main branch - running full format check');
|
|
766
|
-
const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd });
|
|
767
|
-
return { ...result, duration: Date.now() - start, fileCount: -1 };
|
|
768
|
-
}
|
|
769
|
-
let changedFiles = [];
|
|
770
|
-
let fileListError = false;
|
|
771
|
-
try {
|
|
772
|
-
changedFiles = await getChangedFilesForIncremental({ git });
|
|
773
|
-
}
|
|
774
|
-
catch (error) {
|
|
775
|
-
fileListError = true;
|
|
776
|
-
logLine(`⚠️ Failed to determine changed files for format check: ${error.message}`);
|
|
777
|
-
}
|
|
778
|
-
const plan = resolveFormatCheckPlan({ changedFiles, fileListError });
|
|
779
|
-
if (plan.mode === 'skip') {
|
|
780
|
-
logLine('\n> format:check (incremental)\n');
|
|
781
|
-
logLine('✅ No files changed - skipping format check');
|
|
782
|
-
return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
|
|
783
|
-
}
|
|
784
|
-
if (plan.mode === 'full') {
|
|
785
|
-
const reason = plan.reason === 'prettier-config'
|
|
786
|
-
? ' (prettier config changed)'
|
|
787
|
-
: plan.reason === 'file-list-error'
|
|
788
|
-
? ' (file list unavailable)'
|
|
789
|
-
: '';
|
|
790
|
-
logLine(`📋 Running full format check${reason}`);
|
|
791
|
-
const result = run(pnpmCmd(SCRIPTS.FORMAT_CHECK), { agentLog, cwd });
|
|
792
|
-
return { ...result, duration: Date.now() - start, fileCount: -1 };
|
|
793
|
-
}
|
|
794
|
-
const existingFiles = await filterExistingFiles(plan.files);
|
|
795
|
-
if (existingFiles.length === 0) {
|
|
796
|
-
logLine('\n> format:check (incremental)\n');
|
|
797
|
-
logLine('✅ All changed files were deleted - skipping format check');
|
|
798
|
-
return { ok: true, duration: Date.now() - start, fileCount: 0, filesChecked: [] };
|
|
799
|
-
}
|
|
800
|
-
logLine(`\n> format:check (incremental: ${existingFiles.length} files)\n`);
|
|
801
|
-
const result = run(buildPrettierCheckCommand(existingFiles), { agentLog, cwd });
|
|
802
|
-
return {
|
|
803
|
-
...result,
|
|
804
|
-
duration: Date.now() - start,
|
|
805
|
-
fileCount: existingFiles.length,
|
|
806
|
-
filesChecked: existingFiles,
|
|
807
|
-
};
|
|
808
|
-
}
|
|
809
|
-
/**
|
|
810
|
-
* Run incremental ESLint on changed files only
|
|
811
|
-
* Falls back to full lint if on main branch or if incremental fails
|
|
812
|
-
* @returns {{ ok: boolean, duration: number, fileCount: number }}
|
|
813
|
-
*/
|
|
814
|
-
async function runIncrementalLint({ agentLog, cwd, }) {
|
|
815
|
-
const start = Date.now();
|
|
816
|
-
const logLine = (line) => {
|
|
817
|
-
if (!agentLog) {
|
|
818
|
-
console.log(line);
|
|
819
|
-
return;
|
|
820
|
-
}
|
|
821
|
-
writeSync(agentLog.logFd, `${line}\n`);
|
|
822
|
-
};
|
|
823
|
-
try {
|
|
824
|
-
// Check if we're on main branch
|
|
825
|
-
const git = createGitForPath(cwd);
|
|
826
|
-
const currentBranch = await git.getCurrentBranch();
|
|
827
|
-
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
828
|
-
if (isMainBranch) {
|
|
829
|
-
logLine('📋 On main branch - running full lint');
|
|
830
|
-
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
|
|
831
|
-
return { ...result, fileCount: -1 };
|
|
832
|
-
}
|
|
833
|
-
const changedFiles = await getChangedLintableFiles({ git });
|
|
834
|
-
const plan = resolveLintPlan({ isMainBranch, changedFiles });
|
|
835
|
-
if (plan.mode === 'skip') {
|
|
836
|
-
logLine('\n> ESLint (incremental)\n');
|
|
837
|
-
logLine('✅ No lintable files changed - skipping lint');
|
|
838
|
-
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
839
|
-
}
|
|
840
|
-
if (plan.mode === 'full') {
|
|
841
|
-
logLine('📋 Running full lint (incremental plan forced full)');
|
|
842
|
-
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
|
|
843
|
-
return { ...result, fileCount: -1 };
|
|
844
|
-
}
|
|
845
|
-
const existingFiles = await filterExistingFiles(plan.files);
|
|
846
|
-
if (existingFiles.length === 0) {
|
|
847
|
-
logLine('\n> ESLint (incremental)\n');
|
|
848
|
-
logLine('✅ All changed files were deleted - skipping lint');
|
|
849
|
-
return { ok: true, duration: Date.now() - start, fileCount: 0 };
|
|
850
|
-
}
|
|
851
|
-
logLine(`\n> ESLint (incremental: ${existingFiles.length} files)\n`);
|
|
852
|
-
logLine(`Files to lint:\n ${existingFiles.join('\n ')}\n`);
|
|
853
|
-
const result = spawnSync(PKG_MANAGER, [
|
|
854
|
-
ESLINT_COMMANDS.ESLINT,
|
|
855
|
-
ESLINT_FLAGS.MAX_WARNINGS,
|
|
856
|
-
ESLINT_DEFAULTS.MAX_WARNINGS,
|
|
857
|
-
ESLINT_FLAGS.NO_WARN_IGNORED,
|
|
858
|
-
ESLINT_FLAGS.CACHE,
|
|
859
|
-
ESLINT_FLAGS.CACHE_STRATEGY,
|
|
860
|
-
CACHE_STRATEGIES.CONTENT,
|
|
861
|
-
ESLINT_FLAGS.CACHE_LOCATION,
|
|
862
|
-
'.eslintcache',
|
|
863
|
-
ESLINT_FLAGS.PASS_ON_UNPRUNED,
|
|
864
|
-
...existingFiles,
|
|
865
|
-
], agentLog
|
|
866
|
-
? {
|
|
867
|
-
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
868
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
869
|
-
cwd,
|
|
870
|
-
}
|
|
871
|
-
: {
|
|
872
|
-
stdio: 'inherit',
|
|
873
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
874
|
-
cwd,
|
|
875
|
-
});
|
|
876
|
-
const duration = Date.now() - start;
|
|
877
|
-
return {
|
|
878
|
-
ok: result.status === EXIT_CODES.SUCCESS,
|
|
879
|
-
duration,
|
|
880
|
-
fileCount: existingFiles.length,
|
|
881
|
-
};
|
|
882
|
-
}
|
|
883
|
-
catch (error) {
|
|
884
|
-
console.error('⚠️ Incremental lint failed, falling back to full lint:', error.message);
|
|
885
|
-
const result = run(pnpmCmd(SCRIPTS.LINT), { agentLog, cwd });
|
|
886
|
-
return { ...result, fileCount: -1 };
|
|
887
|
-
}
|
|
888
|
-
}
|
|
889
|
-
/**
|
|
890
|
-
* Run changed tests using configured test runner's incremental mode.
|
|
891
|
-
* WU-1356: Updated to use configured commands from gates-config.
|
|
892
|
-
* Falls back to full test suite if on main branch or if the run fails.
|
|
893
|
-
*
|
|
894
|
-
* @returns {{ ok: boolean, duration: number, isIncremental: boolean }}
|
|
895
|
-
*/
|
|
896
|
-
async function runChangedTests({ agentLog, cwd, }) {
|
|
897
|
-
const start = Date.now();
|
|
898
|
-
// eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
|
|
899
|
-
const logLine = (line) => {
|
|
900
|
-
if (!agentLog) {
|
|
901
|
-
console.log(line);
|
|
902
|
-
return;
|
|
903
|
-
}
|
|
904
|
-
writeSync(agentLog.logFd, `${line}\n`);
|
|
905
|
-
};
|
|
906
|
-
// WU-1356: Get configured commands
|
|
907
|
-
const gatesCommands = resolveGatesCommands(cwd);
|
|
908
|
-
const testRunner = resolveTestRunner(cwd);
|
|
909
|
-
try {
|
|
910
|
-
const git = createGitForPath(cwd);
|
|
911
|
-
const currentBranch = await git.getCurrentBranch();
|
|
912
|
-
const isMainBranch = currentBranch === BRANCHES.MAIN || currentBranch === BRANCHES.MASTER;
|
|
913
|
-
if (isMainBranch) {
|
|
914
|
-
logLine('📋 On main branch - running full test suite');
|
|
915
|
-
const result = run(gatesCommands.test_full, { agentLog, cwd });
|
|
916
|
-
return { ...result, isIncremental: false };
|
|
917
|
-
}
|
|
918
|
-
let changedFiles = [];
|
|
919
|
-
let fileListError = false;
|
|
920
|
-
try {
|
|
921
|
-
changedFiles = await getChangedFilesForIncremental({ git });
|
|
922
|
-
}
|
|
923
|
-
catch (error) {
|
|
924
|
-
fileListError = true;
|
|
925
|
-
logLine(`⚠️ Failed to determine changed files for tests: ${error.message}`);
|
|
926
|
-
}
|
|
927
|
-
const hasConfigChange = !fileListError && changedFiles.some(isTestConfigFile);
|
|
928
|
-
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
929
|
-
const untrackedFiles = untrackedOutput
|
|
930
|
-
.split(/\r?\n/)
|
|
931
|
-
.map((f) => f.trim())
|
|
932
|
-
.filter(Boolean);
|
|
933
|
-
const untrackedCodeFiles = untrackedFiles.filter(isCodeFilePath);
|
|
934
|
-
const hasUntrackedCode = untrackedCodeFiles.length > 0;
|
|
935
|
-
const plan = resolveTestPlan({
|
|
936
|
-
isMainBranch,
|
|
937
|
-
hasUntrackedCode,
|
|
938
|
-
hasConfigChange,
|
|
939
|
-
fileListError,
|
|
940
|
-
});
|
|
941
|
-
if (plan.mode === 'full') {
|
|
942
|
-
if (plan.reason === 'untracked-code') {
|
|
943
|
-
const preview = untrackedCodeFiles.slice(0, 5).join(', ');
|
|
944
|
-
logLine(`⚠️ Untracked code files detected (${untrackedCodeFiles.length}): ${preview}${untrackedCodeFiles.length > 5 ? '...' : ''}`);
|
|
945
|
-
}
|
|
946
|
-
else if (plan.reason === 'test-config') {
|
|
947
|
-
logLine('⚠️ Test config changes detected - running full test suite');
|
|
948
|
-
}
|
|
949
|
-
else if (plan.reason === 'file-list-error') {
|
|
950
|
-
logLine('⚠️ Changed file list unavailable - running full test suite');
|
|
951
|
-
}
|
|
952
|
-
logLine('📋 Running full test suite to avoid missing coverage');
|
|
953
|
-
const result = run(gatesCommands.test_full, { agentLog, cwd });
|
|
954
|
-
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
955
|
-
}
|
|
956
|
-
// WU-1356: Use configured incremental test command
|
|
957
|
-
logLine(`\n> Running tests (${testRunner} --changed)\n`);
|
|
958
|
-
// If test_incremental is configured, use it directly
|
|
959
|
-
if (gatesCommands.test_incremental) {
|
|
960
|
-
const result = run(gatesCommands.test_incremental, { agentLog, cwd });
|
|
961
|
-
return { ...result, duration: Date.now() - start, isIncremental: true };
|
|
962
|
-
}
|
|
963
|
-
// Fallback: For vitest, use the built-in changed args helper
|
|
964
|
-
if (testRunner === 'vitest') {
|
|
965
|
-
const result = run(pnpmCmd('vitest', 'run', ...buildVitestChangedArgs({ baseBranch: 'origin/main' })), { agentLog, cwd });
|
|
966
|
-
return { ...result, duration: Date.now() - start, isIncremental: true };
|
|
967
|
-
}
|
|
968
|
-
// For other runners without configured incremental, fall back to full
|
|
969
|
-
logLine('⚠️ No incremental test command configured, running full suite');
|
|
970
|
-
const result = run(gatesCommands.test_full, { agentLog, cwd });
|
|
971
|
-
return { ...result, duration: Date.now() - start, isIncremental: false };
|
|
972
|
-
}
|
|
973
|
-
catch (error) {
|
|
974
|
-
console.error('⚠️ Changed tests failed, falling back to full suite:', error.message);
|
|
975
|
-
const result = run(gatesCommands.test_full, { agentLog, cwd });
|
|
976
|
-
return { ...result, isIncremental: false };
|
|
977
|
-
}
|
|
978
|
-
}
|
|
979
|
-
/**
|
|
980
|
-
* Safety-critical test file patterns (relative to apps/web).
|
|
981
|
-
* These patterns are passed as positional arguments to vitest run.
|
|
982
|
-
* Must match the vitest include patterns in the workspace config.
|
|
983
|
-
* @type {string[]}
|
|
984
|
-
*/
|
|
985
|
-
const SAFETY_CRITICAL_TEST_FILES = [
|
|
986
|
-
// PHI protection tests
|
|
987
|
-
'src/components/ui/__tests__/PHIGuard.test.tsx',
|
|
988
|
-
'src/components/ui/__tests__/WidgetPHIConsentDialog.test.tsx',
|
|
989
|
-
'src/components/ui/__tests__/Composer.phi.test.tsx',
|
|
990
|
-
// Privacy detection tests
|
|
991
|
-
'src/lib/llm/__tests__/privacyDetector.test.ts',
|
|
992
|
-
// Escalation trigger tests
|
|
993
|
-
'src/lib/llm/__tests__/escalationTrigger.test.ts',
|
|
994
|
-
'src/components/escalation/__tests__/EscalationHistory.test.tsx',
|
|
995
|
-
// Constitutional enforcer tests
|
|
996
|
-
'src/lib/llm/__tests__/constitutionalEnforcer.test.ts',
|
|
997
|
-
// Safe prompt wrapper tests
|
|
998
|
-
'src/lib/llm/__tests__/safePromptWrapper.test.ts',
|
|
999
|
-
// Crisis/emergency handling tests
|
|
1000
|
-
'src/lib/prompts/__tests__/golden-crisis.test.ts',
|
|
1001
|
-
];
|
|
1002
|
-
/**
|
|
1003
|
-
* WU-2062: Run safety-critical tests
|
|
1004
|
-
* These tests ALWAYS run regardless of which files changed.
|
|
1005
|
-
* Includes: PHI, escalation, privacy, red-flag, constitutional enforcer tests
|
|
1006
|
-
*
|
|
1007
|
-
* Runs from apps/web directory with explicit file paths to ensure
|
|
1008
|
-
* compatibility with vitest workspace include patterns.
|
|
1009
|
-
*
|
|
1010
|
-
* @param {object} options - Options
|
|
1011
|
-
* @param {object} [options.agentLog] - Agent log context
|
|
1012
|
-
* @returns {Promise<{ ok: boolean, duration: number, testCount: number }>}
|
|
1013
|
-
*/
|
|
1014
|
-
async function runSafetyCriticalTests({ agentLog, cwd, }) {
|
|
1015
|
-
const start = Date.now();
|
|
1016
|
-
// eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
|
|
1017
|
-
const logLine = (line) => {
|
|
1018
|
-
if (!agentLog) {
|
|
1019
|
-
console.log(line);
|
|
1020
|
-
return;
|
|
1021
|
-
}
|
|
1022
|
-
writeSync(agentLog.logFd, `${line}\n`);
|
|
1023
|
-
};
|
|
1024
|
-
// WU-1006: Skip safety-critical tests if apps/web doesn't exist (repo-agnostic)
|
|
1025
|
-
const webDir = path.join(cwd, DIRECTORIES.APPS_WEB);
|
|
1026
|
-
try {
|
|
1027
|
-
await access(webDir);
|
|
1028
|
-
}
|
|
1029
|
-
catch {
|
|
1030
|
-
logLine('\n> Safety-critical tests skipped (apps/web not present)\n');
|
|
1031
|
-
return { ok: true, duration: Date.now() - start, testCount: 0 };
|
|
1032
|
-
}
|
|
1033
|
-
try {
|
|
1034
|
-
logLine('\n> Safety-critical tests (always run)\n');
|
|
1035
|
-
logLine(`Test files: ${SAFETY_CRITICAL_TEST_FILES.length} files\n`);
|
|
1036
|
-
// Run vitest with --project web to target the web workspace
|
|
1037
|
-
// Using explicit file paths for compatibility with workspace include patterns
|
|
1038
|
-
const result = spawnSync(PKG_MANAGER, [
|
|
1039
|
-
'vitest',
|
|
1040
|
-
'run',
|
|
1041
|
-
'--project',
|
|
1042
|
-
PACKAGES.WEB,
|
|
1043
|
-
'--reporter=verbose',
|
|
1044
|
-
...SAFETY_CRITICAL_TEST_FILES,
|
|
1045
|
-
'--passWithNoTests', // Don't fail if some files don't exist
|
|
1046
|
-
], agentLog
|
|
1047
|
-
? {
|
|
1048
|
-
stdio: ['ignore', agentLog.logFd, agentLog.logFd],
|
|
1049
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
1050
|
-
cwd,
|
|
1051
|
-
}
|
|
1052
|
-
: {
|
|
1053
|
-
stdio: 'inherit',
|
|
1054
|
-
encoding: FILE_SYSTEM.ENCODING,
|
|
1055
|
-
cwd,
|
|
1056
|
-
});
|
|
1057
|
-
const duration = Date.now() - start;
|
|
1058
|
-
return {
|
|
1059
|
-
ok: result.status === EXIT_CODES.SUCCESS,
|
|
1060
|
-
duration,
|
|
1061
|
-
testCount: SAFETY_CRITICAL_TEST_FILES.length,
|
|
1062
|
-
};
|
|
1063
|
-
}
|
|
1064
|
-
catch (error) {
|
|
1065
|
-
console.error('⚠️ Safety-critical tests failed:', error.message);
|
|
1066
|
-
return { ok: false, duration: Date.now() - start, testCount: 0 };
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
/**
|
|
1070
|
-
* WU-2062: Run integration tests for high-risk changes
|
|
1071
|
-
* Only runs when auth, PHI, RLS, or migration files are modified.
|
|
1072
|
-
*
|
|
1073
|
-
* @param {object} options - Options
|
|
1074
|
-
* @param {object} [options.agentLog] - Agent log context
|
|
1075
|
-
* @returns {Promise<{ ok: boolean, duration: number }>}
|
|
1076
|
-
*/
|
|
1077
|
-
async function runIntegrationTests({ agentLog, cwd, }) {
|
|
1078
|
-
const start = Date.now();
|
|
1079
|
-
// eslint-disable-next-line sonarjs/no-identical-functions -- Pre-existing: logLine helper duplicated across gate runners
|
|
1080
|
-
const logLine = (line) => {
|
|
1081
|
-
if (!agentLog) {
|
|
1082
|
-
console.log(line);
|
|
1083
|
-
return;
|
|
1084
|
-
}
|
|
1085
|
-
writeSync(agentLog.logFd, `${line}\n`);
|
|
1086
|
-
};
|
|
1087
|
-
try {
|
|
1088
|
-
logLine('\n> Integration tests (high-risk changes detected)\n');
|
|
1089
|
-
// WU-1415: vitest doesn't support --include flag
|
|
1090
|
-
// Pass glob patterns as positional arguments instead
|
|
1091
|
-
const result = run(`RUN_INTEGRATION_TESTS=1 ${pnpmCmd('vitest', 'run', "'**/*.integration.*'", "'**/golden-*.test.*'")}`, { agentLog, cwd });
|
|
1092
|
-
const duration = Date.now() - start;
|
|
1093
|
-
return {
|
|
1094
|
-
ok: result.ok,
|
|
1095
|
-
duration,
|
|
1096
|
-
};
|
|
1097
|
-
}
|
|
1098
|
-
catch (error) {
|
|
1099
|
-
console.error('⚠️ Integration tests failed:', error.message);
|
|
1100
|
-
return { ok: false, duration: Date.now() - start };
|
|
1101
|
-
}
|
|
1102
|
-
}
|
|
1103
|
-
async function getChangedFilesForIncremental({ git, baseBranch = 'origin/main', }) {
|
|
1104
|
-
const mergeBase = await git.mergeBase('HEAD', baseBranch);
|
|
1105
|
-
const committedOutput = await git.raw(['diff', '--name-only', `${mergeBase}...HEAD`]);
|
|
1106
|
-
const committedFiles = committedOutput
|
|
1107
|
-
.split('\n')
|
|
1108
|
-
.map((f) => f.trim())
|
|
1109
|
-
.filter(Boolean);
|
|
1110
|
-
const unstagedOutput = await git.raw(['diff', '--name-only']);
|
|
1111
|
-
const unstagedFiles = unstagedOutput
|
|
1112
|
-
.split('\n')
|
|
1113
|
-
.map((f) => f.trim())
|
|
1114
|
-
.filter(Boolean);
|
|
1115
|
-
const untrackedOutput = await git.raw(['ls-files', '--others', '--exclude-standard']);
|
|
1116
|
-
const untrackedFiles = untrackedOutput
|
|
1117
|
-
.split('\n')
|
|
1118
|
-
.map((f) => f.trim())
|
|
1119
|
-
.filter(Boolean);
|
|
1120
|
-
return [...new Set([...committedFiles, ...unstagedFiles, ...untrackedFiles])];
|
|
1121
|
-
}
|
|
1122
|
-
async function getAllChangedFiles(options = {}) {
|
|
1123
|
-
const { git = createGitForPath(options.cwd ?? process.cwd()) } = options;
|
|
1124
|
-
try {
|
|
1125
|
-
return await getChangedFilesForIncremental({ git });
|
|
1126
|
-
}
|
|
1127
|
-
catch (error) {
|
|
1128
|
-
console.error('⚠️ Failed to get changed files:', error.message);
|
|
1129
|
-
return [];
|
|
1130
|
-
}
|
|
1131
|
-
}
|
|
162
|
+
// ── Public API ─────────────────────────────────────────────────────────
|
|
1132
163
|
/**
|
|
1133
164
|
* Run gates for a specific working directory without mutating global process cwd.
|
|
1134
165
|
*/
|
|
@@ -1144,7 +175,7 @@ export async function runGates(options = {}) {
|
|
|
1144
175
|
return false;
|
|
1145
176
|
}
|
|
1146
177
|
}
|
|
1147
|
-
// Main
|
|
178
|
+
// ── Main orchestrator ──────────────────────────────────────────────────
|
|
1148
179
|
// eslint-disable-next-line sonarjs/cognitive-complexity -- Pre-existing: main() orchestrates multi-step gate workflow
|
|
1149
180
|
async function executeGates(opts) {
|
|
1150
181
|
const cwd = opts.cwd ?? process.cwd();
|
|
@@ -1161,7 +192,6 @@ async function executeGates(opts) {
|
|
|
1161
192
|
// WU-2244: Full coverage flag forces full test suite and coverage gate (deterministic)
|
|
1162
193
|
const isFullCoverage = opts.fullCoverage || false;
|
|
1163
194
|
// WU-1262: Resolve coverage config from methodology policy
|
|
1164
|
-
// This derives coverage threshold and mode from methodology.testing setting
|
|
1165
195
|
// WU-1280: Use resolveTestPolicy to also get tests_required for test failure handling
|
|
1166
196
|
const resolvedTestPolicy = resolveTestPolicy(cwd);
|
|
1167
197
|
// WU-1433: Coverage gate mode (warn or block)
|
|
@@ -1170,7 +200,6 @@ async function executeGates(opts) {
|
|
|
1170
200
|
const coverageMode = opts.coverageMode || resolvedTestPolicy.mode || COVERAGE_GATE_MODES.BLOCK;
|
|
1171
201
|
const coverageThreshold = resolvedTestPolicy.threshold;
|
|
1172
202
|
// WU-1280: Determine if tests are required (affects whether test failures block or warn)
|
|
1173
|
-
// When tests_required=false (methodology.testing: none), test failures produce warnings only
|
|
1174
203
|
const testsRequired = resolvedTestPolicy.tests_required;
|
|
1175
204
|
// WU-1191: Lane health gate mode (warn, error, or off)
|
|
1176
205
|
const laneHealthMode = loadLaneHealthConfig(cwd);
|
|
@@ -1182,7 +211,7 @@ async function executeGates(opts) {
|
|
|
1182
211
|
// WU-1520: Track gate results for summary
|
|
1183
212
|
const gateResults = [];
|
|
1184
213
|
if (useAgentMode) {
|
|
1185
|
-
console.log(
|
|
214
|
+
console.log(`\uD83E\uDDFE gates (agent mode): output -> ${agentLog.logPath} (use --verbose for streaming)\n`);
|
|
1186
215
|
}
|
|
1187
216
|
// WU-2062: Detect risk tier from changed files (unless explicit --docs-only flag)
|
|
1188
217
|
let riskTier = null;
|
|
@@ -1194,14 +223,14 @@ async function executeGates(opts) {
|
|
|
1194
223
|
const logLine = useAgentMode
|
|
1195
224
|
? (line) => writeSync(agentLog.logFd, `${line}\n`)
|
|
1196
225
|
: (line) => console.log(line);
|
|
1197
|
-
logLine(`\n
|
|
226
|
+
logLine(`\n\uD83C\uDFAF Risk tier detected: ${riskTier.tier}`);
|
|
1198
227
|
if (riskTier.highRiskPaths.length > 0) {
|
|
1199
228
|
logLine(` High-risk paths: ${riskTier.highRiskPaths.slice(0, 3).join(', ')}${riskTier.highRiskPaths.length > 3 ? '...' : ''}`);
|
|
1200
229
|
}
|
|
1201
230
|
logLine('');
|
|
1202
231
|
}
|
|
1203
232
|
catch (error) {
|
|
1204
|
-
console.error('
|
|
233
|
+
console.error('\u26A0\uFE0F Risk detection failed, defaulting to standard tier:', error.message);
|
|
1205
234
|
riskTier = {
|
|
1206
235
|
tier: RISK_TIERS.STANDARD,
|
|
1207
236
|
isDocsOnly: false,
|
|
@@ -1219,9 +248,6 @@ async function executeGates(opts) {
|
|
|
1219
248
|
docsOnlyTestPlan = resolveDocsOnlyTestPlan({ codePaths });
|
|
1220
249
|
}
|
|
1221
250
|
// WU-1550: Build gate list via GateRegistry (declarative, Open-Closed Principle)
|
|
1222
|
-
// New gates can be added by calling registry.register() without modifying this function.
|
|
1223
|
-
// WU-2252: Invariants gate runs FIRST and is included in both docs-only and regular modes
|
|
1224
|
-
// WU-1520: scriptName field maps gates to their package.json script for existence checking
|
|
1225
251
|
const gateRegistry = new GateRegistry();
|
|
1226
252
|
if (effectiveDocsOnly) {
|
|
1227
253
|
registerDocsOnlyGates(gateRegistry, {
|
|
@@ -1242,8 +268,6 @@ async function executeGates(opts) {
|
|
|
1242
268
|
});
|
|
1243
269
|
}
|
|
1244
270
|
// WU-1550: Inject run functions for gates that need them.
|
|
1245
|
-
// The registry stores declarative metadata; run functions are bound here
|
|
1246
|
-
// because they depend on local gate-runner functions in this module.
|
|
1247
271
|
const gateRunFunctions = {
|
|
1248
272
|
[GATE_NAMES.FORMAT_CHECK]: runFormatCheckGate,
|
|
1249
273
|
[GATE_NAMES.SPEC_LINTER]: runSpecLinterGate,
|
|
@@ -1275,7 +299,7 @@ async function executeGates(opts) {
|
|
|
1275
299
|
// WU-1299: Show clear messaging about what's being skipped/run in docs-only mode
|
|
1276
300
|
const docsOnlyMessage = docsOnlyTestPlan && docsOnlyTestPlan.mode === 'filtered'
|
|
1277
301
|
? formatDocsOnlySkipMessage(docsOnlyTestPlan)
|
|
1278
|
-
: '
|
|
302
|
+
: '\uD83D\uDCDD Docs-only mode: skipping lint, typecheck, and all tests (no code packages in code_paths)';
|
|
1279
303
|
if (!useAgentMode) {
|
|
1280
304
|
console.log(`${docsOnlyMessage}\n`);
|
|
1281
305
|
}
|
|
@@ -1306,7 +330,7 @@ async function executeGates(opts) {
|
|
|
1306
330
|
}
|
|
1307
331
|
if (gateAction === 'fail') {
|
|
1308
332
|
const logLine = makeGateLogger({ agentLog, useAgentMode, cwd });
|
|
1309
|
-
logLine(`\n
|
|
333
|
+
logLine(`\n\u274C "${gateScriptName}" script not found in package.json (--strict mode)\n`);
|
|
1310
334
|
gateResults.push({
|
|
1311
335
|
name: gate.name,
|
|
1312
336
|
status: 'failed',
|
|
@@ -1347,7 +371,11 @@ async function executeGates(opts) {
|
|
|
1347
371
|
}
|
|
1348
372
|
else if (gate.cmd === GATE_COMMANDS.INCREMENTAL_TEST) {
|
|
1349
373
|
// WU-1920: Special handling for changed tests
|
|
1350
|
-
result = await runChangedTests({
|
|
374
|
+
result = await runChangedTests({
|
|
375
|
+
agentLog,
|
|
376
|
+
cwd,
|
|
377
|
+
scopedTestPaths: opts.scopedTestPaths,
|
|
378
|
+
});
|
|
1351
379
|
lastTestResult = result;
|
|
1352
380
|
}
|
|
1353
381
|
else if (gate.cmd === GATE_COMMANDS.TIERED_TEST) {
|
|
@@ -1358,7 +386,7 @@ async function executeGates(opts) {
|
|
|
1358
386
|
// WU-1920: Skip coverage gate when tests were changed (partial coverage)
|
|
1359
387
|
// WU-2244: --full-coverage overrides incremental skip behavior
|
|
1360
388
|
if (!isFullCoverage && lastTestResult?.isIncremental) {
|
|
1361
|
-
const msg = '
|
|
389
|
+
const msg = '\u23ED\uFE0F Skipping coverage gate (changed tests - coverage is partial)';
|
|
1362
390
|
if (!useAgentMode) {
|
|
1363
391
|
console.log(`\n${msg}\n`);
|
|
1364
392
|
}
|
|
@@ -1419,7 +447,7 @@ async function executeGates(opts) {
|
|
|
1419
447
|
if (!result.ok) {
|
|
1420
448
|
// WU-2315: Warn-only gates log warning but don't block
|
|
1421
449
|
if (gate.warnOnly) {
|
|
1422
|
-
const warnMsg =
|
|
450
|
+
const warnMsg = `\u26A0\uFE0F ${gate.name} failed (warn-only, not blocking)`;
|
|
1423
451
|
if (!useAgentMode) {
|
|
1424
452
|
console.log(`\n${warnMsg}\n`);
|
|
1425
453
|
}
|
|
@@ -1448,7 +476,7 @@ async function executeGates(opts) {
|
|
|
1448
476
|
logLine(`\n${formatGateSummary(gateResults)}\n`);
|
|
1449
477
|
if (useAgentMode) {
|
|
1450
478
|
const tail = readLogTail(agentLog.logPath);
|
|
1451
|
-
console.error(`\n
|
|
479
|
+
console.error(`\n\u274C ${gate.name} failed (agent mode). Log: ${agentLog.logPath}\n`);
|
|
1452
480
|
if (tail) {
|
|
1453
481
|
console.error(`Last log lines:\n${tail}\n`);
|
|
1454
482
|
}
|
|
@@ -1470,16 +498,16 @@ async function executeGates(opts) {
|
|
|
1470
498
|
const summaryLogLine = makeGateLogger({ agentLog, useAgentMode, cwd });
|
|
1471
499
|
summaryLogLine(`\n${formatGateSummary(gateResults)}`);
|
|
1472
500
|
if (!useAgentMode) {
|
|
1473
|
-
console.log('\n
|
|
501
|
+
console.log('\n\u2705 All gates passed!\n');
|
|
1474
502
|
}
|
|
1475
503
|
else {
|
|
1476
|
-
console.log(
|
|
504
|
+
console.log(`\u2705 All gates passed (agent mode). Log: ${agentLog.logPath}\n`);
|
|
1477
505
|
}
|
|
1478
506
|
return true;
|
|
1479
507
|
}
|
|
1480
508
|
// WU-1537: Wrap executeGates in a standard main() for runCLI consistency
|
|
1481
509
|
async function main() {
|
|
1482
|
-
const opts =
|
|
510
|
+
const opts = parseGatesOptions();
|
|
1483
511
|
const ok = await executeGates({ ...opts, argv: process.argv.slice(2) });
|
|
1484
512
|
if (!ok) {
|
|
1485
513
|
process.exit(EXIT_CODES.ERROR);
|