@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
@@ -14,17 +14,50 @@
14
14
  * statically by `capabilities/distribution.ts` and never revalidated at dispatch
15
15
  * (`collect.projectStoryTiers`'s default-origin carve-out); this runs for config-
16
16
  * and package-declared stories, and in `distribution.test.ts`'s round-trip.
17
+ *
18
+ * That carve-out is exactly why the rest of the compilability rules are NOT
19
+ * here. A rule stated only in this file reaches a third-party author at
20
+ * declaration and the distribution's own stories not at all — they meet it as a
21
+ * first-call CONFIG_ERROR. Those rules live in {@link ./storyRules}, which
22
+ * `compileStoryModule` runs for every tier, and {@link refineListShape} reports
23
+ * them as zod issues so a config author still reads them here, in the same
24
+ * words and at the same paths.
17
25
  */
18
26
 
19
27
  import { z } from "zod";
20
28
  import { DETAIL_LEVELS, RECOVERY_CLI_PREFIX } from "../../constants.js";
21
29
  import { PragmaError } from "../error/index.js";
22
- import type { PackDefinition } from "./types.js";
30
+ import { listShapeIssues, tierScopeIssues } from "./storyRules.js";
31
+ import {
32
+ type PackDefinition,
33
+ type PackList,
34
+ type PackTierScope,
35
+ RESERVED_STORY_PARAMS,
36
+ } from "./types.js";
23
37
 
24
38
  const NOUN_PATTERN = /^[a-z][a-z0-9-]*$/;
25
39
  /** Message for {@link NOUN_PATTERN} — third-party authors never see the regex. */
26
40
  const NOUN_MESSAGE = 'must be lowercase kebab-case, e.g. "design-token"';
27
- const FILTER_PARAM_PATTERN = /^[a-z][a-z0-9]*$/;
41
+ /**
42
+ * A filter's parameter name: lowercase-initial, then letters and digits.
43
+ *
44
+ * camelCase is admitted because the PROJECTOR already relies on it —
45
+ * `emitSurface`'s `emitVerb` writes a flag as `--${kebabCase(param)}`, so a
46
+ * param named `channelOf` was always going to become `--channel-of`, and only
47
+ * this gate refused to let anyone declare it. A story wanting a two-word
48
+ * dimension had no spelling at all: `channel-of` fails the pattern too, and
49
+ * `channelof` projects to the unreadable `--channelof`.
50
+ *
51
+ * This is a WIDENING of what an author may declare, matched to what the
52
+ * projector already emits, and it closes a tier split rather than opening one:
53
+ * the distribution's own stories are compiled statically and never revalidated
54
+ * (see `storyRules.ts`), so `pragma.conf.ts` could already declare a param this
55
+ * schema would have refused from a package. The same rule now reaches both.
56
+ *
57
+ * Still lowercase-INITIAL: a leading capital would kebab to a leading dash and
58
+ * Commander would read the flag as empty.
59
+ */
60
+ const FILTER_PARAM_PATTERN = /^[a-z][a-zA-Z0-9]*$/;
28
61
  const FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
29
62
  const GRAPHQL_NAME_PATTERN = /^[_A-Za-z][_0-9A-Za-z]*$/;
30
63
  /**
@@ -38,14 +71,22 @@ const GRAPHQL_NAME_PATTERN = /^[_A-Za-z][_0-9A-Za-z]*$/;
38
71
  const TERM_PATTERN =
39
72
  /^(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/[^<>"\s]+|\^?[A-Za-z][\w-]*:[^/<>"\s]+(?:\/\^?[A-Za-z][\w-]*:[^/<>"\s]+)*)$/;
40
73
 
41
- /** Params a filter/verb may not claim (they are the shared read vocabulary). */
42
- const RESERVED_PARAMS = new Set(["search", "detail", "name", "count"]);
74
+ /** Params a filter/verb may not claim — the kernel's own, from the grammar. */
75
+ const RESERVED_PARAMS = new Set(RESERVED_STORY_PARAMS);
43
76
 
44
77
  /**
45
78
  * Whether an author-supplied query is a SELECT (optionally preceded by its own
46
79
  * PREFIX lines). Every author query a story declares runs through `runSelect`,
47
80
  * which reads `result.bindings` — so a non-SELECT is a shape mismatch caught
48
81
  * here rather than downstream.
82
+ *
83
+ * Used for a `vocabulary.query` only, which is run exactly as the author wrote
84
+ * it. A `list.query` gets the real reader instead ({@link ./storyRules}, via
85
+ * `sparql/authorQuery.ts`), because this pattern is NARROWER than what a page
86
+ * can now serve: it admits `PREFIX` lines but not a leading comment, not
87
+ * `BASE`, and not a lower-case `prefix` — all of which the lift handles. A
88
+ * grammar that refuses the shape the kernel supports is the same defect as one
89
+ * that admits the shape it cannot, read from the other side.
49
90
  */
50
91
  function isSelectQuery(query: string): boolean {
51
92
  return /^\s*(?:PREFIX\s+[^\n]*\n\s*)*SELECT\s/i.test(query);
@@ -89,6 +130,12 @@ const filterSchema = z
89
130
  .refine((f) => !f.vocabulary || isSelectQuery(f.vocabulary.query), {
90
131
  message: '"vocabulary.query" must be a SPARQL SELECT query',
91
132
  });
133
+ // A filter must also say WHAT it admits, one way or the other — it used to be
134
+ // able to say neither and fall back to the values the returned ROWS carried,
135
+ // which a page makes actively wrong. That rule is a compilability rule rather
136
+ // than a shape rule, so it is checked in `storyRules.ts` (which the
137
+ // distribution's own stories reach too) and reported from `refineListShape`
138
+ // below at the same `…filters.N` path it always used.
92
139
 
93
140
  const searchSchema = z
94
141
  .object({
@@ -146,6 +193,7 @@ const fieldSchema = z
146
193
 
147
194
  const sectionSchema = fieldSchema.extend({
148
195
  kind: z.enum(["field", "code"]).optional(),
196
+ note: z.string().optional(),
149
197
  });
150
198
 
151
199
  const nestedExpandSchema = z
@@ -174,6 +222,7 @@ const expandFieldSchema = z
174
222
  property: term,
175
223
  label: z.string().optional(),
176
224
  graphqlField: graphqlName.optional(),
225
+ blankWhenSelf: z.literal(true).optional(),
177
226
  })
178
227
  .strict();
179
228
 
@@ -187,6 +236,12 @@ const expandSchema = z
187
236
  select: z.array(z.union([nestedExpandSchema, expandFieldSchema])).min(1),
188
237
  showWhenEmpty: z.boolean().optional(),
189
238
  level: z.string().optional(),
239
+ note: z.string().optional(),
240
+ orderBy: z.array(fieldName).min(1).optional(),
241
+ // Only "sparql": see PackExpand.source — the SPARQL lane is every lookup's
242
+ // default, so an expand opting INTO GraphQL would ask for a document the
243
+ // lookup never builds.
244
+ source: z.literal("sparql").optional(),
190
245
  })
