pipework 0.269.0 → 0.271.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/CHANGELOG.md +9 -0
- package/REFERENCE.md +44 -0
- package/dist/cli/commands/check-steps.d.ts +34 -0
- package/dist/cli/commands/check-steps.d.ts.map +1 -0
- package/dist/cli/commands/check-steps.js +60 -0
- package/dist/cli/commands/check-steps.js.map +1 -0
- package/dist/cli/commands/check.d.ts +43 -1
- package/dist/cli/commands/check.d.ts.map +1 -1
- package/dist/cli/commands/check.js +232 -133
- package/dist/cli/commands/check.js.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +36 -1
- package/dist/cli/commands/lint.js.map +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +55 -2
- package/dist/cli/index.js.map +1 -1
- package/dist/lint/config.d.ts +4 -0
- package/dist/lint/config.d.ts.map +1 -1
- package/dist/lint/config.js +6 -0
- package/dist/lint/config.js.map +1 -1
- package/dist/lint/doctrine.d.ts +4 -0
- package/dist/lint/doctrine.d.ts.map +1 -1
- package/dist/lint/doctrine.js +14 -7
- package/dist/lint/doctrine.js.map +1 -1
- package/dist/lint/rules/doctrine.d.ts +3 -0
- package/dist/lint/rules/doctrine.d.ts.map +1 -0
- package/dist/lint/rules/doctrine.js +28 -0
- package/dist/lint/rules/doctrine.js.map +1 -0
- package/dist/trace/cardinality/check.d.ts +5 -1
- package/dist/trace/cardinality/check.d.ts.map +1 -1
- package/dist/trace/cardinality/check.js +3 -1
- package/dist/trace/cardinality/check.js.map +1 -1
- package/dist/trace/check/coverage-check.d.ts +2 -0
- package/dist/trace/check/coverage-check.d.ts.map +1 -1
- package/dist/trace/check/coverage-check.js +3 -1
- package/dist/trace/check/coverage-check.js.map +1 -1
- package/dist/trace/check/flow-check.d.ts +2 -0
- package/dist/trace/check/flow-check.d.ts.map +1 -1
- package/dist/trace/check/flow-check.js +3 -1
- package/dist/trace/check/flow-check.js.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/check-steps.ts +98 -0
- package/src/cli/commands/check.ts +283 -147
- package/src/cli/commands/lint.ts +34 -1
- package/src/cli/index.ts +56 -2
- package/src/lint/config.ts +6 -0
- package/src/lint/doctrine.ts +17 -8
- package/src/lint/rules/doctrine.ts +31 -0
- package/src/trace/cardinality/check.ts +9 -2
- package/src/trace/check/coverage-check.ts +5 -1
- package/src/trace/check/flow-check.ts +5 -1
|
@@ -2,7 +2,7 @@ import { execSync, execFile } from 'node:child_process';
|
|
|
2
2
|
import { promisify } from 'node:util';
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync } from "node:fs";
|
|
4
4
|
import { createHash } from 'node:crypto';
|
|
5
|
-
import { join, dirname, isAbsolute, resolve } from 'node:path';
|
|
5
|
+
import { join, dirname, isAbsolute, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
7
|
import { createRequire } from 'node:module';
|
|
8
8
|
const execFileAsync = promisify(execFile);
|
|
@@ -22,6 +22,10 @@ import { expandGlob, resolveModules } from '../../workspace/discover.js';
|
|
|
22
22
|
import { filesMatchingGlobs, globRoot, lintIncludeGlobs } from '../../lint/include.js';
|
|
23
23
|
import { StartupValidationError } from '../../core/validation/types.js';
|
|
24
24
|
import { shardArg, shardModules } from './shard.js';
|
|
25
|
+
import { record, emitJson } from './check-steps.js';
|
|
26
|
+
function stepRecord(step, status, violations = [], scannedFiles) {
|
|
27
|
+
return record(step, pipeworkVersion(), status, violations, scannedFiles);
|
|
28
|
+
}
|
|
25
29
|
// #322: discoverInstance evaluates pipework.config.ts — a dynamic import plus a
|
|
26
30
|
// full module evaluation. A single `check` run needs it from four call sites;
|
|
27
31
|
// memoizing collapses that to one load. The cache is reset at the start of
|
|
@@ -35,21 +39,39 @@ async function discoverInstanceCached(cwd) {
|
|
|
35
39
|
}
|
|
36
40
|
return loading;
|
|
37
41
|
}
|
|
38
|
-
export async function check(cwd, scope, shard) {
|
|
42
|
+
export async function check(cwd, scope, shard, options = {}) {
|
|
39
43
|
instanceCache = new Map();
|
|
40
44
|
let failed = false;
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
45
|
+
const selected = options.select ?? (() => true);
|
|
46
|
+
const records = [];
|
|
47
|
+
// A deselected step is named in the human output; a step whose precondition is
|
|
48
|
+
// absent is skipped as it always was, without a line.
|
|
49
|
+
const skip = (step, label, reason) => {
|
|
50
|
+
if (!selected(step))
|
|
51
|
+
warn(`${label} — skipped (not selected)`);
|
|
52
|
+
records.push(record(step, pipeworkVersion(), 'skipped', [], undefined, selected(step) ? reason : 'not selected'));
|
|
53
|
+
};
|
|
54
|
+
// A step that prints its own lines and answers pass/fail.
|
|
55
|
+
const plain = async (step, label, fn) => {
|
|
56
|
+
if (!selected(step))
|
|
57
|
+
return skip(step, label, '');
|
|
58
|
+
const ok = await fn();
|
|
59
|
+
records.push(stepRecord(step, ok ? 'passed' : 'failed'));
|
|
60
|
+
if (!ok)
|
|
61
|
+
failed = true;
|
|
62
|
+
};
|
|
63
|
+
// A step that prints its own lines and returns its record.
|
|
64
|
+
const rich = async (step, label, fn) => {
|
|
65
|
+
if (!selected(step))
|
|
66
|
+
return skip(step, label, '');
|
|
67
|
+
const result = await fn();
|
|
68
|
+
records.push(result);
|
|
69
|
+
if (result.status === 'failed')
|
|
70
|
+
failed = true;
|
|
71
|
+
};
|
|
72
|
+
await plain('checksums', 'Workspace checksums', () => verifyChecksums(cwd));
|
|
73
|
+
await plain('vitest-config', 'Vitest config', () => verifyVitestConfig(cwd));
|
|
74
|
+
await plain('eslint-config', 'Eslint config', () => verifyEslintConfig(cwd));
|
|
53
75
|
const { paths: modulePaths, all: fullScope, configError, basis } = await resolveModulePaths(cwd, scope);
|
|
54
76
|
// #335: a pipework.config.ts that exists but cannot be loaded used to be
|
|
55
77
|
// swallowed — module scoping silently degraded to filesystem discovery, which
|
|
@@ -58,84 +80,132 @@ export async function check(cwd, scope, shard) {
|
|
|
58
80
|
// 0 with "All checks passed". A step that cannot run is a failed step: the
|
|
59
81
|
// load failure is reported here and fails the run. The remaining steps still
|
|
60
82
|
// execute so one run surfaces everything it can, but none of them can green it.
|
|
61
|
-
if (
|
|
83
|
+
if (!selected('config-load')) {
|
|
84
|
+
skip('config-load', 'Config load', '');
|
|
85
|
+
}
|
|
86
|
+
else if (configError !== undefined) {
|
|
62
87
|
info('Config load');
|
|
63
88
|
logError('Config load — pipework.config.ts could not be loaded. Config-derived steps ran degraded or not at all.');
|
|
64
89
|
for (const line of configError.message.split('\n')) {
|
|
65
90
|
process.stderr.write(` ${line}\n`);
|
|
66
91
|
}
|
|
92
|
+
records.push(stepRecord('config-load', 'failed', [{ rule: 'config/load', file: CONFIG_FILE, line: 0, message: configError.message }]));
|
|
67
93
|
failed = true;
|
|
68
94
|
}
|
|
95
|
+
else {
|
|
96
|
+
records.push(stepRecord('config-load', 'passed'));
|
|
97
|
+
}
|
|
69
98
|
if (modulePaths.length > 0) {
|
|
70
99
|
const sourceFiles = findSourceFiles(cwd, modulePaths);
|
|
71
|
-
|
|
72
|
-
if (!doctrineOk) {
|
|
73
|
-
failed = true;
|
|
74
|
-
}
|
|
100
|
+
await rich('doctrine', 'Doctrine validation', () => doctrineStep(cwd, sourceFiles));
|
|
75
101
|
// Phase 4b: the flow and cardinality checks are hard errors. Consumers
|
|
76
102
|
// upgrading from an earlier release run `pipework codemod` once to fill
|
|
77
103
|
// missing `@downstream` cardinalities; anything the codemod cannot
|
|
78
104
|
// resolve (indirection, non-trivial form, method flow steps, orphans)
|
|
79
105
|
// must be fixed by hand before the build will pass.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
const configuredStructuralMax = await loadConfiguredStructuralMax(cwd);
|
|
84
|
-
if (!runCardinalityCheck(cwd, sourceFiles, configuredStructuralMax, fullScope)) {
|
|
85
|
-
failed = true;
|
|
86
|
-
}
|
|
106
|
+
await rich('flow', 'Flow annotations', () => flowStep(cwd, sourceFiles));
|
|
107
|
+
await rich('cardinality', 'Flow cardinality', async () => cardinalityStep(cwd, sourceFiles, await loadConfiguredStructuralMax(cwd), fullScope));
|
|
87
108
|
// Phase 6f: Tier-2 coverage ships as warning. Surfaces structural gaps —
|
|
88
109
|
// I/O outside any flow_step, more than one I/O in a single flow_step —
|
|
89
110
|
// without failing the build. Phase 7a flips it to error once consumer
|
|
90
111
|
// corpora have migrated to the new write surface. Consumers extend the
|
|
91
112
|
// I/O call-target registry through `trace.io` in pipework.config.ts (Q9).
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
await rich('coverage', 'Flow coverage', async () => coverageStep(cwd, sourceFiles, await loadConsumerIoPatterns(cwd)));
|
|
114
|
+
await plain('lint', 'Lint', () => runLint(cwd, modulePaths, shard));
|
|
115
|
+
await plain('typecheck', 'Type check', () => runTypeCheck(cwd, modulePaths, shard));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
for (const [step, label] of [
|
|
119
|
+
['doctrine', 'Doctrine validation'], ['flow', 'Flow annotations'], ['cardinality', 'Flow cardinality'],
|
|
120
|
+
['coverage', 'Flow coverage'], ['lint', 'Lint'], ['typecheck', 'Type check'],
|
|
121
|
+
]) {
|
|
122
|
+
skip(step, label, 'no modules resolved');
|
|
97
123
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
124
|
+
if (scope !== undefined && scope.kind === 'diff') {
|
|
125
|
+
// The basis says WHY the set is empty — a green here must state the scope
|
|
126
|
+
// it rested on, or an empty basis reads as "everything passed".
|
|
127
|
+
success(`No affected modules — per-module checks skipped (basis: ${basis ?? 'unknown'})`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
// Not a diff scope, so this is not "nothing changed" — no module could be
|
|
131
|
+
// resolved at all. Every per-module step was skipped; say so rather than
|
|
132
|
+
// letting their absence read as a clean pass (#335).
|
|
133
|
+
warn('No modules resolved — lint, type check, doctrine and flow checks did NOT run');
|
|
101
134
|
}
|
|
102
135
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// it rested on, or an empty basis reads as "everything passed".
|
|
106
|
-
success(`No affected modules — per-module checks skipped (basis: ${basis ?? 'unknown'})`);
|
|
136
|
+
if (configError === undefined && findConfig(cwd) !== null) {
|
|
137
|
+
await plain('startup', 'Startup checks', () => runStartupChecks(cwd));
|
|
107
138
|
}
|
|
108
139
|
else {
|
|
109
|
-
|
|
110
|
-
// resolved at all. Every per-module step was skipped; say so rather than
|
|
111
|
-
// letting their absence read as a clean pass (#335).
|
|
112
|
-
warn('No modules resolved — lint, type check, doctrine and flow checks did NOT run');
|
|
113
|
-
}
|
|
114
|
-
if (configError === undefined && findConfig(cwd) !== null) {
|
|
115
|
-
if (!(await runStartupChecks(cwd))) {
|
|
116
|
-
failed = true;
|
|
117
|
-
}
|
|
140
|
+
skip('startup', 'Startup checks', 'no loadable pipework.config.ts');
|
|
118
141
|
}
|
|
119
142
|
const boundaryScript = join(cwd, 'scripts', 'check-boundaries.js');
|
|
120
143
|
if (existsSync(boundaryScript)) {
|
|
121
|
-
|
|
122
|
-
failed = true;
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
const depCheckOk = runDependencyCheck(cwd);
|
|
126
|
-
if (!depCheckOk) {
|
|
127
|
-
failed = true;
|
|
144
|
+
await plain('boundaries', 'Boundaries', () => runStep('Boundaries', 'node scripts/check-boundaries.js', cwd));
|
|
128
145
|
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
failed = true;
|
|
146
|
+
else {
|
|
147
|
+
skip('boundaries', 'Boundaries', 'no scripts/check-boundaries.js');
|
|
132
148
|
}
|
|
149
|
+
await plain('dependencies', 'Dependency boundaries', () => runDependencyCheck(cwd));
|
|
150
|
+
await plain('install', 'pipework install integrity', () => runPipeworkCopiesCheck(cwd));
|
|
151
|
+
if (options.format === 'json')
|
|
152
|
+
emitJson(records, options.outputFile);
|
|
133
153
|
if (failed)
|
|
134
154
|
process.exit(1);
|
|
135
|
-
|
|
155
|
+
const deselected = records.filter(r => r.reason === 'not selected').map(r => r.step);
|
|
156
|
+
success(deselected.length === 0 ? 'All checks passed' : `Selected checks passed (not run: ${deselected.join(', ')})`);
|
|
157
|
+
}
|
|
158
|
+
/** `pipework doctrine|flow|cardinality|startup`: one step alone. Returns the exit code. */
|
|
159
|
+
export async function stepCommand(cwd, step, options = {}) {
|
|
160
|
+
instanceCache = new Map();
|
|
161
|
+
const files = options.files ?? [];
|
|
162
|
+
let result;
|
|
163
|
+
if (step === 'startup') {
|
|
164
|
+
if (files.length > 0)
|
|
165
|
+
throw new Error('startup takes no files.');
|
|
166
|
+
result = await startupStep(cwd);
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
let sourceFiles;
|
|
170
|
+
let fullScope = false;
|
|
171
|
+
let configError;
|
|
172
|
+
if (files.length > 0) {
|
|
173
|
+
sourceFiles = files.map(f => {
|
|
174
|
+
const rel = relative(cwd, resolve(cwd, f));
|
|
175
|
+
if (rel.startsWith('..') || isAbsolute(rel))
|
|
176
|
+
throw new Error(`${f} is outside ${cwd}.`);
|
|
177
|
+
if (!existsSync(join(cwd, rel)))
|
|
178
|
+
throw new Error(`${f} does not exist.`);
|
|
179
|
+
return rel.split(sep).join('/');
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
const resolved = await resolveModulePaths(cwd, options.scope);
|
|
184
|
+
configError = resolved.configError;
|
|
185
|
+
fullScope = resolved.all;
|
|
186
|
+
sourceFiles = findSourceFiles(cwd, resolved.paths);
|
|
187
|
+
}
|
|
188
|
+
if (configError !== undefined) {
|
|
189
|
+
logError('Config load — pipework.config.ts could not be loaded.');
|
|
190
|
+
process.stderr.write(` ${configError.message}\n`);
|
|
191
|
+
result = stepRecord(step, 'failed', [{ rule: 'config/load', file: CONFIG_FILE, line: 0, message: configError.message }]);
|
|
192
|
+
}
|
|
193
|
+
else if (step === 'doctrine') {
|
|
194
|
+
result = doctrineStep(cwd, sourceFiles);
|
|
195
|
+
}
|
|
196
|
+
else if (step === 'flow') {
|
|
197
|
+
result = flowStep(cwd, sourceFiles);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
result = cardinalityStep(cwd, sourceFiles, await loadConfiguredStructuralMax(cwd), fullScope);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
if (options.format === 'json')
|
|
204
|
+
emitJson(result, options.outputFile);
|
|
205
|
+
return result.status === 'failed' ? 1 : 0;
|
|
136
206
|
}
|
|
137
207
|
/** The startup checks that need no database — what `serve` would refuse before connecting. */
|
|
138
|
-
export async function
|
|
208
|
+
export async function startupStep(cwd) {
|
|
139
209
|
info('Startup checks');
|
|
140
210
|
let doors = [];
|
|
141
211
|
let roles = [];
|
|
@@ -153,12 +223,14 @@ export async function runStartupChecks(cwd) {
|
|
|
153
223
|
for (const issue of err.issues) {
|
|
154
224
|
process.stderr.write(` [${issue.phase}] ${issue.message}\n`);
|
|
155
225
|
}
|
|
226
|
+
return stepRecord('startup', 'failed', err.issues.map(issue => ({
|
|
227
|
+
rule: `startup/${issue.phase}`, file: CONFIG_FILE, line: 0, message: issue.message,
|
|
228
|
+
})));
|
|
156
229
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
}
|
|
161
|
-
return false;
|
|
230
|
+
logError('Startup checks — could not run');
|
|
231
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
232
|
+
process.stderr.write(` ${message}\n`);
|
|
233
|
+
return stepRecord('startup', 'failed', [{ rule: 'startup/could-not-run', file: CONFIG_FILE, line: 0, message }]);
|
|
162
234
|
}
|
|
163
235
|
success('Startup checks');
|
|
164
236
|
for (const line of roles)
|
|
@@ -168,7 +240,10 @@ export async function runStartupChecks(cwd) {
|
|
|
168
240
|
for (const line of doors)
|
|
169
241
|
info(` ${line}`);
|
|
170
242
|
}
|
|
171
|
-
return
|
|
243
|
+
return stepRecord('startup', 'passed');
|
|
244
|
+
}
|
|
245
|
+
export async function runStartupChecks(cwd) {
|
|
246
|
+
return (await startupStep(cwd)).status === 'passed';
|
|
172
247
|
}
|
|
173
248
|
/** One line per platform-mode database: the role a running manifold connects as, and the only callers that may open the owner. */
|
|
174
249
|
export function describeRuntimeRoles(databases) {
|
|
@@ -435,18 +510,8 @@ function findStaleVitestConfigs(cwd) {
|
|
|
435
510
|
}
|
|
436
511
|
return stale.sort();
|
|
437
512
|
}
|
|
438
|
-
function
|
|
439
|
-
|
|
440
|
-
if (sourceFiles.length === 0) {
|
|
441
|
-
success('Doctrine validation (no source files)');
|
|
442
|
-
return true;
|
|
443
|
-
}
|
|
444
|
-
const result = validateDoctrine(cwd, sourceFiles);
|
|
445
|
-
if (result.violations.length === 0) {
|
|
446
|
-
success(`Doctrine validation (${String(result.scanned)} files scanned)`);
|
|
447
|
-
return true;
|
|
448
|
-
}
|
|
449
|
-
const grouped = groupByRule(result.violations);
|
|
513
|
+
function printGrouped(violations) {
|
|
514
|
+
const grouped = groupByRule(violations);
|
|
450
515
|
for (const [rule, items] of grouped) {
|
|
451
516
|
logError(`${rule} (${String(items.length)} violation${items.length === 1 ? '' : 's'})`);
|
|
452
517
|
for (const v of items.slice(0, 5)) {
|
|
@@ -456,36 +521,44 @@ function runDoctrineValidation(cwd, sourceFiles) {
|
|
|
456
521
|
warn(` ... and ${String(items.length - 5)} more`);
|
|
457
522
|
}
|
|
458
523
|
}
|
|
459
|
-
return false;
|
|
460
524
|
}
|
|
461
|
-
export function
|
|
525
|
+
export function doctrineStep(cwd, sourceFiles) {
|
|
526
|
+
info('Doctrine validation');
|
|
527
|
+
if (sourceFiles.length === 0) {
|
|
528
|
+
success('Doctrine validation (no source files)');
|
|
529
|
+
return stepRecord('doctrine', 'passed', [], []);
|
|
530
|
+
}
|
|
531
|
+
const result = validateDoctrine(cwd, sourceFiles);
|
|
532
|
+
if (result.violations.length === 0) {
|
|
533
|
+
success(`Doctrine validation (${String(result.scanned)} files scanned)`);
|
|
534
|
+
return stepRecord('doctrine', 'passed', [], result.scannedFiles);
|
|
535
|
+
}
|
|
536
|
+
printGrouped(result.violations);
|
|
537
|
+
return stepRecord('doctrine', 'failed', result.violations, result.scannedFiles);
|
|
538
|
+
}
|
|
539
|
+
export function flowStep(cwd, sourceFiles) {
|
|
462
540
|
info('Flow annotations');
|
|
463
541
|
if (sourceFiles.length === 0) {
|
|
464
542
|
success('Flow annotations (no source files)');
|
|
465
|
-
return
|
|
543
|
+
return stepRecord('flow', 'passed', [], []);
|
|
466
544
|
}
|
|
467
545
|
const result = checkFlowAnnotations(cwd, sourceFiles);
|
|
468
546
|
if (result.violations.length === 0) {
|
|
469
547
|
success(`Flow annotations (${String(result.scanned)} files scanned)`);
|
|
470
|
-
return
|
|
471
|
-
}
|
|
472
|
-
const grouped = groupByRule(result.violations);
|
|
473
|
-
for (const [rule, items] of grouped) {
|
|
474
|
-
logError(`${rule} (${String(items.length)} violation${items.length === 1 ? '' : 's'})`);
|
|
475
|
-
for (const v of items.slice(0, 5)) {
|
|
476
|
-
warn(` ${v.file}:${String(v.line)} — ${v.message}`);
|
|
477
|
-
}
|
|
478
|
-
if (items.length > 5) {
|
|
479
|
-
warn(` ... and ${String(items.length - 5)} more`);
|
|
480
|
-
}
|
|
548
|
+
return stepRecord('flow', 'passed', [], result.scannedFiles);
|
|
481
549
|
}
|
|
482
|
-
|
|
550
|
+
printGrouped(result.violations);
|
|
551
|
+
return stepRecord('flow', 'failed', result.violations, result.scannedFiles);
|
|
483
552
|
}
|
|
484
|
-
export function
|
|
553
|
+
export function runFlowCheck(cwd, sourceFiles) {
|
|
554
|
+
return flowStep(cwd, sourceFiles).status === 'passed';
|
|
555
|
+
}
|
|
556
|
+
const CONFIG_FILE = 'pipework.config.ts';
|
|
557
|
+
export function cardinalityStep(cwd, sourceFiles, configuredStructuralMax, fullScope = true) {
|
|
485
558
|
info('Flow cardinality');
|
|
486
559
|
if (sourceFiles.length === 0) {
|
|
487
560
|
success('Flow cardinality (no source files)');
|
|
488
|
-
return
|
|
561
|
+
return stepRecord('cardinality', 'passed', [], []);
|
|
489
562
|
}
|
|
490
563
|
const result = checkCardinality(cwd, sourceFiles);
|
|
491
564
|
// Verify the configured cap against what the analyzer just derived (Q7).
|
|
@@ -504,54 +577,57 @@ export function runCardinalityCheck(cwd, sourceFiles, configuredStructuralMax, f
|
|
|
504
577
|
// derived < configured → only conclusive when the analyzer saw every module
|
|
505
578
|
// (`fullScope`). Under a scoped run an unscanned module may hold the
|
|
506
579
|
// deeper chain, so this is deferred to the full-scope check (pre-push).
|
|
507
|
-
|
|
580
|
+
const configViolations = [];
|
|
508
581
|
if (result.analyzed > 0 && configuredStructuralMax === undefined) {
|
|
509
582
|
warn(` trace.structuralMax is unset — runtime step cap is open (truncation never fires). ` +
|
|
510
583
|
`Add this to pipework.config.ts: trace: { structuralMax: ${String(result.structuralMax)} }`);
|
|
511
584
|
}
|
|
512
585
|
else if (result.analyzed > 0 && result.structuralMax > configuredStructuralMax) {
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
586
|
+
configViolations.push({
|
|
587
|
+
rule: 'cardinality/structural-max-too-low',
|
|
588
|
+
file: CONFIG_FILE,
|
|
589
|
+
line: 0,
|
|
590
|
+
message: `trace.structuralMax is too low — config has ${String(configuredStructuralMax)}, ` +
|
|
591
|
+
`analyzer derived ${String(result.structuralMax)}. A trace this deep exceeds the cap and ` +
|
|
592
|
+
`would be truncated. Update pipework.config.ts: trace: { structuralMax: ${String(result.structuralMax)} }`,
|
|
593
|
+
});
|
|
517
594
|
}
|
|
518
595
|
else if (result.analyzed > 0 && result.structuralMax < configuredStructuralMax && fullScope) {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
warn(` ... and ${String(items.length - 5)} more`);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
return false;
|
|
596
|
+
configViolations.push({
|
|
597
|
+
rule: 'cardinality/structural-max-stale',
|
|
598
|
+
file: CONFIG_FILE,
|
|
599
|
+
line: 0,
|
|
600
|
+
message: `trace.structuralMax is stale — config has ${String(configuredStructuralMax)}, ` +
|
|
601
|
+
`analyzer derived ${String(result.structuralMax)}. ` +
|
|
602
|
+
`Update pipework.config.ts: trace: { structuralMax: ${String(result.structuralMax)} }`,
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
for (const v of configViolations)
|
|
606
|
+
logError(` ${v.message}`);
|
|
607
|
+
const violations = [...result.violations, ...configViolations];
|
|
608
|
+
if (violations.length === 0) {
|
|
609
|
+
success(`Flow cardinality (${String(result.analyzed)} flow step${result.analyzed === 1 ? '' : 's'} analyzed)`);
|
|
610
|
+
return stepRecord('cardinality', 'passed', [], result.scannedFiles);
|
|
611
|
+
}
|
|
612
|
+
printGrouped(result.violations);
|
|
613
|
+
return stepRecord('cardinality', 'failed', violations, result.scannedFiles);
|
|
541
614
|
}
|
|
542
|
-
export function
|
|
615
|
+
export function runCardinalityCheck(cwd, sourceFiles, configuredStructuralMax, fullScope = true) {
|
|
616
|
+
return cardinalityStep(cwd, sourceFiles, configuredStructuralMax, fullScope).status === 'passed';
|
|
617
|
+
}
|
|
618
|
+
export function coverageStep(cwd, sourceFiles, extras = {}) {
|
|
543
619
|
info('Flow coverage (Tier-2, warning)');
|
|
544
620
|
if (sourceFiles.length === 0) {
|
|
545
621
|
success('Flow coverage (no source files)');
|
|
546
|
-
return;
|
|
622
|
+
return stepRecord('coverage', 'passed', [], []);
|
|
547
623
|
}
|
|
548
624
|
const result = checkCoverage(cwd, sourceFiles, extras);
|
|
549
625
|
if (result.violations.length === 0) {
|
|
550
626
|
success(`Flow coverage (${String(result.scanned)} files scanned)`);
|
|
551
|
-
return;
|
|
627
|
+
return stepRecord('coverage', 'passed', [], result.scannedFiles);
|
|
552
628
|
}
|
|
553
|
-
// Warning mode: emit diagnostics but never
|
|
554
|
-
//
|
|
629
|
+
// Warning mode: emit diagnostics but never fail. This rule cannot fail the
|
|
630
|
+
// build until Phase 7a, so the record is passed with its warnings listed.
|
|
555
631
|
const grouped = groupByRule(result.violations);
|
|
556
632
|
warn(`Flow coverage (${String(result.violations.length)} warning${result.violations.length === 1 ? '' : 's'})`);
|
|
557
633
|
for (const [rule, items] of grouped) {
|
|
@@ -563,6 +639,10 @@ export function runCoverageCheckWarning(cwd, sourceFiles, extras = {}) {
|
|
|
563
639
|
warn(` ... and ${String(items.length - 5)} more`);
|
|
564
640
|
}
|
|
565
641
|
}
|
|
642
|
+
return stepRecord('coverage', 'passed', result.violations, result.scannedFiles);
|
|
643
|
+
}
|
|
644
|
+
export function runCoverageCheckWarning(cwd, sourceFiles, extras = {}) {
|
|
645
|
+
coverageStep(cwd, sourceFiles, extras);
|
|
566
646
|
}
|
|
567
647
|
function groupByRule(items) {
|
|
568
648
|
const map = new Map();
|
|
@@ -1126,7 +1206,7 @@ function runDependencyCheck(cwd) {
|
|
|
1126
1206
|
// limits on constrained hosts (#322 ran these via `Promise.all`; that is the
|
|
1127
1207
|
// OOM this reverts). The wall-clock cost of a serial loop over 2–3 modules is
|
|
1128
1208
|
// negligible next to an OOM kill.
|
|
1129
|
-
async function runLint(cwd, allModules, shard) {
|
|
1209
|
+
export async function runLint(cwd, allModules, shard) {
|
|
1130
1210
|
const modulePaths = shardModules(allModules, shard);
|
|
1131
1211
|
if (shard !== undefined && allModules.length > 0) {
|
|
1132
1212
|
info(`Lint shard ${shardArg(shard)}: ${String(modulePaths.length)} of ${String(allModules.length)} module(s)`);
|
|
@@ -1136,6 +1216,7 @@ async function runLint(cwd, allModules, shard) {
|
|
|
1136
1216
|
info('Lint');
|
|
1137
1217
|
const eslintBin = resolveEslintBin(cwd);
|
|
1138
1218
|
const includes = await loadLintIncludes(cwd);
|
|
1219
|
+
const population = lintPopulation(cwd, allModules, includes, shard);
|
|
1139
1220
|
// Per-module eslint cache, version-stamped and under the workspace-root
|
|
1140
1221
|
// node_modules/.cache — always present, always gitignored. The version stamp
|
|
1141
1222
|
// busts the cache on a pipework ruleset bump; the node_modules location keeps
|
|
@@ -1150,8 +1231,8 @@ async function runLint(cwd, allModules, shard) {
|
|
|
1150
1231
|
return ok;
|
|
1151
1232
|
}
|
|
1152
1233
|
let ok = true;
|
|
1153
|
-
for (const mod of
|
|
1154
|
-
if (!await lintDirectory(eslintBin, join(cwd, mod), mod, cacheFor(mod), includes
|
|
1234
|
+
for (const mod of population.modules) {
|
|
1235
|
+
if (!await lintDirectory(eslintBin, join(cwd, mod.path), mod.path, cacheFor(mod.path), mod.includes)) {
|
|
1155
1236
|
ok = false;
|
|
1156
1237
|
}
|
|
1157
1238
|
}
|
|
@@ -1159,16 +1240,14 @@ async function runLint(cwd, allModules, shard) {
|
|
|
1159
1240
|
// root, outside every module. Those files were missing from the files-linted
|
|
1160
1241
|
// count entirely — a coverage reading produced from a population that excluded
|
|
1161
1242
|
// them. They get their own pass, counted and named like any module's.
|
|
1162
|
-
const rootOnly =
|
|
1163
|
-
? []
|
|
1164
|
-
: rootFilesOutsideModules(cwd, allModules, includes.for('.'));
|
|
1243
|
+
const rootOnly = population.root;
|
|
1165
1244
|
if (rootOnly.length > 0) {
|
|
1166
1245
|
if (!await lintDirectory(eslintBin, cwd, 'root', cacheFor('__root'), rootOnly)) {
|
|
1167
1246
|
ok = false;
|
|
1168
1247
|
}
|
|
1169
1248
|
}
|
|
1170
1249
|
if (ok) {
|
|
1171
|
-
const modules = `${String(
|
|
1250
|
+
const modules = `${String(population.modules.length)} module${population.modules.length === 1 ? '' : 's'}`;
|
|
1172
1251
|
const root = rootOnly.length > 0
|
|
1173
1252
|
? ` + ${String(rootOnly.length)} root file${rootOnly.length === 1 ? '' : 's'}`
|
|
1174
1253
|
: '';
|
|
@@ -1176,6 +1255,26 @@ async function runLint(cwd, allModules, shard) {
|
|
|
1176
1255
|
}
|
|
1177
1256
|
return ok;
|
|
1178
1257
|
}
|
|
1258
|
+
/**
|
|
1259
|
+
* The files `check`'s lint pass covers: each sharded module's include globs plus the
|
|
1260
|
+
* root-outside-modules pass (shard 1 only). `runLint` lints this population and
|
|
1261
|
+
* `pipework lint --list-files` prints it, so the two cannot drift.
|
|
1262
|
+
*/
|
|
1263
|
+
export function lintPopulation(cwd, allModules, includes, shard) {
|
|
1264
|
+
const modules = shardModules([...allModules], shard).map(path => ({ path, includes: includes.for(path) }));
|
|
1265
|
+
const root = shard !== undefined && shard.index !== 1
|
|
1266
|
+
? []
|
|
1267
|
+
: rootFilesOutsideModules(cwd, [...allModules], includes.for('.'));
|
|
1268
|
+
const files = new Set(root);
|
|
1269
|
+
for (const mod of modules) {
|
|
1270
|
+
const dir = join(cwd, mod.path);
|
|
1271
|
+
const prefix = normalizeModulePath(mod.path);
|
|
1272
|
+
for (const f of filesMatchingGlobs(dir, lintIncludeGlobs(dir, mod.includes))) {
|
|
1273
|
+
files.add(prefix === '' ? f : `${prefix}/${f}`);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
return { modules, root, files: [...files].sort() };
|
|
1277
|
+
}
|
|
1179
1278
|
/**
|
|
1180
1279
|
* Root-level lintable files that no module covers (#402).
|
|
1181
1280
|
*
|