@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,66 @@
1
+ /**
2
+ * `graph query` over the SHIPPED graph, addressed the way pragma addresses it.
3
+ *
4
+ * The two queries below are the ones a user actually typed, having copied the
5
+ * names out of `graph inspect` and `variable list`. Both were SPARQL parse
6
+ * errors — the graph's local names carry dots, slashes and leading dashes that
7
+ * `PN_LOCAL` cannot — and the failure arrived as a Unicode-range dump pointing
8
+ * at line 16 of a one-line query.
9
+ *
10
+ * Against the fixture graph this could only be argued; the names in question
11
+ * are the SHIPPED pack's, so this is where it is proven.
12
+ */
13
+
14
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
15
+ import { verbKey } from "../../kernel/packs/uniqueness.js";
16
+ import { bootRuntime } from "../../kernel/runtime/boot.js";
17
+ import type { PragmaRuntime } from "../../kernel/runtime/types.js";
18
+ import type { VerbSpec } from "../../kernel/spec/types.js";
19
+ import { TEST_FLAGS } from "../../testing/helpers/projectCli.js";
20
+ import { graphModule } from "./index.js";
21
+
22
+ type Select = { type: string; bindings?: readonly unknown[] };
23
+
24
+ const queryVerb = graphModule.verbs.find(
25
+ (v) => verbKey(v.path) === "graph query",
26
+ ) as VerbSpec;
27
+
28
+ let rt: PragmaRuntime;
29
+ beforeAll(() => {
30
+ rt = bootRuntime(TEST_FLAGS);
31
+ });
32
+ afterAll(async () => {
33
+ (await rt.store.get()).store.dispose();
34
+ });
35
+
36
+ const rows = async (sparql: string): Promise<readonly unknown[]> => {
37
+ const result = (await queryVerb.run({ sparql }, rt)) as Select;
38
+ expect(result.type).toBe("select");
39
+ return result.bindings ?? [];
40
+ };
41
+
42
+ describe("a name pragma printed can be pasted back (PROTECTED)", () => {
43
+ it("answers a block addressed as `graph inspect` prints it", async () => {
44
+ const bindings = await rows(
45
+ "SELECT ?p ?o WHERE { ds:global.component.button ?p ?o } LIMIT 2",
46
+ );
47
+ expect(bindings).toHaveLength(2);
48
+ });
49
+
50
+ it("answers a variable addressed as `variable list` prints it", async () => {
51
+ const bindings = await rows(
52
+ "SELECT ?p ?o WHERE { dt:s4/web/--color-text ?p ?o } LIMIT 2",
53
+ );
54
+ expect(bindings.length).toBeGreaterThan(0);
55
+ });
56
+
57
+ it("still answers the absolute form, byte for byte the same", async () => {
58
+ const prefixed = await rows(
59
+ "SELECT ?p ?o WHERE { ds:global.component.button ?p ?o } ORDER BY ?p ?o",
60
+ );
61
+ const absolute = await rows(
62
+ "SELECT ?p ?o WHERE { <https://ds.canonical.com/global.component.button> ?p ?o } ORDER BY ?p ?o",
63
+ );
64
+ expect(prefixed).toEqual(absolute);
65
+ });
66
+ });
@@ -116,6 +116,23 @@ describe("graph query — errors & boot", () => {
116
116
  ).toMatchObject({ tool: "ontology_list" });
117
117
  });
118
118
 
