@canonical/pragma-cli 0.32.0 → 0.34.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 +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +19 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +15 -6
  6. package/src/capabilities/block.parity.test.ts +250 -0
  7. package/src/capabilities/capabilities/capabilities.render.ts +3 -2
  8. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  9. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  10. package/src/capabilities/capabilities/catalog.ts +20 -8
  11. package/src/capabilities/capabilities/hints.ts +21 -36
  12. package/src/capabilities/capabilities/types.ts +2 -0
  13. package/src/capabilities/colophon/collectColophon.ts +34 -19
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +11 -4
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/types.ts +16 -7
  20. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  21. package/src/capabilities/config/collectConfigShow.ts +41 -4
  22. package/src/capabilities/config/fields.ts +10 -108
  23. package/src/capabilities/config/set.test.ts +6 -6
  24. package/src/capabilities/config/set.verb.ts +12 -8
  25. package/src/capabilities/config/show.render.test.ts +137 -0
  26. package/src/capabilities/config/show.render.ts +57 -16
  27. package/src/capabilities/config/show.verb.ts +9 -8
  28. package/src/capabilities/config/types.ts +8 -0
  29. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/byteEquality.test.ts +82 -101
  31. package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
  32. package/src/capabilities/create/constants.ts +75 -0
  33. package/src/capabilities/create/create.mcp.test.ts +2 -2
  34. package/src/capabilities/create/create.test.ts +192 -16
  35. package/src/capabilities/create/create.verb.ts +208 -79
  36. package/src/capabilities/create/index.ts +2 -10
  37. package/src/capabilities/create/pickGenerator.ts +34 -28
  38. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  39. package/src/capabilities/create/types.ts +10 -6
  40. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  41. package/src/capabilities/distribution.test.ts +120 -0
  42. package/src/capabilities/distribution.ts +59 -0
  43. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  44. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  45. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  46. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
  47. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  48. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  49. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  50. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  51. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  52. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  53. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  54. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  55. package/src/capabilities/doctor/doctor.render.ts +117 -34
  56. package/src/capabilities/doctor/doctor.test.ts +68 -9
  57. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  58. package/src/capabilities/doctor/runChecks.ts +33 -9
  59. package/src/capabilities/doctor/types.ts +16 -1
  60. package/src/capabilities/emptyRecovery.test.ts +32 -15
  61. package/src/capabilities/graph/inspect.verb.ts +3 -2
  62. package/src/capabilities/graph/query.verb.ts +3 -2
  63. package/src/capabilities/graph/runQuery.ts +8 -5
  64. package/src/capabilities/index.ts +22 -18
  65. package/src/capabilities/info/collectInfo.ts +10 -6
  66. package/src/capabilities/info/info.render.ts +3 -2
  67. package/src/capabilities/info/info.verb.ts +3 -2
  68. package/src/capabilities/lazy.test.ts +93 -2
  69. package/src/capabilities/ontology/index.ts +10 -3
  70. package/src/capabilities/ontology/ontology.test.ts +157 -5
  71. package/src/capabilities/ontology/verbs.ts +203 -104
  72. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  73. package/src/capabilities/prompt/prompt.render.ts +23 -6
  74. package/src/capabilities/prompt/prompt.test.ts +68 -1
  75. package/src/capabilities/prompt/verbs.ts +24 -11
  76. package/src/capabilities/reference.test.ts +32 -0
  77. package/src/capabilities/resources/provider.ts +58 -25
  78. package/src/capabilities/resources/resources.test.ts +176 -21
  79. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  80. package/src/capabilities/setup/index.ts +7 -1
  81. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  82. package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
  83. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  84. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  85. package/src/capabilities/setup/operations/setupSkills.ts +10 -4
  86. package/src/capabilities/setup/setup.render.test.ts +109 -0
  87. package/src/capabilities/setup/setup.render.ts +61 -14
  88. package/src/capabilities/setup/setup.test.ts +352 -1
  89. package/src/capabilities/setup/setup.verb.ts +68 -9
  90. package/src/capabilities/setup/shell.ts +29 -3
  91. package/src/capabilities/setup/types.ts +59 -2
  92. package/src/capabilities/shared/bands.ts +16 -0
  93. package/src/capabilities/skill/discover.ts +11 -5
  94. package/src/capabilities/skill/render.ts +4 -5
  95. package/src/capabilities/skill/verbs.ts +7 -7
  96. package/src/capabilities/sources/collectStatus.ts +69 -49
  97. package/src/capabilities/sources/index.ts +1 -1
  98. package/src/capabilities/sources/installSkills.ts +2 -2
  99. package/src/capabilities/sources/runUpdate.ts +146 -98
  100. package/src/capabilities/sources/sources.test.ts +264 -84
  101. package/src/capabilities/sources/status.render.test.ts +94 -0
  102. package/src/capabilities/sources/status.render.ts +55 -33
  103. package/src/capabilities/sources/status.verb.ts +15 -8
  104. package/src/capabilities/sources/types.ts +18 -25
  105. package/src/capabilities/sources/update.render.ts +9 -2
  106. package/src/capabilities/sources/update.verb.ts +12 -23
  107. package/src/capabilities/surface.test.ts +65 -17
  108. package/src/capabilities/tier.parity.test.ts +144 -0
  109. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  110. package/src/constants.test.ts +9 -7
  111. package/src/constants.ts +49 -23
  112. package/src/identity.test.ts +333 -0
  113. package/src/index.ts +2 -2
  114. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
  115. package/src/kernel/completion/complete.test.ts +3 -5
  116. package/src/kernel/completion/emitScripts.test.ts +1 -1
  117. package/src/kernel/completion/emitScripts.ts +4 -2
  118. package/src/kernel/completion/entitySource.test.ts +185 -41
  119. package/src/kernel/completion/entitySource.ts +131 -113
  120. package/src/kernel/completion/model.test.ts +3 -3
  121. package/src/kernel/completion/model.ts +11 -1
  122. package/src/kernel/completion/parse.test.ts +2 -4
  123. package/src/kernel/completion/safety.test.ts +82 -36
  124. package/src/kernel/completion/shellDrive.test.ts +592 -0
  125. package/src/kernel/completion/templates/fish.ts +28 -6
  126. package/src/kernel/completion/types.ts +3 -3
  127. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  128. package/src/kernel/config/defaults.test.ts +85 -0
  129. package/src/kernel/config/defaults.ts +12 -23
  130. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  131. package/src/kernel/config/findProjectConfig.ts +4 -3
  132. package/src/kernel/config/firstRun.ts +12 -5
  133. package/src/kernel/config/globalConfig.ts +2 -2
  134. package/src/kernel/config/index.ts +4 -5
  135. package/src/kernel/config/paths.ts +17 -17
  136. package/src/kernel/config/readConfig.test.ts +242 -6
  137. package/src/kernel/config/readConfig.ts +8 -8
  138. package/src/kernel/config/schema.test.ts +50 -0
  139. package/src/kernel/config/schema.ts +97 -10
  140. package/src/kernel/config/types.ts +73 -19
  141. package/src/kernel/copy.test.ts +453 -0
  142. package/src/kernel/error/PragmaError.test.ts +5 -5
  143. package/src/kernel/error/constants.ts +3 -2
  144. package/src/kernel/error/index.ts +1 -1
  145. package/src/kernel/error/recovery.test.ts +22 -45
  146. package/src/kernel/error/recovery.ts +22 -27
  147. package/src/kernel/error/renderError.ts +1 -1
  148. package/src/kernel/error/types.ts +5 -4
  149. package/src/kernel/packs/collect.test.ts +195 -11
  150. package/src/kernel/packs/collect.ts +213 -58
  151. package/src/kernel/packs/compile.test.ts +163 -7
  152. package/src/kernel/packs/compile.ts +15 -12
  153. package/src/kernel/packs/disclosure.test.ts +5 -2
  154. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  155. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  156. package/src/kernel/packs/iri.ts +5 -7
  157. package/src/kernel/packs/renderPack.ts +5 -3
  158. package/src/kernel/packs/resolveEntity.ts +21 -17
  159. package/src/kernel/packs/runBodies.ts +15 -14
  160. package/src/kernel/packs/schema.ts +96 -10
  161. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  162. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  163. package/src/kernel/packs/toolDescription.test.ts +10 -2
  164. package/src/kernel/packs/types.ts +39 -4
  165. package/src/kernel/packs/uniqueness.ts +4 -4
  166. package/src/kernel/project/cli/buildProgram.test.ts +60 -11
  167. package/src/kernel/project/cli/buildProgram.ts +11 -0
  168. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  169. package/src/kernel/project/cli/dispatch.ts +34 -12
  170. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  171. package/src/kernel/project/cli/globalFlags.ts +14 -14
  172. package/src/kernel/project/cli/helpFormat.ts +1 -1
  173. package/src/kernel/project/cli/rootHelp.ts +109 -107
  174. package/src/kernel/project/mcp/buildServer.ts +11 -6
  175. package/src/kernel/project/mcp/instructions.ts +12 -7
  176. package/src/kernel/project/mcp/mcpError.ts +52 -0
  177. package/src/kernel/project/mcp/parity.test.ts +39 -1
  178. package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
  179. package/src/kernel/project/mcp/prompts/provider.ts +51 -7
  180. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  181. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  182. package/src/kernel/project/mcp/serve.ts +11 -3
  183. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  184. package/src/kernel/render/disclosure.ts +4 -2
  185. package/src/kernel/render/formatters.ts +5 -4
  186. package/src/kernel/render/prefixes.test.ts +36 -0
  187. package/src/kernel/render/prefixes.ts +40 -13
  188. package/src/kernel/render/renderers.test.ts +35 -1
  189. package/src/kernel/render/renderers.ts +25 -7
  190. package/src/kernel/render/style.test.ts +31 -0
  191. package/src/kernel/render/style.ts +74 -0
  192. package/src/kernel/runtime/graphpack/build.ts +57 -3
  193. package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
  194. package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
  195. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
  196. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
  197. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  198. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  199. package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
  200. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  201. package/src/kernel/runtime/graphpack/index.ts +1 -1
  202. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  203. package/src/kernel/runtime/graphpack/read.ts +32 -22
  204. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  205. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  206. package/src/kernel/runtime/graphpack/types.ts +72 -45
  207. package/src/kernel/runtime/loadSession.ts +4 -12
  208. package/src/kernel/runtime/paths.ts +64 -15
  209. package/src/kernel/runtime/probe.test.ts +8 -4
  210. package/src/kernel/runtime/readEntity.ts +6 -5
  211. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  212. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  213. package/src/kernel/runtime/refs/resolve.ts +141 -65
  214. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  215. package/src/kernel/runtime/resolveSources.ts +36 -25
  216. package/src/kernel/runtime/storeReadiness.ts +62 -0
  217. package/src/kernel/runtime/storeless.test.ts +1 -1
  218. package/src/kernel/runtime/types.ts +1 -1
  219. package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
  220. package/src/kernel/spec/emitReference.test.ts +196 -0
  221. package/src/kernel/spec/emitReference.ts +624 -0
  222. package/src/kernel/spec/emitSurface.ts +13 -7
  223. package/src/kernel/spec/index.ts +2 -0
  224. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  225. package/src/kernel/spec/surfaceConformance.ts +8 -0
  226. package/src/kernel/spec/types.ts +29 -9
  227. package/src/kernel/spec/validate.ts +2 -1
  228. package/src/kernel/vocabulary.test.ts +164 -0
  229. package/src/kernel/vocabulary.ts +146 -0
  230. package/src/testing/PARITY_GAPS.ts +9 -3
  231. package/src/testing/behavioral/README.md +3 -3
  232. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  233. package/src/testing/behavioral/completion.test.ts +21 -8
  234. package/src/testing/behavioral/disclosure.test.ts +12 -0
  235. package/src/testing/behavioral/docExamples.test.ts +283 -0
  236. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  237. package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
  238. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  239. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  240. package/src/testing/behavioral/journeys.cli.test.ts +116 -73
  241. package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
  242. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  243. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  244. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  245. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
  246. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  247. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  248. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  249. package/src/testing/behavioral/staleCommands.test.ts +86 -0
  250. package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
  251. package/src/testing/eval/cases/assembly.ts +16 -9
  252. package/src/testing/eval/cases/effectful.ts +31 -16
  253. package/src/testing/eval/cases/readNouns.ts +23 -18
  254. package/src/testing/eval/cases/stable.ts +5 -1
  255. package/src/testing/eval/eval.test.ts +7 -1
  256. package/src/testing/fixtures/fixtureCapability.ts +201 -1
  257. package/src/testing/fixtures/graph/canonical.ts +6 -3
  258. package/src/testing/fixtures/graph/defaultPack.ts +179 -0
  259. package/src/testing/fixtures/graph/owlExport.ts +97 -0
  260. package/src/testing/helpers/extractSnippets.ts +29 -0
  261. package/src/testing/helpers/fixtureGraph.ts +33 -16
  262. package/src/testing/helpers/golden.ts +1 -1
  263. package/src/testing/helpers/packRuntime.ts +2 -3
  264. package/src/testing/helpers/projectCli.ts +2 -1
  265. package/src/testing/helpers/projectMcp.ts +6 -0
  266. package/src/testing/perf/budgets.test.ts +4 -0
  267. package/src/testing/perf/budgets.ts +22 -7
  268. package/src/testing/perf/globalSetup.ts +121 -6
  269. package/src/types.ts +2 -2
  270. package/src/capabilities/block/blockList.verb.ts +0 -150
  271. package/src/capabilities/block/index.ts +0 -25
  272. package/src/capabilities/block/pack.ts +0 -155
  273. package/src/capabilities/block/parity.test.ts +0 -180
  274. package/src/capabilities/block/tierChain.ts +0 -78
  275. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  276. package/src/capabilities/config/field.test.ts +0 -188
  277. package/src/capabilities/modifier/index.ts +0 -15
  278. package/src/capabilities/modifier/pack.ts +0 -64
  279. package/src/capabilities/standard/index.ts +0 -18
  280. package/src/capabilities/standard/pack.ts +0 -128
  281. package/src/capabilities/tier/index.ts +0 -19
  282. package/src/capabilities/tier/lookup.render.ts +0 -34
  283. package/src/capabilities/tier/lookup.test.ts +0 -68
  284. package/src/capabilities/tier/lookup.verb.ts +0 -44
  285. package/src/capabilities/tier/pack.ts +0 -28
  286. package/src/capabilities/tier/runLookup.ts +0 -59
  287. package/src/capabilities/token/addConfig.render.ts +0 -27
  288. package/src/capabilities/token/addConfig.test.ts +0 -62
  289. package/src/capabilities/token/addConfig.verb.ts +0 -40
  290. package/src/capabilities/token/index.ts +0 -19
  291. package/src/capabilities/token/pack.ts +0 -58
  292. package/src/capabilities/token/runAddConfig.ts +0 -95
  293. package/src/kernel/completion/bashDrive.test.ts +0 -73
  294. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  295. package/src/kernel/runtime/lock.ts +0 -90
