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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/dist/commands/baseline.command.d.ts.map +1 -1
  2. package/dist/commands/baseline.command.js +42 -11
  3. package/dist/commands/changelog-data.d.ts.map +1 -1
  4. package/dist/commands/changelog-data.js +20 -0
  5. package/dist/commands/check.command.d.ts.map +1 -1
  6. package/dist/commands/check.command.js +119 -11
  7. package/dist/commands/command-catalog.d.ts.map +1 -1
  8. package/dist/commands/command-catalog.js +8 -0
  9. package/dist/commands/daily.commands.d.ts.map +1 -1
  10. package/dist/commands/daily.commands.js +11 -1
  11. package/dist/commands/gates.command.d.ts +11 -1
  12. package/dist/commands/gates.command.d.ts.map +1 -1
  13. package/dist/commands/gates.command.js +53 -10
  14. package/dist/commands/generated.command.d.ts.map +1 -1
  15. package/dist/commands/generated.command.js +36 -6
  16. package/dist/commands/help.command.d.ts.map +1 -1
  17. package/dist/commands/help.command.js +64 -2
  18. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  19. package/dist/commands/policy-lint.command.js +52 -10
  20. package/dist/commands/registry.command.d.ts.map +1 -1
  21. package/dist/commands/registry.command.js +34 -9
  22. package/dist/exit-codes.d.ts +27 -7
  23. package/dist/exit-codes.d.ts.map +1 -1
  24. package/dist/exit-codes.js +47 -8
  25. package/dist/gates/gate-envelope.d.ts +64 -0
  26. package/dist/gates/gate-envelope.d.ts.map +1 -0
  27. package/dist/gates/gate-envelope.js +26 -0
  28. package/dist/gates/gate-rule-view.d.ts +1 -1
  29. package/dist/gates/gate-rule-view.d.ts.map +1 -1
  30. package/dist/gates/gate-rule-view.js +5 -4
  31. package/dist/main.d.ts.map +1 -1
  32. package/dist/main.js +2 -1
  33. package/package.json +33 -33
@@ -1 +1 @@
1
- {"version":3,"file":"baseline.command.d.ts","sourceRoot":"","sources":["../../src/commands/baseline.command.ts"],"names":[],"mappings":"AA6BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAiShC,eAAO,MAAM,mBAAmB,EAAE,eAyCjC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAgFlC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,eA8BjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAwDnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eAoDpC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,eAc7B,CAAC"}
1
+ {"version":3,"file":"baseline.command.d.ts","sourceRoot":"","sources":["../../src/commands/baseline.command.ts"],"names":[],"mappings":"AA8BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA6ShC,eAAO,MAAM,mBAAmB,EAAE,eAyCjC,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAyGlC,CAAC;AAEF,eAAO,MAAM,mBAAmB,EAAE,eA8BjC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAwDnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eA2DpC,CAAC;AAEF,eAAO,MAAM,eAAe,EAAE,eAc7B,CAAC"}
@@ -17,11 +17,13 @@
17
17
  import { spawnSync } from 'node:child_process';
18
18
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
19
19
  import * as nodePath from 'node:path';
20
+ import { failsWhenEmpty } from '@shrkcrft/core';
20
21
  import { baselineCount, baselineFails, computeBaselineFromExtractor, diffBaseline, matchesAny, } from '@shrkcrft/boundaries';
21
22
  import { resolveChangedFiles, resolveProjectConfig } from '@shrkcrft/inspector';
22
23
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
23
24
  import { ExitCode } from "../exit-codes.js";
24
25
  import { asJson, header, kv } from "../output/format-output.js";
26
+ import { buildGateEnvelope } from "../gates/gate-envelope.js";
25
27
  const SCHEMA = 'sharkcraft.baseline/v1';
26
28
  const DEFAULT_TIMEOUT_MS = 60_000;
