@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,487 @@
1
+ /**
2
+ * The tier scope: which tiers a read of a tiered noun answers from.
3
+ *
4
+ * Over a fixture with a REAL hierarchy — two levels, a base, an untiered
5
+ * entity, and a name two tiers share — because every interesting case in this
6
+ * rule is a case about depth, and a flat graph can state none of them. The
7
+ * shipped pack's own fifteen tiers are exercised in
8
+ * `capabilities/block.tierRank.exec.test.ts`; what is held here is the RULE:
9
+ *
10
+ * - the default is the tiers nothing sits above;
11
+ * - a chosen tier is that tier, its ancestors, and the declared base;
12
+ * - `all` is no scope at all;
13
+ * - the flag beats the config, and the config beats the default;
14
+ * - a tier that does not exist is a refusal that names the ones that do;
15
+ * - an UNTIERED entity is in every scope — it cannot be placed, so it cannot be
16
+ * placed outside (the `ds:button.icon` property `block.parity.test.ts` holds
17
+ * for the shipped story);
18
+ * - a lookup PREFERS the scope and falls back rather than answering "no such
19
+ * thing" about a thing the store holds.
20
+ */
21
+
22
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
23
+ import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
24
+ import { executeVerb } from "../project/cli/dispatch.js";
25
+ import type { GlobalFlags, PragmaRuntime } from "../runtime/types.js";
26
+ import type { VerbSpec } from "../spec/types.js";
27
+ import { compilePack } from "./compile.js";
28
+ import type { LookupOutput } from "./resolveEntity.js";
29
+ import { buildTierHierarchy, resolveTierScope } from "./tierScope.js";
30
+ import {
31
+ distributionSource,
32
+ type PackDefinition,
33
+ type PackPage,
34
+ type PackTierScope,
35
+ } from "./types.js";
36
+ import { verbKey } from "./uniqueness.js";
37
+
38
+ const PREFIXES = {
39
+ ex: "https://example.org/kit#",
40
+ owl: "http://www.w3.org/2002/07/owl#",
41
+ rdfs: "http://www.w3.org/2000/01/rdf-schema#",
42
+ xsd: "http://www.w3.org/2001/XMLSchema#",
43
+ };
44
+
45
+ /**
46
+ * Four tiers over two levels, plus the shapes the rule has to survive: an
47
+ * untiered part, and one name (`Chip`) carried by both a top-level tier and a
48
+ * nested one.
49
+ */
50
+ const TTL = `
51
+ @prefix ex: <https://example.org/kit#> .
52
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
53
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
54
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
55
+
56
+ ex:Widget a owl:Class .
57
+ ex:Tier a owl:Class .
58
+ ex:name a owl:DatatypeProperty ; rdfs:domain ex:Widget ; rdfs:range xsd:string .
59
+ ex:tier a owl:ObjectProperty ; rdfs:domain ex:Widget ; rdfs:range ex:Tier .
60
+
61
+ ex:global a ex:Tier ; ex:name "Global" .
62
+ ex:apps a ex:Tier ; ex:name "Apps" .
63
+ ex:apps_lxd a ex:Tier ; ex:name "Apps/LXD" .
64
+ ex:sites a ex:Tier ; ex:name "Sites" .
65
+
66
+ ex:button a ex:Widget ; ex:name "Button" ; ex:tier ex:global .
67
+ ex:chip a ex:Widget ; ex:name "Chip" ; ex:tier ex:global .
68
+ ex:bar a ex:Widget ; ex:name "Bar" ; ex:tier ex:apps .
69
+ ex:banner a ex:Widget ; ex:name "Banner" ; ex:tier ex:sites .
70
+ ex:meter a ex:Widget ; ex:name "Meter" ; ex:tier ex:apps_lxd .
71
+ ex:lxd_chip a ex:Widget ; ex:name "Chip" ; ex:tier ex:apps_lxd .
72
+ ex:loose a ex:Widget ; ex:name "Loose" .
73
+ `;
74
+
75
+ const TIER_SCOPE: PackTierScope = {
76
+ type: "ex:Tier",
77
+ via: "ex:tier",
78
+ by: "ex:name",
79
+ base: "ex:global",
80
+ };
81
+
82
+ const KIT: PackDefinition = {
83
+ noun: "widget",
84
+ description: "List widgets.",
85
+ tierScope: TIER_SCOPE,
86
+ list: {
87
+ query: [
88
+ "SELECT ?uri ?name WHERE {",
89
+ " ?uri a ex:Widget ; ex:name ?name .",
90
+ "} ORDER BY ?name STR(?uri)",
91
+ ].join("\n"),
92
+ columns: [
93
+ { field: "uri", label: "IRI" },
94
+ { field: "name", label: "Name" },
95
+ ],
96
+ search: { variables: ["name"] },
97
+ },
98
+ lookup: { by: "ex:name", type: "ex:Widget" },
99
+ };
100
+
101
+ /** The same pack with no hierarchy declared — the unscoped control. */
102
+ const UNSCOPED: PackDefinition = { ...KIT, tierScope: undefined };
103
+
104
+ const verbFor = (pack: PackDefinition, label: string): VerbSpec =>
105
+ compilePack(pack, distributionSource("test:tier-scope"), PREFIXES).find(
106
+ (v) => verbKey(v.path) === `${pack.noun} ${label}`,
107
+ ) as VerbSpec;
108
+
109
+ const REAL = { dryRun: false, undo: false, yes: false };
110
+ const PLAIN: GlobalFlags = {
111
+ llm: false,
112
+ autoLlm: false,
113
+ format: "plain",
114
+ verbose: false,
115
+ };
116
+ const LLM: GlobalFlags = { ...PLAIN, llm: true, format: "llm" };
117
+ const JSON_FLAGS: GlobalFlags = { ...PLAIN, format: "json" };
118
+
119
+ let rt: PragmaRuntime;
120
+
121
+ /** The runtime with a `tier` config value, and optionally another format. */
122
+ const withTier = (tier?: string, flags: GlobalFlags = PLAIN): PragmaRuntime =>
123
+ ({
124
+ ...rt,
125
+ globalFlags: flags,
126
+ loadConfig: async () => {
127
+ const layers = await rt.loadConfig();
128
+ return {
129
+ ...layers,
130
+ config: { ...layers.config, ...(tier === undefined ? {} : { tier }) },
131
+ origins: {
132
+ ...layers.origins,
133
+ tier: tier === undefined ? "default" : "global",
134
+ },
135
+ };
136
+ },
137
+ }) as PragmaRuntime;
138
+
139
+ /** One `widget list`, as the names it printed. */
140
+ async function names(
141
+ params: Record<string, unknown>,
142
+ runtime: PragmaRuntime = rt,
143
+ pack: PackDefinition = KIT,
144
+ ): Promise<string[]> {
145
+ const page = (await verbFor(pack, "list").run(params, runtime)) as PackPage;
146
+ return page.rows.map((row) => String(row.name));
147
+ }
148
+
149
+ /** One `widget list`'s page, for the facts it carries about itself. */
150
+ async function page(
151
+ params: Record<string, unknown>,
152
+ runtime: PragmaRuntime = rt,
153
+ ): Promise<PackPage> {
154
+ return (await verbFor(KIT, "list").run(params, runtime)) as PackPage;
155
+ }
156
+
157
+ beforeAll(async () => {
158
+ ({ rt } = await buildFixtureRuntime({ ttl: TTL, prefixes: PREFIXES }));
159
+ });
160
+ afterAll(async () => {
161
+ (await rt.store.get()).store.dispose();
162
+ });
163
+
164
+ describe("the scope rule, over the tiers themselves", () => {
165
+ const hierarchy = () =>
166
+ buildTierHierarchy(
167
+ [
168
+ { iri: "https://example.org/kit#global", name: "Global" },
169
+ { iri: "https://example.org/kit#apps", name: "Apps" },
170
+ { iri: "https://example.org/kit#apps_lxd", name: "Apps/LXD" },
171
+ { iri: "https://example.org/kit#sites", name: "Sites" },
172
+ ],
173
+ TIER_SCOPE,
174
+ );
175
+
176
+ const locals = (
177
+ request: Parameters<typeof resolveTierScope>[1],
178
+ ): string[] => {
179
+ const scope = resolveTierScope(hierarchy(), request);
180
+ return scope.kind === "all" ? ["*"] : scope.tiers.map((t) => t.local);
181
+ };
182
+
183
+ it("defaults to the tiers nothing sits above, base first", () => {
184
+ // Top-level is a property of the NAME — `Apps/LXD` has a `/`, so something
185
+ // sits above it — and the base leads so the chain reads root-to-leaf.
186
+ expect(locals({})).toEqual(["global", "apps", "sites"]);
187
+ });
188
+
189
+ it("a chosen tier is itself, its ancestors, and the base", () => {
190
+ expect(locals({ requested: "apps_lxd" })).toEqual([
191
+ "global",
192
+ "apps",
193
+ "apps_lxd",
194
+ ]);
195
+ // A top-level choice is itself plus the base, and the base chosen is just
196
+ // the base — never a scope wider than what was asked for.
197
+ expect(locals({ requested: "sites" })).toEqual(["global", "sites"]);
198
+ expect(locals({ requested: "global" })).toEqual(["global"]);
199
+ });
200
+
201
+ it("answers to every spelling this distribution publishes", () => {
202
+ // The `Tier` column prints `apps_lxd`, `tier list` prints `Apps/LXD`, and
203
+ // the config key's own documentation used `apps/lxd`. All three are the
204
+ // reader's own words for one tier.
205
+ for (const spelling of ["apps_lxd", "Apps/LXD", "apps/lxd", "APPS_LXD"]) {
206
+ expect(locals({ requested: spelling })).toEqual([
207
+ "global",
208
+ "apps",
209
+ "apps_lxd",
210
+ ]);
211
+ }
212
+ });
213
+
214
+ it("`all` turns the scope off, from either source", () => {
215
+ expect(locals({ requested: "all" })).toEqual(["*"]);
216
+ expect(locals({ configured: "all" })).toEqual(["*"]);
217
+ expect(locals({ requested: "ALL" })).toEqual(["*"]);
218
+ });
219
+
220
+ it("the flag REPLACES the config, it does not intersect it", () => {
221
+ // A chain is not an intersection of chains: an agent passing
222
+ // `--tier apps_lxd` on a machine configured to `sites` means the first.
223
+ expect(locals({ requested: "apps_lxd", configured: "sites" })).toEqual([
224
+ "global",
225
+ "apps",
226
+ "apps_lxd",
227
+ ]);
228
+ expect(locals({ configured: "apps_lxd" })).toEqual([
229
+ "global",
230
+ "apps",
231
+ "apps_lxd",
232
+ ]);
233
+ // An empty flag is not a choice — it falls through to the config.
234
+ expect(locals({ requested: "", configured: "sites" })).toEqual([
235
+ "global",
236
+ "sites",
237
+ ]);
238
+ });
239
+
240
+ it("refuses a tier nothing answers to, naming the ones that do", () => {
241
+ expect(() => locals({ requested: "apps_nope" })).toThrowError(
242
+ /Invalid tier/,
243
+ );
244
+ try {
245
+ locals({ requested: "apps_nope" });
246
+ } catch (error) {
247
+ expect((error as { validOptions?: string[] }).validOptions).toEqual([
248
+ "all",
249
+ "apps",
250
+ "apps_lxd",
251
+ "global",
252
+ "sites",
253
+ ]);
254
+ }
255
+ });
256
+
257
+ it("a graph with no tiers is not a narrowed graph", () => {
258
+ // Nothing to scope by, so the read answers as it always did — and says
259
+ // nothing about a scope it never applied.
260
+ expect(resolveTierScope(buildTierHierarchy([], TIER_SCOPE), {}).kind).toBe(
261
+ "all",
262
+ );
263
+ });
264
+ });
265
+
266
+ describe("the scope is compiled into the list query", () => {
267
+ it("the default shows the top-level tiers, and nothing below them", async () => {
268
+ // `Meter` and the LXD `Chip` are two levels down; `Loose` carries no tier
269
+ // at all and is NOT a thing the scope can drop.
270
+ expect(await names({})).toEqual([
271
+ "Banner",
272
+ "Bar",
273
+ "Button",
274
+ "Chip",
275
+ "Loose",
276
+ ]);
277
+ });
278
+
279
+ it("a chosen tier adds its own rows to its ancestors'", async () => {
280
+ expect(await names({ tier: "apps_lxd" })).toEqual([
281
+ "Bar",
282
+ "Button",
283
+ "Chip",
284
+ "Chip",
285
+ "Loose",
286
+ "Meter",
287
+ ]);
288
+
289
+ // `Banner` is a SIBLING branch (Sites), so the chain excludes it.
290
+ expect(await names({ tier: "apps_lxd" })).not.toContain("Banner");
291
+ });
292
+
293
+ it("`--tier all` is the unscoped read, byte for byte", async () => {
294
+ expect(await names({ tier: "all" })).toEqual(await names({}, rt, UNSCOPED));
295
+ });
296
+
297
+ it("a story that declares no hierarchy is not scoped at all", async () => {
298
+ // The scope reaches a noun because the noun DECLARES a hierarchy — the
299
+ // same kernel, the same fixture, no narrowing.
300
+ expect(await names({}, rt, UNSCOPED)).toHaveLength(7);
301
+ });
302
+
303
+ it("the scope decides before the page is cut", async () => {
304
+ // Filtering after the page would let a row outside the scope consume one of
305
+ // the caller's two slots, and the scope would then depend on where the page
306
+ // happened to fall.
307
+ const first = await page({ limit: 2 });
308
+ expect(first.rows.map((row) => row.name)).toEqual(["Banner", "Bar"]);
309
+ expect(first.nextAfter).toBeDefined();
310
+ const second = await page({ limit: 2, after: first.nextAfter });
311
+ expect(second.rows.map((row) => row.name)).toEqual(["Button", "Chip"]);
312
+ });
313
+
314
+ it("a cursor cannot be spent under a different scope", async () => {
315
+ // The cursor is spendable only on the read that issued it, and the scope is
316
+ // part of that read: paging on with a wider scope would walk a population
317
+ // the cursor was not cut from.
318
+ const first = await page({ limit: 2, tier: "apps_lxd" });
319
+ await expect(
320
+ page({ limit: 2, after: first.nextAfter, tier: "all" }),
321
+ ).rejects.toMatchObject({ code: "INVALID_INPUT" });
322
+ });
323
+
324
+ it("the configured tier is the default, and the flag overrides it", async () => {
325
+ expect(await names({}, withTier("apps_lxd"))).toContain("Meter");
326
+ expect(await names({ tier: "sites" }, withTier("apps_lxd"))).not.toContain(
327
+ "Meter",
328
+ );
329
+ expect(await names({}, withTier("all"))).toHaveLength(7);
330
+ });
331
+ });
332
+
333
+ describe("a scoped answer says so, in every format", () => {
334
+ it("the page carries the scope as data", async () => {
335
+ expect((await page({})).scope).toEqual({
336
+ tiers: ["global", "apps", "sites"],
337
+ });
338
+ expect((await page({ tier: "all" })).scope).toBeUndefined();
339
+ });
340
+
341
+ it("llm: the heading names the tiers, beside the count", async () => {
342
+ const outcome = await executeVerb(
343
+ verbFor(KIT, "list"),
344
+ {},
345
+ REAL,
346
+ withTier(undefined, LLM),
347
+ );
348
+ expect(outcome.stdout).toContain(
349
+ "## Widget (5, tier scope: global, apps, sites)",
350
+ );
351
+ });
352
+
353
+ it("llm: a page that is cut AND scoped admits both", async () => {
354
+ const outcome = await executeVerb(
355
+ verbFor(KIT, "list"),
356
+ { limit: 2 },
357
+ REAL,
358
+ withTier(undefined, LLM),
359
+ );
360
+ expect(outcome.stdout).toContain(
361
+ "## Widget (2, more exist, tier scope: global, apps, sites)",
362
+ );
363
+ });
364
+
365
+ it("plain: the sentence reaches stderr, where the notice seam puts it", async () => {
366
+ // The plain form is a table a pipe reads as records, so it has no heading
367
+ // to carry the scope — and stdout must stay data.
368
+ const outcome = await executeVerb(
369
+ verbFor(KIT, "list"),
370
+ {},
371
+ REAL,
372
+ withTier(undefined, PLAIN),
373
+ );
374
+ expect(outcome.stdout).toContain("Button");
375
+ expect(outcome.stderr).toContain("Tier scope: global, apps, sites.");
376
+ expect(outcome.stderr).toContain("`--tier all` for every tier");
377
+ });
378
+
379
+ it("json: the scope rides meta as DATA, not only as prose", async () => {
380
+ const outcome = await executeVerb(
381
+ verbFor(KIT, "list"),
382
+ {},
383
+ REAL,
384
+ withTier(undefined, JSON_FLAGS),
385
+ );
386
+ const envelope = JSON.parse(outcome.stdout as string);
387
+ expect(envelope.meta.scope).toEqual({ tiers: ["global", "apps", "sites"] });
388
+ expect(envelope.meta.notice).toContain("Tier scope: global, apps, sites.");
389
+ // `data` keeps its shape: the bare row array it has always been.
390
+ expect(Array.isArray(envelope.data)).toBe(true);
391
+ });
392
+
393
+ it("an unscoped read says nothing about a scope", async () => {
394
+ const outcome = await executeVerb(
395
+ verbFor(KIT, "list"),
396
+ { tier: "all" },
397
+ REAL,
398
+ withTier(undefined, JSON_FLAGS),
399
+ );
400
+ const envelope = JSON.parse(outcome.stdout as string);
401
+ expect(envelope.meta.scope).toBeUndefined();
402
+ expect(envelope.meta.notice).toBeUndefined();
403
+ });
404
+
405
+ it("a `--tier` that narrows to nothing names the flag it narrowed by", async () => {
406
+ const outcome = await executeVerb(
407
+ verbFor(KIT, "list"),
408
+ { tier: "sites", search: "meter" },
409
+ REAL,
410
+ withTier(undefined, PLAIN),
411
+ );
412
+ const said = `${outcome.stdout ?? ""}${outcome.stderr ?? ""}`;
413
+ expect(said).toContain("--tier sites");
414
+ });
415
+ });
416
+
417
+ describe("a lookup prefers the scope and falls back rather than refusing", () => {
418
+ const lookup = async (
419
+ name: string,
420
+ params: Record<string, unknown> = {},
421
+ runtime: PragmaRuntime = rt,
422
+ ): Promise<LookupOutput> =>
423
+ (await verbFor(KIT, "lookup").run(
424
+ { name: [name], ...params },
425
+ runtime,
426
+ )) as LookupOutput;
427
+
428
+ it("answers a shared name with the in-scope one alone", async () => {
429
+ const out = await lookup("Chip");
430
+ expect(out.results.map((entity) => entity.uri)).toEqual([
431
+ "https://example.org/kit#chip",
432
+ ]);
433
+ expect(out.outOfScope).toBeUndefined();
434
+ });
435
+
436
+ it("`--tier all` answers with every one of them", async () => {
437
+ const out = await lookup("Chip", { tier: "all" });
438
+ expect(out.results).toHaveLength(2);
439
+ });
440
+
441
+ it("answers an OUT-OF-SCOPE name anyway, and says where from", async () => {
442
+ // The alternative is ENTITY_NOT_FOUND about a widget the store holds and
443
+ // `widget list --tier apps_lxd` prints. A name a reader typed is evidence
444
+ // they mean something; the scope decides WHICH, never WHETHER.
445
+ const out = await lookup("Meter");
446
+ expect(out.results.map((entity) => entity.uri)).toEqual([
447
+ "https://example.org/kit#meter",
448
+ ]);
449
+ expect(out.outOfScope).toEqual([
450
+ { query: "Meter", tiers: ["apps_lxd"], scope: "global, apps, sites" },
451
+ ]);
452
+ });
453
+
454
+ it("the fallback is one line, in the body a reader is looking at", async () => {
455
+ const outcome = await executeVerb(
456
+ verbFor(KIT, "lookup"),
457
+ { name: ["Meter"] },
458
+ REAL,
459
+ withTier(undefined, PLAIN),
460
+ );
461
+ expect(outcome.stdout).toContain(
462
+ 'No "Meter" in the tier scope (global, apps, sites) — answering from apps_lxd.',
463
+ );
464
+ expect(outcome.stdout).toContain("--tier all");
465
+ });
466
+
467
+ it("an untiered entity is in every scope", async () => {
468
+ const out = await lookup("Loose");
469
+ expect(out.results).toHaveLength(1);
470
+ expect(out.outOfScope).toBeUndefined();
471
+ });
472
+
473
+ it("an IRI-addressed lookup is never scoped", async () => {
474
+ // An IRI reaches exactly one entity, so there is nothing for a scope to
475
+ // choose between — and a caller who pasted it has already said which tier.
476
+ const out = await lookup("ex:meter");
477
+ expect(out.results.map((entity) => entity.uri)).toEqual([
478
+ "https://example.org/kit#meter",
479
+ ]);
480
+ expect(out.outOfScope).toBeUndefined();
481
+ });
482
+
483
+ it("does not leak the kernel's scope variable into the answer", async () => {
484
+ const out = await lookup("Button");
485
+ expect(Object.keys(out.results[0] ?? {})).toEqual(["uri", "name"]);
486
+ });
487
+ });