@canonical/pragma-cli 0.35.0 → 0.37.0

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 (295) hide show
  1. package/README.md +2 -2
  2. package/bundled-skills/add-standard/SKILL.md +338 -0
  3. package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
  4. package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
  5. package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
  6. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
  7. package/bundled-skills/anatomy-author/SKILL.md +1226 -0
  8. package/bundled-skills/design-auditor/SKILL.md +455 -0
  9. package/bundled-skills/implement-component/SKILL.md +333 -0
  10. package/bundled-skills/specify-component/SKILL.md +331 -0
  11. package/bundled-skills/specify-pattern/SKILL.md +292 -0
  12. package/dist/package.json +24 -20
  13. package/dist/pragma.conf.js +266 -34
  14. package/dist/pragma.conf.js.map +1 -1
  15. package/dist/src/bin.js +1 -1
  16. package/dist/src/bin.js.map +1 -1
  17. package/dist/src/capabilities/capabilities/hints.js +1 -1
  18. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  19. package/dist/src/capabilities/colophon/colophon.render.js +1 -1
  20. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  21. package/dist/src/capabilities/create/create.verb.js +2 -1
  22. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  23. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  24. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  25. package/dist/src/capabilities/create/mount.js +4 -3
  26. package/dist/src/capabilities/create/mount.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  28. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  29. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  30. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  32. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  33. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  34. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  35. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  36. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  37. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  38. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  39. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  40. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  41. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  42. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  43. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  44. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  45. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  46. package/dist/src/capabilities/graph/query.render.js +2 -2
  47. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  48. package/dist/src/capabilities/info/collectInfo.js +8 -2
  49. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  50. package/dist/src/capabilities/info/info.render.js +6 -2
  51. package/dist/src/capabilities/info/info.render.js.map +1 -1
  52. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  53. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  54. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  55. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  56. package/dist/src/capabilities/setup/mode.js +2 -2
  57. package/dist/src/capabilities/setup/operations/index.js +4 -4
  58. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  60. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
  64. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  65. package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
  66. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  67. package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
  68. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  69. package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
  70. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  71. package/dist/src/capabilities/setup/plan.js.map +1 -1
  72. package/dist/src/capabilities/setup/plan.render.js +144 -66
  73. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  74. package/dist/src/capabilities/setup/setup.render.js +1 -1
  75. package/dist/src/capabilities/setup/setup.verb.js +165 -63
  76. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  77. package/dist/src/capabilities/setup/targets.js +122 -51
  78. package/dist/src/capabilities/setup/targets.js.map +1 -1
  79. package/dist/src/capabilities/shared/index.js +7 -6
  80. package/dist/src/capabilities/shared/index.js.map +1 -1
  81. package/dist/src/capabilities/shared/packageManager.js +300 -54
  82. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  83. package/dist/src/capabilities/skill/discover.js +97 -11
  84. package/dist/src/capabilities/skill/discover.js.map +1 -1
  85. package/dist/src/capabilities/skill/render.js +2 -2
  86. package/dist/src/capabilities/skill/render.js.map +1 -1
  87. package/dist/src/capabilities/sources/installSkills.js +145 -3
  88. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  89. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  90. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  91. package/dist/src/capabilities/sources/update.verb.js +3 -3
  92. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  93. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  94. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  95. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  96. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  97. package/dist/src/constants.js +13 -2
  98. package/dist/src/constants.js.map +1 -1
  99. package/dist/src/kernel/index.js +11 -2
  100. package/dist/src/kernel/index.js.map +1 -1
  101. package/dist/src/kernel/interactivity.js +68 -0
  102. package/dist/src/kernel/interactivity.js.map +1 -0
  103. package/dist/src/kernel/packs/renderPack.js +3 -3
  104. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  105. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  106. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  107. package/dist/src/kernel/packs/runBodies.js +57 -4
  108. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  109. package/dist/src/kernel/packs/schema.js +54 -2
  110. package/dist/src/kernel/packs/schema.js.map +1 -1
  111. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  112. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
  114. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/types.js.map +1 -1
  116. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  117. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  118. package/dist/src/kernel/project/cli/dispatch.js +46 -24
  119. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  120. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  121. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  122. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  123. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  124. package/dist/src/kernel/project/cli/rootHelp.js +42 -9
  125. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  126. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  127. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  128. package/dist/src/kernel/render/renderers.js +1 -1
  129. package/dist/src/kernel/render/style.js +29 -9
  130. package/dist/src/kernel/render/style.js.map +1 -1
  131. package/dist/src/kernel/render/vocabulary.js +174 -0
  132. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  133. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  134. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  135. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  136. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  137. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  138. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  139. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  140. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  141. package/dist/src/kernel/runtime/graphpack/read.js +14 -9
  142. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  143. package/dist/src/kernel/runtime/loadSession.js +3 -1
  144. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  145. package/dist/src/kernel/runtime/probe.js +1 -1
  146. package/dist/src/kernel/runtime/probe.js.map +1 -1
  147. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  148. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  149. package/dist/src/kernel/runtime/refs/resolve.js +9 -3
  150. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  151. package/dist/src/kernel/spec/emitReference.js +1 -1
  152. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  153. package/package.json +24 -20
  154. package/pragma.conf.ts +272 -34
  155. package/src/bin.ts +8 -1
  156. package/src/capabilities/block.parity.test.ts +25 -11
  157. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  158. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  159. package/src/capabilities/capabilities/hints.ts +1 -1
  160. package/src/capabilities/colophon/colophon.render.ts +1 -2
  161. package/src/capabilities/colophon/colophon.test.ts +2 -2
  162. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  163. package/src/capabilities/create/byteEquality.test.ts +28 -1
  164. package/src/capabilities/create/create.test.ts +1 -0
  165. package/src/capabilities/create/create.verb.ts +2 -1
  166. package/src/capabilities/create/createGrammar.test.ts +5 -2
  167. package/src/capabilities/create/createSurface.generated.ts +23 -1
  168. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  169. package/src/capabilities/create/mount.ts +3 -3
  170. package/src/capabilities/distribution.parity.test.ts +4 -0
  171. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  172. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  173. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  174. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  175. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  176. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  177. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  178. package/src/capabilities/doctor/checks/index.ts +6 -6
  179. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  180. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  181. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  182. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  183. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  184. package/src/capabilities/doctor/doctor.render.ts +56 -80
  185. package/src/capabilities/doctor/doctor.test.ts +247 -35
  186. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  187. package/src/capabilities/doctor/runChecks.ts +14 -11
  188. package/src/capabilities/doctor/types.ts +15 -24
  189. package/src/capabilities/emptyRecovery.test.ts +10 -2
  190. package/src/capabilities/graph/query.render.ts +2 -2
  191. package/src/capabilities/info/collectInfo.ts +8 -1
  192. package/src/capabilities/info/info.render.ts +6 -2
  193. package/src/capabilities/info/info.test.ts +37 -1
  194. package/src/capabilities/info/types.ts +10 -2
  195. package/src/capabilities/lazy.test.ts +26 -2
  196. package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
  197. package/src/capabilities/prompt/prompt.render.ts +2 -2
  198. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  199. package/src/capabilities/resources/resources.test.ts +38 -2
  200. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  201. package/src/capabilities/setup/buildPlan.ts +45 -41
  202. package/src/capabilities/setup/index.ts +1 -1
  203. package/src/capabilities/setup/mode.ts +2 -2
  204. package/src/capabilities/setup/operations/index.ts +6 -1
  205. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  206. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  207. package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
  208. package/src/capabilities/setup/operations/setupLsp.ts +203 -38
  209. package/src/capabilities/setup/operations/setupMcp.ts +139 -62
  210. package/src/capabilities/setup/operations/setupSkills.ts +399 -60
  211. package/src/capabilities/setup/plan.render.ts +171 -76
  212. package/src/capabilities/setup/plan.ts +17 -12
  213. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  214. package/src/capabilities/setup/setup.render.test.ts +50 -36
  215. package/src/capabilities/setup/setup.render.ts +1 -1
  216. package/src/capabilities/setup/setup.test.ts +1126 -50
  217. package/src/capabilities/setup/setup.verb.ts +174 -68
  218. package/src/capabilities/setup/targets.ts +138 -60
  219. package/src/capabilities/setup/types.ts +10 -18
  220. package/src/capabilities/shared/index.ts +18 -6
  221. package/src/capabilities/shared/packageManager.test.ts +421 -6
  222. package/src/capabilities/shared/packageManager.ts +394 -59
  223. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  224. package/src/capabilities/skill/discover.ts +100 -11
  225. package/src/capabilities/skill/render.ts +2 -2
  226. package/src/capabilities/skill/skill.test.ts +17 -4
  227. package/src/capabilities/sources/installSkills.ts +181 -3
  228. package/src/capabilities/sources/runUpdate.ts +78 -2
  229. package/src/capabilities/sources/sources.test.ts +228 -0
  230. package/src/capabilities/sources/update.verb.ts +3 -3
  231. package/src/capabilities/surface.test.ts +14 -6
  232. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  233. package/src/capabilities/upgrade/types.ts +10 -2
  234. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  235. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  236. package/src/constants.ts +14 -1
  237. package/src/identity.test.ts +18 -5
  238. package/src/kernel/completion/entitySource.test.ts +15 -2
  239. package/src/kernel/completion/safety.test.ts +16 -9
  240. package/src/kernel/config/defaults.test.ts +19 -3
  241. package/src/kernel/config/readConfig.test.ts +1 -1
  242. package/src/kernel/index.ts +11 -2
  243. package/src/kernel/interactivity.test.ts +76 -0
  244. package/src/kernel/interactivity.ts +69 -0
  245. package/src/kernel/packs/compile.test.ts +92 -0
  246. package/src/kernel/packs/renderPack.ts +3 -3
  247. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  248. package/src/kernel/packs/resolveEntity.ts +88 -24
  249. package/src/kernel/packs/runBodies.ts +84 -19
  250. package/src/kernel/packs/schema.ts +60 -4
  251. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  252. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  253. package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
  254. package/src/kernel/packs/toolDescription.test.ts +35 -1
  255. package/src/kernel/packs/types.ts +125 -2
  256. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  257. package/src/kernel/project/cli/buildProgram.ts +8 -1
  258. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  259. package/src/kernel/project/cli/dispatch.ts +48 -25
  260. package/src/kernel/project/cli/globalFlags.ts +4 -1
  261. package/src/kernel/project/cli/helpFormat.ts +1 -2
  262. package/src/kernel/project/cli/rootHelp.ts +44 -8
  263. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.ts +1 -1
  266. package/src/kernel/render/style.ts +32 -9
  267. package/src/kernel/render/vocabulary.ts +276 -0
  268. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  269. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  270. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  271. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  272. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  273. package/src/kernel/runtime/graphpack/read.ts +14 -12
  274. package/src/kernel/runtime/loadSession.ts +3 -1
  275. package/src/kernel/runtime/probe.ts +1 -1
  276. package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
  277. package/src/kernel/runtime/refs/parseRef.ts +25 -1
  278. package/src/kernel/runtime/refs/resolve.ts +9 -3
  279. package/src/kernel/runtime/types.ts +17 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/types.ts +35 -6
  282. package/src/testing/PARITY_GAPS.ts +9 -3
  283. package/src/testing/behavioral/docExamples.test.ts +4 -2
  284. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  285. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  286. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  287. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  288. package/src/testing/eval/cases/readNouns.ts +245 -9
  289. package/src/testing/fixtures/blockGraph.ts +28 -13
  290. package/src/testing/fixtures/graph/canonical.ts +95 -12
  291. package/src/testing/helpers/runCli.test.ts +44 -0
  292. package/src/testing/helpers/runCli.ts +27 -3
  293. package/dist/src/capabilities/shared/bands.js +0 -16
  294. package/dist/src/capabilities/shared/bands.js.map +0 -1
  295. package/src/capabilities/shared/bands.ts +0 -16
