@shrkcrft/inspector 0.1.0-alpha.2 → 0.1.0-alpha.20

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 (192) hide show
  1. package/dist/agent-brief.d.ts.map +1 -1
  2. package/dist/agent-brief.js +59 -10
  3. package/dist/agent-contract-gate.d.ts.map +1 -1
  4. package/dist/agent-contract-gate.js +25 -2
  5. package/dist/agent-instructions.d.ts.map +1 -1
  6. package/dist/agent-instructions.js +11 -0
  7. package/dist/agent-task-prep.d.ts.map +1 -1
  8. package/dist/agent-task-prep.js +1 -3
  9. package/dist/ai-readiness.d.ts +84 -9
  10. package/dist/ai-readiness.d.ts.map +1 -1
  11. package/dist/ai-readiness.js +181 -35
  12. package/dist/apply-dispatch-trace.d.ts +1 -2
  13. package/dist/apply-dispatch-trace.d.ts.map +1 -1
  14. package/dist/apply-dispatch-trace.js +0 -9
  15. package/dist/area-explore.d.ts.map +1 -1
  16. package/dist/area-explore.js +4 -6
  17. package/dist/area-map.d.ts +0 -5
  18. package/dist/area-map.d.ts.map +1 -1
  19. package/dist/area-map.js +0 -10
  20. package/dist/changed-preflight.d.ts +7 -0
  21. package/dist/changed-preflight.d.ts.map +1 -1
  22. package/dist/changed-preflight.js +56 -9
  23. package/dist/changes-summary.d.ts.map +1 -1
  24. package/dist/changes-summary.js +10 -1
  25. package/dist/check-guardrail-globs.d.ts +16 -0
  26. package/dist/check-guardrail-globs.d.ts.map +1 -0
  27. package/dist/check-guardrail-globs.js +38 -0
  28. package/dist/code-intelligence-doctor.d.ts +21 -0
  29. package/dist/code-intelligence-doctor.d.ts.map +1 -0
  30. package/dist/code-intelligence-doctor.js +985 -0
  31. package/dist/command-recommender.d.ts.map +1 -1
  32. package/dist/command-recommender.js +23 -0
  33. package/dist/compliance-profiles.js +1 -1
  34. package/dist/construct-adoption-diff.d.ts.map +1 -1
  35. package/dist/construct-adoption-diff.js +2 -1
  36. package/dist/construct-adoption.d.ts.map +1 -1
  37. package/dist/construct-adoption.js +10 -11
  38. package/dist/construct-inference.d.ts.map +1 -1
  39. package/dist/construct-inference.js +5 -2
  40. package/dist/construct-registry.d.ts.map +1 -1
  41. package/dist/construct-registry.js +2 -10
  42. package/dist/contract-file-rule.d.ts +8 -0
  43. package/dist/contract-file-rule.d.ts.map +1 -1
  44. package/dist/contract-file-rule.js +8 -3
  45. package/dist/contract-template-registry.d.ts.map +1 -1
  46. package/dist/contract-template-registry.js +2 -10
  47. package/dist/contradictions.d.ts +8 -1
  48. package/dist/contradictions.d.ts.map +1 -1
  49. package/dist/contradictions.js +37 -35
  50. package/dist/convention-registry.d.ts.map +1 -1
  51. package/dist/convention-registry.js +2 -10
  52. package/dist/coverage-report.d.ts.map +1 -1
  53. package/dist/coverage-report.js +14 -1
  54. package/dist/dashboard/dashboard-knowledge.d.ts +8 -0
  55. package/dist/dashboard/dashboard-knowledge.d.ts.map +1 -0
  56. package/dist/dashboard/dashboard-knowledge.js +259 -0
  57. package/dist/decision-records.d.ts.map +1 -1
  58. package/dist/decision-records.js +5 -10
  59. package/dist/delegate-catalog.d.ts +45 -0
  60. package/dist/delegate-catalog.d.ts.map +1 -0
  61. package/dist/delegate-catalog.js +50 -0
  62. package/dist/delegate-doctor.d.ts +15 -0
  63. package/dist/delegate-doctor.d.ts.map +1 -0
  64. package/dist/delegate-doctor.js +36 -0
  65. package/dist/delegate-pack-recipes.d.ts +29 -0
  66. package/dist/delegate-pack-recipes.d.ts.map +1 -0
  67. package/dist/delegate-pack-recipes.js +77 -0
  68. package/dist/demo-script.d.ts +0 -1
  69. package/dist/demo-script.d.ts.map +1 -1
  70. package/dist/demo-script.js +0 -43
  71. package/dist/docs-check.js +1 -1
  72. package/dist/drift-baseline.d.ts.map +1 -1
  73. package/dist/drift-baseline.js +5 -2
  74. package/dist/feedback-ingestion.d.ts.map +1 -1
  75. package/dist/feedback-ingestion.js +2 -16
  76. package/dist/git-helpers.d.ts +15 -0
  77. package/dist/git-helpers.d.ts.map +1 -1
  78. package/dist/git-helpers.js +51 -4
  79. package/dist/helper-registry.d.ts +27 -54
  80. package/dist/helper-registry.d.ts.map +1 -1
  81. package/dist/helper-registry.js +16 -517
  82. package/dist/impact-analysis.d.ts.map +1 -1
  83. package/dist/impact-analysis.js +14 -7
  84. package/dist/index.d.ts +8 -2
  85. package/dist/index.d.ts.map +1 -1
  86. package/dist/index.js +8 -2
  87. package/dist/ingest-drafts.js +8 -4
  88. package/dist/migration-profile-registry.d.ts.map +1 -1
  89. package/dist/migration-profile-registry.js +2 -10
  90. package/dist/monorepo-onboarding.js +2 -2
  91. package/dist/onboarding-report.d.ts.map +1 -1
  92. package/dist/onboarding-report.js +5 -1
  93. package/dist/onboarding.d.ts +1 -1
  94. package/dist/onboarding.d.ts.map +1 -1
  95. package/dist/onboarding.js +9 -66
  96. package/dist/ownership.js +2 -10
  97. package/dist/pack-contributions-inventory.d.ts +0 -1
  98. package/dist/pack-contributions-inventory.d.ts.map +1 -1
  99. package/dist/pack-contributions-inventory.js +17 -29
  100. package/dist/pack-helper-registry.d.ts.map +1 -1
  101. package/dist/pack-helper-registry.js +2 -10
  102. package/dist/pack-release-check.d.ts.map +1 -1
  103. package/dist/pack-release-check.js +4 -11
  104. package/dist/pack-signature-status.d.ts.map +1 -1
  105. package/dist/pack-signature-status.js +18 -2
  106. package/dist/pack-test-runner.js +2 -10
  107. package/dist/plan-review.d.ts.map +1 -1
  108. package/dist/plan-review.js +5 -10
  109. package/dist/plan-simulation.d.ts +13 -0
  110. package/dist/plan-simulation.d.ts.map +1 -1
  111. package/dist/plan-simulation.js +4 -21
  112. package/dist/playbook-registry.d.ts.map +1 -1
  113. package/dist/playbook-registry.js +2 -10
  114. package/dist/policy-engine.d.ts.map +1 -1
  115. package/dist/policy-engine.js +3 -11
  116. package/dist/policy-test.js +3 -11
  117. package/dist/profile-registry.d.ts +0 -1
  118. package/dist/profile-registry.d.ts.map +1 -1
  119. package/dist/profile-registry.js +4 -32
  120. package/dist/propose-knowledge.d.ts +15 -0
  121. package/dist/propose-knowledge.d.ts.map +1 -1
  122. package/dist/propose-knowledge.js +37 -4
  123. package/dist/quality-baseline.d.ts.map +1 -1
  124. package/dist/quality-baseline.js +3 -1
  125. package/dist/ranker-explainability.d.ts.map +1 -1
  126. package/dist/ranker-explainability.js +3 -9
  127. package/dist/registration-hint-registry.d.ts.map +1 -1
  128. package/dist/registration-hint-registry.js +2 -10
  129. package/dist/registry-lifecycle.d.ts +6 -0
  130. package/dist/registry-lifecycle.d.ts.map +1 -1
  131. package/dist/registry-lifecycle.js +137 -10
  132. package/dist/release-readiness.js +3 -3
  133. package/dist/repo-memory.d.ts.map +1 -1
  134. package/dist/repo-memory.js +3 -1
  135. package/dist/reposet.js +1 -1
  136. package/dist/repository-intelligence.d.ts.map +1 -1
  137. package/dist/repository-intelligence.js +7 -2
  138. package/dist/repository-knowledge-model.d.ts +1 -1
  139. package/dist/repository-knowledge-model.d.ts.map +1 -1
  140. package/dist/repository-stats.d.ts.map +1 -1
  141. package/dist/repository-stats.js +3 -1
  142. package/dist/resolve-verification-commands.d.ts +26 -0
  143. package/dist/resolve-verification-commands.d.ts.map +1 -0
  144. package/dist/resolve-verification-commands.js +55 -0
  145. package/dist/review-packet.d.ts.map +1 -1
  146. package/dist/review-packet.js +14 -17
  147. package/dist/rule-drift.d.ts.map +1 -1
  148. package/dist/rule-drift.js +24 -9
  149. package/dist/rule-scaffold.d.ts.map +1 -1
  150. package/dist/rule-scaffold.js +12 -4
  151. package/dist/scaffold-patterns.js +2 -10
  152. package/dist/search-tuning-registry.d.ts.map +1 -1
  153. package/dist/search-tuning-registry.js +2 -10
  154. package/dist/self-config-doctor-v2.d.ts +1 -1
  155. package/dist/self-config-doctor-v2.d.ts.map +1 -1
  156. package/dist/self-config-doctor-v2.js +6 -10
  157. package/dist/self-config-doctor.d.ts.map +1 -1
  158. package/dist/self-config-doctor.js +7 -13
  159. package/dist/sharkcraft-inspector.d.ts +14 -0
  160. package/dist/sharkcraft-inspector.d.ts.map +1 -1
  161. package/dist/sharkcraft-inspector.js +103 -1
  162. package/dist/start-here.d.ts +2 -2
  163. package/dist/start-here.d.ts.map +1 -1
  164. package/dist/start-here.js +16 -1
  165. package/dist/synthesize-from-onboarding.d.ts +68 -0
  166. package/dist/synthesize-from-onboarding.d.ts.map +1 -0
  167. package/dist/synthesize-from-onboarding.js +508 -0
  168. package/dist/task-packet.d.ts +13 -0
  169. package/dist/task-packet.d.ts.map +1 -1
  170. package/dist/task-packet.js +59 -6
  171. package/dist/task-ranker.d.ts.map +1 -1
  172. package/dist/task-ranker.js +1 -31
  173. package/dist/task-routing-hint-registry.d.ts.map +1 -1
  174. package/dist/task-routing-hint-registry.js +2 -10
  175. package/dist/template-drift.d.ts +7 -0
  176. package/dist/template-drift.d.ts.map +1 -1
  177. package/dist/template-drift.js +14 -6
  178. package/dist/test-impact.d.ts.map +1 -1
  179. package/dist/test-impact.js +5 -2
  180. package/dist/test-runner.d.ts.map +1 -1
  181. package/dist/test-runner.js +12 -17
  182. package/dist/universal-search.d.ts +0 -1
  183. package/dist/universal-search.d.ts.map +1 -1
  184. package/dist/universal-search.js +0 -12
  185. package/dist/why-file.js +66 -22
  186. package/package.json +18 -18
  187. package/dist/plugin-lifecycle-profile-registry.d.ts +0 -52
  188. package/dist/plugin-lifecycle-profile-registry.d.ts.map +0 -1
  189. package/dist/plugin-lifecycle-profile-registry.js +0 -202
  190. package/dist/plugin-lifecycle.d.ts +0 -132
  191. package/dist/plugin-lifecycle.d.ts.map +0 -1
  192. package/dist/plugin-lifecycle.js +0 -477
