@canonical/pragma-cli 0.33.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 (250) hide show
  1. package/README.md +7 -7
  2. package/dist/pragma +0 -0
  3. package/package.json +18 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +13 -4
  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.verb.ts +31 -13
  9. package/src/capabilities/capabilities/catalog.ts +18 -10
  10. package/src/capabilities/capabilities/hints.ts +13 -19
  11. package/src/capabilities/colophon/collectColophon.ts +34 -19
  12. package/src/capabilities/colophon/colophon.verb.ts +8 -4
  13. package/src/capabilities/colophon/types.ts +16 -7
  14. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  15. package/src/capabilities/config/collectConfigShow.ts +41 -4
  16. package/src/capabilities/config/fields.ts +0 -27
  17. package/src/capabilities/config/set.verb.ts +7 -3
  18. package/src/capabilities/config/show.render.test.ts +77 -5
  19. package/src/capabilities/config/show.render.ts +7 -12
  20. package/src/capabilities/config/show.verb.ts +4 -3
  21. package/src/capabilities/config/types.ts +8 -0
  22. package/src/capabilities/create/byteEquality.test.ts +78 -100
  23. package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
  24. package/src/capabilities/create/constants.ts +75 -0
  25. package/src/capabilities/create/create.mcp.test.ts +2 -2
  26. package/src/capabilities/create/create.test.ts +162 -15
  27. package/src/capabilities/create/create.verb.ts +140 -69
  28. package/src/capabilities/create/index.ts +2 -10
  29. package/src/capabilities/create/pickGenerator.ts +34 -28
  30. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  31. package/src/capabilities/create/types.ts +10 -6
  32. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  33. package/src/capabilities/distribution.test.ts +120 -0
  34. package/src/capabilities/distribution.ts +59 -0
  35. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  36. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  37. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
  38. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  39. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  40. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  41. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  42. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  43. package/src/capabilities/doctor/doctor.render.test.ts +6 -6
  44. package/src/capabilities/doctor/doctor.render.ts +2 -1
  45. package/src/capabilities/doctor/doctor.test.ts +37 -8
  46. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  47. package/src/capabilities/doctor/runChecks.ts +5 -5
  48. package/src/capabilities/doctor/types.ts +1 -1
  49. package/src/capabilities/emptyRecovery.test.ts +32 -15
  50. package/src/capabilities/graph/inspect.verb.ts +3 -2
  51. package/src/capabilities/graph/query.verb.ts +3 -2
  52. package/src/capabilities/graph/runQuery.ts +8 -5
  53. package/src/capabilities/index.ts +22 -18
  54. package/src/capabilities/info/collectInfo.ts +10 -6
  55. package/src/capabilities/info/info.render.ts +3 -2
  56. package/src/capabilities/info/info.verb.ts +3 -2
  57. package/src/capabilities/lazy.test.ts +93 -2
  58. package/src/capabilities/ontology/verbs.ts +21 -21
  59. package/src/capabilities/prompt/prompt.test.ts +12 -6
  60. package/src/capabilities/prompt/verbs.ts +24 -11
  61. package/src/capabilities/resources/provider.ts +30 -11
  62. package/src/capabilities/resources/resources.test.ts +85 -29
  63. package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
  64. package/src/capabilities/setup/operations/setupMcp.ts +2 -2
  65. package/src/capabilities/setup/operations/setupSkills.ts +3 -3
  66. package/src/capabilities/setup/setup.verb.ts +12 -5
  67. package/src/capabilities/setup/shell.ts +29 -3
  68. package/src/capabilities/skill/discover.ts +11 -5
  69. package/src/capabilities/skill/render.ts +4 -5
  70. package/src/capabilities/skill/verbs.ts +7 -7
  71. package/src/capabilities/sources/collectStatus.ts +55 -50
  72. package/src/capabilities/sources/index.ts +1 -1
  73. package/src/capabilities/sources/installSkills.ts +2 -2
  74. package/src/capabilities/sources/runUpdate.ts +113 -122
  75. package/src/capabilities/sources/sources.test.ts +111 -117
  76. package/src/capabilities/sources/status.render.test.ts +53 -24
  77. package/src/capabilities/sources/status.render.ts +24 -29
  78. package/src/capabilities/sources/status.verb.ts +15 -8
  79. package/src/capabilities/sources/types.ts +18 -25
  80. package/src/capabilities/sources/update.render.ts +9 -2
  81. package/src/capabilities/sources/update.verb.ts +12 -23
  82. package/src/capabilities/surface.test.ts +33 -10
  83. package/src/capabilities/tier.parity.test.ts +144 -0
  84. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  85. package/src/constants.test.ts +8 -6
  86. package/src/constants.ts +47 -21
  87. package/src/identity.test.ts +333 -0
  88. package/src/index.ts +2 -2
  89. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
  90. package/src/kernel/completion/emitScripts.test.ts +1 -1
  91. package/src/kernel/completion/emitScripts.ts +4 -2
  92. package/src/kernel/completion/entitySource.test.ts +185 -41
  93. package/src/kernel/completion/entitySource.ts +117 -107
  94. package/src/kernel/completion/model.test.ts +1 -1
  95. package/src/kernel/completion/model.ts +11 -0
  96. package/src/kernel/completion/safety.test.ts +82 -36
  97. package/src/kernel/completion/shellDrive.test.ts +592 -0
  98. package/src/kernel/completion/templates/fish.ts +28 -6
  99. package/src/kernel/completion/types.ts +3 -3
  100. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  101. package/src/kernel/config/defaults.test.ts +85 -0
  102. package/src/kernel/config/defaults.ts +12 -23
  103. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  104. package/src/kernel/config/findProjectConfig.ts +4 -3
  105. package/src/kernel/config/firstRun.ts +12 -5
  106. package/src/kernel/config/globalConfig.ts +2 -2
  107. package/src/kernel/config/index.ts +4 -5
  108. package/src/kernel/config/paths.ts +17 -17
  109. package/src/kernel/config/readConfig.test.ts +242 -6
  110. package/src/kernel/config/readConfig.ts +8 -8
  111. package/src/kernel/config/schema.test.ts +50 -0
  112. package/src/kernel/config/schema.ts +97 -10
  113. package/src/kernel/config/types.ts +73 -19
  114. package/src/kernel/copy.test.ts +453 -0
  115. package/src/kernel/error/PragmaError.test.ts +1 -1
  116. package/src/kernel/error/constants.ts +3 -2
  117. package/src/kernel/error/index.ts +1 -1
  118. package/src/kernel/error/recovery.test.ts +22 -45
  119. package/src/kernel/error/recovery.ts +22 -27
  120. package/src/kernel/error/types.ts +5 -4
  121. package/src/kernel/packs/collect.test.ts +195 -11
  122. package/src/kernel/packs/collect.ts +213 -58
  123. package/src/kernel/packs/compile.test.ts +163 -7
  124. package/src/kernel/packs/compile.ts +15 -12
  125. package/src/kernel/packs/disclosure.test.ts +5 -2
  126. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  127. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  128. package/src/kernel/packs/iri.ts +5 -7
  129. package/src/kernel/packs/renderPack.ts +5 -3
  130. package/src/kernel/packs/resolveEntity.ts +21 -17
  131. package/src/kernel/packs/runBodies.ts +15 -14
  132. package/src/kernel/packs/schema.ts +96 -10
  133. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  134. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  135. package/src/kernel/packs/toolDescription.test.ts +10 -2
  136. package/src/kernel/packs/types.ts +39 -4
  137. package/src/kernel/packs/uniqueness.ts +4 -4
  138. package/src/kernel/project/cli/buildProgram.test.ts +9 -4
  139. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  140. package/src/kernel/project/cli/dispatch.ts +34 -12
  141. package/src/kernel/project/cli/helpFormat.ts +1 -1
  142. package/src/kernel/project/cli/rootHelp.ts +105 -105
  143. package/src/kernel/project/mcp/buildServer.ts +11 -6
  144. package/src/kernel/project/mcp/instructions.ts +11 -6
  145. package/src/kernel/project/mcp/parity.test.ts +39 -1
  146. package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
  147. package/src/kernel/project/mcp/prompts/provider.ts +22 -8
  148. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  149. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  150. package/src/kernel/project/mcp/serve.ts +11 -3
  151. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  152. package/src/kernel/render/disclosure.ts +4 -2
  153. package/src/kernel/render/prefixes.test.ts +36 -0
  154. package/src/kernel/render/prefixes.ts +40 -13
  155. package/src/kernel/render/renderers.test.ts +1 -1
  156. package/src/kernel/runtime/graphpack/build.ts +29 -3
  157. package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
  158. package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
  159. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
  160. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
  161. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  162. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  163. package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
  164. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  165. package/src/kernel/runtime/graphpack/index.ts +1 -1
  166. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  167. package/src/kernel/runtime/graphpack/read.ts +4 -9
  168. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  169. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  170. package/src/kernel/runtime/graphpack/types.ts +61 -48
  171. package/src/kernel/runtime/paths.ts +64 -15
  172. package/src/kernel/runtime/probe.test.ts +8 -4
  173. package/src/kernel/runtime/readEntity.ts +6 -5
  174. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  175. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  176. package/src/kernel/runtime/refs/resolve.ts +78 -62
  177. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  178. package/src/kernel/runtime/resolveSources.ts +36 -25
  179. package/src/kernel/runtime/storeReadiness.ts +9 -8
  180. package/src/kernel/runtime/storeless.test.ts +1 -1
  181. package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
  182. package/src/kernel/spec/emitReference.test.ts +3 -3
  183. package/src/kernel/spec/emitReference.ts +144 -11
  184. package/src/kernel/spec/emitSurface.ts +9 -5
  185. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  186. package/src/kernel/spec/surfaceConformance.ts +8 -0
  187. package/src/kernel/spec/types.ts +29 -9
  188. package/src/kernel/spec/validate.ts +2 -1
  189. package/src/kernel/vocabulary.test.ts +164 -0
  190. package/src/kernel/vocabulary.ts +146 -0
  191. package/src/testing/PARITY_GAPS.ts +8 -2
  192. package/src/testing/behavioral/README.md +3 -3
  193. package/src/testing/behavioral/completion.test.ts +19 -6
  194. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  195. package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
  196. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  197. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  198. package/src/testing/behavioral/journeys.cli.test.ts +116 -75
  199. package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
  200. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  201. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
  202. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  203. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  204. package/src/testing/behavioral/mcpSurface.test.ts +9 -4
  205. package/src/testing/behavioral/staleCommands.test.ts +10 -0
  206. package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
  207. package/src/testing/eval/cases/assembly.ts +13 -6
  208. package/src/testing/eval/cases/effectful.ts +25 -13
  209. package/src/testing/eval/cases/readNouns.ts +22 -20
  210. package/src/testing/eval/cases/stable.ts +5 -1
  211. package/src/testing/eval/eval.test.ts +7 -1
  212. package/src/testing/fixtures/fixtureCapability.ts +45 -1
  213. package/src/testing/fixtures/graph/canonical.ts +6 -3
  214. package/src/testing/fixtures/graph/defaultPack.ts +6 -5
  215. package/src/testing/fixtures/graph/owlExport.ts +7 -5
  216. package/src/testing/helpers/fixtureGraph.ts +33 -16
  217. package/src/testing/helpers/packRuntime.ts +2 -3
  218. package/src/testing/helpers/projectCli.ts +2 -1
  219. package/src/testing/helpers/projectMcp.ts +6 -0
  220. package/src/testing/perf/budgets.test.ts +4 -0
  221. package/src/testing/perf/budgets.ts +22 -7
  222. package/src/testing/perf/globalSetup.ts +121 -6
  223. package/src/types.ts +2 -2
  224. package/src/capabilities/block/blockList.test.ts +0 -79
  225. package/src/capabilities/block/blockList.verb.ts +0 -163
  226. package/src/capabilities/block/index.ts +0 -25
  227. package/src/capabilities/block/pack.ts +0 -166
  228. package/src/capabilities/block/parity.test.ts +0 -187
  229. package/src/capabilities/block/tierChain.ts +0 -78
  230. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  231. package/src/capabilities/modifier/index.ts +0 -15
  232. package/src/capabilities/modifier/pack.ts +0 -64
  233. package/src/capabilities/standard/index.ts +0 -18
  234. package/src/capabilities/standard/pack.ts +0 -128
  235. package/src/capabilities/tier/index.ts +0 -19
  236. package/src/capabilities/tier/lookup.render.test.ts +0 -72
  237. package/src/capabilities/tier/lookup.render.ts +0 -59
  238. package/src/capabilities/tier/lookup.test.ts +0 -83
  239. package/src/capabilities/tier/lookup.verb.ts +0 -44
  240. package/src/capabilities/tier/pack.ts +0 -28
  241. package/src/capabilities/tier/runLookup.ts +0 -87
  242. package/src/capabilities/token/addConfig.render.ts +0 -27
  243. package/src/capabilities/token/addConfig.test.ts +0 -62
  244. package/src/capabilities/token/addConfig.verb.ts +0 -40
  245. package/src/capabilities/token/index.ts +0 -19
  246. package/src/capabilities/token/pack.ts +0 -58
  247. package/src/capabilities/token/runAddConfig.ts +0 -95
  248. package/src/kernel/completion/bashDrive.test.ts +0 -73
  249. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  250. 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
  }
