@shrkcrft/cli 0.1.0-alpha.28 → 0.1.0-alpha.30

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 (55) hide show
  1. package/dist/commands/baseline.command.d.ts +25 -0
  2. package/dist/commands/baseline.command.d.ts.map +1 -1
  3. package/dist/commands/baseline.command.js +64 -17
  4. package/dist/commands/changelog-data.d.ts.map +1 -1
  5. package/dist/commands/changelog-data.js +46 -0
  6. package/dist/commands/check.command.d.ts.map +1 -1
  7. package/dist/commands/check.command.js +137 -12
  8. package/dist/commands/code-intel.command.d.ts.map +1 -1
  9. package/dist/commands/code-intel.command.js +5 -1
  10. package/dist/commands/command-catalog.d.ts.map +1 -1
  11. package/dist/commands/command-catalog.js +8 -0
  12. package/dist/commands/daily.commands.d.ts.map +1 -1
  13. package/dist/commands/daily.commands.js +11 -1
  14. package/dist/commands/docs-references.command.d.ts +7 -0
  15. package/dist/commands/docs-references.command.d.ts.map +1 -0
  16. package/dist/commands/docs-references.command.js +323 -0
  17. package/dist/commands/doctor.command.d.ts.map +1 -1
  18. package/dist/commands/doctor.command.js +3 -2
  19. package/dist/commands/gates.command.d.ts +13 -1
  20. package/dist/commands/gates.command.d.ts.map +1 -1
  21. package/dist/commands/gates.command.js +693 -26
  22. package/dist/commands/generated.command.d.ts +32 -0
  23. package/dist/commands/generated.command.d.ts.map +1 -1
  24. package/dist/commands/generated.command.js +214 -53
  25. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  26. package/dist/commands/graph-code-subverbs.js +5 -1
  27. package/dist/commands/help.command.d.ts.map +1 -1
  28. package/dist/commands/help.command.js +64 -2
  29. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  30. package/dist/commands/policy-lint.command.js +52 -10
  31. package/dist/commands/registry.command.d.ts.map +1 -1
  32. package/dist/commands/registry.command.js +34 -9
  33. package/dist/commands/wiring.command.d.ts.map +1 -1
  34. package/dist/commands/wiring.command.js +4 -3
  35. package/dist/exit-codes.d.ts +27 -7
  36. package/dist/exit-codes.d.ts.map +1 -1
  37. package/dist/exit-codes.js +47 -8
  38. package/dist/gates/gate-envelope.d.ts +64 -0
  39. package/dist/gates/gate-envelope.d.ts.map +1 -0
  40. package/dist/gates/gate-envelope.js +26 -0
  41. package/dist/gates/gate-rule-globs.d.ts +33 -0
  42. package/dist/gates/gate-rule-globs.d.ts.map +1 -0
  43. package/dist/gates/gate-rule-globs.js +101 -0
  44. package/dist/gates/gate-rule-view.d.ts +9 -3
  45. package/dist/gates/gate-rule-view.d.ts.map +1 -1
  46. package/dist/gates/gate-rule-view.js +18 -4
  47. package/dist/gates/rule-coverage.d.ts +39 -1
  48. package/dist/gates/rule-coverage.d.ts.map +1 -1
  49. package/dist/gates/rule-coverage.js +123 -8
  50. package/dist/gates/run-gate-planes.d.ts +44 -0
  51. package/dist/gates/run-gate-planes.d.ts.map +1 -0
  52. package/dist/gates/run-gate-planes.js +261 -0
  53. package/dist/main.d.ts.map +1 -1
  54. package/dist/main.js +7 -2
  55. package/package.json +33 -33
@@ -2,6 +2,8 @@ import * as nodePath from 'node:path';
2
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']);
7
9
  export const POLICY_EXPLAIN_SCHEMA = 'sharkcraft.policy-explain/v1';