@@ -1,73 +1,107 @@
1
1
  /**
2
- * Doctor check: report how each configured semantic package resolves.
2
+ * Doctor check: report which pack answers this project's reads.
3
3
  *
4
- * ADAPTED for v2: the old local>git>bundled loader chain is retired — resolution
5
- * now flows through `sources update` into `pragma.lock.json` + the cached pack.
6
- * This check reads the lock (which packages resolved) and the storeless pack
7
- * index (the entity total), never booting the store. An absent lock with
8
- * configured packages is the "run `sources update`" case.
4
+ * It switches on the ONE boot decision ({@link resolveSources}) rather than
5
+ * re-deriving it from config, so doctor can never call an install healthy that
6
+ * every read would fail. That is the whole point of the check: a project that
7
+ * configured its own packs and never built them reports `fail` with the update
8
+ * remedy — not a pass listing the distribution's packs it is not reading.
9
+ * Storeless throughout: the pointer, the manifest, and the entity index are
10
+ * read off disk, never through a store session.
11
+ *
12
+ * It also names every read story that pack carries but cannot use. Those are
13
+ * DROPPED at dispatch rather than thrown (a third-party story must never break
14
+ * a command), so doctor is where the user finds out — and it goes through the
15
+ * same `validateStories` the dispatch path uses, so the two can never disagree.
9
16
  */
