@shrkcrft/cli 0.1.0-alpha.22 → 0.1.0-alpha.24
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.
- package/dist/command-registry.d.ts +9 -0
- package/dist/command-registry.d.ts.map +1 -1
- package/dist/command-registry.js +15 -0
- package/dist/commands/ai-status.command.d.ts.map +1 -1
- package/dist/commands/ai-status.command.js +10 -0
- package/dist/commands/apply.command.d.ts.map +1 -1
- package/dist/commands/apply.command.js +9 -2
- package/dist/commands/ask.command.d.ts.map +1 -1
- package/dist/commands/ask.command.js +2 -1
- package/dist/commands/changes.command.d.ts.map +1 -1
- package/dist/commands/changes.command.js +14 -4
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +139 -7
- package/dist/commands/command-catalog.d.ts +11 -0
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +291 -0
- package/dist/commands/commands.command.d.ts +15 -0
- package/dist/commands/commands.command.d.ts.map +1 -1
- package/dist/commands/commands.command.js +162 -3
- package/dist/commands/compress.command.d.ts.map +1 -1
- package/dist/commands/compress.command.js +39 -4
- package/dist/commands/context.command.d.ts.map +1 -1
- package/dist/commands/context.command.js +3 -1
- package/dist/commands/daily.commands.d.ts.map +1 -1
- package/dist/commands/daily.commands.js +48 -5
- package/dist/commands/diff-check.command.d.ts.map +1 -1
- package/dist/commands/diff-check.command.js +6 -2
- package/dist/commands/feedback.command.d.ts.map +1 -1
- package/dist/commands/feedback.command.js +4 -0
- package/dist/commands/finish.command.d.ts +3 -0
- package/dist/commands/finish.command.d.ts.map +1 -0
- package/dist/commands/finish.command.js +70 -0
- package/dist/commands/framework.command.d.ts.map +1 -1
- package/dist/commands/framework.command.js +20 -3
- package/dist/commands/gate.command.d.ts.map +1 -1
- package/dist/commands/gate.command.js +98 -13
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +215 -81
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +6 -0
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +20 -2
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +86 -1
- package/dist/commands/knowledge.command.d.ts.map +1 -1
- package/dist/commands/knowledge.command.js +21 -1
- package/dist/commands/onboard.command.d.ts.map +1 -1
- package/dist/commands/onboard.command.js +11 -2
- package/dist/commands/orchestrate.command.d.ts.map +1 -1
- package/dist/commands/orchestrate.command.js +6 -1
- package/dist/commands/pack-author.command.d.ts.map +1 -1
- package/dist/commands/pack-author.command.js +13 -1
- package/dist/commands/packs.command.d.ts.map +1 -1
- package/dist/commands/packs.command.js +95 -17
- package/dist/commands/paths.command.d.ts.map +1 -1
- package/dist/commands/paths.command.js +8 -1
- package/dist/commands/pipelines.command.d.ts.map +1 -1
- package/dist/commands/pipelines.command.js +2 -1
- package/dist/commands/plan-context.command.d.ts.map +1 -1
- package/dist/commands/plan-context.command.js +6 -3
- package/dist/commands/plan-simulate.command.js +1 -1
- package/dist/commands/policy-lint.command.d.ts.map +1 -1
- package/dist/commands/policy-lint.command.js +20 -7
- package/dist/commands/recommend.command.d.ts.map +1 -1
- package/dist/commands/recommend.command.js +10 -2
- package/dist/commands/registry.command.d.ts.map +1 -1
- package/dist/commands/registry.command.js +117 -9
- package/dist/commands/report.command.js +1 -1
- package/dist/commands/reuse.command.d.ts.map +1 -1
- package/dist/commands/reuse.command.js +17 -5
- package/dist/commands/review.command.d.ts.map +1 -1
- package/dist/commands/review.command.js +20 -2
- package/dist/commands/rule-graph-subverbs.d.ts.map +1 -1
- package/dist/commands/rule-graph-subverbs.js +31 -4
- package/dist/commands/search-structural.command.d.ts.map +1 -1
- package/dist/commands/search-structural.command.js +2 -2
- package/dist/commands/search.command.d.ts.map +1 -1
- package/dist/commands/search.command.js +19 -1
- package/dist/commands/simulate.command.d.ts.map +1 -1
- package/dist/commands/simulate.command.js +6 -1
- package/dist/commands/smart-context.command.d.ts.map +1 -1
- package/dist/commands/smart-context.command.js +101 -41
- package/dist/commands/spec.command.d.ts.map +1 -1
- package/dist/commands/spec.command.js +73 -13
- package/dist/commands/task-context.command.d.ts.map +1 -1
- package/dist/commands/task-context.command.js +26 -2
- package/dist/commands/test.command.d.ts.map +1 -1
- package/dist/commands/test.command.js +17 -0
- package/dist/commands/trace.command.d.ts.map +1 -1
- package/dist/commands/trace.command.js +87 -4
- package/dist/commands/wiring.command.d.ts +12 -0
- package/dist/commands/wiring.command.d.ts.map +1 -0
- package/dist/commands/wiring.command.js +384 -0
- package/dist/diff/collect-changed-paths.d.ts +5 -3
- package/dist/diff/collect-changed-paths.d.ts.map +1 -1
- package/dist/diff/collect-changed-paths.js +73 -36
- package/dist/diff/deleted-orphans.d.ts +42 -0
- package/dist/diff/deleted-orphans.d.ts.map +1 -0
- package/dist/diff/deleted-orphans.js +46 -0
- package/dist/finish/run-finish.d.ts +62 -0
- package/dist/finish/run-finish.d.ts.map +1 -0
- package/dist/finish/run-finish.js +239 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +19 -1
- package/dist/status/freshness.d.ts +30 -0
- package/dist/status/freshness.d.ts.map +1 -0
- package/dist/status/freshness.js +81 -0
- package/package.json +33 -33
|
@@ -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, flagNumber, 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,32 @@ 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;
|
|
70
|
+
/**
|
|
71
|
+
* Resolve a `--limit N` per-list cap for the graph read commands. Absent → the
|
|
72
|
+
* command's `fallback` (the historical default — 50 for `context`, 200 for
|
|
73
|
+
* `callers`/`impact`); `--limit 0` (or any non-positive) → unbounded so a
|
|
74
|
+
* consumer can pull the full set off a high-fan-in barrel/hub in one call.
|
|
75
|
+
* Returns `Infinity` for the unbounded case: `arr.slice(0, Infinity)` yields
|
|
76
|
+
* every row and `len > Infinity` is `false`, so the `<list>Truncated` flag stays
|
|
77
|
+
* honest (never "truncated" when nothing was dropped). Malformed `--limit abc`
|
|
78
|
+
* (NaN) degrades to `fallback`, matching `flagPositiveInt`.
|
|
79
|
+
*/
|
|
80
|
+
function resolveListLimit(args, fallback) {
|
|
81
|
+
const v = flagNumber(args, 'limit');
|
|
82
|
+
if (v === undefined)
|
|
83
|
+
return fallback;
|
|
84
|
+
if (v <= 0)
|
|
85
|
+
return Number.POSITIVE_INFINITY;
|
|
86
|
+
return Math.floor(v);
|
|
87
|
+
}
|
|
62
88
|
/**
|
|
63
89
|
* Refresh-by-default: incrementally reindex changed/deleted files BEFORE
|
|
64
90
|
* querying so an agent's just-saved edits are reflected, then print a one-line
|
|
@@ -122,8 +148,31 @@ function indexBehindHint(cwd) {
|
|
|
122
148
|
return null;
|
|
123
149
|
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
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Reject a stray positional on a subverb that takes NO file argument
|
|
153
|
+
* (status/cycles/unresolved/index). These previously printed a byte-identical
|
|
154
|
+
* global report for any/no/bogus arg, so `shrk graph status foo` silently
|
|
155
|
+
* swallowed `foo`. Returns the exit code to use (2) when an unexpected
|
|
156
|
+
* positional[1+] is present, or null when the args are clean. `subverb` is
|
|
157
|
+
* positional[0]; anything at positional[1] is the offending extra.
|
|
158
|
+
*/
|
|
159
|
+
function strayPositionalError(args, subverb) {
|
|
160
|
+
const extra = args.positional[1];
|
|
161
|
+
if (extra === undefined)
|
|
162
|
+
return null;
|
|
163
|
+
if (flagBool(args, 'json')) {
|
|
164
|
+
process.stdout.write(asJson({ ok: false, error: 'unexpected-argument', argument: extra }) + '\n');
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
process.stderr.write(`'shrk graph ${subverb}' takes no positional argument (got "${extra}"). Usage: shrk graph ${subverb} [--json]\n`);
|
|
168
|
+
}
|
|
169
|
+
return 2;
|
|
170
|
+
}
|
|
125
171
|
// ─── shrk graph index ─────────────────────────────────────────────────
|
|
126
172
|
export async function runGraphIndex(args) {
|
|
173
|
+
const stray = strayPositionalError(args, 'index');
|
|
174
|
+
if (stray !== null)
|
|
175
|
+
return stray;
|
|
127
176
|
// --watch: run the index once, then re-run on file changes. Every
|
|
128
177
|
// tick after the first uses the incremental updater so a 5-file edit
|
|
129
178
|
// takes < 100ms. Default watch path is the project root; pass
|
|
@@ -214,6 +263,9 @@ async function runGraphIndexOnce(args) {
|
|
|
214
263
|
export async function runGraphCycles(args) {
|
|
215
264
|
const cwd = resolveCwd(args);
|
|
216
265
|
const wantJson = flagBool(args, 'json');
|
|
266
|
+
const stray = strayPositionalError(args, 'cycles');
|
|
267
|
+
if (stray !== null)
|
|
268
|
+
return stray;
|
|
217
269
|
const limit = parseLimit(args);
|
|
218
270
|
const minSize = parseMinSize(args);
|
|
219
271
|
const store = new GraphStore(cwd);
|
|
@@ -230,7 +282,9 @@ export async function runGraphCycles(args) {
|
|
|
230
282
|
process.stderr.write(STALE_HINT + '\n');
|
|
231
283
|
return 1;
|
|
232
284
|
}
|
|
233
|
-
const api = GraphQueryApi.fromStore(cwd);
|
|
285
|
+
const api = loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
|
|
286
|
+
if (!api)
|
|
287
|
+
return 1;
|
|
234
288
|
const allCycles = api.cycles();
|
|
235
289
|
const filtered = allCycles.filter((c) => c.size >= minSize);
|
|
236
290
|
const limited = filtered.slice(0, limit);
|
|
@@ -289,6 +343,9 @@ function parseMinSize(args) {
|
|
|
289
343
|
export async function runGraphUnresolved(args) {
|
|
290
344
|
const cwd = resolveCwd(args);
|
|
291
345
|
const wantJson = flagBool(args, 'json');
|
|
346
|
+
const stray = strayPositionalError(args, 'unresolved');
|
|
347
|
+
if (stray !== null)
|
|
348
|
+
return stray;
|
|
292
349
|
const limit = parseLimit(args);
|
|
293
350
|
const store = new GraphStore(cwd);
|
|
294
351
|
if (!store.exists()) {
|
|
@@ -304,7 +361,9 @@ export async function runGraphUnresolved(args) {
|
|
|
304
361
|
process.stderr.write(STALE_HINT + '\n');
|
|
305
362
|
return 1;
|
|
306
363
|
}
|
|
307
|
-
const snap = store.loadSnapshot();
|
|
364
|
+
const snap = loadGuarded(() => store.loadSnapshot(), wantJson);
|
|
365
|
+
if (!snap)
|
|
366
|
+
return 1;
|
|
308
367
|
const groups = new Map();
|
|
309
368
|
for (const e of snap.edges.values()) {
|
|
310
369
|
if (e.kind !== EdgeKind.ImportsFile)
|
|
@@ -396,7 +455,9 @@ export async function runGraphDeps(args) {
|
|
|
396
455
|
process.stderr.write(STALE_HINT + '\n');
|
|
397
456
|
return 1;
|
|
398
457
|
}
|
|
399
|
-
const api = GraphQueryApi.fromStore(cwd);
|
|
458
|
+
const api = loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
|
|
459
|
+
if (!api)
|
|
460
|
+
return 1;
|
|
400
461
|
const pkgId = `package:${pkg}`;
|
|
401
462
|
// Existence guard (mirrors the MCP tool): without it, an unknown package
|
|
402
463
|
// name returns a confidently-wrong empty `dependsOn/dependedOnBy` that reads
|
|
@@ -453,6 +514,9 @@ export async function runGraphDeps(args) {
|
|
|
453
514
|
export async function runGraphStatus(args) {
|
|
454
515
|
const cwd = resolveCwd(args);
|
|
455
516
|
const wantJson = flagBool(args, 'json');
|
|
517
|
+
const stray = strayPositionalError(args, 'status');
|
|
518
|
+
if (stray !== null)
|
|
519
|
+
return stray;
|
|
456
520
|
const store = new GraphStore(cwd);
|
|
457
521
|
if (!store.exists()) {
|
|
458
522
|
const payload = {
|
|
@@ -468,8 +532,20 @@ export async function runGraphStatus(args) {
|
|
|
468
532
|
process.stderr.write(STALE_HINT + '\n');
|
|
469
533
|
return 1;
|
|
470
534
|
}
|
|
471
|
-
|
|
472
|
-
|
|
535
|
+
// A corrupt store (a truncated/garbled JSONL row) must surface as a `corrupt`
|
|
536
|
+
// status line + non-zero exit, NOT a raw `Fatal: JSON Parse error` crash.
|
|
537
|
+
let verify;
|
|
538
|
+
let snap;
|
|
539
|
+
try {
|
|
540
|
+
verify = store.verifyDigest();
|
|
541
|
+
snap = store.loadSnapshot();
|
|
542
|
+
}
|
|
543
|
+
catch (err) {
|
|
544
|
+
const code = emitCorruptStore(err, wantJson);
|
|
545
|
+
if (code !== null)
|
|
546
|
+
return code;
|
|
547
|
+
throw err;
|
|
548
|
+
}
|
|
473
549
|
const manifestNodeCount = sumValues(snap.manifest.nodesByKind);
|
|
474
550
|
const manifestEdgeCount = sumValues(snap.manifest.edgesByKind);
|
|
475
551
|
// Honest freshness vs the working tree. `corrupt` (store self-integrity) and
|
|
@@ -536,40 +612,63 @@ export async function runGraphSearch(args) {
|
|
|
536
612
|
return 2;
|
|
537
613
|
}
|
|
538
614
|
const kindFlag = flagString(args, 'kind');
|
|
539
|
-
|
|
615
|
+
// NaN-safe --limit: a fat-fingered `--limit abc` must fall back to the default,
|
|
616
|
+
// not become `Number('abc') === NaN` (which zeroed the result via a NaN slice).
|
|
617
|
+
const limit = flagPositiveInt(args, 'limit', 20);
|
|
540
618
|
maybeRefresh(args, cwd);
|
|
541
619
|
const api = loadOrFail(cwd, wantJson);
|
|
542
620
|
if (!api)
|
|
543
621
|
return 1;
|
|
544
|
-
|
|
622
|
+
// Compute the TRUE pre-slice match count so `total`/`truncated` stay honest —
|
|
623
|
+
// the old code reported the post-cap length as the total, so 285 matches read
|
|
624
|
+
// as `total: 20` with no signal there was more.
|
|
625
|
+
let page;
|
|
626
|
+
let total;
|
|
545
627
|
if (hasUnresolved) {
|
|
546
628
|
const all = api.filesWithUnresolvedImports();
|
|
547
|
-
|
|
629
|
+
const filtered = query
|
|
548
630
|
? all.filter((n) => (n.path ?? '').toLowerCase().includes(query.toLowerCase()))
|
|
549
|
-
: [...all]
|
|
631
|
+
: [...all];
|
|
632
|
+
total = filtered.length;
|
|
633
|
+
page = filtered.slice(0, limit);
|
|
550
634
|
}
|
|
551
635
|
else {
|
|
552
|
-
|
|
553
|
-
|
|
636
|
+
const res = collectSearchMatches(api, query, kindFlag, limit);
|
|
637
|
+
page = res.matches;
|
|
638
|
+
total = res.total;
|
|
639
|
+
}
|
|
640
|
+
// Targeted staleness over the displayed page (parity with MCP / callers): drop
|
|
641
|
+
// deleted result files, flag modified ones, and reduce `total` by the
|
|
642
|
+
// deletions we observed so a dead file is never counted.
|
|
643
|
+
const summarised = page.map(toSearchHit);
|
|
644
|
+
const fresh = resultStaleness(api, cwd, summarised.map((m) => m.path));
|
|
645
|
+
const live = summarised.filter((m) => !m.path || !fresh.deletedSet.has(m.path));
|
|
646
|
+
const adjustedTotal = total - (summarised.length - live.length);
|
|
647
|
+
const truncated = adjustedTotal > limit;
|
|
554
648
|
if (wantJson) {
|
|
555
649
|
process.stdout.write(asJson({
|
|
556
650
|
schema: 'sharkcraft.graph-search/v1',
|
|
557
651
|
query,
|
|
558
652
|
kind: kindFlag ?? 'any',
|
|
559
|
-
total:
|
|
560
|
-
|
|
653
|
+
total: adjustedTotal,
|
|
654
|
+
truncated,
|
|
655
|
+
matches: live,
|
|
656
|
+
...(fresh.field ?? {}),
|
|
561
657
|
}) + '\n');
|
|
562
658
|
return 0;
|
|
563
659
|
}
|
|
564
660
|
const headerLabel = query ?? (hasUnresolved ? 'files with unresolved imports' : '');
|
|
565
|
-
if (
|
|
661
|
+
if (live.length === 0) {
|
|
566
662
|
process.stdout.write(`No matches for "${headerLabel}".\n`);
|
|
567
663
|
return 0;
|
|
568
664
|
}
|
|
569
665
|
process.stdout.write(header(`Graph search: ${headerLabel}`));
|
|
570
|
-
for (const m of
|
|
666
|
+
for (const m of live) {
|
|
571
667
|
process.stdout.write(` ${m.kind.padEnd(8)} ${m.label}${m.path ? ' ' + m.path : ''}${m.line ? ':' + m.line : ''}\n`);
|
|
572
668
|
}
|
|
669
|
+
if (truncated) {
|
|
670
|
+
process.stdout.write(`\n(${adjustedTotal - live.length} more — pass --limit ${adjustedTotal} to see all)\n`);
|
|
671
|
+
}
|
|
573
672
|
return 0;
|
|
574
673
|
}
|
|
575
674
|
// ─── shrk graph context ───────────────────────────────────────────────
|
|
@@ -578,10 +677,11 @@ export async function runGraphContext(args) {
|
|
|
578
677
|
const wantJson = flagBool(args, 'json');
|
|
579
678
|
const target = args.positional[1];
|
|
580
679
|
if (!target) {
|
|
581
|
-
process.stderr.write('Usage: shrk graph context <fileOrSymbol> [--depth N] [--no-bridge] [--no-framework]\n');
|
|
680
|
+
process.stderr.write('Usage: shrk graph context <fileOrSymbol> [--depth N] [--limit N|0] [--no-bridge] [--no-framework]\n');
|
|
582
681
|
return 2;
|
|
583
682
|
}
|
|
584
|
-
const depth = Math.
|
|
683
|
+
const depth = Math.min(3, flagPositiveInt(args, 'depth', 1));
|
|
684
|
+
const cap = resolveListLimit(args, CONTEXT_LIST_CAP);
|
|
585
685
|
const includeBridge = !flagBool(args, 'no-bridge');
|
|
586
686
|
const includeFramework = !flagBool(args, 'no-framework');
|
|
587
687
|
maybeRefresh(args, cwd);
|
|
@@ -622,16 +722,18 @@ export async function runGraphContext(args) {
|
|
|
622
722
|
const frameworkEntities = (includeFramework && frameworkStore.exists() && anchorFile?.path)
|
|
623
723
|
? FrameworkQueryApi.fromStore(cwd).forFile(anchorFile.path)
|
|
624
724
|
: [];
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
725
|
+
// Pre-slice the homogeneous filtered arrays so the payload can report the TRUE
|
|
726
|
+
// count + a truncated flag instead of silently capping each list at 50.
|
|
727
|
+
const importsFromAll = neighbours.out.filter((o) => o.edge.kind === 'imports-file');
|
|
728
|
+
const importedByAll = neighbours.in.filter((i) => i.edge.kind === 'imports-file');
|
|
729
|
+
const importsFromList = importsFromAll
|
|
730
|
+
.slice(0, cap)
|
|
628
731
|
.map((o) => ('target' in o ? targetSummary(o.target) : { id: 'unknown', resolved: false }));
|
|
629
|
-
const importedByList =
|
|
630
|
-
.
|
|
631
|
-
.slice(0, 50)
|
|
732
|
+
const importedByList = importedByAll
|
|
733
|
+
.slice(0, cap)
|
|
632
734
|
.map((i) => ('source' in i ? sourceSummary(i.source) : { id: 'unknown', resolved: false }));
|
|
633
|
-
const referencedByList = references.slice(0,
|
|
634
|
-
const calledByList = callers.slice(0,
|
|
735
|
+
const referencedByList = references.slice(0, cap).map(nodeSummary);
|
|
736
|
+
const calledByList = callers.slice(0, cap).map(nodeSummary);
|
|
635
737
|
// Staleness over the anchor + every referenced file: drop dead paths from the
|
|
636
738
|
// usage lists, flag changed ones.
|
|
637
739
|
const ctxPathOf = (x) => x.path;
|
|
@@ -649,12 +751,20 @@ export async function runGraphContext(args) {
|
|
|
649
751
|
declaredIn: anchor.kind === NodeKind.Symbol && anchorFile ? nodeSummary(anchorFile) : null,
|
|
650
752
|
depth,
|
|
651
753
|
importsFrom: ctxDropDel(importsFromList),
|
|
754
|
+
totalImportsFrom: importsFromAll.length,
|
|
755
|
+
importsFromTruncated: importsFromAll.length > cap,
|
|
652
756
|
importedBy: ctxDropDel(importedByList),
|
|
653
|
-
|
|
757
|
+
totalImportedBy: importedByAll.length,
|
|
758
|
+
importedByTruncated: importedByAll.length > cap,
|
|
759
|
+
symbols: symbols.slice(0, cap).map(nodeSummary),
|
|
654
760
|
referencedBy: ctxDropDel(referencedByList),
|
|
761
|
+
totalReferencedBy: references.length,
|
|
762
|
+
referencedByTruncated: references.length > cap,
|
|
655
763
|
calledBy: ctxDropDel(calledByList),
|
|
656
|
-
|
|
657
|
-
|
|
764
|
+
totalCalledBy: callers.length,
|
|
765
|
+
calledByTruncated: callers.length > cap,
|
|
766
|
+
...(subtypes.length > 0 ? { subtypes: subtypes.slice(0, cap).map(nodeSummary) } : {}),
|
|
767
|
+
...(supertypes.length > 0 ? { supertypes: supertypes.slice(0, cap).map(nodeSummary) } : {}),
|
|
658
768
|
...(fresh.field ?? {}),
|
|
659
769
|
bridge: bridgeFor
|
|
660
770
|
? {
|
|
@@ -769,11 +879,11 @@ export async function runGraphImpact(args) {
|
|
|
769
879
|
const wantFull = flagBool(args, 'full');
|
|
770
880
|
const target = args.positional[1];
|
|
771
881
|
if (!target) {
|
|
772
|
-
process.stderr.write('Usage: shrk graph impact <fileOrSymbol> [--max-depth N] [--limit N] [--full]\n');
|
|
882
|
+
process.stderr.write('Usage: shrk graph impact <fileOrSymbol> [--max-depth N] [--limit N|0] [--full]\n');
|
|
773
883
|
return 2;
|
|
774
884
|
}
|
|
775
|
-
const maxDepth = Math.
|
|
776
|
-
const limit =
|
|
885
|
+
const maxDepth = Math.min(10, flagPositiveInt(args, 'max-depth', 5));
|
|
886
|
+
const limit = resolveListLimit(args, 200);
|
|
777
887
|
maybeRefresh(args, cwd);
|
|
778
888
|
// --full → delegate to the impact-engine for a richer v3 payload.
|
|
779
889
|
if (wantFull) {
|
|
@@ -858,7 +968,8 @@ export async function runGraphImpact(args) {
|
|
|
858
968
|
schema: 'sharkcraft.graph-impact/v1',
|
|
859
969
|
anchor: nodeSummary(anchor),
|
|
860
970
|
maxDepth,
|
|
861
|
-
limit
|
|
971
|
+
// 0 signals "unbounded" (`--limit 0`); JSON can't carry Infinity.
|
|
972
|
+
limit: Number.isFinite(limit) ? limit : 0,
|
|
862
973
|
truncated: closure.truncated,
|
|
863
974
|
directDependents: liveDirect,
|
|
864
975
|
transitiveDependents: liveTransitive,
|
|
@@ -899,8 +1010,22 @@ export async function runGraphImpact(args) {
|
|
|
899
1010
|
export async function runGraphHubs(args) {
|
|
900
1011
|
const cwd = resolveCwd(args);
|
|
901
1012
|
const wantJson = flagBool(args, 'json');
|
|
902
|
-
const limit = Math.
|
|
903
|
-
|
|
1013
|
+
const limit = Math.min(100, flagPositiveInt(args, 'limit', 15));
|
|
1014
|
+
// A positional scope (`shrk graph hubs packages/cli`) is honoured the same as
|
|
1015
|
+
// --path; passing BOTH is ambiguous, so error rather than silently ignoring
|
|
1016
|
+
// the positional (which previously yielded a byte-identical global report).
|
|
1017
|
+
const pathFlag = flagString(args, 'path');
|
|
1018
|
+
const positionalPath = args.positional[1];
|
|
1019
|
+
if (pathFlag && positionalPath) {
|
|
1020
|
+
if (wantJson) {
|
|
1021
|
+
process.stdout.write(asJson({ ok: false, error: 'ambiguous-path', positional: positionalPath, flag: pathFlag }) + '\n');
|
|
1022
|
+
}
|
|
1023
|
+
else {
|
|
1024
|
+
process.stderr.write(`Pass the scope path either as a positional or with --path, not both (got "${positionalPath}" and --path ${pathFlag}).\n`);
|
|
1025
|
+
}
|
|
1026
|
+
return 2;
|
|
1027
|
+
}
|
|
1028
|
+
const pathScope = pathFlag ?? positionalPath;
|
|
904
1029
|
maybeRefresh(args, cwd);
|
|
905
1030
|
const api = loadOrFail(cwd, wantJson);
|
|
906
1031
|
if (!api)
|
|
@@ -947,16 +1072,15 @@ export async function runGraphCallers(args) {
|
|
|
947
1072
|
const wantJson = flagBool(args, 'json');
|
|
948
1073
|
const target = args.positional[1];
|
|
949
1074
|
if (!target) {
|
|
950
|
-
process.stderr.write('Usage: shrk graph callers <symbol> [--mode call|reference] [--limit N] [--no-refresh]\n');
|
|
1075
|
+
process.stderr.write('Usage: shrk graph callers <symbol> [--mode call|reference] [--limit N|0] [--no-refresh]\n');
|
|
951
1076
|
return 2;
|
|
952
1077
|
}
|
|
953
1078
|
const mode = (flagString(args, 'mode') ?? 'call');
|
|
954
|
-
// --limit N: cap the returned call sites (default 200
|
|
955
|
-
// the true uncapped count, so a truncated result stays
|
|
956
|
-
// non-numeric input
|
|
957
|
-
//
|
|
958
|
-
const
|
|
959
|
-
const limit = Number.isFinite(parsedLimit) && parsedLimit > 0 ? parsedLimit : 200;
|
|
1079
|
+
// --limit N: cap the returned call sites (default 200; `--limit 0` = all).
|
|
1080
|
+
// `total` still reports the true uncapped count, so a truncated result stays
|
|
1081
|
+
// honest. resolveListLimit guards non-numeric input (NaN → fallback) so a
|
|
1082
|
+
// fat-fingered `--limit foo` can't zero the list via `slice(0, NaN)`.
|
|
1083
|
+
const limit = resolveListLimit(args, 200);
|
|
960
1084
|
maybeRefresh(args, cwd);
|
|
961
1085
|
const api = loadOrFail(cwd, wantJson);
|
|
962
1086
|
if (!api)
|
|
@@ -1081,7 +1205,7 @@ export async function runGraphPath(args) {
|
|
|
1081
1205
|
process.stderr.write('Usage: shrk graph path <from> <to> [--max-depth N] [--no-refresh] [--json]\n');
|
|
1082
1206
|
return 2;
|
|
1083
1207
|
}
|
|
1084
|
-
const maxDepth = Math.
|
|
1208
|
+
const maxDepth = Math.min(32, flagPositiveInt(args, 'max-depth', 16));
|
|
1085
1209
|
maybeRefresh(args, cwd);
|
|
1086
1210
|
const api = loadOrFail(cwd, wantJson);
|
|
1087
1211
|
if (!api)
|
|
@@ -1183,6 +1307,45 @@ export async function runGraphPath(args) {
|
|
|
1183
1307
|
return 0;
|
|
1184
1308
|
}
|
|
1185
1309
|
// ─── helpers ──────────────────────────────────────────────────────────
|
|
1310
|
+
/**
|
|
1311
|
+
* Convert the typed corrupt-store error (one bad JSONL line) into a
|
|
1312
|
+
* deterministic "rebuild the index" message + non-zero exit, instead of letting
|
|
1313
|
+
* an unhandled `Fatal: JSON Parse error` escape the CLI. Returns the exit code
|
|
1314
|
+
* (1) when `err` is a corrupt-store error, or null to signal "not mine — rethrow".
|
|
1315
|
+
*/
|
|
1316
|
+
function emitCorruptStore(err, wantJson) {
|
|
1317
|
+
if (!isGraphStoreCorruptError(err))
|
|
1318
|
+
return null;
|
|
1319
|
+
if (wantJson) {
|
|
1320
|
+
process.stdout.write(asJson({
|
|
1321
|
+
ok: false,
|
|
1322
|
+
state: 'corrupt',
|
|
1323
|
+
nextCommand: 'shrk graph index',
|
|
1324
|
+
message: CORRUPT_HINT,
|
|
1325
|
+
...(err.details ?? {}),
|
|
1326
|
+
}) + '\n');
|
|
1327
|
+
}
|
|
1328
|
+
else {
|
|
1329
|
+
process.stderr.write(CORRUPT_HINT + '\n');
|
|
1330
|
+
}
|
|
1331
|
+
return 1;
|
|
1332
|
+
}
|
|
1333
|
+
/**
|
|
1334
|
+
* Run a store-loading thunk, mapping a corrupt store to a clean rebuild hint.
|
|
1335
|
+
* Returns the loaded value, or undefined when the store was corrupt (the
|
|
1336
|
+
* message + exit have already been emitted; the caller should `return 1`). Any
|
|
1337
|
+
* other error is rethrown unchanged.
|
|
1338
|
+
*/
|
|
1339
|
+
function loadGuarded(load, wantJson) {
|
|
1340
|
+
try {
|
|
1341
|
+
return load();
|
|
1342
|
+
}
|
|
1343
|
+
catch (err) {
|
|
1344
|
+
if (emitCorruptStore(err, wantJson) !== null)
|
|
1345
|
+
return undefined;
|
|
1346
|
+
throw err;
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1186
1349
|
function loadOrFail(cwd, wantJson) {
|
|
1187
1350
|
const store = new GraphStore(cwd);
|
|
1188
1351
|
if (!store.exists()) {
|
|
@@ -1199,7 +1362,7 @@ function loadOrFail(cwd, wantJson) {
|
|
|
1199
1362
|
}
|
|
1200
1363
|
return undefined;
|
|
1201
1364
|
}
|
|
1202
|
-
return GraphQueryApi.fromStore(cwd);
|
|
1365
|
+
return loadGuarded(() => GraphQueryApi.fromStore(cwd), wantJson);
|
|
1203
1366
|
}
|
|
1204
1367
|
function resolveAnchor(api, target) {
|
|
1205
1368
|
// Exact node id wins.
|
|
@@ -1221,43 +1384,14 @@ function resolveAnchor(api, target) {
|
|
|
1221
1384
|
return syms[0];
|
|
1222
1385
|
return undefined;
|
|
1223
1386
|
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Delegate to the shared {@link GraphQueryApi.searchNodes} so the CLI and the
|
|
1389
|
+
* MCP `get_graph_search` tool can never disagree on results OR the honest
|
|
1390
|
+
* pre-slice `total` (the two surfaces used to maintain divergent copies of this
|
|
1391
|
+
* fuzzy-match logic). Returns the display page + the true match count.
|
|
1392
|
+
*/
|
|
1224
1393
|
function collectSearchMatches(api, query, kind, limit) {
|
|
1225
|
-
|
|
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);
|
|
1394
|
+
return api.searchNodes(query, { ...(kind ? { kind } : {}), limit });
|
|
1261
1395
|
}
|
|
1262
1396
|
function reverseClosure(api, anchor, maxDepth, limit) {
|
|
1263
1397
|
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,
|
|
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":"help.command.d.ts","sourceRoot":"","sources":["../../src/commands/help.command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"help.command.d.ts","sourceRoot":"","sources":["../../src/commands/help.command.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AA4B9D;;;;;;;;GAQG;AACH,wBAAgB,iBAAiB,IAAI,MAAM,CAwC1C;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,eAAe;;;;cAK3C;QAAE,UAAU,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,CAAA;KAAE,GAAG,MAAM;EAwIpF"}
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { header } from "../output/format-output.js";
|
|
2
|
-
import { COMMAND_CATALOG, defaultShowInHelp } from "./command-catalog.js";
|
|
2
|
+
import { COMMAND_CATALOG, defaultShowInHelp, listExplainFamily } from "./command-catalog.js";
|
|
3
|
+
/** First sentence of a catalog description, for the compact explain-family list. */
|
|
4
|
+
function firstSentence(description) {
|
|
5
|
+
const dot = description.indexOf('. ');
|
|
6
|
+
const head = dot > 0 ? description.slice(0, dot + 1) : description;
|
|
7
|
+
return head.length > 100 ? head.slice(0, 97).trimEnd() + '…' : head;
|
|
8
|
+
}
|
|
3
9
|
const EXTRA_HELP_LINES = Object.freeze({
|
|
4
10
|
graph: [
|
|
5
11
|
'',
|
|
@@ -60,7 +66,7 @@ export function renderStartScreen() {
|
|
|
60
66
|
lines.push('Discover the rest (everything stays callable — this screen shows ~17 of ~70 verbs):');
|
|
61
67
|
lines.push(' $ shrk surface list — full catalog by tier');
|
|
62
68
|
lines.push(' $ shrk help <command> — usage for a specific command');
|
|
63
|
-
lines.push(' $ shrk --full-help — long, exhaustive help');
|
|
69
|
+
lines.push(' $ shrk --full-help — long, exhaustive help (incl. the explain/dry-run family)');
|
|
64
70
|
lines.push(' $ shrk --about — what shrk is and is not');
|
|
65
71
|
lines.push('');
|
|
66
72
|
lines.push('Free-form input is fine — `shrk "<task>"` routes to `shrk recommend`.');
|
|
@@ -188,6 +194,18 @@ export function makeHelpCommand(registry) {
|
|
|
188
194
|
process.stdout.write(` ${group} ${s.name.padEnd(10)} — ${s.description}\n`);
|
|
189
195
|
}
|
|
190
196
|
}
|
|
197
|
+
// Explain / dry-run family — several carry an Advanced surface and are
|
|
198
|
+
// filtered out of the listings above, so surface them explicitly. These
|
|
199
|
+
// show you what a gate, ranker, or graph SEES before you act; an agent
|
|
200
|
+
// would otherwise find them only by guessing (the #4.4 friction).
|
|
201
|
+
const explainFamily = listExplainFamily();
|
|
202
|
+
if (explainFamily.length > 0) {
|
|
203
|
+
process.stdout.write(header('Inspect / explain (dry-run what a gate, ranker, or graph sees)'));
|
|
204
|
+
for (const e of explainFamily) {
|
|
205
|
+
process.stdout.write(` ${e.command.padEnd(24)} — ${firstSentence(e.description)}\n`);
|
|
206
|
+
}
|
|
207
|
+
process.stdout.write(' (also: shrk check wiring --explain <ruleId>)\n');
|
|
208
|
+
}
|
|
191
209
|
process.stdout.write('\nRun `shrk help <command>` for detailed usage.\n');
|
|
192
210
|
return 0;
|
|
193
211
|
},
|
|
@@ -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;
|
|
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;AA2ahC,eAAO,MAAM,aAAa,EAAE,eAsW3B,CAAC"}
|