@shrkcrft/cli 0.1.0-alpha.27 → 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.
Files changed (36) hide show
  1. package/dist/commands/baseline.command.d.ts +8 -0
  2. package/dist/commands/baseline.command.d.ts.map +1 -0
  3. package/dist/commands/baseline.command.js +511 -0
  4. package/dist/commands/changelog-data.d.ts.map +1 -1
  5. package/dist/commands/changelog-data.js +22 -0
  6. package/dist/commands/check.command.d.ts.map +1 -1
  7. package/dist/commands/check.command.js +28 -1
  8. package/dist/commands/command-catalog.d.ts.map +1 -1
  9. package/dist/commands/command-catalog.js +112 -0
  10. package/dist/commands/gates.command.d.ts +6 -0
  11. package/dist/commands/gates.command.d.ts.map +1 -0
  12. package/dist/commands/gates.command.js +334 -0
  13. package/dist/commands/generated.command.d.ts +6 -0
  14. package/dist/commands/generated.command.d.ts.map +1 -0
  15. package/dist/commands/generated.command.js +514 -0
  16. package/dist/commands/ingest.command.d.ts +11 -0
  17. package/dist/commands/ingest.command.d.ts.map +1 -1
  18. package/dist/commands/ingest.command.js +49 -23
  19. package/dist/commands/policy-lint.command.d.ts +37 -0
  20. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  21. package/dist/commands/policy-lint.command.js +119 -2
  22. package/dist/commands/registry.command.d.ts.map +1 -1
  23. package/dist/commands/registry.command.js +36 -4
  24. package/dist/commands/wiring.command.d.ts.map +1 -1
  25. package/dist/commands/wiring.command.js +25 -0
  26. package/dist/finish/run-finish.d.ts.map +1 -1
  27. package/dist/finish/run-finish.js +9 -3
  28. package/dist/gates/gate-rule-view.d.ts +35 -0
  29. package/dist/gates/gate-rule-view.d.ts.map +1 -0
  30. package/dist/gates/gate-rule-view.js +80 -0
  31. package/dist/gates/rule-coverage.d.ts +53 -0
  32. package/dist/gates/rule-coverage.d.ts.map +1 -0
  33. package/dist/gates/rule-coverage.js +165 -0
  34. package/dist/main.d.ts.map +1 -1
  35. package/dist/main.js +25 -2
  36. package/package.json +33 -33
