@shrkcrft/cli 0.1.0-alpha.21 → 0.1.0-alpha.23

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 (94) hide show
  1. package/dist/command-registry.d.ts +9 -0
  2. package/dist/command-registry.d.ts.map +1 -1
  3. package/dist/command-registry.js +15 -0
  4. package/dist/commands/ai-status.command.d.ts.map +1 -1
  5. package/dist/commands/ai-status.command.js +10 -0
  6. package/dist/commands/apply.command.d.ts.map +1 -1
  7. package/dist/commands/apply.command.js +9 -2
  8. package/dist/commands/ask.command.d.ts.map +1 -1
  9. package/dist/commands/ask.command.js +2 -1
  10. package/dist/commands/changes.command.d.ts.map +1 -1
  11. package/dist/commands/changes.command.js +14 -4
  12. package/dist/commands/check.command.d.ts.map +1 -1
  13. package/dist/commands/check.command.js +119 -3
  14. package/dist/commands/command-catalog.d.ts.map +1 -1
  15. package/dist/commands/command-catalog.js +224 -0
  16. package/dist/commands/commands.command.d.ts +15 -0
  17. package/dist/commands/commands.command.d.ts.map +1 -1
  18. package/dist/commands/commands.command.js +162 -3
  19. package/dist/commands/compress.command.d.ts.map +1 -1
  20. package/dist/commands/compress.command.js +39 -4
  21. package/dist/commands/constructs.command.d.ts.map +1 -1
  22. package/dist/commands/constructs.command.js +40 -9
  23. package/dist/commands/context.command.d.ts.map +1 -1
  24. package/dist/commands/context.command.js +3 -1
  25. package/dist/commands/daily.commands.d.ts.map +1 -1
  26. package/dist/commands/daily.commands.js +48 -5
  27. package/dist/commands/diff-check.command.d.ts.map +1 -1
  28. package/dist/commands/diff-check.command.js +6 -2
  29. package/dist/commands/feedback.command.d.ts.map +1 -1
  30. package/dist/commands/feedback.command.js +4 -0
  31. package/dist/commands/framework.command.d.ts.map +1 -1
  32. package/dist/commands/framework.command.js +20 -3
  33. package/dist/commands/gate.command.d.ts.map +1 -1
  34. package/dist/commands/gate.command.js +99 -2
  35. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  36. package/dist/commands/graph-code-subverbs.js +183 -68
  37. package/dist/commands/graph.command.d.ts.map +1 -1
  38. package/dist/commands/graph.command.js +6 -0
  39. package/dist/commands/impact.command.d.ts.map +1 -1
  40. package/dist/commands/impact.command.js +91 -1
  41. package/dist/commands/knowledge.command.d.ts.map +1 -1
  42. package/dist/commands/knowledge.command.js +21 -1
  43. package/dist/commands/onboard.command.d.ts.map +1 -1
  44. package/dist/commands/onboard.command.js +11 -2
  45. package/dist/commands/orchestrate.command.d.ts.map +1 -1
  46. package/dist/commands/orchestrate.command.js +6 -1
  47. package/dist/commands/pack-author.command.d.ts.map +1 -1
  48. package/dist/commands/pack-author.command.js +13 -1
  49. package/dist/commands/packs.command.d.ts.map +1 -1
  50. package/dist/commands/packs.command.js +95 -17
  51. package/dist/commands/paths.command.d.ts.map +1 -1
  52. package/dist/commands/paths.command.js +8 -1
  53. package/dist/commands/pipelines.command.d.ts.map +1 -1
  54. package/dist/commands/pipelines.command.js +2 -1
  55. package/dist/commands/plan-context.command.d.ts.map +1 -1
  56. package/dist/commands/plan-context.command.js +6 -3
  57. package/dist/commands/plan-simulate.command.js +1 -1
  58. package/dist/commands/policy-lint.command.d.ts +3 -0
  59. package/dist/commands/policy-lint.command.d.ts.map +1 -0
  60. package/dist/commands/policy-lint.command.js +135 -0
  61. package/dist/commands/recommend.command.d.ts.map +1 -1
  62. package/dist/commands/recommend.command.js +10 -2
  63. package/dist/commands/registry.command.d.ts.map +1 -1
  64. package/dist/commands/registry.command.js +117 -9
  65. package/dist/commands/report.command.js +1 -1
  66. package/dist/commands/reuse.command.d.ts +3 -0
  67. package/dist/commands/reuse.command.d.ts.map +1 -0
  68. package/dist/commands/reuse.command.js +202 -0
  69. package/dist/commands/review.command.d.ts.map +1 -1
  70. package/dist/commands/review.command.js +20 -2
  71. package/dist/commands/rule-graph-subverbs.d.ts.map +1 -1
  72. package/dist/commands/rule-graph-subverbs.js +31 -4
  73. package/dist/commands/search-structural.command.d.ts.map +1 -1
  74. package/dist/commands/search-structural.command.js +2 -2
  75. package/dist/commands/search.command.d.ts.map +1 -1
  76. package/dist/commands/search.command.js +19 -1
  77. package/dist/commands/simulate.command.d.ts.map +1 -1
  78. package/dist/commands/simulate.command.js +6 -1
  79. package/dist/commands/smart-context.command.d.ts.map +1 -1
  80. package/dist/commands/smart-context.command.js +87 -4
  81. package/dist/commands/spec.command.d.ts.map +1 -1
  82. package/dist/commands/spec.command.js +73 -13
  83. package/dist/commands/task-context.command.d.ts.map +1 -1
  84. package/dist/commands/task-context.command.js +26 -2
  85. package/dist/commands/test.command.d.ts.map +1 -1
  86. package/dist/commands/test.command.js +17 -0
  87. package/dist/commands/trace.command.d.ts.map +1 -1
  88. package/dist/commands/trace.command.js +14 -1
  89. package/dist/main.d.ts.map +1 -1
  90. package/dist/main.js +19 -1
  91. package/dist/status/freshness.d.ts +30 -0
  92. package/dist/status/freshness.d.ts.map +1 -0
  93. package/dist/status/freshness.js +81 -0
  94. package/package.json +33 -33
