@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
@@ -0,0 +1,448 @@
1
+ /**
2
+ * Variable-noun semantic parity (PROTECTED) — the `variable` story
3
+ * `pragma.conf.ts` declares, over a platform-stratum fixture graph.
4
+ *
5
+ * Three properties of the story are only falsifiable against an authored graph,
6
+ * and the fixture exists for them:
7
+ *
8
+ * - the PUBLISHED NAME is the CSS name without its leading dashes. That is not
9
+ * cosmetic: a `--`-prefixed positional cannot be typed at all, so a noun
10
+ * publishing `--color-text` would be addressable by nothing. The fixture
11
+ * carries a variable whose name would collide with a SYMBOL name if the
12
+ * dashes were kept differently, and one standing for no symbol at all.
13
+ * - the AT-RULE STACK is an rdf:List, so its path needs the list walk. One
14
+ * condition here nests two levels, which is the only shape that tells the
15
+ * walk apart from reading the list's head — and the corpus has exactly one
16
+ * such condition, so a shipped-only test would pass on the head alone.
17
+ * - the CHAIN is transitively closed over every declaration of every hop. The
18
+ * fixture is a three-hop chain through a variable that stands for nothing, so
19
+ * the walk has to pass THROUGH a symbol-less variable to reach the symbol at
20
+ * the end. That is the case a one-hop join silently gets wrong.
21
+ *
22
+ * Counts are pinned because this file AUTHORS the graph. The invariants the
23
+ * shipped corpus decides — that the dash-stripping is injective and collides
24
+ * with no symbol name — are asserted over the real corpus in
25
+ * `tokenGraph.shipped.exec.test.ts`.
26
+ */
27
+
28
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
29
+ import { compilePack } from "../kernel/packs/compile.js";
30
+ import type { LookupOutput } from "../kernel/packs/resolveEntity.js";
31
+ import { distributionSource, type PackPage } from "../kernel/packs/types.js";
32
+ import { verbKey } from "../kernel/packs/uniqueness.js";
33
+ import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
34
+ import type { PragmaRuntime } from "../kernel/runtime/types.js";
35
+ import type { VerbSpec } from "../kernel/spec/types.js";
36
+ import { buildFixtureRuntime } from "../testing/helpers/packRuntime.js";
37
+ import { declaredStories } from "./distribution.js";
38
+
39
+ const variablePack = declaredStories.get("variable");
40
+ if (!variablePack) {
41
+ throw new Error('pragma.conf.ts declares no story for "variable"');
42
+ }
43
+
44
+ const DT = "https://dt.canonical.com/";
45
+ const DT_WEB = "https://dt.canonical.com/platform/web/";
46
+ const PREFIXES = {
47
+ dt: DT,
48
+ "dt-web": DT_WEB,
49
+ rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
50
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
51
+ owl: "http://www.w3.org/2002/07/owl#",
52
+ xsd: "http://www.w3.org/2001/XMLSchema#",
53
+ };
54
+
55
+ /**
56
+ * Four variables under one platform, and the chain that joins them:
57
+ *
58
+ * --modifier-color-text (stands for NOTHING; selected at criticality.success,
59
+ * and nested two at-rules deep)
60
+ * references
61
+ * --color-text-success (stands for nothing either — the middle of the walk)
62
+ * references
63
+ * --color-text (stands for dt:color.text; declared under :root,
64
+ * also at mode.dark)
65
+ *
66
+ * plus `--typography-weight-semi-bold`, a second spelling of a symbol another
67
+ * variable already stands for, which is why a lookup by name answers one row
68
+ * each rather than two.
69
+ */
70
+ const TTL = `
71
+ @prefix dt: <${DT}> .
72
+ @prefix dt-web: <${DT_WEB}> .
73
+ @prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
74
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
75
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
76
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
77
+
78
+ dt:Variable a owl:Class .
79
+ dt:TokenSymbol a owl:Class .
80
+ dt:Declaration a owl:Class .
81
+ dt:Condition a owl:Class .
82
+ dt:Coordinate a owl:Class .
83
+ dt:Tier a owl:Class .
84
+ dt:Visibility a owl:Class .
85
+ dt:Derivation a owl:Class .
86
+
87
+ dt:ofSymbol a owl:ObjectProperty ; rdfs:range dt:TokenSymbol .
88
+ dt:tier a owl:ObjectProperty ; rdfs:range dt:Tier .
89
+ dt:visibility a owl:ObjectProperty ; rdfs:range dt:Visibility .
90
+ dt:declaredAt a owl:ObjectProperty ; rdfs:range dt:Declaration .
91
+ dt:under a owl:ObjectProperty ; rdfs:range dt:Condition .
92
+ dt:emits a owl:DatatypeProperty ; rdfs:range xsd:string .
93
+ dt:at a owl:DatatypeProperty ; rdfs:range xsd:string .
94
+ dt:alsoAt a owl:ObjectProperty ; rdfs:range dt:Coordinate .
95
+ dt:derives a owl:ObjectProperty ; rdfs:range dt:Derivation .
96
+ dt:references a owl:ObjectProperty ; rdfs:range rdf:List .
97
+ dt:selectsCoordinate a owl:ObjectProperty ; rdfs:range dt:Coordinate .
98
+ dt-web:selector a owl:DatatypeProperty ; rdfs:range xsd:string .
99
+ dt-web:inAtRule a owl:ObjectProperty ; rdfs:range rdf:List .
100
+
101
+ dt:tier.semantic a dt:Tier .
102
+ dt:tier.derived a dt:Tier .
103
+ dt:visibility.public a dt:Visibility .
104
+ dt:visibility.internal a dt:Visibility .
105
+ dt:coordinate.mode.dark a dt:Coordinate .
106
+ dt:coordinate.criticality.success a dt:Coordinate .
107
+ dt:derivation.channel-modifier a dt:Derivation .
108
+
109
+ dt:color.text a dt:TokenSymbol ; rdfs:label "color.text" .
110
+ dt:typography.weight.semiBold a dt:TokenSymbol ;
111
+ rdfs:label "typography.weight.semiBold" .
112
+
113
+ # — The conditions ————————————————————————————————————————————————————————
114
+ # One at-rule, the ordinary case.
115
+ <${DT}s4/web/cond/root> a dt:Condition ;
116
+ dt-web:selector ":root" ;
117
+ dt-web:inAtRule ( "@layer ds.tokens" ) .
118
+ # TWO at-rules, nested: this is what the list walk is for. Reading the head
119
+ # alone would lose the inner layer and nobody would notice on a corpus where
120
+ # one condition in thirty-one nests.
121
+ <${DT}s4/web/cond/success> a dt:Condition ;
122
+ dt-web:selector ".success" ;
123
+ dt-web:inAtRule ( "@media (min-width: 40em)" "@layer ds.modifiers" ) ;
124
+ dt:selectsCoordinate dt:coordinate.criticality.success .
125
+
126
+ # — The variables ——————————————————————————————————————————————————————————
127
+ <${DT}s4/web/--color-text> a dt:Variable ;
128
+ rdfs:label "color-text" ;
129
+ dt:ofSymbol dt:color.text ;
130
+ dt:tier dt:tier.semantic ;
131
+ dt:visibility dt:visibility.public ;
132
+ dt:declaredAt [
133
+ a dt:Declaration ;
134
+ dt:under <${DT}s4/web/cond/root> ;
135
+ dt:emits "light-dark(black, white)" ;
136
+ dt:at "tokens.css:12" ;
137
+ dt:alsoAt dt:coordinate.mode.dark
138
+ ] .
139
+
140
+ # Stands for NOTHING — one of the population a symbol-keyed surface cannot
141
+ # reach, and the middle hop of the walk below.
142
+ <${DT}s4/web/--color-text-success> a dt:Variable ;
143
+ rdfs:label "color-text-success" ;
144
+ dt:tier dt:tier.semantic ;
145
+ dt:visibility dt:visibility.internal ;
146
+ dt:declaredAt [
147
+ a dt:Declaration ;
148
+ dt:under <${DT}s4/web/cond/root> ;
149
+ dt:emits "var(--color-text)" ;
150
+ dt:at "tokens.css:20" ;
151
+ dt:references ( <${DT}s4/web/--color-text> )
152
+ ] .
153
+
154
+ # Also stands for nothing, and is where the walk starts.
155
+ <${DT}s4/web/--modifier-color-text> a dt:Variable ;
156
+ rdfs:label "modifier-color-text" ;
157
+ dt:tier dt:tier.derived ;
158
+ dt:visibility dt:visibility.internal ;
159
+ dt:declaredAt [
160
+ a dt:Declaration ;
161
+ dt:under <${DT}s4/web/cond/success> ;
162
+ dt:emits "var(--color-text-success)" ;
163
+ dt:at "modifiers.css:7" ;
164
+ dt:derives dt:derivation.channel-modifier ;
165
+ dt:references ( <${DT}s4/web/--color-text-success> )
166
+ ] .
167
+
168
+ # The second spelling of a symbol another variable already stands for.
169
+ <${DT}s4/web/--typography-weight-semiBold> a dt:Variable ;
170
+ rdfs:label "typography-weight-semiBold" ;
171
+ dt:ofSymbol dt:typography.weight.semiBold ;
172
+ dt:tier dt:tier.semantic ;
173
+ dt:visibility dt:visibility.public .
174
+ <${DT}s4/web/--typography-weight-semi-bold> a dt:Variable ;
175
+ rdfs:label "typography-weight-semi-bold" ;
176
+ dt:ofSymbol dt:typography.weight.semiBold ;
177
+ dt:tier dt:tier.semantic ;
178
+ dt:visibility dt:visibility.public .
179
+ `;
180
+
181
+ const verbs = () =>
182
+ compilePack(
183
+ variablePack,
184
+ distributionSource("pragma.conf.ts"),
185
+ DEFAULT_PREFIX_MAP,
186
+ );
187
+ const verb = (label: string) =>
188
+ verbs().find((v) => verbKey(v.path) === `variable ${label}`) as VerbSpec;
189
+
190
+ let rt: PragmaRuntime;
191
+ beforeAll(async () => {
192
+ ({ rt } = await buildFixtureRuntime({ ttl: TTL, prefixes: PREFIXES }));
193
+ });
194
+ afterAll(async () => {
195
+ (await rt.store.get()).store.dispose();
196
+ });
197
+
198
+ const page = (label: string, params: Record<string, unknown> = {}) =>
199
+ verb(label).run(params, rt) as Promise<PackPage>;
200
+
201
+ async function rows(
202
+ label: string,
203
+ params: Record<string, unknown> = {},
204
+ ): Promise<Record<string, string>[]> {
205
+ return [...(await page(label, params)).rows];
206
+ }
207
+
208
+ async function lookupAll(...names: string[]): Promise<LookupOutput> {
209
+ return (await verb("lookup").run({ name: names }, rt)) as LookupOutput;
210
+ }
211
+
212
+ async function lookup(name: string): Promise<Record<string, unknown>> {
213
+ const out = await lookupAll(name);
214
+ expect(out.errors).toEqual([]);
215
+ return out.results.at(0) as Record<string, unknown>;
216
+ }
217
+
218
+ function children(
219
+ entity: Record<string, unknown>,
220
+ name: string,
221
+ ): Record<string, string>[] {
222
+ return (entity[name] ?? []) as Record<string, string>[];
223
+ }
224
+
225
+ describe("variable list — the platform names, and what they stand for (PROTECTED)", () => {
226
+ it("publishes the CSS name WITHOUT its leading dashes", async () => {
227
+ // The whole reason the noun is addressable. Commander reads a leading `--`
228
+ // as an option, so a row publishing `--color-text` would name something no
229
+ // caller could pass as a positional.
230
+ const names = (await rows("list")).map((row) => row.name);
231
+ expect(names).toEqual([
232
+ "color-text",
233
+ "color-text-success",
234
+ "modifier-color-text",
235
+ "typography-weight-semi-bold",
236
+ "typography-weight-semiBold",
237
+ ]);
238
+ expect(names.some((name) => name.startsWith("--"))).toBe(false);
239
+ });
240
+
241
+ it("reads the platform out of the IRI base rather than an assertion", async () => {
242
+ // Nothing asserts the platform; the file identity in the IRI is the only
243
+ // statement of it, which is why it is a filter and not a noun.
244
+ expect(new Set((await rows("list")).map((row) => row.platform))).toEqual(
245
+ new Set(["web"]),
246
+ );
247
+ });
248
+
249
+ it("publishes a row for a variable standing for NO symbol", async () => {
250
+ // The measurement the second noun exists for: a symbol-keyed surface
251
+ // cannot address these at all.
252
+ const orphan = (await rows("list")).find(
253
+ (row) => row.name === "modifier-color-text",
254
+ );
255
+ expect(orphan).toBeDefined();
256
+ expect(orphan?.symbol).toBeUndefined();
257
+ });
258
+
259
+ it("names tier and visibility by their own local names", async () => {
260
+ const byName = new Map((await rows("list")).map((r) => [r.name, r]));
261
+ expect(byName.get("color-text")).toMatchObject({
262
+ symbol: "color.text",
263
+ tier: "semantic",
264
+ visibility: "public",
265
+ });
266
+ expect(byName.get("modifier-color-text")).toMatchObject({
267
+ tier: "derived",
268
+ visibility: "internal",
269
+ });
270
+ });
271
+
272
+ it("collects EVERY coordinate a variable reaches, from both sources", async () => {
273
+ const byName = new Map((await rows("list")).map((r) => [r.name, r]));
274
+ // From the declaration's own `dt:alsoAt`.
275
+ expect(byName.get("color-text")?.coordinate).toBe("mode.dark");
276
+ // From the CONDITION the declaration sits under.
277
+ expect(byName.get("modifier-color-text")?.coordinate).toBe(
278
+ "criticality.success",
279
+ );
280
+ });
281
+
282
+ it("--coordinate set-matches, so a multi-coordinate cell answers for each", async () => {
283
+ expect(
284
+ (await rows("list", { coordinate: "mode.dark" })).map((r) => r.name),
285
+ ).toEqual(["color-text"]);
286
+ expect(
287
+ (await rows("list", { coordinate: "criticality.success" })).map(
288
+ (r) => r.name,
289
+ ),
290
+ ).toEqual(["modifier-color-text"]);
291
+ });
292
+
293
+ it("--symbol, --tier, --visibility and --platform each narrow", async () => {
294
+ expect(
295
+ (await rows("list", { symbol: "color.text" })).map((r) => r.name),
296
+ ).toEqual(["color-text"]);
297
+ expect(
298
+ (await rows("list", { tier: "derived" })).map((r) => r.name),
299
+ ).toEqual(["modifier-color-text"]);
300
+ expect(
301
+ (await rows("list", { visibility: "internal" })).map((r) => r.name),
302
+ ).toEqual(["color-text-success", "modifier-color-text"]);
303
+ expect(await rows("list", { platform: "web" })).toHaveLength(5);
304
+ });
305
+
306
+ it("--symbol answers BOTH spellings that stand for one symbol", async () => {
307
+ // A symbol can have more than one variable, which is the fact that makes
308
+ // this noun necessary rather than a flag on `token`.
309
+ expect(
310
+ (await rows("list", { symbol: "typography.weight.semiBold" })).map(
311
+ (r) => r.name,
312
+ ),
313
+ ).toEqual(["typography-weight-semi-bold", "typography-weight-semiBold"]);
314
+ });
315
+
316
+ it("refuses a tier the graph does not admit, naming the ones it does", async () => {
317
+ await expect(page("list", { tier: "cosmic" })).rejects.toMatchObject({
318
+ code: "INVALID_INPUT",
319
+ validOptions: ["derived", "semantic"],
320
+ });
321
+ });
322
+ });
323
+
324
+ describe("variable lookup — every place it is declared (PROTECTED)", () => {
325
+ it("resolves the dash-stripped name the list publishes", async () => {
326
+ expect(await lookup("color-text")).toMatchObject({
327
+ name: "color-text",
328
+ symbol: "color.text",
329
+ });
330
+ });
331
+
332
+ it("each spelling answers ONE row, because each carries its own label", async () => {
333
+ // Both stand for the same symbol, and both are addressable — the surface
334
+ // shows both because the ontology holds both.
335
+ const out = await lookupAll(
336
+ "typography-weight-semiBold",
337
+ "typography-weight-semi-bold",
338
+ );
339
+ expect(out.errors).toEqual([]);
340
+ expect(out.results).toHaveLength(2);
341
+ for (const result of out.results) {
342
+ expect(result).toMatchObject({ symbol: "typography.weight.semiBold" });
343
+ }
344
+ });
345
+
346
+ it("shows the selector, the emitted value, the source and the coordinate", async () => {
347
+ const declarations = children(await lookup("color-text"), "declarations");
348
+ expect(declarations).toHaveLength(1);
349
+ expect(declarations[0]).toMatchObject({
350
+ selector: ":root",
351
+ emits: "light-dark(black, white)",
352
+ at: "tokens.css:12",
353
+ inAtRule: "@layer ds.tokens",
354
+ });
355
+ // `dt:at` is the SOURCE LOCATION and `dt:alsoAt` the coordinate. The two
356
+ // read alike and mean different things, so both are asserted.
357
+ expect(declarations[0]?.alsoAt).toContain("mode.dark");
358
+ });
359
+
360
+ it("walks the at-rule LIST, so a nested stack contributes each level", async () => {
361
+ // The one shape a head-only read gets wrong. Two rows for one declaration
362
+ // here is the stack, read outermost-in.
363
+ const declarations = children(
364
+ await lookup("modifier-color-text"),
365
+ "declarations",
366
+ );
367
+ expect(declarations.map((d) => d.inAtRule).sort()).toEqual([
368
+ "@layer ds.modifiers",
369
+ "@media (min-width: 40em)",
370
+ ]);
371
+ expect(new Set(declarations.map((d) => d.selector))).toEqual(
372
+ new Set([".success"]),
373
+ );
374
+ });
375
+
376
+ it("shows the derivation that computed a derived variable", async () => {
377
+ const declarations = children(
378
+ await lookup("modifier-color-text"),
379
+ "declarations",
380
+ );
381
+ expect(declarations[0]?.derives).toContain("channel-modifier");
382
+ });
383
+
384
+ it("a miss carries did-you-mean suggestions", async () => {
385
+ let caught: unknown;
386
+ try {
387
+ await verb("lookup").run({ name: ["color-txt"] }, rt);
388
+ } catch (error) {
389
+ caught = error;
390
+ }
391
+ expect(caught).toMatchObject({ code: "ENTITY_NOT_FOUND" });
392
+ expect((caught as { suggestions: string[] }).suggestions).toContain(
393
+ "color-text",
394
+ );
395
+ });
396
+ });
397
+
398
+ describe("variable chain — the transitive walk (PROTECTED)", () => {
399
+ it("reaches a symbol THROUGH a variable that stands for nothing", async () => {
400
+ // Three hops, and the middle one stands for no symbol. A one-hop join
401
+ // would answer nothing here and be silently wrong rather than empty.
402
+ expect(await rows("chain", { variable: "modifier-color-text" })).toEqual([
403
+ {
404
+ variable: "modifier-color-text",
405
+ reaches: "color-text",
406
+ symbol: "color.text",
407
+ },
408
+ ]);
409
+ });
410
+
411
+ it("answers from every starting variable that reaches a symbol", async () => {
412
+ expect(
413
+ (await rows("chain")).map((row) => [row.variable, row.symbol]),
414
+ ).toEqual([
415
+ ["color-text-success", "color.text"],
416
+ ["modifier-color-text", "color.text"],
417
+ ]);
418
+ });
419
+
420
+ it("--symbol answers the walks that REACH one symbol", async () => {
421
+ expect(await rows("chain", { symbol: "color.text" })).toHaveLength(2);
422
+ });
423
+
424
+ it("a variable that references nothing has no walk, calmly", async () => {
425
+ const answered = await page("chain", { variable: "color-text" });
426
+ expect(answered.rows).toEqual([]);
427
+ expect(answered.nextAfter).toBeUndefined();
428
+ });
429
+ });
430
+
431
+ describe("variable — the name literal is the whole grammar (PROTECTED)", () => {
432
+ it("every published name resolves through lookup, verbatim", async () => {
433
+ const names = (await rows("list")).map((row) => row.name);
434
+ const out = await lookupAll(...names);
435
+ expect(out.errors).toEqual([]);
436
+ expect(out.results.map((r) => (r as { name: string }).name).sort()).toEqual(
437
+ [...names].sort(),
438
+ );
439
+ });
440
+
441
+ it("the dash-stripping is injective over this graph", async () => {
442
+ // Asserted here over an authored graph and over the WHOLE shipped corpus in
443
+ // `tokenGraph.shipped.exec.test.ts`: if two variables ever stripped to one
444
+ // name, a lookup would answer one of them and silently hide the other.
445
+ const names = (await rows("list")).map((row) => row.name);
446
+ expect(new Set(names).size).toBe(names.length);
447
+ });
448
+ });
package/src/constants.ts CHANGED
@@ -41,6 +41,13 @@ const ISSUES_URL = identity.issuesUrl;
41
41
  /** Semver version string read from package.json. */
