@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
|
@@ -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;AAMhC,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,eAmN/B,CAAC"}
|
|
@@ -1,13 +1,115 @@
|
|
|
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
5
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
6
6
|
const VALID_SURFACES = new Set(['template', 'style', 'ts']);
|
|
7
|
+
export const POLICY_EXPLAIN_SCHEMA = 'sharkcraft.policy-explain/v1';
|
|
8
|
+
/**
|
|
9
|
+
* Dry-run ONE policy rule and return everything it saw — including the hits an
|
|
10
|
+
* exemption swallowed.
|
|
11
|
+
*
|
|
12
|
+
* Showing suppressed hits is the point: an exemption that silently deletes a
|
|
13
|
+
* finding is indistinguishable from a stale glob, so both the kept and the
|
|
14
|
+
* dropped hits are reported, each labelled with the exemption that applied.
|
|
15
|
+
*/
|
|
16
|
+
export function runPolicyExplain(cwd, rule, excludeDirs) {
|
|
17
|
+
const report = runPolicyLint(cwd, [rule], { excludeDirs });
|
|
18
|
+
const result = report.rules[0];
|
|
19
|
+
const skip = report.skipped[0];
|
|
20
|
+
return {
|
|
21
|
+
schema: POLICY_EXPLAIN_SCHEMA,
|
|
22
|
+
ruleId: rule.id,
|
|
23
|
+
...(rule.description ? { description: rule.description } : {}),
|
|
24
|
+
surface: rule.surface,
|
|
25
|
+
severity: rule.severity ?? 'error',
|
|
26
|
+
pattern: rule.pattern,
|
|
27
|
+
scan: rule.scan ?? 'all',
|
|
28
|
+
unitsScanned: result?.unitsScanned ?? 0,
|
|
29
|
+
status: result?.status ?? 'error',
|
|
30
|
+
findings: report.findings,
|
|
31
|
+
suppressed: report.suppressed,
|
|
32
|
+
exemptFiles: rule.exemptFiles ?? [],
|
|
33
|
+
...(rule.exemptLines ? { exemptLines: rule.exemptLines } : {}),
|
|
34
|
+
diagnostics: report.diagnostics,
|
|
35
|
+
...(skip ? { skipReason: skip.reason } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
/** Render an {@link IPolicyExplain}. Always returns 0 — explain is informational. */
|
|
39
|
+
export function renderPolicyExplain(explain, wantJson) {
|
|
40
|
+
if (wantJson) {
|
|
41
|
+
process.stdout.write(asJson(explain) + '\n');
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
process.stdout.write(header(`Policy explain: ${explain.ruleId} (${explain.surface})`));
|
|
45
|
+
if (explain.description)
|
|
46
|
+
process.stdout.write(` ${explain.description}\n`);
|
|
47
|
+
process.stdout.write(kv('pattern', `/${explain.pattern}/`) + '\n');
|
|
48
|
+
process.stdout.write(kv('scan zone', explain.scan) + '\n');
|
|
49
|
+
process.stdout.write(kv('units scanned', String(explain.unitsScanned)) + '\n');
|
|
50
|
+
process.stdout.write(kv('status', explain.status) + '\n');
|
|
51
|
+
if (explain.exemptFiles.length > 0) {
|
|
52
|
+
process.stdout.write(kv('exemptFiles', explain.exemptFiles.join(', ')) + '\n');
|
|
53
|
+
}
|
|
54
|
+
if (explain.exemptLines)
|
|
55
|
+
process.stdout.write(kv('exemptLines', explain.exemptLines) + '\n');
|
|
56
|
+
process.stdout.write(`\nHits that COUNT (${explain.findings.length}):\n`);
|
|
57
|
+
for (const f of explain.findings.slice(0, 60)) {
|
|
58
|
+
process.stdout.write(` ✗ ${f.match} (${f.file}:${f.line})${f.inlineTemplate ? ' [inline template]' : ''}\n`);
|
|
59
|
+
}
|
|
60
|
+
if (explain.findings.length > 60) {
|
|
61
|
+
process.stdout.write(` … (${explain.findings.length - 60} more)\n`);
|
|
62
|
+
}
|
|
63
|
+
if (explain.suppressed.length > 0) {
|
|
64
|
+
process.stdout.write(`\nHits an exemption DROPPED (${explain.suppressed.length}):\n`);
|
|
65
|
+
for (const s of explain.suppressed.slice(0, 60)) {
|
|
66
|
+
process.stdout.write(` – ${s.match} (${s.file}:${s.line}) via ${s.via}\n`);
|
|
67
|
+
}
|
|
68
|
+
if (explain.suppressed.length > 60) {
|
|
69
|
+
process.stdout.write(` … (${explain.suppressed.length - 60} more)\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (explain.skipReason) {
|
|
73
|
+
process.stdout.write(`\n! SKIPPED — ${explain.skipReason}. A rule that scans nothing is a bug in the rule,\n` +
|
|
74
|
+
' not a pass. Fix the glob, or set `failOnEmpty: true` to make this a hard failure.\n');
|
|
75
|
+
}
|
|
76
|
+
for (const d of explain.diagnostics)
|
|
77
|
+
process.stdout.write(` ! ${d}\n`);
|
|
78
|
+
return 0;
|
|
79
|
+
}
|
|
80
|
+
export const policyLintExplainCommand = {
|
|
81
|
+
name: 'explain',
|
|
82
|
+
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.',
|
|
83
|
+
usage: 'shrk policy-lint explain <ruleId> [--json]',
|
|
84
|
+
booleanFlags: new Set(['json']),
|
|
85
|
+
async run(args) {
|
|
86
|
+
const id = args.positional[0] ?? flagString(args, 'id');
|
|
87
|
+
if (!id) {
|
|
88
|
+
process.stderr.write('Usage: shrk policy-lint explain <ruleId> [--json]\n');
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
const cwd = resolveCwd(args);
|
|
92
|
+
const loaded = await resolveProjectConfig(cwd);
|
|
93
|
+
if (!loaded.ok) {
|
|
94
|
+
process.stderr.write(`Could not load config: ${loaded.error.message}\n`);
|
|
95
|
+
return 2;
|
|
96
|
+
}
|
|
97
|
+
const rules = loaded.value.config.policyRules ?? [];
|
|
98
|
+
const rule = rules.find((r) => r.id === id);
|
|
99
|
+
if (!rule) {
|
|
100
|
+
process.stderr.write(`No policy rule "${id}". Configured: ${rules.map((r) => r.id).join(', ') || '(none)'}\n`);
|
|
101
|
+
return 2;
|
|
102
|
+
}
|
|
103
|
+
const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
|
|
104
|
+
const excludeDirs = rel && !rel.startsWith('..') ? [rel] : [];
|
|
105
|
+
return renderPolicyExplain(runPolicyExplain(cwd, rule, excludeDirs), flagBool(args, 'json'));
|
|
106
|
+
},
|
|
107
|
+
};
|
|
7
108
|
export const policyLintCommand = {
|
|
8
109
|
name: 'policy-lint',
|
|
9
110
|
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
111
|
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)',
|
|
112
|
+
booleanFlags: new Set(['json', 'changed-only', 'new-only']),
|
|
11
113
|
async run(args) {
|
|
12
114
|
const cwd = resolveCwd(args);
|
|
13
115
|
const wantJson = flagBool(args, 'json');
|
|
@@ -126,12 +228,23 @@ export const policyLintCommand = {
|
|
|
126
228
|
if (report.evaluated === 0) {
|
|
127
229
|
process.stdout.write(` ! Nothing evaluated — ${report.rules.length} rule(s) configured but none matched files in scope` +
|
|
128
230
|
(changedOnly || since ? ' (changed-only).\n' : '.\n'));
|
|
129
|
-
|
|
231
|
+
for (const sk of report.skipped) {
|
|
232
|
+
process.stdout.write(` – ${sk.ruleId}: ${sk.reason}${sk.failed ? ' (failOnEmpty → FAILED)' : ''}\n`);
|
|
233
|
+
}
|
|
234
|
+
// Scanning nothing is not a pass. `2` = not verified (the repo-wide
|
|
235
|
+
// contract); a `failOnEmpty` rule promotes it to a real failure.
|
|
236
|
+
return report.skipped.some((sk) => sk.failed) ? 1 : 2;
|
|
130
237
|
}
|
|
131
238
|
process.stdout.write(kv('rules evaluated', `${report.evaluated} of ${report.rules.length}`) + '\n');
|
|
132
239
|
const errors = report.findings.filter((f) => f.severity === 'error').length;
|
|
133
240
|
const warnings = report.findings.filter((f) => f.severity === 'warning').length;
|
|
134
241
|
process.stdout.write(kv('findings', `${errors} error(s), ${warnings} warning(s)`) + '\n');
|
|
242
|
+
if (report.suppressed.length > 0) {
|
|
243
|
+
process.stdout.write(kv('suppressed', `${report.suppressed.length} hit(s) dropped by an exemption — see \`policy-lint explain <id>\``) + '\n');
|
|
244
|
+
}
|
|
245
|
+
for (const sk of report.skipped) {
|
|
246
|
+
process.stdout.write(` ${sk.failed ? '✗' : '–'} ${sk.ruleId} ${sk.failed ? 'FAILED' : 'SKIPPED'} — ${sk.reason}\n`);
|
|
247
|
+
}
|
|
135
248
|
if (newOnly) {
|
|
136
249
|
process.stdout.write(kv('scope', `new-only (${hiddenBaseline} pre-existing finding(s) hidden — run without --new-only to see all)`) + '\n');
|
|
137
250
|
}
|
|
@@ -140,6 +253,10 @@ export const policyLintCommand = {
|
|
|
140
253
|
for (const d of report.diagnostics)
|
|
141
254
|
process.stdout.write(` ! ${d}\n`);
|
|
142
255
|
}
|
|
256
|
+
if (report.skipped.some((sk) => sk.failed)) {
|
|
257
|
+
process.stdout.write('\nA rule with `failOnEmpty: true` matched nothing — that is a bug in the rule, not a pass.\n');
|
|
258
|
+
return 1;
|
|
259
|
+
}
|
|
143
260
|
if (report.findings.length === 0 && report.diagnostics.length === 0) {
|
|
144
261
|
process.stdout.write(newOnly
|
|
145
262
|
? `\nNo NEW policy violations from this change${hiddenBaseline > 0 ? ` (${hiddenBaseline} pre-existing hidden)` : ''}. ✓\n`
|
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
* 3. singular/plural normalization (`commands` ↔ `command`),
|
|
13
13
|
* 4. suffix strip/append (`button` ↔ `button-command`, `foo` ↔ `foo.tool`).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Layers 3 and 4 CHAIN: the singular/plural variants and the suffix transform
|
|
16
|
+
* compose, so a doubly-off noun still lands (`buttons` →(plural)→ `button`
|
|
17
|
+
* →(suffix)→ `button-command`) where a single layer would no-op. When both
|
|
18
|
+
* transforms fire, the resolution reports `SingularPluralSuffix`.
|
|
19
|
+
*
|
|
20
|
+
* The first candidate that lands on a DECLARED id wins; when nothing resolves,
|
|
21
|
+
* the noun is returned unchanged and unmatched (the honest "genuinely not
|
|
22
|
+
* declared" answer). No layer invents an id that isn't in the registry.
|
|
18
23
|
*/
|
|
19
24
|
/** How a noun resolved to its canonical registered id (for a truthful report). */
|
|
20
25
|
export declare enum ERegistryResolveVia {
|
|
@@ -22,7 +27,9 @@ export declare enum ERegistryResolveVia {
|
|
|
22
27
|
Alias = "alias",
|
|
23
28
|
Case = "case-fold",
|
|
24
29
|
SingularPlural = "singular/plural",
|
|
25
|
-
Suffix = "suffix"
|
|
30
|
+
Suffix = "suffix",
|
|
31
|
+
/** Both a singular/plural AND a suffix transform were applied (chained). */
|
|
32
|
+
SingularPluralSuffix = "singular/plural+suffix"
|
|
26
33
|
}
|
|
27
34
|
export interface IRegistryResolution {
|
|
28
35
|
/** The canonical id to test for existence (may equal the input noun). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"registry-resolve.d.ts","sourceRoot":"","sources":["../../src/commands/registry-resolve.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"registry-resolve.d.ts","sourceRoot":"","sources":["../../src/commands/registry-resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,kFAAkF;AAClF,oBAAY,mBAAmB;IAC7B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,IAAI,cAAc;IAClB,cAAc,oBAAoB;IAClC,MAAM,WAAW;IACjB,4EAA4E;IAC5E,oBAAoB,2BAA2B;CAChD;AAED,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC;CACnC;AAYD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,EACrD,IAAI,EAAE,MAAM,GACX,mBAAmB,CAwErB"}
|
|
@@ -12,9 +12,14 @@
|
|
|
12
12
|
* 3. singular/plural normalization (`commands` ↔ `command`),
|
|
13
13
|
* 4. suffix strip/append (`button` ↔ `button-command`, `foo` ↔ `foo.tool`).
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* Layers 3 and 4 CHAIN: the singular/plural variants and the suffix transform
|
|
16
|
+
* compose, so a doubly-off noun still lands (`buttons` →(plural)→ `button`
|
|
17
|
+
* →(suffix)→ `button-command`) where a single layer would no-op. When both
|
|
18
|
+
* transforms fire, the resolution reports `SingularPluralSuffix`.
|
|
19
|
+
*
|
|
20
|
+
* The first candidate that lands on a DECLARED id wins; when nothing resolves,
|
|
21
|
+
* the noun is returned unchanged and unmatched (the honest "genuinely not
|
|
22
|
+
* declared" answer). No layer invents an id that isn't in the registry.
|
|
18
23
|
*/
|
|
19
24
|
/** How a noun resolved to its canonical registered id (for a truthful report). */
|
|
20
25
|
export var ERegistryResolveVia;
|
|
@@ -24,6 +29,8 @@ export var ERegistryResolveVia;
|
|
|
24
29
|
ERegistryResolveVia["Case"] = "case-fold";
|
|
25
30
|
ERegistryResolveVia["SingularPlural"] = "singular/plural";
|
|
26
31
|
ERegistryResolveVia["Suffix"] = "suffix";
|
|
32
|
+
/** Both a singular/plural AND a suffix transform were applied (chained). */
|
|
33
|
+
ERegistryResolveVia["SingularPluralSuffix"] = "singular/plural+suffix";
|
|
27
34
|
})(ERegistryResolveVia || (ERegistryResolveVia = {}));
|
|
28
35
|
function singularPluralVariants(noun) {
|
|
29
36
|
const out = [];
|
|
@@ -60,28 +67,47 @@ export function resolveRegistryNoun(declaredIds, aliases, noun) {
|
|
|
60
67
|
if (ciHit !== undefined) {
|
|
61
68
|
return { canonical: ciHit, matched: true, via: ERegistryResolveVia.Case };
|
|
62
69
|
}
|
|
63
|
-
// 3.
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
70
|
+
// 3 + 4. Chained singular/plural × suffix strip/append.
|
|
71
|
+
//
|
|
72
|
+
// Build an ordered candidate list — the noun itself first, then its
|
|
73
|
+
// singular/plural variants (plural variants BEFORE the suffix test) — and for
|
|
74
|
+
// EACH candidate test an exact (case-insensitive) match against the declared
|
|
75
|
+
// ids, then a suffix strip, then a suffix append. The first candidate that
|
|
76
|
+
// lands on a DECLARED id wins. Because the transforms compose, a doubly-off
|
|
77
|
+
// noun (`buttons` →(plural)→ `button` →(suffix)→ `button-command`) resolves
|
|
78
|
+
// where either layer alone would no-op. `via` records which transforms fired:
|
|
79
|
+
// a variant matching exactly is `SingularPlural`, the original noun matching
|
|
80
|
+
// by suffix is `Suffix`, and a variant matching by suffix is the chained
|
|
81
|
+
// `SingularPluralSuffix`. The original noun can never match here EXACTLY (the
|
|
82
|
+
// identity / case-fold short-circuits above already handled that).
|
|
83
|
+
const candidates = [noun, ...singularPluralVariants(noun)];
|
|
84
|
+
for (const candidate of candidates) {
|
|
85
|
+
const isVariant = candidate !== noun;
|
|
86
|
+
const cLower = candidate.toLowerCase();
|
|
87
|
+
const exact = declaredIds.find((d) => d.toLowerCase() === cLower);
|
|
88
|
+
if (exact !== undefined && isVariant) {
|
|
89
|
+
return { canonical: exact, matched: true, via: ERegistryResolveVia.SingularPlural };
|
|
69
90
|
}
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
const suffixStripped = declaredIds.find((d) => {
|
|
92
|
+
const stripped = d.replace(/[-_.][a-z0-9]+$/i, '');
|
|
93
|
+
return stripped.toLowerCase() === cLower && stripped.toLowerCase() !== d.toLowerCase();
|
|
94
|
+
});
|
|
95
|
+
if (suffixStripped !== undefined) {
|
|
96
|
+
return {
|
|
97
|
+
canonical: suffixStripped,
|
|
98
|
+
matched: true,
|
|
99
|
+
via: isVariant ? ERegistryResolveVia.SingularPluralSuffix : ERegistryResolveVia.Suffix,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
for (const d of declaredIds) {
|
|
103
|
+
const m = d.match(/([-_.][a-z0-9]+)$/i);
|
|
104
|
+
if (m && `${cLower}${m[1].toLowerCase()}` === d.toLowerCase()) {
|
|
105
|
+
return {
|
|
106
|
+
canonical: d,
|
|
107
|
+
matched: true,
|
|
108
|
+
via: isVariant ? ERegistryResolveVia.SingularPluralSuffix : ERegistryResolveVia.Suffix,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
85
111
|
}
|
|
86
112
|
}
|
|
87
113
|
// Nothing resolved — the honest unmatched identity.
|
|
@@ -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;AAKhC,eAAO,MAAM,wBAAwB,EAAE,eA8CtC,CAAC;AAoLF,eAAO,MAAM,eAAe,EAAE,eA4B7B,CAAC"}
|
|
@@ -8,13 +8,14 @@
|
|
|
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";
|
|
@@ -166,6 +167,37 @@ async function runRegistryInventory(args, name) {
|
|
|
166
167
|
process.stdout.write(`${exists ? 'yes' : 'no'} — "${canonical}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
|
|
167
168
|
return code;
|
|
168
169
|
}
|
|
170
|
+
if (action === 'duplicates') {
|
|
171
|
+
// Two roots claiming the same id compile fine; whichever registration wins
|
|
172
|
+
// at runtime is an accident of load order. Every site is printed so the
|
|
173
|
+
// duplicate can be resolved, not merely detected.
|
|
174
|
+
const dupes = registryDuplicates(inventory);
|
|
175
|
+
if (json) {
|
|
176
|
+
process.stdout.write(asJson({
|
|
177
|
+
name: inventory.name,
|
|
178
|
+
scanned: inventory.entries.length,
|
|
179
|
+
duplicates: dupes,
|
|
180
|
+
diagnostics: [...inventory.diagnostics, ...loaded.planeDiagnostics],
|
|
181
|
+
}) + '\n');
|
|
182
|
+
return inventory.entries.length === 0 ? 2 : dupes.length > 0 ? 1 : 0;
|
|
183
|
+
}
|
|
184
|
+
if (inventory.entries.length === 0) {
|
|
185
|
+
process.stdout.write(`Registry "${inventory.name}" matched 0 ids — nothing was checked. This is NOT a pass;\n` +
|
|
186
|
+
' the source selector is probably stale (see `shrk gates coverage`).\n');
|
|
187
|
+
return 2;
|
|
188
|
+
}
|
|
189
|
+
if (dupes.length === 0) {
|
|
190
|
+
process.stdout.write(`No duplicate ids in registry "${inventory.name}" (${inventory.entries.length} scanned). ✓\n`);
|
|
191
|
+
return 0;
|
|
192
|
+
}
|
|
193
|
+
process.stdout.write(`Duplicate ids in registry "${inventory.name}" (${dupes.length}):\n`);
|
|
194
|
+
for (const e of dupes) {
|
|
195
|
+
process.stdout.write(` ✗ ${e.id} (${e.sites.length} declarations)\n`);
|
|
196
|
+
for (const s of e.sites)
|
|
197
|
+
process.stdout.write(` ${s.file}:${s.line}\n`);
|
|
198
|
+
}
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
169
201
|
if (action === 'where') {
|
|
170
202
|
if (!id) {
|
|
171
203
|
process.stderr.write(`Usage: shrk registry ${name} where <id>\n`);
|
|
@@ -187,13 +219,13 @@ async function runRegistryInventory(args, name) {
|
|
|
187
219
|
process.stdout.write(` consumed ${s.file}:${s.line}\n`);
|
|
188
220
|
return 0;
|
|
189
221
|
}
|
|
190
|
-
process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id
|
|
222
|
+
process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id> | duplicates\n`);
|
|
191
223
|
return 2;
|
|
192
224
|
}
|
|
193
225
|
export const registryCommand = {
|
|
194
226
|
name: 'registry',
|
|
195
227
|
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>',
|
|
228
|
+
usage: 'shrk registry lifecycle | <name> list | <name> exists <id> [--resolve] [--fail-if-taken|--fail-if-missing] | <name> where <id> | <name> duplicates',
|
|
197
229
|
// Guard-mode + query flags take no value — declare them so `exists <id>
|
|
198
230
|
// --fail-if-taken` (flag last) and `exists --resolve <id>` (flag first) both
|
|
199
231
|
// keep the id as a positional instead of swallowing it.
|
|
@@ -211,7 +243,7 @@ export const registryCommand = {
|
|
|
211
243
|
const cwd = resolveCwd(args);
|
|
212
244
|
const loaded = await loadRegistries(cwd);
|
|
213
245
|
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
|
|
246
|
+
process.stderr.write('Usage: shrk registry lifecycle | <name> list | <name> exists <id> | <name> where <id> | <name> duplicates\n' +
|
|
215
247
|
(names.length > 0 ? `Declared registries: ${names.join(', ')}.\n` : 'No registries declared (sharkcraft.config.ts `registries[]`).\n'));
|
|
216
248
|
return 2;
|
|
217
249
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"trace.command.d.ts","sourceRoot":"","sources":["../../src/commands/trace.command.ts"],"names":[],"mappings":"AAmBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"trace.command.d.ts","sourceRoot":"","sources":["../../src/commands/trace.command.ts"],"names":[],"mappings":"AAmBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA8EhC,eAAO,MAAM,YAAY,EAAE,eA+H1B,CAAC"}
|
|
@@ -17,6 +17,7 @@ const TRACE_ROLE_ORDER = [
|
|
|
17
17
|
TraceRole.Declare,
|
|
18
18
|
TraceRole.Register,
|
|
19
19
|
TraceRole.Consume,
|
|
20
|
+
TraceRole.Render,
|
|
20
21
|
TraceRole.Reference,
|
|
21
22
|
];
|
|
22
23
|
function renderTraceLiteral(report, limit) {
|
|
@@ -31,7 +32,7 @@ function renderTraceLiteral(report, limit) {
|
|
|
31
32
|
}
|
|
32
33
|
// Mirror `shrk registry <name> where`'s `<role> file:line` line idiom so the
|
|
33
34
|
// two surfaces read the same — `trace literal` is the same scanner + classifier
|
|
34
|
-
// without a pre-declared registry, just with
|
|
35
|
+
// without a pre-declared registry, just with extra roles (`register`, `render`,
|
|
35
36
|
// `reference`). The raw source line stays in `--json` (`text`); the human view
|
|
36
37
|
// is classification-first (direction), not a grep-style text dump.
|
|
37
38
|
process.stdout.write('\n');
|
|
@@ -154,6 +155,11 @@ export const traceCommand = {
|
|
|
154
155
|
process.stdout.write(header(`Trace: ${query}`));
|
|
155
156
|
if (!resolution.bestMatch) {
|
|
156
157
|
process.stdout.write(' no matches found.\n');
|
|
158
|
+
// The bare-`trace` path resolves a FUZZY registry query, not an exact
|
|
159
|
+
// string literal — quotes are stripped by the shell, so we can't tell a
|
|
160
|
+
// literal from a query and must not auto-route. Point the agent at the
|
|
161
|
+
// exact-literal tracer (`trace literal`) so a no-match here isn't a dead end.
|
|
162
|
+
process.stderr.write(`hint: to trace an exact string literal across files, use: shrk trace literal "${query}"\n`);
|
|
157
163
|
return 1;
|
|
158
164
|
}
|
|
159
165
|
process.stdout.write(`Confidence: ${resolution.confidence}\n`);
|
|
@@ -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,
|
|
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"}
|