@shrkcrft/cli 0.1.0-alpha.1 → 0.1.0-alpha.11
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/README.md +1 -1
- package/dist/commands/api-diff.command.d.ts +11 -0
- package/dist/commands/api-diff.command.d.ts.map +1 -0
- package/dist/commands/api-diff.command.js +116 -0
- package/dist/commands/arch.command.d.ts +9 -0
- package/dist/commands/arch.command.d.ts.map +1 -0
- package/dist/commands/arch.command.js +186 -0
- package/dist/commands/boundaries.command.d.ts.map +1 -1
- package/dist/commands/boundaries.command.js +0 -12
- package/dist/commands/check.command.d.ts.map +1 -1
- package/dist/commands/check.command.js +20 -30
- package/dist/commands/code-intel.command.d.ts +18 -0
- package/dist/commands/code-intel.command.d.ts.map +1 -0
- package/dist/commands/code-intel.command.js +146 -0
- package/dist/commands/command-catalog.d.ts +7 -3
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +201 -47
- package/dist/commands/commands.command.d.ts.map +1 -1
- package/dist/commands/commands.command.js +4 -4
- package/dist/commands/completion.command.d.ts +10 -0
- package/dist/commands/completion.command.d.ts.map +1 -0
- package/dist/commands/completion.command.js +121 -0
- package/dist/commands/constructs.command.d.ts.map +1 -1
- package/dist/commands/constructs.command.js +5 -22
- package/dist/commands/context.command.d.ts.map +1 -1
- package/dist/commands/context.command.js +89 -0
- package/dist/commands/diff-check.command.d.ts +30 -0
- package/dist/commands/diff-check.command.d.ts.map +1 -0
- package/dist/commands/diff-check.command.js +210 -0
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +42 -9
- package/dist/commands/export.command.d.ts.map +1 -1
- package/dist/commands/export.command.js +76 -3
- package/dist/commands/framework.command.d.ts +12 -0
- package/dist/commands/framework.command.d.ts.map +1 -0
- package/dist/commands/framework.command.js +180 -0
- package/dist/commands/gate.command.d.ts +15 -0
- package/dist/commands/gate.command.d.ts.map +1 -0
- package/dist/commands/gate.command.js +296 -0
- package/dist/commands/graph-code-subverbs.d.ts +11 -0
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -0
- package/dist/commands/graph-code-subverbs.js +818 -0
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +22 -0
- package/dist/commands/help.command.d.ts +4 -3
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +77 -21
- package/dist/commands/helper.command.js +1 -1
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +170 -1
- package/dist/commands/import.command.d.ts.map +1 -1
- package/dist/commands/import.command.js +121 -5
- package/dist/commands/init.command.d.ts.map +1 -1
- package/dist/commands/init.command.js +184 -16
- package/dist/commands/mcp.command.d.ts.map +1 -1
- package/dist/commands/mcp.command.js +2 -131
- package/dist/commands/migrate.command.d.ts +13 -0
- package/dist/commands/migrate.command.d.ts.map +1 -0
- package/dist/commands/migrate.command.js +152 -0
- package/dist/commands/onboard.command.d.ts.map +1 -1
- package/dist/commands/onboard.command.js +3 -15
- package/dist/commands/packs-new.d.ts +1 -1
- package/dist/commands/packs-new.d.ts.map +1 -1
- package/dist/commands/packs-new.js +5 -36
- package/dist/commands/packs.command.d.ts.map +1 -1
- package/dist/commands/packs.command.js +3 -17
- package/dist/commands/plan-context.command.d.ts +11 -0
- package/dist/commands/plan-context.command.d.ts.map +1 -0
- package/dist/commands/plan-context.command.js +77 -0
- package/dist/commands/profiles.command.js +4 -4
- package/dist/commands/release.command.js +13 -13
- package/dist/commands/review.command.d.ts.map +1 -1
- package/dist/commands/review.command.js +2 -28
- package/dist/commands/rule-graph-subverbs.d.ts +3 -0
- package/dist/commands/rule-graph-subverbs.d.ts.map +1 -0
- package/dist/commands/rule-graph-subverbs.js +132 -0
- package/dist/commands/search-structural.command.d.ts +18 -0
- package/dist/commands/search-structural.command.d.ts.map +1 -0
- package/dist/commands/search-structural.command.js +376 -0
- package/dist/commands/search.command.js +1 -1
- package/dist/commands/task-context.command.js +0 -16
- package/dist/commands/task.command.d.ts.map +1 -1
- package/dist/commands/task.command.js +8 -2
- package/dist/dashboard/code-intelligence-data.d.ts +33 -0
- package/dist/dashboard/code-intelligence-data.d.ts.map +1 -0
- package/dist/dashboard/code-intelligence-data.js +307 -0
- package/dist/dashboard/dashboard-api-server.d.ts.map +1 -1
- package/dist/dashboard/dashboard-api-server.js +162 -1
- package/dist/export/claude-commands-export.d.ts +60 -0
- package/dist/export/claude-commands-export.d.ts.map +1 -0
- package/dist/export/claude-commands-export.js +276 -0
- package/dist/export/export-formats.d.ts +1 -1
- package/dist/export/export-formats.d.ts.map +1 -1
- package/dist/export/export-formats.js +139 -12
- package/dist/init/init-templates.d.ts.map +1 -1
- package/dist/init/init-templates.js +133 -113
- package/dist/init/paths-advisory.d.ts +20 -0
- package/dist/init/paths-advisory.d.ts.map +1 -0
- package/dist/init/paths-advisory.js +88 -0
- package/dist/main.d.ts +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +137 -46
- package/dist/output/failure-hints.d.ts +1 -9
- package/dist/output/failure-hints.d.ts.map +1 -1
- package/dist/output/failure-hints.js +2 -8
- package/dist/output/watch-loop.d.ts +9 -1
- package/dist/output/watch-loop.d.ts.map +1 -1
- package/dist/output/watch-loop.js +13 -3
- package/dist/schemas/json-schemas.d.ts +36 -36
- package/dist/schemas/json-schemas.js +36 -36
- package/dist/surface/about.d.ts.map +1 -1
- package/dist/surface/about.js +37 -15
- package/dist/surface/no-args-landing.d.ts.map +1 -1
- package/dist/surface/no-args-landing.js +9 -13
- package/dist/surface/surface-config-writer.d.ts.map +1 -1
- package/dist/surface/surface-config-writer.js +23 -11
- package/package.json +37 -25
- package/dist/commands/plugin.command.d.ts +0 -11
- package/dist/commands/plugin.command.d.ts.map +0 -1
- package/dist/commands/plugin.command.js +0 -394
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CLI subverbs for the `@shrkcrft/graph` code-intelligence layer.
|
|
3
|
+
*
|
|
4
|
+
* Lives separately from `graph.command.ts` to keep the dispatch file
|
|
5
|
+
* focused. The entry command imports each `run*` and routes when the
|
|
6
|
+
* first positional matches the subverb name.
|
|
7
|
+
*/
|
|
8
|
+
import { buildFullIndex, changedFilesSince, detectChangedAndDeleted, EdgeKind, GraphQueryApi, GraphStore, NodeKind, updateChanged, } from '@shrkcrft/graph';
|
|
9
|
+
import { analyzeGraphImpact } from '@shrkcrft/impact-engine';
|
|
10
|
+
import { BridgeStore, RuleGraphQueryApi } from '@shrkcrft/rule-graph';
|
|
11
|
+
import { FrameworkQueryApi, FrameworkStore } from '@shrkcrft/framework-scanners';
|
|
12
|
+
import { flagBool, flagString, resolveCwd } from "../command-registry.js";
|
|
13
|
+
import { asJson, header, kv } from "../output/format-output.js";
|
|
14
|
+
import { maybeRunInWatchMode } from "../output/watch-loop.js";
|
|
15
|
+
const STALE_HINT = `Index is missing or stale. Run 'shrk graph index' to build it.`;
|
|
16
|
+
// ─── shrk graph index ─────────────────────────────────────────────────
|
|
17
|
+
export async function runGraphIndex(args) {
|
|
18
|
+
// --watch: run the index once, then re-run on file changes. Every
|
|
19
|
+
// tick after the first uses the incremental updater so a 5-file edit
|
|
20
|
+
// takes < 100ms. Default watch path is the project root; pass
|
|
21
|
+
// `--paths a,b,c` to narrow.
|
|
22
|
+
const watchExit = await maybeRunInWatchMode(args, async (inner) => {
|
|
23
|
+
const innerFlags = new Map(inner.flags);
|
|
24
|
+
innerFlags.set('changed', true);
|
|
25
|
+
return runGraphIndexOnce({ ...inner, flags: innerFlags });
|
|
26
|
+
}, { defaultPaths: ['.'] });
|
|
27
|
+
if (watchExit !== null)
|
|
28
|
+
return watchExit;
|
|
29
|
+
return runGraphIndexOnce(args);
|
|
30
|
+
}
|
|
31
|
+
async function runGraphIndexOnce(args) {
|
|
32
|
+
const cwd = resolveCwd(args);
|
|
33
|
+
const wantJson = flagBool(args, 'json');
|
|
34
|
+
const wantChanged = flagBool(args, 'changed');
|
|
35
|
+
const since = flagString(args, 'since');
|
|
36
|
+
const wantFull = flagBool(args, 'full');
|
|
37
|
+
// Incremental path: --changed OR --since OR no store yet but the user
|
|
38
|
+
// asked for incremental — fall through to a full build in that case.
|
|
39
|
+
const store = new GraphStore(cwd);
|
|
40
|
+
const isIncremental = (wantChanged || since) && !wantFull;
|
|
41
|
+
if (isIncremental && store.exists()) {
|
|
42
|
+
let changed = [];
|
|
43
|
+
let deleted = [];
|
|
44
|
+
if (since) {
|
|
45
|
+
changed = changedFilesSince(cwd, since);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
const detected = detectChangedAndDeleted(cwd);
|
|
49
|
+
changed = detected.changed;
|
|
50
|
+
deleted = detected.deleted;
|
|
51
|
+
}
|
|
52
|
+
const result = updateChanged({ projectRoot: cwd, changedFiles: changed, deletedFiles: deleted });
|
|
53
|
+
if (wantJson) {
|
|
54
|
+
process.stdout.write(asJson({
|
|
55
|
+
ok: true,
|
|
56
|
+
mode: 'incremental',
|
|
57
|
+
manifest: result.manifest,
|
|
58
|
+
durationMs: result.durationMs,
|
|
59
|
+
updated: result.updated,
|
|
60
|
+
deleted: result.deleted,
|
|
61
|
+
skipped: result.skipped,
|
|
62
|
+
}) + '\n');
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write(header('Graph index (incremental)'));
|
|
66
|
+
process.stdout.write(kv('updated', String(result.updated.length)) + '\n');
|
|
67
|
+
process.stdout.write(kv('deleted', String(result.deleted.length)) + '\n');
|
|
68
|
+
process.stdout.write(kv('skipped', String(result.skipped.length)) + '\n');
|
|
69
|
+
process.stdout.write(kv('files total', String(result.manifest.filesIndexed)) + '\n');
|
|
70
|
+
process.stdout.write(kv('duration', `${result.durationMs}ms`) + '\n');
|
|
71
|
+
process.stdout.write(kv('digest', result.manifest.digest.slice(0, 12) + '…') + '\n');
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
// Full path.
|
|
75
|
+
const result = buildFullIndex({ projectRoot: cwd });
|
|
76
|
+
if (wantJson) {
|
|
77
|
+
process.stdout.write(asJson({
|
|
78
|
+
ok: true,
|
|
79
|
+
mode: 'full',
|
|
80
|
+
manifest: result.manifest,
|
|
81
|
+
durationMs: result.durationMs,
|
|
82
|
+
}) + '\n');
|
|
83
|
+
return 0;
|
|
84
|
+
}
|
|
85
|
+
process.stdout.write(header('Graph index'));
|
|
86
|
+
process.stdout.write(kv('files', String(result.manifest.filesIndexed)) + '\n');
|
|
87
|
+
process.stdout.write(kv('nodes', String(sumValues(result.manifest.nodesByKind))) + '\n');
|
|
88
|
+
process.stdout.write(kv('edges', String(sumValues(result.manifest.edgesByKind))) + '\n');
|
|
89
|
+
process.stdout.write(kv('packages', String(result.manifest.workspacePackages.length)) + '\n');
|
|
90
|
+
if (typeof result.manifest.cycleCount === 'number') {
|
|
91
|
+
const largest = typeof result.manifest.largestCycleSize === 'number' && result.manifest.largestCycleSize > 0
|
|
92
|
+
? ` (largest ${result.manifest.largestCycleSize})`
|
|
93
|
+
: '';
|
|
94
|
+
process.stdout.write(kv('cycles', `${result.manifest.cycleCount}${largest}`) + '\n');
|
|
95
|
+
}
|
|
96
|
+
if (typeof result.manifest.unresolvedImportCount === 'number' &&
|
|
97
|
+
result.manifest.unresolvedImportCount > 0) {
|
|
98
|
+
process.stdout.write(kv('unresolved imports', `${result.manifest.unresolvedImportCount} across ${result.manifest.filesWithUnresolvedImports ?? 0} file(s)`) + '\n');
|
|
99
|
+
}
|
|
100
|
+
process.stdout.write(kv('duration', `${result.durationMs}ms`) + '\n');
|
|
101
|
+
process.stdout.write(kv('digest', result.manifest.digest.slice(0, 12) + '…') + '\n');
|
|
102
|
+
return 0;
|
|
103
|
+
}
|
|
104
|
+
// ─── shrk graph cycles ────────────────────────────────────────────────
|
|
105
|
+
export async function runGraphCycles(args) {
|
|
106
|
+
const cwd = resolveCwd(args);
|
|
107
|
+
const wantJson = flagBool(args, 'json');
|
|
108
|
+
const limit = parseLimit(args);
|
|
109
|
+
const minSize = parseMinSize(args);
|
|
110
|
+
const store = new GraphStore(cwd);
|
|
111
|
+
if (!store.exists()) {
|
|
112
|
+
if (wantJson) {
|
|
113
|
+
process.stdout.write(asJson({
|
|
114
|
+
ok: false,
|
|
115
|
+
state: 'missing',
|
|
116
|
+
nextCommand: 'shrk graph index',
|
|
117
|
+
message: STALE_HINT,
|
|
118
|
+
}) + '\n');
|
|
119
|
+
return 1;
|
|
120
|
+
}
|
|
121
|
+
process.stderr.write(STALE_HINT + '\n');
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
const api = GraphQueryApi.fromStore(cwd);
|
|
125
|
+
const allCycles = api.cycles();
|
|
126
|
+
const filtered = allCycles.filter((c) => c.size >= minSize);
|
|
127
|
+
const limited = filtered.slice(0, limit);
|
|
128
|
+
if (wantJson) {
|
|
129
|
+
process.stdout.write(asJson({
|
|
130
|
+
ok: true,
|
|
131
|
+
total: filtered.length,
|
|
132
|
+
truncated: filtered.length > limit,
|
|
133
|
+
cycles: limited.map((c) => ({
|
|
134
|
+
size: c.size,
|
|
135
|
+
paths: c.paths ?? c.nodeIds.map((id) => id.replace(/^file:/, '')),
|
|
136
|
+
})),
|
|
137
|
+
}) + '\n');
|
|
138
|
+
return 0;
|
|
139
|
+
}
|
|
140
|
+
process.stdout.write(header('Graph cycles'));
|
|
141
|
+
process.stdout.write(kv('total', String(filtered.length)) + '\n');
|
|
142
|
+
if (filtered.length === 0) {
|
|
143
|
+
process.stdout.write('\nNo cycles in the file-import graph. ✓\n');
|
|
144
|
+
return 0;
|
|
145
|
+
}
|
|
146
|
+
process.stdout.write(kv('shown', `${limited.length}/${filtered.length}`) + '\n');
|
|
147
|
+
process.stdout.write('\n');
|
|
148
|
+
for (let i = 0; i < limited.length; i += 1) {
|
|
149
|
+
const c = limited[i];
|
|
150
|
+
const paths = c.paths ?? c.nodeIds.map((id) => id.replace(/^file:/, ''));
|
|
151
|
+
process.stdout.write(`#${i + 1} (size ${c.size}):\n`);
|
|
152
|
+
for (const p of paths)
|
|
153
|
+
process.stdout.write(` ${p}\n`);
|
|
154
|
+
// Closing arrow indicates the cycle wraps back to the first node.
|
|
155
|
+
if (paths[0])
|
|
156
|
+
process.stdout.write(` → ${paths[0]}\n`);
|
|
157
|
+
if (i + 1 < limited.length)
|
|
158
|
+
process.stdout.write('\n');
|
|
159
|
+
}
|
|
160
|
+
if (filtered.length > limit) {
|
|
161
|
+
process.stdout.write(`\n(${filtered.length - limit} more — pass --limit ${filtered.length} to see all)\n`);
|
|
162
|
+
}
|
|
163
|
+
return 0;
|
|
164
|
+
}
|
|
165
|
+
function parseLimit(args) {
|
|
166
|
+
const raw = flagString(args, 'limit');
|
|
167
|
+
if (!raw)
|
|
168
|
+
return 20;
|
|
169
|
+
const n = Number.parseInt(raw, 10);
|
|
170
|
+
return Number.isFinite(n) && n > 0 ? n : 20;
|
|
171
|
+
}
|
|
172
|
+
function parseMinSize(args) {
|
|
173
|
+
const raw = flagString(args, 'min-size');
|
|
174
|
+
if (!raw)
|
|
175
|
+
return 2;
|
|
176
|
+
const n = Number.parseInt(raw, 10);
|
|
177
|
+
return Number.isFinite(n) && n >= 2 ? n : 2;
|
|
178
|
+
}
|
|
179
|
+
// ─── shrk graph unresolved ────────────────────────────────────────────
|
|
180
|
+
export async function runGraphUnresolved(args) {
|
|
181
|
+
const cwd = resolveCwd(args);
|
|
182
|
+
const wantJson = flagBool(args, 'json');
|
|
183
|
+
const limit = parseLimit(args);
|
|
184
|
+
const store = new GraphStore(cwd);
|
|
185
|
+
if (!store.exists()) {
|
|
186
|
+
if (wantJson) {
|
|
187
|
+
process.stdout.write(asJson({
|
|
188
|
+
ok: false,
|
|
189
|
+
state: 'missing',
|
|
190
|
+
nextCommand: 'shrk graph index',
|
|
191
|
+
message: STALE_HINT,
|
|
192
|
+
}) + '\n');
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
process.stderr.write(STALE_HINT + '\n');
|
|
196
|
+
return 1;
|
|
197
|
+
}
|
|
198
|
+
const snap = store.loadSnapshot();
|
|
199
|
+
const groups = new Map();
|
|
200
|
+
for (const e of snap.edges.values()) {
|
|
201
|
+
if (e.kind !== EdgeKind.ImportsFile)
|
|
202
|
+
continue;
|
|
203
|
+
if (!e.to.startsWith('unresolved:'))
|
|
204
|
+
continue;
|
|
205
|
+
const fromNode = snap.nodes.get(e.from);
|
|
206
|
+
const g = groups.get(e.from);
|
|
207
|
+
const spec = e.to.slice('unresolved:'.length);
|
|
208
|
+
if (g) {
|
|
209
|
+
g.specifiers.push(spec);
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
groups.set(e.from, {
|
|
213
|
+
from: e.from,
|
|
214
|
+
...(fromNode?.path ? { path: fromNode.path } : {}),
|
|
215
|
+
specifiers: [spec],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const list = [...groups.values()].sort((a, b) => {
|
|
220
|
+
if (b.specifiers.length !== a.specifiers.length) {
|
|
221
|
+
return b.specifiers.length - a.specifiers.length;
|
|
222
|
+
}
|
|
223
|
+
return (a.path ?? a.from).localeCompare(b.path ?? b.from);
|
|
224
|
+
});
|
|
225
|
+
// De-dupe specifiers per file + sort, so the output is stable.
|
|
226
|
+
for (const g of list)
|
|
227
|
+
g.specifiers = [...new Set(g.specifiers)].sort();
|
|
228
|
+
const total = list.reduce((n, g) => n + g.specifiers.length, 0);
|
|
229
|
+
const limited = list.slice(0, limit);
|
|
230
|
+
if (wantJson) {
|
|
231
|
+
process.stdout.write(asJson({
|
|
232
|
+
ok: true,
|
|
233
|
+
totalEdges: total,
|
|
234
|
+
totalFiles: list.length,
|
|
235
|
+
truncated: list.length > limit,
|
|
236
|
+
files: limited.map((g) => ({
|
|
237
|
+
path: g.path ?? g.from.replace(/^file:/, ''),
|
|
238
|
+
unresolved: g.specifiers,
|
|
239
|
+
})),
|
|
240
|
+
}) + '\n');
|
|
241
|
+
return 0;
|
|
242
|
+
}
|
|
243
|
+
process.stdout.write(header('Unresolved imports'));
|
|
244
|
+
process.stdout.write(kv('total edges', String(total)) + '\n');
|
|
245
|
+
process.stdout.write(kv('files', String(list.length)) + '\n');
|
|
246
|
+
if (list.length === 0) {
|
|
247
|
+
process.stdout.write('\nNo unresolved imports. ✓\n');
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
process.stdout.write(kv('shown', `${limited.length}/${list.length}`) + '\n');
|
|
251
|
+
process.stdout.write('\n');
|
|
252
|
+
for (const g of limited) {
|
|
253
|
+
process.stdout.write(`${g.path ?? g.from.replace(/^file:/, '')}\n`);
|
|
254
|
+
for (const s of g.specifiers) {
|
|
255
|
+
process.stdout.write(` • ${s}\n`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
if (list.length > limit) {
|
|
259
|
+
process.stdout.write(`\n(${list.length - limit} more — pass --limit ${list.length} to see all)\n`);
|
|
260
|
+
}
|
|
261
|
+
return 0;
|
|
262
|
+
}
|
|
263
|
+
// ─── shrk graph deps ──────────────────────────────────────────────────
|
|
264
|
+
export async function runGraphDeps(args) {
|
|
265
|
+
const cwd = resolveCwd(args);
|
|
266
|
+
const wantJson = flagBool(args, 'json');
|
|
267
|
+
const pkg = args.positional[0];
|
|
268
|
+
if (!pkg) {
|
|
269
|
+
if (wantJson) {
|
|
270
|
+
process.stdout.write(asJson({ ok: false, error: 'missing-package' }) + '\n');
|
|
271
|
+
return 2;
|
|
272
|
+
}
|
|
273
|
+
process.stderr.write('Usage: shrk graph deps <package-name> [--json]\n');
|
|
274
|
+
return 2;
|
|
275
|
+
}
|
|
276
|
+
const store = new GraphStore(cwd);
|
|
277
|
+
if (!store.exists()) {
|
|
278
|
+
if (wantJson) {
|
|
279
|
+
process.stdout.write(asJson({
|
|
280
|
+
ok: false,
|
|
281
|
+
state: 'missing',
|
|
282
|
+
nextCommand: 'shrk graph index',
|
|
283
|
+
message: STALE_HINT,
|
|
284
|
+
}) + '\n');
|
|
285
|
+
return 1;
|
|
286
|
+
}
|
|
287
|
+
process.stderr.write(STALE_HINT + '\n');
|
|
288
|
+
return 1;
|
|
289
|
+
}
|
|
290
|
+
const api = GraphQueryApi.fromStore(cwd);
|
|
291
|
+
const pkgId = `package:${pkg}`;
|
|
292
|
+
// outbound: packages this one depends on
|
|
293
|
+
const outbound = api.packageDeps(pkg).map((n) => n.id.replace(/^package:/, ''));
|
|
294
|
+
// inbound: packages that depend on this one
|
|
295
|
+
const inbound = [];
|
|
296
|
+
for (const p of api.allPackages()) {
|
|
297
|
+
const name = p.id.replace(/^package:/, '');
|
|
298
|
+
if (name === pkg)
|
|
299
|
+
continue;
|
|
300
|
+
if (api.packageDeps(name).some((n) => n.id === pkgId))
|
|
301
|
+
inbound.push(name);
|
|
302
|
+
}
|
|
303
|
+
outbound.sort();
|
|
304
|
+
inbound.sort();
|
|
305
|
+
if (wantJson) {
|
|
306
|
+
process.stdout.write(asJson({
|
|
307
|
+
ok: true,
|
|
308
|
+
package: pkg,
|
|
309
|
+
dependsOn: outbound,
|
|
310
|
+
dependedOnBy: inbound,
|
|
311
|
+
}) + '\n');
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
process.stdout.write(header(`Package deps: ${pkg}`));
|
|
315
|
+
process.stdout.write(kv('depends on', String(outbound.length)) + '\n');
|
|
316
|
+
process.stdout.write(kv('depended on by', String(inbound.length)) + '\n');
|
|
317
|
+
if (outbound.length > 0) {
|
|
318
|
+
process.stdout.write('\nDepends on:\n');
|
|
319
|
+
for (const n of outbound)
|
|
320
|
+
process.stdout.write(` → ${n}\n`);
|
|
321
|
+
}
|
|
322
|
+
if (inbound.length > 0) {
|
|
323
|
+
process.stdout.write('\nDepended on by:\n');
|
|
324
|
+
for (const n of inbound)
|
|
325
|
+
process.stdout.write(` ← ${n}\n`);
|
|
326
|
+
}
|
|
327
|
+
if (outbound.length === 0 && inbound.length === 0) {
|
|
328
|
+
process.stdout.write('\n(no workspace-internal edges)\n');
|
|
329
|
+
}
|
|
330
|
+
return 0;
|
|
331
|
+
}
|
|
332
|
+
// ─── shrk graph status ────────────────────────────────────────────────
|
|
333
|
+
export async function runGraphStatus(args) {
|
|
334
|
+
const cwd = resolveCwd(args);
|
|
335
|
+
const wantJson = flagBool(args, 'json');
|
|
336
|
+
const store = new GraphStore(cwd);
|
|
337
|
+
if (!store.exists()) {
|
|
338
|
+
const payload = {
|
|
339
|
+
ok: false,
|
|
340
|
+
state: 'missing',
|
|
341
|
+
nextCommand: 'shrk graph index',
|
|
342
|
+
message: STALE_HINT,
|
|
343
|
+
};
|
|
344
|
+
if (wantJson) {
|
|
345
|
+
process.stdout.write(asJson(payload) + '\n');
|
|
346
|
+
return 1;
|
|
347
|
+
}
|
|
348
|
+
process.stderr.write(STALE_HINT + '\n');
|
|
349
|
+
return 1;
|
|
350
|
+
}
|
|
351
|
+
const verify = store.verifyDigest();
|
|
352
|
+
const snap = store.loadSnapshot();
|
|
353
|
+
const payload = {
|
|
354
|
+
ok: verify.ok,
|
|
355
|
+
state: verify.ok ? 'fresh' : 'corrupt',
|
|
356
|
+
schema: snap.manifest.schema,
|
|
357
|
+
fileCount: snap.manifest.filesIndexed,
|
|
358
|
+
nodeCount: snap.nodes.size,
|
|
359
|
+
edgeCount: snap.edges.size,
|
|
360
|
+
lastIndexedAt: snap.manifest.lastIndexedAt,
|
|
361
|
+
lastIndexDurationMs: snap.manifest.lastIndexDurationMs,
|
|
362
|
+
workspacePackages: snap.manifest.workspacePackages,
|
|
363
|
+
cycleCount: snap.manifest.cycleCount ?? null,
|
|
364
|
+
largestCycleSize: snap.manifest.largestCycleSize ?? null,
|
|
365
|
+
filesInCycles: snap.manifest.filesInCycles ?? null,
|
|
366
|
+
unresolvedImportCount: snap.manifest.unresolvedImportCount ?? null,
|
|
367
|
+
filesWithUnresolvedImports: snap.manifest.filesWithUnresolvedImports ?? null,
|
|
368
|
+
unresolvedImportSamples: snap.manifest.unresolvedImportSamples ?? null,
|
|
369
|
+
digest: verify.ok ? snap.manifest.digest : { expected: verify.expected, actual: verify.actual },
|
|
370
|
+
};
|
|
371
|
+
if (wantJson) {
|
|
372
|
+
process.stdout.write(asJson(payload) + '\n');
|
|
373
|
+
return verify.ok ? 0 : 1;
|
|
374
|
+
}
|
|
375
|
+
process.stdout.write(header('Graph status'));
|
|
376
|
+
process.stdout.write(kv('schema', payload.schema) + '\n');
|
|
377
|
+
process.stdout.write(kv('files', String(payload.fileCount)) + '\n');
|
|
378
|
+
process.stdout.write(kv('nodes', String(payload.nodeCount)) + '\n');
|
|
379
|
+
process.stdout.write(kv('edges', String(payload.edgeCount)) + '\n');
|
|
380
|
+
process.stdout.write(kv('packages', String(payload.workspacePackages.length)) + '\n');
|
|
381
|
+
if (typeof payload.cycleCount === 'number') {
|
|
382
|
+
const largest = payload.largestCycleSize ? ` (largest ${payload.largestCycleSize})` : '';
|
|
383
|
+
process.stdout.write(kv('cycles', `${payload.cycleCount}${largest}`) + '\n');
|
|
384
|
+
}
|
|
385
|
+
if (typeof payload.unresolvedImportCount === 'number' && payload.unresolvedImportCount > 0) {
|
|
386
|
+
process.stdout.write(kv('unresolved imports', `${payload.unresolvedImportCount} across ${payload.filesWithUnresolvedImports ?? 0} file(s)`) + '\n');
|
|
387
|
+
}
|
|
388
|
+
process.stdout.write(kv('last indexed', payload.lastIndexedAt) + '\n');
|
|
389
|
+
process.stdout.write(kv('state', payload.state) + '\n');
|
|
390
|
+
return verify.ok ? 0 : 1;
|
|
391
|
+
}
|
|
392
|
+
// ─── shrk graph search ────────────────────────────────────────────────
|
|
393
|
+
export async function runGraphSearch(args) {
|
|
394
|
+
const cwd = resolveCwd(args);
|
|
395
|
+
const wantJson = flagBool(args, 'json');
|
|
396
|
+
const query = args.positional[1];
|
|
397
|
+
if (!query) {
|
|
398
|
+
process.stderr.write('Usage: shrk graph search <query> [--kind file|symbol|package] [--limit N]\n');
|
|
399
|
+
return 2;
|
|
400
|
+
}
|
|
401
|
+
const kindFlag = flagString(args, 'kind');
|
|
402
|
+
const limit = Number(flagString(args, 'limit') ?? '20');
|
|
403
|
+
const api = loadOrFail(cwd, wantJson);
|
|
404
|
+
if (!api)
|
|
405
|
+
return 1;
|
|
406
|
+
const matches = collectSearchMatches(api, query, kindFlag, limit);
|
|
407
|
+
if (wantJson) {
|
|
408
|
+
process.stdout.write(asJson({
|
|
409
|
+
schema: 'sharkcraft.graph-search/v1',
|
|
410
|
+
query,
|
|
411
|
+
kind: kindFlag ?? 'any',
|
|
412
|
+
total: matches.length,
|
|
413
|
+
matches: matches.map(toSearchHit),
|
|
414
|
+
}) + '\n');
|
|
415
|
+
return 0;
|
|
416
|
+
}
|
|
417
|
+
if (matches.length === 0) {
|
|
418
|
+
process.stdout.write(`No matches for "${query}".\n`);
|
|
419
|
+
return 0;
|
|
420
|
+
}
|
|
421
|
+
process.stdout.write(header(`Graph search: ${query}`));
|
|
422
|
+
for (const m of matches) {
|
|
423
|
+
process.stdout.write(` ${m.kind.padEnd(8)} ${m.label}${m.path ? ' ' + m.path : ''}${m.line ? ':' + m.line : ''}\n`);
|
|
424
|
+
}
|
|
425
|
+
return 0;
|
|
426
|
+
}
|
|
427
|
+
// ─── shrk graph context ───────────────────────────────────────────────
|
|
428
|
+
export async function runGraphContext(args) {
|
|
429
|
+
const cwd = resolveCwd(args);
|
|
430
|
+
const wantJson = flagBool(args, 'json');
|
|
431
|
+
const target = args.positional[1];
|
|
432
|
+
if (!target) {
|
|
433
|
+
process.stderr.write('Usage: shrk graph context <fileOrSymbol> [--depth N] [--no-bridge] [--no-framework]\n');
|
|
434
|
+
return 2;
|
|
435
|
+
}
|
|
436
|
+
const depth = Math.max(1, Math.min(3, Number(flagString(args, 'depth') ?? '1')));
|
|
437
|
+
const includeBridge = !flagBool(args, 'no-bridge');
|
|
438
|
+
const includeFramework = !flagBool(args, 'no-framework');
|
|
439
|
+
const api = loadOrFail(cwd, wantJson);
|
|
440
|
+
if (!api)
|
|
441
|
+
return 1;
|
|
442
|
+
const anchor = resolveAnchor(api, target);
|
|
443
|
+
if (!anchor) {
|
|
444
|
+
const payload = { ok: false, error: 'not-found', target };
|
|
445
|
+
if (wantJson) {
|
|
446
|
+
process.stdout.write(asJson(payload) + '\n');
|
|
447
|
+
return 1;
|
|
448
|
+
}
|
|
449
|
+
process.stderr.write(`No graph node matched "${target}".\n`);
|
|
450
|
+
return 1;
|
|
451
|
+
}
|
|
452
|
+
const neighbours = api.neighbours(anchor.id);
|
|
453
|
+
const symbols = anchor.kind === NodeKind.File ? api.symbolsIn(anchor.id) : [];
|
|
454
|
+
// Optional bridge enrichment: rules / paths / templates applying to
|
|
455
|
+
// the anchor file (only meaningful when anchor.kind === File).
|
|
456
|
+
const bridgeStore = new BridgeStore(cwd);
|
|
457
|
+
const bridgeFor = (includeBridge && bridgeStore.exists() && anchor.kind === NodeKind.File && anchor.path)
|
|
458
|
+
? RuleGraphQueryApi.fromStores(cwd).forFile(anchor.path)
|
|
459
|
+
: undefined;
|
|
460
|
+
// Optional framework enrichment.
|
|
461
|
+
const frameworkStore = new FrameworkStore(cwd);
|
|
462
|
+
const frameworkEntities = (includeFramework && frameworkStore.exists() && anchor.kind === NodeKind.File && anchor.path)
|
|
463
|
+
? FrameworkQueryApi.fromStore(cwd).forFile(anchor.path)
|
|
464
|
+
: [];
|
|
465
|
+
const payload = {
|
|
466
|
+
schema: 'sharkcraft.graph-context/v1',
|
|
467
|
+
anchor: nodeSummary(anchor),
|
|
468
|
+
depth,
|
|
469
|
+
importsFrom: neighbours.out
|
|
470
|
+
.filter((o) => o.edge.kind === 'imports-file')
|
|
471
|
+
.slice(0, 50)
|
|
472
|
+
.map((o) => 'target' in o ? targetSummary(o.target) : { id: 'unknown', resolved: false }),
|
|
473
|
+
importedBy: neighbours.in
|
|
474
|
+
.filter((i) => i.edge.kind === 'imports-file')
|
|
475
|
+
.slice(0, 50)
|
|
476
|
+
.map((i) => 'source' in i ? sourceSummary(i.source) : { id: 'unknown', resolved: false }),
|
|
477
|
+
symbols: symbols.slice(0, 50).map(nodeSummary),
|
|
478
|
+
bridge: bridgeFor
|
|
479
|
+
? {
|
|
480
|
+
rules: bridgeFor.rules.map((h) => ({
|
|
481
|
+
id: h.target.id,
|
|
482
|
+
label: h.target.label,
|
|
483
|
+
severity: h.edge.data?.['severity'] ?? undefined,
|
|
484
|
+
})),
|
|
485
|
+
paths: bridgeFor.paths.map((h) => ({ id: h.target.id, label: h.target.label })),
|
|
486
|
+
templates: bridgeFor.templates.map((h) => ({ id: h.target.id, label: h.target.label })),
|
|
487
|
+
}
|
|
488
|
+
: null,
|
|
489
|
+
framework: frameworkEntities.length > 0
|
|
490
|
+
? {
|
|
491
|
+
entities: frameworkEntities.map((n) => ({
|
|
492
|
+
id: n.id,
|
|
493
|
+
label: n.label,
|
|
494
|
+
framework: n.data?.['framework'] ?? null,
|
|
495
|
+
subtype: n.data?.['subtype'] ?? null,
|
|
496
|
+
})),
|
|
497
|
+
}
|
|
498
|
+
: null,
|
|
499
|
+
};
|
|
500
|
+
if (wantJson) {
|
|
501
|
+
process.stdout.write(asJson(payload) + '\n');
|
|
502
|
+
return 0;
|
|
503
|
+
}
|
|
504
|
+
process.stdout.write(header(`Graph context: ${anchor.kind}:${anchor.label}`));
|
|
505
|
+
process.stdout.write(kv('path', anchor.path ?? '(none)') + '\n');
|
|
506
|
+
if (payload.symbols.length > 0) {
|
|
507
|
+
process.stdout.write(`\nDeclares ${payload.symbols.length} symbols:\n`);
|
|
508
|
+
for (const s of payload.symbols.slice(0, 20)) {
|
|
509
|
+
process.stdout.write(` ${s.label}${s.line ? ':' + s.line : ''}\n`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
if (payload.importsFrom.length > 0) {
|
|
513
|
+
process.stdout.write(`\nImports from (${payload.importsFrom.length}):\n`);
|
|
514
|
+
for (const o of payload.importsFrom.slice(0, 20)) {
|
|
515
|
+
process.stdout.write(` → ${describeTarget(o)}\n`);
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (payload.importedBy.length > 0) {
|
|
519
|
+
process.stdout.write(`\nImported by (${payload.importedBy.length}):\n`);
|
|
520
|
+
for (const i of payload.importedBy.slice(0, 20)) {
|
|
521
|
+
process.stdout.write(` ← ${describeTarget(i)}\n`);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (payload.bridge) {
|
|
525
|
+
if (payload.bridge.rules.length > 0) {
|
|
526
|
+
process.stdout.write(`\nApplies rules (${payload.bridge.rules.length}):\n`);
|
|
527
|
+
for (const r of payload.bridge.rules.slice(0, 10)) {
|
|
528
|
+
process.stdout.write(` • ${r.id}${r.severity ? ` [${r.severity}]` : ''} — ${r.label}\n`);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (payload.bridge.paths.length > 0) {
|
|
532
|
+
process.stdout.write(`\nPath conventions (${payload.bridge.paths.length}):\n`);
|
|
533
|
+
for (const p of payload.bridge.paths.slice(0, 10)) {
|
|
534
|
+
process.stdout.write(` • ${p.id} — ${p.label}\n`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
if (payload.bridge.templates.length > 0) {
|
|
538
|
+
process.stdout.write(`\nCovered by templates (${payload.bridge.templates.length}):\n`);
|
|
539
|
+
for (const t of payload.bridge.templates.slice(0, 10)) {
|
|
540
|
+
process.stdout.write(` • ${t.id} — ${t.label}\n`);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (payload.framework && payload.framework.entities.length > 0) {
|
|
545
|
+
process.stdout.write(`\nFramework entities (${payload.framework.entities.length}):\n`);
|
|
546
|
+
for (const e of payload.framework.entities.slice(0, 10)) {
|
|
547
|
+
process.stdout.write(` • ${e.framework}:${e.subtype} ${e.label}\n`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return 0;
|
|
551
|
+
}
|
|
552
|
+
// ─── shrk graph impact ────────────────────────────────────────────────
|
|
553
|
+
export async function runGraphImpact(args) {
|
|
554
|
+
const cwd = resolveCwd(args);
|
|
555
|
+
const wantJson = flagBool(args, 'json');
|
|
556
|
+
const wantFull = flagBool(args, 'full');
|
|
557
|
+
const target = args.positional[1];
|
|
558
|
+
if (!target) {
|
|
559
|
+
process.stderr.write('Usage: shrk graph impact <fileOrSymbol> [--max-depth N] [--limit N] [--full]\n');
|
|
560
|
+
return 2;
|
|
561
|
+
}
|
|
562
|
+
const maxDepth = Math.max(1, Math.min(10, Number(flagString(args, 'max-depth') ?? '5')));
|
|
563
|
+
const limit = Math.max(1, Number(flagString(args, 'limit') ?? '200'));
|
|
564
|
+
// --full → delegate to the impact-engine for a richer v3 payload.
|
|
565
|
+
if (wantFull) {
|
|
566
|
+
const isSymbol = target.startsWith('symbol:') || /^[A-Za-z_][\w$]*$/.test(target);
|
|
567
|
+
const input = isSymbol && !target.includes('/')
|
|
568
|
+
? { kind: 'symbol', symbolId: target }
|
|
569
|
+
: { kind: 'files', files: [target] };
|
|
570
|
+
const analysis = analyzeGraphImpact(input, { projectRoot: cwd, limit, maxDepth });
|
|
571
|
+
if (wantJson) {
|
|
572
|
+
process.stdout.write(asJson(analysis) + '\n');
|
|
573
|
+
return 0;
|
|
574
|
+
}
|
|
575
|
+
process.stdout.write(header(`Graph impact (full): ${target}`));
|
|
576
|
+
process.stdout.write(kv('risk', analysis.risk) + '\n');
|
|
577
|
+
process.stdout.write(kv('direct', String(analysis.directDependents.length)) + '\n');
|
|
578
|
+
process.stdout.write(kv('transitive', String(analysis.transitiveDependents.length)) + '\n');
|
|
579
|
+
process.stdout.write(kv('symbols', String(analysis.affectedSymbols.length)) + '\n');
|
|
580
|
+
process.stdout.write(kv('caller files', String(analysis.affectedCallerFiles.length)) + '\n');
|
|
581
|
+
process.stdout.write(kv('packages', String(analysis.affectedPackages.length)) + '\n');
|
|
582
|
+
process.stdout.write(kv('rules', String(analysis.affectedRules.length)) + '\n');
|
|
583
|
+
process.stdout.write(kv('templates', String(analysis.affectedTemplates.length)) + '\n');
|
|
584
|
+
process.stdout.write(kv('likely tests', String(analysis.likelyTests.length)) + '\n');
|
|
585
|
+
process.stdout.write(kv('public API touched', analysis.publicApiTouched ? 'yes' : 'no') + '\n');
|
|
586
|
+
if (analysis.riskReasons.length > 0) {
|
|
587
|
+
process.stdout.write('\nRisk reasons:\n');
|
|
588
|
+
for (const r of analysis.riskReasons)
|
|
589
|
+
process.stdout.write(` • ${r}\n`);
|
|
590
|
+
}
|
|
591
|
+
if (analysis.validationScope.length > 0) {
|
|
592
|
+
process.stdout.write('\nRun before merging:\n');
|
|
593
|
+
for (const c of analysis.validationScope)
|
|
594
|
+
process.stdout.write(` $ ${c}\n`);
|
|
595
|
+
}
|
|
596
|
+
for (const d of analysis.diagnostics.slice(0, 5))
|
|
597
|
+
process.stdout.write(`! ${d}\n`);
|
|
598
|
+
return 0;
|
|
599
|
+
}
|
|
600
|
+
const api = loadOrFail(cwd, wantJson);
|
|
601
|
+
if (!api)
|
|
602
|
+
return 1;
|
|
603
|
+
const anchor = resolveAnchor(api, target);
|
|
604
|
+
if (!anchor) {
|
|
605
|
+
const payload = { ok: false, error: 'not-found', target };
|
|
606
|
+
if (wantJson) {
|
|
607
|
+
process.stdout.write(asJson(payload) + '\n');
|
|
608
|
+
return 1;
|
|
609
|
+
}
|
|
610
|
+
process.stderr.write(`No graph node matched "${target}".\n`);
|
|
611
|
+
return 1;
|
|
612
|
+
}
|
|
613
|
+
const closure = reverseClosure(api, anchor.id, maxDepth, limit);
|
|
614
|
+
const direct = closure.layer[1] ?? [];
|
|
615
|
+
const transitive = closure.all.filter((id) => id !== anchor.id && !direct.includes(id));
|
|
616
|
+
const payload = {
|
|
617
|
+
schema: 'sharkcraft.graph-impact/v1',
|
|
618
|
+
anchor: nodeSummary(anchor),
|
|
619
|
+
maxDepth,
|
|
620
|
+
limit,
|
|
621
|
+
truncated: closure.truncated,
|
|
622
|
+
directDependents: direct.map((id) => nodeSummary(api.neighbours(id).node)),
|
|
623
|
+
transitiveDependents: transitive
|
|
624
|
+
.slice(0, limit)
|
|
625
|
+
.map((id) => nodeSummary(api.neighbours(id).node)),
|
|
626
|
+
totalReached: closure.all.length - 1,
|
|
627
|
+
};
|
|
628
|
+
if (wantJson) {
|
|
629
|
+
process.stdout.write(asJson(payload) + '\n');
|
|
630
|
+
return 0;
|
|
631
|
+
}
|
|
632
|
+
process.stdout.write(header(`Graph impact: ${anchor.label}`));
|
|
633
|
+
process.stdout.write(kv('direct', String(direct.length)) + '\n');
|
|
634
|
+
process.stdout.write(kv('transitive', String(transitive.length)) + '\n');
|
|
635
|
+
process.stdout.write(kv('max-depth', String(maxDepth)) + '\n');
|
|
636
|
+
if (closure.truncated)
|
|
637
|
+
process.stdout.write(kv('truncated', 'yes') + '\n');
|
|
638
|
+
for (const d of payload.directDependents.slice(0, 30)) {
|
|
639
|
+
process.stdout.write(` ${d.path ?? d.id}\n`);
|
|
640
|
+
}
|
|
641
|
+
return 0;
|
|
642
|
+
}
|
|
643
|
+
// ─── shrk graph callers ───────────────────────────────────────────────
|
|
644
|
+
export async function runGraphCallers(args) {
|
|
645
|
+
const cwd = resolveCwd(args);
|
|
646
|
+
const wantJson = flagBool(args, 'json');
|
|
647
|
+
const target = args.positional[1];
|
|
648
|
+
if (!target) {
|
|
649
|
+
process.stderr.write('Usage: shrk graph callers <symbol> [--mode call|reference]\n');
|
|
650
|
+
return 2;
|
|
651
|
+
}
|
|
652
|
+
const mode = (flagString(args, 'mode') ?? 'call');
|
|
653
|
+
const api = loadOrFail(cwd, wantJson);
|
|
654
|
+
if (!api)
|
|
655
|
+
return 1;
|
|
656
|
+
const sym = resolveSymbolTarget(api, target);
|
|
657
|
+
if (!sym) {
|
|
658
|
+
const payload = { ok: false, error: 'not-found', target };
|
|
659
|
+
if (wantJson) {
|
|
660
|
+
process.stdout.write(asJson(payload) + '\n');
|
|
661
|
+
return 1;
|
|
662
|
+
}
|
|
663
|
+
process.stderr.write(`No symbol matched "${target}".\n`);
|
|
664
|
+
return 1;
|
|
665
|
+
}
|
|
666
|
+
const hits = mode === 'reference' ? api.referencesOf(sym.id) : api.callersOf(sym.id);
|
|
667
|
+
const payload = {
|
|
668
|
+
schema: 'sharkcraft.graph-callers/v1',
|
|
669
|
+
symbol: nodeSummary(sym),
|
|
670
|
+
mode,
|
|
671
|
+
total: hits.length,
|
|
672
|
+
callers: hits.slice(0, 200).map(nodeSummary),
|
|
673
|
+
};
|
|
674
|
+
if (wantJson) {
|
|
675
|
+
process.stdout.write(asJson(payload) + '\n');
|
|
676
|
+
return 0;
|
|
677
|
+
}
|
|
678
|
+
process.stdout.write(header(`Graph callers: ${sym.label} (${mode})`));
|
|
679
|
+
process.stdout.write(kv('total', String(hits.length)) + '\n');
|
|
680
|
+
for (const c of payload.callers.slice(0, 50)) {
|
|
681
|
+
process.stdout.write(` ${c.path ?? c.id}\n`);
|
|
682
|
+
}
|
|
683
|
+
return 0;
|
|
684
|
+
}
|
|
685
|
+
function resolveSymbolTarget(api, target) {
|
|
686
|
+
if (target.startsWith('symbol:')) {
|
|
687
|
+
return api.neighbours(target)?.node;
|
|
688
|
+
}
|
|
689
|
+
const syms = api.findSymbol(target, { exact: true, limit: 5 });
|
|
690
|
+
if (syms.length === 0)
|
|
691
|
+
return undefined;
|
|
692
|
+
if (syms.length === 1)
|
|
693
|
+
return syms[0];
|
|
694
|
+
// Multiple symbols with the same name. Prefer an exported one if any.
|
|
695
|
+
const exported = syms.find((s) => (s.data?.['isExported'] ?? false) === true);
|
|
696
|
+
return exported ?? syms[0];
|
|
697
|
+
}
|
|
698
|
+
// ─── helpers ──────────────────────────────────────────────────────────
|
|
699
|
+
function loadOrFail(cwd, wantJson) {
|
|
700
|
+
const store = new GraphStore(cwd);
|
|
701
|
+
if (!store.exists()) {
|
|
702
|
+
if (wantJson) {
|
|
703
|
+
process.stdout.write(asJson({
|
|
704
|
+
ok: false,
|
|
705
|
+
state: 'missing',
|
|
706
|
+
nextCommand: 'shrk graph index',
|
|
707
|
+
message: STALE_HINT,
|
|
708
|
+
}) + '\n');
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
process.stderr.write(STALE_HINT + '\n');
|
|
712
|
+
}
|
|
713
|
+
return undefined;
|
|
714
|
+
}
|
|
715
|
+
return GraphQueryApi.fromStore(cwd);
|
|
716
|
+
}
|
|
717
|
+
function resolveAnchor(api, target) {
|
|
718
|
+
// Exact node id wins.
|
|
719
|
+
const direct = api.neighbours(target);
|
|
720
|
+
if (direct)
|
|
721
|
+
return direct.node;
|
|
722
|
+
// Prefixed id forms.
|
|
723
|
+
for (const prefix of ['file:', 'symbol:', 'package:']) {
|
|
724
|
+
if (target.startsWith(prefix))
|
|
725
|
+
return undefined;
|
|
726
|
+
}
|
|
727
|
+
// File path?
|
|
728
|
+
const f = api.findFile(target);
|
|
729
|
+
if (f)
|
|
730
|
+
return f;
|
|
731
|
+
// Symbol by name (exact).
|
|
732
|
+
const syms = api.findSymbol(target, { exact: true, limit: 1 });
|
|
733
|
+
if (syms.length > 0)
|
|
734
|
+
return syms[0];
|
|
735
|
+
return undefined;
|
|
736
|
+
}
|
|
737
|
+
function collectSearchMatches(api, query, kind, limit) {
|
|
738
|
+
const out = [];
|
|
739
|
+
if (!kind || kind === 'file') {
|
|
740
|
+
const f = api.findFile(query);
|
|
741
|
+
if (f)
|
|
742
|
+
out.push(f);
|
|
743
|
+
}
|
|
744
|
+
if (!kind || kind === 'symbol') {
|
|
745
|
+
for (const s of api.findSymbol(query, { exact: false, limit }))
|
|
746
|
+
out.push(s);
|
|
747
|
+
}
|
|
748
|
+
if (!kind || kind === 'package') {
|
|
749
|
+
const p = api.neighbours(`package:${query}`);
|
|
750
|
+
if (p)
|
|
751
|
+
out.push(p.node);
|
|
752
|
+
}
|
|
753
|
+
return out.slice(0, limit);
|
|
754
|
+
}
|
|
755
|
+
function reverseClosure(api, startId, maxDepth, limit) {
|
|
756
|
+
const seen = new Set([startId]);
|
|
757
|
+
const layer = {};
|
|
758
|
+
let frontier = [startId];
|
|
759
|
+
let depth = 1;
|
|
760
|
+
let truncated = false;
|
|
761
|
+
while (depth <= maxDepth && frontier.length > 0) {
|
|
762
|
+
const next = [];
|
|
763
|
+
for (const id of frontier) {
|
|
764
|
+
const importers = api.importersOf(id);
|
|
765
|
+
for (const imp of importers) {
|
|
766
|
+
if (seen.has(imp.id))
|
|
767
|
+
continue;
|
|
768
|
+
seen.add(imp.id);
|
|
769
|
+
next.push(imp.id);
|
|
770
|
+
if (seen.size - 1 >= limit) {
|
|
771
|
+
truncated = true;
|
|
772
|
+
break;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (truncated)
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
if (next.length > 0)
|
|
779
|
+
layer[depth] = next;
|
|
780
|
+
frontier = next;
|
|
781
|
+
depth += 1;
|
|
782
|
+
if (truncated)
|
|
783
|
+
break;
|
|
784
|
+
}
|
|
785
|
+
return { all: [...seen], layer, truncated };
|
|
786
|
+
}
|
|
787
|
+
function nodeSummary(n) {
|
|
788
|
+
return {
|
|
789
|
+
id: n.id,
|
|
790
|
+
kind: n.kind,
|
|
791
|
+
label: n.label,
|
|
792
|
+
...(n.path ? { path: n.path } : {}),
|
|
793
|
+
...(n.line ? { line: n.line } : {}),
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
function targetSummary(target) {
|
|
797
|
+
if ('resolved' in target) {
|
|
798
|
+
return { id: target.id, resolved: false };
|
|
799
|
+
}
|
|
800
|
+
return { id: target.id, resolved: true, kind: target.kind, label: target.label, ...(target.path ? { path: target.path } : {}) };
|
|
801
|
+
}
|
|
802
|
+
function sourceSummary(source) {
|
|
803
|
+
return targetSummary(source);
|
|
804
|
+
}
|
|
805
|
+
function toSearchHit(n) {
|
|
806
|
+
return nodeSummary(n);
|
|
807
|
+
}
|
|
808
|
+
function describeTarget(t) {
|
|
809
|
+
if (!t.resolved)
|
|
810
|
+
return t.id;
|
|
811
|
+
return `${t.path ?? t.label ?? t.id}`;
|
|
812
|
+
}
|
|
813
|
+
function sumValues(record) {
|
|
814
|
+
let n = 0;
|
|
815
|
+
for (const v of Object.values(record))
|
|
816
|
+
n += v;
|
|
817
|
+
return n;
|
|
818
|
+
}
|