pipework 0.269.0 → 0.270.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/REFERENCE.md +42 -0
  3. package/dist/cli/commands/check-steps.d.ts +34 -0
  4. package/dist/cli/commands/check-steps.d.ts.map +1 -0
  5. package/dist/cli/commands/check-steps.js +60 -0
  6. package/dist/cli/commands/check-steps.js.map +1 -0
  7. package/dist/cli/commands/check.d.ts +43 -1
  8. package/dist/cli/commands/check.d.ts.map +1 -1
  9. package/dist/cli/commands/check.js +232 -133
  10. package/dist/cli/commands/check.js.map +1 -1
  11. package/dist/cli/commands/lint.d.ts.map +1 -1
  12. package/dist/cli/commands/lint.js +36 -1
  13. package/dist/cli/commands/lint.js.map +1 -1
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +55 -2
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/lint/doctrine.d.ts +2 -0
  18. package/dist/lint/doctrine.d.ts.map +1 -1
  19. package/dist/lint/doctrine.js +3 -1
  20. package/dist/lint/doctrine.js.map +1 -1
  21. package/dist/trace/cardinality/check.d.ts +5 -1
  22. package/dist/trace/cardinality/check.d.ts.map +1 -1
  23. package/dist/trace/cardinality/check.js +3 -1
  24. package/dist/trace/cardinality/check.js.map +1 -1
  25. package/dist/trace/check/coverage-check.d.ts +2 -0
  26. package/dist/trace/check/coverage-check.d.ts.map +1 -1
  27. package/dist/trace/check/coverage-check.js +3 -1
  28. package/dist/trace/check/coverage-check.js.map +1 -1
  29. package/dist/trace/check/flow-check.d.ts +2 -0
  30. package/dist/trace/check/flow-check.d.ts.map +1 -1
  31. package/dist/trace/check/flow-check.js +3 -1
  32. package/dist/trace/check/flow-check.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/cli/commands/check-steps.ts +98 -0
  35. package/src/cli/commands/check.ts +283 -147
  36. package/src/cli/commands/lint.ts +34 -1
  37. package/src/cli/index.ts +56 -2
  38. package/src/lint/doctrine.ts +5 -1
  39. package/src/trace/cardinality/check.ts +9 -2
  40. package/src/trace/check/coverage-check.ts +5 -1
  41. package/src/trace/check/flow-check.ts +5 -1
@@ -2,7 +2,8 @@ import { spawnSync } from 'node:child_process'
2
2
  import { dirname, relative } from 'node:path'
3
3
  import { findConfig } from '../../core/config/discover.js'
4
4
  import { error } from '../output.js'
5
- import { builtinConfigArgs, lintTargets, loadLintIncludes, resolveEslintBin } from './check.js'
5
+ import { builtinConfigArgs, lintPopulation, lintTargets, loadLintIncludes, resolveEslintBin, resolveModulePaths } from './check.js'
6
+ import { parseShard } from './shard.js'
6
7
 
7
8
  // eslint options that take their value as the next argument. Anything else starting
8
9
  // with `-` is a switch, so the argument after it is a file.
@@ -36,6 +37,7 @@ export function namesFiles(args: readonly string[]): boolean {
36
37
  * those. Every argument is forwarded verbatim. Returns eslint's exit code.
37
38
  */
38
39
  export async function lintCommand(cwd: string, args: readonly string[]): Promise<number> {
40
+ if (args.includes('--list-files')) return listFiles(cwd, args)
39
41
  const eslintBin = resolveEslintBin(cwd)
40
42
  const configPath = findConfig(cwd)
41
43
  const root = configPath === null ? cwd : dirname(configPath)
@@ -60,3 +62,34 @@ export async function lintCommand(cwd: string, args: readonly string[]): Promise
60
62
  }
61
63
  return result.status ?? 1
62
64
  }
