@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.
Files changed (81) hide show
  1. package/dist/audit/knowledge-audit-llm.d.ts +19 -0
  2. package/dist/audit/knowledge-audit-llm.d.ts.map +1 -0
  3. package/dist/audit/knowledge-audit-llm.js +164 -0
  4. package/dist/audit/knowledge-audit.d.ts +61 -0
  5. package/dist/audit/knowledge-audit.d.ts.map +1 -0
  6. package/dist/audit/knowledge-audit.js +203 -0
  7. package/dist/audit/knowledge-fix-plan-llm.d.ts +11 -0
  8. package/dist/audit/knowledge-fix-plan-llm.d.ts.map +1 -0
  9. package/dist/audit/knowledge-fix-plan-llm.js +141 -0
  10. package/dist/audit/knowledge-fix-plan.d.ts +41 -0
  11. package/dist/audit/knowledge-fix-plan.d.ts.map +1 -0
  12. package/dist/audit/knowledge-fix-plan.js +125 -0
  13. package/dist/audit/pipeline-audit-llm.d.ts +11 -0
  14. package/dist/audit/pipeline-audit-llm.d.ts.map +1 -0
  15. package/dist/audit/pipeline-audit-llm.js +134 -0
  16. package/dist/audit/pipeline-audit.d.ts +69 -0
  17. package/dist/audit/pipeline-audit.d.ts.map +1 -0
  18. package/dist/audit/pipeline-audit.js +166 -0
  19. package/dist/audit/templates-audit-llm.d.ts +19 -0
  20. package/dist/audit/templates-audit-llm.d.ts.map +1 -0
  21. package/dist/audit/templates-audit-llm.js +207 -0
  22. package/dist/audit/templates-audit.d.ts +63 -0
  23. package/dist/audit/templates-audit.d.ts.map +1 -0
  24. package/dist/audit/templates-audit.js +171 -0
  25. package/dist/audit/templates-fix-plan-llm.d.ts +19 -0
  26. package/dist/audit/templates-fix-plan-llm.d.ts.map +1 -0
  27. package/dist/audit/templates-fix-plan-llm.js +162 -0
  28. package/dist/audit/templates-fix-plan.d.ts +37 -0
  29. package/dist/audit/templates-fix-plan.d.ts.map +1 -0
  30. package/dist/audit/templates-fix-plan.js +174 -0
  31. package/dist/commands/ai-status.command.d.ts +19 -0
  32. package/dist/commands/ai-status.command.d.ts.map +1 -0
  33. package/dist/commands/ai-status.command.js +94 -0
  34. package/dist/commands/ask.command.d.ts.map +1 -1
  35. package/dist/commands/ask.command.js +10 -9
  36. package/dist/commands/command-catalog.d.ts.map +1 -1
  37. package/dist/commands/command-catalog.js +110 -1
  38. package/dist/commands/deps-audit.command.d.ts +23 -0
  39. package/dist/commands/deps-audit.command.d.ts.map +1 -0
  40. package/dist/commands/deps-audit.command.js +266 -0
  41. package/dist/commands/doctor.command.d.ts.map +1 -1
  42. package/dist/commands/doctor.command.js +100 -3
  43. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  44. package/dist/commands/graph-code-subverbs.js +144 -26
  45. package/dist/commands/graph.command.d.ts.map +1 -1
  46. package/dist/commands/graph.command.js +3 -2
  47. package/dist/commands/help.command.d.ts.map +1 -1
  48. package/dist/commands/help.command.js +22 -1
  49. package/dist/commands/impact.command.d.ts.map +1 -1
  50. package/dist/commands/impact.command.js +3 -2
  51. package/dist/commands/move-plan.command.d.ts +23 -0
  52. package/dist/commands/move-plan.command.d.ts.map +1 -0
  53. package/dist/commands/move-plan.command.js +360 -0
  54. package/dist/commands/scaffold-validate.command.d.ts +22 -0
  55. package/dist/commands/scaffold-validate.command.d.ts.map +1 -0
  56. package/dist/commands/scaffold-validate.command.js +215 -0
  57. package/dist/commands/smart-context.command.d.ts +58 -0
  58. package/dist/commands/smart-context.command.d.ts.map +1 -0
  59. package/dist/commands/smart-context.command.js +4524 -0
  60. package/dist/commands/spike.command.d.ts +22 -0
  61. package/dist/commands/spike.command.d.ts.map +1 -0
  62. package/dist/commands/spike.command.js +235 -0
  63. package/dist/commands/surface.command.d.ts +1 -0
  64. package/dist/commands/surface.command.d.ts.map +1 -1
  65. package/dist/commands/surface.command.js +10 -3
  66. package/dist/commands/template-quality.command.d.ts.map +1 -1
  67. package/dist/commands/template-quality.command.js +39 -3
  68. package/dist/commands/templates.command.d.ts.map +1 -1
  69. package/dist/commands/templates.command.js +37 -2
  70. package/dist/commands/watch.command.d.ts +26 -0
  71. package/dist/commands/watch.command.d.ts.map +1 -0
  72. package/dist/commands/watch.command.js +456 -0
  73. package/dist/env/load-dotenv.d.ts +15 -0
  74. package/dist/env/load-dotenv.d.ts.map +1 -0
  75. package/dist/env/load-dotenv.js +70 -0
  76. package/dist/main.d.ts.map +1 -1
  77. package/dist/main.js +105 -2
  78. package/dist/schemas/json-schemas.d.ts +384 -36
  79. package/dist/schemas/json-schemas.d.ts.map +1 -1
  80. package/dist/schemas/json-schemas.js +247 -36
  81. 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;AA0JhC,eAAO,MAAM,aAAa,EAAE,eAW3B,CAAC;AA+bF,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"}
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,CA+DtE;AAID,wBAAsB,cAAc,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCtE;AAID,wBAAsB,eAAe,CAAC,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,CA6HvE;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"}
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"}