@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
@@ -5,7 +5,8 @@
5
5
  * and which properties to read; the query text is generated here so
6
6
  * user-supplied names are always escaped SPARQL string literals, and level-gated
7
7
  * fields below the active level are excluded from the projection (fetch-gating).
8
- * Matching on the name is exact and case-insensitive.
8
+ * Matching on the name is exact, and insensitive to case and to surrounding
9
+ * whitespace — see {@link nameFilter}.
9
10
  *
10
11
  * WHAT an entity's name IS lives in one place — {@link nameBinding} — and every
11
12
  * form here uses it: the asserted `by` value, plus, for a lookup that DECLARES
@@ -49,9 +50,49 @@
49
50
  */
50
51
 
51
52
  import { activeExpands, activeFields } from "../disclosure.js";
52
- import type { PackExpand, PackLookup } from "../types.js";
53
+ import {
54
+ type PackExpand,
55
+ type PackExpandField,
56
+ type PackLookup,
57
+ RESERVED_VARIABLE_PREFIX,
58
+ } from "../types.js";
53
59
  import { escapeSparqlString, formatTerm } from "./escape.js";
54
60
 
61
+ /**
62
+ * The variable a scoped name resolve reports each match's tier in.
63
+ *
64
+ * Under the reserved prefix because it is the kernel's, not the story's: it
65
+ * exists so the resolver can tell an in-scope match from an out-of-scope one,
66
+ * and it is stripped before an entity is handed to a formatter
67
+ * ({@link ../resolveEntity}) so no answer grows a field a caller did not ask
68
+ * for.
69
+ */
70
+ export const SCOPE_TIER_VARIABLE = "__pragmaTier";
71
+
72
+ /**
73
+ * Report each match's tier, for a name resolve over a SCOPED noun.
74
+ *
75
+ * A name resolve RANKS and returns everything the name reaches, so the scope
76
+ * does not need to be a filter here — the resolver prefers the in-scope matches
77
+ * and falls back to the rest, which is what lets `block lookup button` answer
78
+ * the global Button while still being able to answer at all for a name that
79
+ * lives only in one product's tier. All it needs from the query is WHICH tier
80
+ * each match belongs to.
81
+ *
82
+ * `OPTIONAL`, so an entity with no tier is still reached: not being in any tier
83
+ * is not a reason to be unaddressable by name.
84
+ */
85
+ function scopeTierProjection(via: string | undefined): {
86
+ variable: string;
87
+ optional: string;
88
+ } {
89
+ if (via === undefined) return { variable: "", optional: "" };
90
+ return {
91
+ variable: ` ?${SCOPE_TIER_VARIABLE}`,
92
+ optional: ` OPTIONAL { ?uri ${formatTerm(via)} ?${SCOPE_TIER_VARIABLE} . }`,
93
+ };
94
+ }
95
+
55
96
  /**
56
97
  * The class-constraint clause for a lookup: a single `a` triple for `type`, a
57
98
  * VALUES-constrained type triple for `types`, or nothing. All terms are
@@ -121,13 +162,17 @@ const DERIVED_NAME = 'REPLACE(REPLACE(STR(?uri), "^.*[#/]", ""), "\\\\.", "/")';
121
162
  *
122
163
  * A story whose `list` SYNTHESIZES a name instead of reading one declares
123
164
  * `nameFallback: "iri"`, and then the triple becomes OPTIONAL with
124
- * {@link DERIVED_NAME} behind it. `standard` is the case that exists: 22 of the
125
- * 156 live code standards carry a `cs:name`, and the code-standards ontology
126
- * says so deliberately — it is "an optional human-readable display title" that
127
- * "never participates in identity". `standard list` published
128
- * `react/component/tsdoc` while `standard lookup react/component/tsdoc`
129
- * answered ENTITY_NOT_FOUND with empty suggestions, because the only
130
- * addressable population was the ~13% carrying an asserted name.
165
+ * {@link DERIVED_NAME} behind it. `standard` is the case that exists. When the
166
+ * defect was measured, 22 of the then-156 live code standards carried a
167
+ * `cs:name`, and the code-standards ontology said so deliberately — "an
168
+ * optional human-readable display title" that "never participates in
169
+ * identity". `standard list` published `react/component/tsdoc` while
170
+ * `standard lookup react/component/tsdoc` answered ENTITY_NOT_FOUND with
171
+ * empty suggestions, because the only addressable population was the ~13%
172
+ * carrying an asserted name. (Upstream has since retired `cs:name` for
173
+ * `rdfs:label`, which every standard carries; the fallback stays because a
174
+ * pack is data, and a dropped label must degrade to the derived name rather
175
+ * than become unaddressable.)
131
176
  *
132
177
  * INFERRING the fallback from the mere PRESENCE of a class constraint is what
133
178
  * this option replaced, and it was over-reach in both directions of the same
@@ -344,12 +389,43 @@ function rankingClause(lookup: PackLookup): {
344
389
  };
345
390
  }
346
391
 
392
+ /**
393
+ * The name FILTER both name-addressed resolves share.
394
+ *
395
+ * A name is matched insensitively to case AND to the whitespace it is padded
396
+ * with, because the padding is an artefact of where the name came from rather
397
+ * than part of the name. Sixty-six `ds:name` literals in the shipped graph end
398
+ * in a space, copied across from the source document, and `block lookup
399
+ * Timeline` answered ENTITY_NOT_FOUND while offering "Timeline " straight back
400
+ * as a suggestion — the query and the answer differing by a character no
401
+ * surface prints and no user can type deliberately. The upstream transform is
402
+ * being corrected separately; a resolve a stray space can defeat is a defect on
403
+ * its own, and this is where that half is fixed.
404
+ *
405
+ * BOTH sides are trimmed, never one: the store side by REPLACE over the two
406
+ * anchored runs, the argument side in TypeScript before it is escaped into the
407
+ * literal. Trimming only `?name` would leave `block lookup "Button "` — a name
408
+ * pasted out of a table cell — missing an entity whose own name is clean.
409
+ *
410
+ * The regex the engine reads is `^\s+|\s+$` — the doubled backslashes in the
411
+ * source are the SPARQL string-literal escape and not part of the pattern.
412
+ * Verified against the pinned oxigraph, whose REPLACE takes both the `\s`
413
+ * class and the alternation, and whose `^`/`$` anchor the whole value rather
414
+ * than each line of it.
415
+ *
416
+ * @param name - The user-supplied name, untrimmed and unescaped.
417
+ * @returns The FILTER line to splice into the WHERE clause.
418
+ */
419
+ function nameFilter(name: string): string {
420
+ return ` FILTER (LCASE(REPLACE(STR(?name), "^\\\\s+|\\\\s+$", "")) = LCASE("${escapeSparqlString(name.trim())}"))`;
421
+ }
422
+
347
423
  /**
348
424
  * Build the SELECT retrieving the entities a name reaches, with their declared
349
425
  * fields, best first. The caller answers with the first and names the rest.
350
426
  *
351
427
  * @param lookup - The pack's lookup declaration.
352
- * @param name - User-supplied entity name (escaped here).
428
+ * @param name - User-supplied entity name (trimmed and escaped here).
353
429
  * @param level - Active canonical level; gated fields below it are excluded.
354
430
  * @returns SPARQL SELECT text, ranked and unlimited.
355
431
  */