10
17
 
18
+ import { BIN_NAME } from "../../../constants.js";
11
19
  import {
12
20
  entityTotal,
13
21
  readPackIndex,
14
22
  } from "../../../kernel/completion/entitySource.js";
15
- import type { PackageEntry } from "../../../kernel/config/types.js";
16
- import { readLock } from "../../../kernel/runtime/lock.js";
23
+ import { validateStories } from "../../../kernel/packs/collect.js";
24
+ import { embeddedManifest } from "../../../kernel/runtime/graphpack/embedded.js";
25
+ import { readManifest } from "../../../kernel/runtime/graphpack/manifest.js";
26
+ import { activeStories } from "../../../kernel/runtime/graphpack/stories.js";
27
+ import type { SourcesDecision } from "../../../kernel/runtime/resolveSources.js";
28
+ import { resolveSources } from "../../../kernel/runtime/resolveSources.js";
17
29
  import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
18
30
  import type { CheckItem, CheckResult } from "../types.js";
19
31
 
20
- const entryName = (entry: PackageEntry): string =>
21
- typeof entry === "string" ? entry : entry.name;
32
+ /**
33
+ * The stories the answering pack carries but cannot use, as check items.
34
+ *
35
+ * The registry is reached through a RUNTIME dynamic import (as
36
+ * `collectColophon` does) so the static `capabilities/index → doctorModule →
37
+ * checkPackageRefs → capabilities/index` cycle can never form.
38
+ *
39
+ * @param decision - The boot decision the check already resolved.
40
+ * @returns One failing item per ignored story, in declaration order.
41
+ * @note Impure — reads the answering pack's `stories.json`.
42
+ */
43
+ async function ignoredStoryItems(
44
+ decision: SourcesDecision,
45
+ ): Promise<CheckItem[]> {
46
+ const { capabilities } = await import("../../index.js");
47
+ const { problems } = validateStories(activeStories(decision), capabilities);
48
+ return problems.map((problem) => ({
49
+ label: problem.source,
50
+ detail: problem.message,
51
+ status: "fail" as const,
52
+ }));
53
+ }
54
+
55
+ /** ` · N story/ies ignored` for a headline, or nothing when all are usable. */
56
+ function ignoredSuffix(items: readonly CheckItem[]): string {
57
+ if (items.length === 0) return "";
58
+ return ` · ${items.length} ${items.length === 1 ? "story" : "stories"} ignored`;
59
+ }
22
60
 