42
42
  const VERSION: string = pkg.version;
43
43
 
44
+ /**
45
+ * The Node.js range this distribution supports, read from `engines.node` in
46
+ * package.json so the manifest stays the single statement of the floor. The
47
+ * doctor check quotes it, and `isSupportedNodeVersion` is pinned to it by test.
48
+ */
49
+ const SUPPORTED_NODE_RANGE: string = pkg.engines.node;
50
+
44
51
  /**
45
52
  * The project config filename the walker looks for and every surface that
46
53
  * quotes it — diagnostics, the onboarding note, the generated reference, the
@@ -116,5 +123,6 @@ export {
116
123
  PROGRAM_LOGO,
117
124
  PROJECT_CONFIG_FILENAME,
118
125
  RECOVERY_CLI_PREFIX,
126
+ SUPPORTED_NODE_RANGE,
119
127
  VERSION,
120
128
  };
@@ -351,12 +351,17 @@ const STRUCTURE = [
351
351
  at: "pragma block lookup --<TAB>",
352
352
  words: ["pragma", "block", "lookup", "--"],
353
353
  cword: 3,
354
+ // The globals, plus the ONE flag this verb declares: `--tier`, which the
355
+ // block story earns by declaring a tier hierarchy. A verb's own flags are
356
+ // in the inlined table like the globals, so offering it still execs
357
+ // nothing.
354
358
  offers: [
355
359
  "--detail",
356
360
  "--format",
357
361
  "--help",
358
362
  "--no-headers",
359
363
  "--quiet",
364
+ "--tier",
360
365
  "--verbose",
361
366
  ],
362
367
  },
@@ -389,6 +394,7 @@ const NOUNS = [
389
394
  "tier",
390
395
  "token",
391
396
  "upgrade",
397
+ "variable",
392
398
  "version",
393
399
  ] as const;
394
400
 
@@ -401,17 +407,16 @@ describe("generated bash — the live grammar's script", () => {
401
407
  expect(parses("bash", fixture.bash)).toBe(0);
402
408
  });
403
409
 
404
- it.each(STRUCTURE)("answers $at from the inlined table, execing nothing", ({
405
- words,
406
- cword,
407
- offers,
408
- }) => {
409
- const { reply, calls } = driveBash(live.bash, words, cword);
410
- expect(reply.sort()).toEqual([...offers].sort());
411
- // The structural tier is the point of a generated script: a TAB here
412
- // must never cost a process spawn, nor depend on a pack being built.
413
- expect(calls).toEqual([]);
414
- });
410
+ it.each(STRUCTURE)(
411
+ "answers $at from the inlined table, execing nothing",
412
+ ({ words, cword, offers }) => {
413
+ const { reply, calls } = driveBash(live.bash, words, cword);
414
+ expect(reply.sort()).toEqual([...offers].sort());
415
+ // The structural tier is the point of a generated script: a TAB here
416
+ // must never cost a process spawn, nor depend on a pack being built.
417
+ expect(calls).toEqual([]);
418
+ },
419
+ );
415
420
 
416
421
  it("hands __complete the protocol argv and OFFERS what it answers", () => {
417
422
  // The last mile, and the only assertion that covers it: everything else
@@ -480,17 +485,14 @@ describe.skipIf(!hasShell("zsh"))(
480
485
  // test below, and the other five are asserted as the exact sets zsh really
481
486
  // returns. An `arrayContaining` here would be blind to the regression this
482
487
  // file exists to catch — a slot that offers EXTRA, wrong candidates.
483
- it.each(
484
- STRUCTURE.filter((row) => row.at !== "pragma co"),
485
- )("OFFERS exactly the candidates for $at, execing nothing", ({
486
- words,
487
- cword,
488
- offers,
489
- }) => {
490
- const { reply, calls } = driveZsh(live.zsh, words, cword);
491
- expect(reply.sort()).toEqual([...offers].sort());
492
- expect(calls).toEqual([]);
493
- });
488
+ it.each(STRUCTURE.filter((row) => row.at !== "pragma co"))(
489
+ "OFFERS exactly the candidates for $at, execing nothing",
490
+ ({ words, cword, offers }) => {
491
+ const { reply, calls } = driveZsh(live.zsh, words, cword);
492
+ expect(reply.sort()).toEqual([...offers].sort());
493
+ expect(calls).toEqual([]);
494
+ },
495
+ );
494
496
 
495
497
  it("offers the WHOLE noun table for a partial noun, leaving the filtering to zsh", () => {
496
498
  const { reply, calls } = driveZsh(live.zsh, ["pragma", "co"], 1);
@@ -552,14 +554,14 @@ describe.skipIf(!hasShell("fish"))(
552
554
  line: "pragma --format=",
553
555
  offers: ["--format=json", "--format=llm", "--format=plain"],
554
556
  },
555
- ])("answers $line from the inlined rules, execing nothing", ({
556
- line,
557
- offers,
558
- }) => {
559
- const { reply, calls } = driveFish(live.fish, line);
560
- expect(reply.sort()).toEqual([...offers].sort());
561
- expect(calls).toEqual([]);
562
- });
557
+ ])(
558
+ "answers $line from the inlined rules, execing nothing",
559
+ ({ line, offers }) => {
560
+ const { reply, calls } = driveFish(live.fish, line);
561
+ expect(reply.sort()).toEqual([...offers].sort());
562
+ expect(calls).toEqual([]);
563
+ },
564
+ );
563
565
 
564
566
  it("hands __complete the SAME protocol argv bash does, and OFFERS the answer", () => {
565
567
  const { reply, calls } = driveFish(
@@ -44,7 +44,12 @@ describe("defaults — the validated distribution config (pragma.conf.ts)", () =
44
44
  expect(defaults.colophon?.summary?.length).toBeLessThan(400);
45
45
  });
46
46
 
47
- it("ships the four canonical default packs (git+https sources)", () => {
47
+ // Five packs, and ONE carries a subdirectory. That is load-bearing, not
48
+ // decoration: `@canonical/token-ontology` lives inside a monorepo, and a
49
+ // git source names a repository — without `:packages/token-ontology` the
50
+ // clone would succeed against a root that has no `definitions/` or `data/`
51
+ // and the pack would contribute nothing, silently.
52
+ it("ships the five canonical default packs (all git+https)", () => {
48
53
  expect(
49
54
  defaults.packs?.map((pack) =>
50
55
  typeof pack === "string"
@@ -60,6 +65,11 @@ describe("defaults — the validated distribution config (pragma.conf.ts)", () =
60
65
  name: "@canonical/anatomy-dsl",
61
66
  source: "git+https://github.com/canonical/anatomy-dsl.git#main",
62
67
  },
68
+ {
69
+ name: "@canonical/token-ontology",
70
+ source:
71
+ "git+https://github.com/canonical/design-tokens.git#main:packages/token-ontology",
72
+ },
63
73
  {
64
74
  name: "@canonical/code-standards",
65
75
  source:
@@ -79,7 +89,11 @@ describe("defaults — the validated distribution config (pragma.conf.ts)", () =
79
89
  const storyCounts = defaults.packs?.map((pack) =>
80
90
  typeof pack === "string" ? 0 : (pack.stories?.length ?? 0),
81
91
  );
82
- expect(storyCounts).toEqual([5, 0, 1, 1]);
92
+ // Six on the design-system pack: block, token, variable, modifier, tier
93
+ // and concept. `token` and `variable` read the token-ontology pack's
94
+ // strata and are declared on this one anyway, because `token consumers`
95
+ // spans both and no single pack's stories could carry the pair.
96
+ expect(storyCounts).toEqual([6, 0, 0, 1, 1]);
83
97
  });
84
98
 
85
99
  it("declares no removed field — the validator would refuse to load one", () => {
@@ -98,6 +112,11 @@ describe("defaults — the validated distribution config (pragma.conf.ts)", () =
98
112
  // can expand — `cs:` is here for that reader.
99
113
  expect(defaults.prefixes).toEqual({
100
114
  ds: "https://ds.canonical.com/",
115
+ dt: "https://dt.canonical.com/",
116
+ // Nested under `dt:`, and bound separately because `compactUri` takes the
117
+ // longest match — with only the parent bound, every term of this
118
+ // vocabulary would render as `dt:w3c-tokens/…`.
119
+ "w3c-tokens": "https://dt.canonical.com/w3c-tokens/",
101
120
  cs: "http://pragma.canonical.com/codestandards#",
102
121
  });
103
122
  });
@@ -59,7 +59,7 @@ describe("readConfig — layering + provenance", () => {
59
59
  expect(config.tier).toBe("core");
60
60
  expect(config.channel).toBe("experimental");
61
61
  expect(config.detail).toBe("detailed");
62
- expect(config.packs).toHaveLength(4); // from defaults
62
+ expect(config.packs).toHaveLength(5); // from defaults
63
63
 
64
64
  expect(origins).toMatchObject({
65
65
  tier: "project",
@@ -10,6 +10,7 @@
10
10
  import type { OutputFormat } from "../../constants.js";
11
11
  import type { PragmaError } from "./PragmaError.js";
12
12
  import { serializeError } from "./serialize.js";
13
+ import { formatValidOptions } from "./validOptions.js";
13
14
 
14
15
  /**
15
16
  * Render a {@link PragmaError} as human-readable plain text for the terminal.
@@ -35,7 +36,7 @@ function renderErrorPlain(error: PragmaError): string {
35
36
  }
36
37
 
37
38
  if (error.validOptions && error.validOptions.length > 0) {
38
- lines.push("", `Valid options: ${error.validOptions.join(", ")}`);
39
+ lines.push("", formatValidOptions(error.validOptions));
39
40
  }
40
41
 
41
42
  if (error.recovery) {
@@ -73,7 +74,7 @@ function renderErrorLlm(error: PragmaError): string {
73
74
  }
74
75
 
75
76
  if (error.validOptions && error.validOptions.length > 0) {
76
- lines.push(`Valid options: ${error.validOptions.join(", ")}`);
77
+ lines.push(formatValidOptions(error.validOptions));
77
78
  }
78
79
 
79
80
  if (error.recovery) {