@shrkcrft/cli 0.1.0-alpha.25 → 0.1.0-alpha.26

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.
@@ -191,5 +191,15 @@ export interface IFlagListOptions {
191
191
  }
192
192
  export declare function flagList(args: ParsedArgs, name: string, options?: IFlagListOptions): string[];
193
193
  export declare function flagVars(args: ParsedArgs): Record<string, string>;
194
+ /**
195
+ * Return the first parsed flag whose name is NOT in `allowed`, or `undefined`
196
+ * when every flag is recognized. The parser does not reject unknown flags on its
197
+ * own — without a per-command allow-list a typo'd flag (`--no-such-flag`) parses
198
+ * as `true`, reads as a confident opt-in, and the verb runs the bare command at
199
+ * exit `0`. A verb that opts into strict flag handling calls this with its known
200
+ * set and rejects the result loudly, so a mistyped/renamed flag can never read
201
+ * as success (see docs/exit-codes.md · a25 §2.1).
202
+ */
203
+ export declare function firstUnknownFlag(args: ParsedArgs, allowed: ReadonlySet<string>): string | undefined;
194
204
  export {};
195
205
  //# sourceMappingURL=command-registry.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"command-registry.d.ts","sourceRoot":"","sources":["../src/command-registry.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"command-registry.d.ts","sourceRoot":"","sources":["../src/command-registry.ts"],"names":[],"mappings":"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"}
@@ -451,3 +451,19 @@ export function flagVars(args) {
451
451
  }
452
452
  return out;
453
453
  }
454
+ /**
455
+ * Return the first parsed flag whose name is NOT in `allowed`, or `undefined`
456
+ * when every flag is recognized. The parser does not reject unknown flags on its
457
+ * own — without a per-command allow-list a typo'd flag (`--no-such-flag`) parses
458
+ * as `true`, reads as a confident opt-in, and the verb runs the bare command at
459
+ * exit `0`. A verb that opts into strict flag handling calls this with its known
460
+ * set and rejects the result loudly, so a mistyped/renamed flag can never read
461
+ * as success (see docs/exit-codes.md · a25 §2.1).
462
+ */
463
+ export function firstUnknownFlag(args, allowed) {
464
+ for (const key of args.flags.keys()) {
465
+ if (!allowed.has(key))
466
+ return key;
467
+ }
468
+ return undefined;
469
+ }
@@ -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,EA2DjE,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,EAmFjE,CAAC"}
@@ -67,4 +67,28 @@ export const RELEASE_SURFACE_DELTAS = [
67
67
  ],
68
68
  removed: [],
69
69
  },
70
+ {
71
+ version: '0.1.0-alpha.26',
72
+ title: 'Honest exit codes',
73
+ added: [
74
+ 'A unified exit-code contract across every gate/verify/check verb: `0` verified pass, `1` failure, `2` NOT-verified (empty scope / degraded / timeout). See docs/exit-codes.md.',
75
+ 'Global `--strict` — promote a NOT-verified (`2`) verdict to a failure-class nonzero (`1`) across every gate, for a hard CI gate in one switch.',
76
+ '`shrk gate baseline --refreeze` — re-freeze the architecture baseline to the current state (operational reset).',
77
+ '`shrk check wiring --base <ref>` (synonym of `--since`); `check wiring` now shows the rule ids selected by footprint under a scoped run.',
78
+ '`shrk reuse … --all` — the full candidate catalog (default is now a scored top-K did-you-mean).',
79
+ ],
80
+ changed: [
81
+ '`shrk check wiring` (0 rules evaluated) and `shrk registry lifecycle` / `check registry-lifecycle` (0 registrations, or wall-clock timeout) now exit `2` (NOT verified), not a green `0` a chained `&& next` would march past.',
82
+ 'The graph/query family (`graph cycles|hubs|callers|impact|search|context|…`) rejects an unknown/misspelled flag (`unknown option --x`, exit `2`) instead of silently swallowing it as a confident success.',
83
+ 'Piping any streaming list (`… | head`, `| grep`) no longer crashes with `write EPIPE` — a downstream early-close is swallowed and the real exit code is preserved.',
84
+ '`shrk check wiring --help` documents its own scoping flags instead of running the bare check.',
85
+ '`shrk reuse "<intent>"` returns a scored top-K did-you-mean (score per row) on a weak/no-match intent instead of dumping the whole catalog.',
86
+ '`shrk registry <name> exists <id> --resolve` also normalizes (case-fold, singular/plural, suffix strip/append), not just the declared `aliases` map.',
87
+ '`shrk compress` prints a `fidelity: lossy|lossless|passthrough` banner on the human text path (parity with `--json`).',
88
+ '`shrk changes summary` ships a built-in config/tooling area for non-lib paths — only genuinely unclassifiable files stay `unknown`.',
89
+ '`shrk gen` saved plans persist the rendered file `body` + a per-entry `sha256` digest (reviewable/diffable/re-appliable), not just `sizeBytes`.',
90
+ '`shrk gate` counts only change-attributable (diff-vs-HEAD) architecture errors as blocking; stale-baseline drift in untouched files is informational.',
91
+ ],
92
+ removed: [],
93
+ },
70
94
  ];
@@ -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;AAm1BhC,eAAO,MAAM,YAAY,EAAE,eAoG1B,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;AAu4BhC,eAAO,MAAM,YAAY,EAAE,eA+G1B,CAAC"}
@@ -3,6 +3,7 @@ import { mkdirSync, writeFileSync, readFileSync, existsSync } from 'node:fs';
3
3
  import * as nodePath from 'node:path';
4
4
  import { flagBool, flagNumber, flagString, flagVars, resolveCwd, } from "../command-registry.js";
5
5
  import { asJson, header, kv } from "../output/format-output.js";
6
+ import { ExitCode } from "../exit-codes.js";
6
7
  import { maybeRunInWatchMode } from "../output/watch-loop.js";
7
8
  import { computeDeletedOrphans } from "../diff/deleted-orphans.js";
8
9
  import { renderWiringExplain } from "./wiring.command.js";