191
246
  .strict();
192
247
 
@@ -247,6 +302,24 @@ const scopeWeightSchema = z
247
302
  })
248
303
  .strict();
249
304
 
305
+ /**
306
+ * The declared tier hierarchy (`PackTierScope`) that SCOPES a noun's reads.
307
+ *
308
+ * Four terms, no options: the class whose instances are the tiers, the edge
309
+ * from an entity to its tier, the tier property whose `/`-separated name IS the
310
+ * hierarchy, and the base tier every scope keeps. Everything else about the
311
+ * scope — the default, the ancestors, the escape — is the kernel's rule, not a
312
+ * knob, so a pack cannot declare a noun that scopes differently from the rest.
313
+ */
314
+ const tierScopeSchema = z
315
+ .object({
316
+ type: term,
317
+ via: term,
318
+ by: term,
319
+ base: term.optional(),
320
+ })
321
+ .strict();
322
+
250
323
  const lookupSchema = z
251
324
  .object({
252
325
  source: z.enum(["sparql", "graphql"]).optional(),
@@ -277,6 +350,7 @@ const definitionSchema = z
277
350
  verbs: z.array(verbSchema).min(1).optional(),
278
351
  lookup: lookupSchema.optional(),
279
352
  colophon: z.string().optional(),
353
+ tierScope: tierScopeSchema.optional(),
280
354
  })
281
355
  .strict()
282
356
  .superRefine((def, ctx) => {
@@ -286,17 +360,19 @@ const definitionSchema = z
286
360
  message: 'a pack must declare at least one of "list" or "lookup".',
287
361
  });
288
362
  }
289
- if (def.list && !isSelectQuery(def.list.query)) {
290
- ctx.addIssue({
291
- code: z.ZodIssueCode.custom,
292
- message: '"list.query" must be a SPARQL SELECT query.',
293
- path: ["list", "query"],
294
- });
295
- }
363
+ // "must be a SELECT" for a `list.query` is `refineListShape`'s to say, and
364
+ // it says it from the reader that actually splits the query — which admits
365
+ // the prologue shapes this file's own pattern does not.
296
366
  refineVerbNames(def, ctx);