@@ -0,0 +1,298 @@
1
+ /**
2
+ * The harness inventory: one row per harness per scope, saying what was detected
3
+ * and whether pragma is registered in it.
4
+ *
5
+ * Every fact this needs was ALREADY COMPUTED and thrown away.
6
+ * `TargetGroup.harnessNames` carries every harness sharing a config file and
7
+ * both consumers reduced it to `shortenPath(group.path)`; `McpDetection`
8
+ * carries the `absent`/`registered`/`drifted` classification of every WRITE in
9
+ * that file. The only thing detection could NOT answer is the negative —
10
+ * `detectHarnesses` filters to hits, so a harness the machine does not have was
11
+ * unrepresentable — which is why the roll-up takes the REGISTRY alongside the
12
+ * groups: the registry supplies the universe, the groups supply the state.
13
+ *
14
+ * The state arrives PER HARNESS, never per file. A shared `.vscode/mcp.json`
15
+ * holds VS Code's `servers` and Cline's `mcpServers` as two independent
16
+ * entries, so the file's aggregate — `drifted`, when one key is current and the
17
+ * other absent — is the standing of NEITHER harness. Pairing each name with
18
+ * the write it owns is the caller's job (it holds the registry's `mcpKey`s and
19
+ * the group's writes); by the time a group reaches this module the association
20
+ * is resolved and every name carries its own state.
21
+ *
22
+ * Three deliberate constraints:
23
+ *
24
+ * 1. **No `@canonical/harnesses` import, of any kind.** `capabilities/lazy.test`
25
+ * forbids one from any file statically reachable from `capabilities/index`,
26
+ * and its regex has no `import type` exemption. This module is reached only
27
+ * through `runChecks`'s dynamic import today, but it is written so that
28
+ * staying legal never depends on that: the inputs are declared here as
29
+ * narrow structural records.
30
+ *
31
+ * 2. **Narrowed to DATA, so `mutates: false` is enforced rather than asserted.**
32
+ * `McpDetection` is write-capable — it carries `writeMcpConfigTargets` and
33
+ * `removeMcpConfigFrom` as live function references. Projecting it down to
34
+ * {@link InventoryGroup} before it reaches this file means a read-only
35
+ * report literally cannot reach a writer.
36
+ *
37
+ * 3. **It never invents a failure.** The states an inventory can report map onto
38
+ * `pass`/`available`/`skip` and NEVER `fail` (see `../types.ts`): a machine
39
+ * that simply does not have Windsurf is not a broken machine, and the `mcp`
40
+ * row already owns the actionable finding for the harnesses it does have.
41
+ * Reporting it twice is how a failure count becomes noise.
42
+ *
43
+ * Placement: this is a doctor helper because doctor is its only consumer today,
44
+ * and `shared/` is explicitly not a utility drawer (see `shared/index.ts`). It
45
+ * is written to MOVE there unchanged — pure, no capability imports beyond the
46
+ * scope type — the moment `setup`'s detection summary becomes the second caller.
47
+ */
48
+
49
+ import type { CheckItem, CheckStatus, Scope } from "../types.js";
50
+
51
+ /**
52
+ * What the inventory says about one harness in one scope.
53
+ *
54
+ * The vocabulary is deliberately NOT "installed". Two different claims used
55
+ * to share that word — `DetectedHarness.configExists` ("the harness's own
56
+ * config file exists", a harnesses-package field this module never surfaces)
57
+ * and the pragma-entry state (`McpTargetState`, which now says `registered`
58
+ * for exactly this reason). Every state here names the PRAGMA-ENTRY meaning.
59
+ * One name per claim, held across the reports: "installed" is reserved for
60
+ * artifacts the CLI itself places (the completion script, the LSP extension),
61
+ * "registered" for pragma's entry in someone else's config, "linked" for
62
+ * skill symlinks.
63
+ *
64
+ * - `registered` — detected, and pragma's MCP entry in this scope is current.
65
+ * - `drifted` — detected, and a pragma entry exists but differs from what a
66
+ * write would emit. Actionable, but the `mcp` row is where it is actionED;
67
+ * here it is reported without inflating the failure count.
68
+ * - `detected` — the harness is on this machine; pragma is not registered.
69
+ * - `undetected` — in the registry, has a location in this scope, not found.
70
+ * - `unscoped` — in the registry, but has NO config location in this scope at
71
+ * all (VS Code is `scope: "project"`, so it can never hold a global entry).
72
+ * Stated rather than omitted: a verbose listing that silently dropped a row
73
+ * reads as a bug in the listing.
74
+ */
75
+ export type InventoryState =
76
+ | "registered"
77
+ | "drifted"
78
+ | "detected"
79
+ | "undetected"
80
+ | "unscoped";
81
+
82
+ /** One harness's standing in one scope. */
83
+ export interface InventoryRow {
84
+ readonly harnessId: string;
85
+ readonly harnessName: string;
86
+ readonly scope: Scope;
87
+ readonly state: InventoryState;
88
+ /** The config paths in this scope that carry (or would carry) the entry. */
89
+ readonly locations: readonly string[];
90
+ }
91
+
92
+ /** A registry entry, narrowed to what the roll-up reads. */
93
+ export interface InventoryHarness {
94
+ readonly id: string;
95
+ readonly name: string;
96
+ /** Whether this harness has a config location in the scope being rolled up. */
97
+ readonly inScope: boolean;
98
+ }
99
+
100
+ /** The prior state of one harness's own write, as detection classified it. */
101
+ export type WriteState = "absent" | "registered" | "drifted";
102
+
103
+ /** One harness's share of a config file: the state of the write IT owns. */
104
+ export interface InventoryMember {
105
+ readonly name: string;
106
+ readonly state: WriteState;
107
+ }
108
+
109
+ /**
110
+ * One detected config file, narrowed off {@link McpDetection}: the group's
111
+ * already-shortened path, and every harness sharing it WITH ITS OWN state.
112
+ *
113
+ * Per harness, not per file — see the module docblock. A file-level state here
114
+ * would report both harnesses sharing `.vscode/mcp.json` as drifted the moment
115
+ * either one of them is registered alone.
116
+ */
117
+ export interface InventoryGroup {
118
+ readonly path: string;
119
+ readonly harnesses: readonly InventoryMember[];
120
+ }
121
+
122
+ /** Whether a state means "this harness is on the machine". */
123
+ export const isDetectedState = (state: InventoryState): boolean =>
124
+ state === "registered" || state === "drifted" || state === "detected";
125
+
126
+ /**
127
+ * Roll one scope's detected config groups up against the registry into one row
128
+ * per harness.
129
+ *
130
+ * Groups are keyed by harness NAME because that is the only identity
131
+ * `TargetGroup` records; the registry is the map back to an id, which is why it
132
+ * is a parameter rather than a lookup. A harness naming no registry row is
133
+ * ignored rather than invented — the inventory reports on the registry, not on
134
+ * whatever a group happened to say.
135
+ *
136
+ * @param registry - Every known harness, each flagged for scope membership.
137
+ * @param groups - This scope's detected config files (narrowed, path-shortened),
138
+ * each naming the harnesses that share it with their own per-write state.
139
+ * @param scope - The scope being rolled up.
140
+ * @returns One row per registry harness, in registry order.
141
+ * @note Pure — it reads no filesystem and holds no live effect.
142
+ */
143
+ export function harnessInventory(
144
+ registry: readonly InventoryHarness[],
145
+ groups: readonly InventoryGroup[],
146
+ scope: Scope,
147
+ ): InventoryRow[] {
148
+ const states = new Map<string, WriteState>();
149
+ const locations = new Map<string, string[]>();
150
+ for (const group of groups) {
151
+ for (const member of group.harnesses) {
152
+ const paths = locations.get(member.name) ?? [];
153
+ paths.push(group.path);
154
+ locations.set(member.name, paths);
155
+ // A harness sharing two files is registered only where EVERY one of them
156
+ // carries the current entry — the same "all or nothing" rule
157
+ // `aggregateMcpStates` applies across the writes within a single file.
158
+ const prior = states.get(member.name);
159
+ states.set(
160
+ member.name,
161
+ prior === undefined || prior === member.state
162
+ ? member.state
163
+ : "drifted",
164
+ );
165
+ }
166
+ }
167
+
168
+ return registry.map((harness): InventoryRow => {
169
+ const detected = states.get(harness.name);
170
+ const paths = locations.get(harness.name) ?? [];
171
+ const state: InventoryState =
172
+ detected === undefined
173
+ ? harness.inScope
174
+ ? "undetected"
175
+ : "unscoped"
176
+ : detected === "registered"
177
+ ? "registered"
178
+ : detected === "drifted"
179
+ ? "drifted"
180
+ : "detected";
181
+ return {
182
+ harnessId: harness.id,
183
+ harnessName: harness.name,
184
+ scope,
185
+ state,
186
+ locations: paths,
187
+ };
188
+ });
189
+ }
190
+
191
+ /** The per-item status each state renders as. Never `fail` — see the docblock. */
192
+ const STATE_STATUS: Record<InventoryState, CheckStatus> = {
193
+ registered: "pass",
194
+ drifted: "available",
195
+ detected: "available",
196
+ undetected: "skip",
197
+ unscoped: "skip",
198
+ };
199
+
200
+ /**
201
+ * The phrase each state reads as, scope-aware for the unscoped case.
202
+ *
203
+ * The unscoped line says WHERE the harness keeps its config rather than naming
204
+ * an internal partition. `no Global band` told the reader nothing: "band" was
205
+ * this repository's word, and the fact underneath it — VS Code stores its MCP
206
+ * config per project, so there is no global file for pragma to be in — is the
207
+ * thing that explains the row.
208
+ */
209
+ const stateDetail = (row: InventoryRow): string => {
210
+ switch (row.state) {
211
+ case "registered":
212
+ return "registered";
213
+ case "drifted":
214
+ return "registered, entry differs from current";
215
+ case "detected":
216
+ return "detected, not registered";
217
+ case "undetected":
218
+ return "not detected";
219
+ case "unscoped":
220
+ return row.scope === "global"
221
+ ? "keeps no global config — it is per-project only"
222
+ : "keeps no per-project config — it is global only";
223
+ }
224
+ };
225
+
226
+ /**
227
+ * Project inventory rows into doctor sub-items.
228
+ *
229
+ * The default listing is DETECTED HARNESSES ONLY — a report that names every
230
+ * tool the user does not have buries the ones they do. `--verbose` widens it to
231
+ * the whole registry, which is the only view in which `undetected`/`unscoped`
232
+ * rows appear at all.
233
+ *
234
+ * @param rows - This scope's inventory rows.
235
+ * @param verbose - Whether to list the whole registry rather than the hits.
236
+ * @returns The sub-items, harness name in the left column.
237
+ */
238
+ export function inventoryItems(
239
+ rows: readonly InventoryRow[],
240
+ verbose: boolean,
241
+ ): CheckItem[] {
242
+ const shown = verbose
243
+ ? rows
244
+ : rows.filter((row) => isDetectedState(row.state));
245
+ return shown.map((row): CheckItem => {
246
+ const where = row.locations.join(", ");
247
+ return {
248
+ label: row.harnessName,
249
+ status: STATE_STATUS[row.state],
250
+ detail: where ? `${stateDetail(row)} — ${where}` : stateDetail(row),
251
+ };
252
+ });
253
+ }
254
+
255
+ /** The inventory check's headline, and whether it has anything to show. */
256
+ export interface InventoryHealth {
257
+ readonly status: CheckStatus;
258
+ readonly detail: string;
259
+ readonly items: readonly CheckItem[];
260
+ }
261
+
262
+ /**
263
+ * The whole inventory row for one scope: a listing, not a verdict.
264
+ *
265
+ * Its status is only ever `pass` (this scope has harnesses) or `skip` (it has
266
+ * none). It is the one scoped row that is not a setup target, so it derives no
267
+ * `fix:` line — the `mcp` and `skills` rows beside it own every action a
268
+ * harness can need, and a second row proposing the same command would be the
269
+ * "same finding seen twice" the report is built to avoid.
270
+ *
271
+ * @param rows - This scope's inventory rows.
272
+ * @param verbose - Whether the listing widens to the whole registry.
273
+ * @returns The status, headline and sub-items for the scope's inventory check.
274
+ */
275
+ export function inventoryHealth(
276
+ rows: readonly InventoryRow[],
277
+ verbose: boolean,
278
+ ): InventoryHealth {
279
+ const detected = rows.filter((row) => isDetectedState(row.state));
280
+ const registered = rows.filter((row) => row.state === "registered");
281
+ const items = inventoryItems(rows, verbose);
282
+ if (detected.length === 0) {
283
+ return {
284
+ status: "skip",
285
+ detail: verbose
286
+ ? `none of ${rows.length} known harnesses detected`
287
+ : "no harnesses detected",
288
+ items,
289
+ };
290
+ }
291
+ return {
292
+ status: "pass",
293
+ detail: `${detected.length} detected · ${registered.length} registered${
294
+ verbose ? ` · ${rows.length} known` : ""
295
+ }`,
296
+ items,
297
+ };
298
+ }
@@ -6,17 +6,17 @@
6
6
  * them apart from the report body is what lets `runChecks` stay a composition
