@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
@@ -71,6 +71,11 @@ export const TOOL_HINTS: Record<string, ToolHint> = {
71
71
  use_when:
72
72
  "Checking the active configuration (tier, channel, detail) and which layer set each field",
73
73
  },
74
+ graph_connect: {
75
+ category: "read",
76
+ use_when:
77
+ "Deciding whether two entities are related at all — the shortest relation paths between them, or which kind of nothing there is",
78
+ },
74
79
  graph_inspect: {
75
80
  category: "read",
76
81
  use_when:
@@ -168,7 +173,7 @@ export const TOOL_HINTS: Record<string, ToolHint> = {
168
173
  },
169
174
  tier_list: {
170
175
  category: "read",
171
- use_when: "Understanding the tier hierarchy before setting a tier filter",
176
+ use_when: "Picking the tier to scope a read to (the `tier` parameter)",
172
177
  },
173
178
  tier_lookup: {
174
179
  category: "read",
@@ -177,17 +182,47 @@ export const TOOL_HINTS: Record<string, ToolHint> = {
177
182
  token_list: {
178
183
  category: "read",
179
184
  use_when:
180
- "Browsing design tokens, optionally filtered by category (color, spacing, etc.)",
185
+ "Browsing which token SYMBOLS exist (the logical dotted names), optionally by type or by the symbol a channel provisions",
181
186
  },
182
187
  token_lookup: {
183
188
  category: "read",
184
189
  use_when:
185
- "Need theme values and resolution details for specific tokens by name or IRI",
190
+ "Need one symbol in full — its definitions, the families that may rebind it, and what it resolves to at each position",
191
+ },
192
+ token_values: {
193
+ category: "read",
194
+ use_when:
195
+ "Need the value a symbol resolves to at a position, with the chain that produced it or the symbol it derives from",
196
+ },
197
+ token_consumers: {
198
+ category: "read",
199
+ use_when:
200
+ "Need which blocks consume a symbol, at which style key, state and rank",
186
201
  },
187
202
  token_sample: {
188
203
  category: "read",
189
204
  use_when:
190
- "See actual token data shapes (with theme values) before querying — returns random instances each call",
205
+ "See actual token symbol data shapes before querying — returns random instances each call",
206
+ },
207
+ variable_list: {
208
+ category: "read",
209
+ use_when:
210
+ "Browsing the platform names a stylesheet declares, by platform, symbol, tier, visibility or coordinate — including the 236 that stand for no symbol",
211
+ },
212
+ variable_lookup: {
213
+ category: "read",
214
+ use_when:
215
+ "Need one platform variable in full — its symbol, tier, visibility, and every selector and at-rule it is declared under",
216
+ },
217
+ variable_chain: {
218
+ category: "read",
219
+ use_when:
220
+ "Need what a variable finally means: the transitive walk to every symbol it reaches",
221
+ },
222
+ variable_sample: {
223
+ category: "read",
224
+ use_when:
225
+ "See actual platform variable data shapes before querying — returns random instances each call",
191
226
  },
192
227
 
193
228
  // — Write ——————————————————————————————————————————————————————————————————
@@ -213,7 +248,7 @@ export const TOOL_HINTS: Record<string, ToolHint> = {
213
248
  create_application: {
214
249
  category: "write",
215
250
  use_when:
216
- "Scaffolding a new React application with SSR, routing, and optional Relay",
251
+ "Scaffolding a new React application with routing, optional Relay, and either server-side rendering or a client-only SPA",
217
252
  },
218
253
  setup: {
219
254
  category: "write",
@@ -36,6 +36,7 @@ describe("config show payload", () => {
36
36
  ).toEqual([
37
37
  "@canonical/design-system",
38
38
  "@canonical/anatomy-dsl",
39
+ "@canonical/token-ontology",
39
40
  "@canonical/code-standards",
40
41
  "@canonical/ds-implementations",
41
42
  ]);
@@ -108,36 +108,35 @@ describe("config set — the reserved clear-markers are refused, naming unset",
108
108
  // Clearing a field is a command, so no value may quietly mean "remove me":
109
109
  // the three strings that used to are rejected, and the rejection names the
110
110
  // command that owns the job. `tier` keeps whatever it held.
111
- it.each([
112
- "none",
113
- "default",
114
- "-",
115
- ])("set tier %s is INVALID_INPUT that recovers with config unset tier", async (reserved) => {
116
- const cwd = tmp("pragma-proj-");
117
- await executeVerb(
118
- setVerb,
119
- { key: "tier", value: "apps/lxd" },
120
- REAL,
121
- bootRuntime(FLAGS, cwd),
122
- );
123
- let caught: unknown;
124
- try {
111
+ it.each(["none", "default", "-"])(
112
+ "set tier %s is INVALID_INPUT that recovers with config unset tier",
113
+ async (reserved) => {
114
+ const cwd = tmp("pragma-proj-");
125
115
  await executeVerb(
126
116
  setVerb,
127
- { key: "tier", value: reserved },
117
+ { key: "tier", value: "apps/lxd" },
128
118
  REAL,
129
119
  bootRuntime(FLAGS, cwd),
130
120
  );
131
- } catch (error) {
132
- caught = error;
133
- }
134
- expect(caught).toBeInstanceOf(PragmaError);
135
- expect((caught as PragmaError).code).toBe("INVALID_INPUT");
136
- expect(JSON.stringify((caught as PragmaError).recovery)).toContain(
137
- "config unset tier",
138
- );
139
- expect(readGlobal().tier).toBe("apps/lxd");
140
- });
121
+ let caught: unknown;
122
+ try {
123
+ await executeVerb(
124
+ setVerb,
125
+ { key: "tier", value: reserved },
126
+ REAL,
127
+ bootRuntime(FLAGS, cwd),
128
+ );
129
+ } catch (error) {
130
+ caught = error;
131
+ }
132
+ expect(caught).toBeInstanceOf(PragmaError);
133
+ expect((caught as PragmaError).code).toBe("INVALID_INPUT");
134
+ expect(JSON.stringify((caught as PragmaError).recovery)).toContain(
135
+ "config unset tier",
136
+ );
137
+ expect(readGlobal().tier).toBe("apps/lxd");
138
+ },
139
+ );
141
140
 
142
141
  it("a reserved marker is only reserved for a free-string field", () => {
143
142
  // `channel`/`detail` are closed enums: their rejection is the enum
@@ -40,7 +40,9 @@ describe("renderConfigShowPlain", () => {
40
40
  it("is byte-identical to the plain form when the styler is disabled", () => {
41
41
  expect(renderConfigShowPlain(DATA, styleFor(false))).toBe(
42
42
  [
43
- "tier: apps/lxd [project]",
43
+ // The value AND the scope it puts every tiered read under: `tier`
44
+ // decides a scope now, so the line that reports it says which.
45
+ "tier: apps/lxd — apps/lxd and its ancestors [project]",
44
46
  "channel: normal",
45
47
  "detail: standard",
46
48
  "packs: @canonical/ds [global]",
@@ -55,7 +57,7 @@ describe("renderConfigShowPlain", () => {
55
57
  const keyWidth = "project config:".length;
56
58
  // Dim, padded key; cyan value; dim `[layer]` marker.
57
59
  expect(out).toContain(
58
- `<d>${"tier:".padEnd(keyWidth)}</d> <c>apps/lxd</c><d> [project]</d>`,
60
+ `<d>${"tier:".padEnd(keyWidth)}</d> <c>apps/lxd — apps/lxd and its ancestors</c><d> [project]</d>`,
59
61
  );
60
62
  // A default-origin value carries no marker.
61
63
  expect(out).toContain(
@@ -82,7 +84,8 @@ describe("renderConfigShowPlain", () => {
82
84
 
83
85
  const out = renderConfigShowPlain(bare, styleFor(false));
84
86
 
85
- expect(out).toContain("tier: (none)");
87
+ // Unset is not "no scope": it is the default scope, and the line says so.
88
+ expect(out).toContain("tier: (none) — the top-level tiers");
86
89
  expect(out).toContain("packs: (none)");
87
90
  expect(out).toContain("project config: (not found)");
88
91
  });
@@ -92,7 +95,9 @@ describe("configShowFormatters.llm", () => {
92
95
  it("renders the layered rows with origin markers", () => {
93
96
  const out = configShowFormatters.llm(DATA);
94
97
 
95
- expect(out).toContain("- **Tier:** apps/lxd [project]");
98
+ expect(out).toContain(
99
+ "- **Tier:** apps/lxd — apps/lxd and its ancestors [project]",
100
+ );
96
101
  expect(out).toContain("- **Packs:** @canonical/ds [global]");
97
102
  });
98
103
 
@@ -7,6 +7,7 @@
7
7
  */
8
8
 
9
9
  import type { ConfigOrigin } from "../../kernel/config/types.js";
10
+ import { EVERY_TIER } from "../../kernel/packs/types.js";
10
11
  import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
11
12
  import type { Formatters } from "../../kernel/spec/index.js";
12
13
  import type { ConfigShowData } from "./types.js";
@@ -16,6 +17,29 @@ function originMarker(origin: ConfigOrigin): string {
16
17
  return origin === "default" ? "" : ` [${origin}]`;
17
18
  }
18
19
 
20
+ /**
21
+ * The `tier` value with the SCOPE it puts every tiered read under.
22
+ *
23
+ * The value alone is not the answer to "what am I reading". `tier` used to be
24
+ * accepted and read by nothing, and `config show` reported it as a setting in
25
+ * force while `block list` answered from all fifteen tiers; now it decides a
26
+ * scope, so this line has to say which. The three cases are the whole rule:
27
+ * unset is the top-level tiers, a tier is that tier and its ancestors, and
28
+ * `all` turns the scope off.
29
+ *
30
+ * It names the RULE and not the tiers, deliberately: `config show` is storeless
31
+ * (`capability.needsStore: false`) and naming the five top-level tiers means
32
+ * booting the pack to read them. The tiers themselves are one command away
33
+ * (`tier list`), and every scoped answer states its own scope in its heading —
34
+ * so the reading a store could add here is already available where it matters.
35
+ */
36
+ function tierScope(tier: string | undefined): string {
37
+ if (tier === undefined) return "the top-level tiers";
38
+ return tier.trim().toLowerCase() === EVERY_TIER
39
+ ? "every tier"
40
+ : `${tier} and its ancestors`;
41
+ }
42
+
19
43
  /** Summarize a list of named entries as a comma-separated set of names. */
20
44
  function entryNames(entries: readonly (string | { name: string })[]): string {
21
45
  const names = entries.map((entry) =>
@@ -31,7 +55,11 @@ type ConfigRow = readonly [label: string, value: string, marker: string];
31
55
  function configRows(data: ConfigShowData): readonly ConfigRow[] {
32
56
  const { config, origins } = data;
33
57
  return [
34
- ["tier", config.tier ?? "(none)", originMarker(origins.tier)],
58
+ [
59
+ "tier",
60
+ `${config.tier ?? "(none)"} — ${tierScope(config.tier)}`,
61
+ originMarker(origins.tier),
62
+ ],
35
63
  ["channel", config.channel, originMarker(origins.channel)],
36
64
  ["detail", config.detail ?? "standard", originMarker(origins.detail)],
37
65
  ["packs", entryNames(config.packs ?? []), originMarker(origins.packs)],
@@ -84,7 +112,7 @@ export const configShowFormatters: Formatters<ConfigShowData> = {
84
112
  const lines = [
85
113
  "## Configuration",
86
114
  "",
87
- `- **Tier:** ${config.tier ?? "none"}${originMarker(origins.tier)}`,
115
+ `- **Tier:** ${config.tier ?? "none"} — ${tierScope(config.tier)}${originMarker(origins.tier)}`,
88
116
  `- **Channel:** ${config.channel}${originMarker(origins.channel)}`,
89
117
  `- **Detail:** ${config.detail ?? "standard"}${originMarker(origins.detail)}`,
90
118
  `- **Packs:** ${entryNames(config.packs ?? [])}${originMarker(origins.packs)}`,
@@ -15,7 +15,8 @@
15
15
  * `create` verbs take. No new package edge: pragma already depends on
16
16
  * summon-core, and never on the summon bin.
17
17
  *
18
- * Five generators are covered: component × {react, svelte, lit}, package, and
18
+ * Five generators are covered across seven fixtures: component × {react, svelte, lit},
19
+ * package, and
19
20
  * application. A cross-binary subprocess guard lives in byteEquality.subprocess.test.ts.
20
21
  */
21
22
 
@@ -175,6 +176,32 @@ const cases: Case[] = [
175
176
  runInstall: false,
176
177
  },
177
178
  },
179
+ {
180
+ fixture: "application-spa",
181
+ kind: "application",
182
+ framework: "",
183
+ params: {
184
+ appPath: "my-app",
185
+ forms: true,
186
+ intl: false,
187
+ relay: false,
188
+ rendering: "spa",
189
+ runInstall: false,
190
+ },
191
+ },
192
+ {
193
+ fixture: "application-spa-all",
194
+ kind: "application",
195
+ framework: "",
196
+ params: {
197
+ appPath: "my-app",
198
+ forms: true,
199
+ intl: true,
200
+ relay: true,
201
+ rendering: "spa",
202
+ runInstall: false,
203
+ },
204
+ },
178
205
  ];
179
206
 
180
207
  describe("byte-equality goldens — pragma create \u2261 the conformance reference (PROTECTED)", () => {
@@ -688,6 +688,7 @@ describe("projection fidelity — the committed surface IS the live generators (
688
688
  "appPath",
689
689
  "forms",
690
690
  "intl",
691
+ "rendering",
691
692
  "relay",
692
693
  "runInstall",
693
694
  ]);
@@ -523,11 +523,12 @@ export const createVerbs: Record<
523
523
  ),
524
524
  application: createVerb(
525
525
  "application",
526
- "Scaffold a full React application with SSR and routing.",
526
+ "Scaffold a full React application with routing, and either server-side rendering or a client-only SPA.",
527
527
  applicationParams,
528
528
  [
529
529
  { cmd: `${BIN_NAME} create application react my-app` },
530
530
  { cmd: `${BIN_NAME} create application react my-app --relay` },
531
+ { cmd: `${BIN_NAME} create application react my-app --rendering spa` },
531
532
  ],
532
533
  ),
533
534
  };
@@ -655,7 +655,10 @@ describe("the mounted create grammar (subprocess)", () => {
655
655
  "--type": "library",
656
656
  "--description": "A replied library.",
657
657
  },
658
- "application/react": { "--app-path": "replied-app" },
658
+ "application/react": {
659
+ "--app-path": "replied-app",
660
+ "--rendering": "spa",
661
+ },
659
662
  };
660
663
  for (const [commandPath, surface] of Object.entries(CREATE_SURFACE)) {
661
664
  it(`${commandPath}: the refusal's own instruction WORKS — its Missing tokens supplied back (+ --dry-run) preview, exit 0`, () => {
@@ -17,7 +17,7 @@ export const CREATE_SURFACE: Readonly<Record<string, SurfaceCommand>> = {
17
17
  "application",
18
18
  "react"
19
19
  ],
20
- "description": "Scaffold a complete React application with SSR and routing",
20
+ "description": "Scaffold a complete React application with routing, and either server-side rendering or a client-only SPA",
21
21
  "prompts": [
22
22
  {
23
23
  "name": "appPath",
@@ -41,6 +41,23 @@ export const CREATE_SURFACE: Readonly<Record<string, SurfaceCommand>> = {
41
41
  "default": false,
42
42
  "group": "Application"
43
43
  },
44
+ {
45
+ "name": "rendering",
46
+ "type": "select",
47
+ "message": "Rendering — ssr keeps the server layer (SSR servers and sitemap), spa is client-only",
48
+ "default": "ssr",
49
+ "choices": [
50
+ {
51
+ "label": "ssr - server-side rendering (Express + Bun servers, sitemap)",
52
+ "value": "ssr"
53
+ },
54
+ {
55
+ "label": "spa - client-only single-page app (no server layer)",
56
+ "value": "spa"
57
+ }
58
+ ],
59
+ "group": "Application"
60
+ },
44
61
  {
45
62
  "name": "relay",
46
63
  "type": "confirm",
@@ -278,6 +295,11 @@ export const CREATE_CLI_SYNTAX: Readonly<
278
295
  "takesValue": false,
279
296
  "kebabName": "intl"
280
297
  },
298
+ "rendering": {
299
+ "flag": "--rendering",
300
+ "takesValue": true,
301
+ "kebabName": "rendering"
302
+ },
281
303
  "relay": {
282
304
  "flag": "--relay",
283
305
  "takesValue": false,
@@ -258,9 +258,16 @@ describe("cross-CLI conformance matrix (PROTECTED)", () => {
258
258
  // here, which is the point).
259
259
  it("every declared generator binding path is driven by a conformance fixture", () => {
260
260
  expect(
261
- [...CONFORMANCE_FIXTURES]
262
- .map((fixture) => commandPathOf(fixture.generator))
263
- .sort(),
261
+ // Deduplicated: the assertion is that every declared path is COVERED, and
262
+ // one path may carry several fixtures — application/react is driven by
263
+ // both the SSR arm and the SPA arm.
264
+ [
265
+ ...new Set(
266
+ CONFORMANCE_FIXTURES.map((fixture) =>
267
+ commandPathOf(fixture.generator),
268
+ ),
269
+ ),
270
+ ].sort(),
264
271
  ).toEqual(
265
272
  Object.values(CREATE_GENERATORS)
266
273
  .flatMap((binding) => binding.paths as readonly string[])
@@ -13,7 +13,7 @@ import { afterAll, beforeAll, describe, expect, it } from "vitest";
13
13
  import { compilePack } from "../kernel/packs/compile.js";
14
14
  import type { LookupOutput } from "../kernel/packs/resolveEntity.js";
15
15
  import { parsePackDefinition } from "../kernel/packs/schema.js";
16
- import type { PackRow } from "../kernel/packs/types.js";
16
+ import type { PackPage } from "../kernel/packs/types.js";
17
17
  import { distributionSource } from "../kernel/packs/types.js";
18
18
  import { verbKey } from "../kernel/packs/uniqueness.js";
19
19
  import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
@@ -122,7 +122,7 @@ describe("standard story definition (PROTECTED)", () => {
122
122
 
123
123
  describe("standard list parity", () => {
124
124
  it("lists every standard with the uniform row shape", async () => {
125
- const rows = (await verb("list").run({}, rt)) as PackRow[];
125
+ const rows = ((await verb("list").run({}, rt)) as PackPage).rows;
126
126
  expect(rows.map((r) => r.name)).toEqual([
127
127
  "code/function/purity",
128
128
  "react/component/props",
@@ -135,25 +135,26 @@ describe("standard list parity", () => {
135
135
  });
136
136
 
137
137
  it("filters by category and searches, conjunctively", async () => {
138
- const react = (await verb("list").run(
139
- { category: "react" },
140
- rt,
141
- )) as PackRow[];
138
+ const react = (
139
+ (await verb("list").run({ category: "react" }, rt)) as PackPage
140
+ ).rows;
142
141
  expect(react.map((r) => r.name).sort()).toEqual([
143
142
  "react/component/props",
144
143
  "react/component/structure",
145
144
  ]);
146
- const searched = (await verb("list").run(
147
- { category: "react", search: "structure" },
148
- rt,
149
- )) as PackRow[];
145
+ const searched = (
146
+ (await verb("list").run(
147
+ { category: "react", search: "structure" },
148
+ rt,
149
+ )) as PackPage
150
+ ).rows;
150
151
  expect(searched.map((r) => r.name)).toEqual(["react/component/structure"]);
151
152
  });
152
153
  });
153
154
 
154
155
  describe("standard categories parity", () => {
155
156
  it("lists categories with their standard counts", async () => {
156
- const rows = (await verb("categories").run({}, rt)) as PackRow[];
157
+ const rows = ((await verb("categories").run({}, rt)) as PackPage).rows;
157
158
  const counts = new Map(rows.map((r) => [r.name, Number(r.count)]));
158
159
  expect(counts.get("react")).toBe(2);
159
160
  expect(counts.get("code")).toBe(1);
@@ -14,6 +14,7 @@
14
14
  import { describe, expect, it } from "vitest";
15
15
  import { compilePack } from "../kernel/packs/compile.js";
16
16
  import { parsePackDefinition } from "../kernel/packs/schema.js";
17
+ import { storyIssues } from "../kernel/packs/storyRules.js";
17
18
  import { DEFAULT_PREFIX_MAP } from "../kernel/render/prefixes.js";
18
19
  import type { VerbSpec } from "../kernel/spec/types.js";
19
20
  import { VOCABULARY } from "../kernel/vocabulary.js";
@@ -45,16 +46,85 @@ describe("the distribution's declared stories (PROTECTED)", () => {
45
46
  "standard",
46
47
  "tier",
47
48
  "token",
49
+ "variable",
48
50
  ]);
49
51
  });
50
52
 
51
- it.each([
52
- ...declaredStories.keys(),
53
- ])("the %s story validates against the pack grammar, unchanged", (noun) => {
54
- const story = declaredStories.get(noun);
55
- expect(
56
- parsePackDefinition(JSON.parse(JSON.stringify(story)), "pragma.conf.ts"),
57
- ).toEqual(story);
53
+ it.each([...declaredStories.keys()])(
54
+ "the %s story validates against the pack grammar, unchanged",
55
+ (noun) => {
56
+ const story = declaredStories.get(noun);
57
+ expect(
58
+ parsePackDefinition(
59
+ JSON.parse(JSON.stringify(story)),
60
+ "pragma.conf.ts",
61
+ ),
62
+ ).toEqual(story);
63
+ },
64
+ );
65
+
66
+ it('every declared filter names "values" or a "vocabulary"', () => {
67
+ // The rule the round-trip above already enforces, said OUT LOUD. It was
68
+ // only implied there — a story satisfies the whole grammar or it does not,
69
+ // and a reader looking for this particular property found no assertion
70
+ // claiming it. It is the property that separates a refusal (INVALID_INPUT
71
+ // naming the admissible values) from a calm empty page, so it is worth a
72
+ // test that fails with the rule's own name on it.
73
+ const filters = [...declaredStories].flatMap(([noun, story]) => [
74
+ ...(story.list?.filters ?? []).map(
75
+ (filter) => [`${noun} list --${filter.param}`, filter] as const,
76
+ ),
77
+ ...(story.verbs ?? []).flatMap((verb) =>
78
+ (verb.filters ?? []).map(
79
+ (filter) =>
80
+ [`${noun} ${verb.verb} --${filter.param}`, filter] as const,
81
+ ),
82
+ ),
83
+ ]);
84
+ // A guard, not a no-op: an empty derivation would make this vacuous.
85
+ expect(filters.map(([label]) => label).sort()).toEqual([
86
+ "concept list --type",
87
+ "implementation list --library",
88
+ "implementation list --platform",
89
+ "standard list --category",
90
+ "token consumers --key",
91
+ "token consumers --state",
92
+ "token consumers --symbol",
93
+ "token consumers --variable",
94
+ "token list --channelOf",
95
+ "token list --type",
96
+ "token values --position",
97
+ "token values --symbol",
98
+ "variable chain --symbol",
99
+ "variable chain --variable",
100
+ "variable list --coordinate",
101
+ "variable list --platform",
102
+ "variable list --symbol",
103
+ "variable list --tier",
104
+ "variable list --visibility",
105
+ ]);
106
+ for (const [label, filter] of filters) {
107
+ expect(
108
+ filter.values !== undefined || filter.vocabulary !== undefined,
109
+ `${label} declares neither "values" nor a "vocabulary"`,
110
+ ).toBe(true);
111
+ // And never both — a declared set IS the vocabulary.
112
+ expect(
113
+ filter.values !== undefined && filter.vocabulary !== undefined,
114
+ `${label} declares both`,
115
+ ).toBe(false);
116
+ }
117
+ });
118
+
119
+ it("every declared story satisfies the compilability rules", () => {
120
+ // These rules are checked in `compileStoryModule`, which is what makes them
121
+ // reach the distribution's own stories rather than only the ones zod sees.
122
+ // Asserted here as a property of the CONFIG so a violation is a red test
123
+ // and not an import-time throw in every command.
124
+ for (const [noun, story] of declaredStories) {
125
+ const issues = storyIssues(story);
126
+ expect(issues, `${noun} has compilability issues`).toEqual([]);
127
+ }
58
128
  });
59
129
 
60
130
  it("every declared noun reaches the registry as an overridable module", () => {
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Covers the two things `checkNodeVersion` owns beyond the range comparison:
3
+ * the shape of the CheckResult, and that the failure path quotes the declared
4
+ * range rather than a number of its own. The range logic itself is pinned in
5
+ * `isSupportedNodeVersion.test.ts`.
6
+ */
7
+
8
+ import { afterEach, describe, expect, it, vi } from "vitest";
9
+ import { SUPPORTED_NODE_RANGE } from "../../../constants.js";
10
+ import { checkNodeVersion } from "./checkNodeVersion.js";
11
+
12
+ /** Swap `process.versions.node`, which is read-only in the normal way. */
13
+ function withNodeVersion(version: string) {
14
+ vi.spyOn(process, "versions", "get").mockReturnValue({
15
+ ...process.versions,
16
+ node: version,
17
+ });
18
+ }
19
+
20
+ afterEach(() => {
21
+ vi.restoreAllMocks();
22
+ });
23
+
24
+ describe("checkNodeVersion — reports against the declared range", () => {
25
+ it("passes on a supported version and reports it", async () => {
26
+ withNodeVersion("24.18.1");
27
+ const result = await checkNodeVersion();
28
+ expect(result).toEqual({
29
+ name: "Node version",
30
+ status: "pass",
31
+ detail: "v24.18.1",
32
+ });
33
+ });
34
+
35
+ it("fails on an unsupported version and quotes the declared range", async () => {
36
+ withNodeVersion("20.19.0");
37
+ const result = await checkNodeVersion();
38
+ expect(result.status).toBe("fail");
39
+ expect(result.detail).toContain("v20.19.0");
40
+ // The range must come from the manifest, never from a literal in the check.
41
+ expect(result.detail).toContain(SUPPORTED_NODE_RANGE);
42
+ expect(result.remedy).toContain(SUPPORTED_NODE_RANGE);
43
+ });
44
+
45
+ it("fails inside the 23.0-23.5 window the range excludes", async () => {
46
+ withNodeVersion("23.2.0");
47
+ expect((await checkNodeVersion()).status).toBe("fail");
48
+ });
49
+ });
@@ -1,25 +1,29 @@
1
+ import { SUPPORTED_NODE_RANGE } from "../../../constants.js";
1
2
  import type { CheckResult } from "../types.js";
2
-
3
- const MIN_NODE_MAJOR = 20;
3
+ import { isSupportedNodeVersion } from "./isSupportedNodeVersion.js";
4
4
 
5
5
  /**
6
- * Check that the Node.js major version is at least {@link MIN_NODE_MAJOR}.
6
+ * Check that the running Node.js version satisfies this package's declared
7
+ * `engines.node` range.
8
+ *
9
+ * The floor is a property of the manifest, not of this file: a check that
10
+ * hardcodes its own number can disagree with the range the package publishes,
11
+ * and a consumer then passes a check the install contract would have failed.
7
12
  *
8
13
  * @returns A CheckResult with the current version and pass/fail status.
9
14
  * @note Impure — reads `process.versions`.
10
15
  */
11
16
  export async function checkNodeVersion(): Promise<CheckResult> {
12
17
  const version = process.versions.node;
13
- const major = Number.parseInt(version.split(".")[0] ?? "0", 10);
14
18
 
15
- if (major >= MIN_NODE_MAJOR) {
19
+ if (isSupportedNodeVersion(version)) {
16
20
  return { name: "Node version", status: "pass", detail: `v${version}` };
17
21
  }
18
22
 
19
23
  return {
20
24
  name: "Node version",
21
25
  status: "fail",
22
- detail: `v${version} (requires >= ${MIN_NODE_MAJOR})`,
23
- remedy: `Install Node.js >= ${MIN_NODE_MAJOR}`,
26
+ detail: `v${version} (requires ${SUPPORTED_NODE_RANGE})`,
27
+ remedy: `Install Node.js ${SUPPORTED_NODE_RANGE}`,
24
28
  };
25
29
  }