297
- if (def.list) refineFilterParams(def.list.filters, ["list"], ctx);
367
+ if (def.list) {
368
+ refineFilterParams(def.list.filters, ["list"], ctx);
369
+ refineListShape(def.list, ["list"], ctx);
370
+ refineTierScope(def.tierScope, def.list, ["list"], ctx);
371
+ }
298
372
  for (const [index, verb] of (def.verbs ?? []).entries()) {
299
373
  refineFilterParams(verb.filters, ["verbs", index], ctx);
374
+ refineListShape(verb, ["verbs", index], ctx);
375
+ refineTierScope(def.tierScope, verb, ["verbs", index], ctx);
300
376
  }
301
377
  if (def.lookup) refineLookup(def.lookup, ctx);
302
378
  });
@@ -360,6 +436,60 @@ function refineFilterParams(
360
436
  }
361
437
  }
362
438
 
439
+ /**
440
+ * The COMPILABILITY rules for one list-shaped body, reported as zod issues.
441
+ *
442
+ * The rules themselves live in {@link ./storyRules}, deliberately outside this
443
+ * module: zod runs only for config- and package-declared stories, and a rule
444
+ * whose violation is a CONFIG_ERROR has to reach the distribution's own stories
445
+ * too — which it does by being checked in `compileStoryModule`, the door every
446
+ * tier comes through. Stated in two places it would be two rules; delegated, a
447
+ * third-party author and the distribution read the same sentence.
448
+ *
449
+ * What they cover: a query a page cannot wrap (its own prologue is lifted, but
450
+ * a `FROM` clause and an unreadable projection cannot be), a filter or search
451
+ * over a variable the query does not project, a filtered query shadowing the
452
+ * generated variable prefix, and a filter that declares neither `values` nor a
453
+ * `vocabulary`. None of them could be checked while filters were predicates
454
+ * over returned rows: a variable that was not there simply matched nothing,
455
+ * silently, with exit 0.
456
+ */
457
+ function refineListShape(
458
+ shape: PackList,
459
+ path: readonly (string | number)[],
460
+ ctx: z.RefinementCtx,
461
+ ): void {
462
+ for (const issue of listShapeIssues(shape, path)) {
463
+ ctx.addIssue({
464
+ code: z.ZodIssueCode.custom,
465
+ message: issue.message,
466
+ path: [...issue.path],
467
+ });
468
+ }
469
+ }
470
+
471
+ /**
472
+ * The tier-scope rules, stated by the same reader the compiler uses.
473
+ *
474
+ * Delegated for the reason every other compilability rule is: a third-party
475
+ * author must be refused where they declare, and the distribution's own stories
476
+ * — which never see zod — must be held to the same rule at compile time.
477
+ */
478
+ function refineTierScope(
479
+ tierScope: PackTierScope | undefined,
480
+ shape: PackList,
481
+ path: readonly (string | number)[],
482
+ ctx: z.RefinementCtx,
483
+ ): void {
484
+ for (const issue of tierScopeIssues(tierScope, shape, path)) {
485
+ ctx.addIssue({
486
+ code: z.ZodIssueCode.custom,
487
+ message: issue.message,
488
+ path: [...issue.path],
489
+ });
490
+ }
491
+ }
492
+
363
493
  /** Cross-field lookup rules — the source rule (§3, F8). */
364
494
  function refineLookup(
365
495
  lookup: z.infer<typeof lookupSchema>,
@@ -446,14 +576,51 @@ function refineLookup(
446
576
  path: ["lookup"],
447
577
  });
448
578
  }