119
+ it("a syntax error is positioned in the CALLER's text, on one line", async () => {
120
+ // The facade prepends one PREFIX line per registered namespace, so the
121
+ // parser's "16:41" named a line the caller never wrote — and its expected-
122
+ // token dump ran to five lines of Unicode ranges.
123
+ let message = "";
124
+ try {
125
+ await queryVerb.run(
126
+ { sparql: "SELECT ?x\nWHERE { this is not valid sparql" },
127
+ fixture.runtime,
128
+ );
129
+ } catch (error) {
130
+ message = (error as Error).message;
131
+ }
132
+ expect(message.split("\n")).toHaveLength(1);
133
+ expect(message).toMatch(/error at line 2/);
134
+ });
135
+
119
136
  it("needsStore boots the store before the query runs", async () => {
120
137
  const fresh = await bootFixtureRuntime({
121
138
  ttl: CANONICAL_TTL,
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Pasting pragma's own names into `graph query`, and reading its complaints.
3
+ *
4
+ * The graph's local names are not SPARQL `PN_LOCAL`: `graph inspect` prints
5
+ * `ds:global.component.button` and `variable list` prints
6
+ * `dt:s4/web/--color-text`, and pasting either into `graph query` produced a
7
+ * five-line Unicode-range dump pointing at line 16 of a query the caller wrote
8
+ * one line of. These pin both halves of the fix — the expansion, and the
9
+ * position the failure is reported at.
10
+ */
11
+
12
+ import { describe, expect, it } from "vitest";
13
+ import { expandPrefixedNames, trimQueryError } from "./queryText.js";
14
+
15
+ const PREFIXES = {
16
+ ds: "https://ds.canonical.com/",
17
+ dt: "https://dt.canonical.com/",
18
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
19
+ };
20
+
21
+ const expand = (sparql: string): string =>
22
+ expandPrefixedNames(sparql, PREFIXES).text;
23
+
24
+ describe("expandPrefixedNames — the names pragma itself hands out", () => {
25
+ it("expands a dotted local part the grammar cannot carry", () => {
26
+ expect(
27
+ expand("SELECT ?p ?o WHERE { ds:global.component.button ?p ?o } LIMIT 2"),
28
+ ).toBe(
29
+ "SELECT ?p ?o WHERE { <https://ds.canonical.com/global.component.button> ?p ?o } LIMIT 2",
30
+ );
31
+ });
32
+
33
+ it("expands a local part with slashes and a leading dash", () => {
34
+ expect(expand("SELECT ?o WHERE { dt:s4/web/--color-text ?p ?o }")).toBe(
35
+ "SELECT ?o WHERE { <https://dt.canonical.com/s4/web/--color-text> ?p ?o }",
36
+ );
37
+ });
38
+
39
+ it("leaves a name the grammar already carries exactly as written", () => {
40
+ const query = "SELECT ?n WHERE { ?c a ds:Component ; rdfs:label ?n }";
41
+ expect(expand(query)).toBe(query);
42
+ expect(expandPrefixedNames(query, PREFIXES).rewritten).toBe(false);
43
+ });
44
+
45
+ it("keeps the caller's statement terminator out of the name", () => {
46
+ expect(expand("SELECT ?p WHERE { ds:a.b.c ?p ?o. }")).toBe(
47
+ "SELECT ?p WHERE { <https://ds.canonical.com/a.b.c> ?p ?o. }",
48
+ );
49
+ });
50
+
51
+ it("never rewrites inside a string literal", () => {
52
+ for (const quote of ['"', "'", '"""']) {
53
+ const query = `SELECT ?s WHERE { ?s ?p ${quote}ds:x/y${quote} }`;
54
+ expect(expand(query)).toBe(query);
55
+ }
56
+ });
57
+
58
+ it("never rewrites inside a comment or an IRI already written out", () => {
59
+ const query = [
60
+ "# see ds:global.component.button",
61
+ "SELECT ?p WHERE { <https://ds.canonical.com/a.b.c> ?p ?o }",
62
+ ].join("\n");
63
+ expect(expand(query)).toBe(query);
64
+ });
65
+
66
+ it("leaves a prefix the query will not be parsed with alone", () => {
67
+ const query = "SELECT ?p WHERE { nope:a.b.c ?p ?o }";
68
+ expect(expand(query)).toBe(query);
69
+ });
70
+
71
+ it("keeps a property path a path, expanding each step it must", () => {
72
+ expect(expand("SELECT ?o WHERE { ?s ds:a.b/ds:c ?o }")).toBe(
73
+ "SELECT ?o WHERE { ?s <https://ds.canonical.com/a.b>/ds:c ?o }",
74
+ );
75
+ });
76
+
77
+ it("expands against the namespace the caller declared, not the shipped one", () => {
78
+ expect(
79
+ expand(
80
+ "PREFIX ds: <https://other.test/> SELECT ?p WHERE { ds:a.b ?p ?o }",
81
+ ),
82
+ ).toContain("<https://other.test/a.b>");
83
+ });
84
+
85
+ it("declines a local part that would break the IRI it is put in", () => {
86
+ const query = "SELECT ?p WHERE { ds:a\\.b ?p ?o }";
87
+ expect(expand(query)).toBe(query);
88
+ });
89
+ });
90
+
91
+ describe("trimQueryError — the caller's own position", () => {
92
+ const DUMP =
93
+ 'error at 16:41: expected one of "!", "$", "(", ":", "<", "?", "^", "a", [\'%\'],\n' +
94
+ "['-' | '0' ..= '9' | '\\u{00B7}' | '\\u{0300}'..='\\u{036F}'],\n" +
95
+ "['A' ..= 'Z' | 'a' ..= 'z' | '\\u{00C0}'..='\\u{00D6}']";
96
+
97
+ it("subtracts the PREFIX lines the caller never wrote", () => {
98
+ expect(trimQueryError("error at 16:41: expected OPTIONAL", 15, false)).toBe(
99
+ "error at line 1, column 41: expected OPTIONAL",
100
+ );
101
+ });
102
+
103
+ it("drops the column when names were expanded (the line moved sideways)", () => {
104
+ expect(trimQueryError("error at 17:41: expected OPTIONAL", 15, true)).toBe(
105
+ "error at line 2: expected OPTIONAL",
106
+ );
107
+ });
108
+
109
+ it("cuts the expected-token dump to one line", () => {
110
+ const message = trimQueryError(DUMP, 15, false);
111
+ expect(message.split("\n")).toHaveLength(1);
112
+ expect(message.length).toBeLessThan(200);
113
+ expect(message.startsWith("error at line 1, column 41:")).toBe(true);
114
+ });
115
+
116
+ it("passes a message it cannot position through, still on one line", () => {
117
+ expect(trimQueryError("unknown prefix\n ds", 15, false)).toBe(
118
+ "unknown prefix ds",
119
+ );
120
+ });
121
+ });
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Making the graph's OWN names typable in `graph query`, and making the
3
+ * parser's complaint about the user's text.
4
+ *
5
+ * Two defects with one cause: the query a caller writes is not the query the
6
+ * parser reads. `rt.query.sparql` prepends one `PREFIX` line per registered
7
+ * namespace, so a caller's line 1 is the parser's line 16 — and the names the
8
+ * rest of this CLI hands out (`ds:global.component.button` from `graph
9
+ * inspect`, `dt:s4/web/--color-text` from `variable list`) are not spellable as
10
+ * SPARQL `PN_LOCAL` at all. Pasting a name pragma itself printed produced a
11
+ * Unicode-range dump pointing at a line the caller could not see.
12
+ *
13
+ * So this module owns both halves of the seam:
14
+ *
15
+ * - {@link expandPrefixedNames} rewrites a prefixed name whose local part the
16
+ * grammar cannot carry into the `<absolute IRI>` it stands for — the same
17
+ * expansion `graph inspect` has always done for its one argument, applied to
18
+ * every token of a query. It is a pure text transform: same namespaces, same
19
+ * meaning, so a query that already parsed answers exactly as before.
20
+ * - {@link trimQueryError} reports the position in the CALLER's text and cuts
21
+ * the parser's expected-token dump to one line.
22
+ */
23
+
24
+ import { isEmbeddableIri } from "../../kernel/packs/iri.js";
25
+
26
+ /**
27
+ * The query's own `PREFIX` declarations. Captured (rather than merely detected)
28
+ * because a caller who declares a prefix wins over the prepended one, so an
29
+ * expansion has to use the namespace the parser would have used.
30
+ */
31
+ const PREFIX_DECLARATION = /\bPREFIX\s+([A-Za-z][\w.-]*)?\s*:\s*<([^<>\s]*)>/gi;
32
+
33
+ /**
34
+ * One pass over the query, in precedence order: comment, long literal, short
35
+ * literal, IRI, prefixed-name candidate, any other character.
36
+ *
37
+ * The first four alternatives exist to be SKIPPED — a `ds:x/y` inside a string
38
+ * literal is data the caller wrote, and inside `<…>` it is already an IRI.
39
+ * Everything the scanner does not recognise passes through one character at a
40
+ * time, which is what keeps an unparsable query byte-identical apart from the
41
+ * names this module set out to expand.
42
+ */
43
+ const SCANNER = new RegExp(
44
+ [
45
+ "#[^\\n]*", // comment to end of line
46
+ '"""[\\s\\S]*?"""', // long literals
47
+ "'''[\\s\\S]*?'''",
48
+ '"(?:[^"\\\\\\n]|\\\\.)*"', // short literals
49
+ "'(?:[^'\\\\\\n]|\\\\.)*'",
50
+ '<[^<>"{}|^`\\\\\\s]*>', // an absolute IRI, already written out
51
+ // A prefixed-name candidate: a name, a colon, and the longest run of
52
+ // characters that could belong to a local part. `/` is included because
53
+ // the token IRIs contain it (`dt:s4/web/--color-text`); a property path
54
+ // is told apart from a local part in `splitPropertyPath`.
55
+ "[A-Za-z_\\u00C0-\\uFFFF][\\w.\\-\\u00C0-\\uFFFF]*:[^\\s<>\"'`{}()\\[\\],;|^\\\\*+?!=&#@$]*",
56
+ "[\\s\\S]", // anything else, one character at a time
57
+ ].join("|"),
58
+ "g",
59
+ );
60
+
61
+ /** A local part every SPARQL parser carries as written — left alone. */
62
+ const PLAIN_LOCAL = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/;
63
+
64
+ /** What a rewrite did, so the caller can say how much of a position it trusts. */
65
+ export interface ExpandedQuery {
66
+ /** The query to hand the parser. */
67
+ readonly text: string;
68
+ /** True when at least one name was rewritten (columns therefore moved). */
69
+ readonly rewritten: boolean;
70
+ }
71
+
72
+ /**
73
+ * Expand every prefixed name whose local part the SPARQL grammar cannot carry.
74
+ *
75
+ * Conservative in the direction that matters: a name is rewritten only when its
76
+ * prefix is one the query will be parsed with, its local part is not a plain
77
+ * name (letters, digits, `_`, `-`), and the expansion is a safe `<IRI>` token.
78
+ * A name inside a string literal, a comment or an existing IRI is never
79
+ * touched, and neither is a property path's operator (`ds:a/ds:b` stays two
80
+ * steps, `dt:s4/web/--x` stays one name).
81
+ *
82
+ * @param sparql - The query as the caller wrote it.
83
+ * @param prefixes - The namespaces the query will be parsed with.
84
+ * @returns The query to execute, and whether anything moved.
85
+ */
86
+ export function expandPrefixedNames(
87
+ sparql: string,
88
+ prefixes: Readonly<Record<string, string>>,
89
+ ): ExpandedQuery {
90
+ const namespaces = { ...prefixes, ...declaredPrefixes(sparql) };
91
+ let rewritten = false;
92
+ const text = sparql.replace(SCANNER, (token) => {
93
+ const expanded = expandToken(token, namespaces);
94
+ if (expanded === undefined) return token;
95
+ rewritten = true;
96
+ return expanded;
97
+ });
98
+ return { text, rewritten };
99
+ }
100
+
101
+ /** The prefixes the query declares for itself, which outrank the prepended ones. */
102
+ function declaredPrefixes(sparql: string): Record<string, string> {
103
+ const declared: Record<string, string> = {};
104
+ for (const match of sparql.matchAll(PREFIX_DECLARATION)) {
105
+ declared[match[1] ?? ""] = match[2] ?? "";
106
+ }
107
+ return declared;
108
+ }
109
+
110
+ /**
111
+ * One matched token, expanded — or `undefined` when it must be left as it is.
112
+ *
113
+ * The token may be a whole property path (`ds:a/ds:b`), because `/` is both the
114
+ * path operator and a character in this graph's local names. The two are told
115
+ * apart by what follows the slash: a segment that itself opens a KNOWN prefixed
116
+ * name is the next step of a path, anything else (`s4/web/--color-text`) is
117
+ * more of the same name. Each step is then expanded on its own, so a path may
118
+ * be half rewritten and stay a path.
119
+ *
120
+ * @param token - The matched token (it may not be a prefixed name at all).
121
+ * @param namespaces - Prefix → namespace, the caller's declarations winning.
122
+ * @returns The rewritten token, or undefined to keep the original.
123
+ */
124
+ function expandToken(
125
+ token: string,
126
+ namespaces: Readonly<Record<string, string>>,
127
+ ): string | undefined {
128
+ // Every alternative but the candidate one is skipped here: a comment, a
129
+ // literal and an IRI all fail one of these guards.
130
+ if (
131
+ token.indexOf(":") <= 0 ||
132
+ token.startsWith("<") ||
133
+ token.startsWith("#")
134
+ ) {
135
+ return undefined;
136
+ }
137
+ let rewritten = false;
138
+ const steps = pathSteps(token, namespaces).map((step) => {
139
+ const expanded = expandName(step, namespaces);
140
+ if (expanded === undefined) return step;
141
+ rewritten = true;
142
+ return expanded;
143
+ });
144
+ return rewritten ? steps.join("/") : undefined;
145
+ }
146
+
147
+ /** Split a token into the property-path steps it is made of (usually one). */
148
+ function pathSteps(
149
+ token: string,
150
+ namespaces: Readonly<Record<string, string>>,
151
+ ): string[] {
152
+ const segments = token.split("/");
153
+ const steps: string[] = [];
154
+ let step = segments[0] as string;
155
+ for (const segment of segments.slice(1)) {
156
+ const colon = segment.indexOf(":");
157
+ if (colon > 0 && namespaces[segment.slice(0, colon)] !== undefined) {
158
+ steps.push(step);
159
+ step = segment;
160
+ } else {
161
+ step = `${step}/${segment}`;
162
+ }
163
+ }
164
+ steps.push(step);
165
+ return steps;
166
+ }
167
+
168
+ /**
169
+ * One prefixed name, as the `<IRI>` it stands for — or `undefined` to keep it.
170
+ *
171
+ * @param name - One path step: `prefix:local`, plus any trailing punctuation.
172
+ * @param namespaces - Prefix → namespace, the caller's declarations winning.
173
+ * @returns The rewritten name, or undefined to keep the original.
174
+ */
175
+ function expandName(
176
+ name: string,
177
+ namespaces: Readonly<Record<string, string>>,
178
+ ): string | undefined {
179
+ const colon = name.indexOf(":");
180
+ if (colon <= 0) return undefined;
181
+ const namespace = namespaces[name.slice(0, colon)];
182
+ if (namespace === undefined) return undefined;
183
+ // A local part may not END in a dot, so a trailing one is the statement
184
+ // terminator the caller typed, not part of the name.
185
+ const local = name.slice(colon + 1).replace(/\.+$/, "");
186
+ const tail = name.slice(colon + 1 + local.length);
187
+ if (local === "" || PLAIN_LOCAL.test(local)) return undefined;
188
+ const iri = `${namespace}${local}`;
189
+ return isEmbeddableIri(iri) ? `<${iri}>${tail}` : undefined;
190
+ }
191
+
192
+ /** How much of the parser's expected-token dump a reader is served by. */
193
+ const MAX_PARSER_DETAIL = 140;
194
+
195
+ /** `error at <line>:<column>: <what the parser expected>`. */
196
+ const PARSER_POSITION = /^error at (\d+):(\d+):\s*([\s\S]*)$/;
197
+
198
+ /**
199
+ * Restate a parser failure in terms of the text the CALLER wrote.
200
+ *
201
+ * Two edits, both about the same lie: the position is the parser's, and the
202
+ * parser read a query with N prepended `PREFIX` lines the caller never saw
203
+ * ("16:41" for an error on line 1). And the expectation is a full dump of every
204
+ * admissible Unicode range — five lines of `'\\u{00C0}'..='\\u{00D6}'` that tell
205
+ * a reader nothing they can act on. The line number is corrected, the dump is
206
+ * collapsed to one line and cut.
207
+ *
208
+ * The COLUMN is dropped when names were expanded: an expansion lengthens the
209
+ * line it sits on, so the parser's column is a position in text the caller
210
+ * never wrote. The line is safe either way — an expansion never adds a newline.
211
+ *
212
+ * @param detail - The parser's own message.
213
+ * @param prependedLines - How many `PREFIX` lines were added above the query.
214
+ * @param rewritten - Whether names were expanded (columns therefore moved).
215
+ * @returns A one-line message positioned in the caller's own text.
216
+ */
217
+ export function trimQueryError(
218
+ detail: string,
219
+ prependedLines: number,
220
+ rewritten: boolean,
221
+ ): string {
222
+ const collapsed = detail.replace(/\s+/g, " ").trim();
223
+ const match = PARSER_POSITION.exec(collapsed);
224
+ if (!match) return cut(collapsed);
225
+ const line = Number(match[1]) - prependedLines;
226
+ const expected = cut(match[3] ?? "");
227
+ if (line < 1)
228
+ return `error at line ${match[1]}, column ${match[2]}: ${expected}`;
229
+ const at = rewritten
230
+ ? `error at line ${line}`
231
+ : `error at line ${line}, column ${match[2]}`;
232
+ return `${at}: ${expected}`;
233
+ }
234
+
235
+ /** One line, at most {@link MAX_PARSER_DETAIL} characters of it. */
236
+ function cut(text: string): string {
237
+ return text.length <= MAX_PARSER_DETAIL
238
+ ? text
239
+ : `${text.slice(0, MAX_PARSER_DETAIL - 1).trimEnd()}…`;
240
+ }