27
29
  async function loadBaselines(cwd) {
@@ -116,12 +118,19 @@ function evaluateRule(cwd, rule, excludeDirs, changedFiles) {
116
118
  }
117
119
  const abs = nodePath.resolve(cwd, rule.baseline);
118
120
  if (!existsSync(abs)) {
121
+ // No artifact yet. `check` must still fail (nothing to compare against),
122
+ // but the CURRENT side is knowable and is exactly what the author needs to
123
+ // see before blessing — so compute it rather than reporting a false `0`.
124
+ const first = computeCurrent(cwd, rule, excludeDirs);
119
125
  return {
120
126
  rule,
121
127
  status: 'error',
122
128
  committedCount: 0,
123
- currentCount: 0,
124
- error: `committed baseline ${rule.baseline} does not exist create it with \`shrk baseline update --id ${rule.id}\``,
129
+ ...(first.error ? {} : { current: first.text }),
130
+ currentCount: first.error ? 0 : baselineCount(rule, first.text),
131
+ missingBaseline: true,
132
+ error: first.error ??
133
+ `committed baseline ${rule.baseline} does not exist — create it with \`shrk baseline update --id ${rule.id}\``,
125
134
  };
126
135
  }
127
136
  let committed;
@@ -151,7 +160,7 @@ function evaluateRule(cwd, rule, excludeDirs, changedFiles) {
151
160
  if (currentCount === 0 && committedCount === 0) {
152
161
  return {
153
162
  rule,
154
- status: rule.failOnEmpty ? 'failed' : 'skipped',
163
+ status: failsWhenEmpty(rule) ? 'failed' : 'skipped',
155
164
  committed,
156
165
  current: computed.text,
157
166
  committedCount,
@@ -190,6 +199,7 @@ function outcomeJson(o) {
190
199
  ...(o.error ? { error: o.error } : {}),
191
200
  ...(o.skipReason ? { skipReason: o.skipReason } : {}),
192
201
  ...(o.emptyCompute ? { emptyCompute: true } : {}),
202
+ ...(o.missingBaseline ? { missingBaseline: true } : {}),
193
203
  hint: hintFor(o.rule),
194
204
  };
195
205
  }
@@ -216,12 +226,12 @@ async function prepare(args, opts) {
216
226
  process.stdout.write(asJson({ schema: SCHEMA, error: loaded.message }) + '\n');
217
227
  else
218
228
  process.stderr.write(`Could not load config: ${loaded.message}\n Run \`shrk doctor\` for details.\n`);
219
- return { ok: false, code: ExitCode.NotVerified };
229
+ return { ok: false, code: ExitCode.UsageError };
220
230
  }
221
231
  const selected = selectRules(loaded.value.rules, flagString(args, 'id') ?? undefined);
222
232
  if (!selected.ok) {
223
233
  process.stderr.write(selected.message + '\n');
224
- return { ok: false, code: ExitCode.NotVerified };
234
+ return { ok: false, code: ExitCode.UsageError };
225
235
  }
226
236
  let changedFiles;
227
237
  if (opts.changedAware) {
@@ -314,9 +324,11 @@ export const baselineCheckCommand = {
314
324
  const failed = outcomes.filter((o) => o.status === 'failed' || (o.status === 'error' && (o.rule.severity ?? 'error') === 'error'));
315
325
  const evaluated = outcomes.filter((o) => o.status !== 'skipped').length;
316
326
  // 0 only when a NON-EMPTY scope was actually compared; 2 when nothing was.
327
+ // A skipped baseline is "partially verified", never a green 0.
328
+ const skippedCount = outcomes.filter((o) => o.status === 'skipped').length;
317
329
  const exit = failed.length > 0
318
330
  ? ExitCode.Failure
319
- : evaluated === 0
331
+ : evaluated === 0 || skippedCount > 0
320
332
  ? ExitCode.NotVerified
321
333
  : ExitCode.VerifiedPass;
322
334
  if (json) {
@@ -327,6 +339,19 @@ export const baselineCheckCommand = {
327
339
  skipped: outcomes.filter((o) => o.status === 'skipped').length,
328
340
  verdict: failed.length > 0 ? 'errors' : evaluated === 0 ? 'not-verified' : 'pass',
329
341
  diagnostics: prep.planeDiagnostics,
342
+ gate: buildGateEnvelope('baseline check', exit, outcomes.map((o) => ({
343
+ id: o.rule.id,
344
+ type: 'baseline',
345
+ status: o.status,
346
+ severity: o.rule.severity ?? 'error',
347
+ counts: { committed: o.committedCount, current: o.currentCount },
348
+ violations: [
349
+ ...(o.diff?.added ?? []).map((id) => ({ id, message: 'added', hint: hintFor(o.rule) })),
350
+ ...(o.diff?.removed ?? []).map((id) => ({ id, message: 'removed', hint: hintFor(o.rule) })),
351
+ ],
352
+ ...(o.skipReason ? { skipReason: o.skipReason } : {}),
353
+ ...(o.error ? { error: o.error } : {}),
354
+ }))),
330
355
  }) + '\n');
331
356
  return exit;
332
357
  }
@@ -343,6 +368,10 @@ export const baselineCheckCommand = {
343
368
  }
344
369
  if (o.status === 'error') {
345
370
  process.stdout.write(` ! ${o.rule.id} ${o.error}\n`);
371
+ if (o.missingBaseline && o.currentCount > 0) {
372
+ process.stdout.write(` the compute currently yields ${o.currentCount} entr${o.currentCount === 1 ? 'y' : 'ies'} — ` +
373
+ `run \`shrk baseline update --id ${o.rule.id}\` to bless them.\n`);
374
+ }
346
375
  continue;
347
376
  }
348
377
  const added = o.diff?.added.length ?? 0;
@@ -414,7 +443,7 @@ export const baselineUpdateCommand = {
414
443
  errors.push({ id: rule.id, error: computed.error });
415
444
  continue;
416
445
  }
417
- if (computed.text.trim() === '' && rule.failOnEmpty) {
446
+ if (computed.text.trim() === '' && failsWhenEmpty(rule)) {
418
447
  errors.push({
419
448
  id: rule.id,
420
449
  error: 'refusing to write an EMPTY baseline for a `failOnEmpty` rule — fix the compute first',
@@ -455,7 +484,7 @@ export const baselineExplainCommand = {
455
484
  const id = flagString(args, 'id') ?? args.positional[0];
456
485
  if (!id) {
457
486
  process.stderr.write('Usage: shrk baseline explain --id <id>\n');
458
- return ExitCode.NotVerified;
487
+ return ExitCode.UsageError;
459
488
  }
460
489
  const prep = await prepare(args, { changedAware: false });
461
490
  if (!prep.ok)
@@ -463,7 +492,7 @@ export const baselineExplainCommand = {
463
492
  const rule = prep.all.find((r) => r.id === id);
464
493
  if (!rule) {
465
494
  process.stderr.write(`No baseline "${id}". Declared: ${prep.all.map((r) => r.id).join(', ') || '(none)'}\n`);
466
- return ExitCode.NotVerified;
495
+ return ExitCode.UsageError;
467
496
  }
468
497
  const outcome = evaluateRule(prep.cwd, rule, prep.excludeDirs, undefined);
469
498
  if (flagBool(args, 'json')) {
@@ -484,7 +513,9 @@ export const baselineExplainCommand = {
484
513
  process.stdout.write(kv('canonical', outcome.diff?.canonical ?? rule.compute.canonical ?? 'auto') + '\n');
485
514
  if (rule.keyBy)
486
515
  process.stdout.write(kv('keyBy', rule.keyBy) + '\n');
487
- process.stdout.write(kv('entries', `${outcome.committedCount} committed → ${outcome.currentCount} now`) + '\n');
516
+ process.stdout.write(kv('entries', outcome.missingBaseline
517
+ ? `committed (none yet) → ${outcome.currentCount} now`
518
+ : `${outcome.committedCount} committed → ${outcome.currentCount} now`) + '\n');
488
519
  process.stdout.write(kv('status', outcome.status) + '\n');
489
520
  if (outcome.error)
490
521
  process.stdout.write(` ! ${outcome.error}\n`);
@@ -506,6 +537,6 @@ export const baselineCommand = {
506
537
  const sub = args.positional[0];
507
538
  process.stderr.write((sub ? `Unknown subcommand "${sub}". ` : '') +
508
539
  'Usage: shrk baseline list | check [--id X] | diff | update | explain --id <id>\n');
509
- return ExitCode.NotVerified;
540
+ return ExitCode.UsageError;
510
541
  },
511
542
  };
@@ -1 +1 @@
1
- {"version":3,"file":"changelog-data.d.ts","sourceRoot":"","sources":["../../src/commands/changelog-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,eAAO,MAAM,sBAAsB,EAAE,SAAS,oBAAoB,EA8HjE,CAAC"}
1
+ {"version":3,"file":"changelog-data.d.ts","sourceRoot":"","sources":["../../src/commands/changelog-data.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,2CAA2C;AAC3C,MAAM,WAAW,oBAAoB;IACnC,oDAAoD;IACpD,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,yCAAyC;IACzC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,iDAAiD;IACjD,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;IAClC,wDAAwD;IACxD,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IACpC,6CAA6C;IAC7C,QAAQ,CAAC,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CACrC;AAED,eAAO,MAAM,sBAAsB,EAAE,SAAS,oBAAoB,EAkJjE,CAAC"}
@@ -134,4 +134,24 @@ export const RELEASE_SURFACE_DELTAS = [
134
134
  ],
135
135
  removed: [],
136
136
  },
137
+ {
138
+ version: '0.1.0-alpha.29',
139
+ title: 'The exit code has to agree with the sentence',
140
+ added: [
141
+ '`shrk check wiring --fix` — deterministic autofix for the mechanically-unambiguous `declared-but-not-registered` case: append the missing id to its sink array. Dry-run by default, `--write` applies. Refuses ambiguity (N sinks, non-array sink, non-unique file/array, chain rules, parity violations) and lists what it left alone WITH the reason — it never guesses.',
142
+ '`shrk explain <ruleId>` resolves a rule id across ALL planes and dispatches to the right explainer; a non-rule token still gets the original topic search. The per-plane forms remain.',
143
+ 'A shared `gate` envelope (`sharkcraft.gate/v1`) inside every gate verb\'s `--json`: `verb` / `exit` / normalized per-rule `{id,type,status,severity,counts,violations,skipReason}`. Additive — the per-plane payloads are unchanged. See docs/gate-json.md.',
144
+ 'Global `--no-hints` silences the advisory piped-exit note (the structured `--exit-trailer` channel is unaffected).',
145
+ 'Exit code `3` = usage error (unloadable config / unknown rule id / bad flag value) on the gate verbs, split out of `2` so "the gate proved nothing" and "the gate never started" are distinguishable.',
146
+ ],
147
+ changed: [
148
+ 'A SKIPPED rule is no longer masked by a passing sibling. `failOnEmpty` now defaults to TRUE for `error`-severity rules (an error rule matching zero subjects is a bug in the rule, not a pass) and any skip with no failures exits `2`, never `0`. `warning`-severity rules still default to `false`. BEHAVIOUR CHANGE: an error-severity rule that matches nothing now exits `1` — fix the selector or set `failOnEmpty: false`.',
149
+ '`shrk help <multi-word verb>` resolves catalog-documented paths (`check wiring`, `wiring unprovided`, …) that are dispatched from a parent handler and therefore never appear in the command trie; it also lists sibling verbs. Previously answered "Unknown command".',
150
+ '`shrk baseline explain` always computes the CURRENT side, so a rule with no committed artifact yet reports `committed (none yet) → N now` instead of a false `0 now`. `baseline check` names what would be blessed.',
151
+ '`shrk registry` accepts both argument orders — `registry list <name>` now works alongside `registry <name> list`; a verb-first call with an unknown name names the correct grammar.',
152
+ '`shrk policy-lint` returns `3` (usage) rather than `1` (violations) when the config cannot be loaded, and `2` when it scanned nothing.',
153
+ 'The piped-exit note is emitted at most once per process.',
154
+ ],
155
+ removed: [],
156
+ },
137
157
  ];
@@ -1 +1 @@
1
- {"version":3,"file":"check.command.d.ts","sourceRoot":"","sources":["../../src/commands/check.command.ts"],"names":[],"mappings":"AAoBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAw6BhC,eAAO,MAAM,YAAY,EAAE,eA+G1B,CAAC"}
1
+ {"version":3,"file":"check.command.d.ts","sourceRoot":"","sources":["../../src/commands/check.command.ts"],"names":[],"mappings":"AAoBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA2iChC,eAAO,MAAM,YAAY,EAAE,eA+G1B,CAAC"}
@@ -9,7 +9,8 @@ import { computeDeletedOrphans } from "../diff/deleted-orphans.js";
9
9
  import { renderWiringExplain } from "./wiring.command.js";
10
10
  import { validateTemplateVariables } from '@shrkcrft/templates';
11
11
  import { FileChangeType, planGeneration } from '@shrkcrft/generator';
12
- import { evaluateBoundaries, explainWiring, loadTsconfigPaths, runWiring, scanImports, summarizeImports, } from '@shrkcrft/boundaries';
12
+ import { evaluateBoundaries, explainWiring, loadTsconfigPaths, runWiring, scanImports, summarizeImports, planWiringFix, readMatchingFiles, wiringGlobsOf, } from '@shrkcrft/boundaries';
13
+ import { buildGateEnvelope } from "../gates/gate-envelope.js";
13
14
  function knowledgeGroup(inspection) {
14
15
  const dup = inspection.validationIssues.filter((i) => i.code === 'duplicate-id');
15
16
  const missing = inspection.validationIssues.filter((i) => i.code !== 'duplicate-id');
@@ -532,6 +533,93 @@ const WIRING_CHECK_USAGE = 'shrk check wiring [--changed-only] [--since <ref>] [
532
533
  ' --only <ids> run only these rule ids (comma-separated)\n' +
533
534
  ' --explain <id> dry-run ONE rule and print the declared/registered sets it extracts\n' +
534
535
  ' Exit: 0 verified pass · 1 violations · 2 not-verified (0 rules evaluated in scope).';
536
+ /**
537
+ * The honest exit code for a wiring run.
538
+ *
539
+ * The banner already said "Not a full green" when some rules were skipped, but
540
+ * the code an agent chains on returned `0` — a rule that enforced NOTHING was
541
+ * masked by its passing siblings. The verdict must match the sentence:
542
+ *
543
+ * `1` violations (or a `failOnEmpty` skip, which the engine folds into the verdict)
544
+ * `2` nothing evaluated, OR anything skipped — partially verified is not verified
545
+ * `0` only when every configured rule actually ran and passed
546
+ */
547
+ function wiringExitCode(report, evaluated) {
548
+ if (report.verdict === 'errors')
549
+ return ExitCode.Failure;
550
+ if (evaluated === 0)
551
+ return ExitCode.NotVerified;
552
+ if (report.skipped.length > 0)
553
+ return ExitCode.NotVerified;
554
+ return ExitCode.VerifiedPass;
555
+ }
556
+ /**
557
+ * `check wiring --fix` — append a declared-but-unregistered token to its sink
558
+ * array, but ONLY where the edit is mechanically unambiguous.
559
+ *
560
+ * Dry-run by default: it prints the exact diff it would make and writes
561
+ * nothing. `--write` applies it. Anything the planner will not touch is listed
562
+ * with the reason, because a gate that silently half-fixes is worse than one
563
+ * that does nothing — the user must be able to see what was left.
564
+ */
565
+ function runWiringFix(cwd, rules, report, write, wantJson) {
566
+ const allEdits = [];
567
+ const allSkips = [];
568
+ // Read every file a sink could live in, once.
569
+ const globs = [...new Set(rules.flatMap((r) => wiringGlobsOf(r)))];
570
+ const cache = readMatchingFiles(cwd, globs, new Set());
571
+ const files = [...cache.entries()].map(([path, content]) => ({ path, content }));
572
+ for (const rule of rules) {
573
+ const violations = report.violations.filter((v) => v.ruleId === rule.id);
574
+ if (violations.length === 0)
575
+ continue;
576
+ const plan = planWiringFix(rule, violations, files);
577
+ allEdits.push(...plan.edits);
578
+ allSkips.push(...plan.skipped);
579
+ }
580
+ // One write per file: the planner threads each file's content forward, so the
581
+ // LAST edit for a path carries every earlier insertion.
582
+ const finalByFile = new Map();
583
+ for (const e of allEdits)
584
+ finalByFile.set(e.file, e.nextContent);
585
+ let written = 0;
586
+ if (write) {
587
+ for (const [rel, content] of finalByFile) {
588
+ writeFileSync(nodePath.resolve(cwd, rel), content, 'utf8');
589
+ written += 1;
590
+ }
591
+ }
592
+ if (wantJson) {
593
+ process.stdout.write(asJson({
594
+ schema: 'sharkcraft.wiring-fix/v1',
595
+ applied: write,
596
+ filesWritten: written,
597
+ edits: allEdits.map(({ nextContent: _drop, ...rest }) => rest),
598
+ skipped: allSkips,
599
+ }) + '\n');
600
+ return allEdits.length === 0 && allSkips.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
601
+ }
602
+ process.stdout.write(header(write ? 'Wiring fix (applied)' : 'Wiring fix (dry run)'));
603
+ if (allEdits.length === 0 && allSkips.length === 0) {
604
+ process.stdout.write(' Nothing to fix — no declared-but-unregistered tokens.\n');
605
+ return ExitCode.VerifiedPass;
606
+ }
607
+ for (const e of allEdits) {
608
+ process.stdout.write(` ${write ? 'wrote ' : 'would add'} ${e.token} → ${e.file}:${e.line}\n`);
609
+ process.stdout.write(` + ${e.insert}\n`);
610
+ }
611
+ if (allSkips.length > 0) {
612
+ process.stdout.write(`\n Left untouched (${allSkips.length}) — not mechanically unambiguous:\n`);
613
+ for (const sk of allSkips) {
614
+ process.stdout.write(` • ${sk.token} [${sk.reason}] ${sk.detail}\n`);
615
+ }
616
+ }
617
+ process.stdout.write(write
618
+ ? `\n${written} file(s) written. Re-run \`shrk check wiring\` to confirm, and review the diff.\n`
619
+ : '\nDry run — nothing written. Re-run with `--write` to apply.\n');
620
+ // An unfixable violation still means the gate is red.
621
+ return allSkips.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
622
+ }
535
623
  async function checkWiring(args) {
536
624
  const cwd = resolveCwd(args);
537
625
  // `--help`/`-h` on the subverb documents its own scoping flags instead of
@@ -570,7 +658,7 @@ async function checkWiring(args) {
570
658
  const explainId = flagString(args, 'explain');
571
659
  if (!explainId) {
572
660
  process.stderr.write('Usage: shrk check wiring --explain <ruleId> [--json]\n');
573
- return 2;
661
+ return ExitCode.UsageError;
574
662
  }
575
663
  const rule = rules.find((r) => r.id === explainId);
576
664
  if (!rule) {
@@ -580,7 +668,7 @@ async function checkWiring(args) {
580
668
  return 2;
581
669
  }
582
670
  process.stderr.write(`No wiring rule "${explainId}". Configured rules: ${ids.length > 0 ? ids.join(', ') : '(none)'}\n`);
583
- return 2;
671
+ return ExitCode.UsageError;
584
672
  }
585
673
  return renderWiringExplain(explainWiring(cwd, rule), wantJson);
586
674
  }
@@ -637,6 +725,10 @@ async function checkWiring(args) {
637
725
  // blast radius, not just the rules whose literal files were edited (a25 §3.2).
638
726
  const selectedRuleIds = report.rules.map((r) => r.ruleId);
639
727
  const scoped = changedOnly || since !== undefined;
728
+ // ── --fix: deterministic, heavily-guarded autofix ─────────────────────
729
+ if (flagBool(args, 'fix')) {
730
+ return runWiringFix(cwd, rules, report, flagBool(args, 'write'), wantJson);
731
+ }
640
732
  if (wantJson) {
641
733
  // Carry the honest counts so a machine consumer can tell "0 evaluated" from
642
734
  // a real green, and see how many rules the scope skipped + which fired.
@@ -650,15 +742,31 @@ async function checkWiring(args) {
650
742
  notVerified,
651
743
  selectedRuleIds,
652
744
  // Distinguish verified-pass / failure / not-verified for a chained gate.
653
- exitCode: evaluated === 0 ? ExitCode.NotVerified : report.verdict === 'errors' ? ExitCode.Failure : ExitCode.VerifiedPass,
745
+ exitCode: wiringExitCode(report, evaluated),
746
+ // One shape across every plane — see docs/gate-json.md.
747
+ gate: buildGateEnvelope('check wiring', wiringExitCode(report, evaluated), report.rules.map((r) => {
748
+ const skip = report.skipped.find((s) => s.ruleId === r.ruleId);
749
+ return {
750
+ id: r.ruleId,
751
+ type: 'wiring',
752
+ status: r.status,
753
+ severity: r.severity,
754
+ counts: { declared: r.declaredCount, registered: r.registeredCount },
755
+ violations: r.violations.map((v) => ({
756
+ id: v.token,
757
+ file: v.file,
758
+ line: v.line,
759
+ ...(v.message ? { message: v.message } : {}),
760
+ ...(v.hint ? { hint: v.hint } : {}),
761
+ })),
762
+ ...(skip ? { skipReason: skip.reason } : {}),
763
+ ...(r.error ? { error: r.error } : {}),
764
+ };
765
+ })),
654
766
  }) + '\n');
655
767
  // `evaluated === 0` = nothing checked in scope → NOT verified (`2`), never a
656
768
  // silent `0` an agent's `&& next` would march past (a25 §1.1).
657
- return evaluated === 0
658
- ? ExitCode.NotVerified
659
- : report.verdict === 'errors'
660
- ? ExitCode.Failure
661
- : ExitCode.VerifiedPass;
769
+ return wiringExitCode(report, evaluated);
662
770
  }
663
771
  process.stdout.write(header('Wiring check'));
664
772
  // `evaluated` counts rules that actually ran a comparison (globs matched >0
@@ -722,10 +830,10 @@ async function checkWiring(args) {
722
830
  process.stdout.write(`\nNo wiring violations among the ${evaluated} rule(s) evaluated — ` +
723
831
  `${notVerified} of ${configured} NOT verified${breakdown}. Not a full green.\n`);
724
832
  }
725
- return 0;
833
+ return wiringExitCode(report, evaluated);
726
834
  }
727
835
  if (report.violations.length === 0) {
728
- return report.verdict === 'errors' ? 1 : 0;
836
+ return wiringExitCode(report, evaluated);
729
837
  }
730
838
  // Group by rule for a readable report.
731
839
  for (const r of report.rules) {
@@ -1 +1 @@
1
- {"version":3,"file":"command-catalog.d.ts","sourceRoot":"","sources":["../../src/commands/command-catalog.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"command-catalog.d.ts","sourceRoot":"","sources":["../../src/commands/command-catalog.ts"],"names":[],"mappings":"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"}
@@ -406,6 +406,14 @@ export const COMMAND_CATALOG = Object.freeze([
406
406
  surface: CommandSurface.Common,
407
407
  taskRole: CommandTaskRole.Validate,
408
408
  }),
409
+ entry({
410
+ command: 'check wiring --fix',
411
+ description: 'Deterministic autofix for `declared-but-not-registered`: append the missing id to its sink array. Dry-run by default; `--write` applies. Refuses anything ambiguous (N sinks, non-array sink, non-unique file/array) and lists what it left alone with the reason — it never guesses. [--write] [--json]',
412
+ category: 'core',
413
+ safetyLevel: SafetyLevel.WritesSource,
414
+ surface: CommandSurface.Advanced,
415
+ taskRole: CommandTaskRole.Apply,
416
+ }),
409
417
  entry({
410
418
  command: 'policy-lint explain',
411
419
  description: 'Dry-run ONE policyRule and print every hit with file:line — INCLUDING the hits an exemption (exemptFiles / exemptLines) or the lexical scan zone dropped, each labelled with which one applied. The author-loop view of what the gate sees. [--json]',
@@ -1 +1 @@
1
- {"version":3,"file":"daily.commands.d.ts","sourceRoot":"","sources":["../../src/commands/daily.commands.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAMhC,eAAO,MAAM,WAAW,EAAE,eAsFzB,CAAC;AAcF,eAAO,MAAM,WAAW,EAAE,eAoGzB,CAAC;AAKF,eAAO,MAAM,cAAc,EAAE,eAyG5B,CAAC"}
1
+ {"version":3,"file":"daily.commands.d.ts","sourceRoot":"","sources":["../../src/commands/daily.commands.ts"],"names":[],"mappings":"AAOA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAOhC,eAAO,MAAM,WAAW,EAAE,eAsFzB,CAAC;AAcF,eAAO,MAAM,WAAW,EAAE,eAoGzB,CAAC;AAKF,eAAO,MAAM,cAAc,EAAE,eAmH5B,CAAC"}
@@ -2,6 +2,7 @@ import { buildAiReadinessReport, buildTaskPacket, inspectSharkcraft, runDoctor,
2
2
  import { recommendPresets } from '@shrkcrft/presets';
3
3
  import { flagBool, flagNumber, resolveCwd, } from "../command-registry.js";
4
4
  import { asJson, header, kv } from "../output/format-output.js";
5
+ import { tryExplainGateRule } from "./gates.command.js";
5
6
  // ────────────────────────────────────────────────────────────────────────
6
7
  // shrk next — "what should I do right now in this repo?"
7
8
  // ────────────────────────────────────────────────────────────────────────
@@ -177,9 +178,18 @@ export const explainCommand = {
177
178
  async run(args) {
178
179
  const topic = args.positional.join(' ').trim();
179
180
  if (!topic) {
180
- process.stderr.write('Usage: shrk explain "<topic>"\n');
181
+ process.stderr.write('Usage: shrk explain "<topic>" (a topic, or the id of any data-defined rule)\n');
181
182
  return 2;
182
183
  }
184
+ // A single token that exactly names a declared rule on ANY plane is
185
+ // explained as that rule — the one entrypoint the trust layer promises.
186
+ // Anything else keeps the original topic search, so no existing call
187
+ // changes meaning.
188
+ if (args.positional.length === 1) {
189
+ const asRule = await tryExplainGateRule(args, topic);
190
+ if (asRule !== undefined)
191
+ return asRule;
192
+ }
183
193
  const inspection = await inspectSharkcraft({ cwd: resolveCwd(args) });
184
194
  const maxEntries = flagNumber(args, 'max-entries') ?? 6;
185
195
  const packet = buildTaskPacket(inspection, topic, { maxTokens: 2500 });
@@ -1,6 +1,16 @@
1
- import { type ICommandHandler } from '../command-registry.js';
1
+ import { type ICommandHandler, type ParsedArgs } from '../command-registry.js';
2
2
  export declare const gatesListCommand: ICommandHandler;
3
3
  export declare const gatesCoverageCommand: ICommandHandler;
4
4
  export declare const gatesExplainCommand: ICommandHandler;
5
+ /**
6
+ * Try to explain `id` as a data-defined rule on ANY plane.
7
+ *
8
+ * Returns the exit code when the id resolves to exactly one declared rule, or
9
+ * `undefined` when it is not a rule id at all — which lets `shrk explain` keep
10
+ * its original topic-search behaviour for everything else. This is the D2
11
+ * unification: a user holding a rule id no longer has to know which plane owns
12
+ * it, and no existing invocation changes meaning.
13
+ */
14
+ export declare function tryExplainGateRule(args: ParsedArgs, id: string): Promise<number | undefined>;
5
15
  export declare const gatesCommand: ICommandHandler;
6
16
  //# sourceMappingURL=gates.command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"gates.command.d.ts","sourceRoot":"","sources":["../../src/commands/gates.command.ts"],"names":[],"mappings":"AAyBA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA+EhC,eAAO,MAAM,gBAAgB,EAAE,eAsD9B,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eA4ElC,CAAC;AAuCF,eAAO,MAAM,mBAAmB,EAAE,eA4FjC,CAAC;AAEF,eAAO,MAAM,YAAY,EAAE,eAe1B,CAAC"}
1
+ {"version":3,"file":"gates.command.d.ts","sourceRoot":"","sources":["../../src/commands/gates.command.ts"],"names":[],"mappings":"AAyBA,OAAO,EAIL,KAAK,eAAe,EACpB,KAAK,UAAU,EAChB,MAAM,wBAAwB,CAAC;AAgFhC,eAAO,MAAM,gBAAgB,EAAE,eAsD9B,CAAC;AAEF,eAAO,MAAM,oBAAoB,EAAE,eAuGlC,CAAC;AAuCF,eAAO,MAAM,mBAAmB,EAAE,eA4FjC,CAAC;AAEF;;;;;;;;GAQG;AACH,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,UAAU,EAChB,EAAE,EAAE,MAAM,GACT,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAQ7B;AAED,eAAO,MAAM,YAAY,EAAE,eAe1B,CAAC"}
@@ -22,6 +22,7 @@ import { ExitCode } from "../exit-codes.js";
22
22
  import { asJson, header, kv } from "../output/format-output.js";
23
23
  import { collectGateRules, GATE_PLANES, } from "../gates/gate-rule-view.js";
24
24
  import { buildGateCoverage } from "../gates/rule-coverage.js";
25
+ import { buildGateEnvelope } from "../gates/gate-envelope.js";
25
26
  import { baselineExplainCommand } from "./baseline.command.js";
26
27
  import { generatedExplainCommand } from "./generated.command.js";
27
28
  import { renderPolicyExplain, runPolicyExplain } from "./policy-lint.command.js";
@@ -37,7 +38,7 @@ async function prepare(args) {
37
38
  process.stdout.write(asJson({ schema: SCHEMA, error: msg }) + '\n');
38
39
  else
39
40
  process.stderr.write(`Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
40
- return { ok: false, code: ExitCode.NotVerified };
41
+ return { ok: false, code: ExitCode.UsageError };
41
42
  }
42
43
  const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
43
44
  return {
@@ -89,7 +90,7 @@ export const gatesListCommand = {
89
90
  return prep.code;
90
91
  const planes = parsePlanes(args);
91
92
  if (!planes.ok)
92
- return ExitCode.NotVerified;
93
+ return ExitCode.UsageError;
93
94
  const json = flagBool(args, 'json');
94
95
  const rules = planes.planes
95
96
  ? prep.value.rules.filter((r) => planes.planes.has(r.plane))
@@ -146,7 +147,7 @@ export const gatesCoverageCommand = {
146
147
  return prep.code;
147
148
  const planes = parsePlanes(args);
148
149
  if (!planes.ok)
149
- return ExitCode.NotVerified;
150
+ return ExitCode.UsageError;
150
151
  const json = flagBool(args, 'json');
151
152
  const rules = planes.planes
152
153
  ? prep.value.rules.filter((r) => planes.planes.has(r.plane))
@@ -165,7 +166,29 @@ export const gatesCoverageCommand = {
165
166
  ? ExitCode.NotVerified
166
167
  : ExitCode.VerifiedPass;
167
168
  if (json) {
168
- process.stdout.write(asJson({ ...report, hardFailures: hardFailures.length, exitCode: exit }) + '\n');
169
+ process.stdout.write(asJson({
170
+ ...report,
171
+ hardFailures: hardFailures.length,
172
+ exitCode: exit,
173
+ gate: buildGateEnvelope('gates coverage', exit, report.rules.map((r) => ({
174
+ id: r.id,
175
+ type: r.plane,
176
+ status: r.status === 'ok'
177
+ ? 'passed'
178
+ : r.status === 'empty'
179
+ ? r.failOnEmpty
180
+ ? 'failed'
181
+ : 'skipped'
182
+ : r.status === 'error'
183
+ ? 'error'
184
+ : 'failed',
185
+ severity: r.failOnEmpty ? 'error' : 'warning',
186
+ counts: { files: r.filesMatched, units: r.unitsMatched },
187
+ violations: r.expectationFailures.map((f) => ({ id: r.id, message: f })),
188
+ ...(r.status === 'empty' ? { skipReason: `matched 0 ${r.unitLabel}` } : {}),
189
+ ...(r.error ? { error: r.error } : {}),
190
+ }))),
191
+ }) + '\n');
169
192
  return exit;
170
193
  }
171
194
  process.stdout.write(header('Gate-rule coverage'));
@@ -244,7 +267,7 @@ export const gatesExplainCommand = {
244
267
  const id = args.positional[0] ?? flagString(args, 'id');
245
268
  if (!id) {
246
269
  process.stderr.write('Usage: shrk gates explain <id> [--json]\n');
247
- return ExitCode.NotVerified;
270
+ return ExitCode.UsageError;
248
271
  }
249
272
  const prep = await prepare(args);
250
273
  if (!prep.ok)
@@ -252,25 +275,25 @@ export const gatesExplainCommand = {
252
275
  const matches = prep.value.rules.filter((r) => r.id === id);
253
276
  if (matches.length === 0) {
254
277
  process.stderr.write(`No gate rule "${id}". Run \`shrk gates list\` to see the ${prep.value.rules.length} declared rule(s).\n`);
255
- return ExitCode.NotVerified;
278
+ return ExitCode.UsageError;
256
279
  }
257
280
  // An id may legitimately exist on two planes (a wiring rule and a registry
258
281
  // can share a name); `--plane` disambiguates instead of guessing.
259
282
  const planes = parsePlanes(args);
260
283
  if (!planes.ok)
261
- return ExitCode.NotVerified;
284
+ return ExitCode.UsageError;
262
285
  const candidates = planes.planes
263
286
  ? matches.filter((r) => planes.planes.has(r.plane))
264
287
  : matches;
265
288
  if (candidates.length > 1) {
266
289
  process.stderr.write(`"${id}" exists on ${candidates.length} planes (${candidates.map((c) => c.plane).join(', ')}). ` +
267
290
  'Disambiguate with --plane <p>.\n');
268
- return ExitCode.NotVerified;
291
+ return ExitCode.UsageError;
269
292
  }
270
293
  const view = candidates[0];
271
294
  if (!view) {
272
295
  process.stderr.write(`No gate rule "${id}" on the requested plane.\n`);
273
- return ExitCode.NotVerified;
296
+ return ExitCode.UsageError;
274
297
  }
275
298
  const json = flagBool(args, 'json');
276
299
  // The two shell-executing planes own their explain output (and their trust
@@ -319,6 +342,26 @@ export const gatesExplainCommand = {
319
342
  return ExitCode.VerifiedPass;
320
343
  },
321
344
  };
345
+ /**
346
+ * Try to explain `id` as a data-defined rule on ANY plane.
347
+ *
348
+ * Returns the exit code when the id resolves to exactly one declared rule, or
349
+ * `undefined` when it is not a rule id at all — which lets `shrk explain` keep
350
+ * its original topic-search behaviour for everything else. This is the D2
351
+ * unification: a user holding a rule id no longer has to know which plane owns
352
+ * it, and no existing invocation changes meaning.
353
+ */
354
+ export async function tryExplainGateRule(args, id) {
355
+ const cwd = resolveCwd(args);
356
+ const loaded = await resolveProjectConfig(cwd);
357
+ if (!loaded.ok)
358
+ return undefined;
359
+ const rules = collectGateRules(loaded.value.config);
360
+ if (!rules.some((r) => r.id === id))
361
+ return undefined;
362
+ const forwarded = { ...args, positional: [id] };
363
+ return gatesExplainCommand.run(forwarded);
364
+ }
322
365
  export const gatesCommand = {
323
366
  name: 'gates',
324
367
  description: 'Rule-authoring trust layer: list every data-defined rule, show what each one MATCHED (the stale-selector detector), and explain any one of them. Read-only. Not `shrk gate`, which runs the quality-gate pipeline.',
@@ -329,6 +372,6 @@ export const gatesCommand = {
329
372
  process.stderr.write((sub ? `Unknown subcommand "${sub}". ` : '') +
330
373
  'Usage: shrk gates list | coverage [--plane <p>] [--strict] | explain <id>\n' +
331
374
  '(`shrk gate`, singular, runs the quality-gate pipeline — a different verb.)\n');
332
- return ExitCode.NotVerified;
375
+ return ExitCode.UsageError;
333
376
  },
334
377
  };
@@ -1 +1 @@
1
- {"version":3,"file":"generated.command.d.ts","sourceRoot":"","sources":["../../src/commands/generated.command.ts"],"names":[],"mappings":"AA+BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAwShC,eAAO,MAAM,oBAAoB,EAAE,eA2ClC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAuFnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eAmDpC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAiErC,CAAC"}
1
+ {"version":3,"file":"generated.command.d.ts","sourceRoot":"","sources":["../../src/commands/generated.command.ts"],"names":[],"mappings":"AA+BA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAyShC,eAAO,MAAM,oBAAoB,EAAE,eA2ClC,CAAC;AAEF,eAAO,MAAM,qBAAqB,EAAE,eAuHnC,CAAC;AAEF,eAAO,MAAM,sBAAsB,EAAE,eAmDpC,CAAC;AAEF,eAAO,MAAM,uBAAuB,EAAE,eAiErC,CAAC"}