579
+ // The EXPAND's lane, not the lookup's: an expand may opt out of a graphql
580
+ // lookup's document, and the three rules below are rules about the lane
581
+ // that will actually resolve it.
582
+ const expandSource = expand.source ?? source;
449
583
  for (const entry of expand.select) {
450
- if ("relation" in entry && source !== "graphql") {
584
+ if ("relation" in entry && expandSource !== "graphql") {
451
585
  ctx.addIssue({
452
586
  code: z.ZodIssueCode.custom,
453
587
  message: `expand "${expand.name}" nests a relation, which requires lookup.source "graphql" (the SPARQL expand sub-SELECT is single-hop).`,
454
588
  path: ["lookup"],
455
589
  });
456
590
  }
591
+ if (
592
+ "blankWhenSelf" in entry &&
593
+ entry.blankWhenSelf &&
594
+ expandSource !== "sparql"
595
+ ) {
596
+ ctx.addIssue({
597
+ code: z.ZodIssueCode.custom,
598
+ message: `expand "${expand.name}" field "${entry.name}" sets "blankWhenSelf", which only the SPARQL lane can express — set the expand's "source" to "sparql".`,
599
+ path: ["lookup"],
600
+ });
601
+ }
602
+ }
603
+ if (expand.orderBy !== undefined) {
604
+ if (expandSource !== "sparql") {
605
+ ctx.addIssue({
606
+ code: z.ZodIssueCode.custom,
607
+ message: `expand "${expand.name}" declares "orderBy", which only the SPARQL lane can express — a GraphQL collection's order is the schema's. Set the expand's "source" to "sparql".`,
608
+ path: ["lookup"],
609
+ });
610
+ }
611
+ // A name that no select entry declares would order by an unbound
612
+ // variable, which SPARQL permits and which silently orders by nothing —
613
+ // so it is refused where it is written rather than ignored at runtime.
614
+ const selected = new Set(expand.select.map((entry) => entry.name));
615
+ for (const name of expand.orderBy) {
616
+ if (!selected.has(name)) {
617
+ ctx.addIssue({
618
+ code: z.ZodIssueCode.custom,
619
+ message: `expand "${expand.name}" orders by "${name}", which its "select" does not declare.`,
620
+ path: ["lookup"],
621
+ });
622
+ }
623
+ }
457
624
  }
458
625
  }
459
626
  }