65
+
66
+ /**
67
+ * `pipework lint --list-files [--shard i/n]`: the repo-relative files `check`'s lint
68
+ * pass covers, one per line, read from pipework.config.ts. Nothing is linted.
69
+ */
70
+ async function listFiles(cwd: string, args: readonly string[]): Promise<number> {
71
+ const rest = args.filter(a => a !== '--list-files')
72
+ let shard
73
+ if (rest.length === 2 && rest[0] === '--shard') {
74
+ try {
75
+ shard = parseShard(rest[1]!)
76
+ } catch (err) {
77
+ error(err instanceof Error ? err.message : String(err))
78
+ return 1
79
+ }
80
+ } else if (rest.length > 0) {
81
+ error(`--list-files takes only --shard <i>/<n>; got ${rest.join(' ')}. It lists files and does not run eslint.`)
82
+ return 1
83
+ }
84
+
85
+ const configPath = findConfig(cwd)
86
+ const root = configPath === null ? cwd : dirname(configPath)
87
+ const scope = await resolveModulePaths(root)
88
+ if (scope.configError !== undefined) {
89
+ error(`Cannot list lint files: pipework.config.ts failed to load: ${scope.configError.message}`)
90
+ return 1
91
+ }
92
+ const population = lintPopulation(root, scope.paths, await loadLintIncludes(root), shard)
93
+ if (population.files.length > 0) process.stdout.write(population.files.join('\n') + '\n')
94
+ return 0
95
+ }
package/src/cli/index.ts CHANGED
@@ -18,8 +18,13 @@ ${bold('Commands:')}
18
18
  openapi Write an http surface's OpenAPI document (no database needed; --surface <name>, --out <file>)
19
19
  run <script> Execute a script as a traced, audited flow (with --tenant: one transaction, rolled back if it throws)
20
20
  test Run tests with automatic database setup
21
- check Type-check, lint, and boundary enforcement
21
+ check Type-check, lint, and boundary enforcement (--format json, --only/--skip steps)
22
+ doctrine [files] Doctrine rules alone; JSON per violation and per scanned file with --format json
23
+ flow [files] Flow annotation rules alone (same options)
24
+ cardinality [files] @downstream cardinality rules alone (same options)
25
+ startup Startup validation alone: pipework.config.ts loaded, no database
22
26
  lint [eslint args] Lint the current module (the workspace at its root) with check's config; every argument goes to eslint
27
+ lint --list-files [--shard i/n]: print the files check's lint covers, lint nothing
23
28
  codemod Fill missing @downstream cardinality across the corpus
24
29
  db Database lifecycle: setup, teardown, reset
25
30
  alerts unit-fault Record an abnormal systemd stop (ExecStopPost=pipework alerts unit-fault --unit %n)
@@ -49,6 +54,15 @@ ${bold('Scoping (check, test):')}
49
54
  --reap Drop orphan test databases and exit (test only)
50
55
  --shard <i>/<n> Run part i of n disjoint parts of the selected run (test, check); the n parts together equal the unsharded run
51
56
 
57
+ ${bold('Steps (check, doctrine, flow, cardinality, startup):')}
58
+ --format json Structured result: {step, status, pipeworkVersion, reads, files, violations}; check emits an array, one record per step
59
+ --output-file <path> Write the JSON there instead of stdout (human lines stay on stderr)
60
+ --only <a,b> check: run only these steps
61
+ --skip <a,b> check: run all but these steps (not with --only)
62
+ Steps: checksums, vitest-config, eslint-config, config-load, doctrine, flow, cardinality,
63
+ coverage, lint, typecheck, startup, boundaries, dependencies, install
64
+ doctrine, flow and cardinality take file paths: only those files are judged
65
+
52
66
  ${bold('Examples:')}
53
67
  pipework init
54
68
  pipework upgrade
@@ -74,14 +88,27 @@ ${bold('Examples:')}
74
88
  pipework test --reap # drop orphan test databases
75
89
  pipework check --staged
76
90
  pipework check --all
91
+ pipework check --format json --skip lint,typecheck --output-file r.json
92
+ pipework doctrine --format json --output-file d.json src/a.ts src/b.ts
77
93
  pipework lint # the module you are in, exactly as check lints it
78
94
  pipework lint --format json --output-file r.json src/a.ts # only src/a.ts
95
+ pipework lint --list-files --shard 1/4 # the files check's lint pass covers for part 1 of 4, from pipework.config.ts
79
96
  pipework codemod
80
97
  pipework codemod --dry-run
81
98
  pipework alerts unit-fault --unit subseam-core.service # reads $SERVICE_RESULT, $EXIT_CODE, $EXIT_STATUS
82
99
  pipework alerts stale --max-age jobs.claim=5m # exit 1 with a line per stale loop; 0 and silent when none
