@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,257 @@
1
+ /**
2
+ * Read a list story's author query as the three parts a page has to keep apart:
3
+ * the PROLOGUE it declares, the VARIABLES it projects, and the BODY a wrapping
4
+ * select can hold.
5
+ *
6
+ * Every list answer is one page now, and a page is a wrapping select over the
7
+ * story's own query ({@link ./buildListQuery.buildListQuery}). Three properties
8
+ * of the author's text decide whether that wrap is legal, and none of them can
9
+ * be asked of the store:
10
+ *
11
+ * - **The prologue cannot be wrapped.** `PREFIX` and `BASE` belong to a QUERY;
12
+ * a group graph pattern admits only patterns, so splicing author text that
13
+ * opens with `PREFIX` inside one is a parse error (`expected OPTIONAL`). The
14
+ * declarations are lifted to the wrapper's own prologue instead, in order, and
15
+ * the body from the `SELECT` keyword on is what gets wrapped.
16
+ * - **The projection has to be reproduced.** `SELECT *` in the wrapper hands
17
+ * back alphabetised binding keys, so `block list`'s rows would go from
18
+ * `{uri,name,type,tier}` to `{name,tier,type,uri}` and every `--format json`
19
+ * answer would change shape for no reason a caller asked for. The store cannot
20
+ * answer this either: `SelectResult.variables` is derived from the rows that
21
+ * came back, not from the query's header — over the shipped pack `block list`
22
+ * reports four of its five, because the fifth is unbound on the first row. So
23
+ * the projection is read from the query TEXT, which is the only place that
24
+ * states it. An author who wrote `SELECT *` is wrapped with `SELECT *`, which
25
+ * preserves their key order because it is the same alphabetisation twice.
26
+ * - **A dataset clause cannot be wrapped either.** `FROM` sits between the
27
+ * select clause and the where clause, and `SubSelect` has no place for it. It
28
+ * is REFUSED rather than lifted, because lifting it would mean cutting a span
29
+ * out of the middle of the author's text — the prologue is a prefix, which is
30
+ * why lifting that one is a split and not an edit.
31
+ *
32
+ * This is a lexer for the two clauses that precede the where clause, not a
33
+ * SPARQL parser. It covers the whole grammar of both — `Prologue ::= (BaseDecl
34
+ * | PrefixDecl)*` and `SelectClause ::= 'SELECT' ('DISTINCT'|'REDUCED')?
35
+ * ((Var | '(' Expression 'AS' Var ')')+ | '*')` — and it skips comments and
36
+ * string literals everywhere it scans, so a `#` inside a prefix IRI, a `# select
37
+ * the blocks` heading, and a `WHERE {` inside a `REPLACE` literal are all read
38
+ * as what they are. What it cannot read it says it cannot read, naming the
39
+ * shape it found: a refusal that misdiagnoses sends the author to the wrong
40
+ * file.
41
+ */
42
+
43
+ /** A SPARQL variable as this reader recognises it (`?name`, `$name`). */
44
+ const VARIABLE = /^[?$]([A-Za-z_][A-Za-z0-9_]*)/;
45
+
46
+ /** The `AS ?var` binding that names a projection expression's result. */
47
+ const ALIAS = /\bAS\s+[?$]([A-Za-z_][A-Za-z0-9_]*)\s*$/i;
48
+
49
+ /** Whitespace and `#` comments, which may sit between any two tokens. */
50
+ const IGNORABLE = /^(?:\s+|#[^\n\r]*)+/;
51
+
52
+ /** `PNAME_NS ::= PN_PREFIX? ':'` — the name a `PREFIX` declaration binds. */
53
+ const PREFIX_NAME = /^[A-Za-z][\w.-]*:|^:/;
54
+
55
+ /** An author query split where a wrapping select can take it over. */
56
+ export interface AuthorQuery {
57
+ /**
58
+ * The author's own `PREFIX`/`BASE` declarations, verbatim and in order, or
59
+ * `""` when there are none. Emitted as the WRAPPER's prologue.
60
+ */
61
+ readonly prologue: string;
62
+ /**
63
+ * The projected variables (without `?`), in author order — or `undefined`
64
+ * for `SELECT *`, which names no variables to reproduce.
65
+ */
66
+ readonly projection: readonly string[] | undefined;
67
+ /** The query from its `SELECT` keyword on: what a sub-select can hold. */
68
+ readonly body: string;
69
+ }
70
+
71
+ /** A read of an author query: the parts, or what stopped the reader. */
72
+ export type AuthorQueryRead =
73
+ | { readonly ok: true; readonly query: AuthorQuery }
74
+ | { readonly ok: false; readonly reason: string };
75
+
76
+ /**
77
+ * Split an author query into its prologue, its projection and its body.
78
+ *
79
+ * @param text - The author's SPARQL SELECT text.
80
+ * @returns The three parts, or the reason the text cannot be wrapped — a
81
+ * sentence fragment naming what the reader FOUND, for a caller to quote.
82
+ */
83
+ export function readAuthorQuery(text: string): AuthorQueryRead {
84
+ const prologue = readPrologue(text);
85
+ const start = skipIgnorable(text, prologue.end);
86
+ if (!/^SELECT\b/i.test(text.slice(start))) {
87
+ return { ok: false, reason: "its text declares no SPARQL SELECT" };
88
+ }
89
+ const clause = readSelectClause(text, start + "SELECT".length);
90
+ if (!clause.ok) return clause;
91
+ return {
92
+ ok: true,
93
+ query: {
94
+ prologue: text.slice(prologue.start, prologue.end),
95
+ projection: clause.projection,
96
+ body: text.slice(start),
97
+ },
98
+ };
99
+ }
100
+
101
+ /** Where the prologue's declarations begin and end (equal when there are none). */
102
+ function readPrologue(text: string): { start: number; end: number } {
103
+ const start = skipIgnorable(text, 0);
104
+ let end = start;
105
+ let at = start;
106
+ for (;;) {
107
+ const declaration = readDeclaration(text, at);
108
+ if (declaration === undefined) return { start, end };
109
+ end = declaration;
110
+ at = skipIgnorable(text, declaration);
111
+ }
112
+ }
113
+
114
+ /**
115
+ * The index just past one `PREFIX`/`BASE` declaration, or `undefined` when the
116
+ * text at {@link at} is not one.
117
+ *
118
+ * The IRI is scanned to its closing `>`, which is exact: `IRIREF` excludes `>`
119
+ * from its character class, so the first one closes it — and it does NOT
120
+ * exclude `#`, which is why a comment-aware scan has to read the IRI as an IRI
121
+ * rather than skip to end of line.
122
+ */
123
+ function readDeclaration(text: string, at: number): number | undefined {
124
+ const keyword = text.slice(at).match(/^(BASE|PREFIX)\b/i);
125
+ if (!keyword?.[1]) return undefined;
126
+ let index = skipIgnorable(text, at + keyword[0].length);
127
+ if (keyword[1].toUpperCase() === "PREFIX") {
128
+ const name = text.slice(index).match(PREFIX_NAME);
129
+ if (!name) return undefined;
130
+ index = skipIgnorable(text, index + name[0].length);
131
+ }
132
+ if (text[index] !== "<") return undefined;
133
+ const close = text.indexOf(">", index + 1);
134
+ return close === -1 ? undefined : close + 1;
135
+ }
136
+
137
+ /** Read the select clause that starts just past the `SELECT` keyword. */
138
+ function readSelectClause(
139
+ text: string,
140
+ from: number,
141
+ ):
142
+ | { readonly ok: true; readonly projection: readonly string[] | undefined }
143
+ | { readonly ok: false; readonly reason: string } {
144
+ const projected: string[] = [];
145
+ let star = false;
146
+ let at = from;
147
+ while (at < text.length) {
148
+ at = skipIgnorable(text, at);
149
+ const rest = text.slice(at);
150
+ // The clause ends at the WHERE keyword or at the group graph pattern's
151
+ // brace, whichever the author wrote (`WHERE` is optional in SPARQL).
152
+ if (rest === "" || /^(?:WHERE\b|\{)/i.test(rest)) break;
153
+ if (/^FROM\b/i.test(rest)) {
154
+ return {
155
+ ok: false,
156
+ reason:
157
+ "it carries a FROM dataset clause, and a sub-select cannot carry one",
158
+ };
159
+ }
160
+ // DISTINCT/REDUCED sit between the keyword and the projection.
161
+ const modifier = rest.match(/^(?:DISTINCT|REDUCED)\b/i);
162
+ if (modifier && projected.length === 0 && !star) {
163
+ at += modifier[0].length;
164
+ continue;
165
+ }
166
+ if (rest.startsWith("*")) {
167
+ star = true;
168
+ at += 1;
169
+ continue;
170
+ }
171
+ const variable = rest.match(VARIABLE);
172
+ if (variable?.[1]) {
173
+ projected.push(variable[1]);
174
+ at += variable[0].length;
175
+ continue;
176
+ }
177
+ if (rest.startsWith("(")) {
178
+ const end = matchingParen(text, at);
179
+ const alias = end === -1 ? null : text.slice(at + 1, end).match(ALIAS);
180
+ if (!alias?.[1]) {
181
+ return {
182
+ ok: false,
183
+ reason:
184
+ "its SELECT clause carries a parenthesised expression that names no result variable (`(… AS ?name)`)",
185
+ };
186
+ }
187
+ projected.push(alias[1]);
188
+ at = end + 1;
189
+ continue;
190
+ }
191
+ return {
192
+ ok: false,
193
+ reason: `its SELECT clause carries ${quoted(rest)}, which is neither a variable nor an \`(expression AS ?variable)\``,
194
+ };
195
+ }
196
+ if (star) {
197
+ return projected.length === 0
198
+ ? { ok: true, projection: undefined }
199
+ : {
200
+ ok: false,
201
+ reason: "its SELECT clause mixes `*` with named variables",
202
+ };
203
+ }
204
+ return projected.length > 0
205
+ ? { ok: true, projection: projected }
206
+ : { ok: false, reason: "its SELECT clause projects nothing" };
207
+ }
208
+
209
+ /** The first token of {@link rest}, quoted short enough to read in a message. */
210
+ function quoted(rest: string): string {
211
+ const token = rest.match(/^\S{1,24}/)?.[0] ?? rest.slice(0, 24);
212
+ return `"${token}"`;
213
+ }
214
+
215
+ /** Whitespace and comments consumed: the next index that is neither. */
216
+ function skipIgnorable(text: string, at: number): number {
217
+ const skip = text.slice(at).match(IGNORABLE);
218
+ return skip ? at + skip[0].length : at;
219
+ }
220
+
221
+ /**
222
+ * The index of the `)` closing the `(` at {@link open}, or -1 when unbalanced.
223
+ *
224
+ * Depth-counted rather than regex-matched: a projection expression nests
225
+ * (`(COALESCE(IF(…), 0) AS ?rank)`), and the first `)` is rarely the right one.
226
+ * Quoted strings are skipped so a `)` inside a literal cannot close the
227
+ * expression.
228
+ */
229
+ function matchingParen(text: string, open: number): number {
230
+ let depth = 0;
231
+ for (let index = open; index < text.length; index += 1) {
232
+ const char = text[index];
233
+ if (char === '"' || char === "'") {
234
+ index = skipLiteral(text, index);
235
+ continue;
236
+ }
237
+ if (char === "(") depth += 1;
238
+ if (char === ")") {
239
+ depth -= 1;
240
+ if (depth === 0) return index;
241
+ }
242
+ }
243
+ return -1;
244
+ }
245
+
246
+ /** The index of the quote closing the literal opened at {@link open}. */
247
+ function skipLiteral(text: string, open: number): number {
248
+ const quote = text[open];
249
+ for (let index = open + 1; index < text.length; index += 1) {
250
+ if (text[index] === "\\") {
251
+ index += 1;
252
+ continue;
253
+ }
254
+ if (text[index] === quote) return index;
255
+ }
256
+ return text.length;
257
+ }
@@ -0,0 +1,344 @@
1
+ /**
2
+ * The generated list query — the SHAPE it composes, and the one property that
3
+ * shape exists to guarantee: a caller's value never reaches query text.
4
+ *
5
+ * These are text assertions on purpose. The behavioural half — that the
6
+ * compiled predicates answer exactly what the row predicates answered — is
7
+ * pinned against a real store in `compile.test.ts` and, over the shipped
8
+ * corpus, in `listQuery.shipped.exec.test.ts`. What can only be asserted here
9
+ * is what the text does NOT contain: no author query edited, no user string
10
+ * spliced into a predicate, no page bounds inside the author's own modifiers.
11
+ */
12
+
13
+ import { describe, expect, it } from "vitest";
14
+ import { PragmaError } from "../../error/index.js";
15
+ import { buildListQuery } from "./buildListQuery.js";
16
+
17
+ /** The author query every case below wraps: aggregate, grouped, ordered. */
18
+ const AUTHORED = [
19
+ "SELECT ?uri ?name ?category",
20
+ ' (GROUP_CONCAT(DISTINCT ?slug; SEPARATOR=" ") AS ?categories)',
21
+ "WHERE {",
22
+ " ?uri a cs:CodeStandard ; rdfs:label ?name .",
23
+ " OPTIONAL { ?uri cs:hasCategory/cs:slug ?category . }",
24
+ " OPTIONAL { ?uri cs:hasCategory/skos:broader*/cs:slug ?slug . }",
25
+ "}",
26
+ "GROUP BY ?uri ?name ?category",
27
+ "ORDER BY ?name",
28
+ ].join("\n");
29
+
30
+ const window = { limit: 10, offset: 0 };
31
+ const label = "pragma.conf.ts";
32
+
33
+ describe("buildListQuery — nothing to filter", () => {
34
+ const paged = buildListQuery({
35
+ query: AUTHORED,
36
+ predicates: [],
37
+ window: { limit: 10, offset: 20 },
38
+ label,
39
+ });
40
+
41
+ it("still wraps, because appending a page to author text cannot be safe", () => {
42
+ // A story whose own text ends in `LIMIT`, `OFFSET` or a trailing `VALUES`
43
+ // is a syntax error the moment a page is appended to it — and the page has
44
+ // a default, so that error fires on every call rather than on paged ones.
45
+ // The wrap is the only shape that holds for all of them.
46
+ expect(paged).toContain(` {\n${AUTHORED}\n }`);
47
+ expect(paged.startsWith("SELECT ?uri ?name ?category ?categories\n")).toBe(
48
+ true,
49
+ );
50
+ expect(paged.trimEnd().endsWith("LIMIT 10 OFFSET 20")).toBe(true);
51
+ });
52
+
53
+ it("carries no predicate at all — an unfiltered page filters nothing", () => {
54
+ expect(paged).not.toContain("FILTER");
55
+ expect(paged).not.toContain("VALUES");
56
+ });
57
+
58
+ it("omits OFFSET 0, which is the no-op it looks like", () => {
59
+ const first = buildListQuery({
60
+ query: AUTHORED,
61
+ predicates: [],
62
+ window,
63
+ label,
64
+ });
65
+ expect(first.trimEnd().endsWith("LIMIT 10")).toBe(true);
66
+ expect(first).not.toContain("OFFSET");
67
+ });
68
+
69
+ it("pages an author query that already ends in its own LIMIT", () => {
70
+ // The shape the appended modifier broke: `LIMIT 5 LIMIT 10` is not a query.
71
+ // Inside a sub-select the author's own cap is legal, and the page's cap
72
+ // applies to what that cap returned.
73
+ const authored = "SELECT ?s WHERE { ?s ?p ?o } ORDER BY ?s LIMIT 5";
74
+ const query = buildListQuery({
75
+ query: authored,
76
+ predicates: [],
77
+ window,
78
+ label,
79
+ });
80
+ expect(query).toContain(` {\n${authored}\n }`);
81
+ expect(query.trimEnd().endsWith("LIMIT 10")).toBe(true);
82
+ });
83
+
84
+ it("pages an author query that already ends in a trailing VALUES block", () => {
85
+ const authored = 'SELECT ?s WHERE { ?s ?p ?o } VALUES ?s { "a" }';
86
+ const query = buildListQuery({
87
+ query: authored,
88
+ predicates: [],
89
+ window,
90
+ label,
91
+ });
92
+ expect(query).toContain(` {\n${authored}\n }`);
93
+ });
94
+
95
+ it("keeps an author's SELECT * as SELECT *, which is its own key order", () => {
96
+ // `SELECT *` in the wrapper over an author `SELECT *` is the same
97
+ // alphabetisation twice, so the rows a caller gets keep their shape.
98
+ const query = buildListQuery({
99
+ query: "SELECT * WHERE { ?s ?p ?o }",
100
+ predicates: [],
101
+ window,
102
+ label,
103
+ });
104
+ expect(query).toBe(
105
+ [
106
+ "SELECT *",
107
+ "WHERE {",
108
+ " {",
109
+ "SELECT * WHERE { ?s ?p ?o }",
110
+ " }",
111
+ "}",
112
+ "LIMIT 10",
113
+ ].join("\n"),
114
+ );
115
+ });
116
+ });
117
+
118
+ describe("buildListQuery — the author's own prologue", () => {
119
+ const AUTHORED_WITH_PROLOGUE = [
120
+ "PREFIX ex: <https://example.test/x#>",
121
+ "PREFIX ds: <https://example.test/ds#>",
122
+ "SELECT ?uri ?kind WHERE { ?uri ex:kind ?kind }",
123
+ ].join("\n");
124
+
125
+ it("lifts PREFIX lines to the wrapper's prologue, not into the pattern", () => {
126
+ // Spliced inside the group graph pattern — where only patterns may appear —
127
+ // a `PREFIX` line is a parse error, and a story shaped this way died the
128
+ // moment somebody passed a filter.
129
+ const query = buildListQuery({
130
+ query: AUTHORED_WITH_PROLOGUE,
131
+ predicates: [{ variable: "kind", match: "exact", terms: ["a"] }],
132
+ window,
133
+ label,
134
+ });
135
+ expect(query.startsWith("PREFIX ex: <https://example.test/x#>\n")).toBe(
136
+ true,
137
+ );
138
+ // The body is a SUFFIX of the author's text, byte for byte: a split, not
139
+ // an edit.
140
+ expect(query).toContain(
141
+ " {\nSELECT ?uri ?kind WHERE { ?uri ex:kind ?kind }\n }",
142
+ );
143
+ // And the prologue is above the wrapper's own SELECT, exactly once.
144
+ expect(query.indexOf("PREFIX ds:")).toBeLessThan(
145
+ query.indexOf("SELECT ?uri ?kind\nWHERE {"),
146
+ );
147
+ expect(query.match(/PREFIX ds:/g)).toHaveLength(1);
148
+ });
149
+
150
+ it("lifts it for an unfiltered page too", () => {
151
+ const query = buildListQuery({
152
+ query: AUTHORED_WITH_PROLOGUE,
153
+ predicates: [],
154
+ window,
155
+ label,
156
+ });
157
+ expect(query.startsWith("PREFIX ex:")).toBe(true);
158
+ expect(query).not.toContain(" {\nPREFIX");
159
+ });
160
+
161
+ it("emits nothing extra for a story that declares no prologue", () => {
162
+ const query = buildListQuery({
163
+ query: AUTHORED,
164
+ predicates: [],
165
+ window,
166
+ label,
167
+ });
168
+ expect(query.startsWith("SELECT ?uri ?name ?category ?categories\n")).toBe(
169
+ true,
170
+ );
171
+ expect(query).not.toContain("PREFIX");
172
+ });
173
+ });
174
+
175
+ describe("buildListQuery — something to filter", () => {
176
+ const filtered = buildListQuery({
177
+ query: AUTHORED,
178
+ predicates: [{ variable: "categories", match: "set", terms: ["testing"] }],
179
+ search: { variables: ["name", "description"], term: "unit" },
180
+ window: { limit: 10, offset: 30 },
181
+ label,
182
+ });
183
+
184
+ it("wraps the author query in a sub-select, verbatim", () => {
185
+ expect(filtered).toContain(` {\n${AUTHORED}\n }`);
186
+ // The author's own GROUP BY and ORDER BY survive inside the sub-select,
187
+ // which is the whole reason a `set` filter can be expressed at all: the
188
+ // cell it reads is what that GROUP BY computed.
189
+ expect(filtered).toContain("GROUP BY ?uri ?name ?category");
190
+ expect(filtered).toContain("ORDER BY ?name");
191
+ });
192
+
193
+ it("projects the author's own variables, in the author's own order", () => {
194
+ expect(
195
+ filtered.startsWith("SELECT ?uri ?name ?category ?categories\n"),
196
+ ).toBe(true);
197
+ // `SELECT *` here would return alphabetised binding keys, changing the
198
+ // shape of every JSON answer for no reason a caller asked for.
199
+ expect(filtered).not.toContain("SELECT *");
200
+ });
201
+
202
+ it("carries the page on the WRAPPER, after the predicates", () => {
203
+ expect(filtered.trimEnd().endsWith("LIMIT 10 OFFSET 30")).toBe(true);
204
+ // Not inside the author query, where it would cut before the filter ran.
205
+ expect(filtered.indexOf("FILTER EXISTS")).toBeLessThan(
206
+ filtered.lastIndexOf("LIMIT 10 OFFSET 30"),
207
+ );
208
+ });
209
+
210
+ it("binds each value through VALUES rather than into the predicate", () => {
211
+ expect(filtered).toContain('VALUES ?__pragmaFilter0 { "testing" }');
212
+ expect(filtered).toContain('VALUES ?__pragmaSearch { "unit" }');
213
+ // The comparison names the bound variable — the literal appears once, in
214
+ // the VALUES row, and nowhere inside an expression.
215
+ expect(filtered).toContain("LCASE(?__pragmaFilter0)");
216
+ expect(filtered.match(/"testing"/g)).toHaveLength(1);
217
+ });
218
+
219
+ it("wraps each VALUES in FILTER EXISTS, so a union cannot duplicate a row", () => {
220
+ // Joined into the group, `VALUES` with two rows would pair every row with
221
+ // every requested value, and a `set` cell carrying both would come back
222
+ // twice. EXISTS makes it a per-row boolean instead.
223
+ const union = buildListQuery({
224
+ query: AUTHORED,
225
+ predicates: [
226
+ { variable: "categories", match: "set", terms: ["testing", "react"] },
227
+ ],
228
+ window,
229
+ label,
230
+ });
231
+ expect(union).toContain(
232
+ 'FILTER EXISTS { VALUES ?__pragmaFilter0 { "testing" "react" }',
233
+ );
234
+ });
235
+ });
236
+
237
+ /**
238
+ * The generated query with every string LITERAL blanked out — what is left is
239
+ * the query TEXT, which is the only place a caller's value could do harm.
240
+ *
241
+ * Asserting on the raw text is not enough: an escaped value legitimately
242
+ * CONTAINS the words a caller hoped to inject, so counting `UNION` over the
243
+ * whole string counts their own string back at them.
244
+ */
245
+ function queryTextOnly(query: string): string {
246
+ return query.replace(/"(?:[^"\\]|\\.)*"/g, '""');
247
+ }
248
+
249
+ describe("buildListQuery — injection safety", () => {
250
+ it("escapes a value that tries to close its own literal and add a clause", () => {
251
+ const hostile = '" } UNION { ?uri a ds:Secret . VALUES ?x { "';
252
+ const query = buildListQuery({
253
+ query: AUTHORED,
254
+ predicates: [{ variable: "category", match: "exact", terms: [hostile] }],
255
+ window,
256
+ label,
257
+ });
258
+ // The quote is escaped, so the literal never closes and the injected
259
+ // pattern stays a string. One VALUES block, one FILTER EXISTS, no UNION.
260
+ expect(query).toContain(
261
+ '{ "\\" } UNION { ?uri a ds:Secret . VALUES ?x { \\"" }',
262
+ );
263
+ // With the literals blanked, the query text carries exactly the one
264
+ // VALUES block the builder minted, and no UNION at all.
265
+ const text = queryTextOnly(query);
266
+ expect(text.match(/VALUES/g)).toHaveLength(1);
267
+ expect(text).not.toContain("UNION");
268
+ expect(text).not.toContain("ds:Secret");
269
+ });
270
+
271
+ it("escapes a newline, so a value cannot start a line of query text", () => {
272
+ const query = buildListQuery({
273
+ query: AUTHORED,
274
+ predicates: [
275
+ { variable: "category", match: "exact", terms: ["a\nLIMIT 1"] },
276
+ ],
277
+ window,
278
+ label,
279
+ });
280
+ expect(query).toContain('{ "a\\nLIMIT 1" }');
281
+ expect(queryTextOnly(query).match(/LIMIT/g)).toHaveLength(1);
282
+ });
283
+
284
+ it("escapes a search term the same way", () => {
285
+ const query = buildListQuery({
286
+ query: AUTHORED,
287
+ predicates: [],
288
+ search: { variables: ["name"], term: '"' },
289
+ window,
290
+ label,
291
+ });
292
+ expect(query).toContain('{ "\\"" }');
293
+ });
294
+ });
295
+
296
+ describe("buildListQuery — a story it cannot serve", () => {
297
+ const build = (query: string, filtered = true) =>
298
+ buildListQuery({
299
+ query,
300
+ predicates: filtered
301
+ ? [{ variable: "kind", match: "exact", terms: ["a"] }]
302
+ : [],
303
+ window,
304
+ label,
305
+ });
306
+
307
+ it("refuses a filterable story whose SELECT projects *", () => {
308
+ expect(() =>
309
+ build("SELECT * WHERE { ?uri a ds:Thing ; ds:kind ?kind }"),
310
+ ).toThrow(PragmaError);
311
+ expect(() =>
312
+ build("SELECT * WHERE { ?uri a ds:Thing ; ds:kind ?kind }"),
313
+ ).toThrow(/project its variables by name/);
314
+ });
315
+
316
+ it("refuses a story whose own query uses the generated variable prefix", () => {
317
+ // Shadowing would leave the predicate comparing the caller's value against
318
+ // the author's variable — a filter that matches nothing, silently.
319
+ expect(() =>
320
+ build("SELECT ?kind ?__pragmaFilter0 WHERE { ?u ds:kind ?kind }"),
321
+ ).toThrow(/reserved variable prefix/);
322
+ });
323
+
324
+ it("refuses a dataset clause, and says it found one", () => {
325
+ // Not the `SELECT *` message it used to get: the author DOES project by
326
+ // name, and a wrong reason sends them to the wrong line of their own file.
327
+ // A sub-select has no place for `FROM`, and lifting it would mean cutting a
328
+ // span out of the middle of the author's text.
329
+ for (const filtered of [true, false]) {
330
+ expect(() =>
331
+ build(
332
+ "SELECT ?kind FROM <https://example.test/g> WHERE { ?u ds:kind ?kind }",
333
+ filtered,
334
+ ),
335
+ ).toThrow(/cannot be paged.*FROM dataset clause/s);
336
+ }
337
+ });
338
+
339
+ it("refuses a text that is not a SELECT, and says that", () => {
340
+ expect(() => build("ASK { ?s ?p ?o }", false)).toThrow(
341
+ /cannot be paged.*no SPARQL SELECT/s,
342
+ );
343
+ });
344
+ });