@@ -24,7 +24,7 @@ const BANDED_DATA: DoctorData = {
24
24
  {
25
25
  name: "Shell completions",
26
26
  status: "pass",
27
- detail: "zsh installed and resolving",
27
+ detail: "zsh up to date and resolving",
28
28
  band: "global",
29
29
  },
30
30
  {
@@ -51,12 +51,12 @@ const COLOR_DATA: DoctorData = {
51
51
  checks: [
52
52
  { name: "Node version", status: "pass", detail: "v24" },
53
53
  {
54
- name: "package refs",
54
+ name: "pack refs",
55
55
  status: "fail",
56
- detail: "3 configured, 0 locked",
56
+ detail: "packs are configured but the store has not been built",
57
57
  items: [
58
- { label: "core", status: "fail", detail: "unlocked" },
59
- { label: "ui", status: "fail", detail: "unlocked" },
58
+ { label: "core", status: "fail", detail: "not built" },
59
+ { label: "ui", status: "fail", detail: "not built" },
60
60
  ],
61
61
  remedy: "pragma sources update",
62
62
  },
@@ -151,7 +151,7 @@ describe("doctor render — piped output is ANSI-free (F1)", () => {
151
151
  // The structural, color-free content still renders (glyphs, names, remedy).
152
152
  expect(out).toContain("pragma doctor");
153
153
  expect(out).toContain("✓ Node version");
154
- expect(out).toContain("✗ package refs");
154
+ expect(out).toContain("✗ pack refs");
155
155
  expect(out).toContain("○ Skills symlinked");
156
156
  expect(out).toContain("↳ fix: pragma sources update");
157
157
  expect(out).toContain(" 1 passed · 1 failed · 1 skipped");
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import chalk from "chalk";
14
+ import { BIN_NAME } from "../../constants.js";
14
15
  import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
15
16
  import type { Formatters } from "../../kernel/spec/types.js";
16
17
  import { BAND_LABELS } from "../shared/bands.js";
@@ -134,7 +135,7 @@ export const doctorFormatters: Formatters<DoctorData> = {
134
135
  plain(data) {
135
136
  const style = doctorStyle();
136
137
  const nameWidth = Math.max(...data.checks.map((c) => c.name.length), 0);
137
- const lines: string[] = [style.bold("pragma doctor"), ""];
138
+ const lines: string[] = [style.bold(`${BIN_NAME} doctor`), ""];
138
139
  const { environment, machine, project } = partitionByBand(data.checks);
139
140
  const section = (heading: string, checks: CheckResult[]): void => {
140
141
  if (checks.length === 0) return;
@@ -79,10 +79,9 @@ const defaultLayers: ConfigLayers = {
79
79
  tier: "default",
80
80
  channel: "default",
81
81
  detail: "default",
82
- packages: "default",
82
+ packs: "default",
83
83
  stories: "default",
84
84
  prefixes: "default",
85
- prompts: "default",
86
85
  },
87
86
  global: { path: "", exists: false },
88
87
  project: { exists: false },
@@ -122,12 +121,12 @@ describe("doctor — shape & spread", () => {
122
121
  }
123
122
  // Deterministic under the isolated env.
124
123
  expect(byName(data, "Node version")?.status).toBe("pass");
125
- // The 3 default-config packages are configured but not locked here → an
126
- // attributable fail listing them, with the sources-update remedy.
127
- const pkgRefs = byName(data, "package refs");
128
- expect(pkgRefs?.status).toBe("fail");
129
- expect(pkgRefs?.remedy).toBe("pragma sources update");
130
- expect(pkgRefs?.items?.length).toBe(3);
124
+ // Nothing built, but the packs are the distribution's own — so the embedded
125
+ // snapshot answers reads and the check passes, naming what it is reading.
126
+ const pkgRefs = byName(data, "pack refs");
127
+ expect(pkgRefs?.status).toBe("pass");
128
+ expect(pkgRefs?.detail).toContain("embedded snapshot @ ");
129
+ expect(pkgRefs?.remedy).toBeUndefined();
131
130
  // No harnesses in an empty HOME/cwd — attributable fail + skips.
132
131
  expect(byName(data, "MCP configured")?.status).toBe("fail");
133
132
  expect(byName(data, "Skills symlinked")?.status).toBe("skip");
@@ -137,6 +136,36 @@ describe("doctor — shape & spread", () => {
137
136
  });
138
137
  });
139
138
 
139
+ describe("doctor — the pack-refs check", () => {
140
+ it("a project with its OWN packs and nothing built is an attributable fail", async () => {
141
+ // The install that must never read healthy: `origins.packs` is "project",
142
+ // so the embedded snapshot is a DIFFERENT graph and every read throws
143
+ // STORE_UNAVAILABLE. Doctor has to say so and name the fix, rather than
144
+ // pass by listing packs it is not actually reading from.
145
+ const cwd = tmp("pragma-unbuilt-");
146
+ writeFileSync(
147
+ join(cwd, "pragma.config.ts"),
148
+ 'export default { packs: [{ name: "unbuilt", source: "file:///pragma-never-built" }] };\n',
149
+ );
150
+ const data = await runChecks(bootRuntime(FLAGS, cwd));
151
+ const pkgRefs = byName(data, "pack refs");
152
+ expect(pkgRefs?.status).toBe("fail");
153
+ expect(pkgRefs?.detail).toContain("the store has not been built");
154
+ expect(pkgRefs?.remedy).toBe("pragma sources update");
155
+ });
156
+
157
+ it("a project pointed at its own built pack passes, naming that pack", async () => {
158
+ const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL });
159
+ const data = await runChecks(fixture.runtime);
160
+ const pkgRefs = byName(data, "pack refs");
161
+ expect(pkgRefs?.status).toBe("pass");
162
+ // The fixture pack's own provenance label, not the embedded snapshot's.
163
+ expect(pkgRefs?.detail).toContain("fixture");
164
+ expect(pkgRefs?.detail).not.toContain("embedded snapshot");
165
+ await fixture.dispose();
166
+ });
167
+ });
168
+
140
169
  describe("doctor — the store check", () => {
141
170
  it("a store that fails to boot is an attributable fail, not a crash", async () => {
142
171
  const data = await runChecks(throwingStoreRuntime(tmp("pragma-proj-")));
@@ -12,6 +12,7 @@
12
12
  * `doctor-exit-zero-with-failures`.
13
13
  */
14
14
 
15
+ import { BIN_NAME } from "../../constants.js";
15
16
  import { asVerb } from "../../kernel/spec/asVerb.js";
16
17
  import type { CapabilityModule, VerbSpec } from "../../kernel/spec/types.js";
17
18
  import { doctorFormatters } from "./doctor.render.js";
@@ -24,8 +25,11 @@ const doctorVerb: VerbSpec<Record<string, unknown>, DoctorData> = {
24
25
  params: [],
25
26
  output: { formatters: doctorFormatters },
26
27
  examples: [
27
- { cmd: "pragma doctor" },
28
- { cmd: "pragma doctor --format json", note: "machine-readable checks" },
28
+ { cmd: `${BIN_NAME} doctor` },
29
+ {
30
+ cmd: `${BIN_NAME} doctor --format json`,
31
+ note: "machine-readable checks",
32
+ },
29
33
  ],
30
34
  capability: {
31
35
  needsStore: false,
@@ -8,6 +8,7 @@
8
8
  * neither the checks nor `@canonical/harnesses` land on the fast path.
9
9
  */
10
10
 
11
+ import { BIN_NAME } from "../../constants.js";
11
12
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
12
13
  import { checkConfigFile } from "./checks/checkConfigFile.js";
13
14
  import { checkKeStore } from "./checks/checkKeStore.js";
@@ -55,9 +56,9 @@ function buildChecks(
55
56
  ): readonly [string, Promise<CheckResult>][] {
56
57
  return [
57
58
  ["Node version", checkNodeVersion()],
58
- ["pragma version", checkPragmaVersion(rt)],
59
- ["pragma config", checkConfigFile(rt)],
60
- ["package refs", checkPackageRefs(rt)],
59
+ [`${BIN_NAME} version`, checkPragmaVersion(rt)],
60
+ [`${BIN_NAME} config`, checkConfigFile(rt)],
61
+ ["pack refs", checkPackageRefs(rt)],
61
62
  ["ke store", checkKeStore(rt)],
62
63
  ["Shell completions", checkShellCompletions(rt.cwd)],
63
64
  ["MCP configured", checkMcpConfigured(rt.cwd)],
@@ -84,8 +85,7 @@ export async function runChecks(rt: PragmaRuntime): Promise<DoctorData> {
84
85
  name,
85
86
  status: "fail",
86
87
  detail: `Check threw an unexpected error: ${reason}`,
87
- remedy:
88
- "Re-run `pragma doctor`; if it persists, report this as a bug.",
88
+ remedy: `Re-run \`${BIN_NAME} doctor\`; if it persists, report this as a bug.`,
89
89
  });
90
90
  }
91
91
  }),