@shrkcrft/cli 0.1.0-alpha.26 → 0.1.0-alpha.28
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/dist/command-registry.d.ts +12 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +25 -0
- package/dist/commands/baseline.command.d.ts +8 -0
- package/dist/commands/baseline.command.d.ts.map +1 -0
- package/dist/commands/baseline.command.js +511 -0
- package/dist/commands/changelog-data.d.ts.map +1 -1
- package/dist/commands/changelog-data.js +43 -0
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +28 -1
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +112 -0
- package/dist/commands/delegate.command.d.ts +76 -1
- package/dist/commands/delegate.command.d.ts.map +1 -1
- package/dist/commands/delegate.command.js +585 -25
- package/dist/commands/finish.command.js +4 -4
- package/dist/commands/gates.command.d.ts +6 -0
- package/dist/commands/gates.command.d.ts.map +1 -0
- package/dist/commands/gates.command.js +334 -0
- package/dist/commands/generated.command.d.ts +6 -0
- package/dist/commands/generated.command.d.ts.map +1 -0
- package/dist/commands/generated.command.js +514 -0
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +73 -0
- package/dist/commands/ingest.command.d.ts +11 -0
- package/dist/commands/ingest.command.d.ts.map +1 -1
- package/dist/commands/ingest.command.js +49 -23
- package/dist/commands/policy-lint.command.d.ts +37 -0
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +119 -2
- package/dist/commands/registry-resolve.d.ts +11 -4
- package/dist/commands/registry-resolve.d.ts.map +1 -1
- package/dist/commands/registry-resolve.js +50 -24
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +36 -4
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +7 -1
- package/dist/commands/wiring.command.d.ts.map +1 -1
- package/dist/commands/wiring.command.js +113 -14
- package/dist/exit-codes.d.ts +41 -0
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +85 -0
- package/dist/finish/run-finish.d.ts +22 -3
- package/dist/finish/run-finish.d.ts.map +1 -1
- package/dist/finish/run-finish.js +194 -18
- package/dist/gates/gate-rule-view.d.ts +35 -0
- package/dist/gates/gate-rule-view.d.ts.map +1 -0
- package/dist/gates/gate-rule-view.js +80 -0
- package/dist/gates/rule-coverage.d.ts +53 -0
- package/dist/gates/rule-coverage.d.ts.map +1 -0
- package/dist/gates/rule-coverage.js +165 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +46 -6
- package/dist/output/output-compression.d.ts.map +1 -1
- package/dist/output/output-compression.js +4 -1
- package/package.json +33 -33
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { buildRegistrationGraph, explainWiring, registrationChain, registrationGraphSignature, registrationOrphans, registrationUnprovided, } from '@shrkcrft/boundaries';
|
|
2
|
-
import { resolveProjectConfig } from '@shrkcrft/inspector';
|
|
2
|
+
import { refExists, resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
4
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
5
5
|
import * as nodePath from 'node:path';
|
|
6
|
-
import { flagBool, resolveCwd } from "../command-registry.js";
|
|
6
|
+
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
7
|
+
import { ExitCode } from "../exit-codes.js";
|
|
7
8
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
8
9
|
const SITE_DISPLAY_CAP = 50;
|
|
9
10
|
/**
|
|
@@ -23,6 +24,10 @@ export function renderWiringExplain(report, wantJson) {
|
|
|
23
24
|
process.stdout.write(` ${report.description}\n`);
|
|
24
25
|
if (report.groupBy)
|
|
25
26
|
process.stdout.write(kv('groupBy', report.groupBy) + '\n');
|
|
27
|
+
if (report.registeredMode === 'intersection') {
|
|
28
|
+
process.stdout.write(kv('registeredMode', 'intersection (must be in EVERY sink)') + '\n');
|
|
29
|
+
}
|
|
30
|
+
process.stdout.write(kv('status', report.status) + '\n');
|
|
26
31
|
process.stdout.write(kv('declared', `${report.declared.distinctCount} distinct across ${report.declared.filesScanned} file(s)`) +
|
|
27
32
|
'\n');
|
|
28
33
|
process.stdout.write(kv('registered', `${report.registered.distinctCount} distinct across ${report.registered.filesScanned} file(s)`) + '\n');
|
|
@@ -51,6 +56,27 @@ export function renderWiringExplain(report, wantJson) {
|
|
|
51
56
|
process.stdout.write(` … (${report.registeredNotDeclared.length - SITE_DISPLAY_CAP} more)\n`);
|
|
52
57
|
}
|
|
53
58
|
}
|
|
59
|
+
if (report.overlap.length > 0) {
|
|
60
|
+
process.stdout.write(`\nPresent on BOTH sides (disjoint, ${report.overlap.length}):\n`);
|
|
61
|
+
for (const s of report.overlap.slice(0, SITE_DISPLAY_CAP)) {
|
|
62
|
+
process.stdout.write(` ✗ ${s.token} (${s.file}:${s.line})\n`);
|
|
63
|
+
}
|
|
64
|
+
if (report.overlap.length > SITE_DISPLAY_CAP) {
|
|
65
|
+
process.stdout.write(` … (${report.overlap.length - SITE_DISPLAY_CAP} more)\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (report.hops && report.hops.length > 0) {
|
|
69
|
+
process.stdout.write('\nChain hops:\n');
|
|
70
|
+
for (const h of report.hops) {
|
|
71
|
+
process.stdout.write(` hop ${h.index}: ${h.fromCount} → ${h.toCount}` +
|
|
72
|
+
`${h.missing > 0 ? ` ✗ ${h.missing} missing` : ' ✓'}\n`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// A rule that checked NOTHING must never read like a clean pass.
|
|
76
|
+
if (report.skipReason) {
|
|
77
|
+
process.stdout.write(`\n! SKIPPED — ${report.skipReason}. A rule that matches nothing is a bug in the rule,\n` +
|
|
78
|
+
' not a pass. Fix the selector, or set `failOnEmpty: true` to make this a hard failure.\n');
|
|
79
|
+
}
|
|
54
80
|
for (const d of report.diagnostics)
|
|
55
81
|
process.stdout.write(` ! ${d}\n`);
|
|
56
82
|
process.stdout.write(`\nVerdict: ${report.verdict}\n`);
|
|
@@ -245,6 +271,33 @@ function noIdiomsHint(wantJson) {
|
|
|
245
271
|
function siteLine(s) {
|
|
246
272
|
return `${s.file}:${s.line} [${s.idiom}]`;
|
|
247
273
|
}
|
|
274
|
+
/**
|
|
275
|
+
* The changed-file scope for a `--changed-only` / `--base <ref>` query, or an
|
|
276
|
+
* empty result when neither flag is set (whole-graph query). `--base <ref>` diffs
|
|
277
|
+
* against that ref; bare `--changed-only` uses the working tree. Reuses the same
|
|
278
|
+
* {@link resolveChangedFiles} the `finish` composite and boundary gates use, so
|
|
279
|
+
* the scope semantics match across every changed-only surface.
|
|
280
|
+
*
|
|
281
|
+
* Two honesty guards: an unresolvable `--base` ref returns a distinct `error`
|
|
282
|
+
* (never a silent empty scope that reads as "nothing changed" over a typo'd ref);
|
|
283
|
+
* and SHRK's own engine-written state under `.sharkcraft/` (this command writes a
|
|
284
|
+
* cache + usage log) is excluded so it can't pollute an otherwise-clean tree into
|
|
285
|
+
* a false non-empty scope.
|
|
286
|
+
*/
|
|
287
|
+
function changedScopeFor(args, cwd) {
|
|
288
|
+
const base = flagString(args, 'base');
|
|
289
|
+
const changedOnly = flagBool(args, 'changed-only');
|
|
290
|
+
if (!base && !changedOnly)
|
|
291
|
+
return {};
|
|
292
|
+
if (base && !refExists(cwd, base)) {
|
|
293
|
+
return { error: `cannot resolve --base ref '${base}' — not a valid commit/branch` };
|
|
294
|
+
}
|
|
295
|
+
const opts = base
|
|
296
|
+
? { projectRoot: cwd, since: base }
|
|
297
|
+
: { projectRoot: cwd, includeWorktree: true };
|
|
298
|
+
const files = resolveChangedFiles(opts).files.filter((f) => f !== '.sharkcraft' && !f.startsWith('.sharkcraft/'));
|
|
299
|
+
return { files };
|
|
300
|
+
}
|
|
248
301
|
async function wiringChain(args) {
|
|
249
302
|
const cwd = resolveCwd(args);
|
|
250
303
|
const wantJson = flagBool(args, 'json');
|
|
@@ -312,15 +365,39 @@ async function wiringUnprovided(args) {
|
|
|
312
365
|
}
|
|
313
366
|
if (!loaded.graph)
|
|
314
367
|
return noIdiomsHint(wantJson);
|
|
315
|
-
const
|
|
368
|
+
const scoped = changedScopeFor(args, cwd);
|
|
369
|
+
if (scoped.error) {
|
|
370
|
+
if (wantJson) {
|
|
371
|
+
process.stdout.write(asJson({ schema: loaded.graph.schema, scoped: true, error: scoped.error, verified: false }) + '\n');
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
process.stderr.write(`error: ${scoped.error}\n`);
|
|
375
|
+
}
|
|
376
|
+
return ExitCode.NotVerified;
|
|
377
|
+
}
|
|
378
|
+
const scope = scoped.files;
|
|
379
|
+
// An empty changed scope evaluated NOTHING — honest `2` (not-verified), never
|
|
380
|
+
// a green `0` that reads as "no unprovided tokens" (a25 §1.1 exit contract).
|
|
381
|
+
if (scope && scope.length === 0) {
|
|
382
|
+
if (wantJson) {
|
|
383
|
+
process.stdout.write(asJson({ schema: loaded.graph.schema, scoped: true, total: 0, unprovided: [], verified: false }) + '\n');
|
|
384
|
+
return ExitCode.NotVerified;
|
|
385
|
+
}
|
|
386
|
+
process.stdout.write(header('Unprovided tokens (declared/injected but never provided)'));
|
|
387
|
+
process.stdout.write(' – No files in the changed scope — nothing to verify (not verified).\n');
|
|
388
|
+
return ExitCode.NotVerified;
|
|
389
|
+
}
|
|
390
|
+
const unprovided = registrationUnprovided(loaded.graph, scope);
|
|
316
391
|
if (wantJson) {
|
|
317
|
-
process.stdout.write(asJson({ schema: loaded.graph.schema, total: unprovided.length, unprovided }) + '\n');
|
|
318
|
-
return unprovided.length > 0 ?
|
|
392
|
+
process.stdout.write(asJson({ schema: loaded.graph.schema, scoped: scope !== undefined, total: unprovided.length, unprovided }) + '\n');
|
|
393
|
+
return unprovided.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
|
|
319
394
|
}
|
|
320
395
|
process.stdout.write(header('Unprovided tokens (declared/injected but never provided)'));
|
|
396
|
+
if (scope)
|
|
397
|
+
process.stdout.write(kv('scope', `changed-only (${scope.length} file(s))`) + '\n');
|
|
321
398
|
if (unprovided.length === 0) {
|
|
322
|
-
process.stdout.write(
|
|
323
|
-
return
|
|
399
|
+
process.stdout.write(` ✓ Every declared/injected token${scope ? ' in the changed scope' : ''} has a provider. ✓\n`);
|
|
400
|
+
return ExitCode.VerifiedPass;
|
|
324
401
|
}
|
|
325
402
|
process.stdout.write(` ${unprovided.length} token(s) resolve to nothing at runtime:\n`);
|
|
326
403
|
for (const u of unprovided) {
|
|
@@ -328,7 +405,7 @@ async function wiringUnprovided(args) {
|
|
|
328
405
|
const where = site ? ` (${siteLine(site)})` : '';
|
|
329
406
|
process.stdout.write(` ✗ ${u.token}${where}\n`);
|
|
330
407
|
}
|
|
331
|
-
return
|
|
408
|
+
return ExitCode.Failure;
|
|
332
409
|
}
|
|
333
410
|
async function wiringOrphans(args) {
|
|
334
411
|
const cwd = resolveCwd(args);
|
|
@@ -343,14 +420,36 @@ async function wiringOrphans(args) {
|
|
|
343
420
|
}
|
|
344
421
|
if (!loaded.graph)
|
|
345
422
|
return noIdiomsHint(wantJson);
|
|
346
|
-
const
|
|
423
|
+
const scoped = changedScopeFor(args, cwd);
|
|
424
|
+
if (scoped.error) {
|
|
425
|
+
if (wantJson) {
|
|
426
|
+
process.stdout.write(asJson({ schema: loaded.graph.schema, scoped: true, error: scoped.error, verified: false }) + '\n');
|
|
427
|
+
}
|
|
428
|
+
else {
|
|
429
|
+
process.stderr.write(`error: ${scoped.error}\n`);
|
|
430
|
+
}
|
|
431
|
+
return ExitCode.NotVerified;
|
|
432
|
+
}
|
|
433
|
+
const scope = scoped.files;
|
|
434
|
+
if (scope && scope.length === 0) {
|
|
435
|
+
if (wantJson) {
|
|
436
|
+
process.stdout.write(asJson({ schema: loaded.graph.schema, scoped: true, total: 0, orphans: [], verified: false }) + '\n');
|
|
437
|
+
return ExitCode.NotVerified;
|
|
438
|
+
}
|
|
439
|
+
process.stdout.write(header('Orphan registrations (provided but nothing consumes)'));
|
|
440
|
+
process.stdout.write(' – No files in the changed scope — nothing to verify (not verified).\n');
|
|
441
|
+
return ExitCode.NotVerified;
|
|
442
|
+
}
|
|
443
|
+
const orphans = registrationOrphans(loaded.graph, scope);
|
|
347
444
|
if (wantJson) {
|
|
348
|
-
process.stdout.write(asJson({ schema: loaded.graph.schema, total: orphans.length, orphans }) + '\n');
|
|
445
|
+
process.stdout.write(asJson({ schema: loaded.graph.schema, scoped: scope !== undefined, total: orphans.length, orphans }) + '\n');
|
|
349
446
|
return 0;
|
|
350
447
|
}
|
|
351
448
|
process.stdout.write(header('Orphan registrations (provided but nothing consumes)'));
|
|
449
|
+
if (scope)
|
|
450
|
+
process.stdout.write(kv('scope', `changed-only (${scope.length} file(s))`) + '\n');
|
|
352
451
|
if (orphans.length === 0) {
|
|
353
|
-
process.stdout.write(
|
|
452
|
+
process.stdout.write(` ✓ Every provided token${scope ? ' in the changed scope' : ''} is consumed somewhere. ✓\n`);
|
|
354
453
|
return 0;
|
|
355
454
|
}
|
|
356
455
|
process.stdout.write(` ${orphans.length} provided token(s) nothing injects:\n`);
|
|
@@ -360,12 +459,12 @@ async function wiringOrphans(args) {
|
|
|
360
459
|
}
|
|
361
460
|
return 0;
|
|
362
461
|
}
|
|
363
|
-
const WIRING_USAGE = 'shrk wiring explain <ruleId> | test <candidate.json|inline> | chain <token> | unprovided | orphans [--json]';
|
|
462
|
+
const WIRING_USAGE = 'shrk wiring explain <ruleId> | test <candidate.json|inline> | chain <token> | unprovided | orphans [--changed-only | --base <ref>] [--json]';
|
|
364
463
|
export const wiringCommand = {
|
|
365
464
|
name: 'wiring',
|
|
366
|
-
description: 'Author-loop + runtime-wiring queries (no config write): `explain <ruleId>` / `test <candidate>` show what a wiring rule extracts; `chain <token>` / `unprovided` / `orphans` query the DI/registration graph (declared→provided→consumed) for the silent-at-runtime bugs imports can\'t see.',
|
|
465
|
+
description: 'Author-loop + runtime-wiring queries (no config write): `explain <ruleId>` / `test <candidate>` show what a wiring rule extracts; `chain <token>` / `unprovided` / `orphans` query the DI/registration graph (declared→provided→consumed) for the silent-at-runtime bugs imports can\'t see. `unprovided` / `orphans` accept `--changed-only` (working tree) or `--base <ref>` to scope the verdict to the changeset.',
|
|
367
466
|
usage: WIRING_USAGE,
|
|
368
|
-
booleanFlags: new Set(['json']),
|
|
467
|
+
booleanFlags: new Set(['json', 'changed-only']),
|
|
369
468
|
async run(args) {
|
|
370
469
|
const sub = args.positional[0];
|
|
371
470
|
if (sub === 'explain')
|
package/dist/exit-codes.d.ts
CHANGED
|
@@ -46,4 +46,45 @@ export declare function promoteForStrict(code: number, strict: boolean): number;
|
|
|
46
46
|
* command that actually returned `2`.
|
|
47
47
|
*/
|
|
48
48
|
export declare function argvHasStrict(argv: readonly string[]): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* True when the argv carries the global `--exit-trailer` (before the `--`
|
|
51
|
+
* sentinel). This is the machine channel that survives a pipe: when set, the
|
|
52
|
+
* final verdict is written as the LAST stderr line (`shrk-exit: <code>`), so an
|
|
53
|
+
* agent that pipes a gate to `head`/`grep` can still read shrk's real exit off a
|
|
54
|
+
* channel the pipe can't swallow. See {@link emitPipeExitSignal}.
|
|
55
|
+
*/
|
|
56
|
+
export declare function argvHasExitTrailer(argv: readonly string[]): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Is `commandPath` (space-joined, e.g. `check boundaries` / `wiring unprovided`
|
|
59
|
+
* / `finish`) a gate/verify verb whose exit code carries a chained verdict?
|
|
60
|
+
* Matches the exact path, its first-two-token subverb, or its top-level verb —
|
|
61
|
+
* so `check boundaries --json` (2 tokens) and a bare `finish` (1) both resolve.
|
|
62
|
+
*/
|
|
63
|
+
export declare function isGateVerb(commandPath: string): boolean;
|
|
64
|
+
/** Injectable surface for {@link emitPipeExitSignal} (isTTY + writer + trailer). */
|
|
65
|
+
export interface IPipeExitOptions {
|
|
66
|
+
/** True when shrk's stdout is NOT a terminal (i.e. piped/redirected). */
|
|
67
|
+
readonly piped: boolean;
|
|
68
|
+
/** True when `--exit-trailer` was requested. */
|
|
69
|
+
readonly trailer: boolean;
|
|
70
|
+
/** stderr writer; defaults to `process.stderr.write`. Overridable for tests. */
|
|
71
|
+
readonly write?: (s: string) => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Keep the honest `0`/`1`/`2` exit code READABLE through the shape agents reach
|
|
75
|
+
* for first — the trailing pipe. `<gate> | head` reports `head`'s `$?`, so a true
|
|
76
|
+
* `2` (not-verified) or `1` (failure) evaporates into a `0`. Two channels survive
|
|
77
|
+
* the pipe because both go to stderr:
|
|
78
|
+
*
|
|
79
|
+
* (a) a one-line WARNING when stdout is piped AND the code is non-zero — a
|
|
80
|
+
* masked `0`→`0` is harmless, so the note is reserved for the case that
|
|
81
|
+
* actually loses information (a masked `1`/`2`);
|
|
82
|
+
* (b) the `shrk-exit: <code>` TRAILER whenever `--exit-trailer` is set (any
|
|
83
|
+
* code), so a caller that opts in gets the verdict machine-readably.
|
|
84
|
+
*
|
|
85
|
+
* A no-op for non-gate verbs. Called once in {@link runCli} after the final
|
|
86
|
+
* (strict-promoted) code is known, so every gate/verify verb is covered without
|
|
87
|
+
* threading anything through each command.
|
|
88
|
+
*/
|
|
89
|
+
export declare function emitPipeExitSignal(commandPath: string, code: number, opts: IPipeExitOptions): void;
|
|
49
90
|
//# sourceMappingURL=exit-codes.d.ts.map
|
package/dist/exit-codes.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,oBAAY,QAAQ;IAClB,YAAY,IAAI;IAChB,OAAO,IAAI;IACX,WAAW,IAAI;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAGtE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM9D"}
|
|
1
|
+
{"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,oBAAY,QAAQ;IAClB,YAAY,IAAI;IAChB,OAAO,IAAI;IACX,WAAW,IAAI;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAGtE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM9D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMnE;AA4BD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAMvD;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,IAAI,CAWN"}
|
package/dist/exit-codes.js
CHANGED
|
@@ -59,3 +59,88 @@ export function argvHasStrict(argv) {
|
|
|
59
59
|
}
|
|
60
60
|
return false;
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* True when the argv carries the global `--exit-trailer` (before the `--`
|
|
64
|
+
* sentinel). This is the machine channel that survives a pipe: when set, the
|
|
65
|
+
* final verdict is written as the LAST stderr line (`shrk-exit: <code>`), so an
|
|
66
|
+
* agent that pipes a gate to `head`/`grep` can still read shrk's real exit off a
|
|
67
|
+
* channel the pipe can't swallow. See {@link emitPipeExitSignal}.
|
|
68
|
+
*/
|
|
69
|
+
export function argvHasExitTrailer(argv) {
|
|
70
|
+
for (const t of argv) {
|
|
71
|
+
if (t === '--')
|
|
72
|
+
break;
|
|
73
|
+
if (t === '--exit-trailer')
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Command paths (space-joined top-level + subverb, as {@link extractCommandPath}
|
|
80
|
+
* emits) whose exit code is a HONEST verdict an agent chains on — the set for
|
|
81
|
+
* which a masked exit is a real hazard. Kept deliberately broad over the gate /
|
|
82
|
+
* verify surface; membership only ever gates whether {@link emitPipeExitSignal}
|
|
83
|
+
* may write a one-line stderr note, never behavior.
|
|
84
|
+
*/
|
|
85
|
+
const GATE_VERB_PATHS = new Set([
|
|
86
|
+
'finish',
|
|
87
|
+
'gate',
|
|
88
|
+
'arch',
|
|
89
|
+
'doctor',
|
|
90
|
+
'diff-check',
|
|
91
|
+
'check boundaries',
|
|
92
|
+
'check wiring',
|
|
93
|
+
'check orphans',
|
|
94
|
+
'check policy',
|
|
95
|
+
'check imports',
|
|
96
|
+
'wiring unprovided',
|
|
97
|
+
'wiring orphans',
|
|
98
|
+
'wiring chain',
|
|
99
|
+
'registry',
|
|
100
|
+
'graph why',
|
|
101
|
+
'graph cycles',
|
|
102
|
+
]);
|
|
103
|
+
/**
|
|
104
|
+
* Is `commandPath` (space-joined, e.g. `check boundaries` / `wiring unprovided`
|
|
105
|
+
* / `finish`) a gate/verify verb whose exit code carries a chained verdict?
|
|
106
|
+
* Matches the exact path, its first-two-token subverb, or its top-level verb —
|
|
107
|
+
* so `check boundaries --json` (2 tokens) and a bare `finish` (1) both resolve.
|
|
108
|
+
*/
|
|
109
|
+
export function isGateVerb(commandPath) {
|
|
110
|
+
if (GATE_VERB_PATHS.has(commandPath))
|
|
111
|
+
return true;
|
|
112
|
+
const parts = commandPath.split(' ').filter((p) => p.length > 0);
|
|
113
|
+
if (parts.length >= 2 && GATE_VERB_PATHS.has(`${parts[0]} ${parts[1]}`))
|
|
114
|
+
return true;
|
|
115
|
+
if (parts.length >= 1 && GATE_VERB_PATHS.has(parts[0]))
|
|
116
|
+
return true;
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Keep the honest `0`/`1`/`2` exit code READABLE through the shape agents reach
|
|
121
|
+
* for first — the trailing pipe. `<gate> | head` reports `head`'s `$?`, so a true
|
|
122
|
+
* `2` (not-verified) or `1` (failure) evaporates into a `0`. Two channels survive
|
|
123
|
+
* the pipe because both go to stderr:
|
|
124
|
+
*
|
|
125
|
+
* (a) a one-line WARNING when stdout is piped AND the code is non-zero — a
|
|
126
|
+
* masked `0`→`0` is harmless, so the note is reserved for the case that
|
|
127
|
+
* actually loses information (a masked `1`/`2`);
|
|
128
|
+
* (b) the `shrk-exit: <code>` TRAILER whenever `--exit-trailer` is set (any
|
|
129
|
+
* code), so a caller that opts in gets the verdict machine-readably.
|
|
130
|
+
*
|
|
131
|
+
* A no-op for non-gate verbs. Called once in {@link runCli} after the final
|
|
132
|
+
* (strict-promoted) code is known, so every gate/verify verb is covered without
|
|
133
|
+
* threading anything through each command.
|
|
134
|
+
*/
|
|
135
|
+
export function emitPipeExitSignal(commandPath, code, opts) {
|
|
136
|
+
if (!isGateVerb(commandPath))
|
|
137
|
+
return;
|
|
138
|
+
const write = opts.write ?? ((s) => void process.stderr.write(s));
|
|
139
|
+
if (opts.piped && code !== 0) {
|
|
140
|
+
write(`note: stdout is piped — $? reflects the downstream command, not shrk (exit ${code}); ` +
|
|
141
|
+
`use PIPESTATUS[0] or --exit-trailer to read shrk's verdict.\n`);
|
|
142
|
+
}
|
|
143
|
+
// The trailer is written LAST so it is the final stderr line a caller reads.
|
|
144
|
+
if (opts.trailer)
|
|
145
|
+
write(`shrk-exit: ${code}\n`);
|
|
146
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type IChangedScopeOptions } from '@shrkcrft/inspector';
|
|
2
|
+
import { ExitCode } from '../exit-codes.js';
|
|
2
3
|
export declare const FINISH_SCHEMA: "sharkcraft.finish/v1";
|
|
3
4
|
/** Outcome of one sub-gate. `skipped` = nothing to evaluate (loud, never silent green). */
|
|
4
5
|
export type FinishGateStatus = 'pass' | 'fail' | 'skipped';
|
|
@@ -9,7 +10,7 @@ export interface IFinishItem {
|
|
|
9
10
|
readonly message: string;
|
|
10
11
|
}
|
|
11
12
|
export interface IFinishGate {
|
|
12
|
-
readonly name: 'boundaries' | 'imports' | 'wiring' | 'policy' | 'orphans';
|
|
13
|
+
readonly name: 'boundaries' | 'imports' | 'wiring' | 'unprovided' | 'policy' | 'orphans' | 'arch';
|
|
13
14
|
readonly status: FinishGateStatus;
|
|
14
15
|
/** One-line reason (e.g. why skipped, or the error/warning counts). */
|
|
15
16
|
readonly detail: string;
|
|
@@ -17,6 +18,14 @@ export interface IFinishGate {
|
|
|
17
18
|
readonly warnings: number;
|
|
18
19
|
/** Failing/notable items (capped by the renderer, full in JSON). */
|
|
19
20
|
readonly items: readonly IFinishItem[];
|
|
21
|
+
/**
|
|
22
|
+
* Advisory gates report signal but NEVER decide the verdict: they cannot fail
|
|
23
|
+
* the composite and do not count as "something was evaluated" (so an advisory
|
|
24
|
+
* pass can't turn an all-skipped run green). Used by `arch`, whose cycle
|
|
25
|
+
* findings are change-informative but must not attribute a pre-existing cycle
|
|
26
|
+
* to this changeset.
|
|
27
|
+
*/
|
|
28
|
+
readonly advisory?: boolean;
|
|
20
29
|
}
|
|
21
30
|
export interface IFinishImpact {
|
|
22
31
|
readonly ran: boolean;
|
|
@@ -35,8 +44,18 @@ export interface IFinishReport {
|
|
|
35
44
|
};
|
|
36
45
|
readonly gates: readonly IFinishGate[];
|
|
37
46
|
readonly impact: IFinishImpact;
|
|
38
|
-
/**
|
|
39
|
-
|
|
47
|
+
/**
|
|
48
|
+
* The honest tri-state verdict:
|
|
49
|
+
* `fail` — a deciding gate failed (or the config could not load).
|
|
50
|
+
* `not-verified` — NOTHING was actually evaluated (every deciding gate
|
|
51
|
+
* skipped / the changed scope had nothing to gate). Never a
|
|
52
|
+
* green `pass` — "evaluated nothing" is `2`, not `0`.
|
|
53
|
+
* `pass` — at least one deciding gate ran over a real scope and every
|
|
54
|
+
* deciding gate passed.
|
|
55
|
+
*/
|
|
56
|
+
readonly verdict: 'pass' | 'fail' | 'not-verified';
|
|
57
|
+
/** The exit code this verdict maps to (0 pass / 1 fail / 2 not-verified). */
|
|
58
|
+
readonly exit: ExitCode;
|
|
40
59
|
/** Total warning-severity findings across gates (non-blocking). */
|
|
41
60
|
readonly warnings: number;
|
|
42
61
|
/** Set when sharkcraft.config.ts could not be loaded — forces a `fail`. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-finish.d.ts","sourceRoot":"","sources":["../../src/finish/run-finish.ts"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"run-finish.d.ts","sourceRoot":"","sources":["../../src/finish/run-finish.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,oBAAoB,EAC1B,MAAM,qBAAqB,CAAC;AAiB7B,OAAO,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAE5C,eAAO,MAAM,aAAa,EAAG,sBAA+B,CAAC;AAQ7D,2FAA2F;AAC3F,MAAM,MAAM,gBAAgB,GAAG,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;AAE3D,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,SAAS,GAAG,QAAQ,GAAG,YAAY,GAAG,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAC;IAClG,QAAQ,CAAC,MAAM,EAAE,gBAAgB,CAAC;IAClC,uEAAuE;IACvE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,oEAAoE;IACpE,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC;;;;;;OAMG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,GAAG,EAAE,OAAO,CAAC;IACtB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IACvC,yEAAyE;IACzE,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,aAAa,CAAC;IACtC,QAAQ,CAAC,KAAK,EAAE;QACd,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;QACzD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;QAClC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;KAC5B,CAAC;IACF,QAAQ,CAAC,KAAK,EAAE,SAAS,WAAW,EAAE,CAAC;IACvC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B;;;;;;;;OAQG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,cAAc,CAAC;IACnD,6EAA6E;IAC7E,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC;IACxB,mEAAmE;IACnE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,2EAA2E;IAC3E,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC7B;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,OAAO,GAAG,OAAO,CAAC;IACzD,QAAQ,CAAC,KAAK,EAAE,oBAAoB,CAAC;CACtC;AAmBD;;;;;;;;GAQG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,aAAa,CAAC,CA2JnF"}
|