@@ -86,19 +88,19 @@ export const policyLintExplainCommand = {
86
88
  const id = args.positional[0] ?? flagString(args, 'id');
87
89
  if (!id) {
88
90
  process.stderr.write('Usage: shrk policy-lint explain <ruleId> [--json]\n');
89
- return 2;
91
+ return ExitCode.UsageError;
90
92
  }
91
93
  const cwd = resolveCwd(args);
92
94
  const loaded = await resolveProjectConfig(cwd);
93
95
  if (!loaded.ok) {
94
96
  process.stderr.write(`Could not load config: ${loaded.error.message}\n`);
95
- return 2;
97
+ return ExitCode.UsageError;
96
98
  }
97
99
  const rules = loaded.value.config.policyRules ?? [];
98
100
  const rule = rules.find((r) => r.id === id);
99
101
  if (!rule) {
100
102
  process.stderr.write(`No policy rule "${id}". Configured: ${rules.map((r) => r.id).join(', ') || '(none)'}\n`);
101
- return 2;
103
+ return ExitCode.UsageError;
102
104
  }
103
105
  const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
104
106
  const excludeDirs = rel && !rel.startsWith('..') ? [rel] : [];
@@ -127,7 +129,7 @@ export const policyLintCommand = {
127
129
  const bad = parts.filter((s) => !VALID_SURFACES.has(s));
128
130
  if (bad.length > 0) {
129
131
  process.stderr.write(`Unknown --surface "${bad.join(', ')}". Use template | style | ts.\n`);
130
- return 2;
132
+ return ExitCode.UsageError;
131
133
  }
132
134
  surfaces = parts;
133
135
  }
@@ -137,11 +139,12 @@ export const policyLintCommand = {
137
139
  const msg = loaded.error.message;
138
140
  if (wantJson) {
139
141
  process.stdout.write(asJson({ schema: 'sharkcraft.policy-lint/v1', error: msg, rules: [], findings: [], diagnostics: [msg], evaluated: 0, verdict: 'errors' }) + '\n');
140
- return 1;
142
+ return ExitCode.UsageError;
141
143
  }
142
144
  process.stdout.write(header('Policy lint'));
143
145
  process.stdout.write(` ✗ Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
144
- return 1;
146
+ // A broken config is a USAGE error (3), not "violations found" (1).
147
+ return ExitCode.UsageError;
145
148
  }
146
149
  const rules = loaded.value.config.policyRules ?? [];
147
150
  const planeDiagnostics = loaded.value.planeDiagnostics;
@@ -162,7 +165,7 @@ export const policyLintCommand = {
162
165
  const unknown = requested.filter((id) => !known.has(id));
163
166
  if (unknown.length > 0) {
164
167
  process.stderr.write(`Unknown --only rule id(s): ${unknown.join(', ')}. Configured: ${[...known].join(', ') || '(none)'}\n`);
165
- return 2;
168
+ return ExitCode.UsageError;
166
169
  }
167
170
  }
168
171
  let changedFiles;
@@ -218,8 +221,41 @@ export const policyLintCommand = {
218
221
  report = { ...report, findings: newFindings, verdict };
219
222
  }
220
223
  if (wantJson) {
221
- process.stdout.write(asJson({ ...report, ...(newOnly ? { newOnly: true, hiddenBaseline } : {}) }) + '\n');
222
- return report.verdict === 'errors' ? 1 : 0;
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;
223
259
  }
224
260
  process.stdout.write(header('Policy lint'));
225
261
  // `evaluated` counts rules that actually scanned ≥1 file. When 0 rules
@@ -254,9 +290,15 @@ export const policyLintCommand = {
254
290
  process.stdout.write(` ! ${d}\n`);
255
291
  }
256
292
  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');
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');
258
295
  return 1;
259
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
+ }
260
302
  if (report.findings.length === 0 && report.diagnostics.length === 0) {
261
303
  process.stdout.write(newOnly
262
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":"AA8BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAKhC,eAAO,MAAM,wBAAwB,EAAE,eA8CtC,CAAC;AAoLF,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;AAQhC,eAAO,MAAM,wBAAwB,EAAE,eA8CtC,CAAC;AAkMF,eAAO,MAAM,eAAe,EAAE,eA0C7B,CAAC"}
@@ -20,6 +20,8 @@ import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
20
20
  import { ExitCode } from "../exit-codes.js";
21
21
  import { asJson } from "../output/format-output.js";
22
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']);
23
25
  export const registryLifecycleCommand = {
24
26
  name: 'lifecycle',
25
27
  description: 'Scan the workspace for register/remove symmetry. Read-only.',
@@ -98,11 +100,20 @@ async function runRegistryInventory(args, name) {
98
100
  if (!decl) {
99
101
  const names = loaded.registries.map((r) => r.name);
100
102
  const avail = names.length > 0 ? `Declared registries: ${names.join(', ')}.` : 'No registries declared in sharkcraft.config.ts `registries[]`.';
101
- if (json)
102
- process.stdout.write(asJson({ error: `unknown registry "${name}"`, available: names }) + '\n');
103
- else
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 {
104
110
  process.stderr.write(`No registry named "${name}". ${avail}\n`);
105
- return 2;
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;
106
117
  }
107
118
  const inventory = scanRegistry(cwd, decl);
108
119
  if (action === 'list' || action === undefined) {
@@ -125,13 +136,13 @@ async function runRegistryInventory(args, name) {
125
136
  if (action === 'exists') {
126
137
  if (!id) {
127
138
  process.stderr.write(`Usage: shrk registry ${name} exists <id> [--resolve] [--fail-if-taken|--fail-if-missing]\n`);
128
- return 2;
139
+ return ExitCode.UsageError;
129
140
  }
130
141
  const failIfTaken = flagBool(args, 'fail-if-taken');
131
142
  const failIfMissing = flagBool(args, 'fail-if-missing');
132
143
  if (failIfTaken && failIfMissing) {
133
144
  process.stderr.write('Pass at most one of --fail-if-taken / --fail-if-missing.\n');
134
- return 2;
145
+ return ExitCode.UsageError;
135
146
  }
136
147
  // `--resolve` maps a human noun to the canonical registered id before the
137
148
  // existence test — via the registry's declared `aliases` map AND generic
@@ -201,7 +212,7 @@ async function runRegistryInventory(args, name) {
201
212
  if (action === 'where') {
202
213
  if (!id) {
203
214
  process.stderr.write(`Usage: shrk registry ${name} where <id>\n`);
204
- return 2;
215
+ return ExitCode.UsageError;
205
216
  }
206
217
  const entry = registryWhere(inventory, id);
207
218
  if (json) {
@@ -220,7 +231,7 @@ async function runRegistryInventory(args, name) {
220
231
  return 0;
221
232
  }
222
233
  process.stderr.write(`Unknown action "${action}". Usage: shrk registry ${name} list | exists <id> | where <id> | duplicates\n`);
223
- return 2;
234
+ return ExitCode.UsageError;
224
235
  }
225
236
  export const registryCommand = {
226
237
  name: 'registry',
@@ -237,7 +248,21 @@ export const registryCommand = {
237
248
  return registryLifecycleCommand.run(args);
238
249
  }
239
250
  if (sub !== undefined && sub.length > 0) {
240
- // `<name> list | exists <id> | where <id>` sub is the registry name.
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.
241
266
  return runRegistryInventory(args, sub);
242
267
  }
243
268
  const cwd = resolveCwd(args);
@@ -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,CAoFrF;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,CAwFrF;AAyZD,eAAO,MAAM,aAAa,EAAE,eAgB3B,CAAC"}
@@ -28,9 +28,10 @@ export function renderWiringExplain(report, wantJson) {
28
28
  process.stdout.write(kv('registeredMode', 'intersection (must be in EVERY sink)') + '\n');
29
29
  }
30
30
  process.stdout.write(kv('status', report.status) + '\n');
31
- process.stdout.write(kv('declared', `${report.declared.distinctCount} distinct across ${report.declared.filesScanned} file(s)`) +
32
- '\n');
33
- process.stdout.write(kv('registered', `${report.registered.distinctCount} distinct across ${report.registered.filesScanned} file(s)`) + '\n');
31
+ process.stdout.write(kv('declared', `${report.declared.distinctCount} distinct across ${report.declared.filesScanned} file(s)` +
32
+ (report.declared.viaExtractor ? ` (via $use:${report.declared.viaExtractor})` : '')) + '\n');
33
+ process.stdout.write(kv('registered', `${report.registered.distinctCount} distinct across ${report.registered.filesScanned} file(s)` +
34
+ (report.registered.viaExtractor ? ` (via $use:${report.registered.viaExtractor})` : '')) + '\n');
34
35
  if (report.declared.error)
35
36
  process.stdout.write(` ! declared side: ${report.declared.error}\n`);
36
37
  if (report.registered.error) {
@@ -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, degraded
16
- * fallback, short-circuit, timeout, or "refused to run".
17
- * Distinct from both pass and fail so a chain can branch
18
- * on it (`|| handle-indeterminate`). This is also the
19
- * code the CLI already uses for usage errors — both mean
20
- * "did not produce a verified result".
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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"exit-codes.d.ts","sourceRoot":"","sources":["../src/exit-codes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,oBAAY,QAAQ;IAClB,YAAY,IAAI;IAChB,OAAO,IAAI;IACX,WAAW,IAAI;CAChB;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,GAAG,MAAM,CAGtE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAM9D;AAED;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAMnE;AA4BD;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,WAAW,EAAE,MAAM,GAAG,OAAO,CAMvD;AAED,oFAAoF;AACpF,MAAM,WAAW,gBAAgB;IAC/B,yEAAyE;IACzE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,gDAAgD;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,gFAAgF;IAChF,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,kBAAkB,CAChC,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,gBAAgB,GACrB,IAAI,CAWN"}
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"}
@@ -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, degraded
16
- * fallback, short-circuit, timeout, or "refused to run".
17
- * Distinct from both pass and fail so a chain can branch
18
- * on it (`|| handle-indeterminate`). This is also the
19
- * code the CLI already uses for usage errors — both mean
20
- * "did not produce a verified result".
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
@@ -29,6 +36,7 @@ export var ExitCode;
29
36
  ExitCode[ExitCode["VerifiedPass"] = 0] = "VerifiedPass";
30
37
  ExitCode[ExitCode["Failure"] = 1] = "Failure";
31
38
  ExitCode[ExitCode["NotVerified"] = 2] = "NotVerified";
39
+ ExitCode[ExitCode["UsageError"] = 3] = "UsageError";
32
40
  })(ExitCode || (ExitCode = {}));
33
41
  /**
34
42
  * Promote a NotVerified (`2`) exit into a Failure-class nonzero (`1`) when the
@@ -116,6 +124,32 @@ export function isGateVerb(commandPath) {
116
124
  return true;
117
125
  return false;
118
126
  }
127
+ /**
128
+ * True when the argv carries the global `--no-hints` (before the `--`
129
+ * sentinel). Suppresses advisory one-liners like the piped-exit note while
130
+ * leaving every real diagnostic — and the `--exit-trailer` machine channel —
131
+ * untouched. For a caller that has already internalised the warning and just
132
+ * wants clean stderr in captured logs.
133
+ */
134
+ export function argvHasNoHints(argv) {
135
+ for (const t of argv) {
136
+ if (t === '--')
137
+ break;
138
+ if (t === '--no-hints')
139
+ return true;
140
+ }
141
+ return false;
142
+ }
143
+ /**
144
+ * Process-lifetime latch for the piped-exit hint. The note is advisory, so it
145
+ * pays rent once: a command that emits several gate verdicts in one process
146
+ * (or a `--watch` loop) should not repeat the same paragraph every cycle.
147
+ */
148
+ let pipeHintEmitted = false;
149
+ /** Reset the one-time hint latch. Test-only seam. */
150
+ export function resetPipeHintLatch() {
151
+ pipeHintEmitted = false;
152
+ }
119
153
  /**
120
154
  * Keep the honest `0`/`1`/`2` exit code READABLE through the shape agents reach
121
155
  * for first — the trailing pipe. `<gate> | head` reports `head`'s `$?`, so a true
@@ -136,9 +170,14 @@ export function emitPipeExitSignal(commandPath, code, opts) {
136
170
  if (!isGateVerb(commandPath))
137
171
  return;
138
172
  const write = opts.write ?? ((s) => void process.stderr.write(s));
139
- if (opts.piped && code !== 0) {
173
+ // Advisory, and therefore rationed: stderr only, only when a NON-zero verdict
174
+ // would actually be lost to the pipe, at most once per process, and never
175
+ // under `--no-hints`. The structured channel (`--exit-trailer`) is unaffected
176
+ // by all of these — it is opt-in and always emitted when asked.
177
+ if (opts.piped && code !== 0 && !opts.noHints && !pipeHintEmitted) {
178
+ pipeHintEmitted = true;
140
179
  write(`note: stdout is piped — $? reflects the downstream command, not shrk (exit ${code}); ` +
141
- `use PIPESTATUS[0] or --exit-trailer to read shrk's verdict.\n`);
180
+ `use PIPESTATUS[0] or --exit-trailer to read shrk's verdict (--no-hints silences this).\n`);
142
181
  }
143
182
  // The trailer is written LAST so it is the final stderr line a caller reads.
144
183
  if (opts.trailer)
@@ -0,0 +1,64 @@
1
+ /**
2
+ * One machine-readable shape for every gate verb.
3
+ *
4
+ * `--json` has always been available on each plane, but each emitted its own
5
+ * schema (`sharkcraft.wiring/v1`, `sharkcraft.baseline/v1`, …), so a CI step or
6
+ * agent had to parse four shapes to answer one question: which rules ran, which
7
+ * failed, and why. This envelope is that answer, identical across planes.
8
+ *
9
+ * It is ADDITIVE. The per-plane payloads are published, documented, and asserted
10
+ * by tests; replacing them would break every existing consumer. The envelope
11
+ * rides along under a `gate` key, so `jq .gate` is the uniform read and nothing
12
+ * that worked before stops working.
13
+ */
14
+ export declare const GATE_ENVELOPE_SCHEMA: "sharkcraft.gate/v1";
15
+ /** Which data-defined plane produced a rule result. */
16
+ export type GateRuleType = 'wiring' | 'policy' | 'registry' | 'registration' | 'baseline' | 'generated' | 'doc-reference';
17
+ /**
18
+ * Per-rule outcome, uniform across planes. `skipped` is deliberately a
19
+ * first-class status, not folded into `passed` — a rule that matched nothing
20
+ * enforced nothing.
21
+ */
22
+ export type GateRuleStatus = 'passed' | 'failed' | 'skipped' | 'error';
23
+ /** One violation, normalized. `id` is the offending token / entry / file. */
24
+ export interface IGateViolation {
25
+ readonly id: string;
26
+ readonly file?: string;
27
+ readonly line?: number;
28
+ readonly message?: string;
29
+ readonly hint?: string;
30
+ }
31
+ /** One rule's result in the shared envelope. */
32
+ export interface IGateRuleResult {
33
+ readonly id: string;
34
+ readonly type: GateRuleType;
35
+ readonly status: GateRuleStatus;
36
+ readonly severity: 'error' | 'warning';
37
+ /**
38
+ * Plane-appropriate match counts — e.g. `{declared, registered}` for wiring,
39
+ * `{committed, current}` for baseline, `{units, findings}` for policy. Always
40
+ * present so "what did this rule actually see?" is answerable uniformly.
41
+ */
42
+ readonly counts: Readonly<Record<string, number>>;
43
+ readonly violations: readonly IGateViolation[];
44
+ /** Why the rule checked nothing, when `status` is `skipped`. */
45
+ readonly skipReason?: string;
46
+ /** Set when the rule is misconfigured (`status: 'error'`). */
47
+ readonly error?: string;
48
+ }
49
+ /** The envelope emitted under the `gate` key of every gate verb's `--json`. */
50
+ export interface IGateEnvelope {
51
+ readonly schema: typeof GATE_ENVELOPE_SCHEMA;
52
+ /** The verb that produced this, space-joined (e.g. `check wiring`). */
53
+ readonly verb: string;
54
+ /** The exit code this run returns — the same number the process exits with. */
55
+ readonly exit: number;
56
+ readonly rules: readonly IGateRuleResult[];
57
+ /** Rules that ran a real comparison (status !== 'skipped'). */
58
+ readonly evaluated: number;
59
+ readonly skipped: number;
60
+ readonly failed: number;
61
+ }
62
+ /** Build the envelope from already-normalized per-rule results. */
63
+ export declare function buildGateEnvelope(verb: string, exit: number, rules: readonly IGateRuleResult[]): IGateEnvelope;
64
+ //# sourceMappingURL=gate-envelope.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-envelope.d.ts","sourceRoot":"","sources":["../../src/gates/gate-envelope.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,oBAAoB,EAAG,oBAA6B,CAAC;AAElE,uDAAuD;AACvD,MAAM,MAAM,YAAY,GACpB,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,cAAc,GACd,UAAU,GACV,WAAW,GACX,eAAe,CAAC;AAEpB;;;;GAIG;AACH,MAAM,MAAM,cAAc,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvE,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,gDAAgD;AAChD,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,IAAI,EAAE,YAAY,CAAC;IAC5B,QAAQ,CAAC,MAAM,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;IACvC;;;;OAIG;IACH,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAClD,QAAQ,CAAC,UAAU,EAAE,SAAS,cAAc,EAAE,CAAC;IAC/C,gEAAgE;IAChE,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;IAC7B,8DAA8D;IAC9D,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,+EAA+E;AAC/E,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,MAAM,EAAE,OAAO,oBAAoB,CAAC;IAC7C,uEAAuE;IACvE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,+EAA+E;IAC/E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,SAAS,eAAe,EAAE,CAAC;IAC3C,+DAA+D;IAC/D,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;CACzB;AAED,mEAAmE;AACnE,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,eAAe,EAAE,GAChC,aAAa,CAUf"}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * One machine-readable shape for every gate verb.
3
+ *
4
+ * `--json` has always been available on each plane, but each emitted its own
5
+ * schema (`sharkcraft.wiring/v1`, `sharkcraft.baseline/v1`, …), so a CI step or
6
+ * agent had to parse four shapes to answer one question: which rules ran, which
7
+ * failed, and why. This envelope is that answer, identical across planes.
8
+ *
9
+ * It is ADDITIVE. The per-plane payloads are published, documented, and asserted
10
+ * by tests; replacing them would break every existing consumer. The envelope
11
+ * rides along under a `gate` key, so `jq .gate` is the uniform read and nothing
12
+ * that worked before stops working.
13
+ */
14
+ export const GATE_ENVELOPE_SCHEMA = 'sharkcraft.gate/v1';
15
+ /** Build the envelope from already-normalized per-rule results. */
16
+ export function buildGateEnvelope(verb, exit, rules) {
17
+ return {
18
+ schema: GATE_ENVELOPE_SCHEMA,
19
+ verb,
20
+ exit,
21
+ rules,
22
+ evaluated: rules.filter((r) => r.status !== 'skipped').length,
23
+ skipped: rules.filter((r) => r.status === 'skipped').length,
24
+ failed: rules.filter((r) => r.status === 'failed' || r.status === 'error').length,
25
+ };
26
+ }
@@ -0,0 +1,33 @@
1
+ import { type IWiringSource } from '@shrkcrft/core';
2
+ import type { IGateRuleView } from './gate-rule-view.js';
3
+ /**
4
+ * The FOOTPRINT of a gate rule: every project-relative glob it reads.
5
+ *
6
+ * `--changed-only` is only trustworthy if the footprint is complete. A rule
7
+ * scoped by its declared side alone would go unevaluated when the change edits
8
+ * the REGISTERED side — a registration deleted in file B silently skipping the
9
+ * rule declared over file A is the precise bug this plane exists to catch, so
10
+ * every side, every hop, and every watched input counts.
11
+ */
12
+ export declare function gateRuleGlobs(view: IGateRuleView): readonly string[];
13
+ /**
14
+ * EVERY extraction source a rule reads — both wiring sides, every union sink,
15
+ * every chain hop, a registry's consumer, all three registration roles.
16
+ *
17
+ * Collecting all of them (not just the rule's primary side) is what lets the
18
+ * coverage view name every consumer of a shared extractor. A rule that `$use`s
19
+ * one on its *registered* side is just as bound to that definition as one that
20
+ * uses it on `declared`, and listing only half of them would understate exactly
21
+ * the guarantee the shared extractor exists to provide.
22
+ */
23
+ export declare function gateRuleSources(view: IGateRuleView): readonly IWiringSource[];
24
+ /**
25
+ * Whether a rule's footprint intersects the changed set.
26
+ *
27
+ * A rule with NO resolvable footprint (a command baseline with no
28
+ * `watchFiles`) cannot be proven out of scope, so it stays IN scope. Guessing
29
+ * "probably unaffected" is how a `--changed-only` run silently stops checking
30
+ * the one thing that broke.
31
+ */
32
+ export declare function ruleTouchedBy(view: IGateRuleView, changed: readonly string[]): boolean;
33
+ //# sourceMappingURL=gate-rule-globs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gate-rule-globs.d.ts","sourceRoot":"","sources":["../../src/gates/gate-rule-globs.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,aAAa,EACnB,MAAM,gBAAgB,CAAC;AAExB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzD;;;;;;;;GAQG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,MAAM,EAAE,CAqBpE;AAcD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,SAAS,aAAa,EAAE,CA+B7E;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,aAAa,EAAE,OAAO,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAItF"}
@@ -0,0 +1,101 @@
1
+ import { resolveSourceGlobs, } from '@shrkcrft/core';
2
+ import { matchesAny } from '@shrkcrft/boundaries';
3
+ /**
4
+ * The FOOTPRINT of a gate rule: every project-relative glob it reads.
5
+ *
6
+ * `--changed-only` is only trustworthy if the footprint is complete. A rule
7
+ * scoped by its declared side alone would go unevaluated when the change edits
8
+ * the REGISTERED side — a registration deleted in file B silently skipping the
9
+ * rule declared over file A is the precise bug this plane exists to catch, so
10
+ * every side, every hop, and every watched input counts.
11
+ */
12
+ export function gateRuleGlobs(view) {
13
+ if (view.plane === 'policy')
14
+ return view.raw.files ?? [];
15
+ if (view.plane === 'baseline') {
16
+ const rule = view.raw;
17
+ // `watchFiles` is the ONLY footprint a command compute has — without it a
18
+ // command baseline can never be scoped, so it always runs (see below).
19
+ return [
20
+ ...gateRuleSources(view).flatMap(sourceGlobs),
21
+ ...(rule.watchFiles ?? []),
22
+ rule.baseline,
23
+ ];
24
+ }
25
+ if (view.plane === 'generated') {
26
+ const rule = view.raw;
27
+ return [
28
+ ...rule.generatedGlob,
29
+ ...(rule.sources ?? []).flatMap((s) => s.glob),
30
+ ...(rule.handMaintained ?? []),
31
+ ];
32
+ }
33
+ return gateRuleSources(view).flatMap(sourceGlobs);
34
+ }
35
+ /**
36
+ * Every glob one source reads: the files it scans, plus an `import-edges`
37
+ * source's TARGET globs.
38
+ *
39
+ * A fence whose footprint covered only the consumer side would go unevaluated
40
+ * when the target subtree moved — and "cannot be proven out of scope" must
41
+ * always resolve to staying IN scope, never to a quiet skip.
42
+ */
43
+ function sourceGlobs(src) {
44
+ return [...resolveSourceGlobs(src), ...(src.to?.files ?? [])];
45
+ }
46
+ /**
47
+ * EVERY extraction source a rule reads — both wiring sides, every union sink,
48
+ * every chain hop, a registry's consumer, all three registration roles.
49
+ *
50
+ * Collecting all of them (not just the rule's primary side) is what lets the
51
+ * coverage view name every consumer of a shared extractor. A rule that `$use`s
52
+ * one on its *registered* side is just as bound to that definition as one that
53
+ * uses it on `declared`, and listing only half of them would understate exactly
54
+ * the guarantee the shared extractor exists to provide.
55
+ */
56
+ export function gateRuleSources(view) {
57
+ switch (view.plane) {
58
+ case 'wiring': {
59
+ const rule = view.raw;
60
+ return [
61
+ rule.declared,
62
+ ...(Array.isArray(rule.registered)
63
+ ? rule.registered
64
+ : rule.registered
65
+ ? [rule.registered]
66
+ : []),
67
+ ...(rule.chain ?? []),
68
+ ].filter((s) => s !== undefined);
69
+ }
70
+ case 'registry': {
71
+ const decl = view.raw;
72
+ return decl.consumer ? [decl.source, decl.consumer] : [decl.source];
73
+ }
74
+ case 'registration': {
75
+ const idiom = view.raw;
76
+ return [idiom.declared, idiom.provided, idiom.consumed];
77
+ }
78
+ case 'baseline': {
79
+ const source = view.raw.compute.source;
80
+ return source ? [source] : [];
81
+ }
82
+ default:
83
+ // `policy` and `generated` select files by glob, not by an extraction
84
+ // source, so neither can reference a shared extractor.
85
+ return [];
86
+ }
87
+ }
88
+ /**
89
+ * Whether a rule's footprint intersects the changed set.
90
+ *
91
+ * A rule with NO resolvable footprint (a command baseline with no
92
+ * `watchFiles`) cannot be proven out of scope, so it stays IN scope. Guessing
93
+ * "probably unaffected" is how a `--changed-only` run silently stops checking
94
+ * the one thing that broke.
95
+ */
96
+ export function ruleTouchedBy(view, changed) {
97
+ const globs = gateRuleGlobs(view).filter((g) => g.length > 0);
98
+ if (globs.length === 0)
99
+ return true;
100
+ return changed.some((file) => matchesAny(file, globs));
101
+ }