@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.
- 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 +119 -3
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +224 -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/constructs.command.d.ts.map +1 -1
- package/dist/commands/constructs.command.js +40 -9
- 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/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 +99 -2
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +183 -68
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +6 -0
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +91 -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 +3 -0
- package/dist/commands/policy-lint.command.d.ts.map +1 -0
- package/dist/commands/policy-lint.command.js +135 -0
- 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 +3 -0
- package/dist/commands/reuse.command.d.ts.map +1 -0
- package/dist/commands/reuse.command.js +202 -0
- 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 +87 -4
- 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 +14 -1
- 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
|
@@ -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
|
|
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
|
-
|
|
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":"
|
|
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
|
-
|
|
472
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
611
|
+
const filtered = query
|
|
548
612
|
? all.filter((n) => (n.path ?? '').toLowerCase().includes(query.toLowerCase()))
|
|
549
|
-
: [...all]
|
|
613
|
+
: [...all];
|
|
614
|
+
total = filtered.length;
|
|
615
|
+
page = filtered.slice(0, limit);
|
|
550
616
|
}
|
|
551
617
|
else {
|
|
552
|
-
|
|
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:
|
|
560
|
-
|
|
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 (
|
|
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
|
|
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.
|
|
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
|
-
|
|
626
|
-
|
|
627
|
-
|
|
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 =
|
|
630
|
-
.
|
|
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,
|
|
634
|
-
const calledByList = callers.slice(0,
|
|
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.
|
|
776
|
-
const limit =
|
|
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.
|
|
903
|
-
|
|
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.
|
|
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
|
-
|
|
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,
|
|
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;
|
|
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"}
|