@@ -1,4 +1,5 @@
1
1
  import { QualityGateReportStore, renderGateReportMarkdown, runQualityGates, } from '@shrkcrft/quality-gates';
2
+ import { inspectSharkcraft, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
2
3
  import { chmodSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
3
4
  import * as nodePath from 'node:path';
4
5
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
@@ -18,7 +19,7 @@ import { asJson, header, kv } from "../output/format-output.js";
18
19
  export const gateCommand = {
19
20
  name: 'gate',
20
21
  description: 'Aggregator: runs the code-intelligence quality gates (graph freshness, architecture, impact-since-ref) and reports a single pass/fail.',
21
- usage: 'shrk gate [--since <gitref>] [--fail-on critical,high] [--arch-all] [--disable arch,impact,api-diff] [--api-baseline <path>] [--no-fail-on-breaking] [--strict] [--no-persist] [--json] [--markdown] [--output <path>]\n (the arch gate is baseline-relative by default once a baseline is frozen — fails only on NEW errors; --arch-all fails on total)\n shrk gate scaffold-ci [--provider github|generic] [--force] [--json]\n shrk gate scaffold-hook [--provider husky|raw] [--force] [--json]',
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]',
22
23
  async run(args) {
23
24
  if (args.positional[0] === 'scaffold-ci') {
24
25
  const sliced = { ...args, positional: args.positional.slice(1) };
@@ -38,19 +39,115 @@ export const gateCommand = {
38
39
  const disableRaw = flagString(args, 'disable');
39
40
  const apiBaseline = flagString(args, 'api-baseline');
40
41
  const noFailOnBreaking = flagBool(args, 'no-fail-on-breaking');
41
- const failOn = failOnRaw
42
+ // `--fail-on` accepts only `high` / `critical`. An unknown token used to
43
+ // silently REPLACE the default `['critical']`, leaving nothing able to fail
44
+ // the gate — reject it loudly (exit 2) instead.
45
+ const failOnTokens = failOnRaw
42
46
  ? failOnRaw.split(',').map((s) => s.trim()).filter(Boolean)
43
47
  : undefined;
48
+ if (failOnTokens) {
49
+ const allowedRisk = new Set(['high', 'critical']);
50
+ const unknown = failOnTokens.filter((t) => !allowedRisk.has(t));
51
+ if (unknown.length > 0) {
52
+ process.stderr.write(`Unknown --fail-on value(s): ${unknown.join(', ')}. Allowed: high, critical.\n`);
53
+ return 2;
54
+ }
55
+ }
56
+ const failOn = failOnTokens && failOnTokens.length > 0
57
+ ? failOnTokens
58
+ : undefined;
44
59
  const disable = disableRaw ? disableRaw.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
45
60
  // --arch-all: fail on TOTAL architecture errors (ignore the frozen baseline).
46
61
  // By default the arch gate is baseline-relative — it fails only on NEW errors.
47
62
  const archAll = flagBool(args, 'arch-all');
63
+ // Changeset scope. `--changed-only` (tracked + untracked worktree),
64
+ // `--staged`, `--files`, and `--since` all narrow the wiring + policy +
65
+ // knowledge-symbol gates to the change. `--since` additionally drives the
66
+ // (ref-based) impact gate, as before.
67
+ const changedOnly = flagBool(args, 'changed-only');
68
+ const staged = flagBool(args, 'staged');
69
+ const filesRaw = flagString(args, 'files');
70
+ const fileList = filesRaw ? filesRaw.split(',').map((s) => s.trim()).filter(Boolean) : [];
71
+ const wantChangedScope = changedOnly || staged || Boolean(sinceRef) || fileList.length > 0;
72
+ let changedFiles;
73
+ if (wantChangedScope) {
74
+ const resolved = resolveChangedFiles({
75
+ projectRoot: cwd,
76
+ ...(fileList.length > 0 ? { files: fileList } : {}),
77
+ ...(staged ? { staged: true } : {}),
78
+ ...(sinceRef ? { since: sinceRef } : {}),
79
+ ...(changedOnly && !staged && !sinceRef && fileList.length === 0
80
+ ? { includeWorktree: true }
81
+ : {}),
82
+ });
83
+ changedFiles = resolved.files;
84
+ }
85
+ // Wiring + policy rules come from the project config; each gate is skipped
86
+ // (never red) when none are declared, so they're inert for projects that
87
+ // don't opt in. An INVALID config is surfaced (warn) rather than silently
88
+ // disabling the plane.
89
+ const loadedConfig = await resolveProjectConfig(cwd);
90
+ const wiringRules = loadedConfig.ok ? loadedConfig.value.config.wiringRules ?? [] : [];
91
+ const policyRules = loadedConfig.ok ? loadedConfig.value.config.policyRules ?? [] : [];
92
+ const configError = loadedConfig.ok ? undefined : loadedConfig.error.message;
93
+ // Pack-plane merge notes (missing/invalid pack rule files, dropped
94
+ // collisions) go to stderr so they never pollute the JSON/markdown report
95
+ // on stdout that CI consumes.
96
+ if (loadedConfig.ok) {
97
+ for (const d of loadedConfig.value.planeDiagnostics) {
98
+ process.stderr.write(`plane: ${d}\n`);
99
+ }
100
+ }
101
+ const scopeOpts = wantChangedScope
102
+ ? { changedOnly: true, changedFiles: changedFiles ?? [] }
103
+ : {};
104
+ // Knowledge symbol-ref integrity needs the loaded knowledge entries. The
105
+ // inspection is async, so we build it here and inject it; the gate stays
106
+ // synchronous and resolves the code graph itself. Best-effort — a failed
107
+ // inspection just skips the gate rather than failing `shrk gate`.
108
+ let inspection;
109
+ if (!disable?.includes('knowledge-symbol')) {
110
+ try {
111
+ inspection = await inspectSharkcraft({ cwd });
112
+ }
113
+ catch {
114
+ inspection = undefined;
115
+ }
116
+ }
48
117
  const report = runQualityGates({
49
118
  projectRoot: cwd,
50
119
  ...(archAll ? { arch: { baselineRelative: false } } : {}),
120
+ wiring: {
121
+ ...(configError
122
+ ? { configError }
123
+ : wiringRules.length > 0
124
+ ? { rules: wiringRules }
125
+ : {}),
126
+ ...scopeOpts,
127
+ },
128
+ policy: {
129
+ ...(configError
130
+ ? { configError }
131
+ : policyRules.length > 0
132
+ ? { rules: policyRules }
133
+ : {}),
134
+ ...scopeOpts,
135
+ },
136
+ ...(inspection
137
+ ? {
138
+ knowledgeSymbol: {
139
+ inspection,
140
+ ...(wantChangedScope ? { changedFiles: changedFiles ?? [] } : {}),
141
+ },
142
+ }
143
+ : {}),
51
144
  impact: {
52
145
  ...(sinceRef ? { sinceRef } : {}),
53
146
  ...(failOn ? { failOn } : {}),
147
+ // Scope the impact gate to the changeset too: with `--since` we keep the
148
+ // gitref diff; with `--changed-only` / `--staged` / `--files` (and no
149
+ // `--since`) we analyze the resolved changed-file set directly.
150
+ ...(wantChangedScope && !sinceRef ? { files: changedFiles ?? [] } : {}),
54
151
  },
55
152
  ...(apiBaseline
56
153
  ? {
@@ -1 +1 @@
1
- {"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AA0BA,OAAO,EAAoC,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAiI3F,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+DtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwF1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA8EpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAmFtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAiDtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgMvE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA8HtE;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+CpE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkGvE;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHpE"}
1
+ {"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AA2BA,OAAO,EAAqD,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AA+J5G,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkEtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA2F1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+EpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+FtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwEtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA0MvE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA8HtE;AAID;;;;;;GAMG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgEpE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkGvE;AAyBD;;;;;;;;;;;;GAYG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgHpE"}
@@ -5,14 +5,14 @@
5
5
  * focused. The entry command imports each `run*` and routes when the
6
6
  * first positional matches the subverb name.
7
7
  */
8
- import { buildFullIndex, changedFilesSince, detectChangedAndDeleted, detectGraphFreshness, EdgeKind, GraphQueryApi, GraphStore, hasCallGraphReferences, NodeKind, updateChanged, } from '@shrkcrft/graph';
8
+ import { buildFullIndex, changedFilesSince, detectChangedAndDeleted, detectGraphFreshness, EdgeKind, GraphQueryApi, GraphStore, hasCallGraphReferences, isGraphStoreCorruptError, NodeKind, updateChanged, } from '@shrkcrft/graph';
9
9
  import { analyzeGraphImpact } from '@shrkcrft/impact-engine';
10
10
  import { BridgeStore, RuleGraphQueryApi } from '@shrkcrft/rule-graph';
11
11
  import { FrameworkQueryApi, FrameworkStore } from '@shrkcrft/framework-scanners';
12
12
  import { existsSync } from 'node:fs';
13
13
  import * as nodePath from 'node:path';
14
14
  import { compactArrayToColumnar } from '@shrkcrft/compress';
15
- import { flagBool, flagString, resolveCwd } from "../command-registry.js";
15
+ import { flagBool, flagPositiveInt, flagString, resolveCwd } from "../command-registry.js";
16
16
  import { asJson, header, kv } from "../output/format-output.js";
17
17
  import { maybeRunInWatchMode } from "../output/watch-loop.js";
18
18
  /**
@@ -59,6 +59,14 @@ function maybeColumnarize(payload, args) {
59
59
  }
60
60
  const STALE_HINT = `Index is missing or stale. Run 'shrk graph index' to build it.`;
61
61
  const STALE_RESULT_HINT = 'Some result files changed since the index was built — auto-refresh is on by default (you passed --no-refresh / SHRK_GRAPH_NO_REFRESH). Drop the opt-out, or run `shrk graph index --changed`, for fresh results.';
62
+ const CORRUPT_HINT = 'code graph store is corrupt — run `shrk graph index` to rebuild.';
63
+ /**
64
+ * Per-list display cap for `graph context`. The JSON payload always reports the
65
+ * true pre-slice count (`total<List>`) and a `<list>Truncated` flag so a
66
+ * high-fan-in node is never silently capped (mirrors runGraphCycles /
67
+ * runGraphCallers). Kept in sync with the MCP `get_graph_context` tool.
68
+ */
69
+ const CONTEXT_LIST_CAP = 50;
62
70
  /**
63
71
  * Refresh-by-default: incrementally reindex changed/deleted files BEFORE
64
72
  * querying so an agent's just-saved edits are reflected, then print a one-line
@@ -122,8 +130,31 @@ function indexBehindHint(cwd) {
122
130
  return null;
123
131
  return `Index is ${behind} file(s) behind (${f.modified.length} modified, ${f.added.length} new, ${f.deleted.length} deleted) — run \`shrk graph index --changed\` and retry.`;
124
132
  }
133
+ /**
134
+ * Reject a stray positional on a subverb that takes NO file argument
135
+ * (status/cycles/unresolved/index). These previously printed a byte-identical
136
+ * global report for any/no/bogus arg, so `shrk graph status foo` silently
137
+ * swallowed `foo`. Returns the exit code to use (2) when an unexpected
138
+ * positional[1+] is present, or null when the args are clean. `subverb` is
139
+ * positional[0]; anything at positional[1] is the offending extra.
140
+ */
141
+ function strayPositionalError(args, subverb) {
142
+ const extra = args.positional[1];
143
+ if (extra === undefined)
144
+ return null;
145
+ if (flagBool(args, 'json')) {
146
+ process.stdout.write(asJson({ ok: false, error: 'unexpected-argument', argument: extra }) + '\n');
147
+ }
148
+ else {
149
+ process.stderr.write(`'shrk graph ${subverb}' takes no positional argument (got "${extra}"). Usage: shrk graph ${subverb} [--json]\n`);
150
+ }
151
+ return 2;
152
+ }
125
153
  // ─── shrk graph index ─────────────────────────────────────────────────
126
154
  export async function runGraphIndex(args) {
155
+ const stray = strayPositionalError(args, 'index');
156
+ if (stray !== null)
157
+ return stray;
127
158
  // --watch: run the index once, then re-run on file changes. Every
128
159
  // tick after the first uses the incremental updater so a 5-file edit
129
160
  // takes < 100ms. Default watch path is the project root; pass
@@ -214,6 +245,9 @@ async function runGraphIndexOnce(args) {
214
245
  export async function runGraphCycles(args) {
215
246
  const cwd = resolveCwd(args);
216
247
  const wantJson = flagBool(args, 'json');
248
+ const stray = strayPositionalError(args, 'cycles');
249
+ if (stray !== null)
250
+ return stray;
217
251
  const limit = parseLimit(args);
218
252
  const minSize = parseMinSize(args);
219
253
  const store = new GraphStore(cwd);
@@ -230,7 +264,9 @@ export async function runGraphCycles(args) {
230
264
  process.stderr.write(STALE_HINT + '\n');
231
265
  return 1;
232
266
  }
233
- const api = GraphQueryApi.fromStore(cwd);
267
+ const api = loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
268
+ if (!api)
269
+ return 1;
234
270
  const allCycles = api.cycles();
235
271
  const filtered = allCycles.filter((c) => c.size >= minSize);
236
272
  const limited = filtered.slice(0, limit);
@@ -289,6 +325,9 @@ function parseMinSize(args) {
289
325
  export async function runGraphUnresolved(args) {
290
326
  const cwd = resolveCwd(args);
291
327
  const wantJson = flagBool(args, 'json');
328
+ const stray = strayPositionalError(args, 'unresolved');
329
+ if (stray !== null)
330
+ return stray;
292
331
  const limit = parseLimit(args);
293
332
  const store = new GraphStore(cwd);
294
333
  if (!store.exists()) {
@@ -304,7 +343,9 @@ export async function runGraphUnresolved(args) {
304
343
  process.stderr.write(STALE_HINT + '\n');
305
344
  return 1;
306
345
  }
307
- const snap = store.loadSnapshot();
346
+ const snap = loadGuarded(() => store.loadSnapshot(), wantJson);
347
+ if (!snap)
348
+ return 1;
308
349
  const groups = new Map();
309
350
  for (const e of snap.edges.values()) {
310
351
  if (e.kind !== EdgeKind.ImportsFile)
@@ -396,7 +437,9 @@ export async function runGraphDeps(args) {
396
437
  process.stderr.write(STALE_HINT + '\n');
397
438
  return 1;
398
439
  }
399
- const api = GraphQueryApi.fromStore(cwd);
440
+ const api = loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
441
+ if (!api)
442
+ return 1;
400
443
  const pkgId = `package:${pkg}`;
401
444
  // Existence guard (mirrors the MCP tool): without it, an unknown package
402
445
  // name returns a confidently-wrong empty `dependsOn/dependedOnBy` that reads
@@ -453,6 +496,9 @@ export async function runGraphDeps(args) {
453
496
  export async function runGraphStatus(args) {
454
497
  const cwd = resolveCwd(args);
455
498
  const wantJson = flagBool(args, 'json');
499
+ const stray = strayPositionalError(args, 'status');
500
+ if (stray !== null)
501
+ return stray;
456
502
  const store = new GraphStore(cwd);
457
503
  if (!store.exists()) {
458
504
  const payload = {
@@ -468,8 +514,20 @@ export async function runGraphStatus(args) {
468
514
  process.stderr.write(STALE_HINT + '\n');
469
515
  return 1;
470
516
  }
471
- const verify = store.verifyDigest();
472
- const snap = store.loadSnapshot();
517
+ // A corrupt store (a truncated/garbled JSONL row) must surface as a `corrupt`
518
+ // status line + non-zero exit, NOT a raw `Fatal: JSON Parse error` crash.
519
+ let verify;
520
+ let snap;
521
+ try {
522
+ verify = store.verifyDigest();
523
+ snap = store.loadSnapshot();
524
+ }
525
+ catch (err) {
526
+ const code = emitCorruptStore(err, wantJson);
527
+ if (code !== null)
528
+ return code;
529
+ throw err;
530
+ }
473
531
  const manifestNodeCount = sumValues(snap.manifest.nodesByKind);
474
532
  const manifestEdgeCount = sumValues(snap.manifest.edgesByKind);
475
533
  // Honest freshness vs the working tree. `corrupt` (store self-integrity) and
@@ -536,40 +594,63 @@ export async function runGraphSearch(args) {
536
594
  return 2;
537
595
  }
538
596
  const kindFlag = flagString(args, 'kind');
539
- const limit = Number(flagString(args, 'limit') ?? '20');
597
+ // NaN-safe --limit: a fat-fingered `--limit abc` must fall back to the default,
598
+ // not become `Number('abc') === NaN` (which zeroed the result via a NaN slice).
599
+ const limit = flagPositiveInt(args, 'limit', 20);
540
600
  maybeRefresh(args, cwd);
541
601
  const api = loadOrFail(cwd, wantJson);
542
602
  if (!api)
543
603
  return 1;
544
- let matches;
604
+ // Compute the TRUE pre-slice match count so `total`/`truncated` stay honest —
605
+ // the old code reported the post-cap length as the total, so 285 matches read
606
+ // as `total: 20` with no signal there was more.
607
+ let page;
608
+ let total;
545
609
  if (hasUnresolved) {
546
610
  const all = api.filesWithUnresolvedImports();
547
- matches = (query
611
+ const filtered = query
548
612
  ? all.filter((n) => (n.path ?? '').toLowerCase().includes(query.toLowerCase()))
549
- : [...all]).slice(0, limit);
613
+ : [...all];
614
+ total = filtered.length;
615
+ page = filtered.slice(0, limit);
550
616
  }
551
617
  else {
552
- matches = collectSearchMatches(api, query, kindFlag, limit);
553
- }
618
+ const res = collectSearchMatches(api, query, kindFlag, limit);
619
+ page = res.matches;
620
+ total = res.total;
621
+ }
622
+ // Targeted staleness over the displayed page (parity with MCP / callers): drop
623
+ // deleted result files, flag modified ones, and reduce `total` by the
624
+ // deletions we observed so a dead file is never counted.
625
+ const summarised = page.map(toSearchHit);
626
+ const fresh = resultStaleness(api, cwd, summarised.map((m) => m.path));
627
+ const live = summarised.filter((m) => !m.path || !fresh.deletedSet.has(m.path));
628
+ const adjustedTotal = total - (summarised.length - live.length);
629
+ const truncated = adjustedTotal > limit;
554
630
  if (wantJson) {
555
631
  process.stdout.write(asJson({
556
632
  schema: 'sharkcraft.graph-search/v1',
557
633
  query,
558
634
  kind: kindFlag ?? 'any',
559
- total: matches.length,
560
- matches: matches.map(toSearchHit),
635
+ total: adjustedTotal,
636
+ truncated,
637
+ matches: live,
638
+ ...(fresh.field ?? {}),
561
639
  }) + '\n');
562
640
  return 0;
563
641
  }
564
642
  const headerLabel = query ?? (hasUnresolved ? 'files with unresolved imports' : '');
565
- if (matches.length === 0) {
643
+ if (live.length === 0) {
566
644
  process.stdout.write(`No matches for "${headerLabel}".\n`);
567
645
  return 0;
568
646
  }
569
647
  process.stdout.write(header(`Graph search: ${headerLabel}`));
570
- for (const m of matches) {
648
+ for (const m of live) {
571
649
  process.stdout.write(` ${m.kind.padEnd(8)} ${m.label}${m.path ? ' ' + m.path : ''}${m.line ? ':' + m.line : ''}\n`);
572
650
  }
651
+ if (truncated) {
652
+ process.stdout.write(`\n(${adjustedTotal - live.length} more — pass --limit ${adjustedTotal} to see all)\n`);
653
+ }
573
654
  return 0;
574
655
  }
575
656
  // ─── shrk graph context ───────────────────────────────────────────────
@@ -581,7 +662,7 @@ export async function runGraphContext(args) {
581
662
  process.stderr.write('Usage: shrk graph context <fileOrSymbol> [--depth N] [--no-bridge] [--no-framework]\n');
582
663
  return 2;
583
664
  }
584
- const depth = Math.max(1, Math.min(3, Number(flagString(args, 'depth') ?? '1')));
665
+ const depth = Math.min(3, flagPositiveInt(args, 'depth', 1));
585
666
  const includeBridge = !flagBool(args, 'no-bridge');
586
667
  const includeFramework = !flagBool(args, 'no-framework');
587
668
  maybeRefresh(args, cwd);
@@ -622,16 +703,18 @@ export async function runGraphContext(args) {
622
703
  const frameworkEntities = (includeFramework && frameworkStore.exists() && anchorFile?.path)
623
704
  ? FrameworkQueryApi.fromStore(cwd).forFile(anchorFile.path)
624
705
  : [];
625
- const importsFromList = neighbours.out
626
- .filter((o) => o.edge.kind === 'imports-file')
627
- .slice(0, 50)
706
+ // Pre-slice the homogeneous filtered arrays so the payload can report the TRUE
707
+ // count + a truncated flag instead of silently capping each list at 50.
708
+ const importsFromAll = neighbours.out.filter((o) => o.edge.kind === 'imports-file');
709
+ const importedByAll = neighbours.in.filter((i) => i.edge.kind === 'imports-file');
710
+ const importsFromList = importsFromAll
711
+ .slice(0, CONTEXT_LIST_CAP)
628
712
  .map((o) => ('target' in o ? targetSummary(o.target) : { id: 'unknown', resolved: false }));
629
- const importedByList = neighbours.in
630
- .filter((i) => i.edge.kind === 'imports-file')
631
- .slice(0, 50)
713
+ const importedByList = importedByAll
714
+ .slice(0, CONTEXT_LIST_CAP)
632
715
  .map((i) => ('source' in i ? sourceSummary(i.source) : { id: 'unknown', resolved: false }));
633
- const referencedByList = references.slice(0, 50).map(nodeSummary);
634
- const calledByList = callers.slice(0, 50).map(nodeSummary);
716
+ const referencedByList = references.slice(0, CONTEXT_LIST_CAP).map(nodeSummary);
717
+ const calledByList = callers.slice(0, CONTEXT_LIST_CAP).map(nodeSummary);
635
718
  // Staleness over the anchor + every referenced file: drop dead paths from the
636
719
  // usage lists, flag changed ones.
637
720
  const ctxPathOf = (x) => x.path;
@@ -649,10 +732,18 @@ export async function runGraphContext(args) {
649
732
  declaredIn: anchor.kind === NodeKind.Symbol && anchorFile ? nodeSummary(anchorFile) : null,
650
733
  depth,
651
734
  importsFrom: ctxDropDel(importsFromList),
735
+ totalImportsFrom: importsFromAll.length,
736
+ importsFromTruncated: importsFromAll.length > CONTEXT_LIST_CAP,
652
737
  importedBy: ctxDropDel(importedByList),
738
+ totalImportedBy: importedByAll.length,
739
+ importedByTruncated: importedByAll.length > CONTEXT_LIST_CAP,
653
740
  symbols: symbols.slice(0, 50).map(nodeSummary),
654
741
  referencedBy: ctxDropDel(referencedByList),
742
+ totalReferencedBy: references.length,
743
+ referencedByTruncated: references.length > CONTEXT_LIST_CAP,
655
744
  calledBy: ctxDropDel(calledByList),
745
+ totalCalledBy: callers.length,
746
+ calledByTruncated: callers.length > CONTEXT_LIST_CAP,
656
747
  ...(subtypes.length > 0 ? { subtypes: subtypes.slice(0, 50).map(nodeSummary) } : {}),
657
748
  ...(supertypes.length > 0 ? { supertypes: supertypes.slice(0, 50).map(nodeSummary) } : {}),
658
749
  ...(fresh.field ?? {}),
@@ -772,8 +863,8 @@ export async function runGraphImpact(args) {
772
863
  process.stderr.write('Usage: shrk graph impact <fileOrSymbol> [--max-depth N] [--limit N] [--full]\n');
773
864
  return 2;
774
865
  }
775
- const maxDepth = Math.max(1, Math.min(10, Number(flagString(args, 'max-depth') ?? '5')));
776
- const limit = Math.max(1, Number(flagString(args, 'limit') ?? '200'));
866
+ const maxDepth = Math.min(10, flagPositiveInt(args, 'max-depth', 5));
867
+ const limit = flagPositiveInt(args, 'limit', 200);
777
868
  maybeRefresh(args, cwd);
778
869
  // --full → delegate to the impact-engine for a richer v3 payload.
779
870
  if (wantFull) {
@@ -899,8 +990,22 @@ export async function runGraphImpact(args) {
899
990
  export async function runGraphHubs(args) {
900
991
  const cwd = resolveCwd(args);
901
992
  const wantJson = flagBool(args, 'json');
902
- const limit = Math.max(1, Math.min(100, Number(flagString(args, 'limit') ?? '15')));
903
- const pathScope = flagString(args, 'path');
993
+ const limit = Math.min(100, flagPositiveInt(args, 'limit', 15));
994
+ // A positional scope (`shrk graph hubs packages/cli`) is honoured the same as
995
+ // --path; passing BOTH is ambiguous, so error rather than silently ignoring
996
+ // the positional (which previously yielded a byte-identical global report).
997
+ const pathFlag = flagString(args, 'path');
998
+ const positionalPath = args.positional[1];
999
+ if (pathFlag && positionalPath) {
1000
+ if (wantJson) {
1001
+ process.stdout.write(asJson({ ok: false, error: 'ambiguous-path', positional: positionalPath, flag: pathFlag }) + '\n');
1002
+ }
1003
+ else {
1004
+ process.stderr.write(`Pass the scope path either as a positional or with --path, not both (got "${positionalPath}" and --path ${pathFlag}).\n`);
1005
+ }
1006
+ return 2;
1007
+ }
1008
+ const pathScope = pathFlag ?? positionalPath;
904
1009
  maybeRefresh(args, cwd);
905
1010
  const api = loadOrFail(cwd, wantJson);
906
1011
  if (!api)
@@ -1081,7 +1186,7 @@ export async function runGraphPath(args) {
1081
1186
  process.stderr.write('Usage: shrk graph path <from> <to> [--max-depth N] [--no-refresh] [--json]\n');
1082
1187
  return 2;
1083
1188
  }
1084
- const maxDepth = Math.max(1, Math.min(32, Number(flagString(args, 'max-depth') ?? '16')));
1189
+ const maxDepth = Math.min(32, flagPositiveInt(args, 'max-depth', 16));
1085
1190
  maybeRefresh(args, cwd);
1086
1191
  const api = loadOrFail(cwd, wantJson);
1087
1192
  if (!api)
@@ -1183,6 +1288,45 @@ export async function runGraphPath(args) {
1183
1288
  return 0;
1184
1289
  }
1185
1290
  // ─── helpers ──────────────────────────────────────────────────────────
1291
+ /**
1292
+ * Convert the typed corrupt-store error (one bad JSONL line) into a
1293
+ * deterministic "rebuild the index" message + non-zero exit, instead of letting
1294
+ * an unhandled `Fatal: JSON Parse error` escape the CLI. Returns the exit code
1295
+ * (1) when `err` is a corrupt-store error, or null to signal "not mine — rethrow".
1296
+ */
1297
+ function emitCorruptStore(err, wantJson) {
1298
+ if (!isGraphStoreCorruptError(err))
1299
+ return null;
1300
+ if (wantJson) {
1301
+ process.stdout.write(asJson({
1302
+ ok: false,
1303
+ state: 'corrupt',
1304
+ nextCommand: 'shrk graph index',
1305
+ message: CORRUPT_HINT,
1306
+ ...(err.details ?? {}),
1307
+ }) + '\n');
1308
+ }
1309
+ else {
1310
+ process.stderr.write(CORRUPT_HINT + '\n');
1311
+ }
1312
+ return 1;
1313
+ }
1314
+ /**
1315
+ * Run a store-loading thunk, mapping a corrupt store to a clean rebuild hint.
1316
+ * Returns the loaded value, or undefined when the store was corrupt (the
1317
+ * message + exit have already been emitted; the caller should `return 1`). Any
1318
+ * other error is rethrown unchanged.
1319
+ */
1320
+ function loadGuarded(load, wantJson) {
1321
+ try {
1322
+ return load();
1323
+ }
1324
+ catch (err) {
1325
+ if (emitCorruptStore(err, wantJson) !== null)
1326
+ return undefined;
1327
+ throw err;
1328
+ }
1329
+ }
1186
1330
  function loadOrFail(cwd, wantJson) {
1187
1331
  const store = new GraphStore(cwd);
1188
1332
  if (!store.exists()) {
@@ -1199,7 +1343,7 @@ function loadOrFail(cwd, wantJson) {
1199
1343
  }
1200
1344
  return undefined;
1201
1345
  }
1202
- return GraphQueryApi.fromStore(cwd);
1346
+ return loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
1203
1347
  }
1204
1348
  function resolveAnchor(api, target) {
1205
1349
  // Exact node id wins.
@@ -1221,43 +1365,14 @@ function resolveAnchor(api, target) {
1221
1365
  return syms[0];
1222
1366
  return undefined;
1223
1367
  }
1368
+ /**
1369
+ * Delegate to the shared {@link GraphQueryApi.searchNodes} so the CLI and the
1370
+ * MCP `get_graph_search` tool can never disagree on results OR the honest
1371
+ * pre-slice `total` (the two surfaces used to maintain divergent copies of this
1372
+ * fuzzy-match logic). Returns the display page + the true match count.
1373
+ */
1224
1374
  function collectSearchMatches(api, query, kind, limit) {
1225
- const out = [];
1226
- if (!kind || kind === 'file') {
1227
- const f = api.findFile(query);
1228
- if (f)
1229
- out.push(f);
1230
- // Fuzzy fallback: substring match on path/basename so `shrk graph
1231
- // search Foo --kind file` finds `libs/x/y/Foo.ts` without forcing the
1232
- // caller to type the full path. Skips the node if exact match already
1233
- // included it.
1234
- if (out.length < limit) {
1235
- const q = query.toLowerCase();
1236
- const seen = new Set(out.map((n) => n.id));
1237
- for (const node of api.allFiles()) {
1238
- if (seen.has(node.id))
1239
- continue;
1240
- const p = node.path?.toLowerCase() ?? '';
1241
- const base = p.includes('/') ? p.slice(p.lastIndexOf('/') + 1) : p;
1242
- if (base.includes(q) || p.includes(q)) {
1243
- out.push(node);
1244
- seen.add(node.id);
1245
- if (out.length >= limit)
1246
- break;
1247
- }
1248
- }
1249
- }
1250
- }
1251
- if (!kind || kind === 'symbol') {
1252
- for (const s of api.findSymbol(query, { exact: false, limit }))
1253
- out.push(s);
1254
- }
1255
- if (!kind || kind === 'package') {
1256
- const p = api.neighbours(`package:${query}`);
1257
- if (p)
1258
- out.push(p.node);
1259
- }
1260
- return out.slice(0, limit);
1375
+ return api.searchNodes(query, { ...(kind ? { kind } : {}), limit });
1261
1376
  }
1262
1377
  function reverseClosure(api, anchor, maxDepth, limit) {
1263
1378
  const seen = new Set([anchor.id]);
@@ -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,eA+M1B,CAAC"}
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"}
@@ -52,6 +52,12 @@ export const graphCommand = {
52
52
  const graph = buildKnowledgeGraph(inspection);
53
53
  const sub = args.positional[0];
54
54
  const typeFlag = flagString(args, 'type');
55
+ // A typo'd `--type` would otherwise silently match zero nodes and return an
56
+ // empty summary / "no node" at exit 0/1. Reject loudly with the valid list.
57
+ if (typeFlag && !KNOWN_KINDS.includes(typeFlag)) {
58
+ process.stderr.write(`Unknown --type ${typeFlag}. Valid: ${KNOWN_KINDS.join(', ')}\n`);
59
+ return 2;
60
+ }
55
61
  const formatFlag = (flagString(args, 'format') ?? 'text');
56
62
  const outputFlag = flagString(args, 'output');
57
63
  // `shrk graph imports` — import-graph analysis.
@@ -1 +1 @@
1
- {"version":3,"file":"impact.command.d.ts","sourceRoot":"","sources":["../../src/commands/impact.command.ts"],"names":[],"mappings":"AAsBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA0VhC,eAAO,MAAM,aAAa,EAAE,eA8V3B,CAAC"}
1
+ {"version":3,"file":"impact.command.d.ts","sourceRoot":"","sources":["../../src/commands/impact.command.ts"],"names":[],"mappings":"AAsBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AAibhC,eAAO,MAAM,aAAa,EAAE,eAsW3B,CAAC"}