23
61
  /**
24
- * Report package resolution status against the lock and the entity total.
62
+ * Report which pack answers reads, its provenance, and its entity total.
25
63
  *
26
64
  * @param rt - The per-invocation runtime.
27
- * @returns A CheckResult with a per-package breakdown.
28
- * @note Impure — reads config, the lock, and the pack index from disk.
65
+ * @returns A CheckResult naming the answering pack, or the failure to build one.
66
+ * @note Impure — reads config, the active-pack pointer, and the pack index.
29
67
  */
30
68
  export async function checkPackageRefs(
31
69
  rt: PragmaRuntime,
32
70
  ): Promise<CheckResult> {
33
- const entries = (await rt.loadConfig()).config.packages ?? [];
34
- const lock = readLock(rt.cwd);
35
- const index = readPackIndex(rt.cwd);
36
- const totalEntities = index ? entityTotal(index) : 0;
71
+ const decision = resolveSources(await rt.loadConfig(), rt.cwd);
72
+ // The display says "packs" (config declarations); the file/function keep
73
+ // "Package" (npm/git resolution vocabulary).
74
+ const name = "pack refs";
37
75
 
38
- if (entries.length === 0) {
76
+ if (decision.kind === "unavailable") {
39
77
  return {
40
- name: "package refs",
41
- status: "pass",
42
- detail: `no packages configured — the embedded pack answers reads (${totalEntities.toLocaleString()} entities)`,
78
+ name,
79
+ status: "fail",
80
+ detail: decision.reason,
81
+ remedy: `${BIN_NAME} sources update`,
43
82
  };
44
83
  }
45
84
 
46
- const items: CheckItem[] = [];
47
- let unresolved = 0;
48
- for (const entry of entries) {
49
- const name = entryName(entry);
50
- const packEntry = lock?.packs.find((pack) => pack.name === name);
51
- if (packEntry) {
52
- items.push({
53
- label: name,
54
- status: "pass",
55
- detail: `resolved ${packEntry.resolved}`,
56
- });
57
- } else {
58
- items.push({ label: name, status: "fail", detail: "not in the lock" });
59
- unresolved += 1;
60
- }
85
+ const index = readPackIndex(decision);
86
+ const entities = (index ? entityTotal(index) : 0).toLocaleString();
87
+ // The pack DOES answer reads, so the check itself passes; the ignored stories
88
+ // are failing sub-items under it. (A `warn` status would ripple through the
89
+ // renderer and the DoctorData counts to buy an icon.)
90
+ const items = await ignoredStoryItems(decision);
91
+ const suffix = ignoredSuffix(items);
92
+ if (decision.kind === "embedded") {
93
+ return {
94
+ name,
95
+ status: "pass",
96
+ detail: `embedded snapshot @ ${embeddedManifest().sourceRef} — ${entities} entities${suffix} · \`${BIN_NAME} sources update\` to build from the configured packs`,
97
+ ...(items.length > 0 ? { items } : {}),
98
+ };
61
99
  }
62
-
63
- const ok = lock !== undefined && unresolved === 0;
100
+ const manifest = readManifest(decision.dir);
64
101
  return {
65
- name: "package refs",
66
- status: ok ? "pass" : "fail",
67
- detail: ok
68
- ? `${entries.length} package(s) resolved · ${totalEntities.toLocaleString()} entities`
69
- : `${unresolved} of ${entries.length} package(s) not resolved`,
70
- items,
71
- ...(ok ? {} : { remedy: "pragma sources update" }),
102
+ name,
103
+ status: "pass",
104
+ detail: `${manifest?.sourceRef ?? decision.contentHash.slice(0, 12)} — ${entities} entities, built ${manifest?.createdAt ?? "?"}${suffix}`,
105
+ ...(items.length > 0 ? { items } : {}),
72
106
  };
73
107
  }
@@ -1,9 +1,10 @@
1
+ import { BIN_NAME } from "../../../constants.js";
1
2
  import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
2
3
  import { detectInstallSource } from "../../shared/packageManager.js";
3
4
  import type { CheckResult } from "../types.js";
4
5
 
5
6
  /**
6
- * Report the pragma version and how it was installed. Always passes — purely
7
+ * Report the CLI version and how it was installed. Always passes — purely
7
8
  * informational.
8
9
  *
9
10
  * @param rt - The per-invocation runtime (for `version`).
@@ -15,7 +16,7 @@ export async function checkPragmaVersion(
15
16
  ): Promise<CheckResult> {
16
17
  const install = detectInstallSource();
17
18
  return {
18
- name: "pragma version",
19
+ name: `${BIN_NAME} version`,
19
20
  status: "pass",
20
21
  detail: `v${rt.version} (installed via ${install.label})`,
21
22
  };
@@ -1,19 +1,28 @@
1
1
  /**
2
2
  * `checkShellCompletions` — the doctor check that verifies completions are
3
- * installed AND functional.
3
+ * installed AND up to date AND functional.
4
4
  *
5
5
  * Runs against an isolated HOME and a controlled `$SHELL` so the three gates —
6
- * the resolver effect test, the real install-path probe, and the zsh fpath
7
- * activation gate — are deterministic. The effect test (gate 1) drives the real
6
+ * the resolver effect test, the install probe, and the zsh fpath activation
7
+ * gate — are deterministic. The effect test (gate 1) drives the real
8
8
  * `runComplete` resolver; the install/fpath gates write real files under the
9
9
  * temp HOME rather than mocking the fs.
10
+ *
11
+ * Gate 2 compares BYTES, not existence, so the "installed" cases here write the
12
+ * real `emitScripts(capabilities)[shell]` body. A file that merely exists is
13
+ * now the failing case, and it has its own test — as does the boundary on the
14
+ * other side, where a per-project `completion` config must NOT be able to
15
+ * condemn the one global script.
10
16
  */
11
17
 
12
18
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
13
19
  import { tmpdir } from "node:os";
14
20
  import { dirname, join } from "node:path";
15
21
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
16
- import { completionScriptPath } from "../../setup/shell.js";
22
+ import { emitScripts } from "../../../kernel/completion/emitScripts.js";
23
+ import { capabilities } from "../../index.js";
24
+ import { detectCompletions } from "../../setup/operations/setupCompletions.js";
25
+ import { completionScriptPath, type ShellId } from "../../setup/shell.js";
17
26
  import { checkShellCompletions } from "./checkShellCompletions.js";
18
27
 
19
28
  const roots: string[] = [];
@@ -38,11 +47,16 @@ afterEach(() => {
38
47
  roots.length = 0;
39
48
  });
40
49
 
41
- /** Write the (empty) completion script to the shell's real install path. */
42
- function installScript(shell: "zsh" | "bash" | "fish"): void {
50
+ /** Write `body` to the shell's real install path. */
51
+ function writeScript(shell: ShellId, body: string): void {
43
52
  const path = completionScriptPath(shell);
44
53
  mkdirSync(dirname(path), { recursive: true });
45
- writeFileSync(path, "# stub\n");
54
+ writeFileSync(path, body);
55
+ }
56
+
57
+ /** Install the script `setup completions` would write for this shell. */
58
+ function installScript(shell: ShellId): void {
59
+ writeScript(shell, emitScripts(capabilities)[shell]);
46
60
  }
47
61
 
48
62
  /** Write a `.zshrc` that puts ~/.zfunc on fpath. */
@@ -73,20 +87,56 @@ describe("checkShellCompletions — install probe (gate 2)", () => {
73
87
  expect(result.remedy).toBe("pragma setup completions");
74
88
  });
75
89
 
76
- it("passes for bash once the script exists at its real path", async () => {
90
+ it("passes for bash once the up-to-date script is at its real path", async () => {
77
91
  process.env.SHELL = "/usr/bin/bash";
78
92
  installScript("bash");
79
93
  const result = await checkShellCompletions(tmp());
80
94
  expect(result.status).toBe("pass");
81
- expect(result.detail).toMatch(/bash installed and resolving/);
95
+ expect(result.detail).toMatch(/bash up to date and resolving/);
82
96
  });
83
97
 
84
- it("passes for fish once the script exists at its real path", async () => {
98
+ it("passes for fish once the up-to-date script is at its real path", async () => {
85
99
  process.env.SHELL = "/usr/bin/fish";
86
100
  installScript("fish");
87
101
  const result = await checkShellCompletions(tmp());
88
102
  expect(result.status).toBe("pass");
89
103
  });
104
+
105
+ it("fails when the installed script is out of date", async () => {
106
+ // Reproduced against the compiled binary before this was fixed: overwrite
107
+ // the installed script with junk and `doctor` still reported
108
+ // "✓ Shell completions: bash installed and resolving (19 nouns)", while
109
+ // `setup completions` in the same breath said "Updating bash completions".
110
+ // Every user upgrading across a grammar change lands here: the old script
111
+ // offers the old nouns and misses the new ones, and doctor is the one
112
+ // command whose job is to say so.
113
+ process.env.SHELL = "/usr/bin/bash";
114
+ writeScript("bash", "# STALE JUNK\n");
115
+ const result = await checkShellCompletions(tmp());
116
+ expect(result.status).toBe("fail");
117
+ expect(result.detail).toMatch(/out of date/);
118
+ expect(result.remedy).toBe("pragma setup completions");
119
+ });
120
+
121
+ it("does NOT condemn the global script for one project's `completion` config", async () => {
122
+ // The script is ONE file per user; `completion` config is layered per
123
+ // project. Comparing the global file against only THIS directory's body
124
+ // made `doctor`'s verdict flip with `cd`: reproduced against the real
125
+ // check, a project declaring `minChars: 5` passed in its own directory and
126
+ // failed in every other, and running the offered remedy there moved the
127
+ // failure back rather than closing it. The first assertion is the
128
+ // discriminator — this project really does ask for a different body — so
129
+ // deleting the config write below turns the test red.
130
+ process.env.SHELL = "/usr/bin/bash";
131
+ installScript("bash");
132
+ const cwd = tmp();
133
+ writeFileSync(
134
+ join(cwd, "pragma.config.ts"),
135
+ "export default { completion: { minChars: 5 } };\n",
136
+ );
137
+ expect((await detectCompletions(cwd)).state).toBe("stale");
138
+ expect((await checkShellCompletions(cwd)).status).toBe("pass");
139
+ });
90
140
  });
91
141
 
92
142
  describe("checkShellCompletions — zsh fpath activation (gate 3)", () => {
@@ -106,6 +156,6 @@ describe("checkShellCompletions — zsh fpath activation (gate 3)", () => {
106
156
  wireZfunc();
107
157
  const result = await checkShellCompletions(tmp());
108
158
  expect(result.status).toBe("pass");
109
- expect(result.detail).toMatch(/zsh installed and resolving/);
159
+ expect(result.detail).toMatch(/zsh up to date and resolving/);
110
160
  });
111
161
  });
@@ -1,20 +1,48 @@
1
- import { existsSync, readFileSync } from "node:fs";
1
+ import { readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
+ import { BIN_NAME } from "../../../constants.js";
4
5
  import {
6
+ emitScripts,
5
7
  indexCompletionEnv,
6
8
  runComplete,
7
9
  } from "../../../kernel/completion/index.js";
8
10
  import { capabilities } from "../../index.js";
9
- import {
10
- activationHint,
11
- completionScriptPath,
12
- detectShell,
13
- } from "../../setup/shell.js";
11
+ import { detectCompletions } from "../../setup/operations/setupCompletions.js";
12
+ import { activationHint, type ShellId } from "../../setup/shell.js";
14
13
  import type { CheckResult } from "../types.js";
15
14
 
16
15
  const NAME = "Shell completions";
17
16
 
17
+ /** The one remedy for an absent or out-of-date script. */
18
+ const INSTALL_REMEDY = `${BIN_NAME} setup completions`;
19
+
20
+ /**
21
+ * Whether the installed script is the config-free emit — the body
22
+ * `setup completions` writes when no `completion` config tunes it.
23
+ *
24
+ * The completion script is ONE file per user, but the `completion` config it is
25
+ * rendered from is layered per project. So `detectCompletions(cwd)`'s `stale`
26
+ * means only "not what THIS directory would write", and on its own it would let
27
+ * any project that tunes `minChars` call a globally-correct script out of date
28
+ * from every other directory — a `doctor` verdict that flips with `cd`, and a
29
+ * remedy that just moves the failure to the project you came from. A script
30
+ * that matches neither this project's body nor the untuned one is the one that
31
+ * is genuinely stale.
32
+ *
33
+ * @param path - The install path.
34
+ * @param shell - The detected shell.
35
+ * @returns Whether the file there is byte-identical to the untuned emit.
36
+ * @note Impure — reads the install path. An unreadable file counts as `false`.
37
+ */
38
+ function isUntunedScript(path: string, shell: ShellId): boolean {
39
+ try {
40
+ return readFileSync(path, "utf-8") === emitScripts(capabilities)[shell];
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
18
46
  /**
19
47
  * Whether the user's `.zshrc` puts `~/.zfunc` on `$fpath` — the activation step
20
48
  * `setup completions` cannot perform for the user. zsh loads nothing from
@@ -53,23 +81,28 @@ async function completeProbe(cwd: string): Promise<number> {
53
81
  }
54
82
 
55
83
  /**
56
- * Check that shell completions are installed AND functional.
84
+ * Check that shell completions are installed AND up to date AND functional.
57
85
  *
58
86
  * Three gates, in order:
59
87
  * 1. The resolver answers (`completeProbe` drives `runComplete` end to end) —
60
88
  * the effect every installed script depends on. A failure here is a real
61
89
  * regression, so it fails even when nothing is installed.
62
- * 2. The script file exists at the shell's real install path (`shell.ts`) —
63
- * NOT a `"pragma"` substring in an RC file, which `setup completions` never
64
- * writes.
90
+ * 2. The installed script is present AND is a script `setup completions` would
91
+ * write. Bytes, not existence: a file that merely exists proves nothing — a
92
+ * user upgrading across a grammar change keeps the old script, loses the
93
+ * moved nouns from TAB, and used to be told everything was fine. The path
94
+ * and this project's body come from `detectCompletions`, the ONE owner of
95
+ * that decision; {@link isUntunedScript} is what keeps a per-project
96
+ * `completion` config from condemning a global file (see its docblock).
65
97
  * 3. For zsh, `~/.zfunc` is on `$fpath` — the activation step setup can only
66
98
  * hint. Installed-but-unwired reports a distinct remedy.
67
99
  *
68
- * @param cwd - The project directory (for the resolver's entity seam).
69
- * @returns A CheckResult: pass (installed + wired + answering), fail (with the
100
+ * @param cwd - The project directory (the resolver's entity seam, and the
101
+ * `completion` config gate 2 renders this project's body from).
102
+ * @returns A CheckResult: pass (up to date + wired + answering), fail (with the
70
103
  * attributable remedy), or skip (shell undetected).
71
- * @note Impure — reads `$SHELL`, the install path, `.zshrc`, and drives the
72
- * storeless resolver.
104
+ * @note Impure — reads `$SHELL`, the install path, the config layers, `.zshrc`,
105
+ * and drives the storeless resolver.
73
106
  */
74
107
  export async function checkShellCompletions(cwd: string): Promise<CheckResult> {
75
108
  // 1. Effect test: the resolver the scripts delegate to must actually answer.
@@ -82,34 +115,41 @@ export async function checkShellCompletions(cwd: string): Promise<CheckResult> {
82
115
  name: NAME,
83
116
  status: "fail",
84
117
  detail: `completion resolver failed: ${reason}`,
85
- remedy: "Report this as a bug — `pragma __complete` should never throw.",
118
+ remedy: `Report this as a bug — \`${BIN_NAME} __complete\` should never throw.`,
86
119
  };
87
120
  }
88
121
  if (candidates === 0) {
89
122
  return {
90
123
  name: NAME,
91
124
  status: "fail",
92
- detail: "completion resolver returned no candidates for `pragma <TAB>`",
125
+ detail: `completion resolver returned no candidates for \`${BIN_NAME} <TAB>\``,
93
126
  remedy: "Report this as a bug — the noun context is always non-empty.",
94
127
  };
95
128
  }
96
129
 
97
- // 2. The script file exists at the shell's real install path.
98
- const shell = detectShell();
99
- if (!shell) {
130
+ // 2. The installed script is a script `setup completions` would write.
131
+ const { shell, path, state } = await detectCompletions(cwd);
132
+ if (shell === null || path === null) {
100
133
  return {
101
134
  name: NAME,
102
135
  status: "skip",
103
136
  detail: "resolver OK; shell not detected ($SHELL unset)",
104
137
  };
105
138
  }
106
- const path = completionScriptPath(shell);
107
- if (!existsSync(path)) {
139
+ if (state === "absent") {
108
140
  return {
109
141
  name: NAME,
110
142
  status: "fail",
111
143
  detail: `resolver OK; ${shell} script not installed`,
112
- remedy: "pragma setup completions",
144
+ remedy: INSTALL_REMEDY,
145
+ };
146
+ }
147
+ if (state === "stale" && !isUntunedScript(path, shell)) {
148
+ return {
149
+ name: NAME,
150
+ status: "fail",
151
+ detail: `resolver OK; ${shell} script at ${path} is out of date`,
152
+ remedy: INSTALL_REMEDY,
113
153
  };
114
154
  }
115
155
 
@@ -126,6 +166,6 @@ export async function checkShellCompletions(cwd: string): Promise<CheckResult> {
126
166
  return {
127
167
  name: NAME,
128
168
  status: "pass",
129
- detail: `${shell} installed and resolving (${candidates} nouns)`,
169
+ detail: `${shell} up to date and resolving (${candidates} nouns)`,
130
170
  };
131
171
  }
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
2
2
  import type { DetectedHarness } from "@canonical/harnesses";
3
3
  import { detectHarnesses } from "@canonical/harnesses";
4
4
  import { runTask } from "@canonical/task/node";
5
+ import { BIN_NAME } from "../../../constants.js";
5
6
  import type { CheckResult } from "../types.js";
6
7
 
7
8
  /**
@@ -21,7 +22,7 @@ export async function checkSkillsSymlinked(cwd: string): Promise<CheckResult> {
21
22
  name: "Skills symlinked",
22
23
  status: "fail",
23
24
  detail: "harness detection failed",
24
- remedy: "pragma setup skills",
25
+ remedy: `${BIN_NAME} setup skills`,
25
26
  };
26
27
  }
27
28
 
@@ -55,6 +56,6 @@ export async function checkSkillsSymlinked(cwd: string): Promise<CheckResult> {
55
56
  name: "Skills symlinked",
56
57
  status: "fail",
57
58
  detail: `missing for ${missing.join(", ")}`,
58
- remedy: "pragma setup skills",
59
+ remedy: `${BIN_NAME} setup skills`,
59
60
  };
60
61
  }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * `deriveBand` — band a doctor check by the harnesses it actually found.
3
+ *
4
+ * Fixtures wrap REAL harness definitions (via `findHarnessById`, no casts), so
5
+ * the scope→band mapping is exercised against the true `scope` fields: the
6
+ * regression this guards is a global-scope harness (Windsurf) being mislabeled
7
+ * PROJECT by a static per-name guess.
8
+ */
9
+
10
+ import type { DetectedHarness } from "@canonical/harnesses";
11
+ import { findHarnessById } from "@canonical/harnesses";
12
+ import { describe, expect, it } from "vitest";
13
+ import { deriveBand } from "./deriveBand.js";
14
+
15
+ /** A minimal {@link DetectedHarness} over a real definition (no `as` casts). */
16
+ function detect(id: string): DetectedHarness {
17
+ const harness = findHarnessById(id);
18
+ if (!harness) throw new Error(`unknown harness fixture: ${id}`);
19
+ return { harness, confidence: "high", configExists: true, configPath: "x" };
20
+ }
21
+
22
+ describe("deriveBand", () => {
23
+ it("bands a project-scope harness (Cursor) as project", () => {
24
+ expect(deriveBand([detect("cursor")])).toBe("project");
25
+ });
26
+
27
+ it("bands a global-scope harness (Windsurf) as global — the fixed mislabel", () => {
28
+ expect(deriveBand([detect("windsurf")])).toBe("global");
29
+ });
30
+
31
+ it("bands a dual-scope harness (Claude Code) by its default band (project)", () => {
32
+ expect(deriveBand([detect("claude-code")])).toBe("project");
33
+ });
34
+
35
+ it("leaves a set spanning BOTH bands unbanded (general, not mislabeled)", () => {
36
+ expect(deriveBand([detect("cursor"), detect("windsurf")])).toBeUndefined();
37
+ });
38
+
39
+ it("leaves an empty set unbanded", () => {
40
+ expect(deriveBand([])).toBeUndefined();
41
+ });
42
+ });
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Derive a doctor check's config band from the harnesses it actually found,
3
+ * rather than a static per-check-name guess. The aggregate MCP checks read each
4
+ * harness's DEFAULT-band file (`defaultBandOf` — the home config for a
5
+ * global-only harness like Windsurf, the project file otherwise), so the band a
6
+ * finding belongs to is exactly that default band.
7
+ */
8
+
9
+ import type { DetectedHarness } from "@canonical/harnesses";
10
+ import { defaultBandOf } from "@canonical/harnesses";
11
+ import type { ScopeBand } from "../types.js";
12
+
13
+ /**
14
+ * The single config band a set of detected harnesses shares, or `undefined` when
15
+ * the set is empty or genuinely spans BOTH bands. A two-band finding stays
16
+ * unbanded (rendered in the general section) instead of being mislabeled as one
17
+ * band — faithful to the two-band model: band by what was found, never a guess.
18
+ *
19
+ * @param harnesses - The detected harnesses the check reports on.
20
+ * @returns The shared {@link ScopeBand}, or `undefined` when none/mixed.
21
+ */
22
+ export function deriveBand(
23
+ harnesses: readonly DetectedHarness[],
24
+ ): ScopeBand | undefined {
25
+ const bands = new Set<ScopeBand>();
26
+ for (const detected of harnesses) bands.add(defaultBandOf(detected.harness));
27
+ return bands.size === 1 ? bands.values().next().value : undefined;
28
+ }