7
7
  * — it orders and awaits, and knows nothing about what any check looks at.
8
8
  *
9
- * Two kinds live here, and the barrel exposes them differently. The UNBANDED
9
+ * Two kinds live here, and the barrel exposes them differently. The UNSCOPED
10
10
  * checks diagnose what `setup` cannot install — the Node floor, the CLI's own
11
11
  * version and install source, the pack answering this project's reads, and the
12
- * store — so each is its own named entry point. The BANDED rows are derived
13
- * per target, per band, from the setup target table rather than authored here,
12
+ * store — so each is its own named entry point. The SCOPED rows are derived
13
+ * per target, per scope, from the setup target table rather than authored here,
14
14
  * so they surface as the one generator that derives them, never as a list of
15
15
  * row names this file would then have to keep in step with `setup`.
16
16
  *
17
17
  * The per-target probes (the completions script check, the MCP command
18
- * resolution helpers) stay internal on purpose: `bandedChecks` is the only way
19
- * they are meant to be reached, and a row reachable past its band is a row
18
+ * resolution helpers) stay internal on purpose: `scopedChecks` is the only way
19
+ * they are meant to be reached, and a row reachable past its scope is a row
20
20
  * `doctor` can report and `setup` cannot fix.
21
21
  */
22
22
 
@@ -24,4 +24,4 @@ export { checkKeStore } from "./checkKeStore.js";
24
24
  export { checkNodeVersion } from "./checkNodeVersion.js";