@@ -522,11 +523,29 @@ async function checkBoundariesOnce(args) {
522
523
  // ────────────────────────────────────────────────────────────────────────
523
524
  // Subcommand: wiring — "declared but not wired" completeness checks
524
525
  // ────────────────────────────────────────────────────────────────────────
526
+ const WIRING_CHECK_USAGE = 'shrk check wiring [--changed-only] [--since <ref>] [--base <ref>] [--only <ids>] [--explain <ruleId>] [--json] [--strict]\n' +
527
+ ' Cross-file "declared but not wired" completeness gate. Scope flags select rules by\n' +
528
+ ' FOOTPRINT — a rule fires when the diff touches EITHER its declared or its registered\n' +
529
+ ' side (so a registration edited in file B fires a rule declared in file A).\n' +
530
+ ' --changed-only run only rules whose footprint intersects the working diff\n' +
531
+ ' --since <ref> scope the diff to changes since <ref> (--base is a synonym)\n' +
532
+ ' --only <ids> run only these rule ids (comma-separated)\n' +
533
+ ' --explain <id> dry-run ONE rule and print the declared/registered sets it extracts\n' +
534
+ ' Exit: 0 verified pass · 1 violations · 2 not-verified (0 rules evaluated in scope).';
525
535
  async function checkWiring(args) {
526
536
  const cwd = resolveCwd(args);
537
+ // `--help`/`-h` on the subverb documents its own scoping flags instead of
538
+ // silently running the bare check (a25 §3.2). The check dispatcher doesn't
539
+ // intercept a subverb-level `--help`, so handle it here.
540
+ if (flagBool(args, 'help') || flagBool(args, 'h')) {
541
+ process.stdout.write(WIRING_CHECK_USAGE + '\n');
542
+ return 0;
543
+ }
527
544
  const wantJson = flagBool(args, 'json');
528
545
  const changedOnly = flagBool(args, 'changed-only');
529
- const since = flagString(args, 'since');
546
+ // `--base <ref>` is an accepted synonym for `--since <ref>` (a25 §3.2)
547
+ // scope the footprint diff to changes since an arbitrary ref.
548
+ const since = flagString(args, 'since') ?? flagString(args, 'base');
530
549
  const only = flagString(args, 'only');
531
550
  // Distinguish "config is invalid" from "config valid with no wiring rules":
532
551
  // an invalid config (e.g. a malformed wiringRule) must NOT fail open with a
@@ -613,22 +632,50 @@ async function checkWiring(args) {
613
632
  if (matchedNothing > 0)
614
633
  parts.push(`${matchedNothing} matched no files`);
615
634
  const breakdown = parts.length > 0 ? ` (${parts.join(', ')})` : '';
635
+ // Rule ids selected by footprint (declared OR registered side intersecting the
636
+ // diff) — positive evidence the gate ran the RIGHT rules for the change's
637
+ // blast radius, not just the rules whose literal files were edited (a25 §3.2).
638
+ const selectedRuleIds = report.rules.map((r) => r.ruleId);
639
+ const scoped = changedOnly || since !== undefined;
616
640
  if (wantJson) {
617
641
  // Carry the honest counts so a machine consumer can tell "0 evaluated" from
618
- // a real green, and see how many rules the scope skipped.
619
- process.stdout.write(asJson({ ...report, configured, selected, evaluated, skippedByScope, matchedNothing, notVerified }) + '\n');
620
- return report.verdict === 'errors' ? 1 : 0;
642
+ // a real green, and see how many rules the scope skipped + which fired.
643
+ process.stdout.write(asJson({
644
+ ...report,
645
+ configured,
646
+ selected,
647
+ evaluated,
648
+ skippedByScope,
649
+ matchedNothing,
650
+ notVerified,
651
+ selectedRuleIds,
652
+ // Distinguish verified-pass / failure / not-verified for a chained gate.
653
+ exitCode: evaluated === 0 ? ExitCode.NotVerified : report.verdict === 'errors' ? ExitCode.Failure : ExitCode.VerifiedPass,
654
+ }) + '\n');
655
+ // `evaluated === 0` = nothing checked in scope → NOT verified (`2`), never a
656
+ // 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;
621
662
  }
622
663
  process.stdout.write(header('Wiring check'));
623
664
  // `evaluated` counts rules that actually ran a comparison (globs matched >0
624
665
  // files). When 0 rules evaluated but rules ARE configured, say so loudly —
625
- // "checked nothing" must never read as the green "every token is wired" pass.
666
+ // "checked nothing" must never read as the green "every token is wired" pass,
667
+ // and the exit code must say NOT verified (`2`) too, not a lying `0`.
626
668
  if (evaluated === 0) {
627
669
  process.stdout.write(` ! 0 rules evaluated — NOT verified. ${configured} rule(s) configured${breakdown}; ` +
628
670
  'none ran a comparison in scope. Wiring was not checked — this is not a pass.\n');
629
- return 0;
671
+ return ExitCode.NotVerified;
630
672
  }
631
673
  process.stdout.write(kv('rules evaluated', `${evaluated} of ${configured}${breakdown}`) + '\n');
674
+ if (scoped && selectedRuleIds.length > 0) {
675
+ // Show WHICH rules the diff's footprint selected so the scoping is provable,
676
+ // not indistinguishable from plain changed-file scoping.
677
+ process.stdout.write(kv('selected by footprint', selectedRuleIds.join(', ')) + '\n');
678
+ }
632
679
  const errors = report.violations.filter((v) => v.severity === 'error').length;
633
680
  const warnings = report.violations.filter((v) => v.severity === 'warning').length;
634
681
  process.stdout.write(kv('violations', `${errors} error(s), ${warnings} warning(s)`) + '\n');
@@ -821,8 +868,18 @@ export const checkCommand = {
821
868
  ...(scope ? { scope } : {}),
822
869
  ...(skipDirs ? { skipDirs } : {}),
823
870
  });
824
- // Deterministic non-zero on timeout so a wedged scan fails loud, not silent.
825
- const exit = report.timedOut ? 2 : report.missingRemovers.length === 0 ? 0 : 1;
871
+ // Honest exit-code contract (a25 §1 / §2.5):
872
+ // timedOut → NOT verified (2): the budget flushed partial results.
873
+ // registersFound=0 → NOT verified (2): nothing to check in scope — a
874
+ // "nothing to verify" run must never read as a green
875
+ // pass (an agent's `&& next` would march past it).
876
+ // missingRemovers>0 → failure (1).
877
+ // otherwise → verified pass (0).
878
+ const exit = report.timedOut || report.registersFound === 0
879
+ ? ExitCode.NotVerified
880
+ : report.missingRemovers.length === 0
881
+ ? ExitCode.VerifiedPass
882
+ : ExitCode.Failure;
826
883
  if (flagBool(args, 'json')) {
827
884
  process.stdout.write(asJson(report) + '\n');
828
885
  return exit;
@@ -1 +1 @@
1
- {"version":3,"file":"compress.command.d.ts","sourceRoot":"","sources":["../../src/commands/compress.command.ts"],"names":[],"mappings":"AAQA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAqEhC,eAAO,MAAM,eAAe,EAAE,eAqH7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,eA4B3B,CAAC"}
1
+ {"version":3,"file":"compress.command.d.ts","sourceRoot":"","sources":["../../src/commands/compress.command.ts"],"names":[],"mappings":"AASA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA8FhC,eAAO,MAAM,eAAe,EAAE,eA0H7B,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,aAAa,EAAE,eA4B3B,CAAC"}
@@ -38,6 +38,31 @@ function resolveContentType(raw) {
38
38
  // legitimately have nothing to compress). Above it, a silent `−0%` re-emit is
39
39
  // the opposite of the tool's purpose — nudge the user toward `--type`.
40
40
  const PASSTHROUGH_HINT_MIN_BYTES = 128;
41
+ /**
42
+ * The no-win / passthrough test, shared by the `--json` envelope and the human
43
+ * text banner: a Passthrough strategy or a non-positive saving means the engine
44
+ * declined to compress and `compressed` is the verbatim original. Keeping it in
45
+ * one place is what makes the two output paths agree on fidelity.
46
+ */
47
+ function isNoWin(result) {
48
+ return result.strategy === ECompressionStrategy.Passthrough || result.savings.saved <= 0;
49
+ }
50
+ /**
51
+ * The fidelity label for the human text path, mirroring the `fidelity` field
52
+ * the `--json` envelope surfaces: `passthrough` for a no-win re-emit, `lossy`
53
+ * for a reduction that drops detail (markdown line-omission or the code
54
+ * outline), and `lossless` otherwise. Same classification as `--json` — the
55
+ * text path just names the no-win case explicitly instead of via a separate
56
+ * `passthrough` boolean, so an agent reading stderr sees the fidelity at a
57
+ * glance.
58
+ */
59
+ function textFidelity(result) {
60
+ if (isNoWin(result))
61
+ return 'passthrough';
62
+ if (result.strategy === ECompressionStrategy.Code || result.lossy)
63
+ return 'lossy';
64
+ return 'lossless';
65
+ }
41
66
  function readInput(args) {
42
67
  const positional = args.positional[0];
43
68
  const useStdin = flagBool(args, 'stdin') || positional === undefined || positional === '-';
@@ -133,7 +158,7 @@ export const compressCommand = {
133
158
  // VERBATIM original as `compressed`. Echoing it back inside the JSON
134
159
  // envelope (plus scaffold) costs more tokens than the input. Signal
135
160
  // passthrough and omit the duplicated content — the caller still has it.
136
- const noWin = result.strategy === ECompressionStrategy.Passthrough || result.savings.saved <= 0;
161
+ const noWin = isNoWin(result);
137
162
  const payload = noWin
138
163
  ? { ...base, passthrough: true, inputBytes: Buffer.byteLength(content, 'utf8') }
139
164
  : { ...base, compressed: result.compressed };
@@ -143,6 +168,11 @@ export const compressCommand = {
143
168
  process.stdout.write(result.compressed + '\n');
144
169
  const cached = result.ccrKey ? ` · original cached as ${result.ccrKey} (shrk expand ${result.ccrKey})` : '';
145
170
  process.stderr.write(`${result.strategy}: ~${result.savings.before} → ~${result.savings.after} tokens (−${pct}%, est.)${cached}\n`);
171
+ // Fidelity parity with `--json`: print the same classification the JSON
172
+ // envelope carries as a one-line banner so an agent reading only stderr can
173
+ // see the output was lossy — and see the no-win `passthrough` case named
174
+ // explicitly rather than inferred from a bare `−0%` ratio.
175
+ process.stderr.write(`fidelity: ${textFidelity(result)}\n`);
146
176
  // Fidelity banner: the code outline elides function bodies, so it is a
147
177
  // footgun as a cheaper Read. Say so loudly — same failure mode as a lossy
148
178
  // result that doesn't *look* lossy.
@@ -1 +1 @@
1
- {"version":3,"file":"gate.command.d.ts","sourceRoot":"","sources":["../../src/commands/gate.command.ts"],"names":[],"mappings":"AAaA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,EAAE,eAsMzB,CAAC"}
1
+ {"version":3,"file":"gate.command.d.ts","sourceRoot":"","sources":["../../src/commands/gate.command.ts"],"names":[],"mappings":"AAcA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAGhC;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,WAAW,EAAE,eAwNzB,CAAC"}
@@ -1,4 +1,5 @@
1
1
  import { QualityGateReportStore, renderGateReportMarkdown, runQualityGates, } from '@shrkcrft/quality-gates';
2
+ import { ArchReportStore, runArchCheck } from '@shrkcrft/architecture-guard';
2
3
  import { inspectSharkcraft, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
3
4
  import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
4
5
  import * as nodePath from 'node:path';
@@ -19,7 +20,7 @@ import { asJson, header, kv } from "../output/format-output.js";
19
20
  export const gateCommand = {
20
21
  name: 'gate',
21
22
  description: 'Aggregator: runs the code-intelligence quality gates (graph freshness, architecture, impact-since-ref) and reports a single pass/fail.',
22
- usage: 'shrk gate [--since <gitref>] [--changed-only] [--staged] [--files a,b,c] [--fail-on critical,high] [--arch-all] [--disable arch,impact,policy,knowledge-symbol,api-diff] [--api-baseline <path>] [--no-fail-on-breaking] [--strict] [--no-persist] [--json] [--markdown] [--output <path>]\n (the arch gate is baseline-relative once a baseline is frozen — fails only on NEW errors; with no baseline it warns on errors rather than going perpetually red — --arch-all fails on total, --strict escalates the warn)\n (--changed-only / --staged / --files / --since scope the wiring + policy + knowledge-symbol gates to the changeset; they also drive the impact gate — --since diffs the gitref, the others analyze the changed-file set)\n shrk gate scaffold-ci [--provider github|generic] [--force] [--json]\n shrk gate scaffold-hook [--provider husky|raw] [--force] [--json]',
23
+ usage: 'shrk gate [--since <gitref>] [--changed-only] [--staged] [--files a,b,c] [--fail-on critical,high] [--arch-all] [--disable arch,impact,policy,knowledge-symbol,api-diff] [--api-baseline <path>] [--no-fail-on-breaking] [--strict] [--no-persist] [--json] [--markdown] [--output <path>]\n (the arch gate is change-scoped once a baseline is frozen — a NEW error blocks only when the working diff vs HEAD touched its origin file; drift in untouched files is informational — --arch-all fails on total, --strict escalates the warn)\n (--changed-only / --staged / --files / --since scope the wiring + policy + knowledge-symbol gates to the changeset; they also drive the impact gate — --since diffs the gitref, the others analyze the changed-file set)\n shrk gate scaffold-ci [--provider github|generic] [--force] [--json]\n shrk gate scaffold-hook [--provider husky|raw] [--force] [--json]\n shrk gate baseline --refreeze [--json] (operational reset: re-freeze the arch baseline to the current state — NOT the change-scoped gate)',
23
24
  async run(args) {
24
25
  if (args.positional[0] === 'scaffold-ci') {
25
26
  const sliced = { ...args, positional: args.positional.slice(1) };
@@ -29,6 +30,10 @@ export const gateCommand = {
29
30
  const sliced = { ...args, positional: args.positional.slice(1) };
30
31
  return runGateScaffoldHook(sliced);
31
32
  }
33
+ if (args.positional[0] === 'baseline') {
34
+ const sliced = { ...args, positional: args.positional.slice(1) };
35
+ return runGateBaseline(sliced);
36
+ }
32
37
  const cwd = resolveCwd(args);
33
38
  const wantJson = flagBool(args, 'json');
34
39
  const wantMarkdown = flagBool(args, 'markdown');
@@ -101,6 +106,17 @@ export const gateCommand = {
101
106
  const scopeOpts = wantChangedScope
102
107
  ? { changedOnly: true, changedFiles: changedFiles ?? [] }
103
108
  : {};
109
+ // §3.1 — the architecture gate's "NEW" is ALWAYS change-scoped: a NEW error
110
+ // means one introduced by the working change (diff vs HEAD), never drift
111
+ // against a frozen (possibly months-old) baseline in a file the change never
112
+ // touched. When an explicit scope flag is passed we reuse its resolution;
113
+ // otherwise we default to the worktree diff vs HEAD — empty in a clean tree,
114
+ // so pre-existing baseline drift stays informational and can't red the gate
115
+ // on its own. `--arch-all` (baselineRelative:false) ignores this and fails
116
+ // on total errors, keeping a clean-tree CI demand expressible.
117
+ const archChangedFiles = wantChangedScope
118
+ ? changedFiles ?? []
119
+ : resolveChangedFiles({ projectRoot: cwd, includeWorktree: true }).files;
104
120
  // Knowledge symbol-ref integrity needs the loaded knowledge entries. The
105
121
  // inspection is async, so we build it here and inject it; the gate stays
106
122
  // synchronous and resolves the code graph itself. Best-effort — a failed
@@ -116,7 +132,10 @@ export const gateCommand = {
116
132
  }
117
133
  const report = runQualityGates({
118
134
  projectRoot: cwd,
119
- ...(archAll ? { arch: { baselineRelative: false } } : {}),
135
+ arch: {
136
+ ...(archAll ? { baselineRelative: false } : {}),
137
+ changedFiles: archChangedFiles,
138
+ },
120
139
  wiring: {
121
140
  ...(configError
122
141
  ? { configError }
@@ -400,3 +419,39 @@ async function runGateScaffoldCi(args) {
400
419
  process.stdout.write(`Scaffolded ${provider} CI runner → ${target}\n`);
401
420
  return 0;
402
421
  }
422
+ /**
423
+ * `shrk gate baseline --refreeze` — deliberate operational reset that re-freezes
424
+ * the architecture baseline to the CURRENT state, absorbing accumulated drift so
425
+ * the informational "baseline drift" line resets to zero.
426
+ *
427
+ * This is an operational complement, NOT the gate itself: the gate's blocking
428
+ * verdict is change-scoped (a NEW error blocks only when the working diff touched
429
+ * its origin file), so a stale baseline never reds the gate on its own. Refreeze
430
+ * is only for tidying the informational drift line. It mirrors `shrk arch
431
+ * baseline write`.
432
+ */
433
+ async function runGateBaseline(args) {
434
+ const cwd = resolveCwd(args);
435
+ const wantJson = flagBool(args, 'json');
436
+ const refreeze = flagBool(args, 'refreeze');
437
+ if (!refreeze) {
438
+ process.stderr.write('Usage: shrk gate baseline --refreeze [--json]\n' +
439
+ ' Re-freeze the architecture baseline to the current state (operational reset,\n' +
440
+ ' not the change-scoped gate). Equivalent to `shrk arch baseline write`.\n');
441
+ return 2;
442
+ }
443
+ const report = runArchCheck({ projectRoot: cwd });
444
+ if (report.diagnostics.some((d) => d.includes('code-graph store missing'))) {
445
+ process.stderr.write('Cannot refreeze — graph index missing. Run `shrk graph index` first.\n');
446
+ return 2;
447
+ }
448
+ const store = new ArchReportStore(cwd);
449
+ const snap = store.writeBaseline(report);
450
+ if (wantJson) {
451
+ process.stdout.write(asJson({ ok: true, wrote: store.baselinePath, baseline: snap }) + '\n');
452
+ return 0;
453
+ }
454
+ process.stdout.write(`Architecture baseline re-frozen → ${store.baselinePath}\n`);
455
+ process.stdout.write(kv('violations', `${snap.countsBySeverity.error} error, ${snap.countsBySeverity.warning} warning`) + '\n');
456
+ return 0;
457
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"graph.command.d.ts","sourceRoot":"","sources":["../../src/commands/graph.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA4BhC,eAAO,MAAM,YAAY,EAAE,eAuN1B,CAAC"}
1
+ {"version":3,"file":"graph.command.d.ts","sourceRoot":"","sources":["../../src/commands/graph.command.ts"],"names":[],"mappings":"AAUA,OAAO,EAKL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAqFhC,eAAO,MAAM,YAAY,EAAE,eAqO1B,CAAC"}
@@ -1,8 +1,63 @@
1
1
  import { mkdirSync, writeFileSync } from 'node:fs';
2
2
  import * as nodePath from 'node:path';
3
3
  import { analyzeImportGraph, buildKnowledgeGraph, findGraphPath, getGraphNode, inspectSharkcraft, } from '@shrkcrft/inspector';
4
- import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
4
+ import { firstUnknownFlag, flagBool, flagString, resolveCwd, } from "../command-registry.js";
5
+ import { ExitCode } from "../exit-codes.js";
5
6
  import { asJson, header, kv } from "../output/format-output.js";
7
+ /**
8
+ * The code-intelligence subverbs that share one arg parser. Each is guarded so
9
+ * an unrecognized/misspelled flag is rejected loudly instead of parsing as a
10
+ * silent `true` that reads as a confident opt-in (a25 §2.1). The allow-set is
11
+ * the UNION of every flag any code-graph subverb reads, plus the global flags,
12
+ * so a real flag is never false-rejected — only a genuinely unknown token is.
13
+ */
14
+ const CODE_GRAPH_SUBVERBS = new Set([
15
+ 'index',
16
+ 'status',
17
+ 'search',
18
+ 'context',
19
+ 'impact',
20
+ 'path',
21
+ 'hubs',
22
+ 'callers',
23
+ 'cycles',
24
+ 'unresolved',
25
+ 'deps',
26
+ ]);
27
+ const CODE_GRAPH_ALLOWED_FLAGS = new Set([
28
+ // code-subverb flags (union across index/status/search/context/impact/path/
29
+ // hubs/callers/cycles/unresolved/deps)
30
+ 'changed',
31
+ 'compact',
32
+ 'depth',
33
+ 'full',
34
+ 'has-unresolved-imports',
35
+ 'include-type-edges',
36
+ 'json',
37
+ 'kind',
38
+ 'limit',
39
+ 'max-depth',
40
+ 'min-size',
41
+ 'mode',
42
+ 'no-bridge',
43
+ 'no-framework',
44
+ 'no-refresh',
45
+ 'path',
46
+ 'since',
47
+ 'table',
48
+ // --watch loop (graph index --watch)
49
+ 'watch',
50
+ 'paths',
51
+ 'debounce',
52
+ 'once',
53
+ // global / meta flags that reach any verb
54
+ 'cwd',
55
+ 'strict',
56
+ 'help',
57
+ 'h',
58
+ 'no-color',
59
+ 'color',
60
+ ]);
6
61
  import { runGraphCallers, runGraphContext, runGraphCycles, runGraphDeps, runGraphHubs, runGraphImpact, runGraphIndex, runGraphPath, runGraphSearch, runGraphStatus, runGraphUnresolved, } from "./graph-code-subverbs.js";
7
62
  const KNOWN_KINDS = [
8
63
  'knowledge',
@@ -26,6 +81,18 @@ export const graphCommand = {
26
81
  // Code-intelligence subverbs (R65) don't need the knowledge graph —
27
82
  // dispatch them before the expensive inspection so they stay fast.
28
83
  const earlySub = args.positional[0];
84
+ // Reject unknown/misspelled flags on the code-graph family BEFORE dispatch:
85
+ // an unrecognized flag must never parse as a silent success (a25 §2.1). Any
86
+ // real flag is in the union allow-set, so this only fires on a genuine typo.
87
+ if (typeof earlySub === 'string' && CODE_GRAPH_SUBVERBS.has(earlySub)) {
88
+ const unknown = firstUnknownFlag(args, CODE_GRAPH_ALLOWED_FLAGS);
89
+ if (unknown) {
90
+ const dash = unknown.length === 1 ? '-' : '--';
91
+ process.stderr.write(`unknown option '${dash}${unknown}' for 'shrk graph ${earlySub}'. ` +
92
+ `Run 'shrk graph --help' for valid flags.\n`);
93
+ return ExitCode.NotVerified;
94
+ }
95
+ }
29
96
  if (earlySub === 'index')
30
97
  return runGraphIndex(args);
31
98
  if (earlySub === 'status')
@@ -0,0 +1,41 @@
1
+ /**
2
+ * `--resolve` support for `shrk registry <name> exists <id>`.
3
+ *
4
+ * The noun an author types is not always the exact registered slug. `--resolve`
5
+ * bridges that vocabulary gap before the existence check so a duplicate guard
6
+ * can't return a false "not declared" for a construct that DOES exist under its
7
+ * canonical id (a25 §2.4). Resolution is deterministic and layered, most
8
+ * author-controlled first:
9
+ *
10
+ * 1. the registry's declared `aliases` map (explicit synonym → canonical),
11
+ * 2. a case-insensitive exact match against the declared ids,
12
+ * 3. singular/plural normalization (`commands` ↔ `command`),
13
+ * 4. suffix strip/append (`button` ↔ `button-command`, `foo` ↔ `foo.tool`).
14
+ *
15
+ * The first layer that lands on a DECLARED id wins; when nothing resolves, the
16
+ * noun is returned unchanged and unmatched (the honest "genuinely not declared"
17
+ * answer). No layer invents an id that isn't in the registry.
18
+ */
19
+ /** How a noun resolved to its canonical registered id (for a truthful report). */
20
+ export declare enum ERegistryResolveVia {
21
+ Identity = "identity",
22
+ Alias = "alias",
23
+ Case = "case-fold",
24
+ SingularPlural = "singular/plural",
25
+ Suffix = "suffix"
26
+ }
27
+ export interface IRegistryResolution {
28
+ /** The canonical id to test for existence (may equal the input noun). */
29
+ readonly canonical: string;
30
+ /** True when `canonical` is actually a declared id in the registry. */
31
+ readonly matched: boolean;
32
+ /** Which normalization layer produced `canonical`. */
33
+ readonly via: ERegistryResolveVia;
34
+ }
35
+ /**
36
+ * Resolve a human/synonym `noun` to a canonical registered id. `declaredIds` is
37
+ * the registry's actual declared id list (so no layer can resolve to something
38
+ * that isn't declared); `aliases` is the registry's optional declared map.
39
+ */
40
+ export declare function resolveRegistryNoun(declaredIds: readonly string[], aliases: Readonly<Record<string, string>> | undefined, noun: string): IRegistryResolution;
41
+ //# sourceMappingURL=registry-resolve.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"registry-resolve.d.ts","sourceRoot":"","sources":["../../src/commands/registry-resolve.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,kFAAkF;AAClF,oBAAY,mBAAmB;IAC7B,QAAQ,aAAa;IACrB,KAAK,UAAU;IACf,IAAI,cAAc;IAClB,cAAc,oBAAoB;IAClC,MAAM,WAAW;CAClB;AAED,MAAM,WAAW,mBAAmB;IAClC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,uEAAuE;IACvE,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,sDAAsD;IACtD,QAAQ,CAAC,GAAG,EAAE,mBAAmB,CAAC;CACnC;AAYD;;;;GAIG;AACH,wBAAgB,mBAAmB,CACjC,WAAW,EAAE,SAAS,MAAM,EAAE,EAC9B,OAAO,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAAG,SAAS,EACrD,IAAI,EAAE,MAAM,GACX,mBAAmB,CAmDrB"}
@@ -0,0 +1,89 @@
1
+ /**
2
+ * `--resolve` support for `shrk registry <name> exists <id>`.
3
+ *
4
+ * The noun an author types is not always the exact registered slug. `--resolve`
5
+ * bridges that vocabulary gap before the existence check so a duplicate guard
6
+ * can't return a false "not declared" for a construct that DOES exist under its
7
+ * canonical id (a25 §2.4). Resolution is deterministic and layered, most
8
+ * author-controlled first:
9
+ *
10
+ * 1. the registry's declared `aliases` map (explicit synonym → canonical),
11
+ * 2. a case-insensitive exact match against the declared ids,
12
+ * 3. singular/plural normalization (`commands` ↔ `command`),
13
+ * 4. suffix strip/append (`button` ↔ `button-command`, `foo` ↔ `foo.tool`).
14
+ *
15
+ * The first layer that lands on a DECLARED id wins; when nothing resolves, the
16
+ * noun is returned unchanged and unmatched (the honest "genuinely not declared"
17
+ * answer). No layer invents an id that isn't in the registry.
18
+ */
19
+ /** How a noun resolved to its canonical registered id (for a truthful report). */
20
+ export var ERegistryResolveVia;
21
+ (function (ERegistryResolveVia) {
22
+ ERegistryResolveVia["Identity"] = "identity";
23
+ ERegistryResolveVia["Alias"] = "alias";
24
+ ERegistryResolveVia["Case"] = "case-fold";
25
+ ERegistryResolveVia["SingularPlural"] = "singular/plural";
26
+ ERegistryResolveVia["Suffix"] = "suffix";
27
+ })(ERegistryResolveVia || (ERegistryResolveVia = {}));
28
+ function singularPluralVariants(noun) {
29
+ const out = [];
30
+ const lower = noun.toLowerCase();
31
+ if (lower.endsWith('ies'))
32
+ out.push(noun.slice(0, -3) + 'y');
33
+ if (lower.endsWith('s'))
34
+ out.push(noun.slice(0, -1));
35
+ if (lower.endsWith('y'))
36
+ out.push(noun.slice(0, -1) + 'ies');
37
+ out.push(noun + 's');
38
+ return out;
39
+ }
40
+ /**
41
+ * Resolve a human/synonym `noun` to a canonical registered id. `declaredIds` is
42
+ * the registry's actual declared id list (so no layer can resolve to something
43
+ * that isn't declared); `aliases` is the registry's optional declared map.
44
+ */
45
+ export function resolveRegistryNoun(declaredIds, aliases, noun) {
46
+ const idSet = new Set(declaredIds);
47
+ // 1. Author-declared alias map — highest priority, honored even if it points
48
+ // at an id the current scan didn't find (the author asserted the mapping).
49
+ const aliased = aliases?.[noun];
50
+ if (aliased !== undefined) {
51
+ return { canonical: aliased, matched: idSet.has(aliased), via: ERegistryResolveVia.Alias };
52
+ }
53
+ // Exact identity.
54
+ if (idSet.has(noun)) {
55
+ return { canonical: noun, matched: true, via: ERegistryResolveVia.Identity };
56
+ }
57
+ const lower = noun.toLowerCase();
58
+ // 2. Case-insensitive exact.
59
+ const ciHit = declaredIds.find((d) => d.toLowerCase() === lower);
60
+ if (ciHit !== undefined) {
61
+ return { canonical: ciHit, matched: true, via: ERegistryResolveVia.Case };
62
+ }
63
+ // 3. Singular/plural.
64
+ for (const variant of singularPluralVariants(noun)) {
65
+ const v = variant.toLowerCase();
66
+ const hit = declaredIds.find((d) => d.toLowerCase() === v);
67
+ if (hit !== undefined) {
68
+ return { canonical: hit, matched: true, via: ERegistryResolveVia.SingularPlural };
69
+ }
70
+ }
71
+ // 4. Suffix strip/append. A declared id whose trailing `-`/`.`/`_` segment,
72
+ // once stripped, equals the noun (`button` ← `button-command`); or the noun
73
+ // plus a suffix another declared id carries yields a declared id.
74
+ const suffixStripped = declaredIds.find((d) => {
75
+ const stripped = d.replace(/[-_.][a-z0-9]+$/i, '');
76
+ return stripped.toLowerCase() === lower && stripped.toLowerCase() !== d.toLowerCase();
77
+ });
78
+ if (suffixStripped !== undefined) {
79
+ return { canonical: suffixStripped, matched: true, via: ERegistryResolveVia.Suffix };
80
+ }
81
+ for (const d of declaredIds) {
82
+ const m = d.match(/([-_.][a-z0-9]+)$/i);
83
+ if (m && `${lower}${m[1].toLowerCase()}` === d.toLowerCase()) {
84
+ return { canonical: d, matched: true, via: ERegistryResolveVia.Suffix };
85
+ }
86
+ }
87
+ // Nothing resolved — the honest unmatched identity.
88
+ return { canonical: noun, matched: false, via: ERegistryResolveVia.Identity };
89
+ }
@@ -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;AAGhC,eAAO,MAAM,wBAAwB,EAAE,eAsCtC,CAAC;AAsIF,eAAO,MAAM,eAAe,EAAE,eA4B7B,CAAC"}
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"}
@@ -16,7 +16,9 @@
16
16
  import { buildRegistryLifecycleReport, renderRegistryLifecycleReportText, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
17
17
  import { scanRegistry, registryExists, registryWhere, } from '@shrkcrft/boundaries';
18
18
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
19
+ import { ExitCode } from "../exit-codes.js";
19
20
  import { asJson } from "../output/format-output.js";
21
+ import { resolveRegistryNoun } from "./registry-resolve.js";
20
22
  export const registryLifecycleCommand = {
21
23
  name: 'lifecycle',
22
24
  description: 'Scan the workspace for register/remove symmetry. Read-only.',
@@ -48,7 +50,14 @@ export const registryLifecycleCommand = {
48
50
  ...(scope ? { scope } : {}),
49
51
  ...(skipDirs ? { skipDirs } : {}),
50
52
  });
51
- const exit = report.timedOut ? 2 : report.missingRemovers.length === 0 ? 0 : 1;
53
+ // Honest exit-code contract (a25 §1 / §2.5): a wedged/timed-out scan OR a
54
+ // scope with zero registrations to check is NOT verified (`2`), never a
55
+ // green `0` an agent's `&& next` would march past.
56
+ const exit = report.timedOut || report.registersFound === 0
57
+ ? ExitCode.NotVerified
58
+ : report.missingRemovers.length === 0
59
+ ? ExitCode.VerifiedPass
60
+ : ExitCode.Failure;
52
61
  if (flagBool(args, 'json')) {
53
62
  process.stdout.write(asJson(report) + '\n');
54
63
  return exit;
@@ -123,11 +132,16 @@ async function runRegistryInventory(args, name) {
123
132
  process.stderr.write('Pass at most one of --fail-if-taken / --fail-if-missing.\n');
124
133
  return 2;
125
134
  }
126
- // `--resolve` maps a human noun to the canonical registered id via the
127
- // registry's `aliases` map before the existence test — so a duplicate guard
128
- // can't return a false "free" on a synonym of an already-taken slug.
135
+ // `--resolve` maps a human noun to the canonical registered id before the
136
+ // existence test — via the registry's declared `aliases` map AND generic
137
+ // normalization (case-fold, singular/plural, suffix strip/append) so a
138
+ // duplicate guard can't return a false "free" on a synonym of an
139
+ // already-taken slug (a25 §2.4).
129
140
  const doResolve = flagBool(args, 'resolve');
130
- const canonical = doResolve ? (decl.aliases?.[id] ?? id) : id;
141
+ const resolution = doResolve
142
+ ? resolveRegistryNoun(inventory.entries.map((e) => e.id), decl.aliases, id)
143
+ : undefined;
144
+ const canonical = resolution ? resolution.canonical : id;
131
145
  const resolved = canonical !== id;
132
146
  const exists = registryExists(inventory, canonical);
133
147
  // Exit-code convention:
@@ -140,14 +154,15 @@ async function runRegistryInventory(args, name) {
140
154
  process.stdout.write(asJson({
141
155
  name: inventory.name,
142
156
  id,
143
- ...(resolved ? { resolvedId: canonical } : {}),
157
+ ...(resolved ? { resolvedId: canonical, resolvedVia: resolution?.via } : {}),
144
158
  exists,
145
159
  exitCode: code,
146
160
  }) + '\n');
147
161
  return code;
148
162
  }
149
- if (resolved)
150
- process.stdout.write(`resolved "${id}" → "${canonical}" (alias)\n`);
163
+ if (resolved) {
164
+ process.stdout.write(`resolved "${id}" → "${canonical}" (${resolution?.via})\n`);
165
+ }
151
166
  process.stdout.write(`${exists ? 'yes' : 'no'} — "${canonical}" is ${exists ? 'declared' : 'NOT declared'} in registry "${inventory.name}".\n`);
152
167
  return code;
153
168
  }
@@ -1,3 +1,23 @@
1
+ import type { IReusePrimitive } from '@shrkcrft/core';
1
2
  import { type ICommandHandler } from '../command-registry.js';
3
+ /** A weak (below-confidence) candidate offered as a did-you-mean, never as an answer. */
4
+ interface IReuseSuggestion {
5
+ symbol: string;
6
+ score: number;
7
+ confidence: number;
8
+ matched: readonly string[];
9
+ roles: readonly string[];
10
+ }
11
+ /**
12
+ * Rank ALL primitives by the matcher's own score (descending; ties broken by
13
+ * symbol name so the order is deterministic), then return the top-`k` as scored
14
+ * did-you-mean suggestions. Pure — no graph, no IO — so it is directly
15
+ * unit-testable. When every candidate scores 0 (a nonsense intent that shares no
16
+ * term) the result is the alphabetically-first `k` primitives, each with
17
+ * `score: 0`; the caller states "no candidate shares any term" in that case
18
+ * rather than dumping the whole catalog.
19
+ */
20
+ export declare function rankReuseSuggestions(primitives: readonly IReusePrimitive[], tokens: readonly string[], k: number): IReuseSuggestion[];
2
21
  export declare const reuseCommand: ICommandHandler;
22
+ export {};
3
23
  //# sourceMappingURL=reuse.command.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"reuse.command.d.ts","sourceRoot":"","sources":["../../src/commands/reuse.command.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA2GhC,eAAO,MAAM,YAAY,EAAE,eA0N1B,CAAC"}
1
+ {"version":3,"file":"reuse.command.d.ts","sourceRoot":"","sources":["../../src/commands/reuse.command.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACtD,OAAO,EAIL,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAkGhC,yFAAyF;AACzF,UAAU,gBAAgB;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CAC1B;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,UAAU,EAAE,SAAS,eAAe,EAAE,EACtC,MAAM,EAAE,SAAS,MAAM,EAAE,EACzB,CAAC,EAAE,MAAM,GACR,gBAAgB,EAAE,CAapB;AAED,eAAO,MAAM,YAAY,EAAE,eA6Q1B,CAAC"}
@@ -60,17 +60,47 @@ function isConfidentMatch(detail, queryTokenCount) {
60
60
  return queryTokenCount <= 1;
61
61
  }
62
62
  const INDEX_RE = /(^|\/)index\.[cm]?[jt]sx?$/;
63
+ /**
64
+ * Rank ALL primitives by the matcher's own score (descending; ties broken by
65
+ * symbol name so the order is deterministic), then return the top-`k` as scored
66
+ * did-you-mean suggestions. Pure — no graph, no IO — so it is directly
67
+ * unit-testable. When every candidate scores 0 (a nonsense intent that shares no
68
+ * term) the result is the alphabetically-first `k` primitives, each with
69
+ * `score: 0`; the caller states "no candidate shares any term" in that case
70
+ * rather than dumping the whole catalog.
71
+ */
72
+ export function rankReuseSuggestions(primitives, tokens, k) {
73
+ const cap = Math.max(1, Math.floor(k));
74
+ return primitives
75
+ .map((p) => ({ p, detail: scorePrimitive(p, tokens) }))
76
+ .sort((a, b) => b.detail.score - a.detail.score || a.p.symbol.localeCompare(b.p.symbol))
77
+ .slice(0, cap)
78
+ .map(({ p, detail }) => ({
79
+ symbol: p.symbol,
80
+ score: detail.score,
81
+ confidence: tokens.length === 0 ? 0 : detail.matched.length / tokens.length,
82
+ matched: detail.matched,
83
+ roles: p.roles,
84
+ }));
85
+ }
63
86
  export const reuseCommand = {
64
87
  name: 'reuse',
65
88
  description: 'Intent → the canonical primitive to reuse. Matches your intent against configured reusePrimitives[], then resolves the symbol in the code graph to its declaration, public import path, sibling exports, and real consumer files to copy. Deterministic; no AI.',
66
- usage: 'shrk reuse "<what I want to build>" [--limit N] [--json]',
89
+ usage: 'shrk reuse "<what I want to build>" [--limit N] [--all] [--json]',
90
+ booleanFlags: new Set(['json', 'all']),
67
91
  async run(args) {
68
92
  const cwd = resolveCwd(args);
69
93
  const wantJson = flagBool(args, 'json');
70
- const limit = flagNumber(args, 'limit') ?? 3;
94
+ const wantAll = flagBool(args, 'all');
95
+ // `--limit N` caps both the confident results (historic default 3) and the
96
+ // did-you-mean suggestion list. When omitted, suggestions default to 5 (a
97
+ // couple more than results — the point of a did-you-mean is a short menu).
98
+ const limitFlag = flagNumber(args, 'limit');
99
+ const limit = limitFlag ?? 3;
100
+ const suggestK = limitFlag ?? 5;
71
101
  const intent = args.positional.join(' ').trim();
72
102
  if (!intent) {
73
- process.stderr.write('Usage: shrk reuse "<what I want to build>" [--limit N] [--json]\n');
103
+ process.stderr.write('Usage: shrk reuse "<what I want to build>" [--limit N] [--all] [--json]\n');
74
104
  return 2;
75
105
  }
76
106
  const loaded = await resolveProjectConfig(cwd);
@@ -115,17 +145,39 @@ export const reuseCommand = {
115
145
  const ranked = confident.slice(0, Math.max(1, limit));
116
146
  const store = new GraphStore(cwd);
117
147
  const api = store.exists() ? GraphQueryApi.fromStore(cwd) : null;
118
- // Zero keyword overlap: nothing matched at all surface available roles.
148
+ // Zero keyword overlap: nothing matched at all. Rather than dump the entire
149
+ // declared catalog (dozens of lines an agent must re-read), rank ALL
150
+ // candidates and surface the nearest top-K by name — every score is 0 here,
151
+ // so this is an alphabetized short menu, stated as such. The full catalog is
152
+ // available only behind an explicit `--all`.
119
153
  if (scored.length === 0) {
154
+ const suggestions = rankReuseSuggestions(primitives, tokens, suggestK);
120
155
  const roles = [...new Set(primitives.flatMap((p) => p.roles))].sort();
121
156
  if (wantJson) {
122
- process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, results: [], availableRoles: roles, ...planeJson }) + '\n');
157
+ process.stdout.write(asJson({
158
+ schema: 'sharkcraft.reuse/v1',
159
+ intent,
160
+ confident: false,
161
+ results: [],
162
+ suggestions,
163
+ ...(wantAll ? { availableRoles: roles } : {}),
164
+ ...planeJson,
165
+ }) + '\n');
123
166
  return 0;
124
167
  }
125
168
  process.stdout.write(header(`Reuse: "${intent}"`));
126
- process.stdout.write(' No primitive matched. Available roles:\n');
127
- for (const r of roles.slice(0, 40))
128
- process.stdout.write(` • ${r}\n`);
169
+ if (wantAll) {
170
+ process.stdout.write(' No primitive matched no candidate shares any term — showing full catalog:\n');
171
+ for (const r of roles.slice(0, 40))
172
+ process.stdout.write(` • ${r}\n`);
173
+ }
174
+ else {
175
+ process.stdout.write(' No strong match — no candidate shares any term with the intent.\n' +
176
+ ' Nearest primitives (pass --all for the full catalog):\n');
177
+ for (const s of suggestions) {
178
+ process.stdout.write(` • ${s.symbol} (score ${s.score}; roles: ${s.roles.join(', ') || '—'})\n`);
179
+ }
180
+ }
129
181
  writePlaneNotes();
130
182
  return 0;
131
183
  }
@@ -133,23 +185,41 @@ export const reuseCommand = {
133
185
  // entry): below the confidence floor. A miss must look like a miss — never
134
186
  // return the nearest collision as a confident answer. Offer did-you-mean.
135
187
  if (ranked.length === 0) {
136
- const didYouMean = scored.slice(0, 5).map((x) => ({
188
+ // Rank the weakly-overlapping candidates (score > 0) and cap at K — never
189
+ // the whole catalog. `suggestions` and the legacy `didYouMean` alias carry
190
+ // the same scored rows; `--all` additionally dumps every declared role.
191
+ const suggestions = scored.slice(0, Math.max(1, suggestK)).map((x) => ({
137
192
  symbol: x.p.symbol,
138
193
  score: x.detail.score,
139
194
  confidence: confidenceOf(x.detail.matched.length),
140
195
  matched: x.detail.matched,
141
196
  roles: x.p.roles,
142
197
  }));
198
+ const roles = [...new Set(primitives.flatMap((p) => p.roles))].sort();
143
199
  if (wantJson) {
144
- process.stdout.write(asJson({ schema: 'sharkcraft.reuse/v1', intent, confident: false, results: [], didYouMean, ...planeJson }) + '\n');
200
+ process.stdout.write(asJson({
201
+ schema: 'sharkcraft.reuse/v1',
202
+ intent,
203
+ confident: false,
204
+ results: [],
205
+ suggestions,
206
+ didYouMean: suggestions,
207
+ ...(wantAll ? { availableRoles: roles } : {}),
208
+ ...planeJson,
209
+ }) + '\n');
145
210
  return 0;
146
211
  }
147
212
  process.stdout.write(header(`Reuse: "${intent}"`));
148
213
  process.stdout.write(' No confident match — the intent only weakly overlaps existing primitives.\n' +
149
214
  ' Did you mean (weak, verify before reusing):\n');
150
- for (const s of didYouMean) {
215
+ for (const s of suggestions) {
151
216
  process.stdout.write(` • ${s.symbol} (score ${s.score}, ${Math.round(s.confidence * 100)}% of intent; matched: ${s.matched.join(', ') || '—'})\n`);
152
217
  }
218
+ if (wantAll) {
219
+ process.stdout.write(' Full catalog (all declared roles):\n');
220
+ for (const r of roles.slice(0, 40))
221
+ process.stdout.write(` • ${r}\n`);
222
+ }
153
223
  writePlaneNotes();
154
224
  return 0;
155
225
  }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The canonical exit-code contract for every gate / verify / check verb.
3
+ *
4
+ * alpha.24 and alpha.25 made the STDOUT verdicts honest ("not verified",
5
+ * "degraded", "0 rules evaluated", "this is not a pass") but left the exit
6
+ * code returning `0` over those same unverified paths. An agent almost never
7
+ * parses the banner — it chains `shrk <cmd> && <next>` on the *exit code*, so a
8
+ * `0` over a "not verified" result marches straight past the gate. This module
9
+ * is the single source of truth so a chained gate can finally tell apart
10
+ * "passed", "failed", and "never ran".
11
+ *
12
+ * 0 VerifiedPass — checks ran over a NON-EMPTY scope and passed. Never
13
+ * returned when zero units were evaluated.
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".
21
+ *
22
+ * The `gen --typecheck` pre-write gate already refuses-to-nonzero rather than
23
+ * emit an unverified artifact; this generalizes that instinct across the gate
24
+ * surface, adding the third code so "unverified" is distinguishable from
25
+ * "broken".
26
+ */
27
+ export declare enum ExitCode {
28
+ VerifiedPass = 0,
29
+ Failure = 1,
30
+ NotVerified = 2
31
+ }
32
+ /**
33
+ * Promote a NotVerified (`2`) exit into a Failure-class nonzero (`1`) when the
34
+ * caller opted into `--strict`. This is the one switch an agent flips to make a
35
+ * hard CI gate treat "unverified" as a failure. Any other code passes through
36
+ * unchanged (a real pass stays `0`, a real failure stays `1`). Applied globally
37
+ * in {@link runCli} after the handler returns, so every gate/verify verb honors
38
+ * `--strict` uniformly without threading the flag through each call site.
39
+ */
40
+ export declare function promoteForStrict(code: number, strict: boolean): number;
41
+ /**
42
+ * True when the argv carries a global `--strict` (bare or `--strict=<level>`).
43
+ * `--strict` is also an established per-command flag (e.g. `check --strict`,
44
+ * `doctor --strict=warnings`) whose local meaning is preserved — this global
45
+ * layer only adds the NotVerified→Failure promotion on top, and only affects a
46
+ * command that actually returned `2`.
47
+ */
48
+ export declare function argvHasStrict(argv: readonly string[]): boolean;
49
+ //# sourceMappingURL=exit-codes.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The canonical exit-code contract for every gate / verify / check verb.
3
+ *
4
+ * alpha.24 and alpha.25 made the STDOUT verdicts honest ("not verified",
5
+ * "degraded", "0 rules evaluated", "this is not a pass") but left the exit
6
+ * code returning `0` over those same unverified paths. An agent almost never
7
+ * parses the banner — it chains `shrk <cmd> && <next>` on the *exit code*, so a
8
+ * `0` over a "not verified" result marches straight past the gate. This module
9
+ * is the single source of truth so a chained gate can finally tell apart
10
+ * "passed", "failed", and "never ran".
11
+ *
12
+ * 0 VerifiedPass — checks ran over a NON-EMPTY scope and passed. Never
13
+ * returned when zero units were evaluated.
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".
21
+ *
22
+ * The `gen --typecheck` pre-write gate already refuses-to-nonzero rather than
23
+ * emit an unverified artifact; this generalizes that instinct across the gate
24
+ * surface, adding the third code so "unverified" is distinguishable from
25
+ * "broken".
26
+ */
27
+ export var ExitCode;
28
+ (function (ExitCode) {
29
+ ExitCode[ExitCode["VerifiedPass"] = 0] = "VerifiedPass";
30
+ ExitCode[ExitCode["Failure"] = 1] = "Failure";
31
+ ExitCode[ExitCode["NotVerified"] = 2] = "NotVerified";
32
+ })(ExitCode || (ExitCode = {}));
33
+ /**
34
+ * Promote a NotVerified (`2`) exit into a Failure-class nonzero (`1`) when the
35
+ * caller opted into `--strict`. This is the one switch an agent flips to make a
36
+ * hard CI gate treat "unverified" as a failure. Any other code passes through
37
+ * unchanged (a real pass stays `0`, a real failure stays `1`). Applied globally
38
+ * in {@link runCli} after the handler returns, so every gate/verify verb honors
39
+ * `--strict` uniformly without threading the flag through each call site.
40
+ */
41
+ export function promoteForStrict(code, strict) {
42
+ if (strict && code === ExitCode.NotVerified)
43
+ return ExitCode.Failure;
44
+ return code;
45
+ }
46
+ /**
47
+ * True when the argv carries a global `--strict` (bare or `--strict=<level>`).
48
+ * `--strict` is also an established per-command flag (e.g. `check --strict`,
49
+ * `doctor --strict=warnings`) whose local meaning is preserved — this global
50
+ * layer only adds the NotVerified→Failure promotion on top, and only affects a
51
+ * command that actually returned `2`.
52
+ */
53
+ export function argvHasStrict(argv) {
54
+ for (const t of argv) {
55
+ if (t === '--')
56
+ break;
57
+ if (t === '--strict' || t.startsWith('--strict='))
58
+ return true;
59
+ }
60
+ return false;
61
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AA6X/B,wBAAgB,aAAa,IAAI,eAAe,CAiY/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CA2BrE;AAqID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.ts"],"names":[],"mappings":";AAEA,OAAO,EACL,eAAe,EAKhB,MAAM,uBAAuB,CAAC;AA8X/B,wBAAgB,aAAa,IAAI,eAAe,CAiY/C;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAiCrE;AAqID;;;;;;;;;;;GAWG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,GAAG,OAAO,CAoDxE"}
package/dist/main.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { loadDotenv } from "./env/load-dotenv.js";
3
3
  import { CommandRegistry, extractGlobalCompress, extractGlobalCwd, parseArgs, } from "./command-registry.js";
4
+ import { argvHasStrict, promoteForStrict } from "./exit-codes.js";
4
5
  import { runCommandWithCompression } from "./output/output-compression.js";
5
6
  import { initCommand } from "./commands/init.command.js";
6
7
  import { inspectCommand } from "./commands/inspect.command.js";
@@ -509,9 +510,15 @@ export function buildRegistry() {
509
510
  export async function runCli(argv) {
510
511
  const usageStart = performance.now();
511
512
  const { cwd: probeCwd, rest: probeArgv } = extractGlobalCwd(argv);
513
+ // Global `--strict` promotes a NotVerified (`2`) verdict to a Failure-class
514
+ // nonzero across every gate/verify verb — one switch to make an "unverified"
515
+ // result fail a hard CI gate. A real pass (`0`) or failure (`1`) is untouched,
516
+ // and each command's own `--strict` semantics (e.g. `check --strict`) still
517
+ // apply beneath this (a25 §1.1).
518
+ const strict = argvHasStrict(argv);
512
519
  let exitCode = 0;
513
520
  try {
514
- exitCode = await runCliInner(argv);
521
+ exitCode = promoteForStrict(await runCliInner(argv), strict);
515
522
  return exitCode;
516
523
  }
517
524
  finally {
@@ -923,6 +930,22 @@ if (isMain ||
923
930
  // test). Commands that re-exec themselves in an isolated child gate on this
924
931
  // so unit tests calling `run()` in-process never spawn a subprocess.
925
932
  process.env.SHRK_CLI = '1';
933
+ // Global broken-pipe containment. When a downstream reader closes early
934
+ // (`shrk registry <name> list | head`, `… | grep`), Node raises `write EPIPE`
935
+ // on stdout from the async write/flush path — with no `error` listener that
936
+ // becomes an uncaught exception and a nonzero exit, forging a false failure on
937
+ // a happy-path query whose data was fine (a25 §2.2). Swallowing EPIPE here (a
938
+ // no-op listener) turns it back into a benign early-close: the command keeps
939
+ // its REAL exit code, and nothing is written to the dead pipe. Registered once
940
+ // at startup so every verb that streams a list is covered.
941
+ const swallowPipeError = (err) => {
942
+ if (err && err.code === 'EPIPE')
943
+ return;
944
+ // Any other stdio error (ENOSPC, …) is unrecoverable mid-write; there is
945
+ // nothing safe to print, so contain it rather than crash the shutdown path.
946
+ };
947
+ process.stdout.on('error', swallowPipeError);
948
+ process.stderr.on('error', swallowPipeError);
926
949
  loadDotenv(process.cwd());
927
950
  const argv = process.argv.slice(2);
928
951
  const cleanShutdown = async (code) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shrkcrft/cli",
3
- "version": "0.1.0-alpha.25",
3
+ "version": "0.1.0-alpha.26",
4
4
  "description": "SharkCraft CLI (`shrk`): structured project intelligence for AI coding agents.",
5
5
  "license": "MIT",
6
6
  "author": "SharkCraft contributors",
@@ -47,38 +47,38 @@
47
47
  "typecheck": "tsc --noEmit -p tsconfig.json"
48
48
  },
49
49
  "dependencies": {
50
- "@shrkcrft/core": "^0.1.0-alpha.25",
51
- "@shrkcrft/compress": "^0.1.0-alpha.25",
52
- "@shrkcrft/config": "^0.1.0-alpha.25",
53
- "@shrkcrft/workspace": "^0.1.0-alpha.25",
54
- "@shrkcrft/knowledge": "^0.1.0-alpha.25",
55
- "@shrkcrft/context": "^0.1.0-alpha.25",
56
- "@shrkcrft/rules": "^0.1.0-alpha.25",
57
- "@shrkcrft/paths": "^0.1.0-alpha.25",
58
- "@shrkcrft/templates": "^0.1.0-alpha.25",
59
- "@shrkcrft/plugin-api": "^0.1.0-alpha.25",
60
- "@shrkcrft/dashboard": "^0.1.0-alpha.25",
61
- "@shrkcrft/dashboard-api": "^0.1.0-alpha.25",
62
- "@shrkcrft/pipelines": "^0.1.0-alpha.25",
63
- "@shrkcrft/presets": "^0.1.0-alpha.25",
64
- "@shrkcrft/boundaries": "^0.1.0-alpha.25",
65
- "@shrkcrft/graph": "^0.1.0-alpha.25",
66
- "@shrkcrft/rule-graph": "^0.1.0-alpha.25",
67
- "@shrkcrft/structural-search": "^0.1.0-alpha.25",
68
- "@shrkcrft/impact-engine": "^0.1.0-alpha.25",
69
- "@shrkcrft/context-planner": "^0.1.0-alpha.25",
70
- "@shrkcrft/architecture-guard": "^0.1.0-alpha.25",
71
- "@shrkcrft/framework-scanners": "^0.1.0-alpha.25",
72
- "@shrkcrft/api-surface-diff": "^0.1.0-alpha.25",
73
- "@shrkcrft/quality-gates": "^0.1.0-alpha.25",
74
- "@shrkcrft/migrate": "^0.1.0-alpha.25",
75
- "@shrkcrft/generator": "^0.1.0-alpha.25",
76
- "@shrkcrft/importer": "^0.1.0-alpha.25",
77
- "@shrkcrft/inspector": "^0.1.0-alpha.25",
78
- "@shrkcrft/ai": "^0.1.0-alpha.25",
79
- "@shrkcrft/embeddings": "^0.1.0-alpha.25",
80
- "@shrkcrft/shared": "^0.1.0-alpha.25",
81
- "@shrkcrft/mcp-server": "^0.1.0-alpha.25",
50
+ "@shrkcrft/core": "^0.1.0-alpha.26",
51
+ "@shrkcrft/compress": "^0.1.0-alpha.26",
52
+ "@shrkcrft/config": "^0.1.0-alpha.26",
53
+ "@shrkcrft/workspace": "^0.1.0-alpha.26",
54
+ "@shrkcrft/knowledge": "^0.1.0-alpha.26",
55
+ "@shrkcrft/context": "^0.1.0-alpha.26",
56
+ "@shrkcrft/rules": "^0.1.0-alpha.26",
57
+ "@shrkcrft/paths": "^0.1.0-alpha.26",
58
+ "@shrkcrft/templates": "^0.1.0-alpha.26",
59
+ "@shrkcrft/plugin-api": "^0.1.0-alpha.26",
60
+ "@shrkcrft/dashboard": "^0.1.0-alpha.26",
61
+ "@shrkcrft/dashboard-api": "^0.1.0-alpha.26",
62
+ "@shrkcrft/pipelines": "^0.1.0-alpha.26",
63
+ "@shrkcrft/presets": "^0.1.0-alpha.26",
64
+ "@shrkcrft/boundaries": "^0.1.0-alpha.26",
65
+ "@shrkcrft/graph": "^0.1.0-alpha.26",
66
+ "@shrkcrft/rule-graph": "^0.1.0-alpha.26",
67
+ "@shrkcrft/structural-search": "^0.1.0-alpha.26",
68
+ "@shrkcrft/impact-engine": "^0.1.0-alpha.26",
69
+ "@shrkcrft/context-planner": "^0.1.0-alpha.26",
70
+ "@shrkcrft/architecture-guard": "^0.1.0-alpha.26",
71
+ "@shrkcrft/framework-scanners": "^0.1.0-alpha.26",
72
+ "@shrkcrft/api-surface-diff": "^0.1.0-alpha.26",
73
+ "@shrkcrft/quality-gates": "^0.1.0-alpha.26",
74
+ "@shrkcrft/migrate": "^0.1.0-alpha.26",
75
+ "@shrkcrft/generator": "^0.1.0-alpha.26",
76
+ "@shrkcrft/importer": "^0.1.0-alpha.26",
77
+ "@shrkcrft/inspector": "^0.1.0-alpha.26",
78
+ "@shrkcrft/ai": "^0.1.0-alpha.26",
79
+ "@shrkcrft/embeddings": "^0.1.0-alpha.26",
80
+ "@shrkcrft/shared": "^0.1.0-alpha.26",
81
+ "@shrkcrft/mcp-server": "^0.1.0-alpha.26",
82
82
  "@huggingface/transformers": "^3.7.5"
83
83
  },
84
84
  "publishConfig": {