@shrkcrft/cli 0.1.0-alpha.11 → 0.1.0-alpha.13
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/audit/knowledge-audit-llm.d.ts +19 -0
- package/dist/audit/knowledge-audit-llm.d.ts.map +1 -0
- package/dist/audit/knowledge-audit-llm.js +164 -0
- package/dist/audit/knowledge-audit.d.ts +61 -0
- package/dist/audit/knowledge-audit.d.ts.map +1 -0
- package/dist/audit/knowledge-audit.js +203 -0
- package/dist/audit/knowledge-fix-plan-llm.d.ts +11 -0
- package/dist/audit/knowledge-fix-plan-llm.d.ts.map +1 -0
- package/dist/audit/knowledge-fix-plan-llm.js +141 -0
- package/dist/audit/knowledge-fix-plan.d.ts +41 -0
- package/dist/audit/knowledge-fix-plan.d.ts.map +1 -0
- package/dist/audit/knowledge-fix-plan.js +125 -0
- package/dist/audit/pipeline-audit-llm.d.ts +11 -0
- package/dist/audit/pipeline-audit-llm.d.ts.map +1 -0
- package/dist/audit/pipeline-audit-llm.js +134 -0
- package/dist/audit/pipeline-audit.d.ts +69 -0
- package/dist/audit/pipeline-audit.d.ts.map +1 -0
- package/dist/audit/pipeline-audit.js +166 -0
- package/dist/audit/templates-audit-llm.d.ts +19 -0
- package/dist/audit/templates-audit-llm.d.ts.map +1 -0
- package/dist/audit/templates-audit-llm.js +207 -0
- package/dist/audit/templates-audit.d.ts +63 -0
- package/dist/audit/templates-audit.d.ts.map +1 -0
- package/dist/audit/templates-audit.js +171 -0
- package/dist/audit/templates-fix-plan-llm.d.ts +19 -0
- package/dist/audit/templates-fix-plan-llm.d.ts.map +1 -0
- package/dist/audit/templates-fix-plan-llm.js +162 -0
- package/dist/audit/templates-fix-plan.d.ts +37 -0
- package/dist/audit/templates-fix-plan.d.ts.map +1 -0
- package/dist/audit/templates-fix-plan.js +174 -0
- package/dist/commands/ai-status.command.d.ts +19 -0
- package/dist/commands/ai-status.command.d.ts.map +1 -0
- package/dist/commands/ai-status.command.js +94 -0
- package/dist/commands/ask.command.d.ts.map +1 -1
- package/dist/commands/ask.command.js +10 -9
- package/dist/commands/command-catalog.d.ts.map +1 -1
- package/dist/commands/command-catalog.js +110 -1
- package/dist/commands/deps-audit.command.d.ts +23 -0
- package/dist/commands/deps-audit.command.d.ts.map +1 -0
- package/dist/commands/deps-audit.command.js +266 -0
- package/dist/commands/doctor.command.d.ts.map +1 -1
- package/dist/commands/doctor.command.js +100 -3
- package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
- package/dist/commands/graph-code-subverbs.js +144 -26
- package/dist/commands/graph.command.d.ts.map +1 -1
- package/dist/commands/graph.command.js +3 -2
- package/dist/commands/help.command.d.ts.map +1 -1
- package/dist/commands/help.command.js +22 -1
- package/dist/commands/impact.command.d.ts.map +1 -1
- package/dist/commands/impact.command.js +3 -2
- package/dist/commands/move-plan.command.d.ts +23 -0
- package/dist/commands/move-plan.command.d.ts.map +1 -0
- package/dist/commands/move-plan.command.js +360 -0
- package/dist/commands/scaffold-validate.command.d.ts +22 -0
- package/dist/commands/scaffold-validate.command.d.ts.map +1 -0
- package/dist/commands/scaffold-validate.command.js +215 -0
- package/dist/commands/smart-context.command.d.ts +58 -0
- package/dist/commands/smart-context.command.d.ts.map +1 -0
- package/dist/commands/smart-context.command.js +4524 -0
- package/dist/commands/spike.command.d.ts +22 -0
- package/dist/commands/spike.command.d.ts.map +1 -0
- package/dist/commands/spike.command.js +235 -0
- package/dist/commands/surface.command.d.ts +1 -0
- package/dist/commands/surface.command.d.ts.map +1 -1
- package/dist/commands/surface.command.js +10 -3
- package/dist/commands/template-quality.command.d.ts.map +1 -1
- package/dist/commands/template-quality.command.js +39 -3
- package/dist/commands/templates.command.d.ts.map +1 -1
- package/dist/commands/templates.command.js +37 -2
- package/dist/commands/watch.command.d.ts +26 -0
- package/dist/commands/watch.command.d.ts.map +1 -0
- package/dist/commands/watch.command.js +456 -0
- package/dist/env/load-dotenv.d.ts +15 -0
- package/dist/env/load-dotenv.d.ts.map +1 -0
- package/dist/env/load-dotenv.js +70 -0
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +105 -2
- package/dist/schemas/json-schemas.d.ts +384 -36
- package/dist/schemas/json-schemas.d.ts.map +1 -1
- package/dist/schemas/json-schemas.js +247 -36
- package/package.json +33 -31
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import * as nodePath from 'node:path';
|
|
3
|
+
import { GraphQueryApi, GraphStore, NodeKind } from '@shrkcrft/graph';
|
|
4
|
+
import { flagBool, resolveCwd, } from "../command-registry.js";
|
|
5
|
+
import { asJson, header } from "../output/format-output.js";
|
|
6
|
+
/**
|
|
7
|
+
* `shrk deps-audit` — for each workspace package, compare the
|
|
8
|
+
* `package.json` `dependencies` / `devDependencies` / `peerDependencies`
|
|
9
|
+
* against the *specifiers actually imported* from source under
|
|
10
|
+
* `<pkg>/src/` (per the SharkCraft graph).
|
|
11
|
+
*
|
|
12
|
+
* Reports:
|
|
13
|
+
* - missing deps: imported but not declared (likely build failure
|
|
14
|
+
* in the wild — the package depends on its host's resolution)
|
|
15
|
+
* - unused deps: declared but never imported (lint waste)
|
|
16
|
+
*
|
|
17
|
+
* Read-only. JSON output via `--json`. Optionally restricted to one
|
|
18
|
+
* package via `--package <name>`.
|
|
19
|
+
*
|
|
20
|
+
* Known limitations:
|
|
21
|
+
* - Type-only imports (`import type x from 'y'`) still count; the
|
|
22
|
+
* graph can't tell them apart in v3.
|
|
23
|
+
* - Subpath imports (`pkg/sub`) are reduced to their root specifier.
|
|
24
|
+
* - Built-in node modules (`node:fs`, `fs`, …) are ignored.
|
|
25
|
+
*/
|
|
26
|
+
export const depsAuditCommand = {
|
|
27
|
+
name: 'deps-audit',
|
|
28
|
+
description: 'Audit declared dependencies vs imports actually seen in each package source. Reports missing + unused deps. Read-only.',
|
|
29
|
+
usage: 'shrk deps-audit [--package <name>] [--json]',
|
|
30
|
+
async run(args) {
|
|
31
|
+
const cwd = resolveCwd(args);
|
|
32
|
+
const json = flagBool(args, 'json');
|
|
33
|
+
const onlyPackage = typeof args.flags.get('package') === 'string'
|
|
34
|
+
? args.flags.get('package')
|
|
35
|
+
: null;
|
|
36
|
+
const store = new GraphStore(cwd);
|
|
37
|
+
if (!store.exists()) {
|
|
38
|
+
process.stderr.write('No SharkCraft graph found. Run `shrk graph build` first so deps-audit has import data.\n');
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
const api = GraphQueryApi.fromStore(cwd);
|
|
42
|
+
const packages = listWorkspacePackages(cwd, onlyPackage);
|
|
43
|
+
if (packages.length === 0) {
|
|
44
|
+
process.stderr.write('No packages found (looked under packages/*, libs/*, apps/*).\n');
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
const reports = [];
|
|
48
|
+
for (const pkg of packages) {
|
|
49
|
+
reports.push(buildPackageReport(api, cwd, pkg));
|
|
50
|
+
}
|
|
51
|
+
if (json) {
|
|
52
|
+
process.stdout.write(asJson({ packages: reports }) + '\n');
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
let missingTotal = 0;
|
|
56
|
+
let unusedTotal = 0;
|
|
57
|
+
for (const r of reports) {
|
|
58
|
+
missingTotal += r.missingDeps.length;
|
|
59
|
+
unusedTotal += r.unusedDeps.length;
|
|
60
|
+
}
|
|
61
|
+
process.stdout.write(header(`deps-audit — ${reports.length} package(s), ${missingTotal} missing dep(s), ${unusedTotal} unused dep(s)`));
|
|
62
|
+
for (const r of reports) {
|
|
63
|
+
if (r.missingDeps.length === 0 && r.unusedDeps.length === 0)
|
|
64
|
+
continue;
|
|
65
|
+
process.stdout.write(`\n${r.packageName} (${r.packageDir})\n`);
|
|
66
|
+
if (r.missingDeps.length > 0) {
|
|
67
|
+
process.stdout.write(' missing (imported, not declared):\n');
|
|
68
|
+
for (const m of r.missingDeps) {
|
|
69
|
+
process.stdout.write(` - ${m.specifier} (imported ${m.importedFromCount}×)\n`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (r.unusedDeps.length > 0) {
|
|
73
|
+
process.stdout.write(' unused (declared, never imported):\n');
|
|
74
|
+
for (const u of r.unusedDeps) {
|
|
75
|
+
process.stdout.write(` - ${u.specifier} [${u.section}]\n`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (missingTotal === 0 && unusedTotal === 0) {
|
|
80
|
+
process.stdout.write('\nAll declared deps match actual imports. ✓\n');
|
|
81
|
+
}
|
|
82
|
+
return 0;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
function listWorkspacePackages(cwd, onlyName) {
|
|
86
|
+
const roots = ['packages', 'libs', 'apps'].map((r) => nodePath.join(cwd, r)).filter((d) => existsSync(d));
|
|
87
|
+
const out = [];
|
|
88
|
+
for (const root of roots) {
|
|
89
|
+
let entries;
|
|
90
|
+
try {
|
|
91
|
+
entries = readdirSync(root);
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
const dir = nodePath.join(root, entry);
|
|
98
|
+
let stat;
|
|
99
|
+
try {
|
|
100
|
+
stat = statSync(dir);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
if (!stat.isDirectory())
|
|
106
|
+
continue;
|
|
107
|
+
const pkgJsonPath = nodePath.join(dir, 'package.json');
|
|
108
|
+
if (!existsSync(pkgJsonPath))
|
|
109
|
+
continue;
|
|
110
|
+
let pkgJson;
|
|
111
|
+
try {
|
|
112
|
+
pkgJson = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (!pkgJson.name)
|
|
118
|
+
continue;
|
|
119
|
+
if (onlyName !== null && pkgJson.name !== onlyName)
|
|
120
|
+
continue;
|
|
121
|
+
out.push({ name: pkgJson.name, dir, pkgJsonPath });
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return out;
|
|
125
|
+
}
|
|
126
|
+
function buildPackageReport(api, cwd, pkg) {
|
|
127
|
+
const declared = readDeclaredDeps(pkg.pkgJsonPath);
|
|
128
|
+
const importedSpecifiers = collectImportedSpecifiersForPackage(api, cwd, pkg.dir);
|
|
129
|
+
// Count how many distinct files inside the package import each specifier.
|
|
130
|
+
const importerCounts = new Map();
|
|
131
|
+
for (const spec of importedSpecifiers) {
|
|
132
|
+
importerCounts.set(spec, (importerCounts.get(spec) ?? 0) + 1);
|
|
133
|
+
}
|
|
134
|
+
const distinctImported = new Set(importedSpecifiers);
|
|
135
|
+
const declaredAll = new Map();
|
|
136
|
+
const declaredSections = [
|
|
137
|
+
['dependencies', declared.dependencies],
|
|
138
|
+
['devDependencies', declared.devDependencies],
|
|
139
|
+
['peerDependencies', declared.peerDependencies],
|
|
140
|
+
['optionalDependencies', declared.optionalDependencies],
|
|
141
|
+
];
|
|
142
|
+
for (const [section, map] of declaredSections) {
|
|
143
|
+
for (const k of Object.keys(map))
|
|
144
|
+
declaredAll.set(k, section);
|
|
145
|
+
}
|
|
146
|
+
const missingDeps = [];
|
|
147
|
+
for (const spec of distinctImported) {
|
|
148
|
+
if (declaredAll.has(spec))
|
|
149
|
+
continue;
|
|
150
|
+
if (spec === pkg.name)
|
|
151
|
+
continue; // self-import via package name
|
|
152
|
+
missingDeps.push({ specifier: spec, importedFromCount: importerCounts.get(spec) ?? 0 });
|
|
153
|
+
}
|
|
154
|
+
missingDeps.sort((a, b) => b.importedFromCount - a.importedFromCount);
|
|
155
|
+
const unusedDeps = [];
|
|
156
|
+
for (const [spec, section] of declaredAll.entries()) {
|
|
157
|
+
if (distinctImported.has(spec))
|
|
158
|
+
continue;
|
|
159
|
+
// devDependencies for build/test tools often don't show up in graph
|
|
160
|
+
// imports; we still report them so the user can prune if desired.
|
|
161
|
+
unusedDeps.push({ specifier: spec, section });
|
|
162
|
+
}
|
|
163
|
+
unusedDeps.sort((a, b) => a.specifier.localeCompare(b.specifier));
|
|
164
|
+
return {
|
|
165
|
+
packageName: pkg.name,
|
|
166
|
+
packageDir: nodePath.relative(cwd, pkg.dir) || '.',
|
|
167
|
+
declared,
|
|
168
|
+
importedSpecifiers: [...distinctImported],
|
|
169
|
+
missingDeps,
|
|
170
|
+
unusedDeps,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function readDeclaredDeps(pkgJsonPath) {
|
|
174
|
+
try {
|
|
175
|
+
const body = JSON.parse(readFileSync(pkgJsonPath, 'utf8'));
|
|
176
|
+
return {
|
|
177
|
+
dependencies: asStringMap(body['dependencies']),
|
|
178
|
+
devDependencies: asStringMap(body['devDependencies']),
|
|
179
|
+
peerDependencies: asStringMap(body['peerDependencies']),
|
|
180
|
+
optionalDependencies: asStringMap(body['optionalDependencies']),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return { dependencies: {}, devDependencies: {}, peerDependencies: {}, optionalDependencies: {} };
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function asStringMap(value) {
|
|
188
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
189
|
+
return {};
|
|
190
|
+
const out = {};
|
|
191
|
+
for (const [k, v] of Object.entries(value)) {
|
|
192
|
+
if (typeof v === 'string')
|
|
193
|
+
out[k] = v;
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
197
|
+
function collectImportedSpecifiersForPackage(api, cwd, packageDir) {
|
|
198
|
+
const out = [];
|
|
199
|
+
const relDir = nodePath.relative(cwd, packageDir).replace(/\\/g, '/');
|
|
200
|
+
for (const file of api.allFiles()) {
|
|
201
|
+
if (file.kind !== NodeKind.File)
|
|
202
|
+
continue;
|
|
203
|
+
const p = file.path ?? '';
|
|
204
|
+
if (!p.startsWith(relDir + '/src/') && !p.startsWith(relDir + '/'))
|
|
205
|
+
continue;
|
|
206
|
+
// Each ImportsFile edge resolves to a file node; we want the *raw*
|
|
207
|
+
// import specifier, which the graph carries on the edge's data
|
|
208
|
+
// payload. We don't have direct access here, so we approximate by
|
|
209
|
+
// reading the file contents and extracting from-clauses.
|
|
210
|
+
const abs = nodePath.isAbsolute(p) ? p : nodePath.join(cwd, p);
|
|
211
|
+
if (!existsSync(abs))
|
|
212
|
+
continue;
|
|
213
|
+
let body;
|
|
214
|
+
try {
|
|
215
|
+
body = readFileSync(abs, 'utf8');
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
for (const spec of extractRootSpecifiers(body)) {
|
|
221
|
+
if (isBuiltinModule(spec))
|
|
222
|
+
continue;
|
|
223
|
+
if (spec.startsWith('.') || spec.startsWith('/'))
|
|
224
|
+
continue; // relative
|
|
225
|
+
out.push(rootOfSpecifier(spec));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
const IMPORT_FROM_RE = /(?:^|\n)\s*(?:import|export)\s+[^;]*?\s+from\s+['"]([^'"]+)['"]/g;
|
|
231
|
+
const REQUIRE_RE = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
232
|
+
const DYNAMIC_IMPORT_RE = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
|
|
233
|
+
function extractRootSpecifiers(body) {
|
|
234
|
+
const out = [];
|
|
235
|
+
for (const m of body.matchAll(IMPORT_FROM_RE)) {
|
|
236
|
+
if (m[1])
|
|
237
|
+
out.push(m[1]);
|
|
238
|
+
}
|
|
239
|
+
for (const m of body.matchAll(REQUIRE_RE)) {
|
|
240
|
+
if (m[1])
|
|
241
|
+
out.push(m[1]);
|
|
242
|
+
}
|
|
243
|
+
for (const m of body.matchAll(DYNAMIC_IMPORT_RE)) {
|
|
244
|
+
if (m[1])
|
|
245
|
+
out.push(m[1]);
|
|
246
|
+
}
|
|
247
|
+
return out;
|
|
248
|
+
}
|
|
249
|
+
function rootOfSpecifier(spec) {
|
|
250
|
+
if (spec.startsWith('@')) {
|
|
251
|
+
const parts = spec.split('/');
|
|
252
|
+
return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : spec;
|
|
253
|
+
}
|
|
254
|
+
return spec.split('/')[0];
|
|
255
|
+
}
|
|
256
|
+
function isBuiltinModule(spec) {
|
|
257
|
+
if (spec.startsWith('node:'))
|
|
258
|
+
return true;
|
|
259
|
+
// Common bare-name builtins.
|
|
260
|
+
return new Set([
|
|
261
|
+
'fs', 'path', 'os', 'crypto', 'http', 'https', 'url', 'util', 'stream',
|
|
262
|
+
'events', 'child_process', 'process', 'buffer', 'querystring', 'zlib',
|
|
263
|
+
'tls', 'net', 'dns', 'dgram', 'cluster', 'worker_threads', 'perf_hooks',
|
|
264
|
+
'readline', 'tty', 'vm',
|
|
265
|
+
]).has(spec);
|
|
266
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"doctor.command.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.command.ts"],"names":[],"mappings":"AAqBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"doctor.command.d.ts","sourceRoot":"","sources":["../../src/commands/doctor.command.ts"],"names":[],"mappings":"AAqBA,OAAO,EAML,KAAK,eAAe,EAErB,MAAM,wBAAwB,CAAC;AA8OhC,eAAO,MAAM,aAAa,EAAE,eAW3B,CAAC;AA4eF,eAAO,MAAM,qBAAqB,EAAE,eAmCnC,CAAC;AAuDF,eAAO,MAAM,yBAAyB,EAAE,eAavC,CAAC;AAIF,eAAO,MAAM,wBAAwB,EAAE,eA2CtC,CAAC;AAgCF,eAAO,MAAM,6BAA6B,EAAE,eAa3C,CAAC"}
|
|
@@ -5,10 +5,12 @@ import { buildSurfaceSummary } from "../surface/surface-summary.js";
|
|
|
5
5
|
import { renderShapeLine } from "../surface/shape-defaults.js";
|
|
6
6
|
import { existsSync } from 'node:fs';
|
|
7
7
|
import { flagBool, flagNumber, flagString, flagList, resolveCwd, } from "../command-registry.js";
|
|
8
|
+
import { SemanticIndex, listIndexableFiles } from '@shrkcrft/embeddings';
|
|
8
9
|
import { asJson, header, kv } from "../output/format-output.js";
|
|
9
10
|
import { maybeRunInWatchMode } from "../output/watch-loop.js";
|
|
10
11
|
import { doctorHints, renderFailureHints } from "../output/failure-hints.js";
|
|
11
12
|
import { foldDoctorChecks, renderFoldedSummary, DoctorState, } from "../doctor/doctor-tags.js";
|
|
13
|
+
import { enrichWithLlmRecommendations, renderRecommendationsMarkdown, } from '@shrkcrft/ai';
|
|
12
14
|
const SEVERITY_LABEL = {
|
|
13
15
|
[DoctorSeverity.Ok]: 'OK ',
|
|
14
16
|
[DoctorSeverity.Info]: 'INFO ',
|
|
@@ -139,10 +141,68 @@ function isBlockerCheck(check) {
|
|
|
139
141
|
async function runDoctorOnce(args) {
|
|
140
142
|
return doctorCommandImpl(args);
|
|
141
143
|
}
|
|
144
|
+
function augmentWithSemanticIndexCheck(result, cwd) {
|
|
145
|
+
const current = listIndexableFiles(cwd, 5000);
|
|
146
|
+
const report = SemanticIndex.freshnessReport(cwd, current);
|
|
147
|
+
const check = renderSemanticIndexCheck(report);
|
|
148
|
+
const checks = [...result.checks, check];
|
|
149
|
+
const summary = { ...result.summary };
|
|
150
|
+
if (check.severity === DoctorSeverity.Ok)
|
|
151
|
+
summary.ok = (summary.ok ?? 0) + 1;
|
|
152
|
+
else if (check.severity === DoctorSeverity.Info)
|
|
153
|
+
summary.info = (summary.info ?? 0) + 1;
|
|
154
|
+
else if (check.severity === DoctorSeverity.Warning)
|
|
155
|
+
summary.warnings = (summary.warnings ?? 0) + 1;
|
|
156
|
+
else if (check.severity === DoctorSeverity.Error)
|
|
157
|
+
summary.errors = (summary.errors ?? 0) + 1;
|
|
158
|
+
return { ...result, checks, summary };
|
|
159
|
+
}
|
|
160
|
+
function renderSemanticIndexCheck(report) {
|
|
161
|
+
if (!report.hasIndex) {
|
|
162
|
+
return {
|
|
163
|
+
id: 'semantic-index-missing',
|
|
164
|
+
title: 'Semantic embedding index',
|
|
165
|
+
severity: DoctorSeverity.Info,
|
|
166
|
+
message: `No semantic index found — ${report.untracked} indexable files on disk. ` +
|
|
167
|
+
'Run `shrk smart-context embeddings-build` to enable embedding-backed retrieval in smart-context.',
|
|
168
|
+
category: 'semantic-index',
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (report.corrupt) {
|
|
172
|
+
return {
|
|
173
|
+
id: 'semantic-index-corrupt',
|
|
174
|
+
title: 'Semantic embedding index',
|
|
175
|
+
severity: DoctorSeverity.Error,
|
|
176
|
+
message: 'Semantic index meta is corrupt.',
|
|
177
|
+
fix: 'shrk smart-context embeddings-build --rebuild',
|
|
178
|
+
category: 'semantic-index',
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const driftCount = report.stale + report.missing + report.untracked;
|
|
182
|
+
const driftPct = report.indexed > 0 ? (driftCount * 100) / report.indexed : 0;
|
|
183
|
+
if (driftCount === 0) {
|
|
184
|
+
return {
|
|
185
|
+
id: 'semantic-index-fresh',
|
|
186
|
+
title: 'Semantic embedding index',
|
|
187
|
+
severity: DoctorSeverity.Ok,
|
|
188
|
+
message: `Index fresh — ${report.indexed} files (model ${report.model}).`,
|
|
189
|
+
category: 'semantic-index',
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
const severity = driftPct >= 10 ? DoctorSeverity.Warning : DoctorSeverity.Info;
|
|
193
|
+
return {
|
|
194
|
+
id: 'semantic-index-stale',
|
|
195
|
+
title: 'Semantic embedding index',
|
|
196
|
+
severity,
|
|
197
|
+
message: `${report.indexed} indexed; ${report.stale} stale, ${report.missing} deleted, ${report.untracked} new on disk (≈ ${Math.round(driftPct)}% drift).`,
|
|
198
|
+
fix: 'shrk smart-context embeddings-build',
|
|
199
|
+
category: 'semantic-index',
|
|
200
|
+
};
|
|
201
|
+
}
|
|
142
202
|
export const doctorCommand = {
|
|
143
203
|
name: 'doctor',
|
|
144
|
-
description: 'Validate the local SharkCraft setup (config, knowledge, templates, project). `--focus errors|warnings-new|info`, `--hide <category,...>`, `--quiet-known` filter the headline view using `sharkcraft/doctor.suppressions.json`. `--watch`/`--once`/`--debounce` for live mode. `--explain-quality` shows the per-warning "why this matters" line so warnings stop being permanent yellow noise. `--blockers` shows only must-fix findings (errors + warning-category in {config-invalid, pack-signature-invalid, plan-signature-divergent, asset-load-failed}); exit code is non-zero iff a blocker remains. Subcommands: `suppress`, `suppressions list|check`, `watch`.',
|
|
145
|
-
usage: 'shrk [--cwd <dir>] doctor [--no-config] [--json] [--strict[=errors|warnings|all]] [--blockers] [--show-advisory] [--min-score <0-100>] [--focus errors,warnings-new,info] [--hide action-hint-quality,...] [--quiet-known] [--explain-quality] [--watch [--once] [--debounce N]]',
|
|
204
|
+
description: 'Validate the local SharkCraft setup (config, knowledge, templates, project). `--focus errors|warnings-new|info`, `--hide <category,...>`, `--quiet-known` filter the headline view using `sharkcraft/doctor.suppressions.json`. `--watch`/`--once`/`--debounce` for live mode. `--explain-quality` shows the per-warning "why this matters" line so warnings stop being permanent yellow noise. `--blockers` shows only must-fix findings (errors + warning-category in {config-invalid, pack-signature-invalid, plan-signature-divergent, asset-load-failed}); exit code is non-zero iff a blocker remains. `--llm-recommendations` layers a local-LLM-derived list of concrete next-steps onto the deterministic output (no-op when no provider is reachable). Subcommands: `suppress`, `suppressions list|check`, `watch`.',
|
|
205
|
+
usage: 'shrk [--cwd <dir>] doctor [--no-config] [--json] [--strict[=errors|warnings|all]] [--blockers] [--show-advisory] [--min-score <0-100>] [--focus errors,warnings-new,info] [--hide action-hint-quality,...] [--quiet-known] [--explain-quality] [--llm-recommendations] [--provider auto|ollama|llamacpp] [--watch [--once] [--debounce N]]',
|
|
146
206
|
async run(args) {
|
|
147
207
|
const watchExit = await maybeRunInWatchMode(args, runDoctorOnce);
|
|
148
208
|
if (watchExit !== null)
|
|
@@ -163,7 +223,7 @@ async function doctorCommandImpl(args) {
|
|
|
163
223
|
inspectOpts.loaderTimeoutMs = loaderTimeout;
|
|
164
224
|
}
|
|
165
225
|
const inspection = await inspectSharkcraft(inspectOpts);
|
|
166
|
-
const result = runDoctor(inspection);
|
|
226
|
+
const result = augmentWithSemanticIndexCheck(runDoctor(inspection), cwd);
|
|
167
227
|
const report = buildAiReadinessReport(inspection);
|
|
168
228
|
if (debug) {
|
|
169
229
|
process.stderr.write(`[debug] inspection elapsed ${inspection.inspectionElapsedMs}ms cache=${inspection.cacheEnabled ? 'on' : 'off'} loaders=${inspection.loaderDiagnostics.length}\n`);
|
|
@@ -237,6 +297,19 @@ async function doctorCommandImpl(args) {
|
|
|
237
297
|
return !isSharkcraftMissing;
|
|
238
298
|
});
|
|
239
299
|
}
|
|
300
|
+
// Optional LLM enrichment: never alters the deterministic emission below;
|
|
301
|
+
// only appended at the end. No-op when the flag is off or no provider is
|
|
302
|
+
// reachable — keeps the deterministic baseline byte-stable.
|
|
303
|
+
const wantLlmRecs = flagBool(args, 'llm-recommendations');
|
|
304
|
+
const llmEnvelope = wantLlmRecs
|
|
305
|
+
? await enrichWithLlmRecommendations({
|
|
306
|
+
surface: 'doctor',
|
|
307
|
+
deterministicSummary: summariseDoctorChecks(visibleChecks),
|
|
308
|
+
providerKind: flagString(args, 'provider') ?? undefined,
|
|
309
|
+
ask: 'For each warning or error, propose ONE concrete next-step the user can execute from a shell — name the `shrk` subcommand, file path, or config key. If a finding has no useful next-step, skip it.',
|
|
310
|
+
maxTokens: 1024,
|
|
311
|
+
})
|
|
312
|
+
: null;
|
|
240
313
|
const ackExpired = ackSummary.expired.length > 0 && failOnExpiredAcknowledgement;
|
|
241
314
|
// Under --no-config + missing sharkcraft, treat the run as advisory: do not
|
|
242
315
|
// red-fail on the inspector's "no sharkcraft" errors / warnings.
|
|
@@ -329,6 +402,7 @@ async function doctorCommandImpl(args) {
|
|
|
329
402
|
})),
|
|
330
403
|
...result,
|
|
331
404
|
...(filtered ? { filtered } : {}),
|
|
405
|
+
...(llmEnvelope ? { llmRecommendations: llmEnvelope } : {}),
|
|
332
406
|
}) + '\n');
|
|
333
407
|
return overallExitCode;
|
|
334
408
|
}
|
|
@@ -531,8 +605,31 @@ async function doctorCommandImpl(args) {
|
|
|
531
605
|
if (previewEligible) {
|
|
532
606
|
process.stdout.write('\nDraft patch available — run `shrk fix preview` for a preview-only patch under `.sharkcraft/fixes/`.\n');
|
|
533
607
|
}
|
|
608
|
+
if (llmEnvelope) {
|
|
609
|
+
process.stdout.write('\n');
|
|
610
|
+
process.stdout.write(renderRecommendationsMarkdown(llmEnvelope));
|
|
611
|
+
}
|
|
534
612
|
return overallExitCode;
|
|
535
613
|
}
|
|
614
|
+
function summariseDoctorChecks(checks) {
|
|
615
|
+
const lines = [];
|
|
616
|
+
const order = [DoctorSeverity.Error, DoctorSeverity.Warning, DoctorSeverity.Info, DoctorSeverity.Ok];
|
|
617
|
+
for (const sev of order) {
|
|
618
|
+
const grouped = checks.filter((c) => c.severity === sev);
|
|
619
|
+
if (grouped.length === 0)
|
|
620
|
+
continue;
|
|
621
|
+
const label = SEVERITY_LABEL[sev].trim();
|
|
622
|
+
lines.push(`## ${label} (${grouped.length})`);
|
|
623
|
+
for (const c of grouped) {
|
|
624
|
+
const fix = c.recommendedFix ?? c.fix;
|
|
625
|
+
lines.push(`- **${c.title}**${c.category ? ` (${c.category})` : ''}: ${c.message}${fix ? ` — suggested fix: ${fix}` : ''}`);
|
|
626
|
+
}
|
|
627
|
+
lines.push('');
|
|
628
|
+
}
|
|
629
|
+
if (lines.length === 0)
|
|
630
|
+
lines.push('(no findings — all checks passed)');
|
|
631
|
+
return lines.join('\n');
|
|
632
|
+
}
|
|
536
633
|
export const doctorSuppressCommand = {
|
|
537
634
|
name: 'suppress',
|
|
538
635
|
description: 'Add a doctor finding to sharkcraft/doctor.suppressions.json. Requires --reason.',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAoC,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAQ3F,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+DtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwF1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAmEpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,
|
|
1
|
+
{"version":3,"file":"graph-code-subverbs.d.ts","sourceRoot":"","sources":["../../src/commands/graph-code-subverbs.ts"],"names":[],"mappings":"AAqBA,OAAO,EAAoC,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAQ3F,wBAAsB,aAAa,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgBrE;AA4FD,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA+DtE;AAiBD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwF1E;AAID,wBAAsB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAmEpE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAiEtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAgDtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAsJvE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAwFtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAuCvE"}
|