@@ -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
- return 0;
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`
@@ -1 +1 @@
1
- {"version":3,"file":"registry.command.d.ts","sourceRoot":"","sources":["../../src/commands/registry.command.ts"],"names":[],"mappings":"AA4BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,wBAAwB,EAAE,eA8CtC,CAAC;AA6IF,eAAO,MAAM,eAAe,EAAE,eA4B7B,CAAC"}
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>\n`);
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>\n' +
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":"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,CAsDrF;AAyZD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}
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`);
@@ -1 +1 @@
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,CAmJnF"}
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"}
@@ -88,16 +88,22 @@ export async function runFinishGates(input) {
88
88
  const report = buildImportHygieneReport(cwd, { files: codeChanged });
89
89
  const errors = report.counts?.['error'] ?? (report.verdict === 'errors' ? report.findings.length : 0);
90
90
  const warnings = report.counts?.['warning'] ?? (report.verdict === 'warnings' ? report.findings.length : 0);
91
+ // Only the findings that actually DRIVE the verdict become "failing items".
92
+ // An allowlisted import is downgraded to `info` by design and is not what
93
+ // failed — listing it anyway pads the renderer's 15-item cap and can push a
94
+ // real error out of view, while its allowlist justification reads like a
95
+ // fix instruction. A fix-list that names non-failures is not a fix-list.
96
+ const driving = report.findings.filter((f) => report.verdict === 'errors' ? f.severity === 'error' : f.severity === 'warning');
91
97
  gates.push({
92
98
  name: 'imports',
93
99
  status: report.verdict === 'errors' ? 'fail' : 'pass',
94
- detail: `verdict=${report.verdict} (${report.findings.length} finding(s))`,
100
+ detail: `verdict=${report.verdict} (${driving.length} of ${report.findings.length} finding(s) drive it)`,
95
101
  errors,
96
102
  warnings,
97
- items: report.findings.map((f) => ({
103
+ items: driving.map((f) => ({
98
104
  file: f.file,
99
105
  line: f.line,
100
- message: `${f.kind}: ${f.suggestedFix || f.reason || f.snippet}`.trim(),
106
+ message: `[${f.severity}] ${f.kind}: ${f.suggestedFix || f.reason || f.snippet}`.trim(),
101
107
  })),
102
108
  });
103
109
  }
@@ -0,0 +1,35 @@
1
+ import type { IBaselineRule, IGeneratedArtifactRule, IPolicyRule, IRegistrationIdiom, IRegistryDeclaration, IRuleSelfTest, IWiringRule } from '@shrkcrft/core';
2
+ /** Which data-defined plane a rule belongs to. */
3
+ export type GatePlane = 'wiring' | 'policy' | 'registry' | 'registration' | 'baseline' | 'generated';
4
+ /** Every plane, in the order `shrk gates list` prints them. */
5
+ export declare const GATE_PLANES: readonly GatePlane[];
6
+ /**
7
+ * One data-defined rule, normalized across planes.
8
+ *
9
+ * The trust layer's whole job is to answer "what did this rule actually match?"
10
+ * for ANY rule, so it needs one shape to iterate. The plane-specific rule object
11
+ * rides along in `raw` for the explain dispatch.
12
+ */
13
+ export interface IGateRuleView {
14
+ readonly id: string;
15
+ readonly plane: GatePlane;
16
+ readonly description?: string;
17
+ readonly severity: 'error' | 'warning';
18
+ /** True when a zero-match is a hard failure rather than a loud skip. */
19
+ readonly failOnEmpty: boolean;
20
+ readonly selfTest?: IRuleSelfTest;
21
+ /** The underlying rule, for the plane-specific explainer. */
22
+ readonly raw: IWiringRule | IPolicyRule | IRegistryDeclaration | IRegistrationIdiom | IBaselineRule | IGeneratedArtifactRule;
23
+ }
24
+ /** The config planes this view is built from. */
25
+ export interface IGatePlanes {
26
+ readonly wiringRules?: readonly IWiringRule[];
27
+ readonly policyRules?: readonly IPolicyRule[];
28
+ readonly registries?: readonly IRegistryDeclaration[];
29
+ readonly registrationGraph?: readonly IRegistrationIdiom[];
30
+ readonly baselines?: readonly IBaselineRule[];
31
+ readonly generatedArtifacts?: readonly IGeneratedArtifactRule[];
32
+ }
33
+ /** Flatten every declared rule across every plane into one iterable list. */
34
+ export declare function collectGateRules(planes: IGatePlanes): IGateRuleView[];
35
+ //# sourceMappingURL=gate-rule-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-rule-view.d.ts","sourceRoot":"","sources":["../../src/gates/gate-rule-view.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,aAAa,EACb,sBAAsB,EACtB,WAAW,EACX,kBAAkB,EAClB,oBAAoB,EACpB,aAAa,EACb,WAAW,EACZ,MAAM,gBAAgB,CAAC;AAExB,kDAAkD;AAClD,MAAM,MAAM,SAAS,GAAG,QAAQ,GAAG,QAAQ,GAAG,UAAU,GAAG,cAAc,GAAG,UAAU,GAAG,WAAW,CAAC;AAErG,+DAA+D;AAC/D,eAAO,MAAM,WAAW,EAAE,SAAS,SAAS,EAO3C,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,SAAS,CAAC;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC,wEAAwE;IACxE,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,QAAQ,CAAC,EAAE,aAAa,CAAC;IAClC,6DAA6D;IAC7D,QAAQ,CAAC,GAAG,EACR,WAAW,GACX,WAAW,GACX,oBAAoB,GACpB,kBAAkB,GAClB,aAAa,GACb,sBAAsB,CAAC;CAC5B;AAED,iDAAiD;AACjD,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,UAAU,CAAC,EAAE,SAAS,oBAAoB,EAAE,CAAC;IACtD,QAAQ,CAAC,iBAAiB,CAAC,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC3D,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,aAAa,EAAE,CAAC;IAC9C,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,sBAAsB,EAAE,CAAC;CACjE;AAED,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,WAAW,GAAG,aAAa,EAAE,CAqErE"}
@@ -0,0 +1,80 @@
1
+ /** Every plane, in the order `shrk gates list` prints them. */
2
+ export const GATE_PLANES = [
3
+ 'wiring',
4
+ 'policy',
5
+ 'registry',
6
+ 'registration',
7
+ 'baseline',
8
+ 'generated',
9
+ ];
10
+ /** Flatten every declared rule across every plane into one iterable list. */
11
+ export function collectGateRules(planes) {
12
+ const out = [];
13
+ for (const r of planes.wiringRules ?? []) {
14
+ out.push({
15
+ id: r.id,
16
+ plane: 'wiring',
17
+ ...(r.description ? { description: r.description } : {}),
18
+ severity: r.severity ?? 'error',
19
+ failOnEmpty: r.failOnEmpty === true,
20
+ ...(r.selfTest ? { selfTest: r.selfTest } : {}),
21
+ raw: r,
22
+ });
23
+ }
24
+ for (const r of planes.policyRules ?? []) {
25
+ out.push({
26
+ id: r.id,
27
+ plane: 'policy',
28
+ ...(r.description ? { description: r.description } : {}),
29
+ severity: r.severity ?? 'error',
30
+ failOnEmpty: r.failOnEmpty === true,
31
+ ...(r.selfTest ? { selfTest: r.selfTest } : {}),
32
+ raw: r,
33
+ });
34
+ }
35
+ for (const r of planes.registries ?? []) {
36
+ out.push({
37
+ id: r.name,
38
+ plane: 'registry',
39
+ ...(r.description ? { description: r.description } : {}),
40
+ // A registry is an inventory, not a gate — it never fails a build on its
41
+ // own, so it carries no severity of its own.
42
+ severity: 'warning',
43
+ failOnEmpty: false,
44
+ raw: r,
45
+ });
46
+ }
47
+ for (const r of planes.registrationGraph ?? []) {
48
+ out.push({
49
+ id: r.name,
50
+ plane: 'registration',
51
+ ...(r.description ? { description: r.description } : {}),
52
+ severity: 'warning',
53
+ failOnEmpty: false,
54
+ raw: r,
55
+ });
56
+ }
57
+ for (const r of planes.baselines ?? []) {
58
+ out.push({
59
+ id: r.id,
60
+ plane: 'baseline',
61
+ ...(r.description ? { description: r.description } : {}),
62
+ severity: r.severity ?? 'error',
63
+ failOnEmpty: r.failOnEmpty === true,
64
+ ...(r.selfTest ? { selfTest: r.selfTest } : {}),
65
+ raw: r,
66
+ });
67
+ }
68
+ for (const r of planes.generatedArtifacts ?? []) {
69
+ out.push({
70
+ id: r.id,
71
+ plane: 'generated',
72
+ ...(r.description ? { description: r.description } : {}),
73
+ severity: r.severity ?? 'error',
74
+ failOnEmpty: r.failOnEmpty === true,
75
+ ...(r.selfTest ? { selfTest: r.selfTest } : {}),
76
+ raw: r,
77
+ });
78
+ }
79
+ return out;
80
+ }
@@ -0,0 +1,53 @@
1
+ import type { IGateRuleView } from './gate-rule-view.js';
2
+ export declare const GATE_COVERAGE_SCHEMA: "sharkcraft.gate-coverage/v1";
3
+ /**
4
+ * Whether a rule is connected to anything at all.
5
+ *
6
+ * `empty` is the finding this whole surface exists for: a stale selector that
7
+ * matches nothing passes every gate forever, so the ONLY way to notice is to
8
+ * report the match count itself and flag zero. `failed-expectation` is the
9
+ * stronger form — the author wrote down what the rule should match, and it
10
+ * doesn't.
11
+ */
12
+ export type GateCoverageStatus = 'ok' | 'empty' | 'error' | 'failed-expectation';
13
+ export interface IGateCoverage {
14
+ readonly id: string;
15
+ readonly plane: IGateRuleView['plane'];
16
+ readonly description?: string;
17
+ readonly status: GateCoverageStatus;
18
+ /** Files the rule's primary selector matched. */
19
+ readonly filesMatched: number;
20
+ /** Ids/units the rule extracted (findings scanned, for the policy plane). */
21
+ readonly unitsMatched: number;
22
+ /** What "units" means for this plane, for honest reporting. */
23
+ readonly unitLabel: string;
24
+ /** A few of the extracted ids, so the author can eyeball correctness. */
25
+ readonly sampleIds: readonly string[];
26
+ /** True when a zero match is a hard failure for this rule. */
27
+ readonly failOnEmpty: boolean;
28
+ readonly error?: string;
29
+ /** Unmet `selfTest` expectations, each a human-readable sentence. */
30
+ readonly expectationFailures: readonly string[];
31
+ }
32
+ export interface IGateCoverageReport {
33
+ readonly schema: typeof GATE_COVERAGE_SCHEMA;
34
+ readonly rules: readonly IGateCoverage[];
35
+ readonly total: number;
36
+ readonly empty: number;
37
+ readonly errored: number;
38
+ readonly expectationFailures: number;
39
+ /**
40
+ * `pass` — every rule matched something and met its expectations.
41
+ * `stale` — at least one rule matched nothing (or broke an expectation).
42
+ */
43
+ readonly verdict: 'pass' | 'stale';
44
+ }
45
+ /**
46
+ * Resolve every declared rule against the live tree and report what it matched.
47
+ *
48
+ * The `command`-compute baseline is the one rule kind that cannot be inspected
49
+ * without side effects; it is reported as un-inspected (never as `empty`), so
50
+ * the report never claims a fact it did not check.
51
+ */
52
+ export declare function buildGateCoverage(cwd: string, rules: readonly IGateRuleView[], excludeDirs?: readonly string[]): IGateCoverageReport;
53
+ //# sourceMappingURL=rule-coverage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rule-coverage.d.ts","sourceRoot":"","sources":["../../src/gates/rule-coverage.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD,eAAO,MAAM,oBAAoB,EAAG,6BAAsC,CAAC;AAE3E;;;;;;;;GAQG;AACH,MAAM,MAAM,kBAAkB,GAAG,IAAI,GAAG,OAAO,GAAG,OAAO,GAAG,oBAAoB,CAAC;AAEjF,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IACvC,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,EAAE,kBAAkB,CAAC;IACpC,iDAAiD;IACjD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,6EAA6E;IAC7E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;IACtC,8DAA8D;IAC9D,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAC9B,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,qEAAqE;IACrE,QAAQ,CAAC,mBAAmB,EAAE,SAAS,MAAM,EAAE,CAAC;CACjD;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,OAAO,oBAAoB,CAAC;IAC7C,QAAQ,CAAC,KAAK,EAAE,SAAS,aAAa,EAAE,CAAC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC;CACpC;AAsHD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAC/B,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,SAAS,aAAa,EAAE,EAC/B,WAAW,GAAE,SAAS,MAAM,EAAO,GAClC,mBAAmB,CA4DrB"}