83
100
  `
84
101
 
102
+ function parseFormat(format: string | undefined, outputFile: string | undefined): 'human' | 'json' {
103
+ if (format !== undefined && format !== 'human' && format !== 'json') {
104
+ throw new Error(`--format takes "json" or "human"; got "${format}".`)
105
+ }
106
+ if (outputFile !== undefined && format !== 'json') {
107
+ throw new Error('--output-file needs --format json.')
108
+ }
109
+ return format === 'json' ? 'json' : 'human'
110
+ }
111
+
85
112
  export async function run(argv: string[]): Promise<void> {
86
113
  // `lint` hands everything after it to eslint, flags included, so it is dispatched
87
114
  // before parseArgs, which would refuse eslint's options.
@@ -129,6 +156,10 @@ export async function run(argv: string[]): Promise<void> {
129
156
  test: { type: 'boolean' },
130
157
  'admin-url': { type: 'string' },
131
158
  'dry-run': { type: 'boolean' },
159
+ format: { type: 'string' },
160
+ 'output-file': { type: 'string' },
161
+ only: { type: 'string' },
162
+ skip: { type: 'string' },
132
163
  },
133
164
  })
134
165
 
@@ -256,7 +287,30 @@ export async function run(argv: string[]): Promise<void> {
256
287
  const { parseShard: parseShardCheck } = await import('./commands/shard.js')
257
288
  const shardCheck = values.shard === undefined ? undefined : parseShardCheck(values.shard)
258
289
  const { check } = await import('./commands/check.js')
259
- await check(cwd, parsedCheck.scope, shardCheck)
290
+ const { parseStepSelection } = await import('./commands/check-steps.js')
291
+ await check(cwd, parsedCheck.scope, shardCheck, {
292
+ format: parseFormat(values.format, values['output-file']),
293
+ outputFile: values['output-file'],
294
+ select: parseStepSelection(values.only, values.skip),
295
+ })
296
+ break
297
+ }
298
+ case 'doctrine':
299
+ case 'flow':
300
+ case 'cardinality':
301
+ case 'startup': {
302
+ if (values.only !== undefined || values.skip !== undefined) {
303
+ throw new Error(`--only and --skip select steps of check; ${command} is one step.`)
304
+ }
305
+ const { parseScope: parseScopeStep } = await import('./commands/scope.js')
306
+ const parsedStep = parseScopeStep({ all: values.all, module: values.module, staged: values.staged, fast: values.fast, full: values.full })
307
+ const { stepCommand } = await import('./commands/check.js')
308
+ process.exitCode = await stepCommand(cwd, command, {
309
+ format: parseFormat(values.format, values['output-file']),
310
+ outputFile: values['output-file'],
311
+ scope: parsedStep.scope,
312
+ files: positionals.slice(1),
313
+ })
260
314
  break
261
315
  }
262
316
  case 'codemod': {
@@ -11,11 +11,14 @@ export interface DoctrineViolation {
11
11
  export interface DoctrineResult {
12
12
  readonly violations: readonly DoctrineViolation[]
13
13
  readonly scanned: number
14
+ /** Every file read, in input order; a clean one has no violation. */
15
+ readonly scannedFiles: readonly string[]
14
16
  }
15
17
 
16
18
  export function validateDoctrine(cwd: string, sourceFiles: readonly string[]): DoctrineResult {
17
19
  const violations: DoctrineViolation[] = []
18
20
  let scanned = 0
21
+ const scannedFiles: string[] = []
19
22
 
20
23
  for (const file of sourceFiles) {
21
24
  const abs = join(cwd, file)
@@ -24,6 +27,7 @@ export function validateDoctrine(cwd: string, sourceFiles: readonly string[]): D
24
27
  const content = readFileSync(abs, 'utf-8')
25
28
  const lines = content.split('\n')
26
29
  scanned++
30
+ scannedFiles.push(file)
27
31
 
28
32
  checkSchemaTableUsage(file, lines, violations)
29
33
  checkRawPgTableImport(file, lines, violations)
@@ -31,7 +35,7 @@ export function validateDoctrine(cwd: string, sourceFiles: readonly string[]): D
31
35
  checkHandWrittenTraitFields(file, lines, violations)
32
36
  }
33
37
 
34
- return { violations, scanned }
38
+ return { violations, scanned, scannedFiles }
35
39
  }
36
40
 
37
41
  const SCHEMA_TABLE_PATTERN = /\bschema\.table\s*\(/
@@ -8,9 +8,15 @@ import { join } from 'node:path'
8
8
  import { collectFlowStepSites } from '../check/flow-step-site.js'
9
9
  import { analyzeCardinality, type AnalyzerEntry, type CardinalityResult } from './analyze.js'
10
10
 
11
+ export interface CardinalityCheckResult extends CardinalityResult {
12
+ /** Every file parsed, in input order; a clean one has no violation. */
13
+ readonly scannedFiles: readonly string[]
14
+ }
15
+
11
16
  /** Analyzes `@downstream` cardinality across `sourceFiles` (paths relative to `cwd`). */
12
- export function checkCardinality(cwd: string, sourceFiles: readonly string[]): CardinalityResult {
17
+ export function checkCardinality(cwd: string, sourceFiles: readonly string[]): CardinalityCheckResult {
13
18
  const entries: AnalyzerEntry[] = []
19
+ const scannedFiles: string[] = []
14
20
 
15
21
  for (const file of sourceFiles) {
16
22
  const abs = join(cwd, file)
@@ -23,11 +29,12 @@ export function checkCardinality(cwd: string, sourceFiles: readonly string[]): C
23
29
  // the type checker's diagnostic, not this check's.
24
30
  continue
25
31
  }
32
+ scannedFiles.push(file)
26
33
 
27
34
  for (const site of sites) {
28
35
  entries.push({ file, site })
29
36
  }
30
37
  }
31
38
 
32
- return analyzeCardinality(entries)
39
+ return { ...analyzeCardinality(entries), scannedFiles }
33
40
  }
@@ -37,6 +37,8 @@ export interface CoverageViolation {
37
37
  export interface CoverageCheckResult {
38
38
  readonly violations: readonly CoverageViolation[]
39
39
  readonly scanned: number
40
+ /** Every file parsed, in input order; a clean one has no violation. */
41
+ readonly scannedFiles: readonly string[]
40
42
  }
41
43
 
42
44
  interface IoSite {
@@ -61,6 +63,7 @@ export function checkCoverage(
61
63
  ): CoverageCheckResult {
62
64
  const violations: CoverageViolation[] = []
63
65
  let scanned = 0
66
+ const scannedFiles: string[] = []
64
67
 
65
68
  for (const file of sourceFiles) {
66
69
  const abs = join(cwd, file)
@@ -78,6 +81,7 @@ export function checkCoverage(
78
81
  continue
79
82
  }
80
83
  scanned++
84
+ scannedFiles.push(file)
81
85
 
82
86
  const ioSites = collectIoSites(program, extras)
83
87
  const flowStepRanges = collectFlowStepRanges(sites)
@@ -124,7 +128,7 @@ export function checkCoverage(
124
128
  }
125
129
  }
126
130
 
127
- return { violations, scanned }
131
+ return { violations, scanned, scannedFiles }
128
132
  }
129
133
 
130
134
  interface FlowStepRange {
@@ -23,12 +23,15 @@ export interface FlowViolation {
23
23
  export interface FlowCheckResult {
24
24
  readonly violations: readonly FlowViolation[]
25
25
  readonly scanned: number
26
+ /** Every file parsed, in input order; a clean one has no violation. */
27
+ readonly scannedFiles: readonly string[]
26
28
  }
27
29
 
28
30
  /** Validates every `@flow_step` annotation in `sourceFiles` (paths relative to `cwd`). */
29
31
  export function checkFlowAnnotations(cwd: string, sourceFiles: readonly string[]): FlowCheckResult {
30
32
  const violations: FlowViolation[] = []
31
33
  let scanned = 0
34
+ const scannedFiles: string[] = []
32
35
 
33
36
  for (const file of sourceFiles) {
34
37
  const abs = join(cwd, file)
@@ -42,13 +45,14 @@ export function checkFlowAnnotations(cwd: string, sourceFiles: readonly string[]
42
45
  continue
43
46
  }
44
47
  scanned++
48
+ scannedFiles.push(file)
45
49
 
46
50
  for (const site of sites) {
47
51
  collectSiteViolations(file, site, violations)
48
52
  }
49
53
  }
50
54
 
51
- return { violations, scanned }
55
+ return { violations, scanned, scannedFiles }
52
56
  }
53
57
 
54
58
  function collectSiteViolations(file: string, site: FlowStepSite, out: FlowViolation[]): void {