25
25
  export { checkPackageRefs } from "./checkPackageRefs.js";
26
26
  export { checkPragmaVersion } from "./checkPragmaVersion.js";
27
- export { bandedChecks } from "./targetHealth.js";
27
+ export { scopedChecks } from "./targetHealth.js";
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Pins the hand-written comparison in `isSupportedNodeVersion` to the range the
3
+ * package actually declares.
4
+ *
5
+ * The implementation encodes `engines.node` as a chain of comparisons rather
6
+ * than parsing it, so nothing structural stops the two drifting apart. This
7
+ * suite closes that by building a SECOND, independent predicate directly from
8
+ * the declared range string and requiring both to agree across a version grid.
9
+ * A change to `engines.node` therefore moves the oracle and fails here until the
10
+ * implementation is moved with it — which a table of hand-written expectations
11
+ * could not do, since its cheapest repair is editing the expectations.
12
+ */
13
+
14
+ import { describe, expect, it } from "vitest";
15
+ import { SUPPORTED_NODE_RANGE } from "../../../constants.js";
16
+ import { isSupportedNodeVersion } from "./isSupportedNodeVersion.js";
17
+
18
+ /**
19
+ * Evaluate a version against a range of the form `>=22.18 <23 || >=23.6`.
20
+ *
21
+ * Deliberately naive and deliberately separate from the implementation: it is
22
+ * an oracle, not a utility, and it throws on any comparator shape it was not
23
+ * written for so a widened range fails loudly instead of silently passing.
24
+ */
25
+ function satisfiesDeclaredRange(version: string, range: string): boolean {
26
+ // Semver excludes prereleases from a range unless a comparator opts in, and
27
+ // none of ours does. Encoded here too, so the oracle and the implementation
28
+ // are held to the same rule rather than agreeing by omission.
29
+ if (version.includes("-")) return false;
30
+
31
+ const rank = (major: number, minor: number) => major * 1_000_000 + minor;
32
+ const [major = Number.NaN, minor = Number.NaN] = version
33
+ .split(".")
34
+ .map(Number);
35
+
36
+ return range.split("||").some((clause) =>
37
+ clause
38
+ .trim()
39
+ .split(/\s+/)
40
+ .every((comparator) => {
41
+ // A comparator may be major-only (`<23`) or major.minor (`>=22.18`).
42
+ const parsed = /^(>=|<)(\d+)(?:\.(\d+))?$/.exec(comparator);
43
+ if (!parsed)
44
+ throw new Error(`oracle cannot parse comparator: ${comparator}`);
45
+ const [, operator, boundMajor, boundMinor] = parsed;
46
+ const actual = rank(major, minor);
47
+ const bound = rank(Number(boundMajor), Number(boundMinor ?? 0));
48
+ return operator === ">=" ? actual >= bound : actual < bound;
49
+ }),
50
+ );
51
+ }
52
+
53
+ const GRID = [
54
+ "20.19.0",
55
+ "21.7.3",
56
+ "22.0.0",
57
+ "22.12.0",
58
+ "22.17.9",
59
+ "22.18.0",
60
+ "22.18.1",
61
+ "22.99.0",
62
+ "23.0.0",
63
+ "23.5.9",
64
+ "23.6.0",
65
+ "23.9.0",
66
+ "24.0.0",
67
+ "24.18.1",
68
+ "25.0.0",
69
+ "26.0.0",
70
+ // Prereleases: numerically inside the range, excluded by semver.
71
+ "22.18.0-pre",
72
+ "23.6.0-nightly",
73
+ "24.0.0-rc.1",
74
+ ];
75
+
76
+ describe("isSupportedNodeVersion — agrees with the declared engines range", () => {
77
+ it.each(GRID)("matches the oracle for %s", (version) => {
78
+ expect(isSupportedNodeVersion(version)).toBe(
79
+ satisfiesDeclaredRange(version, SUPPORTED_NODE_RANGE),
80
+ );
81
+ });
82
+
83
+ // The oracle is only trustworthy if it can disagree; prove it discriminates
84
+ // rather than returning a constant.
85
+ it("uses an oracle that actually discriminates", () => {
86
+ const verdicts = GRID.map((v) =>
87
+ satisfiesDeclaredRange(v, SUPPORTED_NODE_RANGE),
88
+ );
89
+ expect(verdicts).toContain(true);
90
+ expect(verdicts).toContain(false);
91
+ });
92
+
93
+ it("rejects prerelease builds whose numeric part is in range", () => {
94
+ // Asserted directly, not only through the oracle: if both sides dropped the
95
+ // rule together the grid would still agree and prove nothing.
96
+ for (const pre of ["22.18.0-pre", "23.6.0-nightly", "24.0.0-rc.1"]) {
97
+ expect(isSupportedNodeVersion(pre)).toBe(false);
98
+ }
99
+ // The same versions without the suffix are supported, so it is the suffix
100
+ // doing the work and not the numbers.
101
+ for (const stable of ["22.18.0", "23.6.0", "24.0.0"]) {
102
+ expect(isSupportedNodeVersion(stable)).toBe(true);
103
+ }
104
+ });
105
+
106
+ it("fails closed on a version it cannot parse", () => {
107
+ for (const bad of ["not-a-version", "", "v24.1.0", "24"]) {
108
+ expect(isSupportedNodeVersion(bad)).toBe(false);
109
+ }
110
+ });
111
+ });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Whether a Node.js version satisfies the range this package declares in its
3
+ * own `engines.node`, surfaced as `SUPPORTED_NODE_RANGE` in `constants.ts`.
4
+ *
5
+ * The range is expressed here as a comparison rather than parsed: the CLI
6
+ * carries no semver dependency, and one range does not justify a range parser.
7
+ * What keeps the two honest is `isSupportedNodeVersion.test.ts`, which builds an
8
+ * independent predicate straight from the declared range string and requires
9
+ * the two to agree across a version grid — so a change to `engines.node` that
10
+ * is not mirrored here fails the suite rather than shipping.
11
+ *
12
+ * A major-only comparison cannot express this range: it cannot say "22.18 but
13
+ * not 22.0", and it cannot exclude the 23.0–23.5 window.
14
+ *
15
+ * @param version - A Node version string, e.g. `process.versions.node`.
16
+ * @returns Whether the version falls inside the declared range.
17
+ */
18
+ export function isSupportedNodeVersion(version: string): boolean {
19
+ // A semver range excludes prereleases unless a comparator opts into them, and
20
+ // `engines.node` opts into none. So `22.18.0-pre` is outside the declared
21
+ // range even though its numeric part is inside it, and reporting it as
22
+ // supported would be doctor passing a runtime the install contract rejects.
23
+ if (version.includes("-")) return false;
24
+
25
+ const [rawMajor, rawMinor] = version.split(".");
26
+ const major = Number(rawMajor);
27
+ const minor = Number(rawMinor);
28
+
29
+ if (!Number.isInteger(major) || !Number.isInteger(minor)) return false;
30
+ if (major < 22) return false;
31
+ if (major === 22) return minor >= 18;
32
+ if (major === 23) return minor >= 6;
33
+ return true;
34
+ }
@@ -45,6 +45,12 @@ describe("commandOf — one executable, either entry shape", () => {
45
45
  });
46
46
  });
47
47
 
48
+ // Every test in this suite writes into a temp directory and then resolves
49
+ // against it, so each one is bound by filesystem latency rather than by any
50
+ // work of its own. Vitest's 5s default is comfortable on an idle machine and
51
+ // marginal on a loaded one — this suite has timed out in CI while asserting
52
+ // nothing slow. Ten seconds keeps the assertions meaningful without making a
53
+ // genuine hang cheap to ignore.
48
54
  describe("commandResolves — the host's own resolution rules", () => {
49
55
  const dirs: string[] = [];
50
56
  const tmp = (): string => {
@@ -97,4 +103,4 @@ describe("commandResolves — the host's own resolution rules", () => {
97
103
  await commandResolves("./local-bin", dir, hostOf("linux", dir)),
98
104
  ).toBe(true);
99
105
  });
100
- });
106
+ }, 10_000);