@canonical/pragma-cli 0.36.0 → 0.38.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 (312) hide show
  1. package/README.md +3 -2
  2. package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
  3. package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
  4. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
  5. package/bundled-skills/anatomy-author/SKILL.md +840 -137
  6. package/bundled-skills/design-auditor/SKILL.md +133 -9
  7. package/bundled-skills/implement-component/SKILL.md +192 -23
  8. package/bundled-skills/specify-component/SKILL.md +124 -12
  9. package/bundled-skills/specify-pattern/SKILL.md +47 -8
  10. package/dist/package.json +23 -20
  11. package/dist/pragma.conf.js +963 -40
  12. package/dist/pragma.conf.js.map +1 -1
  13. package/dist/src/capabilities/capabilities/catalog.js +14 -5
  14. package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
  15. package/dist/src/capabilities/capabilities/hints.js +33 -5
  16. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  17. package/dist/src/capabilities/config/show.render.js +30 -2
  18. package/dist/src/capabilities/config/show.render.js.map +1 -1
  19. package/dist/src/capabilities/create/create.verb.js +2 -1
  20. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  21. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  22. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  23. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  24. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  25. package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
  26. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  28. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  29. package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
  30. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  31. package/dist/src/capabilities/graph/connect.render.js +101 -0
  32. package/dist/src/capabilities/graph/connect.render.js.map +1 -0
  33. package/dist/src/capabilities/graph/connect.types.js +21 -0
  34. package/dist/src/capabilities/graph/connect.types.js.map +1 -0
  35. package/dist/src/capabilities/graph/connect.verb.js +147 -0
  36. package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
  37. package/dist/src/capabilities/graph/index.js +5 -3
  38. package/dist/src/capabilities/graph/index.js.map +1 -1
  39. package/dist/src/capabilities/graph/queryText.js +206 -0
  40. package/dist/src/capabilities/graph/queryText.js.map +1 -0
  41. package/dist/src/capabilities/graph/relationIndex.js +302 -0
  42. package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
  43. package/dist/src/capabilities/graph/runConnect.js +456 -0
  44. package/dist/src/capabilities/graph/runConnect.js.map +1 -0
  45. package/dist/src/capabilities/graph/runQuery.js +21 -4
  46. package/dist/src/capabilities/graph/runQuery.js.map +1 -1
  47. package/dist/src/capabilities/setup/buildPlan.js +6 -2
  48. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  49. package/dist/src/capabilities/setup/operations/index.js +7 -2
  50. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  51. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  52. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  53. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  54. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  55. package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
  56. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  57. package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
  58. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
  60. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/writability.js +114 -0
  64. package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
  65. package/dist/src/capabilities/setup/plan.js.map +1 -1
  66. package/dist/src/capabilities/setup/setup.verb.js +80 -31
  67. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  68. package/dist/src/capabilities/setup/targets.js +124 -42
  69. package/dist/src/capabilities/setup/targets.js.map +1 -1
  70. package/dist/src/capabilities/shared/index.js +1 -1
  71. package/dist/src/capabilities/shared/index.js.map +1 -1
  72. package/dist/src/constants.js +7 -1
  73. package/dist/src/constants.js.map +1 -1
  74. package/dist/src/kernel/error/renderError.js +3 -2
  75. package/dist/src/kernel/error/renderError.js.map +1 -1
  76. package/dist/src/kernel/error/validOptions.js +41 -0
  77. package/dist/src/kernel/error/validOptions.js.map +1 -0
  78. package/dist/src/kernel/packs/compile.js +90 -6
  79. package/dist/src/kernel/packs/compile.js.map +1 -1
  80. package/dist/src/kernel/packs/cursor.js +135 -0
  81. package/dist/src/kernel/packs/cursor.js.map +1 -0
  82. package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
  83. package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
  84. package/dist/src/kernel/packs/paging.js +108 -0
  85. package/dist/src/kernel/packs/paging.js.map +1 -0
  86. package/dist/src/kernel/packs/renderPack.js +154 -5
  87. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  88. package/dist/src/kernel/packs/resolveEntity.js +118 -19
  89. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  90. package/dist/src/kernel/packs/runBodies.js +156 -18
  91. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  92. package/dist/src/kernel/packs/schema.js +160 -12
  93. package/dist/src/kernel/packs/schema.js.map +1 -1
  94. package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
  95. package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
  96. package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
  97. package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
  98. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
  99. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  100. package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
  101. package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
  102. package/dist/src/kernel/packs/sparql/index.js +24 -14
  103. package/dist/src/kernel/packs/sparql/index.js.map +1 -1
  104. package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
  105. package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
  106. package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
  107. package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
  108. package/dist/src/kernel/packs/storyRules.js +151 -0
  109. package/dist/src/kernel/packs/storyRules.js.map +1 -0
  110. package/dist/src/kernel/packs/tierScope.js +310 -0
  111. package/dist/src/kernel/packs/tierScope.js.map +1 -0
  112. package/dist/src/kernel/packs/types.js +80 -0
  113. package/dist/src/kernel/packs/types.js.map +1 -1
  114. package/dist/src/kernel/project/cli/buildProgram.js +10 -1
  115. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  116. package/dist/src/kernel/project/cli/dispatch.js +104 -3
  117. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  118. package/dist/src/kernel/project/cli/rootHelp.js +8 -5
  119. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  120. package/dist/src/kernel/project/cli/suggestNames.js +31 -7
  121. package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
  122. package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
  123. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  124. package/dist/src/kernel/render/renderers.js +34 -2
  125. package/dist/src/kernel/render/renderers.js.map +1 -1
  126. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  127. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  128. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  129. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  130. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  131. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  132. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  133. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  134. package/dist/src/kernel/runtime/graphpack/read.js +17 -10
  135. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  136. package/dist/src/kernel/runtime/loadSession.js +3 -1
  137. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  138. package/dist/src/kernel/runtime/probe.js +1 -1
  139. package/dist/src/kernel/runtime/probe.js.map +1 -1
  140. package/dist/src/kernel/runtime/readEntity.js +19 -4
  141. package/dist/src/kernel/runtime/readEntity.js.map +1 -1
  142. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  143. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  144. package/dist/src/kernel/runtime/refs/resolve.js +38 -5
  145. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  146. package/dist/src/kernel/spec/emitReference.js +1 -1
  147. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  148. package/package.json +23 -20
  149. package/pragma.conf.ts +983 -40
  150. package/src/capabilities/block.parity.test.ts +207 -27
  151. package/src/capabilities/block.shipped.exec.test.ts +156 -14
  152. package/src/capabilities/block.tierRank.exec.test.ts +46 -9
  153. package/src/capabilities/capabilities/catalog.ts +14 -5
  154. package/src/capabilities/capabilities/hints.ts +40 -5
  155. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  156. package/src/capabilities/config/set.test.ts +24 -25
  157. package/src/capabilities/config/show.render.test.ts +9 -4
  158. package/src/capabilities/config/show.render.ts +30 -2
  159. package/src/capabilities/create/byteEquality.test.ts +28 -1
  160. package/src/capabilities/create/create.test.ts +1 -0
  161. package/src/capabilities/create/create.verb.ts +2 -1
  162. package/src/capabilities/create/createGrammar.test.ts +4 -1
  163. package/src/capabilities/create/createSurface.generated.ts +23 -1
  164. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  165. package/src/capabilities/distribution.parity.test.ts +12 -11
  166. package/src/capabilities/distribution.test.ts +77 -7
  167. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  168. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  169. package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
  170. package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
  171. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  172. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  173. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  174. package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
  175. package/src/capabilities/doctor/doctor.test.ts +327 -36
  176. package/src/capabilities/emptyRecovery.test.ts +84 -28
  177. package/src/capabilities/graph/connect.render.ts +141 -0
  178. package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
  179. package/src/capabilities/graph/connect.test.ts +361 -0
  180. package/src/capabilities/graph/connect.types.ts +136 -0
  181. package/src/capabilities/graph/connect.verb.ts +165 -0
  182. package/src/capabilities/graph/index.ts +5 -3
  183. package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
  184. package/src/capabilities/graph/query.test.ts +17 -0
  185. package/src/capabilities/graph/queryText.test.ts +121 -0
  186. package/src/capabilities/graph/queryText.ts +240 -0
  187. package/src/capabilities/graph/relationIndex.ts +372 -0
  188. package/src/capabilities/graph/runConnect.ts +577 -0
  189. package/src/capabilities/graph/runQuery.ts +21 -6
  190. package/src/capabilities/lazy.test.ts +32 -2
  191. package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
  192. package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
  193. package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
  194. package/src/capabilities/resources/resources.test.ts +38 -2
  195. package/src/capabilities/setup/buildPlan.ts +7 -1
  196. package/src/capabilities/setup/operations/index.ts +14 -0
  197. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  198. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  199. package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
  200. package/src/capabilities/setup/operations/setupLsp.ts +544 -85
  201. package/src/capabilities/setup/operations/setupMcp.ts +212 -4
  202. package/src/capabilities/setup/operations/setupSkills.ts +7 -2
  203. package/src/capabilities/setup/operations/writability.test.ts +151 -0
  204. package/src/capabilities/setup/operations/writability.ts +135 -0
  205. package/src/capabilities/setup/plan.ts +11 -0
  206. package/src/capabilities/setup/setup.test.ts +992 -16
  207. package/src/capabilities/setup/setup.verb.ts +84 -36
  208. package/src/capabilities/setup/targets.ts +152 -48
  209. package/src/capabilities/setup/types.ts +41 -5
  210. package/src/capabilities/shared/index.ts +1 -0
  211. package/src/capabilities/sources/sources.test.ts +55 -0
  212. package/src/capabilities/surface.test.ts +59 -11
  213. package/src/capabilities/tier.parity.test.ts +2 -2
  214. package/src/capabilities/token.parity.test.ts +784 -0
  215. package/src/capabilities/tokenGraph.cli.test.ts +279 -0
  216. package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
  217. package/src/capabilities/variable.parity.test.ts +448 -0
  218. package/src/constants.ts +8 -0
  219. package/src/kernel/completion/shellDrive.test.ts +32 -30
  220. package/src/kernel/config/defaults.test.ts +21 -2
  221. package/src/kernel/config/readConfig.test.ts +1 -1
  222. package/src/kernel/error/renderError.ts +3 -2
  223. package/src/kernel/error/validOptions.test.ts +57 -0
  224. package/src/kernel/error/validOptions.ts +45 -0
  225. package/src/kernel/packs/collect.test.ts +2 -0
  226. package/src/kernel/packs/compile.test.ts +188 -15
  227. package/src/kernel/packs/compile.ts +109 -14
  228. package/src/kernel/packs/cursor.ts +135 -0
  229. package/src/kernel/packs/filteredEmpty.test.ts +213 -0
  230. package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
  231. package/src/kernel/packs/graphqlSource.test.ts +152 -0
  232. package/src/kernel/packs/listNotice.test.ts +156 -0
  233. package/src/kernel/packs/paging.ts +110 -0
  234. package/src/kernel/packs/renderPack.ts +186 -13
  235. package/src/kernel/packs/resolveEntity.test.ts +62 -3
  236. package/src/kernel/packs/resolveEntity.ts +186 -26
  237. package/src/kernel/packs/runBodies.ts +196 -26
  238. package/src/kernel/packs/schema.ts +180 -13
  239. package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
  240. package/src/kernel/packs/sparql/authorQuery.ts +257 -0
  241. package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
  242. package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
  243. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
  244. package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
  245. package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
  246. package/src/kernel/packs/sparql/filterValues.ts +182 -0
  247. package/src/kernel/packs/sparql/index.ts +30 -14
  248. package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
  249. package/src/kernel/packs/sparql/runSelect.ts +42 -2
  250. package/src/kernel/packs/sparql/searchTerm.ts +29 -0
  251. package/src/kernel/packs/storyRules.ts +180 -0
  252. package/src/kernel/packs/tierScope.test.ts +487 -0
  253. package/src/kernel/packs/tierScope.ts +409 -0
  254. package/src/kernel/packs/toolDescription.test.ts +13 -10
  255. package/src/kernel/packs/types.ts +311 -9
  256. package/src/kernel/project/cli/buildProgram.ts +17 -1
  257. package/src/kernel/project/cli/dispatch.test.ts +87 -0
  258. package/src/kernel/project/cli/dispatch.ts +114 -4
  259. package/src/kernel/project/cli/rootHelp.ts +8 -5
  260. package/src/kernel/project/cli/suggest.test.ts +35 -0
  261. package/src/kernel/project/cli/suggestNames.ts +34 -7
  262. package/src/kernel/project/mcp/registerVerb.ts +5 -1
  263. package/src/kernel/render/contracts.ts +2 -0
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.test.ts +43 -0
  266. package/src/kernel/render/renderers.ts +34 -1
  267. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  268. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  269. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  270. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  271. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  272. package/src/kernel/runtime/graphpack/read.ts +17 -13
  273. package/src/kernel/runtime/loadSession.ts +3 -1
  274. package/src/kernel/runtime/probe.ts +1 -1
  275. package/src/kernel/runtime/readEntity.ts +19 -4
  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 +39 -5
  279. package/src/kernel/runtime/types.ts +30 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  282. package/src/kernel/spec/types.ts +17 -0
  283. package/src/testing/PARITY_GAPS.ts +4 -4
  284. package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
  285. package/src/testing/behavioral/completion.test.ts +7 -6
  286. package/src/testing/behavioral/disclosure.test.ts +39 -37
  287. package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
  288. package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
  289. package/src/testing/behavioral/journeys.cli.test.ts +71 -32
  290. package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
  291. package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
  292. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
  293. package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
  294. package/src/testing/behavioral/mcpSurface.test.ts +28 -25
  295. package/src/testing/behavioral/parity.test.ts +25 -23
  296. package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
  297. package/src/testing/eval/cases/effectful.ts +7 -1
  298. package/src/testing/eval/cases/readNouns.ts +100 -11
  299. package/src/testing/fixtures/blockGraph.ts +89 -5
  300. package/src/testing/fixtures/connectGraph.ts +159 -0
  301. package/src/testing/fixtures/graph/canonical.ts +128 -32
  302. package/src/testing/helpers/fsProbe.ts +88 -0
  303. package/src/testing/helpers/runCli.test.ts +44 -0
  304. package/src/testing/helpers/runCli.ts +27 -3
  305. package/src/testing/perf/budgets.test.ts +3 -1
  306. package/src/testing/perf/budgets.ts +138 -54
  307. package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
  308. package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
  309. package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
  310. package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
  311. package/src/kernel/packs/sparql/applyFilters.ts +0 -203
  312. package/src/kernel/packs/sparql/applySearch.ts +0 -33