@@ -0,0 +1,179 @@
1
+ /**
2
+ * The author-query reader: the prologue it lifts, the projection it reproduces,
3
+ * and the shapes it refuses BY NAME.
4
+ *
5
+ * The last of those is the point. Every refusal here used to be the same
6
+ * sentence — "project its variables by name" — whether the reader had found
7
+ * `SELECT *`, a comment where it wanted a variable, or a `FROM` clause. Two of
8
+ * those three were wrong, and a wrong reason sends an author to the wrong part
9
+ * of their own file. So each case below asserts what the message SAYS, not only
10
+ * that it refused.
11
+ */
12
+
13
+ import { describe, expect, it } from "vitest";
14
+ import { readAuthorQuery } from "./authorQuery.js";
15
+
16
+ /** The parts, for a query the reader can read. */
17
+ function parts(text: string) {
18
+ const read = readAuthorQuery(text);
19
+ if (!read.ok) throw new Error(`unreadable: ${read.reason}`);
20
+ return read.query;
21
+ }
22
+
23
+ /** The reason, for one it cannot. */
24
+ function reason(text: string): string {
25
+ const read = readAuthorQuery(text);
26
+ if (read.ok) throw new Error("expected a refusal");
27
+ return read.reason;
28
+ }
29
+
30
+ describe("the projection", () => {
31
+ it("reads bare variables and aliased expressions, in author order", () => {
32
+ expect(
33
+ parts(
34
+ [
35
+ "SELECT ?uri ?name ?category",
36
+ ' (GROUP_CONCAT(DISTINCT ?slug; SEPARATOR=" ") AS ?categories)',
37
+ "WHERE { ?uri a cs:CodeStandard }",
38
+ ].join("\n"),
39
+ ).projection,
40
+ ).toEqual(["uri", "name", "category", "categories"]);
41
+ });
42
+
43
+ it("reads through DISTINCT and through a nested projection expression", () => {
44
+ expect(
45
+ parts(
46
+ "SELECT DISTINCT ?a (COALESCE(IF(?d = 0, 1, 1 - (0.2 * ?d)), 0) AS ?rank) WHERE { ?a ds:x ?d }",
47
+ ).projection,
48
+ ).toEqual(["a", "rank"]);
49
+ });
50
+
51
+ it("reads $-sigil variables, and a projection with no WHERE keyword", () => {
52
+ expect(parts("SELECT $a $b { $a ds:x $b }").projection).toEqual(["a", "b"]);
53
+ });
54
+
55
+ it("answers undefined for SELECT * — the one projection it cannot enumerate", () => {
56
+ expect(parts("SELECT * WHERE { ?s ?p ?o }").projection).toBeUndefined();
57
+ });
58
+
59
+ it("is not fooled by a brace or a WHERE inside a string literal", () => {
60
+ expect(
61
+ parts(
62
+ 'SELECT ?a (REPLACE(?a, "WHERE {", ")") AS ?b) WHERE { ?a ds:x ?y }',
63
+ ).projection,
64
+ ).toEqual(["a", "b"]);
65
+ });
66
+
67
+ it("reads through a comment sitting where a variable was expected", () => {
68
+ // Previously undefined, which refused the story with a message saying it
69
+ // must not use `SELECT *` — which it does not.
70
+ expect(
71
+ parts("SELECT # the columns\n ?uri ?name\nWHERE { ?uri ds:name ?name }")
72
+ .projection,
73
+ ).toEqual(["uri", "name"]);
74
+ });
75
+ });
76
+
77
+ describe("the prologue", () => {
78
+ it("is empty, and the body is the whole text, when the author declares none", () => {
79
+ const query = "SELECT ?s WHERE { ?s ?p ?o }";
80
+ expect(parts(query)).toEqual({
81
+ prologue: "",
82
+ projection: ["s"],
83
+ body: query,
84
+ });
85
+ });
86
+
87
+ it("is lifted whole, and the body starts at SELECT", () => {
88
+ const read = parts(
89
+ [
90
+ "PREFIX a: <https://example.test/a#>",
91
+ "PREFIX b: <https://example.test/b#>",
92
+ "SELECT ?s WHERE { ?s a:p ?o }",
93
+ ].join("\n"),
94
+ );
95
+ expect(read.prologue).toBe(
96
+ "PREFIX a: <https://example.test/a#>\nPREFIX b: <https://example.test/b#>",
97
+ );
98
+ expect(read.body).toBe("SELECT ?s WHERE { ?s a:p ?o }");
99
+ });
100
+
101
+ it("reads a `#` and a `>`-free IRI as an IRI, not as a comment", () => {
102
+ // `IRIREF` excludes `>` and admits `#`, so the IRI ends at its own `>` and
103
+ // a `#` inside it does not start a comment.
104
+ const read = parts(
105
+ "prefix q: <https://example.test/x#select> SELECT ?s WHERE { ?s q:p ?o }",
106
+ );
107
+ expect(read.prologue).toBe("prefix q: <https://example.test/x#select>");
108
+ expect(read.projection).toEqual(["s"]);
109
+ });
110
+
111
+ it("tolerates lower case, a leading comment, and comments between declarations", () => {
112
+ const read = parts(
113
+ [
114
+ "# select the standards",
115
+ "base <https://example.test/>",
116
+ "# and their categories",
117
+ "Prefix cs: <https://example.test/cs#>",
118
+ "select ?s where { ?s cs:p ?o }",
119
+ ].join("\n"),
120
+ );
121
+ expect(read.prologue).toBe(
122
+ [
123
+ "base <https://example.test/>",
124
+ "# and their categories",
125
+ "Prefix cs: <https://example.test/cs#>",
126
+ ].join("\n"),
127
+ );
128
+ expect(read.body).toBe("select ?s where { ?s cs:p ?o }");
129
+ expect(read.projection).toEqual(["s"]);
130
+ });
131
+
132
+ it("is not confused by a `PREFIX` IRI containing the word select", () => {
133
+ expect(
134
+ parts("PREFIX p: <https://example.test/select#> SELECT ?s { ?s p:x ?o }")
135
+ .projection,
136
+ ).toEqual(["s"]);
137
+ });
138
+
139
+ it("takes the default prefix (`PREFIX : <…>`) as a declaration", () => {
140
+ const read = parts(
141
+ "PREFIX : <https://example.test/> SELECT ?s { ?s :x ?o }",
142
+ );
143
+ expect(read.prologue).toBe("PREFIX : <https://example.test/>");
144
+ });
145
+ });
146
+
147
+ describe("what it refuses, and what it says it found", () => {
148
+ it("names a FROM dataset clause", () => {
149
+ expect(
150
+ reason("SELECT ?s FROM <https://example.test/g> WHERE { ?s ?p ?o }"),
151
+ ).toContain("FROM dataset clause");
152
+ });
153
+
154
+ it("names a FROM NAMED clause the same way", () => {
155
+ expect(
156
+ reason(
157
+ "SELECT ?s FROM NAMED <https://example.test/g> WHERE { ?s ?p ?o }",
158
+ ),
159
+ ).toContain("FROM dataset clause");
160
+ });
161
+
162
+ it("names a text with no SELECT at all", () => {
163
+ expect(reason("ASK { ?s ?p ?o }")).toContain("no SPARQL SELECT");
164
+ });
165
+
166
+ it("names a projection that binds no result variable", () => {
167
+ expect(reason("SELECT (COUNT(?s)) WHERE { ?s ?p ?o }")).toContain(
168
+ "names no result variable",
169
+ );
170
+ });
171
+
172
+ it("names a token it does not recognise, and quotes it", () => {
173
+ expect(reason("SELECT ?a , ?b WHERE { ?a ?p ?b }")).toContain('","');
174
+ });
175
+
176
+ it("names a SELECT clause that projects nothing", () => {
177
+ expect(reason("SELECT WHERE { ?s ?p ?o }")).toContain("projects nothing");
178
+ });
179
+ });