@shrkcrft/cli 0.1.0-alpha.27 → 0.1.0-alpha.29
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/commands/baseline.command.d.ts +8 -0
- package/dist/commands/baseline.command.d.ts.map +1 -0
- package/dist/commands/baseline.command.js +542 -0
- package/dist/commands/changelog-data.d.ts.map +1 -1
- package/dist/commands/changelog-data.js +42 -0
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +147 -12
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +120 -0
- package/dist/commands/daily.commands.d.ts.map +1 -1
- package/dist/commands/daily.commands.js +11 -1
- package/dist/commands/gates.command.d.ts +16 -0
- package/dist/commands/gates.command.d.ts.map +1 -0
- package/dist/commands/gates.command.js +377 -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 +544 -0
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +64 -2
- 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 +167 -8
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +70 -13
- package/dist/commands/wiring.command.d.ts.map +1 -1
- package/dist/commands/wiring.command.js +25 -0
- package/dist/exit-codes.d.ts +27 -7
- package/dist/exit-codes.d.ts.map +1 -1
- package/dist/exit-codes.js +47 -8
- package/dist/finish/run-finish.d.ts.map +1 -1
- package/dist/finish/run-finish.js +9 -3
- package/dist/gates/gate-envelope.d.ts +64 -0
- package/dist/gates/gate-envelope.d.ts.map +1 -0
- package/dist/gates/gate-envelope.js +26 -0
- 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 +81 -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 +27 -3
- package/package.json +33 -33
|
@@ -458,32 +458,16 @@ export const contradictionsCommand = {
|
|
|
458
458
|
return 0;
|
|
459
459
|
},
|
|
460
460
|
};
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
461
|
+
/** Classify the tree's generated code (the `report` half of `shrk generated`). */
|
|
462
|
+
export const generatedReportCommand = {
|
|
463
|
+
name: 'report',
|
|
464
|
+
description: 'Classify which parts of the tree are generated code. Read-only.',
|
|
465
|
+
usage: 'shrk [--cwd <dir>] generated report [--format text|markdown|json]',
|
|
465
466
|
async run(args) {
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
const cwd = resolveCwd(rest);
|
|
469
|
-
const format = parseFormat(flagString(rest, 'format'));
|
|
467
|
+
const cwd = resolveCwd(args);
|
|
468
|
+
const format = parseFormat(flagString(args, 'format'));
|
|
470
469
|
const inspection = await inspectSharkcraft({ cwd });
|
|
471
470
|
const report = buildGeneratedCodeReport({ inspection });
|
|
472
|
-
if (sub === 'protect') {
|
|
473
|
-
const writeDrafts = flagBool(rest, 'write-drafts');
|
|
474
|
-
const outDir = nodePath.join(cwd, INGEST_BASE);
|
|
475
|
-
if (writeDrafts) {
|
|
476
|
-
if (!existsSync(outDir))
|
|
477
|
-
mkdirSync(outDir, { recursive: true });
|
|
478
|
-
const target = nodePath.join(outDir, 'GENERATED_PROTECT.md');
|
|
479
|
-
writeFileSync(target, renderGeneratedCodeReportMarkdown(report), 'utf8');
|
|
480
|
-
process.stdout.write(`Wrote ${target}\n`);
|
|
481
|
-
return 0;
|
|
482
|
-
}
|
|
483
|
-
process.stdout.write(renderGeneratedCodeReportMarkdown(report) + '\n');
|
|
484
|
-
process.stdout.write('\nRe-run with --write-drafts to save the recommended protect rules under sharkcraft/ingestion/.\n');
|
|
485
|
-
return 0;
|
|
486
|
-
}
|
|
487
471
|
if (format === 'json')
|
|
488
472
|
process.stdout.write(renderGeneratedCodeReportJson(report) + '\n');
|
|
489
473
|
else if (format === 'markdown')
|
|
@@ -493,6 +477,48 @@ export const generatedCommand = {
|
|
|
493
477
|
return 0;
|
|
494
478
|
},
|
|
495
479
|
};
|
|
480
|
+
/** Recommend protect rules for the classified generated roots. */
|
|
481
|
+
export const generatedProtectCommand = {
|
|
482
|
+
name: 'protect',
|
|
483
|
+
description: 'Recommend protect rules for the detected generated roots. Read-only unless --write-drafts (writes under sharkcraft/ingestion/).',
|
|
484
|
+
usage: 'shrk [--cwd <dir>] generated protect [--write-drafts]',
|
|
485
|
+
booleanFlags: new Set(['write-drafts']),
|
|
486
|
+
async run(args) {
|
|
487
|
+
const cwd = resolveCwd(args);
|
|
488
|
+
const inspection = await inspectSharkcraft({ cwd });
|
|
489
|
+
const report = buildGeneratedCodeReport({ inspection });
|
|
490
|
+
if (flagBool(args, 'write-drafts')) {
|
|
491
|
+
const outDir = nodePath.join(cwd, INGEST_BASE);
|
|
492
|
+
if (!existsSync(outDir))
|
|
493
|
+
mkdirSync(outDir, { recursive: true });
|
|
494
|
+
const target = nodePath.join(outDir, 'GENERATED_PROTECT.md');
|
|
495
|
+
writeFileSync(target, renderGeneratedCodeReportMarkdown(report), 'utf8');
|
|
496
|
+
process.stdout.write(`Wrote ${target}\n`);
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
process.stdout.write(renderGeneratedCodeReportMarkdown(report) + '\n');
|
|
500
|
+
process.stdout.write('\nRe-run with --write-drafts to save the recommended protect rules under sharkcraft/ingestion/.\n');
|
|
501
|
+
return 0;
|
|
502
|
+
},
|
|
503
|
+
};
|
|
504
|
+
/**
|
|
505
|
+
* The `generated` group. The bare verb keeps its historical behaviour (the
|
|
506
|
+
* classifier report); `report` / `protect` are the classifier subverbs, and
|
|
507
|
+
* `list` / `check` / `update` / `explain` are the drift GATE (registered from
|
|
508
|
+
* `generated.command.ts`). One noun: that half FINDS what is generated, this
|
|
509
|
+
* half proves it has not drifted.
|
|
510
|
+
*/
|
|
511
|
+
export const generatedCommand = {
|
|
512
|
+
name: 'generated',
|
|
513
|
+
description: 'Generated-code classifier (`report` default, `protect --write-drafts`) + the generated-artifact drift GATE (`list | check | update | explain`, see docs/generated-drift.md). Read-only by default.',
|
|
514
|
+
usage: 'shrk [--cwd <dir>] generated [report|protect] [--format text|markdown|json] [--write-drafts]\n (drift gate: shrk generated list | check [--headers-only] | update | explain --id <id>)',
|
|
515
|
+
booleanFlags: new Set(['write-drafts']),
|
|
516
|
+
async run(args) {
|
|
517
|
+
// Bare `shrk generated` (and any stray positional) → the classifier report,
|
|
518
|
+
// exactly as before the gate verbs joined the group.
|
|
519
|
+
return generatedReportCommand.run({ ...args, positional: [] });
|
|
520
|
+
},
|
|
521
|
+
};
|
|
496
522
|
export const stabilityCommand = {
|
|
497
523
|
name: 'stability',
|
|
498
524
|
description: 'Stability classification (stable/experimental/deprecated/legacy/generated/internal/public-api/high-risk). Read-only.',
|
|
@@ -1,3 +1,40 @@
|
|
|
1
|
+
import type { IPolicyRule, PolicySurface } from '@shrkcrft/core';
|
|
2
|
+
import { type IPolicyFinding, type IPolicySuppression } from '@shrkcrft/boundaries';
|
|
1
3
|
import { type ICommandHandler } from '../command-registry.js';
|
|
4
|
+
export declare const POLICY_EXPLAIN_SCHEMA: "sharkcraft.policy-explain/v1";
|
|
5
|
+
/** What ONE policy rule resolved to against the live tree. */
|
|
6
|
+
export interface IPolicyExplain {
|
|
7
|
+
readonly schema: typeof POLICY_EXPLAIN_SCHEMA;
|
|
8
|
+
readonly ruleId: string;
|
|
9
|
+
readonly description?: string;
|
|
10
|
+
readonly surface: PolicySurface;
|
|
11
|
+
readonly severity: 'error' | 'warning';
|
|
12
|
+
readonly pattern: string;
|
|
13
|
+
readonly scan: string;
|
|
14
|
+
/** Content units the rule scanned (files, or inline-template bodies). */
|
|
15
|
+
readonly unitsScanned: number;
|
|
16
|
+
readonly status: string;
|
|
17
|
+
/** Every hit that COUNTED, with file:line. */
|
|
18
|
+
readonly findings: readonly IPolicyFinding[];
|
|
19
|
+
/** Every hit an exemption or the scan zone dropped, and which one applied. */
|
|
20
|
+
readonly suppressed: readonly IPolicySuppression[];
|
|
21
|
+
readonly exemptFiles: readonly string[];
|
|
22
|
+
readonly exemptLines?: string;
|
|
23
|
+
readonly diagnostics: readonly string[];
|
|
24
|
+
/** Why the rule scanned nothing, when it did. */
|
|
25
|
+
readonly skipReason?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Dry-run ONE policy rule and return everything it saw — including the hits an
|
|
29
|
+
* exemption swallowed.
|
|
30
|
+
*
|
|
31
|
+
* Showing suppressed hits is the point: an exemption that silently deletes a
|
|
32
|
+
* finding is indistinguishable from a stale glob, so both the kept and the
|
|
33
|
+
* dropped hits are reported, each labelled with the exemption that applied.
|
|
34
|
+
*/
|
|
35
|
+
export declare function runPolicyExplain(cwd: string, rule: IPolicyRule, excludeDirs: readonly string[]): IPolicyExplain;
|
|
36
|
+
/** Render an {@link IPolicyExplain}. Always returns 0 — explain is informational. */
|
|
37
|
+
export declare function renderPolicyExplain(explain: IPolicyExplain, wantJson: boolean): number;
|
|
38
|
+
export declare const policyLintExplainCommand: ICommandHandler;
|
|
2
39
|
export declare const policyLintCommand: ICommandHandler;
|
|
3
40
|
//# sourceMappingURL=policy-lint.command.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"policy-lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/policy-lint.command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"policy-lint.command.d.ts","sourceRoot":"","sources":["../../src/commands/policy-lint.command.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACjE,OAAO,EAEL,KAAK,cAAc,EAEnB,KAAK,kBAAkB,EACxB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAQhC,eAAO,MAAM,qBAAqB,EAAG,8BAAuC,CAAC;AAE7E,8DAA8D;AAC9D,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,OAAO,qBAAqB,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,yEAAyE;IACzE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,QAAQ,CAAC,QAAQ,EAAE,SAAS,cAAc,EAAE,CAAC;IAC7C,8EAA8E;IAC9E,QAAQ,CAAC,UAAU,EAAE,SAAS,kBAAkB,EAAE,CAAC;IACnD,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,iDAAiD;IACjD,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC9B;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,WAAW,EACjB,WAAW,EAAE,SAAS,MAAM,EAAE,GAC7B,cAAc,CAqBhB;AAED,qFAAqF;AACrF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CA0CtF;AAGD,eAAO,MAAM,wBAAwB,EAAE,eA8BtC,CAAC;AAEF,eAAO,MAAM,iBAAiB,EAAE,eAoQ/B,CAAC"}
|
|
@@ -1,13 +1,117 @@
|
|
|
1
1
|
import * as nodePath from 'node:path';
|
|
2
|
-
import { runPolicyLint } from '@shrkcrft/boundaries';
|
|
2
|
+
import { runPolicyLint, } from '@shrkcrft/boundaries';
|
|
3
3
|
import { classifyChangedScope, resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
|
|
4
4
|
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
5
|
+
import { ExitCode } from "../exit-codes.js";
|
|
6
|
+
import { buildGateEnvelope } from "../gates/gate-envelope.js";
|
|
5
7
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
6
8
|
const VALID_SURFACES = new Set(['template', 'style', 'ts']);
|
|
9
|
+
export const POLICY_EXPLAIN_SCHEMA = 'sharkcraft.policy-explain/v1';
|
|
10
|
+
/**
|
|
11
|
+
* Dry-run ONE policy rule and return everything it saw — including the hits an
|
|
12
|
+
* exemption swallowed.
|
|
13
|
+
*
|
|
14
|
+
* Showing suppressed hits is the point: an exemption that silently deletes a
|
|
15
|
+
* finding is indistinguishable from a stale glob, so both the kept and the
|
|
16
|
+
* dropped hits are reported, each labelled with the exemption that applied.
|
|
17
|
+
*/
|
|
18
|
+
export function runPolicyExplain(cwd, rule, excludeDirs) {
|
|
19
|
+
const report = runPolicyLint(cwd, [rule], { excludeDirs });
|
|
20
|
+
const result = report.rules[0];
|
|
21
|
+
const skip = report.skipped[0];
|
|
22
|
+
return {
|
|
23
|
+
schema: POLICY_EXPLAIN_SCHEMA,
|
|
24
|
+
ruleId: rule.id,
|
|
25
|
+
...(rule.description ? { description: rule.description } : {}),
|
|
26
|
+
surface: rule.surface,
|
|
27
|
+
severity: rule.severity ?? 'error',
|
|
28
|
+
pattern: rule.pattern,
|
|
29
|
+
scan: rule.scan ?? 'all',
|
|
30
|
+
unitsScanned: result?.unitsScanned ?? 0,
|
|
31
|
+
status: result?.status ?? 'error',
|
|
32
|
+
findings: report.findings,
|
|
33
|
+
suppressed: report.suppressed,
|
|
34
|
+
exemptFiles: rule.exemptFiles ?? [],
|
|
35
|
+
...(rule.exemptLines ? { exemptLines: rule.exemptLines } : {}),
|
|
36
|
+
diagnostics: report.diagnostics,
|
|
37
|
+
...(skip ? { skipReason: skip.reason } : {}),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Render an {@link IPolicyExplain}. Always returns 0 — explain is informational. */
|
|
41
|
+
export function renderPolicyExplain(explain, wantJson) {
|
|
42
|
+
if (wantJson) {
|
|
43
|
+
process.stdout.write(asJson(explain) + '\n');
|
|
44
|
+
return 0;
|
|
45
|
+
}
|
|
46
|
+
process.stdout.write(header(`Policy explain: ${explain.ruleId} (${explain.surface})`));
|
|
47
|
+
if (explain.description)
|
|
48
|
+
process.stdout.write(` ${explain.description}\n`);
|
|
49
|
+
process.stdout.write(kv('pattern', `/${explain.pattern}/`) + '\n');
|
|
50
|
+
process.stdout.write(kv('scan zone', explain.scan) + '\n');
|
|
51
|
+
process.stdout.write(kv('units scanned', String(explain.unitsScanned)) + '\n');
|
|
52
|
+
process.stdout.write(kv('status', explain.status) + '\n');
|
|
53
|
+
if (explain.exemptFiles.length > 0) {
|
|
54
|
+
process.stdout.write(kv('exemptFiles', explain.exemptFiles.join(', ')) + '\n');
|
|
55
|
+
}
|
|
56
|
+
if (explain.exemptLines)
|
|
57
|
+
process.stdout.write(kv('exemptLines', explain.exemptLines) + '\n');
|
|
58
|
+
process.stdout.write(`\nHits that COUNT (${explain.findings.length}):\n`);
|
|
59
|
+
for (const f of explain.findings.slice(0, 60)) {
|
|
60
|
+
process.stdout.write(` ✗ ${f.match} (${f.file}:${f.line})${f.inlineTemplate ? ' [inline template]' : ''}\n`);
|
|
61
|
+
}
|
|
62
|
+
if (explain.findings.length > 60) {
|
|
63
|
+
process.stdout.write(` … (${explain.findings.length - 60} more)\n`);
|
|
64
|
+
}
|
|
65
|
+
if (explain.suppressed.length > 0) {
|
|
66
|
+
process.stdout.write(`\nHits an exemption DROPPED (${explain.suppressed.length}):\n`);
|
|
67
|
+
for (const s of explain.suppressed.slice(0, 60)) {
|
|
68
|
+
process.stdout.write(` – ${s.match} (${s.file}:${s.line}) via ${s.via}\n`);
|
|
69
|
+
}
|
|
70
|
+
if (explain.suppressed.length > 60) {
|
|
71
|
+
process.stdout.write(` … (${explain.suppressed.length - 60} more)\n`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (explain.skipReason) {
|
|
75
|
+
process.stdout.write(`\n! SKIPPED — ${explain.skipReason}. A rule that scans nothing is a bug in the rule,\n` +
|
|
76
|
+
' not a pass. Fix the glob, or set `failOnEmpty: true` to make this a hard failure.\n');
|
|
77
|
+
}
|
|
78
|
+
for (const d of explain.diagnostics)
|
|
79
|
+
process.stdout.write(` ! ${d}\n`);
|
|
80
|
+
return 0;
|
|
81
|
+
}
|
|
82
|
+
export const policyLintExplainCommand = {
|
|
83
|
+
name: 'explain',
|
|
84
|
+
description: 'Dry-run ONE policyRule and print every hit with file:line — INCLUDING the hits an exemption or the scan zone dropped, each labelled with which one applied.',
|
|
85
|
+
usage: 'shrk policy-lint explain <ruleId> [--json]',
|
|
86
|
+
booleanFlags: new Set(['json']),
|
|
87
|
+
async run(args) {
|
|
88
|
+
const id = args.positional[0] ?? flagString(args, 'id');
|
|
89
|
+
if (!id) {
|
|
90
|
+
process.stderr.write('Usage: shrk policy-lint explain <ruleId> [--json]\n');
|
|
91
|
+
return ExitCode.UsageError;
|
|
92
|
+
}
|
|
93
|
+
const cwd = resolveCwd(args);
|
|
94
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
95
|
+
if (!loaded.ok) {
|
|
96
|
+
process.stderr.write(`Could not load config: ${loaded.error.message}\n`);
|
|
97
|
+
return ExitCode.UsageError;
|
|
98
|
+
}
|
|
99
|
+
const rules = loaded.value.config.policyRules ?? [];
|
|
100
|
+
const rule = rules.find((r) => r.id === id);
|
|
101
|
+
if (!rule) {
|
|
102
|
+
process.stderr.write(`No policy rule "${id}". Configured: ${rules.map((r) => r.id).join(', ') || '(none)'}\n`);
|
|
103
|
+
return ExitCode.UsageError;
|
|
104
|
+
}
|
|
105
|
+
const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
|
|
106
|
+
const excludeDirs = rel && !rel.startsWith('..') ? [rel] : [];
|
|
107
|
+
return renderPolicyExplain(runPolicyExplain(cwd, rule, excludeDirs), flagBool(args, 'json'));
|
|
108
|
+
},
|
|
109
|
+
};
|
|
7
110
|
export const policyLintCommand = {
|
|
8
111
|
name: 'policy-lint',
|
|
9
112
|
description: 'Lint template/markup, stylesheet, and AOT-invisible TS surfaces against data-defined policyRules[] (e.g. flag raw markup when a primitive exists). Sees `.html` files AND inline `template:` strings — surfaces tsc/AOT cannot. Deterministic; no AI.',
|
|
10
113
|
usage: 'shrk [--cwd <dir>] policy-lint [--surface template|style|ts] [--changed-only] [--new-only] [--since <ref>] [--only <ids>] [--json]\n (--changed-only SCANS just the changed files; --new-only scans the whole tree but shows only findings the change introduced, hiding pre-existing baseline debt)',
|
|
114
|
+
booleanFlags: new Set(['json', 'changed-only', 'new-only']),
|
|
11
115
|
async run(args) {
|
|
12
116
|
const cwd = resolveCwd(args);
|
|
13
117
|
const wantJson = flagBool(args, 'json');
|
|
@@ -25,7 +129,7 @@ export const policyLintCommand = {
|
|
|
25
129
|
const bad = parts.filter((s) => !VALID_SURFACES.has(s));
|
|
26
130
|
if (bad.length > 0) {
|
|
27
131
|
process.stderr.write(`Unknown --surface "${bad.join(', ')}". Use template | style | ts.\n`);
|
|
28
|
-
return
|
|
132
|
+
return ExitCode.UsageError;
|
|
29
133
|
}
|
|
30
134
|
surfaces = parts;
|
|
31
135
|
}
|
|
@@ -35,11 +139,12 @@ export const policyLintCommand = {
|
|
|
35
139
|
const msg = loaded.error.message;
|
|
36
140
|
if (wantJson) {
|
|
37
141
|
process.stdout.write(asJson({ schema: 'sharkcraft.policy-lint/v1', error: msg, rules: [], findings: [], diagnostics: [msg], evaluated: 0, verdict: 'errors' }) + '\n');
|
|
38
|
-
return
|
|
142
|
+
return ExitCode.UsageError;
|
|
39
143
|
}
|
|
40
144
|
process.stdout.write(header('Policy lint'));
|
|
41
145
|
process.stdout.write(` ✗ Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
|
|
42
|
-
|
|
146
|
+
// A broken config is a USAGE error (3), not "violations found" (1).
|
|
147
|
+
return ExitCode.UsageError;
|
|
43
148
|
}
|
|
44
149
|
const rules = loaded.value.config.policyRules ?? [];
|
|
45
150
|
const planeDiagnostics = loaded.value.planeDiagnostics;
|
|
@@ -60,7 +165,7 @@ export const policyLintCommand = {
|
|
|
60
165
|
const unknown = requested.filter((id) => !known.has(id));
|
|
61
166
|
if (unknown.length > 0) {
|
|
62
167
|
process.stderr.write(`Unknown --only rule id(s): ${unknown.join(', ')}. Configured: ${[...known].join(', ') || '(none)'}\n`);
|
|
63
|
-
return
|
|
168
|
+
return ExitCode.UsageError;
|
|
64
169
|
}
|
|
65
170
|
}
|
|
66
171
|
let changedFiles;
|
|
@@ -116,8 +221,41 @@ export const policyLintCommand = {
|
|
|
116
221
|
report = { ...report, findings: newFindings, verdict };
|
|
117
222
|
}
|
|
118
223
|
if (wantJson) {
|
|
119
|
-
|
|
120
|
-
|
|
224
|
+
const exit = report.verdict === 'errors' || report.skipped.some((sk) => sk.failed)
|
|
225
|
+
? ExitCode.Failure
|
|
226
|
+
: report.evaluated === 0 || report.skipped.length > 0
|
|
227
|
+
? ExitCode.NotVerified
|
|
228
|
+
: ExitCode.VerifiedPass;
|
|
229
|
+
process.stdout.write(asJson({
|
|
230
|
+
...report,
|
|
231
|
+
...(newOnly ? { newOnly: true, hiddenBaseline } : {}),
|
|
232
|
+
gate: buildGateEnvelope('policy-lint', exit, report.rules.map((r) => {
|
|
233
|
+
const skip = report.skipped.find((s) => s.ruleId === r.ruleId);
|
|
234
|
+
return {
|
|
235
|
+
id: r.ruleId,
|
|
236
|
+
type: 'policy',
|
|
237
|
+
status: r.status,
|
|
238
|
+
severity: r.severity,
|
|
239
|
+
counts: {
|
|
240
|
+
units: r.unitsScanned,
|
|
241
|
+
findings: r.findingCount,
|
|
242
|
+
suppressed: r.suppressedCount,
|
|
243
|
+
},
|
|
244
|
+
violations: report.findings
|
|
245
|
+
.filter((f) => f.ruleId === r.ruleId)
|
|
246
|
+
.map((f) => ({
|
|
247
|
+
id: f.match,
|
|
248
|
+
file: f.file,
|
|
249
|
+
line: f.line,
|
|
250
|
+
message: f.message,
|
|
251
|
+
...(f.suggest ? { hint: f.suggest } : {}),
|
|
252
|
+
})),
|
|
253
|
+
...(skip ? { skipReason: skip.reason } : {}),
|
|
254
|
+
...(r.error ? { error: r.error } : {}),
|
|
255
|
+
};
|
|
256
|
+
})),
|
|
257
|
+
}) + '\n');
|
|
258
|
+
return exit;
|
|
121
259
|
}
|
|
122
260
|
process.stdout.write(header('Policy lint'));
|
|
123
261
|
// `evaluated` counts rules that actually scanned ≥1 file. When 0 rules
|
|
@@ -126,12 +264,23 @@ export const policyLintCommand = {
|
|
|
126
264
|
if (report.evaluated === 0) {
|
|
127
265
|
process.stdout.write(` ! Nothing evaluated — ${report.rules.length} rule(s) configured but none matched files in scope` +
|
|
128
266
|
(changedOnly || since ? ' (changed-only).\n' : '.\n'));
|
|
129
|
-
|
|
267
|
+
for (const sk of report.skipped) {
|
|
268
|
+
process.stdout.write(` – ${sk.ruleId}: ${sk.reason}${sk.failed ? ' (failOnEmpty → FAILED)' : ''}\n`);
|
|
269
|
+
}
|
|
270
|
+
// Scanning nothing is not a pass. `2` = not verified (the repo-wide
|
|
271
|
+
// contract); a `failOnEmpty` rule promotes it to a real failure.
|
|
272
|
+
return report.skipped.some((sk) => sk.failed) ? 1 : 2;
|
|
130
273
|
}
|
|
131
274
|
process.stdout.write(kv('rules evaluated', `${report.evaluated} of ${report.rules.length}`) + '\n');
|
|
132
275
|
const errors = report.findings.filter((f) => f.severity === 'error').length;
|
|
133
276
|
const warnings = report.findings.filter((f) => f.severity === 'warning').length;
|
|
134
277
|
process.stdout.write(kv('findings', `${errors} error(s), ${warnings} warning(s)`) + '\n');
|
|
278
|
+
if (report.suppressed.length > 0) {
|
|
279
|
+
process.stdout.write(kv('suppressed', `${report.suppressed.length} hit(s) dropped by an exemption — see \`policy-lint explain <id>\``) + '\n');
|
|
280
|
+
}
|
|
281
|
+
for (const sk of report.skipped) {
|
|
282
|
+
process.stdout.write(` ${sk.failed ? '✗' : '–'} ${sk.ruleId} ${sk.failed ? 'FAILED' : 'SKIPPED'} — ${sk.reason}\n`);
|
|
283
|
+
}
|
|
135
284
|
if (newOnly) {
|
|
136
285
|
process.stdout.write(kv('scope', `new-only (${hiddenBaseline} pre-existing finding(s) hidden — run without --new-only to see all)`) + '\n');
|
|
137
286
|
}
|
|
@@ -140,6 +289,16 @@ export const policyLintCommand = {
|
|
|
140
289
|
for (const d of report.diagnostics)
|
|
141
290
|
process.stdout.write(` ! ${d}\n`);
|
|
142
291
|
}
|
|
292
|
+
if (report.skipped.some((sk) => sk.failed)) {
|
|
293
|
+
process.stdout.write('\nA rule that matched nothing is a bug in the rule, not a pass. (`error`-severity\n' +
|
|
294
|
+
' rules fail on empty by default — set `failOnEmpty: false` if the set may be empty.)\n');
|
|
295
|
+
return 1;
|
|
296
|
+
}
|
|
297
|
+
// A non-failing skip still means "partially verified" — never a green 0.
|
|
298
|
+
if (report.skipped.length > 0 && report.findings.length === 0) {
|
|
299
|
+
process.stdout.write(`\n${report.skipped.length} rule(s) scanned nothing — partially verified, not a full green.\n`);
|
|
300
|
+
return 2;
|
|
301
|
+
}
|
|
143
302
|
if (report.findings.length === 0 && report.diagnostics.length === 0) {
|
|
144
303
|
process.stdout.write(newOnly
|
|
145
304
|
? `\nNo NEW policy violations from this change${hiddenBaseline > 0 ? ` (${hiddenBaseline} pre-existing hidden)` : ''}. ✓\n`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AA8BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAQhC,eAAO,MAAM,wBAAwB,EAAE,eA8CtC,CAAC;AAkMF,eAAO,MAAM,eAAe,EAAE,eA0C7B,CAAC"}
|
|
@@ -8,17 +8,20 @@
|
|
|
8
8
|
* [--fail-if-taken] # guard: non-zero when taken (free → 0)
|
|
9
9
|
* [--fail-if-missing] # guard: non-zero when NOT registered
|
|
10
10
|
* shrk registry <name> where <id> [--json] # declaration (+ consumer) sites
|
|
11
|
+
* shrk registry <name> duplicates [--json] # ids declared in more than one place
|
|
11
12
|
*
|
|
12
13
|
* `<name>` resolves a `registries[]` declaration in sharkcraft.config.ts — one
|
|
13
14
|
* deterministic multi-root scan that answers "is this id taken / where is it"
|
|
14
15
|
* without an agent re-running a fragile grep.
|
|
15
16
|
*/
|
|
16
17
|
import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
|
|
17
|
-
import { scanRegistry, registryExists, registryWhere, } from '@shrkcrft/boundaries';
|
|
18
|
+
import { scanRegistry, registryDuplicates, registryExists, registryWhere, } from '@shrkcrft/boundaries';
|
|
18
19
|
import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
|
|
19
20
|
import { ExitCode } from "../exit-codes.js";
|
|
20
21
|
import { asJson } from "../output/format-output.js";
|
|
21
22
|
import { resolveRegistryNoun } from "./registry-resolve.js";
|
|
23
|
+
/** The inventory verbs, used to detect (and forgive) a verb-first invocation. */
|
|
24
|
+
const INVENTORY_VERBS = new Set(['list', 'exists', 'where', 'duplicates']);
|
|
22
25
|
export const registryLifecycleCommand = {
|
|
23
26
|
name: 'lifecycle',
|
|
24
27
|
description: 'Scan the workspace for register/remove symmetry. Read-only.',
|
|
@@ -97,11 +100,20 @@ async function runRegistryInventory(args, name) {
|
|
|
97
100
|
if (!decl) {
|
|
98
101
|
const names = loaded.registries.map((r) => r.name);
|
|
99
102
|
const avail = names.length > 0 ? `Declared registries: ${names.join(', ')}.` : 'No registries declared in sharkcraft.config.ts `registries[]`.';
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
// Typing the verb first is the common stumble (siblings are verb-first).
|
|
104
|
+
// Name the correct grammar instead of only reporting the miss.
|
|
105
|
+
const verbFirst = INVENTORY_VERBS.has(name);
|
|
106
|
+
if (json) {
|
|
107
|
+
process.stdout.write(asJson({ error: `unknown registry "${name}"`, available: names, ...(verbFirst ? { hint: `did you mean 'registry <name> ${name}'?` } : {}) }) + '\n');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
103
110
|
process.stderr.write(`No registry named "${name}". ${avail}\n`);
|
|
104
|
-
|
|
111
|
+
if (verbFirst) {
|
|
112
|
+
const example = names[0] ?? '<name>';
|
|
113
|
+
process.stderr.write(`"${name}" is a verb, not a registry — did you mean \`shrk registry ${example} ${name}\`?\n`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return ExitCode.UsageError;
|
|
105
117
|
}
|
|
106
118
|
const inventory = scanRegistry(cwd, decl);
|
|
107
119
|
if (action === 'list' || action === undefined) {
|
|
@@ -124,13 +136,13 @@ async function runRegistryInventory(args, name) {
|
|
|
124
136
|
if (action === 'exists') {
|
|
125
137
|
if (!id) {
|
|
126
138
|
process.stderr.write(`Usage: shrk registry ${name} exists <id> [--resolve] [--fail-if-taken|--fail-if-missing]\n`);
|
|
127
|
-
return
|
|
139
|
+
return ExitCode.UsageError;
|
|
128
140
|
}
|
|
129
141
|
const failIfTaken = flagBool(args, 'fail-if-taken');
|
|
130
142
|
const failIfMissing = flagBool(args, 'fail-if-missing');
|
|
131
143
|
if (failIfTaken && failIfMissing) {
|
|
132
144
|
process.stderr.write('Pass at most one of --fail-if-taken / --fail-if-missing.\n');
|
|
133
|
-
return
|
|
145
|
+
return ExitCode.UsageError;
|
|
134
146
|
}
|
|
135
147
|
// `--resolve` maps a human noun to the canonical registered id before the
|
|
136
148
|
// existence test — via the registry's declared `aliases` map AND generic
|
|
@@ -166,10 +178,41 @@ async function runRegistryInventory(args, name) {
|
|
|
166
178
|
process.stdout.write(`${exists ? 'yes' : 'no'} — "${canonical}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
|
|
167
179
|
return code;
|
|
168
180
|
}
|
|
181
|
+
if (action === 'duplicates') {
|
|
182
|
+
// Two roots claiming the same id compile fine; whichever registration wins
|
|
183
|
+
// at runtime is an accident of load order. Every site is printed so the
|
|
184
|
+
// duplicate can be resolved, not merely detected.
|
|
185
|
+
const dupes = registryDuplicates(inventory);
|
|
186
|
+
if (json) {
|
|
187
|
+
process.stdout.write(asJson({
|
|
188
|
+
name: inventory.name,
|
|
189
|
+
scanned: inventory.entries.length,
|
|
190
|
+
duplicates: dupes,
|
|
191
|
+
diagnostics: [...inventory.diagnostics, ...loaded.planeDiagnostics],
|
|
192
|
+
}) + '\n');
|
|
193
|
+
return inventory.entries.length === 0 ? 2 : dupes.length > 0 ? 1 : 0;
|
|
194
|
+
}
|
|
195
|
+
if (inventory.entries.length === 0) {
|
|
196
|
+
process.stdout.write(`Registry "${inventory.name}" matched 0 ids — nothing was checked. This is NOT a pass;\n` +
|
|
197
|
+
' the source selector is probably stale (see `shrk gates coverage`).\n');
|
|
198
|
+
return 2;
|
|
199
|
+
}
|
|
200
|
+
if (dupes.length === 0) {
|
|
201
|
+
process.stdout.write(`No duplicate ids in registry "${inventory.name}" (${inventory.entries.length} scanned). ✓\n`);
|
|
202
|
+
return 0;
|
|
203
|
+
}
|
|
204
|
+
process.stdout.write(`Duplicate ids in registry "${inventory.name}" (${dupes.length}):\n`);
|
|
205
|
+
for (const e of dupes) {
|
|
206
|
+
process.stdout.write(` ✗ ${e.id} (${e.sites.length} declarations)\n`);
|
|
207
|
+
for (const s of e.sites)
|
|
208
|
+
process.stdout.write(` ${s.file}:${s.line}\n`);
|
|
209
|
+
}
|
|
210
|
+
return 1;
|
|
211
|
+
}
|
|
169
212
|
if (action === 'where') {
|
|
170
213
|
if (!id) {
|
|
171
214
|
process.stderr.write(`Usage: shrk registry ${name} where <id>\n`);
|
|
172
|
-
return
|
|
215
|
+
return ExitCode.UsageError;
|
|
173
216
|
}
|
|
174
217
|
const entry = registryWhere(inventory, id);
|
|
175
218
|
if (json) {
|
|
@@ -187,13 +230,13 @@ async function runRegistryInventory(args, name) {
|
|
|
187
230
|
process.stdout.write(` consumed ${s.file}:${s.line}\n`);
|
|
188
231
|
return 0;
|
|
189
232
|
}
|
|
190
|
-
process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id
|
|
191
|
-
return
|
|
233
|
+
process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id> | duplicates\n`);
|
|
234
|
+
return ExitCode.UsageError;
|
|
192
235
|
}
|
|
193
236
|
export const registryCommand = {
|
|
194
237
|
name: 'registry',
|
|
195
238
|
description: 'Registry inspections: lifecycle symmetry + declared-registry inventory. Read-only.',
|
|
196
|
-
usage: 'shrk registry lifecycle | <name> list | <name> exists <id> [--resolve] [--fail-if-taken|--fail-if-missing] | <name> where <id>',
|
|
239
|
+
usage: 'shrk registry lifecycle | <name> list | <name> exists <id> [--resolve] [--fail-if-taken|--fail-if-missing] | <name> where <id> | <name> duplicates',
|
|
197
240
|
// Guard-mode + query flags take no value — declare them so `exists <id>
|
|
198
241
|
// --fail-if-taken` (flag last) and `exists --resolve <id>` (flag first) both
|
|
199
242
|
// keep the id as a positional instead of swallowing it.
|
|
@@ -205,13 +248,27 @@ export const registryCommand = {
|
|
|
205
248
|
return registryLifecycleCommand.run(args);
|
|
206
249
|
}
|
|
207
250
|
if (sub !== undefined && sub.length > 0) {
|
|
208
|
-
//
|
|
251
|
+
// Grammar is `registry <name> <verb>`, but `baseline`/`generated` read
|
|
252
|
+
// verb-first, so `registry list <name>` is the instinctive form. Accept
|
|
253
|
+
// BOTH: when arg1 is a known verb and arg2 names a declared registry,
|
|
254
|
+
// swap them. The canonical order is unchanged; the stumble is removed.
|
|
255
|
+
const second = args.positional[1];
|
|
256
|
+
if (INVENTORY_VERBS.has(sub) && second !== undefined && second.length > 0) {
|
|
257
|
+
const loaded = await loadRegistries(resolveCwd(args));
|
|
258
|
+
const known = loaded.ok && findRegistry(loaded.registries, second) !== undefined;
|
|
259
|
+
if (known) {
|
|
260
|
+
// [verb, name, ...rest] → [name, verb, ...rest]
|
|
261
|
+
args.positional = [second, sub, ...args.positional.slice(2)];
|
|
262
|
+
return runRegistryInventory(args, second);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
// `<name> list | exists <id> | where <id> | duplicates` — sub is the name.
|
|
209
266
|
return runRegistryInventory(args, sub);
|
|
210
267
|
}
|
|
211
268
|
const cwd = resolveCwd(args);
|
|
212
269
|
const loaded = await loadRegistries(cwd);
|
|
213
270
|
const names = loaded.ok ? loaded.registries.map((r) => r.name) : [];
|
|
214
|
-
process.stderr.write('Usage: shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id
|
|
271
|
+
process.stderr.write('Usage: shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id> | <name> duplicates\n' +
|
|
215
272
|
(names.length > 0 ? `Declared registries: ${names.join(', ')}.\n` : 'No registries declared (sharkcraft.config.ts `registries[]`).\n'));
|
|
216
273
|
return 2;
|
|
217
274
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wiring.command.d.ts","sourceRoot":"","sources":["../../src/commands/wiring.command.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAMhC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"wiring.command.d.ts","sourceRoot":"","sources":["../../src/commands/wiring.command.ts"],"names":[],"mappings":"AAAA,OAAO,EASL,KAAK,cAAc,EACpB,MAAM,sBAAsB,CAAC;AAM9B,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAMhC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,CAoFrF;AAyZD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}
|
|
@@ -24,6 +24,10 @@ export function renderWiringExplain(report, wantJson) {
|
|
|
24
24
|
process.stdout.write(` ${report.description}\n`);
|
|
25
25
|
if (report.groupBy)
|
|
26
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');
|
|
27
31
|
process.stdout.write(kv('declared', `${report.declared.distinctCount} distinct across ${report.declared.filesScanned} file(s)`) +
|
|
28
32
|
'\n');
|
|
29
33
|
process.stdout.write(kv('registered', `${report.registered.distinctCount} distinct across ${report.registered.filesScanned} file(s)`) + '\n');
|
|
@@ -52,6 +56,27 @@ export function renderWiringExplain(report, wantJson) {
|
|
|
52
56
|
process.stdout.write(` … (${report.registeredNotDeclared.length - SITE_DISPLAY_CAP} more)\n`);
|
|
53
57
|
}
|
|
54
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
|
+
}
|
|
55
80
|
for (const d of report.diagnostics)
|
|
56
81
|
process.stdout.write(` ! ${d}\n`);
|
|
57
82
|
process.stdout.write(`\nVerdict: ${report.verdict}\n`);
|
package/dist/exit-codes.d.ts
CHANGED
|
@@ -12,12 +12,19 @@
|
|
|
12
12
|
* 0 VerifiedPass — checks ran over a NON-EMPTY scope and passed. Never
|
|
13
13
|
* returned when zero units were evaluated.
|
|
14
14
|
* 1 Failure — checks ran and found violations.
|
|
15
|
-
* 2 NotVerified — indeterminate: empty evaluation scope,
|
|
16
|
-
* fallback, short-circuit, timeout, or
|
|
17
|
-
* Distinct from both pass and fail so a
|
|
18
|
-
* on it (`|| handle-indeterminate`).
|
|
19
|
-
*
|
|
20
|
-
*
|
|
15
|
+
* 2 NotVerified — indeterminate: empty evaluation scope, all rules
|
|
16
|
+
* skipped, degraded fallback, short-circuit, timeout, or
|
|
17
|
+
* "refused to run". Distinct from both pass and fail so a
|
|
18
|
+
* chain can branch on it (`|| handle-indeterminate`).
|
|
19
|
+
* 3 UsageError — the request itself was malformed: unloadable config, an
|
|
20
|
+
* unknown rule id, a bad flag value. Split out of `2` on
|
|
21
|
+
* the GATE verbs because the two demand different
|
|
22
|
+
* responses: `2` means "the gate ran but proved nothing"
|
|
23
|
+
* (investigate the rules), `3` means "the gate never
|
|
24
|
+
* started" (fix the invocation or the config). Non-gate
|
|
25
|
+
* verbs keep returning `2` for usage errors — widening the
|
|
26
|
+
* split across the whole CLI would churn a documented
|
|
27
|
+
* contract far beyond what it buys.
|
|
21
28
|
*
|
|
22
29
|
* The `gen --typecheck` pre-write gate already refuses-to-nonzero rather than
|
|
23
30
|
* emit an unverified artifact; this generalizes that instinct across the gate
|
|
@@ -27,7 +34,8 @@
|
|
|
27
34
|
export declare enum ExitCode {
|
|
28
35
|
VerifiedPass = 0,
|
|
29
36
|
Failure = 1,
|
|
30
|
-
NotVerified = 2
|
|
37
|
+
NotVerified = 2,
|
|
38
|
+
UsageError = 3
|
|
31
39
|
}
|
|
32
40
|
/**
|
|
33
41
|
* Promote a NotVerified (`2`) exit into a Failure-class nonzero (`1`) when the
|
|
@@ -61,12 +69,24 @@ export declare function argvHasExitTrailer(argv: readonly string[]): boolean;
|
|
|
61
69
|
* so `check boundaries --json` (2 tokens) and a bare `finish` (1) both resolve.
|
|
62
70
|
*/
|
|
63
71
|
export declare function isGateVerb(commandPath: string): boolean;
|
|
72
|
+
/**
|
|
73
|
+
* True when the argv carries the global `--no-hints` (before the `--`
|
|
74
|
+
* sentinel). Suppresses advisory one-liners like the piped-exit note while
|
|
75
|
+
* leaving every real diagnostic — and the `--exit-trailer` machine channel —
|
|
76
|
+
* untouched. For a caller that has already internalised the warning and just
|
|
77
|
+
* wants clean stderr in captured logs.
|
|
78
|
+
*/
|
|
79
|
+
export declare function argvHasNoHints(argv: readonly string[]): boolean;
|
|
80
|
+
/** Reset the one-time hint latch. Test-only seam. */
|
|
81
|
+
export declare function resetPipeHintLatch(): void;
|
|
64
82
|
/** Injectable surface for {@link emitPipeExitSignal} (isTTY + writer + trailer). */
|
|
65
83
|
export interface IPipeExitOptions {
|
|
66
84
|
/** True when shrk's stdout is NOT a terminal (i.e. piped/redirected). */
|
|
67
85
|
readonly piped: boolean;
|
|
68
86
|
/** True when `--exit-trailer` was requested. */
|
|
69
87
|
readonly trailer: boolean;
|
|
88
|
+
/** True when `--no-hints` was requested — suppress the advisory note only. */
|
|
89
|
+
readonly noHints?: boolean;
|
|
70
90
|
/** stderr writer; defaults to `process.stderr.write`. Overridable for tests. */
|
|
71
91
|
readonly write?: (s: string) => void;
|
|
72
92
|
}
|
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
|
|
1
|
+
{"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,oBAAY,QAAQ;IAClB,YAAY,IAAI;IAChB,OAAO,IAAI;IACX,WAAW,IAAI;IACf,UAAU,IAAI;CACf;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;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM/D;AASD,qDAAqD;AACrD,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC;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,8EAA8E;IAC9E,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;IAC3B,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,CAgBN"}
|