@@ -33,7 +33,10 @@ import {
33
33
  CANONICAL_TTL,
34
34
  } from "../../testing/fixtures/graph/canonical.js";
35
35
  import { bootFixtureRuntime } from "../../testing/helpers/fixtureGraph.js";
36
+ import { storeProbe } from "../../testing/helpers/fsProbe.js";
36
37
  import { projectMcp } from "../../testing/helpers/projectMcp.js";
38
+ import type { FsProbe } from "../setup/operations/writability.js";
39
+ import { checkPackageRefs } from "./checks/checkPackageRefs.js";
37
40
  import { scopedChecks } from "./checks/targetHealth.js";
38
41
  import { doctorModule } from "./index.js";
39
42
  import { runChecks } from "./runChecks.js";
@@ -119,7 +122,15 @@ const byName = (data: DoctorData, name: string) =>
119
122
 
120
123
  describe("doctor — shape & spread", () => {
121
124
  it("returns the environment checks plus one row per scoped target", async () => {
122
- const data = await runChecks(bootRuntime(FLAGS, tmp("pragma-proj-")));
125
+ // A fixture pack, not the distribution's own: the shape under test is the
126
+ // row table, and the store check booting the embedded snapshot cold costs
127
+ // three seconds on an idle machine (65,000 triples) and more than the
128
+ // five-second budget when every package's suite runs at once, which is how
129
+ // the release workflow runs them. What the embedded snapshot proves — that
130
+ // a first install's pack refs pass naming it — has its own case below,
131
+ // through the check that never boots the store.
132
+ const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL });
133
+ const data = await runChecks(fixture.runtime);
123
134
  // Four unscoped environment checks, then the target table in both scopes
124
135
  // (all five targets are global, mcp and skills are also project) = 11,
125
136
  // plus the two `harnesses` inventory rows, one per scope = 13.
@@ -130,24 +141,6 @@ describe("doctor — shape & spread", () => {
130
141
  }
131
142
  // Deterministic under the isolated env.
132
143
  expect(byName(data, "Node version")?.status).toBe("pass");
133
- // Nothing built, but the packs are the distribution's own — so the embedded
134
- // snapshot answers reads and the check passes, naming what it is reading.
135
- const pkgRefs = byName(data, "pack refs");
136
- expect(pkgRefs?.status).toBe("pass");
137
- expect(pkgRefs?.detail).toContain("shipped with the CLI");
138
- expect(pkgRefs?.remedy).toBeUndefined();
139
- // Provenance is one ITEM PER PACK, not a comma-joined string in the
140
- // headline: four packs and two forty-character SHAs on one line was
141
- // unreadable, and every other multi-part check here already uses items.
142
- // The headline counts them; the items say which revision each one is.
143
- expect(pkgRefs?.detail).toContain("packs");
144
- expect((pkgRefs?.items?.length ?? 0) > 1).toBe(true);
145
- for (const item of pkgRefs?.items ?? []) {
146
- expect(item.label).not.toContain(",");
147
- // A git hash is cut to seven — the length every other tool in this
148
- // workflow prints, and short enough that four rows stay scannable.
149
- expect(item.detail ?? "").not.toMatch(/[0-9a-f]{40}/);
150
- }
151
144
 
152
145
  // The scoped rows carry the TARGET IDS verbatim — `mcp`, not "MCP
153
146
  // configured" — because the row name IS the fix command's argument. The
@@ -188,10 +181,37 @@ describe("doctor — shape & spread", () => {
188
181
  // No global config in the isolated XDG — an opt-in that is not set up yet.
189
182
  expect(byName(data, "config")?.status).toBe("available");
190
183
  expect(byName(data, "config")?.remedy).toBe("pragma setup config");
184
+ await fixture.dispose();
191
185
  });
192
186
  });
193
187
 
194
188
  describe("doctor — the pack-refs check", () => {
189
+ it("a first install reads the distribution's own packs, and says so", async () => {
190
+ // Nothing built, but the packs are the distribution's own — so the embedded
191
+ // snapshot answers reads and the check passes, naming what it is reading.
192
+ // The check alone, not the whole doctor: pack refs never boot the store, and
193
+ // booting the embedded snapshot cold is the one expensive thing a doctor
194
+ // run does.
195
+ const pkgRefs = await checkPackageRefs(
196
+ bootRuntime(FLAGS, tmp("pragma-proj-")),
197
+ );
198
+ expect(pkgRefs.status).toBe("pass");
199
+ expect(pkgRefs.detail).toContain("shipped with the CLI");
200
+ expect(pkgRefs.remedy).toBeUndefined();
201
+ // Provenance is one ITEM PER PACK, not a comma-joined string in the
202
+ // headline: four packs and two forty-character SHAs on one line was
203
+ // unreadable, and every other multi-part check here already uses items.
204
+ // The headline counts them; the items say which revision each one is.
205
+ expect(pkgRefs.detail).toContain("packs");
206
+ expect((pkgRefs.items?.length ?? 0) > 1).toBe(true);
207
+ for (const item of pkgRefs.items ?? []) {
208
+ expect(item.label).not.toContain(",");
209
+ // A git hash is cut to seven — the length every other tool in this
210
+ // workflow prints, and short enough that four rows stay scannable.
211
+ expect(item.detail ?? "").not.toMatch(/[0-9a-f]{40}/);
212
+ }
213
+ });
214
+
195
215
  it("a project with its OWN packs and nothing built is an attributable fail", async () => {
196
216
  // The install that must never read healthy: `origins.packs` is "project",
197
217
  // so the embedded snapshot is a DIFFERENT graph and every read throws
@@ -249,17 +269,20 @@ describe("doctor — the store check", () => {
249
269
 
250
270
  describe("doctor — dispatch & MCP", () => {
251
271
  it("exits 0 even when checks fail (failures live in the envelope)", async () => {
252
- const outcome = await executeVerb(
253
- doctorVerb,
254
- {},
255
- NO_MUT,
256
- bootRuntime(FLAGS, tmp("pragma-proj-")),
257
- );
272
+ // On the fixture pack, for the same reason the shape case is: a whole doctor
273
+ // run boots the store, and the embedded snapshot is the expensive one.
274
+ const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL });
275
+ const outcome = await executeVerb(doctorVerb, {}, NO_MUT, fixture.runtime);
258
276
  expect(outcome.exitCode).toBe(0);
277
+ await fixture.dispose();
259
278
  });
260
279
 
261
280
  it("MCP doctor is read-only and returns the checks envelope", async () => {
262
- const mcp = await projectMcp([doctorModule], tmp("pragma-proj-"));
281
+ // The MCP boots its own runtime from the cwd; the fixture's cwd carries a
282
+ // config that points at the built fixture pack, so the embedded snapshot is
283
+ // never parsed here either.
284
+ const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL });
285
+ const mcp = await projectMcp([doctorModule], fixture.cwd);
263
286
  const tools = await mcp.listTools();
264
287
  const doctorTool = tools.find((t) => t.name === "doctor");
265
288
  expect(
@@ -271,6 +294,7 @@ describe("doctor — dispatch & MCP", () => {
271
294
  await mcp.cleanup();
272
295
  expect(envelope.ok).toBe(true);
273
296
  expect((envelope.data as DoctorData).checks).toHaveLength(13);
297
+ await fixture.dispose();
274
298
  });
275
299
  });
276
300
 
@@ -334,10 +358,45 @@ describe("doctor — the harness inventory", () => {
334
358
  expect(project?.items?.[0]?.detail).toContain("detected, not registered");
335
359
  expect(project?.items?.[0]?.detail).toContain(".vscode/mcp.json");
336
360
 
337
- // Nothing detected in the global scope, and that is a skip — not a fault.
361
+ // The GLOBAL scope holds nothing. VS Code is dual-scope now, so the row
362
+ // CAN write a per-user file — but a committed `.vscode/` is evidence about
363
+ // this repository, not about this machine, and the global band has to be
364
+ // earned by a user-level signal. Without one there is no global location
365
+ // to report and nothing for `setup mcp` to do there.
338
366
  expect(global?.status).toBe("skip");
339
- expect(global?.detail).toBe("no harnesses detected");
340
- expect(global?.items).toBeUndefined();
367
+ expect(global?.items ?? []).toEqual([]);
368
+ const globalMcp = rows.find(
369
+ (r) => r.name === "mcp" && r.scope === "global",
370
+ );
371
+ expect(globalMcp?.status).toBe("skip");
372
+ });
373
+
374
+ it("holds the global hit once a USER-LEVEL signal has earned it", async () => {
375
+ // The same repository, on a machine that really has VS Code: its per-user
376
+ // directory exists, so the global band is earned and the row reports the
377
+ // per-user `mcp.json` it would write — detected, not registered — with the
378
+ // `mcp` row beside it naming the command that settles it.
379
+ const cwd = tmp("pragma-doctor-proj-");
380
+ mkdirSync(join(cwd, ".vscode"), { recursive: true });
381
+ mkdirSync(join(process.env.XDG_CONFIG_HOME as string, "Code", "User"), {
382
+ recursive: true,
383
+ });
384
+
385
+ const rows = await scopedChecks(bootRuntime(FLAGS, cwd), "pragma");
386
+ const { global } = inventory(rows);
387
+
388
+ expect(global?.status).toBe("pass");
389
+ expect(global?.detail).toBe("1 detected · 0 registered");
390
+ expect(global?.items?.map((i) => i.label)).toEqual(["VS Code"]);
391
+ expect(global?.items?.[0]?.status).toBe("available");
392
+ expect(global?.items?.[0]?.detail).toContain("detected, not registered");
393
+ expect(global?.items?.[0]?.detail).toContain("Code/User/mcp.json");
394
+
395
+ const globalMcp = rows.find(
396
+ (r) => r.name === "mcp" && r.scope === "global",
397
+ );
398
+ expect(globalMcp?.status).toBe("available");
399
+ expect(globalMcp?.remedy).toBe("pragma setup mcp");
341
400
  });
342
401
 
343
402
  it("--verbose lists every registry harness, undetected ones as a NON-failing skip", async () => {
@@ -348,9 +407,9 @@ describe("doctor — the harness inventory", () => {
348
407
  const { global, project } = inventory(rows);
349
408
 
350
409
  // Every harness the registry knows, in both scopes.
351
- expect(project?.items).toHaveLength(13);
352
- expect(global?.items).toHaveLength(13);
353
- expect(project?.detail).toBe("1 detected · 0 registered · 13 known");
410
+ expect(project?.items).toHaveLength(16);
411
+ expect(global?.items).toHaveLength(16);
412
+ expect(project?.detail).toBe("1 detected · 0 registered · 16 known");
354
413
 
355
414
  // `types.ts` forbids inflating the failure count: a machine that simply
356
415
  // does not have Cursor is not a broken machine.
@@ -368,11 +427,15 @@ describe("doctor — the harness inventory", () => {
368
427
  });
369
428
 
370
429
  it("says where a harness DOES keep its config, rather than omitting it", async () => {
371
- // `vscode` is `scope: "project"`, so it can NEVER carry a global entry. A
430
+ // `roo-code` is `scope: "project"`, so it can NEVER carry a global entry. A
372
431
  // verbose global listing that silently dropped it would read as a bug in
373
432
  // the listing; the row says why instead. The mirror case is Windsurf, which
374
433
  // is `scope: "global"` and keeps nothing per project.
375
434
  //
435
+ // VS Code used to be this example and stopped being one when its per-user
436
+ // `mcp.json` was added: it is `scope: "both"` now, and asserting otherwise
437
+ // would pin a fact the registry no longer holds.
438
+ //
376
439
  // The sentence states the FACT, not the partition: `no Global band` named
377
440
  // an internal word for the answer instead of giving it.
378
441
  const rows = await scopedChecks(
@@ -381,11 +444,11 @@ describe("doctor — the harness inventory", () => {
381
444
  );
382
445
  const { global, project } = inventory(rows);
383
446
 
384
- const vscodeGlobal = global?.items?.find((i) => i.label === "VS Code");
385
- expect(vscodeGlobal?.detail).toBe(
447
+ const rooGlobal = global?.items?.find((i) => i.label === "Roo Code");
448
+ expect(rooGlobal?.detail).toBe(
386
449
  "keeps no global config — it is per-project only",
387
450
  );
388
- expect(vscodeGlobal?.status).toBe("skip");
451
+ expect(rooGlobal?.status).toBe("skip");
389
452
 
390
453
  const windsurfProject = project?.items?.find((i) => i.label === "Windsurf");
391
454
  expect(windsurfProject?.detail).toBe(
@@ -537,3 +600,231 @@ describe("doctor — an unconfigured opt-in integration is available, not a faul
537
600
  expect(check?.remedy).toBe("pragma setup mcp");
538
601
  });
539
602
  });
603
+
604
+ /**
605
+ * The `lsp` row's per-editor provenance, and the rows that say a step was
606
+ * skipped rather than failing at it.
607
+ *
608
+ * Per the owner, 2026-09-16: doctor says per editor HOW it was found and WHY a
609
+ * step was skipped. A colleague whose macOS run reported no editor had VS Code
610
+ * installed the whole time, and a one-line row could not tell them whether
611
+ * pragma had missed the editor or found it and could not act.
612
+ *
613
+ * The blocked arms read a filesystem no CI host has, so they are driven over
614
+ * `scopedChecks`'s own probe seam — the same entry point `doctor` runs, with
615
+ * the filesystem answer injected, rather than the row bodies opened up as
616
+ * exports nothing in production would call.
617
+ */
618
+ describe("doctor — the lsp row's per-editor provenance", () => {
619
+ /** The `lsp` row of a report, optionally over an injected filesystem. */
620
+ const lspRow = async (probe?: FsProbe) => {
621
+ const rows = await scopedChecks(
622
+ bootRuntime(FLAGS, tmp("pragma-doctor-proj-")),
623
+ "pragma",
624
+ probe,
625
+ );
626
+ return rows.find((r) => r.name === "lsp");
627
+ };
628
+
629
+ it("names one item per editor, with `via PATH` and its standing", async () => {
630
+ const stubDir = tmp("pragma-doctor-editors-");
631
+ writeFileSync(join(stubDir, "codium"), "");
632
+ process.env.PATH = stubDir;
633
+
634
+ const lsp = await lspRow();
635
+ // Nothing installed yet, so the row is the optional integration it is —
636
+ // and its derived fix keeps the "every available row has a remedy" rule.
637
+ expect(lsp?.status).toBe("available");
638
+ expect(lsp?.remedy).toBe("pragma setup lsp");
639
+ expect(lsp?.items?.map((i) => i.label)).toEqual(["VSCodium"]);
640
+ expect(lsp?.items?.[0]?.status).toBe("available");
641
+ expect(lsp?.items?.[0]?.detail).toContain("via PATH");
642
+ expect(lsp?.items?.[0]?.detail).toContain(join(stubDir, "codium"));
643
+ expect(lsp?.items?.[0]?.detail).toContain("not installed");
644
+ });
645
+
646
+ it("reports an editor found only by its user directory as a skipped item", async () => {
647
+ // Installed, and nothing to install WITH. The item says both.
648
+ mkdirSync(join(process.env.XDG_CONFIG_HOME as string, "Code", "User"), {
649
+ recursive: true,
650
+ });
651
+
652
+ const lsp = await lspRow();
653
+ expect(lsp?.status).toBe("skip");
654
+ expect(lsp?.detail).toContain("no command-line launcher");
655
+ expect(lsp?.items?.[0]?.label).toBe("VS Code");
656
+ expect(lsp?.items?.[0]?.status).toBe("skip");
657
+ expect(lsp?.items?.[0]?.detail).toContain("via user directory");
658
+ // The remedy is the editor's own palette command — NOT the derived
659
+ // `pragma setup lsp`, which would only reproduce the skip.
660
+ expect(lsp?.remedy).toContain("Install 'code' command in PATH");
661
+ expect(lsp?.remedy).not.toContain("\n");
662
+ });
663
+
664
+ it("a store-managed extensions folder is a skip carrying the Nix declaration", async () => {
665
+ const stubDir = tmp("pragma-doctor-nix-path-");
666
+ writeFileSync(join(stubDir, "codium"), "");
667
+ process.env.PATH = stubDir;
668
+ const extensions = join(
669
+ process.env.HOME as string,
670
+ ".vscode-oss",
671
+ "extensions",
672
+ );
673
+ mkdirSync(extensions, { recursive: true });
674
+
675
+ const lsp = await lspRow(storeProbe(extensions, "9z8y7x-vscodium"));
676
+ expect(lsp?.status).toBe("skip");
677
+ expect(lsp?.detail).toContain("managed by Nix");
678
+ expect(lsp?.items?.[0]?.status).toBe("skip");
679
+ expect(lsp?.items?.[0]?.detail).toContain("via PATH");
680
+ expect(lsp?.items?.[0]?.detail).toContain("managed by Nix");
681
+ // There is no marketplace listing, so the line gives both halves a
682
+ // home-manager user needs: where the VSIX comes from, and how to declare it.
683
+ expect(lsp?.remedy).toContain("buildVscodeExtension");
684
+ expect(lsp?.remedy).toContain("terrazzo-lsp.vsix");
685
+ expect(lsp?.remedy).not.toContain("\n");
686
+ });
687
+
688
+ it("a blocked editor beside an installable one keeps the row available", async () => {
689
+ // The invariant the row must not break: any editor that CAN be installed
690
+ // into makes the row `available`, whose derived fix is the setup command.
691
+ // A blocked sibling is reported as its own skipped item, not as the row's
692
+ // verdict.
693
+ const stubDir = tmp("pragma-doctor-mixed-path-");
694
+ writeFileSync(join(stubDir, "code"), "");
695
+ writeFileSync(join(stubDir, "codium"), "");
696
+ process.env.PATH = stubDir;
697
+ const extensions = join(
698
+ process.env.HOME as string,
699
+ ".vscode-oss",
700
+ "extensions",
701
+ );
702
+ mkdirSync(extensions, { recursive: true });
703
+
704
+ const lsp = await lspRow(storeProbe(extensions, "9z8y7x-vscodium"));
705
+ expect(lsp?.status).toBe("available");
706
+ expect(lsp?.detail).toBe("not installed in VS Code");
707
+ expect(lsp?.items?.map((i) => [i.label, i.status])).toEqual([
708
+ ["VS Code", "available"],
709
+ ["VSCodium", "skip"],
710
+ ]);
711
+ // The derived fix stands, because there is still something to install.
712
+ expect(lsp?.remedy).toBe("pragma setup lsp");
713
+ });
714
+
715
+ it("reports an installed editor as a pass, provenance included", async () => {
716
+ const stubDir = tmp("pragma-doctor-installed-path-");
717
+ writeFileSync(join(stubDir, "codium"), "");
718
+ process.env.PATH = stubDir;
719
+ mkdirSync(
720
+ join(
721
+ process.env.HOME as string,
722
+ ".vscode-oss",
723
+ "extensions",
724
+ "canonical.terrazzo-lsp-extension-1.2.3",
725
+ ),
726
+ { recursive: true },
727
+ );
728
+
729
+ const lsp = await lspRow();
730
+ expect(lsp?.status).toBe("pass");
731
+ expect(lsp?.detail).toBe("installed in VSCodium");
732
+ expect(lsp?.items?.[0]?.status).toBe("pass");
733
+ expect(lsp?.items?.[0]?.detail).toContain("via PATH");
734
+ expect(lsp?.items?.[0]?.detail).toContain("· installed");
735
+ });
736
+
737
+ it("an INSTALLED editor in a blocked folder is a pass, not a skip", async () => {
738
+ // A site-managed (or Nix-managed) extensions folder that ALREADY holds the
739
+ // extension is a working machine. Reporting a skip there told its owner to
740
+ // chmod the folder and install an extension they already have, and made
741
+ // the row a permanent `○ lsp` nothing could clear.
742
+ const stubDir = tmp("pragma-doctor-blocked-installed-");
743
+ writeFileSync(join(stubDir, "codium"), "");
744
+ process.env.PATH = stubDir;
745
+ const extensions = join(
746
+ process.env.HOME as string,
747
+ ".vscode-oss",
748
+ "extensions",
749
+ );
750
+ mkdirSync(join(extensions, "canonical.terrazzo-lsp-extension-1.2.3"), {
751
+ recursive: true,
752
+ });
753
+
754
+ const lsp = await lspRow(storeProbe(extensions, "9z8y7x-vscodium"));
755
+ expect(lsp?.status).toBe("pass");
756
+ expect(lsp?.detail).toBe("installed in VSCodium");
757
+ expect(lsp?.items?.[0]?.status).toBe("pass");
758
+ expect(lsp?.items?.[0]?.detail).toContain("· installed");
759
+ expect(lsp?.remedy).toBeUndefined();
760
+ });
761
+
762
+ it("no editor anywhere is a skip naming every place that was looked", async () => {
763
+ process.env.PATH = tmp("pragma-doctor-empty-path-");
764
+ const lsp = await lspRow();
765
+ expect(lsp?.status).toBe("skip");
766
+ expect(lsp?.detail).toContain("no VS Code-family editor found");
767
+ expect(lsp?.detail).toContain("under /Applications or ~/Applications");
768
+ expect(lsp?.detail).toContain("by its user directory");
769
+ expect(lsp?.remedy).toContain("no action is possible on this machine");
770
+ });
771
+ });
772
+
773
+ /**
774
+ * The `mcp` row over a location pragma must not write. It used to be an
775
+ * `available` row whose `fix:` would have failed the moment it ran.
776
+ */
777
+ describe("doctor — an unwritable MCP location", () => {
778
+ it("reports a store-managed file as a skip whose remedy IS the entry", async () => {
779
+ const cwd = tmp("pragma-doctor-proj-");
780
+ mkdirSync(join(cwd, ".vscode"), { recursive: true });
781
+
782
+ const rows = await scopedChecks(
783
+ bootRuntime(FLAGS, cwd),
784
+ "pragma",
785
+ storeProbe(cwd, "4b5c6d-project"),
786
+ );
787
+ const mcp = rows.find((r) => r.name === "mcp" && r.scope === "project");
788
+
789
+ expect(mcp?.status).toBe("skip");
790
+ expect(mcp?.detail).toContain("managed by Nix");
791
+ expect(mcp?.items?.every((i) => i.status === "skip")).toBe(true);
792
+ // The exact entry a write would have emitted, so what the user declares
793
+ // classifies as registered next time rather than as drift.
794
+ expect(mcp?.remedy).toContain('"servers"');
795
+ expect(mcp?.remedy).toContain('"pragma"');
796
+ expect(mcp?.remedy).not.toContain("\n");
797
+ });
798
+
799
+ it("counts only the WRITABLE files in the row's denominator", async () => {
800
+ // Two locations, one of them managed: "registered in 1 of 1" is the honest
801
+ // headline, not "1 of 2" over a file this command will never write.
802
+ const cwd = tmp("pragma-doctor-proj-");
803
+ // A `pragma` on PATH, so the registered entry reads `pass` rather than
804
+ // "registered, but `pragma` is not on PATH" — this case is about the
805
+ // DENOMINATOR, and a failing sibling would decide the row instead.
806
+ const binDir = tmp("pragma-doctor-bin-");
807
+ writeFileSync(join(binDir, "pragma"), "");
808
+ process.env.PATH = binDir;
809
+ mkdirSync(join(cwd, ".vscode"), { recursive: true });
810
+ mkdirSync(join(cwd, ".roo"), { recursive: true });
811
+ writeFileSync(
812
+ join(cwd, ".roo", "mcp.json"),
813
+ JSON.stringify({
814
+ mcpServers: {
815
+ pragma: { command: "pragma", args: ["mcp", "serve"], cwd },
816
+ },
817
+ }),
818
+ );
819
+
820
+ const rows = await scopedChecks(
821
+ bootRuntime(FLAGS, cwd),
822
+ "pragma",
823
+ storeProbe(join(cwd, ".vscode"), "7e8f9a-vscode"),
824
+ );
825
+ const mcp = rows.find((r) => r.name === "mcp" && r.scope === "project");
826
+
827
+ expect(mcp?.detail).toBe("registered in 1 of 1 config files");
828
+ expect(mcp?.items?.filter((i) => i.status === "skip")).toHaveLength(1);
829
+ });
830
+ });
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
17
17
  import { compilePack } from "../kernel/packs/compile.js";
18
+ import { DEFAULT_LIST_LIMIT } from "../kernel/packs/paging.js";
18
19
  import type { PackDefinition } from "../kernel/packs/types.js";
19
20
  import { distributionSource } from "../kernel/packs/types.js";
20
21
  import { verbKey } from "../kernel/packs/uniqueness.js";
@@ -41,28 +42,82 @@ const PREFIXES = {
41
42
  owl: "http://www.w3.org/2002/07/owl#",
42
43
  rdfs: "http://www.w3.org/2000/01/rdf-schema#",
43
44
  xsd: "http://www.w3.org/2001/XMLSchema#",
45
+ dt: "https://dt.canonical.com/",
46
+ "dt-web": "https://dt.canonical.com/platform/web/",
47
+ "w3c-tokens": "https://dt.canonical.com/w3c-tokens/",
48
+ anatomy: "https://anatomy.canonical.com/",
49
+ sh: "http://www.w3.org/ns/shacl#",
50
+ rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
44
51
  };
45
52
 
46
53
  // The ontology (TBox) with NO individuals — a booted-but-empty store.
47
54
  const TTL = `
48
55
  @prefix ds: <https://ds.canonical.com/> .
49
56
  @prefix cs: <https://ds.canonical.com/code-standards/> .
57
+ @prefix dt: <https://dt.canonical.com/> .
58
+ @prefix dt-web: <https://dt.canonical.com/platform/web/> .
59
+ @prefix w3c-tokens: <https://dt.canonical.com/w3c-tokens/> .
60
+ @prefix anatomy: <https://anatomy.canonical.com/> .
61
+ @prefix sh: <http://www.w3.org/ns/shacl#> .
50
62
  @prefix owl: <http://www.w3.org/2002/07/owl#> .
51
63
  @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
52
64
  @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
53
65
  ds:ModifierFamily a owl:Class .
54
66
  ds:Modifier a owl:Class .
55
- ds:Token a owl:Class .
56
- ds:TokenType a owl:Class .
57
67
  cs:CodeStandard a owl:Class .
58
68
  cs:Category a owl:Class .
59
69
  ds:name a owl:DatatypeProperty ; rdfs:domain ds:ModifierFamily ; rdfs:range xsd:string .
60
70
  ds:hasModifier a owl:ObjectProperty ; rdfs:domain ds:ModifierFamily ; rdfs:range ds:Modifier .
61
71
  ds:modifierFamily a owl:ObjectProperty ; rdfs:domain ds:Modifier ; rdfs:range ds:ModifierFamily .
62
- ds:tokenId a owl:DatatypeProperty ; rdfs:domain ds:Token ; rdfs:range xsd:string .
63
- ds:tokenType a owl:ObjectProperty ; rdfs:domain ds:Token ; rdfs:range ds:TokenType .
64
72
  cs:description a owl:DatatypeProperty ; rdfs:domain cs:CodeStandard ; rdfs:range xsd:string .
65
73
  cs:slug a owl:DatatypeProperty ; rdfs:domain cs:Category ; rdfs:range xsd:string .
74
+ # The token strata's TBox, which replaces the retired ds:Token declarations: the
75
+ # story reads dt: and w3c-tokens: now, and a booted-but-empty store has to BIND
76
+ # the terms a declared read names or the read fails as an unbuilt store instead
77
+ # of answering the empty list this suite is about. Declarations only — no
78
+ # individuals — which is the whole point of the fixture.
79
+ dt:TokenSymbol a owl:Class .
80
+ dt:ResolvedValue a owl:Class .
81
+ dt:Variable a owl:Class .
82
+ dt:Declaration a owl:Class .
83
+ dt:Coordinate a owl:Class .
84
+ dt:Tier a owl:Class .
85
+ dt:Visibility a owl:Class .
86
+ w3c-tokens:Token a owl:Class .
87
+ w3c-tokens:TokenType a owl:Class .
88
+ dt:channelOf a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
89
+ dt:symbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
90
+ dt:tokenType a owl:ObjectProperty ; rdfs:range w3c-tokens:TokenType .
91
+ dt:covers a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
92
+ dt:forSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
93
+ dt:coordinate a owl:ObjectProperty ; rdfs:range dt:Coordinate .
94
+ dt:derivedFrom a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
95
+ dt:resolutionChain a owl:ObjectProperty .
96
+ dt:resolvesTo a owl:DatatypeProperty ; rdfs:range xsd:string .
97
+ dt:ofSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
98
+ dt:tier a owl:ObjectProperty ; rdfs:range dt:Tier .
99
+ dt:visibility a owl:ObjectProperty ; rdfs:range dt:Visibility .
100
+ dt:declaredAt a owl:ObjectProperty ; rdfs:range dt:Declaration .
101
+ dt:under a owl:ObjectProperty .
102
+ dt:emits a owl:DatatypeProperty ; rdfs:range xsd:string .
103
+ dt:at a owl:DatatypeProperty ; rdfs:range xsd:string .
104
+ dt:alsoAt a owl:ObjectProperty ; rdfs:range dt:Coordinate .
105
+ dt:derives a owl:ObjectProperty .
106
+ dt:references a owl:ObjectProperty .
107
+ dt:selectsCoordinate a owl:ObjectProperty ; rdfs:range dt:Coordinate .
108
+ dt-web:selector a owl:DatatypeProperty ; rdfs:range xsd:string .
109
+ dt-web:inAtRule a owl:ObjectProperty .
110
+ w3c-tokens:description a owl:DatatypeProperty ; rdfs:range xsd:string .
111
+ w3c-tokens:inFile a owl:ObjectProperty .
112
+ w3c-tokens:path a owl:DatatypeProperty ; rdfs:range xsd:string .
113
+ anatomy:StyleKey a owl:Class .
114
+ anatomy:styleKey a owl:DatatypeProperty ; rdfs:range xsd:string .
115
+ anatomy:styleState a owl:DatatypeProperty ; rdfs:range xsd:string .
116
+ ds:hasTokenBinding a owl:ObjectProperty .
117
+ ds:consumesSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
118
+ ds:rank a owl:DatatypeProperty .
119
+ ds:node a owl:DatatypeProperty .
120
+ ds:viaBlock a owl:ObjectProperty .
66
121
  `;
67
122
 
68
123
  let rt: PragmaRuntime;
@@ -99,31 +154,32 @@ const PACK_LISTS: readonly { pack: PackDefinition; hint: RegExp }[] = [
99
154
  const REAL = { dryRun: false, undo: false, yes: false };
100
155
 
101
156
  describe("pack list empty-state (U5, PROTECTED)", () => {
102
- it.each(
103
- PACK_LISTS,
104
- )("$pack.noun list on an empty store returns [] — never throws", async ({
105
- pack,
106
- }) => {
107
- await expect(listVerb(pack).run({}, rt)).resolves.toEqual([]);
108
- });
157
+ it.each(PACK_LISTS)(
158
+ "$pack.noun list on an empty store returns [] — never throws",
159
+ async ({ pack }) => {
160
+ await expect(listVerb(pack).run({}, rt)).resolves.toEqual({
161
+ rows: [],
162
+ limit: DEFAULT_LIST_LIMIT,
163
+ });
164
+ },
165
+ );
109
166
 
110
- it.each(
111
- PACK_LISTS,
112
- )("$pack.noun list declares a non-blank empty notice (+ hint), JSON stays []", ({
113
- pack,
114
- hint,
115
- }) => {
116
- const { formatters } = listVerb(pack).output;
117
- // The message + runnable hint live on the empty-state seam — the
118
- // dispatcher routes them to stderr, keeping plain stdout pure data.
119
- const notice = formatters.notice?.([]);
120
- expect(notice).toContain(`No ${pack.noun} entries found.`);
121
- expect(notice).toMatch(hint);
122
- // JSON is the uniform empty array — unchanged by the message.
123
- expect(formatters.json([])).toBe("[]");
124
- // The llm view stays non-blank too (the `(0)` heading plus the message).
125
- expect(formatters.llm([]).trim().length).toBeGreaterThan(0);
126
- });
167
+ it.each(PACK_LISTS)(
168
+ "$pack.noun list declares a non-blank empty notice (+ hint), JSON stays []",
169
+ ({ pack, hint }) => {
170
+ const { formatters } = listVerb(pack).output;
171
+ // The message + runnable hint live on the empty-state seam — the
172
+ // dispatcher routes them to stderr, keeping plain stdout pure data.
173
+ const empty = { rows: [], limit: DEFAULT_LIST_LIMIT };
174
+ const notice = formatters.notice?.(empty);
175
+ expect(notice).toContain(`No ${pack.noun} entries found.`);
176
+ expect(notice).toMatch(hint);
177
+ // JSON is the uniform empty array — unchanged by the message.
178
+ expect(formatters.json(empty)).toBe("[]");
179
+ // The llm view stays non-blank too (the `(0)` heading plus the message).
180
+ expect(formatters.llm(empty).trim().length).toBeGreaterThan(0);
181
+ },
182
+ );
127
183
 
128
184
  it("dispatch routes the empty message to stderr and exits 0 (end-to-end)", async () => {
129
185
  const outcome = await executeVerb(