@@ -357,16 +433,19 @@ export function buildLookupQuery(
357
433
  lookup: PackLookup,
358
434
  name: string,
359
435
  level?: string,
436
+ scopeVia?: string,
360
437
  ): string {
361
438
  const { header, constraint, optionals } = lookupProjection(lookup, level);
362
439
  const ranking = rankingClause(lookup);
440
+ const scope = scopeTierProjection(scopeVia);
363
441
  return [
364
- header,
442
+ header.replace(" WHERE {", `${scope.variable} WHERE {`),
365
443
  constraint,
366
444
  nameBinding(lookup),
367
445
  ranking.binds,
368
446
  optionals,
369
- ` FILTER (LCASE(STR(?name)) = LCASE("${escapeSparqlString(name)}"))`,
447
+ scope.optional,
448
+ nameFilter(name),
370
449
  "}",
371
450
  ranking.orderBy,
372
451
  ]
@@ -410,7 +489,7 @@ export function buildLookupByIriQuery(
410
489
  * Build the minimal name→URI resolve for a graphql-sourced lookup (and the
411
490
  * shared entry point for the sparql path's name form): maps the user-supplied
412
491
  * name to the entity IRIs, everything else comes from the field fetch. The name
413
- * is an escaped literal; all terms are validated pack terms.
492
+ * is a trimmed, escaped literal; all terms are validated pack terms.
414
493
  *
415
494
  * Ranked and unlimited, exactly like {@link buildLookupQuery} — the two paths
416
495
  * differ in where the VALUES come from, never in which entities a name reaches.
@@ -421,14 +500,17 @@ export function buildLookupByIriQuery(
421
500
  export function buildNameResolveQuery(
422
501
  lookup: PackLookup,
423
502
  name: string,
503
+ scopeVia?: string,
424
504
  ): string {
425
505
  const ranking = rankingClause(lookup);
506
+ const scope = scopeTierProjection(scopeVia);
426
507
  return [
427
- "SELECT ?uri ?name WHERE {",
508
+ `SELECT ?uri ?name${scope.variable} WHERE {`,
428
509
  buildTypeConstraint(lookup).trimEnd(),
429
510
  nameBinding(lookup),
430
511
  ranking.binds,
431
- ` FILTER (LCASE(STR(?name)) = LCASE("${escapeSparqlString(name)}"))`,
512
+ scope.optional,
513
+ nameFilter(name),
432
514
  "}",
433
515
  ranking.orderBy,
434
516
  ]
@@ -479,27 +561,65 @@ export function buildLookupIrisQuery(lookup: PackLookup): string {
479
561
  *
480
562
  * Injection-safe: the only interpolated value is `entityUri`, the IRI the base
481
563
  * lookup already resolved from the store (never user input); the
482
- * relation/properties are validated pack terms. SPARQL expands are single-hop,
483
- * so every select entry here is a plain field.
564
+ * relation/properties, the ordering names and the entity's identity property
565
+ * are validated pack terms. SPARQL expands are single-hop, so every select
566
+ * entry here is a plain field.
567
+ *
568
+ * `lookup` is read for ONE thing — the entity's own identity property, which a
569
+ * `blankWhenSelf` field compares its value against. A lookup declaring no such
570
+ * field never binds it.
484
571
  */
485
572
  export function buildExpandQuery(
486
573
  expand: PackExpand,
487
574
  entityUri: string,
575
+ lookup?: Pick<PackLookup, "by">,
488
576
  ): string {
489
577
  const vars = expand.select.map((field) => `?${field.name}`).join(" ");
490
578
  const optionals = expand.select
491
579
  .map((field) =>
492
- "property" in field
493
- ? ` OPTIONAL { ?child ${formatTerm(field.property)} ?${field.name} . }`
494
- : "",
580
+ "property" in field ? expandFieldClause(field, entityUri, lookup) : "",
495
581
  )
496
582
  .filter((line) => line !== "")
497
583
  .join("\n");
584
+ const order =
585
+ expand.orderBy && expand.orderBy.length > 0
586
+ ? `ORDER BY ${expand.orderBy.map((name) => `?${name}`).join(" ")}`
587
+ : "";
498
588
  return [
499
589
  `SELECT ${vars} WHERE {`,
500
590
  ` <${entityUri}> ${formatTerm(expand.relation)} ?child .`,
501
591
  optionals,
502
592
  "}",
593
+ order,
594
+ ]
595
+ .filter((line) => line !== "")
596
+ .join("\n");
597
+ }
598
+
599
+ /**
600
+ * The OPTIONAL clause reading one child field.
601
+ *
602
+ * A plain field is one line. A `blankWhenSelf` field binds the ENTITY's own
603
+ * identity value beside the child's and keeps the row only where the two
604
+ * differ — inside the OPTIONAL, so a match leaves the variable UNBOUND (the
605
+ * blank cell) rather than dropping the child row. Compared as strings, because
606
+ * the identity value is a literal and the cell may be read through a path that
607
+ * ends at one too.
608
+ */
609
+ function expandFieldClause(
610
+ field: PackExpandField,
611
+ entityUri: string,
612
+ lookup?: Pick<PackLookup, "by">,
613
+ ): string {
614
+ const read = `?child ${formatTerm(field.property)} ?${field.name} .`;
615
+ if (!field.blankWhenSelf || !lookup) return ` OPTIONAL { ${read} }`;
616
+ const self = `?${RESERVED_VARIABLE_PREFIX}Self`;
617
+ return [
618
+ " OPTIONAL {",
619
+ ` ${read}`,
620
+ ` <${entityUri}> ${formatTerm(lookup.by)} ${self} .`,
621
+ ` FILTER(STR(?${field.name}) != STR(${self}))`,
622
+ " }",
503
623
  ].join("\n");
504
624
  }
505
625
 
@@ -0,0 +1,86 @@
1
+ /**
2
+ * One rejected filter value, ONE list of the values it should have been.
3
+ *
4
+ * `variable list --symbol nope` printed the 40-name head of a 745-name
5
+ * vocabulary twice — once as the options line, once inside the recovery
6
+ * message, about 1.4KB of duplicate terminal output. The vocabulary rides
7
+ * `validOptions`, which every renderer already spells out, truncates and
8
+ * counts; a recovery says what to DO.
9
+ */
10
+
11
+ import { describe, expect, it } from "vitest";
12
+ import {
13
+ type PragmaError,
14
+ renderErrorLlm,
15
+ renderErrorPlain,
16
+ } from "../../error/index.js";
17
+ import type { PackFilter } from "../types.js";
18
+ import { resolveFilterPredicates } from "./filterValues.js";
19
+
20
+ /** The 745-name catalogue that made the duplication measurable. */
21
+ const VOCABULARY = Array.from({ length: 745 }, (_, i) => `color.text.${i}`);
22
+
23
+ const FREE_FILTER: PackFilter = { param: "symbol", variable: "symbol" };
24
+ const SET_FILTER: PackFilter = {
25
+ param: "type",
26
+ variable: "type",
27
+ values: ["color", "dimension", "duration"],
28
+ };
29
+
30
+ /** The error a filter raises for a value it does not admit. */
31
+ function rejection(
32
+ filter: PackFilter,
33
+ value: string,
34
+ vocabulary?: readonly string[],
35
+ ): PragmaError {
36
+ try {
37
+ resolveFilterPredicates(
38
+ [filter],
39
+ { [filter.param]: value },
40
+ vocabulary ? new Map([[filter.param, vocabulary]]) : undefined,
41
+ "test:story",
42
+ );
43
+ } catch (error) {
44
+ return error as PragmaError;
45
+ }
46
+ throw new Error("the filter admitted a value it should have refused");
47
+ }
48
+
49
+ /** How many times `needle` appears in `text`. */
50
+ const occurrences = (text: string, needle: string): number =>
51
+ text.split(needle).length - 1;
52
+
53
+ describe("a refused filter value names its vocabulary once", () => {
54
+ it("renders the catalogue once, not twice", () => {
55
+ const error = rejection(FREE_FILTER, "nope", VOCABULARY);
56
+ const plain = renderErrorPlain(error);
57
+
58
+ expect(occurrences(plain, "color.text.0,")).toBe(1);
59
+ expect(plain).toContain("Valid options (745), first 40:");
60
+ // The recovery says what to do, and names the flag — without the list.
61
+ expect(plain).toContain("Pick one of the 745 values --symbol accepts.");
62
+ expect(renderErrorLlm(error)).toContain("Pick one of the 745 values");
63
+ expect(occurrences(renderErrorLlm(error), "color.text.0,")).toBe(1);
64
+ });
65
+
66
+ it("keeps the whole vocabulary on the machine channel", () => {
67
+ // Truncation is a RENDERING decision; a machine consumer loses nothing.
68
+ expect(rejection(FREE_FILTER, "nope", VOCABULARY).validOptions).toEqual(
69
+ [...VOCABULARY].sort(),
70
+ );
71
+ });
72
+
73
+ it("says it once for a declared set too", () => {
74
+ const plain = renderErrorPlain(rejection(SET_FILTER, "nope"));
75
+
76
+ expect(occurrences(plain, "dimension")).toBe(1);
77
+ expect(plain).toContain("Valid options: color, dimension, duration");
78
+ expect(plain).toContain("Pick one of the 3 values --type accepts.");
79
+ });
80
+
81
+ it("a short vocabulary stays cheap to read", () => {
82
+ const plain = renderErrorPlain(rejection(SET_FILTER, "nope"));
83
+
84
+ expect(plain.length).toBeLessThan(200);
85
+ });
86
+ });
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Admit (or refuse) the values a caller supplied for a list story's declared
3
+ * filters, and hand the admitted ones to the query builder.
4
+ *
5
+ * This is the half of filtering that does NOT move into the query. Deciding
6
+ * whether a value is admissible is a question about the graph's vocabulary;
7
+ * deciding which rows carry it is a question about the population. Keeping them
8
+ * apart is what makes the two answers a caller gets distinguishable, and that
9
+ * distinction is load-bearing:
10
+ *
11
+ * - a value the vocabulary does NOT admit is `INVALID_INPUT`, carrying the
12
+ * admissible values as `validOptions` — the caller mistyped;
13
+ * - a value it DOES admit that no row carries is a calm empty list, exit 0,
14
+ * with the story's own `emptyRecovery` as the hint — `standard categories`
15
+ * reports a category with count 0, and asking for it is not a mistake.
16
+ *
17
+ * WHICH part of the graph is the vocabulary matters, and it is not the rows.
18
+ * Before the filters moved inside the query the rows were the FALLBACK
19
+ * vocabulary for a filter that declared none — knowingly narrower than the
20
+ * truth, but the only evidence there was. A page makes that fallback actively
21
+ * wrong: a real value outside the window would be refused as a bad argument,
22
+ * with a truncated list of alternatives. So the fallback is gone and the
23
+ * declaration rule replaces it — a filter declares `values` or a `vocabulary`
24
+ * query, and one that declares neither is refused where it is declared
25
+ * ({@link ../schema}) rather than misanswering at run time.
26
+ */
27
+
28
+ import { PragmaError } from "../../error/index.js";
29
+ import type { PackFilter } from "../types.js";
30
+ import type { ListPredicate } from "./buildListQuery.js";
31
+
32
+ /**
33
+ * Authoritative values per filter `param`, read from each filter's declared
34
+ * `vocabulary`. A filter with declared `values` needs no entry — the set IS the
35
+ * vocabulary.
36
+ */
37
+ export type FilterVocabularies = ReadonlyMap<string, readonly string[]>;
38
+
39
+ /**
40
+ * Turn the filter arguments a caller supplied into query predicates.
41
+ *
42
+ * @param filters - The story's declared filters (absent means none).
43
+ * @param params - Story parameters as provided by the surface.
44
+ * @param vocabularies - Authoritative values per value-free filter param.
45
+ * @param label - The story's label, for a configuration diagnosis.
46
+ * @returns One predicate per filter the caller supplied a value for.
47
+ * @throws PragmaError INVALID_INPUT when a value is not in a filter's declared
48
+ * set or its graph vocabulary, or when a value-free filter receives a
49
+ * non-string value; CONFIG_ERROR when a value-free filter declares no
50
+ * vocabulary.
51
+ */
52
+ export function resolveFilterPredicates(
53
+ filters: readonly PackFilter[] | undefined,
54
+ params: Record<string, unknown>,
55
+ vocabularies: FilterVocabularies | undefined,
56
+ label: string,
57
+ ): ListPredicate[] {
58
+ const predicates: ListPredicate[] = [];
59
+ for (const filter of filters ?? []) {
60
+ const provided = params[filter.param];
61
+ if (provided === undefined) continue;
62
+ // A repeated CLI flag accumulates into an array; MCP args stay scalar.
63
+ // Several values for ONE filter are a union (a row matches any of them);
64
+ // several filters still combine conjunctively.
65
+ const occurrences = Array.isArray(provided) ? provided : [provided];
66
+ if (occurrences.length === 0) continue;
67
+ const values = filter.values;
68
+ const terms = occurrences.map((occurrence) =>
69
+ values === undefined
70
+ ? requireStringValue(occurrence, filter)
71
+ : canonicalizeFilterValue(occurrence, filter, values),
72
+ );
73
+ if (values === undefined) {
74
+ rejectUnknownValue(
75
+ requireVocabulary(filter, vocabularies, label),
76
+ filter,
77
+ occurrences,
78
+ terms,
79
+ );
80
+ }
81
+ predicates.push({
82
+ variable: filter.variable,
83
+ match: filter.match ?? "exact",
84
+ terms,
85
+ });
86
+ }
87
+ return predicates;
88
+ }
89
+
90
+ /**
91
+ * The vocabulary a value-free filter is checked against.
92
+ *
93
+ * @throws PragmaError CONFIG_ERROR when the filter declares neither `values`
94
+ * nor a `vocabulary` — the declaration rule the schema enforces, restated at
95
+ * the point of use so a story that reached here another way still says why.
96
+ */
97
+ function requireVocabulary(
98
+ filter: PackFilter,
99
+ vocabularies: FilterVocabularies | undefined,
100
+ label: string,
101
+ ): readonly string[] {
102
+ const vocabulary = vocabularies?.get(filter.param);
103
+ if (vocabulary) return vocabulary;
104
+ throw PragmaError.configError(
105
+ `Filter "--${filter.param}" in ${label} declares neither "values" nor a ` +
106
+ '"vocabulary" query, so there is nothing to check a caller\'s value against.',
107
+ );
108
+ }
109
+
110
+ /**
111
+ * Reject a value-free filter value the vocabulary does not admit, naming the
112
+ * ones it does.
113
+ *
114
+ * Silent when the vocabulary is EMPTY: an unbuilt or genuinely-empty store is
115
+ * not the caller's mistake, and the list's own `emptyRecovery` is the right
116
+ * voice for it.
117
+ *
118
+ * A value the vocabulary admits is never rejected here even when no row carries
119
+ * it — a real slug with zero standards is a calm empty list, which is what the
120
+ * tool descriptions document and what `standard categories` reporting count 0
121
+ * promises.
122
+ *
123
+ * @throws PragmaError INVALID_INPUT when a term matches none of the admissible
124
+ * values, carrying them as `validOptions`.
125
+ */
126
+ function rejectUnknownValue(
127
+ vocabulary: readonly string[],
128
+ filter: PackFilter,
129
+ occurrences: readonly unknown[],
130
+ terms: readonly string[],
131
+ ): void {
132
+ const admissible = new Map<string, string>();
133
+ for (const value of vocabulary) {
134
+ const key = value.normalize("NFC").toLowerCase();
135
+ if (!admissible.has(key)) admissible.set(key, value.normalize("NFC"));
136
+ }
137
+ if (admissible.size === 0) return;
138
+ const index = terms.findIndex((term) => !admissible.has(term.toLowerCase()));
139
+ if (index === -1) return;
140
+ const validOptions = [...admissible.values()].sort();
141
+ // The VALUES ride `validOptions`, which every renderer already prints (and
142
+ // truncates, and counts). Repeating them in the recovery printed the same
143
+ // 40-name, 1.4KB list twice on one error — see `error/validOptions.ts`.
144
+ throw PragmaError.invalidInput(filter.param, String(occurrences[index]), {
145
+ validOptions,
146
+ recovery: {
147
+ message: `Pick one of the ${validOptions.length} values --${filter.param} accepts.`,
148
+ },
149
+ });
150
+ }
151
+
152
+ /** @throws PragmaError INVALID_INPUT when a value-free filter value is not a string. */
153
+ function requireStringValue(provided: unknown, filter: PackFilter): string {
154
+ if (typeof provided !== "string") {
155
+ throw PragmaError.invalidInput(filter.param, String(provided), {
156
+ recovery: { message: `Provide a string value for --${filter.param}.` },
157
+ });
158
+ }
159
+ return provided.trim().normalize("NFC");
160
+ }
161
+
162
+ /** @throws PragmaError INVALID_INPUT when the value is not in the declared set. */
163
+ function canonicalizeFilterValue(
164
+ provided: unknown,
165
+ filter: PackFilter,
166
+ values: readonly string[],
167
+ ): string {
168
+ if (typeof provided === "string") {
169
+ const normalized = provided.trim().normalize("NFC");
170
+ const match = values.find(
171
+ (value) =>
172
+ value.normalize("NFC").toLowerCase() === normalized.toLowerCase(),
173
+ );
174
+ if (match !== undefined) return match.normalize("NFC");
175
+ }
176
+ throw PragmaError.invalidInput(filter.param, String(provided), {
177
+ validOptions: [...values],
178
+ recovery: {
179
+ message: `Pick one of the ${values.length} values --${filter.param} accepts.`,
180
+ },
181
+ });
182
+ }
@@ -1,22 +1,31 @@
1
1
  /**
2
- * The SPARQL path — every generated read a pack story performs, and the two
3
- * row operations that must NOT be generated.
2
+ * The SPARQL path — every generated read a pack story performs.
4
3
  *
5
4
  * The domain is organised around one invariant: user input never reaches query
6
- * text. The query builders compose only from validated pack terms and escaped
7
- * literals, so a name a user typed becomes a SPARQL string literal or nothing;
8
- * `runSelect` is the single choke point every one of those trusted queries
9
- * goes through, which is what makes "a generated query failed" distinguishable
10
- * from "the user's own `graph query` failed". Filtering and search then run as
11
- * predicates over the resolved ROWS rather than as clauses in the query —
12
- * which is why they live in this domain despite touching no SPARQL at all:
13
- * they are the operations that would otherwise be tempting to express as query
14
- * text, and keeping them here keeps the temptation answered.
5
+ * TEXT. The query builders compose only from validated pack terms, the page's
6
+ * own integers, and escaped literals bound through `VALUES` — so a name a user
7
+ * typed becomes a SPARQL string literal or nothing; `runSelect` is the single
8
+ * choke point every one of those trusted queries goes through, which is what
9
+ * makes "a generated query failed" distinguishable from "the user's own
10
+ * `graph query` failed".
11
+ *
12
+ * Filtering and search used to sit here as predicates over the resolved ROWS —
13
+ * kept in this domain precisely because they were the operations it would
14
+ * otherwise be tempting to express as query text. They are query text now, and
15
+ * the temptation is answered a different way: {@link ./buildListQuery} is the
16
+ * only thing that composes a predicate, it binds every caller-supplied value,
17
+ * and it never edits the author's own query. What stayed on the row side is the
18
+ * part that was never about rows at all — deciding whether a value is
19
+ * ADMISSIBLE ({@link ./filterValues}), which is a question about the graph's
20
+ * vocabulary and is what keeps a refusal distinguishable from a calm empty
21
+ * answer.
15
22
  *
16
23
  * The escaping primitives stay internal. They are how the builders keep that
17
24
  * invariant, not a service for composing query text elsewhere — a caller with
18
25
  * the escaper is a caller who can build an unvalidated query, which is exactly
19
- * the shape this domain exists to prevent.
26
+ * the shape this domain exists to prevent. {@link ./projection}'s reader is
27
+ * internal for the same reason: it exists so a wrapping select can reproduce
28
+ * the author's own projection, not so callers can reason about author text.
20
29
  *
21
30
  * Two names re-exported by `buildLookupQuery.ts` for its own callers'
22
31
  * convenience — the disclosure-level field and expand selectors — are also
@@ -25,8 +34,12 @@
25
34
  * pass-through that has no reason to be permanent.
26
35
  */
27
36
 
28
- export { applyPackFilters } from "./applyFilters.js";
29
- export { applyPackSearch } from "./applySearch.js";
37
+ export type {
38
+ ListPredicate,
39
+ ListSearch,
40
+ ListWindow,
41
+ } from "./buildListQuery.js";
42
+ export { buildListQuery } from "./buildListQuery.js";
30
43
  export {
31
44
  buildExpandQuery,
32
45
  buildLookupByIriQuery,
@@ -34,4 +47,7 @@ export {
34
47
  buildLookupQuery,
35
48
  buildNameResolveQuery,
36
49
  } from "./buildLookupQuery.js";
50
+ export type { FilterVocabularies } from "./filterValues.js";
51
+ export { resolveFilterPredicates } from "./filterValues.js";
37
52
  export { runSelect } from "./runSelect.js";
53
+ export { readSearchTerm } from "./searchTerm.js";
@@ -172,3 +172,66 @@ describe("runSelect — an unbound prefix is diagnosed by PROVENANCE", () => {
172
172
  }
173
173
  });
174
174
  });
175
+
176
+ describe("runSelect — one SECTION degrades where the answer itself raises", () => {
177
+ const unbound = (): Error => new Error("Prefix not found: anatomy");
178
+
179
+ it("answers no rows for a section whose vocabulary the store does not bind", async () => {
180
+ // A lookup's section can name a vocabulary from a different pack than the
181
+ // entity's: a block is `ds:`, and the style key and state of the tokens it
182
+ // consumes are the anatomy DSL's. Taking the whole entity down over one
183
+ // absent section would fail `block lookup` outright on a graph that can
184
+ // answer every other section — so the section renders empty, which is what
185
+ // the lookup's GraphQL lane already does with the same condition.
186
+ //
187
+ // It cannot hide an unbuilt store: a section is only read after the base
188
+ // resolve has already answered from it.
189
+ const rows = await runSelect(
190
+ throwingRuntime(unbound()),
191
+ "SELECT ?key WHERE { ?child anatomy:styleKey ?key }",
192
+ distributionSource("pragma.conf.ts"),
193
+ { degradeOnUnboundPrefix: true },
194
+ );
195
+ expect(rows).toEqual([]);
196
+ });
197
+
198
+ it("degrades for a package and config story too, by the same rule", async () => {
199
+ // The provenance split above is about WHICH error an unbound prefix is. A
200
+ // section that degrades raises none, so there is nothing left to diagnose
201
+ // — and a third party's section is no more entitled to take an entity down
202
+ // than the distribution's.
203
+ for (const source of [PACKAGE, CONFIG]) {
204
+ expect(
205
+ await runSelect(
206
+ throwingRuntime(unbound()),
207
+ "SELECT ?x WHERE { ?x a acme:Thing }",
208
+ source,
209
+ { degradeOnUnboundPrefix: true },
210
+ ),
211
+ ).toEqual([]);
212
+ }
213
+ });
214
+
215
+ it("still passes a non-prefix failure through, and still refuses a non-SELECT", async () => {
216
+ // Narrow by construction: the ONE condition it swallows is the one that
217
+ // says "this pack's vocabulary is narrower than the story's". Everything
218
+ // else is a bug and still raises.
219
+ const boom = new Error("some other engine failure");
220
+ await expect(
221
+ runSelect(throwingRuntime(boom), "SELECT ?x WHERE {}", PACKAGE, {
222
+ degradeOnUnboundPrefix: true,
223
+ }),
224
+ ).rejects.toBe(boom);
225
+
226
+ const askRuntime = {
227
+ query: {
228
+ sparql: async () => ({ type: "ask", boolean: true }),
229
+ } as unknown as PragmaRuntime["query"],
230
+ };
231
+ await expect(
232
+ runSelect(askRuntime, "ASK { ?x ?y ?z }", PACKAGE, {
233
+ degradeOnUnboundPrefix: true,
234
+ }),
235
+ ).rejects.toMatchObject({ code: "CONFIG_ERROR" });
236
+ });
237
+ });