@@ -0,0 +1,985 @@
1
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
+ import * as nodePath from 'node:path';
3
+ import { DoctorSeverity } from "./doctor-result.js";
4
+ const STALE_THRESHOLD_DAYS = 7;
5
+ const DAY_MS = 24 * 60 * 60 * 1000;
6
+ const CATEGORY = 'code-intelligence';
7
+ function readJsonFile(absPath) {
8
+ if (!existsSync(absPath))
9
+ return undefined;
10
+ try {
11
+ return JSON.parse(readFileSync(absPath, 'utf8'));
12
+ }
13
+ catch {
14
+ return undefined;
15
+ }
16
+ }
17
+ function ageDays(iso, nowMs) {
18
+ if (!iso)
19
+ return undefined;
20
+ const t = Date.parse(iso);
21
+ if (Number.isNaN(t))
22
+ return undefined;
23
+ return (nowMs - t) / DAY_MS;
24
+ }
25
+ function fmtAge(days) {
26
+ if (days < 1 / 24) {
27
+ const minutes = Math.max(1, Math.round(days * 24 * 60));
28
+ return `${minutes}m ago`;
29
+ }
30
+ if (days < 1) {
31
+ const hours = Math.max(1, Math.round(days * 24));
32
+ return `${hours}h ago`;
33
+ }
34
+ return `${Math.round(days)}d ago`;
35
+ }
36
+ function sumValues(rec) {
37
+ if (!rec)
38
+ return 0;
39
+ let total = 0;
40
+ for (const v of Object.values(rec)) {
41
+ if (typeof v === 'number' && Number.isFinite(v))
42
+ total += v;
43
+ }
44
+ return total;
45
+ }
46
+ /**
47
+ * Read every code-intelligence package's persisted state and produce a
48
+ * compact set of doctor findings. The function is sync (matches the
49
+ * rest of `runDoctor`) and silent when no state is present (e.g. the
50
+ * user has never run `shrk graph index`).
51
+ */
52
+ export function buildCodeIntelligenceChecks(projectRoot, options = {}) {
53
+ const nowMs = options.nowMs ?? Date.now();
54
+ const staleDays = options.staleThresholdDays ?? STALE_THRESHOLD_DAYS;
55
+ const checks = [];
56
+ checks.push(...graphChecks(projectRoot, nowMs, staleDays));
57
+ checks.push(...ruleGraphChecks(projectRoot, nowMs, staleDays));
58
+ checks.push(...apiSurfaceChecks(projectRoot, nowMs, staleDays));
59
+ checks.push(...qualityGateChecks(projectRoot, nowMs, staleDays));
60
+ checks.push(...migrationChecks(projectRoot));
61
+ checks.push(...architectureChecks(projectRoot, nowMs, staleDays));
62
+ checks.push(...impactRunChecks(projectRoot, nowMs, staleDays));
63
+ checks.push(...frameworkChecks(projectRoot, nowMs, staleDays));
64
+ checks.push(...structuralRegistryChecks(projectRoot));
65
+ checks.push(...contextPlannerChecks(projectRoot, nowMs, staleDays));
66
+ checks.push(...schemaCompatChecks(projectRoot));
67
+ return checks;
68
+ }
69
+ /**
70
+ * Expected on-disk schema strings, keyed by `.sharkcraft/<rel>` store
71
+ * file. Used by the schema-compat check to detect when a stored payload
72
+ * was written by an older (incompatible) version of a package. When a
73
+ * package bumps to a new major schema we add a row here AND the
74
+ * matching reader on the producing side; the doctor flags any tree
75
+ * that still has the older version.
76
+ */
77
+ const EXPECTED_SCHEMAS = [
78
+ { rel: 'graph/meta.json', expected: 'sharkcraft.graph/v1', package: '@shrkcrft/graph' },
79
+ { rel: 'bridge/meta.json', expected: 'sharkcraft.rule-graph/v1', package: '@shrkcrft/rule-graph' },
80
+ { rel: 'api-surface/signatures.json', expected: 'sharkcraft.api-surface-cache/v1', package: '@shrkcrft/api-surface-diff' },
81
+ { rel: 'quality-gates/last.json', expected: 'sharkcraft.quality-gate-report/v1', package: '@shrkcrft/quality-gates' },
82
+ { rel: 'framework/meta.json', expected: 'sharkcraft.framework/v1', package: '@shrkcrft/framework-scanners' },
83
+ { rel: 'architecture/baseline.json', expected: 'sharkcraft.architecture-snapshot/v1', package: '@shrkcrft/architecture-guard' },
84
+ { rel: 'architecture/last.json', expected: 'sharkcraft.architecture-snapshot/v1', package: '@shrkcrft/architecture-guard' },
85
+ { rel: 'impact/last.json', expected: 'sharkcraft.impact-run/v1', package: '@shrkcrft/impact-engine' },
86
+ { rel: 'impact/baseline.json', expected: 'sharkcraft.impact-run/v1', package: '@shrkcrft/impact-engine' },
87
+ { rel: 'structural/patterns.json', expected: 'sharkcraft.structural-pattern-registry/v1', package: '@shrkcrft/structural-search' },
88
+ { rel: 'context-planner/intent-benchmark.json', expected: 'sharkcraft.intent-benchmark/v1', package: '@shrkcrft/context-planner' },
89
+ ];
90
+ function graphChecks(projectRoot, nowMs, staleDays) {
91
+ const metaPath = nodePath.join(projectRoot, '.sharkcraft', 'graph', 'meta.json');
92
+ if (!existsSync(metaPath)) {
93
+ return [
94
+ {
95
+ id: 'code-intelligence-graph',
96
+ title: 'Code-intelligence graph index',
97
+ severity: DoctorSeverity.Info,
98
+ category: CATEGORY,
99
+ message: 'No code graph indexed yet — `shrk impact`, `shrk graph callers`, and context packs fall back to slower scans.',
100
+ fix: 'Run `shrk graph index` once to enable code-intelligence queries.',
101
+ },
102
+ ];
103
+ }
104
+ const manifest = readJsonFile(metaPath);
105
+ if (!manifest) {
106
+ return [
107
+ {
108
+ id: 'code-intelligence-graph',
109
+ title: 'Code-intelligence graph index',
110
+ severity: DoctorSeverity.Warning,
111
+ category: CATEGORY,
112
+ message: '.sharkcraft/graph/meta.json exists but is not valid JSON.',
113
+ fix: 'Rebuild with `shrk graph index --full`.',
114
+ whyThisMatters: 'Without a usable graph manifest the dependent surfaces (impact, callers, context-planner) silently degrade.',
115
+ },
116
+ ];
117
+ }
118
+ const days = ageDays(manifest.lastIndexedAt, nowMs);
119
+ const stale = days !== undefined && days > staleDays;
120
+ const ageStr = days !== undefined ? ` (${fmtAge(days)})` : '';
121
+ const counts = `${manifest.filesIndexed ?? 0} files, ${sumValues(manifest.nodesByKind)} nodes, ${sumValues(manifest.edgesByKind)} edges`;
122
+ const cycleTag = typeof manifest.cycleCount === 'number' && manifest.cycleCount > 0
123
+ ? `, ${manifest.cycleCount} cycle${manifest.cycleCount === 1 ? '' : 's'}` +
124
+ (typeof manifest.largestCycleSize === 'number' && manifest.largestCycleSize > 0
125
+ ? ` (largest ${manifest.largestCycleSize})`
126
+ : '')
127
+ : '';
128
+ const out = [];
129
+ if (stale) {
130
+ out.push({
131
+ id: 'code-intelligence-graph',
132
+ title: 'Code-intelligence graph index',
133
+ severity: DoctorSeverity.Warning,
134
+ advisory: true,
135
+ category: CATEGORY,
136
+ message: `Graph index is stale${ageStr} — ${counts}${cycleTag}.`,
137
+ fix: 'Re-index with `shrk graph index --changed` (or `--full`).',
138
+ whyThisMatters: 'Stale code graph makes `shrk impact`, `shrk graph callers`, and context packs return outdated answers.',
139
+ });
140
+ }
141
+ else {
142
+ out.push({
143
+ id: 'code-intelligence-graph',
144
+ title: 'Code-intelligence graph index',
145
+ severity: DoctorSeverity.Ok,
146
+ category: CATEGORY,
147
+ message: `Graph index fresh${ageStr} — ${counts}${cycleTag}.`,
148
+ });
149
+ }
150
+ // Cycles ≥ a heuristic threshold of 5 (or any 3+-file cycle) become
151
+ // an advisory hint, so the doctor surfaces a refactor target rather
152
+ // than only the running count. Boundary on >=3 catches the harder
153
+ // refactors; small 2-file cycles often come and go and would otherwise
154
+ // be noisy.
155
+ const largeCycle = typeof manifest.largestCycleSize === 'number' && manifest.largestCycleSize >= 3;
156
+ const manyCycles = typeof manifest.cycleCount === 'number' && manifest.cycleCount >= 5;
157
+ if (largeCycle || manyCycles) {
158
+ out.push({
159
+ id: 'code-intelligence-graph-cycles',
160
+ title: 'Code-intelligence graph cycles',
161
+ severity: DoctorSeverity.Warning,
162
+ advisory: true,
163
+ category: CATEGORY,
164
+ message: `${manifest.cycleCount ?? 0} import cycle(s) in the graph` +
165
+ (manifest.largestCycleSize ? ` (largest spans ${manifest.largestCycleSize} files)` : '') +
166
+ (manifest.filesInCycles ? `, ${manifest.filesInCycles} file(s) in cycles.` : '.'),
167
+ fix: 'List with `shrk graph cycles` or breakdown with `shrk arch check`.',
168
+ whyThisMatters: 'Import cycles freeze refactors and make `shrk impact` overestimate blast radius (everything in the cycle becomes reachable from everything else).',
169
+ });
170
+ }
171
+ // Unresolved imports — high-signal DX warning. The indexer emits a
172
+ // `file: → unresolved:<spec>` edge for every import the resolver
173
+ // couldn't match against an on-disk file. Almost always a typo,
174
+ // missing dependency, or a path-alias that was renamed without
175
+ // updating the importer.
176
+ if (typeof manifest.unresolvedImportCount === 'number' &&
177
+ manifest.unresolvedImportCount > 0) {
178
+ const samples = manifest.unresolvedImportSamples ?? [];
179
+ const sampleStr = samples.length > 0
180
+ ? ` — first ${Math.min(samples.length, 3)}: ${samples
181
+ .slice(0, 3)
182
+ .map((s) => JSON.stringify(s))
183
+ .join(', ')}${samples.length > 3 ? '…' : ''}`
184
+ : '';
185
+ out.push({
186
+ id: 'code-intelligence-graph-unresolved',
187
+ title: 'Code-intelligence unresolved imports',
188
+ severity: DoctorSeverity.Warning,
189
+ category: CATEGORY,
190
+ message: `${manifest.unresolvedImportCount} unresolved import(s) across ` +
191
+ `${manifest.filesWithUnresolvedImports ?? 0} file(s)${sampleStr}.`,
192
+ fix: 'Inspect with `shrk graph search --kind file --has-unresolved-imports` (or grep for the sample specifiers). Likely causes: typo in path, deleted file still imported, alias renamed without updating callers.',
193
+ whyThisMatters: "Unresolved imports leak past the typechecker for path-alias / dynamic-import paths and cause `shrk impact` to under-count dependents (no edge exists from the broken import to the intended target).",
194
+ });
195
+ }
196
+ return out;
197
+ }
198
+ function ruleGraphChecks(projectRoot, nowMs, staleDays) {
199
+ const metaPath = nodePath.join(projectRoot, '.sharkcraft', 'bridge', 'meta.json');
200
+ if (!existsSync(metaPath)) {
201
+ // Silent when no bridge has been built. The bridge is downstream of
202
+ // `shrk graph index`; the graph check above is enough of a nudge.
203
+ return [];
204
+ }
205
+ const manifest = readJsonFile(metaPath);
206
+ if (!manifest) {
207
+ return [
208
+ {
209
+ id: 'code-intelligence-rule-graph',
210
+ title: 'Code-intelligence rule-graph bridge',
211
+ severity: DoctorSeverity.Warning,
212
+ category: CATEGORY,
213
+ message: '.sharkcraft/bridge/meta.json exists but is not valid JSON.',
214
+ fix: 'Rebuild with `shrk graph index`.',
215
+ },
216
+ ];
217
+ }
218
+ const days = ageDays(manifest.lastBuiltAt, nowMs);
219
+ const stale = days !== undefined && days > staleDays;
220
+ const ageStr = days !== undefined ? ` (${fmtAge(days)})` : '';
221
+ const counts = `${sumValues(manifest.nodesByKind)} bridge nodes, ${sumValues(manifest.edgesByKind)} edges`;
222
+ const out = [];
223
+ if (stale) {
224
+ out.push({
225
+ id: 'code-intelligence-rule-graph',
226
+ title: 'Code-intelligence rule-graph bridge',
227
+ severity: DoctorSeverity.Warning,
228
+ advisory: true,
229
+ category: CATEGORY,
230
+ message: `Rule-graph bridge is stale${ageStr} — ${counts}.`,
231
+ fix: 'Re-build with `shrk graph index` (bridges build alongside graph).',
232
+ whyThisMatters: 'A stale bridge means `shrk rules where applies-to <file>` and rule-aware impact answers may miss recent edits.',
233
+ });
234
+ }
235
+ else {
236
+ out.push({
237
+ id: 'code-intelligence-rule-graph',
238
+ title: 'Code-intelligence rule-graph bridge',
239
+ severity: DoctorSeverity.Ok,
240
+ category: CATEGORY,
241
+ message: `Rule-graph bridge fresh${ageStr} — ${counts}.`,
242
+ });
243
+ }
244
+ // §3.2 exit criterion: bridge coverage gap. Surface when more than
245
+ // half of indexed files have no applicable rule edge. We deliberately
246
+ // skip the case where bridge coverage fields are absent (forward-
247
+ // compat with older manifest writers) — they were added in 2026-05.
248
+ const total = manifest.filesTotal;
249
+ const uncovered = manifest.filesUncoveredByRules;
250
+ const covered = manifest.filesCoveredByRules;
251
+ if (typeof total === 'number' &&
252
+ total > 0 &&
253
+ typeof uncovered === 'number' &&
254
+ typeof covered === 'number') {
255
+ const ratio = uncovered / total;
256
+ const pct = Math.round(ratio * 100);
257
+ const baseMsg = `${covered}/${total} files covered by rules (${100 - pct}%).`;
258
+ if (ratio > 0.5) {
259
+ out.push({
260
+ id: 'code-intelligence-rule-coverage',
261
+ title: 'Code-intelligence rule coverage',
262
+ severity: DoctorSeverity.Warning,
263
+ advisory: true,
264
+ category: CATEGORY,
265
+ message: `${baseMsg} ${uncovered} file(s) have no applicable rule.`,
266
+ fix: 'Inspect with `shrk rules where applies-to <file>` and either broaden a rule\'s `appliesTo` / boundary `from`, or accept the gap.',
267
+ whyThisMatters: 'Files with no applicable rule are invisible to rule-aware impact, validation hints, and agent context packs. A growing coverage gap usually means the rule registry is drifting behind the codebase.',
268
+ });
269
+ }
270
+ else {
271
+ out.push({
272
+ id: 'code-intelligence-rule-coverage',
273
+ title: 'Code-intelligence rule coverage',
274
+ severity: DoctorSeverity.Ok,
275
+ category: CATEGORY,
276
+ message: baseMsg,
277
+ });
278
+ }
279
+ }
280
+ return out;
281
+ }
282
+ function apiSurfaceChecks(projectRoot, nowMs, staleDays) {
283
+ const cachePath = nodePath.join(projectRoot, '.sharkcraft', 'api-surface', 'signatures.json');
284
+ if (!existsSync(cachePath)) {
285
+ return [];
286
+ }
287
+ const cache = readJsonFile(cachePath);
288
+ if (!cache) {
289
+ return [
290
+ {
291
+ id: 'code-intelligence-api-surface',
292
+ title: 'API surface signature cache',
293
+ severity: DoctorSeverity.Info,
294
+ category: CATEGORY,
295
+ message: '.sharkcraft/api-surface/signatures.json exists but is not valid JSON. Next `shrk api-diff --with-signatures` will rebuild it.',
296
+ },
297
+ ];
298
+ }
299
+ const days = ageDays(cache.generatedAt, nowMs);
300
+ const stale = days !== undefined && days > staleDays;
301
+ const ageStr = days !== undefined ? ` (${fmtAge(days)})` : '';
302
+ const fileCount = cache.files ? Object.keys(cache.files).length : 0;
303
+ return [
304
+ {
305
+ id: 'code-intelligence-api-surface',
306
+ title: 'API surface signature cache',
307
+ severity: stale ? DoctorSeverity.Warning : DoctorSeverity.Ok,
308
+ advisory: stale,
309
+ category: CATEGORY,
310
+ message: stale
311
+ ? `API surface cache stale${ageStr} — ${fileCount} files cached.`
312
+ : `API surface cache fresh${ageStr} — ${fileCount} files cached.`,
313
+ ...(stale
314
+ ? {
315
+ fix: 'Refresh with `shrk api-diff --with-signatures` next time you diff a release.',
316
+ whyThisMatters: 'Stale signatures make `shrk api-diff` miss real signature-changed findings until the cache is rebuilt.',
317
+ }
318
+ : {}),
319
+ },
320
+ ];
321
+ }
322
+ function qualityGateChecks(projectRoot, nowMs, staleDays) {
323
+ const reportPath = nodePath.join(projectRoot, '.sharkcraft', 'quality-gates', 'last.json');
324
+ if (!existsSync(reportPath)) {
325
+ return [];
326
+ }
327
+ const report = readJsonFile(reportPath);
328
+ if (!report) {
329
+ return [
330
+ {
331
+ id: 'code-intelligence-quality-gate',
332
+ title: 'Quality gate (last run)',
333
+ severity: DoctorSeverity.Info,
334
+ category: CATEGORY,
335
+ message: '.sharkcraft/quality-gates/last.json exists but is not valid JSON. Re-run `shrk gate`.',
336
+ },
337
+ ];
338
+ }
339
+ const days = ageDays(report.startedAt, nowMs);
340
+ const ageStr = days !== undefined ? ` ${fmtAge(days)}` : '';
341
+ const stale = days !== undefined && days > staleDays;
342
+ const status = report.overall ?? 'unknown';
343
+ const failingGates = (report.gates ?? [])
344
+ .filter((g) => g.status === 'fail')
345
+ .map((g) => g.id ?? '?');
346
+ if (status === 'pass') {
347
+ return [
348
+ {
349
+ id: 'code-intelligence-quality-gate',
350
+ title: 'Quality gate (last run)',
351
+ severity: DoctorSeverity.Ok,
352
+ category: CATEGORY,
353
+ message: `Last gate pass${ageStr}.`,
354
+ },
355
+ ];
356
+ }
357
+ if (status === 'fail') {
358
+ const failMsg = failingGates.length > 0
359
+ ? `Last gate FAIL${ageStr} — ${failingGates.join(', ')}.`
360
+ : `Last gate FAIL${ageStr}.`;
361
+ // An old FAIL is stale maintenance, not a verified current regression:
362
+ // age it out into a folded advisory that nudges a re-run instead of a
363
+ // hard Warning that masks the (now-unknown) state of the tree. A fresh
364
+ // FAIL stays loud. Mirrors the stale handling in apiSurfaceChecks /
365
+ // architectureChecks so the whole code-intelligence section is consistent.
366
+ if (stale) {
367
+ return [
368
+ {
369
+ id: 'code-intelligence-quality-gate',
370
+ title: 'Quality gate (last run)',
371
+ severity: DoctorSeverity.Info,
372
+ advisory: true,
373
+ category: CATEGORY,
374
+ message: `${failMsg} Stale (>${staleDays}d) — may not reflect the current tree.`,
375
+ fix: 'Re-run `shrk gate` to refresh.',
376
+ },
377
+ ];
378
+ }
379
+ return [
380
+ {
381
+ id: 'code-intelligence-quality-gate',
382
+ title: 'Quality gate (last run)',
383
+ severity: DoctorSeverity.Warning,
384
+ category: CATEGORY,
385
+ message: failMsg,
386
+ fix: 'Re-run with `shrk gate` and address the failing gate(s).',
387
+ whyThisMatters: 'The quality gate is the one-shot pass/fail the dashboard and CI agents trust — leaving it red hides real regressions.',
388
+ },
389
+ ];
390
+ }
391
+ // warn / skipped / unknown
392
+ return [
393
+ {
394
+ id: 'code-intelligence-quality-gate',
395
+ title: 'Quality gate (last run)',
396
+ severity: DoctorSeverity.Info,
397
+ advisory: true,
398
+ category: CATEGORY,
399
+ message: `Last gate ${status}${ageStr}.`,
400
+ },
401
+ ];
402
+ }
403
+ function architectureChecks(projectRoot, nowMs, staleDays) {
404
+ const archDir = nodePath.join(projectRoot, '.sharkcraft', 'architecture');
405
+ const baselinePath = nodePath.join(archDir, 'baseline.json');
406
+ const lastPath = nodePath.join(archDir, 'last.json');
407
+ const hasBaseline = existsSync(baselinePath);
408
+ const hasLast = existsSync(lastPath);
409
+ if (!hasBaseline && !hasLast)
410
+ return [];
411
+ const baseline = hasBaseline
412
+ ? readJsonFile(baselinePath)
413
+ : undefined;
414
+ const last = hasLast ? readJsonFile(lastPath) : undefined;
415
+ // Treat "last present, baseline absent" as a soft hint to freeze a
416
+ // baseline so future regressions are caught.
417
+ if (hasLast && !hasBaseline) {
418
+ const lastDays = ageDays(last?.generatedAt, nowMs);
419
+ const ageStr = lastDays !== undefined ? ` (${fmtAge(lastDays)})` : '';
420
+ const errCount = last?.countsBySeverity?.['error'] ?? 0;
421
+ const warnCount = last?.countsBySeverity?.['warning'] ?? 0;
422
+ return [
423
+ {
424
+ id: 'code-intelligence-architecture',
425
+ title: 'Architecture baseline',
426
+ severity: DoctorSeverity.Info,
427
+ category: CATEGORY,
428
+ message: `No baseline frozen. Last arch run${ageStr}: ${errCount} error, ${warnCount} warning.`,
429
+ fix: 'Freeze a baseline with `shrk arch baseline write` so doctor surfaces regressions.',
430
+ },
431
+ ];
432
+ }
433
+ // Baseline present but no last run — surface as info pointing at the
434
+ // command that fills the gap.
435
+ if (hasBaseline && !hasLast) {
436
+ return [
437
+ {
438
+ id: 'code-intelligence-architecture',
439
+ title: 'Architecture baseline',
440
+ severity: DoctorSeverity.Info,
441
+ category: CATEGORY,
442
+ message: 'Architecture baseline present, but no recent arch run to compare against.',
443
+ fix: 'Run `shrk arch check` to refresh `.sharkcraft/architecture/last.json`.',
444
+ },
445
+ ];
446
+ }
447
+ if (!baseline || !last) {
448
+ return [
449
+ {
450
+ id: 'code-intelligence-architecture',
451
+ title: 'Architecture baseline',
452
+ severity: DoctorSeverity.Warning,
453
+ category: CATEGORY,
454
+ message: '.sharkcraft/architecture/{baseline,last}.json could not be read.',
455
+ fix: 'Re-freeze with `shrk arch baseline write`.',
456
+ },
457
+ ];
458
+ }
459
+ const baseIds = new Set(baseline.violationIds ?? []);
460
+ const lastIds = new Set(last.violationIds ?? []);
461
+ let newCount = 0;
462
+ let fixedCount = 0;
463
+ const newSample = [];
464
+ for (const id of lastIds) {
465
+ if (!baseIds.has(id)) {
466
+ newCount += 1;
467
+ if (newSample.length < 3)
468
+ newSample.push(id);
469
+ }
470
+ }
471
+ for (const id of baseIds)
472
+ if (!lastIds.has(id))
473
+ fixedCount += 1;
474
+ const lastDays = ageDays(last.generatedAt, nowMs);
475
+ const lastStale = lastDays !== undefined && lastDays > staleDays;
476
+ const errDelta = (last.countsBySeverity?.['error'] ?? 0) -
477
+ (baseline.countsBySeverity?.['error'] ?? 0);
478
+ const warnDelta = (last.countsBySeverity?.['warning'] ?? 0) -
479
+ (baseline.countsBySeverity?.['warning'] ?? 0);
480
+ if (newCount === 0 && errDelta <= 0 && warnDelta <= 0) {
481
+ return [
482
+ {
483
+ id: 'code-intelligence-architecture',
484
+ title: 'Architecture baseline',
485
+ severity: DoctorSeverity.Ok,
486
+ category: CATEGORY,
487
+ message: `Within baseline — error ${errDelta}, warning ${warnDelta}` +
488
+ (fixedCount > 0 ? `, ${fixedCount} fixed since baseline.` : '.') +
489
+ (lastStale ? ` (last run ${fmtAge(lastDays)} — stale).` : ''),
490
+ ...(lastStale
491
+ ? {
492
+ fix: 'Refresh with `shrk arch check`.',
493
+ }
494
+ : {}),
495
+ },
496
+ ];
497
+ }
498
+ return [
499
+ {
500
+ id: 'code-intelligence-architecture',
501
+ title: 'Architecture baseline',
502
+ severity: DoctorSeverity.Warning,
503
+ category: CATEGORY,
504
+ message: `${newCount} new arch violation(s) since baseline` +
505
+ (newSample.length > 0 ? ` — ${newSample.join(', ')}${newCount > newSample.length ? '…' : ''}` : '') +
506
+ ` (error ${errDelta >= 0 ? '+' : ''}${errDelta}, warning ${warnDelta >= 0 ? '+' : ''}${warnDelta}).`,
507
+ fix: 'Inspect with `shrk arch check`. If the new violations are intentional, re-freeze with `shrk arch baseline write`.',
508
+ whyThisMatters: 'The baseline lets the doctor catch architecture regressions the moment they appear, rather than waiting for someone to scroll through `shrk arch check` output.',
509
+ },
510
+ ];
511
+ }
512
+ function migrationChecks(projectRoot) {
513
+ const dir = nodePath.join(projectRoot, '.sharkcraft', 'migrations');
514
+ if (!existsSync(dir))
515
+ return [];
516
+ let entries = [];
517
+ try {
518
+ entries = readdirSync(dir).filter((f) => f.endsWith('.state.json'));
519
+ }
520
+ catch {
521
+ return [];
522
+ }
523
+ const failed = [];
524
+ for (const entry of entries) {
525
+ const report = readJsonFile(nodePath.join(dir, entry));
526
+ if (!report)
527
+ continue;
528
+ if (report.overall !== 'fail')
529
+ continue;
530
+ const failedStep = (report.steps ?? []).find((s) => s.status === 'failed');
531
+ const id = report.migration?.id ?? entry.replace(/\.state\.json$/, '');
532
+ failed.push({
533
+ id,
534
+ ...(failedStep?.id ? { failedStep: failedStep.id } : {}),
535
+ });
536
+ }
537
+ if (failed.length === 0)
538
+ return [];
539
+ const first = failed[0];
540
+ const head = failed
541
+ .slice(0, 3)
542
+ .map((f) => f.id + (f.failedStep ? ` @ ${f.failedStep}` : ''))
543
+ .join(', ');
544
+ const tail = failed.length > 3 ? '…' : '';
545
+ return [
546
+ {
547
+ id: 'code-intelligence-migrations',
548
+ title: 'Code-intelligence migrations',
549
+ severity: DoctorSeverity.Warning,
550
+ category: CATEGORY,
551
+ message: `${failed.length} failed migration checkpoint(s) on disk: ${head}${tail}`,
552
+ fix: `Resume with \`shrk migrate resume ${first.id}\` (or \`shrk migrate prune --include-failed\` to discard).`,
553
+ whyThisMatters: "Failed migration checkpoints persist between runs so the agent or human can resume them. Doctor flags them so they don't linger silently.",
554
+ },
555
+ ];
556
+ }
557
+ function impactRunChecks(projectRoot, nowMs, staleDays) {
558
+ const reportPath = nodePath.join(projectRoot, '.sharkcraft', 'impact', 'last.json');
559
+ const baselinePath = nodePath.join(projectRoot, '.sharkcraft', 'impact', 'baseline.json');
560
+ const baselineChecks = impactBaselineCheck(projectRoot, reportPath, baselinePath, nowMs);
561
+ if (!existsSync(reportPath))
562
+ return baselineChecks;
563
+ const report = readJsonFile(reportPath);
564
+ if (!report) {
565
+ return [
566
+ {
567
+ id: 'code-intelligence-impact',
568
+ title: 'Code-intelligence impact (last run)',
569
+ severity: DoctorSeverity.Info,
570
+ category: CATEGORY,
571
+ message: '.sharkcraft/impact/last.json exists but is not valid JSON. Next `shrk impact --via-graph` will overwrite it.',
572
+ },
573
+ ];
574
+ }
575
+ const days = ageDays(report.generatedAt, nowMs);
576
+ const ageStr = days !== undefined ? ` ${fmtAge(days)}` : '';
577
+ const stale = days !== undefined && days > staleDays;
578
+ const risk = report.risk ?? 'low';
579
+ const direct = report.directDependentCount ?? 0;
580
+ const transitive = report.transitiveDependentCount ?? 0;
581
+ const pkgs = report.affectedPackageCount ?? 0;
582
+ const tests = report.likelyTestCount ?? 0;
583
+ const input = report.inputSummary ?? '(unknown input)';
584
+ // High-risk + recent → real warning. low/medium → OK with summary.
585
+ // Anything stale → advisory, since the impact is from an old code
586
+ // state and may have decayed.
587
+ if (risk === 'high' || risk === 'critical') {
588
+ return [
589
+ {
590
+ id: 'code-intelligence-impact',
591
+ title: 'Code-intelligence impact (last run)',
592
+ severity: DoctorSeverity.Warning,
593
+ ...(stale ? { advisory: true } : {}),
594
+ category: CATEGORY,
595
+ message: `Last impact (${risk}) on ${input}${ageStr}: ${direct} direct + ${transitive} transitive across ${pkgs} package(s), ${tests} test(s) recommended` +
596
+ (report.publicApiTouched ? '. Public API touched.' : '.'),
597
+ fix: 'Re-run `shrk impact --via-graph` if stale, or follow the `validationScope` commands from the v3 report.',
598
+ whyThisMatters: 'High-risk impact analyses are the load-bearing signal for `shrk gate` and PR review. A stale or never-acknowledged high-risk run usually means tests + reviews are missing.',
599
+ },
600
+ ...baselineChecks,
601
+ ];
602
+ }
603
+ return [
604
+ {
605
+ id: 'code-intelligence-impact',
606
+ title: 'Code-intelligence impact (last run)',
607
+ severity: stale ? DoctorSeverity.Info : DoctorSeverity.Ok,
608
+ ...(stale ? { advisory: true } : {}),
609
+ category: CATEGORY,
610
+ message: `Last impact (${risk}) on ${input}${ageStr}: ${direct} direct + ${transitive} transitive, ${pkgs} package(s), ${tests} test(s)` +
611
+ (report.publicApiTouched ? '. Public API touched.' : '.'),
612
+ },
613
+ ...baselineChecks,
614
+ ];
615
+ }
616
+ function impactBaselineCheck(projectRoot, reportPath, baselinePath, nowMs) {
617
+ const hasLast = existsSync(reportPath);
618
+ const hasBaseline = existsSync(baselinePath);
619
+ if (!hasBaseline)
620
+ return [];
621
+ if (!hasLast) {
622
+ return [
623
+ {
624
+ id: 'code-intelligence-impact-baseline',
625
+ title: 'Code-intelligence impact baseline',
626
+ severity: DoctorSeverity.Info,
627
+ category: CATEGORY,
628
+ message: 'Impact baseline present, but no recent `last.json` to compare against.',
629
+ fix: 'Run `shrk impact --via-graph <target>` to refresh `last.json`.',
630
+ },
631
+ ];
632
+ }
633
+ const baseline = readJsonFile(baselinePath);
634
+ const last = readJsonFile(reportPath);
635
+ if (!baseline || !last) {
636
+ return [
637
+ {
638
+ id: 'code-intelligence-impact-baseline',
639
+ title: 'Code-intelligence impact baseline',
640
+ severity: DoctorSeverity.Warning,
641
+ category: CATEGORY,
642
+ message: '.sharkcraft/impact/{baseline,last}.json could not be read.',
643
+ fix: 'Re-freeze with `shrk impact --via-graph <target> && shrk impact baseline write`.',
644
+ },
645
+ ];
646
+ }
647
+ const baseDeps = (baseline.directDependentCount ?? 0) + (baseline.transitiveDependentCount ?? 0);
648
+ const lastDeps = (last.directDependentCount ?? 0) + (last.transitiveDependentCount ?? 0);
649
+ const baseRisk = riskRankLike(baseline.risk);
650
+ const lastRisk = riskRankLike(last.risk);
651
+ const riskWorsened = lastRisk > baseRisk;
652
+ const depDelta = lastDeps - baseDeps;
653
+ const pkgDelta = (last.affectedPackageCount ?? 0) - (baseline.affectedPackageCount ?? 0);
654
+ const worsened = riskWorsened || depDelta > 0 || pkgDelta > 0;
655
+ void nowMs;
656
+ if (!worsened) {
657
+ return [
658
+ {
659
+ id: 'code-intelligence-impact-baseline',
660
+ title: 'Code-intelligence impact baseline',
661
+ severity: DoctorSeverity.Ok,
662
+ category: CATEGORY,
663
+ message: `Impact within baseline — dependents ${depDelta >= 0 ? '+' : ''}${depDelta}, ` +
664
+ `packages ${pkgDelta >= 0 ? '+' : ''}${pkgDelta}.`,
665
+ },
666
+ ];
667
+ }
668
+ const riskStr = baseline.risk !== last.risk
669
+ ? `, risk ${baseline.risk} → ${last.risk}`
670
+ : '';
671
+ return [
672
+ {
673
+ id: 'code-intelligence-impact-baseline',
674
+ title: 'Code-intelligence impact baseline',
675
+ severity: DoctorSeverity.Warning,
676
+ category: CATEGORY,
677
+ message: `Impact worsened since baseline: ` +
678
+ `dependents ${depDelta >= 0 ? '+' : ''}${depDelta}, ` +
679
+ `packages ${pkgDelta >= 0 ? '+' : ''}${pkgDelta}${riskStr}.`,
680
+ fix: 'Investigate the new dependents. If the growth is intentional, re-freeze with `shrk impact baseline write`.',
681
+ whyThisMatters: 'A growing impact baseline means edits in this area are increasingly load-bearing — tests + reviews need to scale with it.',
682
+ },
683
+ ];
684
+ }
685
+ function riskRankLike(r) {
686
+ switch (r) {
687
+ case 'low':
688
+ return 0;
689
+ case 'medium':
690
+ return 1;
691
+ case 'high':
692
+ return 2;
693
+ case 'critical':
694
+ return 3;
695
+ default:
696
+ return 0;
697
+ }
698
+ }
699
+ function frameworkChecks(projectRoot, nowMs, staleDays) {
700
+ const metaPath = nodePath.join(projectRoot, '.sharkcraft', 'framework', 'meta.json');
701
+ if (!existsSync(metaPath))
702
+ return [];
703
+ const manifest = readJsonFile(metaPath);
704
+ if (!manifest) {
705
+ return [
706
+ {
707
+ id: 'code-intelligence-framework',
708
+ title: 'Code-intelligence framework scan',
709
+ severity: DoctorSeverity.Warning,
710
+ category: CATEGORY,
711
+ message: '.sharkcraft/framework/meta.json exists but is not valid JSON.',
712
+ fix: 'Rebuild with `shrk graph framework <name>` (or re-run the framework extractor pipeline).',
713
+ },
714
+ ];
715
+ }
716
+ const frameworks = manifest.frameworks ?? [];
717
+ const counts = manifest.countsByFramework ?? {};
718
+ const total = sumValues(counts);
719
+ const days = ageDays(manifest.lastBuiltAt, nowMs);
720
+ const stale = days !== undefined && days > staleDays;
721
+ const ageStr = days !== undefined ? ` (${fmtAge(days)})` : '';
722
+ // Render the per-framework breakdown for the message line. Sorted by
723
+ // count desc so the most-populated framework leads.
724
+ const breakdown = Object.entries(counts)
725
+ .filter(([, n]) => typeof n === 'number' && n > 0)
726
+ .sort((a, b) => (b[1] ?? 0) - (a[1] ?? 0))
727
+ .map(([k, n]) => `${k}=${n}`)
728
+ .slice(0, 6);
729
+ const breakdownStr = breakdown.length > 0 ? ` [${breakdown.join(', ')}]` : '';
730
+ if (total === 0) {
731
+ return [
732
+ {
733
+ id: 'code-intelligence-framework',
734
+ title: 'Code-intelligence framework scan',
735
+ severity: DoctorSeverity.Info,
736
+ advisory: true,
737
+ category: CATEGORY,
738
+ message: `Framework scan ran but found no framework entities${ageStr}.`,
739
+ fix: 'Check that the scanned files actually contain framework markers (decorators, JSX components, etc.).',
740
+ },
741
+ ];
742
+ }
743
+ if (stale) {
744
+ return [
745
+ {
746
+ id: 'code-intelligence-framework',
747
+ title: 'Code-intelligence framework scan',
748
+ severity: DoctorSeverity.Warning,
749
+ advisory: true,
750
+ category: CATEGORY,
751
+ message: `Framework scan stale${ageStr} — ${total} entities across ${frameworks.length} framework(s)${breakdownStr}.`,
752
+ fix: 'Re-run `shrk graph index` (framework scan rebuilds alongside).',
753
+ },
754
+ ];
755
+ }
756
+ return [
757
+ {
758
+ id: 'code-intelligence-framework',
759
+ title: 'Code-intelligence framework scan',
760
+ severity: DoctorSeverity.Ok,
761
+ category: CATEGORY,
762
+ message: `${total} framework entities across ${frameworks.length} framework(s)${ageStr}${breakdownStr}.`,
763
+ },
764
+ ];
765
+ }
766
+ function structuralRegistryChecks(projectRoot) {
767
+ const registryPath = nodePath.join(projectRoot, '.sharkcraft', 'structural', 'patterns.json');
768
+ if (!existsSync(registryPath))
769
+ return [];
770
+ const reg = readJsonFile(registryPath);
771
+ if (!reg) {
772
+ return [
773
+ {
774
+ id: 'code-intelligence-structural-search',
775
+ title: 'Code-intelligence structural patterns',
776
+ severity: DoctorSeverity.Warning,
777
+ category: CATEGORY,
778
+ message: '.sharkcraft/structural/patterns.json exists but is not valid JSON.',
779
+ fix: 'Inspect with `shrk search-structural registry list` then re-`add` the affected entries.',
780
+ },
781
+ ];
782
+ }
783
+ const patterns = reg.patterns ?? [];
784
+ if (patterns.length === 0) {
785
+ return [
786
+ {
787
+ id: 'code-intelligence-structural-search',
788
+ title: 'Code-intelligence structural patterns',
789
+ severity: DoctorSeverity.Info,
790
+ advisory: true,
791
+ category: CATEGORY,
792
+ message: 'Pattern registry exists but is empty.',
793
+ fix: 'Register patterns via `shrk search-structural registry add --id <id> --pattern <json>`.',
794
+ },
795
+ ];
796
+ }
797
+ const broken = patterns.filter((p) => typeof p.lastValidationError === 'string');
798
+ if (broken.length > 0) {
799
+ const head = broken
800
+ .slice(0, 3)
801
+ .map((p) => `${p.id ?? '?'} (${p.lastValidationError ?? 'invalid'})`)
802
+ .join('; ');
803
+ const tail = broken.length > 3 ? '…' : '';
804
+ return [
805
+ {
806
+ id: 'code-intelligence-structural-search',
807
+ title: 'Code-intelligence structural patterns',
808
+ severity: DoctorSeverity.Warning,
809
+ category: CATEGORY,
810
+ message: `${broken.length}/${patterns.length} registered pattern(s) failed validation: ${head}${tail}`,
811
+ fix: 'Re-validate with `shrk search-structural registry validate`, then re-`add` each failing pattern with a corrected envelope.',
812
+ whyThisMatters: 'Invalid registry entries never match anything at runtime, so the agent silently misses cases the pattern was meant to flag.',
813
+ },
814
+ ];
815
+ }
816
+ // Roll-up of "needs validation" — entries with no lastValidatedAt at all.
817
+ const unvalidated = patterns.filter((p) => !p.lastValidatedAt);
818
+ if (unvalidated.length > 0) {
819
+ return [
820
+ {
821
+ id: 'code-intelligence-structural-search',
822
+ title: 'Code-intelligence structural patterns',
823
+ severity: DoctorSeverity.Info,
824
+ advisory: true,
825
+ category: CATEGORY,
826
+ message: `${patterns.length} pattern(s) registered, ${unvalidated.length} never validated.`,
827
+ fix: 'Run `shrk search-structural registry validate`.',
828
+ },
829
+ ];
830
+ }
831
+ return [
832
+ {
833
+ id: 'code-intelligence-structural-search',
834
+ title: 'Code-intelligence structural patterns',
835
+ severity: DoctorSeverity.Ok,
836
+ category: CATEGORY,
837
+ message: `${patterns.length} valid pattern(s) registered.`,
838
+ },
839
+ ];
840
+ }
841
+ function contextPlannerChecks(projectRoot, nowMs, staleDays) {
842
+ // Authoring fixture lives under `sharkcraft/` (checked in, not derived);
843
+ // the run report lives under `.sharkcraft/context-planner/...`.
844
+ const fixturePath = nodePath.join(projectRoot, 'sharkcraft', 'intent-benchmark.json');
845
+ const runPath = nodePath.join(projectRoot, '.sharkcraft', 'context-planner', 'intent-benchmark.json');
846
+ const hasFixture = existsSync(fixturePath);
847
+ const hasRun = existsSync(runPath);
848
+ if (!hasFixture && !hasRun)
849
+ return [];
850
+ if (hasFixture && !hasRun) {
851
+ return [
852
+ {
853
+ id: 'code-intelligence-context-planner',
854
+ title: 'Code-intelligence intent classifier',
855
+ severity: DoctorSeverity.Info,
856
+ category: CATEGORY,
857
+ message: 'Intent benchmark fixture present but never run. Doctor cannot report accuracy until you run it.',
858
+ fix: 'Run `shrk context benchmark` once to record accuracy.',
859
+ },
860
+ ];
861
+ }
862
+ const run = hasRun ? readJsonFile(runPath) : undefined;
863
+ if (!run) {
864
+ return [
865
+ {
866
+ id: 'code-intelligence-context-planner',
867
+ title: 'Code-intelligence intent classifier',
868
+ severity: DoctorSeverity.Warning,
869
+ category: CATEGORY,
870
+ message: '.sharkcraft/context-planner/intent-benchmark.json exists but is not valid JSON.',
871
+ fix: 'Re-run `shrk context benchmark` to overwrite the report.',
872
+ },
873
+ ];
874
+ }
875
+ const days = ageDays(run.ranAt, nowMs);
876
+ const ageStr = days !== undefined ? ` (${fmtAge(days)})` : '';
877
+ const stale = days !== undefined && days > staleDays;
878
+ const total = run.total ?? 0;
879
+ const passed = run.passed ?? 0;
880
+ const failed = run.failed ?? Math.max(0, total - passed);
881
+ const accuracy = total === 0 ? 1 : (run.accuracy ?? passed / total);
882
+ const pct = Math.round(accuracy * 1000) / 10;
883
+ if (total === 0) {
884
+ return [
885
+ {
886
+ id: 'code-intelligence-context-planner',
887
+ title: 'Code-intelligence intent classifier',
888
+ severity: DoctorSeverity.Info,
889
+ advisory: true,
890
+ category: CATEGORY,
891
+ message: 'Intent benchmark ran with zero cases.',
892
+ fix: 'Add labelled cases to sharkcraft/intent-benchmark.json.',
893
+ },
894
+ ];
895
+ }
896
+ if (failed > 0) {
897
+ const sample = (run.cases ?? [])
898
+ .filter((c) => c?.passed === false)
899
+ .slice(0, 3)
900
+ .map((c) => `expected=${c?.expected} actual=${c?.actual}`)
901
+ .join('; ');
902
+ return [
903
+ {
904
+ id: 'code-intelligence-context-planner',
905
+ title: 'Code-intelligence intent classifier',
906
+ severity: DoctorSeverity.Warning,
907
+ ...(pct >= 80 ? { advisory: true } : {}),
908
+ category: CATEGORY,
909
+ message: `Intent classifier accuracy ${pct}% (${passed}/${total})${ageStr}. ${failed} miss(es): ${sample || '(see report)'}.`,
910
+ fix: 'Inspect with `shrk context benchmark` and add a keyword to `classifyIntent` if the regression is real.',
911
+ whyThisMatters: 'The classifier drives ranker weights in `shrk context`; wrong intent → wrong files surfaced first → wasted agent turns.',
912
+ },
913
+ ];
914
+ }
915
+ return [
916
+ {
917
+ id: 'code-intelligence-context-planner',
918
+ title: 'Code-intelligence intent classifier',
919
+ severity: stale ? DoctorSeverity.Info : DoctorSeverity.Ok,
920
+ ...(stale ? { advisory: true } : {}),
921
+ category: CATEGORY,
922
+ message: `Intent classifier accuracy ${pct}% (${passed}/${total})${ageStr}.`,
923
+ ...(stale ? { fix: 'Re-run `shrk context benchmark`.' } : {}),
924
+ },
925
+ ];
926
+ }
927
+ function schemaCompatChecks(projectRoot) {
928
+ const mismatches = [];
929
+ // `migrations` is a directory of files; check every state file
930
+ // individually so a single bad write doesn't poison the whole list.
931
+ const migDir = nodePath.join(projectRoot, '.sharkcraft', 'migrations');
932
+ if (existsSync(migDir)) {
933
+ let entries = [];
934
+ try {
935
+ entries = readdirSync(migDir).filter((f) => f.endsWith('.state.json'));
936
+ }
937
+ catch {
938
+ entries = [];
939
+ }
940
+ for (const entry of entries) {
941
+ const data = readJsonFile(nodePath.join(migDir, entry));
942
+ if (!data?.schema)
943
+ continue;
944
+ if (data.schema !== 'sharkcraft.migration-run/v1') {
945
+ mismatches.push({
946
+ rel: `migrations/${entry}`,
947
+ expected: 'sharkcraft.migration-run/v1',
948
+ actual: data.schema,
949
+ package: '@shrkcrft/migrate',
950
+ });
951
+ }
952
+ }
953
+ }
954
+ for (const target of EXPECTED_SCHEMAS) {
955
+ const abs = nodePath.join(projectRoot, '.sharkcraft', target.rel);
956
+ if (!existsSync(abs))
957
+ continue;
958
+ const data = readJsonFile(abs);
959
+ if (!data?.schema)
960
+ continue;
961
+ if (data.schema !== target.expected) {
962
+ mismatches.push({ ...target, actual: data.schema });
963
+ }
964
+ }
965
+ if (mismatches.length === 0)
966
+ return [];
967
+ const head = mismatches
968
+ .slice(0, 3)
969
+ .map((m) => `${m.rel} (${m.actual} ≠ ${m.expected})`)
970
+ .join('; ');
971
+ const tail = mismatches.length > 3 ? '…' : '';
972
+ return [
973
+ {
974
+ id: 'code-intelligence-schema-mismatch',
975
+ title: 'Code-intelligence schema compatibility',
976
+ severity: DoctorSeverity.Warning,
977
+ category: CATEGORY,
978
+ message: `${mismatches.length} stored file(s) using outdated schemas: ${head}${tail}`,
979
+ fix: mismatches.length === 1
980
+ ? `Regenerate by running the writing package's CLI again (e.g. \`shrk graph index\`, \`shrk gate\`, \`shrk arch check\`).`
981
+ : 'Regenerate the affected stores by re-running each owning CLI (shrk graph index / gate / arch check / api-diff / impact / migrate).',
982
+ whyThisMatters: 'Schema drift between stored state and the loading package produces empty reads (the loader returns undefined on mismatch). Doctor surfaces the drift so the user knows why a downstream surface suddenly looks blank.',
983
+ },
984
+ ];
985
+ }