@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
@@ -36,14 +36,16 @@ import {
36
36
  buildLookupNamesQuery,
37
37
  buildLookupQuery,
38
38
  buildNameResolveQuery,
39
+ SCOPE_TIER_VARIABLE,
39
40
  } from "./sparql/buildLookupQuery.js";
40
41
  import { runSelect } from "./sparql/runSelect.js";
41
- import type {
42
- PackChildRow,
43
- PackEntity,
44
- PackLookup,
45
- PackRow,
46
- StorySource,
42
+ import {
43
+ expandIsSparql,
44
+ type PackChildRow,
45
+ type PackEntity,
46
+ type PackLookup,
47
+ type PackRow,
48
+ type StorySource,
47
49
  } from "./types.js";
48
50
 
49
51
  /** A structured per-query lookup failure (never rejects the whole batch). */
@@ -54,6 +56,32 @@ export interface LookupError {
54
56
  readonly suggestions?: readonly string[];
55
57
  }
56
58
 
59
+ /**
60
+ * The tier scope a name resolve answers under, as the resolver needs it.
61
+ *
62
+ * Built by the run body from the noun's declaration and the caller's arguments
63
+ * ({@link ../tierScope}); the resolver only has to know which tiers count and
64
+ * how to say so.
65
+ */
66
+ export interface LookupScope {
67
+ /** The entity → tier edge, a validated pack term. */
68
+ readonly via: string;
69
+ /** The in-scope tier IRIs. */
70
+ readonly tiers: readonly string[];
71
+ /** The scope in the words a reader can type back (`global, apps`). */
72
+ readonly label: string;
73
+ }
74
+
75
+ /** A name whose only matches were outside the tier scope. */
76
+ export interface OutOfScopeAnswer {
77
+ /** The name (or IRI, or glob expansion) that was looked up. */
78
+ readonly query: string;
79
+ /** The tiers the answers actually came from, as local names. */
80
+ readonly tiers: readonly string[];
81
+ /** The scope that held none of them, in the words `--tier` accepts. */
82
+ readonly scope: string;
83
+ }
84
+
57
85
  /**
58
86
  * The result of a (possibly multi-name) lookup.
59
87
  *
@@ -62,10 +90,23 @@ export interface LookupError {
62
90
  * naming the ones not answered with, because there are none — an earlier draft
63
91
  * kept the single-entity arity and carried the rest as IRIs in a notice, which
64
92
  * is a sentence about an address where the payload is the address.
93
+ *
94
+ * The tier scope narrows that: for a SCOPED noun the in-scope matches are the
95
+ * answer, and the out-of-scope ones are not returned — `block lookup button`
96
+ * answers with the global Button alone. A name whose matches are ALL out of
97
+ * scope is answered anyway, from the tiers it does live in, and says so through
98
+ * {@link outOfScope}: refusing it would be answering "no such block" about a
99
+ * block that exists, which is the one thing a lookup must never do.
65
100
  */
66
101
  export interface LookupOutput {
67
102
  readonly results: PackEntity[];
68
103
  readonly errors: LookupError[];
104
+ /**
105
+ * The names answered from OUTSIDE the tier scope, because the scope held
106
+ * nothing they reached. Absent when every answer was in scope — the ordinary
107
+ * case, and the case the payload speaks for itself in.
108
+ */
109
+ readonly outOfScope?: OutOfScopeAnswer[];
69
110
  }
70
111
 
71
112
  /** What the resolver needs from the runtime: the store + the query facade. */
@@ -84,6 +125,7 @@ export async function resolveLookup(
84
125
  source: StorySource,
85
126
  prefixes: Readonly<Record<string, string>>,
86
127
  level: string | undefined,
128
+ scope?: LookupScope,
87
129
  ): Promise<LookupOutput> {
88
130
  if (queries.length === 0) {
89
131
  throw PragmaError.invalidInput("names", "(empty)", {
@@ -106,16 +148,18 @@ export async function resolveLookup(
106
148
  );
107
149
  const results: PackEntity[] = [];
108
150
  const errors: LookupError[] = [...expanded.globErrors];
151
+ const outOfScope: OutOfScopeAnswer[] = [];
109
152
  const settled = await Promise.allSettled(
110
153
  expanded.names.map((query) =>
111
- lookupOne(rt, lookup, noun, query, source, prefixes, level),
154
+ lookupOne(rt, lookup, noun, query, source, prefixes, level, scope),
112
155
  ),
113
156
  );
114
157
  for (const [index, outcome] of settled.entries()) {
115
158
  const query = expanded.names[index];
116
159
  if (query === undefined) continue;
117
160
  if (outcome.status === "fulfilled") {
118
- results.push(...outcome.value);
161
+ results.push(...outcome.value.entities);
162
+ if (outcome.value.outOfScope) outOfScope.push(outcome.value.outOfScope);
119
163
  continue;
120
164
  }
121
165
  const error = outcome.reason;
@@ -136,7 +180,11 @@ export async function resolveLookup(
136
180
  });
137
181
  }
138
182
  }
139
- return { results, errors };
183
+ return {
184
+ results,
185
+ errors,
186
+ ...(outOfScope.length > 0 ? { outOfScope } : {}),
187
+ };
140
188
  }
141
189
 
142
190
  /**
@@ -235,15 +283,16 @@ async function lookupOne(
235
283
  source: StorySource,
236
284
  prefixes: Readonly<Record<string, string>>,
237
285
  level: string | undefined,
238
- ): Promise<PackEntity[]> {
286
+ scope?: LookupScope,
287
+ ): Promise<{ entities: PackEntity[]; outOfScope?: OutOfScopeAnswer }> {
239
288
  const graphqlSourced = lookup.source === "graphql";
240
289
  const rows = await runSelect(
241
290
  rt,
242
- buildResolveQuery(lookup, query, prefixes, level),
291
+ buildResolveQuery(lookup, query, prefixes, level, scope),
243
292
  source,
244
293
  );
245
- const bases = firstRowPerEntity(rows);
246
- if (bases.length === 0) {
294
+ const resolved = firstRowPerEntity(rows);
295
+ if (resolved.length === 0) {
247
296
  const candidates = await listEntityNames(rt, lookup, source);
248
297
  throw PragmaError.notFound(noun, query, {
249
298
  suggestions: suggestNames(query, candidates),
@@ -256,8 +305,12 @@ async function lookupOne(
256
305
  });
257
306
  }
258
307
 
308
+ const chosen = applyScope(resolved, query, scope);
309
+ const bases = chosen.rows.map(withoutScopeVariable);
310
+ const fallback = chosen.outOfScope ? { outOfScope: chosen.outOfScope } : {};
311
+
259
312
  if (graphqlSourced) {
260
- return Promise.all(
313
+ const fetched = await Promise.all(
261
314
  bases.map((base) =>
262
315
  fetchGraphqlLookup(
263
316
  rt,
@@ -273,21 +326,123 @@ async function lookupOne(
273
326
  ),
274
327
  ),
275
328
  );
329
+ // A graphql lookup may still declare SPARQL-lane expands (see
330
+ // `PackExpand.source`), and they are fetched HERE rather than inside the
331
+ // GraphQL lane: one entity, two lanes, merged on the way out. The document
332
+ // generator skips exactly what this loop claims, so no expand is fetched
333
+ // twice and none is dropped.
334
+ const entities = await Promise.all(
335
+ fetched.map(async (entity) =>
336
+ addSparqlExpands(rt, lookup, entity, source, level),
337
+ ),
338
+ );
339
+ return { entities, ...fallback };
276
340
  }
277
341
 
278
342
  const entities: PackEntity[] = [];
279
343
  for (const base of bases) {
280
- const entity: PackEntity = { ...base };
281
- for (const expand of activeExpands(lookup, level)) {
282
- entity[expand.name] = (await runSelect(
283
- rt,
284
- buildExpandQuery(expand, String(base.uri)),
285
- source,
286
- )) as readonly PackChildRow[];
287
- }
288
- entities.push(entity);
344
+ entities.push(
345
+ await addSparqlExpands(rt, lookup, { ...base }, source, level),
346
+ );
347
+ }
348
+ return { entities, ...fallback };
349
+ }
350
+
351
+ /**
352
+ * Narrow a resolve to the tier scope — or, when the scope holds none of what
353
+ * the name reached, answer from outside it and say so.
354
+ *
355
+ * THE FALLBACK IS THE POINT, and it is why the scope is not a `FILTER` in the
356
+ * resolve query. A scoped filter would turn `block lookup back-link` — an LXD
357
+ * component, out of the default scope — into ENTITY_NOT_FOUND with suggestions,
358
+ * i.e. into "no such block" about a block the store holds and `block list
359
+ * --tier apps_lxd` prints. A name a reader typed is evidence they mean
360
+ * something; the scope decides WHICH of several it means, never whether it
361
+ * exists.
362
+ *
363
+ * An entity with no tier at all is treated as IN scope. It cannot be placed, so
364
+ * it cannot be placed outside — and being unplaceable is not grounds for being
365
+ * unfindable.
366
+ */
367
+ function applyScope(
368
+ rows: readonly PackRow[],
369
+ query: string,
370
+ scope: LookupScope | undefined,
371
+ ): { rows: readonly PackRow[]; outOfScope?: OutOfScopeAnswer } {
372
+ if (!scope) return { rows };
373
+ const inScope = new Set(scope.tiers);
374
+ const kept = rows.filter((row) => {
375
+ const tier = row[SCOPE_TIER_VARIABLE];
376
+ return tier === undefined || tier === "" || inScope.has(tier);
377
+ });
378
+ if (kept.length > 0) return { rows: kept };
379
+ const tiers = [
380
+ ...new Set(
381
+ rows
382
+ .map((row) => row[SCOPE_TIER_VARIABLE] ?? "")
383
+ .filter((tier) => tier !== "")
384
+ .map(scopeTierName),
385
+ ),
386
+ ];
387
+ return { rows, outOfScope: { query, tiers, scope: scope.label } };
388
+ }
389
+
390
+ /** A tier IRI as the scope reports it: the local name `--tier` accepts. */
391
+ function scopeTierName(iri: string): string {
392
+ return iri.replace(/^.*[#/]/, "");
393
+ }
394
+
395
+ /**
396
+ * Drop the kernel's scope variable from a resolved row.
397
+ *
398
+ * The sparql-sourced lane spreads its resolve row straight into the entity it
399
+ * answers with, so a variable the kernel added for its own decision would land
400
+ * in `--format json` as a field the story never declared.
401
+ */
402
+ function withoutScopeVariable(row: PackRow): PackRow {
403
+ if (!(SCOPE_TIER_VARIABLE in row)) return row;
404
+ const { [SCOPE_TIER_VARIABLE]: _tier, ...rest } = row;
405
+ return rest;
406
+ }
407
+
408
+ /**
409
+ * Fetch every SPARQL-lane expand active at this level onto one entity.
410
+ *
411
+ * The lane test is the expand's, not the lookup's, so this serves both callers:
412
+ * on a sparql lookup every expand is one of these, and on a graphql lookup only
413
+ * those that opted out of the document.
414
+ *
415
+ * @param rt - The runtime (store + query facade).
416
+ * @param lookup - The validated lookup declaration.
417
+ * @param entity - The entity to add child arrays to (mutated and returned).
418
+ * @param source - Pack source, for error attribution.
419
+ * @param level - Active canonical disclosure level.
420
+ * @returns The same entity, with one array per fetched expand.
421
+ * @note Impure — queries the store, once per expand.
422
+ */
423
+ async function addSparqlExpands(
424
+ rt: LookupRuntime,
425
+ lookup: PackLookup,
426
+ entity: PackEntity,
427
+ source: StorySource,
428
+ level: string | undefined,
429
+ ): Promise<PackEntity> {
430
+ for (const expand of activeExpands(lookup, level)) {
431
+ if (!expandIsSparql(lookup, expand)) continue;
432
+ entity[expand.name] = (await runSelect(
433
+ rt,
434
+ buildExpandQuery(expand, String(entity.uri), lookup),
435
+ source,
436
+ // A section whose vocabulary this store does not bind renders EMPTY
437
+ // rather than taking the whole entity down with it — see
438
+ // `RunSelectOptions.degradeOnUnboundPrefix`. The block lookup's tokens
439
+ // read the anatomy DSL's style key and state, which a store built
440
+ // without that pack does not bind, and every other section of that
441
+ // block is still perfectly answerable.
442
+ { degradeOnUnboundPrefix: true },
443
+ )) as readonly PackChildRow[];
289
444
  }
290
- return entities;
445
+ return entity;
291
446
  }
292
447
 
293
448
  /** Collapse a ranked resolve to one row per entity, keeping the best-ranked. */
@@ -316,13 +471,18 @@ function buildResolveQuery(
316
471
  query: string,
317
472
  prefixes: Readonly<Record<string, string>>,
318
473
  level: string | undefined,
474
+ scope?: LookupScope,
319
475
  ): string {
320
476
  const graphqlSourced = lookup.source === "graphql";
321
477
  if (!looksLikeIri(query)) {
322
478
  return graphqlSourced
323
- ? buildNameResolveQuery(lookup, query)
324
- : buildLookupQuery(lookup, query, level);
479
+ ? buildNameResolveQuery(lookup, query, scope?.via)
480
+ : buildLookupQuery(lookup, query, level, scope?.via);
325
481
  }
482
+ // An IRI-ADDRESSED form is never scoped, and that is not an omission. An IRI
483
+ // reaches exactly one entity, so there is nothing for a scope to choose
484
+ // between — the whole job the scope does. A caller who pastes
485
+ // `ds:apps_lxd.component.back_link` has already said which tier they mean.
326
486
  const resolved = resolveUri(query, prefixes);
327
487
  if (!isEmbeddableIri(resolved)) {
328
488
  throw PragmaError.invalidInput("name", query, {
@@ -2,35 +2,49 @@
2
2
  * The shared run bodies a compiled pack verb closes over.
3
3
  *
4
4
  * Each factory returns a `VerbSpec.run` closure. Reads are plain async: the list
5
- * body runs the pack's SELECT then applies post-query filters/search; the lookup
6
- * body resolves names → IRIs and fetches per the declared source, gated by the
7
- * resolved disclosure level; the sample body draws N random entities at the
8
- * highest level. All store access is through the runtime facade (lazy), so these
9
- * factories carry no heavy static import.
5
+ * body admits the caller's filter values against the graph's vocabulary, then
6
+ * runs ONE generated SELECT that carries the filters, the search and the page's
7
+ * bounds; the lookup body resolves names → IRIs and fetches per the declared
8
+ * source, gated by the resolved disclosure level; the sample body draws N random
9
+ * entities at the highest level. All store access is through the runtime facade
10
+ * (lazy), so these factories carry no heavy static import.
10
11
  */
11
12
 
12
13
  import { cliRecovery, PragmaError } from "../error/index.js";
13
14
  import type { PragmaRuntime } from "../runtime/index.js";
15
+ import { encodeCursor, pageFingerprint, readCursor } from "./cursor.js";
14
16
  import { resolvePackDetail } from "./disclosure.js";
17
+ import { readLimit } from "./paging.js";
15
18
  import type { SampleOutput } from "./renderPack.js";
16
19
  import {
17
20
  type LookupOutput,
21
+ type LookupScope,
18
22
  listEntityNames,
19
23
  resolveLookup,
20
24
  } from "./resolveEntity.js";
21
25
  import { parseSampleCount, pickRandom } from "./sample.js";
26
+ import { buildListQuery } from "./sparql/buildListQuery.js";
22
27
  import {
23
- applyPackFilters,
24
28
  type FilterVocabularies,
25
- } from "./sparql/applyFilters.js";
26
- import { applyPackSearch } from "./sparql/applySearch.js";
29
+ resolveFilterPredicates,
30
+ } from "./sparql/filterValues.js";
27
31
  import { runSelect } from "./sparql/runSelect.js";
28
- import type {
29
- PackFilter,
30
- PackList,
31
- PackLookup,
32
- PackRow,
33
- StorySource,
32
+ import { readSearchTerm } from "./sparql/searchTerm.js";
33
+ import {
34
+ resolveReadScope,
35
+ scopeIris,
36
+ scopeLabel,
37
+ type TierScope,
38
+ } from "./tierScope.js";
39
+ import {
40
+ ENTITY_VARIABLE,
41
+ type PackAppliedFilter,
42
+ type PackFilter,
43
+ type PackList,
44
+ type PackLookup,
45
+ type PackPage,
46
+ type PackTierScope,
47
+ type StorySource,
34
48
  } from "./types.js";
35
49
 
36
50
  /** The highest canonical level — sample fetches everything for shape discovery. */
@@ -55,37 +69,152 @@ function listRecovery(noun: string) {
55
69
  /** Facts a list-shaped run body needs beyond its `shape`. */
56
70
  export interface ListRunMeta {
57
71
  readonly source: StorySource;
72
+ /**
73
+ * The noun's declared tier hierarchy, when its entities are tiered: the list
74
+ * is then narrowed to a tier scope, compiled into the same query the filters
75
+ * are. Absent leaves the read exactly as it was.
76
+ */
77
+ readonly tierScope?: PackTierScope;
58
78
  }
59
79
 
60
80
  /**
61
81
  * Build the run body for a list-shaped verb (`list` or an extra verb).
62
82
  *
63
- * Zero rows is a plain empty list — a SUCCESS, not an error. It returns `[]`
64
- * (JSON stays `[]`, exit 0); the formatter turns the emptiness into a non-blank
65
- * message (a pack's `emptyRecovery` becomes that message's hint). Routing zero
66
- * results through EMPTY_RESULTS would map to exit 1 and break the uniform
67
- * `ok:true` list contract, so the run body never throws on emptiness.
83
+ * Zero rows is a plain empty list — a SUCCESS, not an error. It returns an
84
+ * empty page (JSON stays `[]`, exit 0); the formatter turns the emptiness into
85
+ * a non-blank message (a pack's `emptyRecovery` becomes that message's hint).
86
+ * Routing zero results through EMPTY_RESULTS would map to exit 1 and break the
87
+ * uniform `ok:true` list contract, so the run body never throws on emptiness.
88
+ * An empty PAGE is the same calm answer: a caller who walked past the last row
89
+ * asked a legitimate question and got a legitimate nothing.
90
+ *
91
+ * `limit + 1` rows are asked for and at most `limit` returned. That extra row
92
+ * is the whole "are there more" answer — a `COUNT` over the filtered
93
+ * population would be a second full evaluation of the same query to learn one
94
+ * boolean.
68
95
  */
69
96
  export function makeListRun(
70
97
  shape: PackList,
71
98
  meta: ListRunMeta,
72
- ): (params: Record<string, unknown>, rt: PragmaRuntime) => Promise<PackRow[]> {
99
+ ): (params: Record<string, unknown>, rt: PragmaRuntime) => Promise<PackPage> {
73
100
  return async (params, rt) => {
74
- const rows = await runSelect(rt, shape.query, meta.source);
75
101
  const vocabularies = await readFilterVocabularies(
76
102
  rt,
77
103
  shape.filters,
78
104
  params,
79
105
  meta.source,
80
106
  );
81
- return applyPackSearch(
82
- applyPackFilters(rows, shape.filters, params, vocabularies),
83
- shape.search,
107
+ const predicates = resolveFilterPredicates(
108
+ shape.filters,
84
109
  params,
110
+ vocabularies,
111
+ meta.source.label,
112
+ );
113
+ const search = readSearchTerm(shape.search, params);
114
+ const scope = await resolveReadScope(
115
+ rt,
116
+ meta.tierScope,
117
+ params.tier,
118
+ meta.source,
119
+ );
120
+ // The scope as the query builder takes it, or nothing: a story with no
121
+ // declared hierarchy, and a read that asked for every tier, both compile
122
+ // the query they always did.
123
+ const tiers = scope ? scopeIris(scope) : [];
124
+ const listScope =
125
+ meta.tierScope && tiers.length > 0
126
+ ? { entity: ENTITY_VARIABLE, via: meta.tierScope.via, tiers }
127
+ : undefined;
128
+ const limit = readLimit(params.limit);
129
+ // The cursor is spendable only on the read that issued it, and the read IS
130
+ // the author query plus the admitted arguments — the tier scope among them.
131
+ // A cursor minted under one scope and spent under another would page
132
+ // through a population it was not cut from, which is the same defect the
133
+ // fingerprint exists to prevent for filters.
134
+ const fingerprint = pageFingerprint([
135
+ shape.query,
136
+ JSON.stringify(predicates),
137
+ JSON.stringify(search ?? null),
138
+ // Only when there IS a scope: the fingerprint names the read, and an
139
+ // unscoped read has no scope to name. Adding an empty one unconditionally
140
+ // would re-hash every cursor every unscoped story has ever issued, for a
141
+ // component that says nothing about them.
142
+ ...(listScope ? [JSON.stringify(tiers)] : []),
143
+ ]);
144
+ const offset = readCursor(params.after, fingerprint);
145
+ const rows = await runSelect(
146
+ rt,
147
+ buildListQuery({
148
+ query: shape.query,
149
+ predicates,
150
+ ...(search ? { search } : {}),
151
+ ...(listScope ? { scope: listScope } : {}),
152
+ window: { limit: limit + 1, offset },
153
+ label: meta.source.label,
154
+ }),
155
+ meta.source,
85
156
  );
157
+ const hasMore = rows.length > limit;
158
+ const applied = appliedFilters(shape, params, search?.term, scope);
159
+ return {
160
+ rows: hasMore ? rows.slice(0, limit) : rows,
161
+ ...(hasMore
162
+ ? { nextAfter: encodeCursor(offset + limit, fingerprint) }
163
+ : {}),
164
+ ...(applied.length > 0 ? { filters: applied } : {}),
165
+ ...(scope?.kind === "tiers"
166
+ ? { scope: { tiers: scope.tiers.map((tier) => tier.local) } }
167
+ : {}),
168
+ limit,
169
+ };
86
170
  };
87
171
  }
88
172
 
173
+ /**
174
+ * The filters this read was narrowed by, in the spelling the caller used.
175
+ *
176
+ * Read off the DECLARED filters and the search term rather than off the
177
+ * compiled predicates: what an empty answer has to name is the argument a
178
+ * reader typed (`--search zzzznotreal`), not the query variable it bound to.
179
+ * A repeated flag accumulates into an array; it is joined here so the renderer
180
+ * never has to know that.
181
+ *
182
+ * A per-call `--tier` counts as one of them, and a CONFIGURED or default scope
183
+ * does not. The distinction is the one this list exists to draw: these are the
184
+ * arguments a reader TYPED, so an empty answer can hand them back as flags to
185
+ * edit. A scope nobody asked for is reported by the page's own `scope` instead,
186
+ * which every format states in its own register.
187
+ *
188
+ * @param shape - The story's list shape (its declared filters).
189
+ * @param params - The arguments this invocation supplied.
190
+ * @param search - The normalised search term, when the caller supplied one.
191
+ * @param scope - The tier scope in force, when the noun is tiered.
192
+ * @returns One entry per filter in force, in declared order, search last.
193
+ */
194
+ function appliedFilters(
195
+ shape: PackList,
196
+ params: Record<string, unknown>,
197
+ search: string | undefined,
198
+ scope: TierScope | undefined,
199
+ ): PackAppliedFilter[] {
200
+ const applied: PackAppliedFilter[] = [];
201
+ for (const filter of shape.filters ?? []) {
202
+ const provided = params[filter.param];
203
+ if (provided === undefined) continue;
204
+ const value = Array.isArray(provided)
205
+ ? provided.map(String).join(", ")
206
+ : String(provided);
207
+ if (value === "") continue;
208
+ applied.push({ param: filter.param, value });
209
+ }
210
+ if (search !== undefined) applied.push({ param: "search", value: search });
211
+ if (scope !== undefined && typeof params.tier === "string") {
212
+ const value = params.tier.trim();
213
+ if (value !== "") applied.push({ param: "tier", value });
214
+ }
215
+ return applied;
216
+ }
217
+
89
218
  /**
90
219
  * Read the declared vocabulary of each value-free filter the caller actually
91
220
  * used.
@@ -130,12 +259,22 @@ async function readFilterVocabularies(
130
259
  return resolved;
131
260
  }
132
261
 
133
- /** Build the run body for a lookup verb (variadic names → resolved entities). */
262
+ /**
263
+ * Build the run body for a lookup verb (variadic names → resolved entities).
264
+ *
265
+ * A TIERED noun's lookup carries the scope too, and it is the same scope its
266
+ * list carries — one rule, so `block list` and `block lookup` cannot disagree
267
+ * about which Button a reader means. What differs is what the scope DOES: a
268
+ * list omits what is out of scope, a lookup prefers what is in it and falls
269
+ * back rather than answering "no such block" about a block that exists
270
+ * ({@link ./resolveEntity.applyScope}).
271
+ */
134
272
  export function makeLookupRun(
135
273
  lookup: PackLookup,
136
274
  noun: string,
137
275
  source: StorySource,
138
276
  prefixes: Readonly<Record<string, string>>,
277
+ tierScope?: PackTierScope,
139
278
  ): (
140
279
  params: Record<string, unknown>,
141
280
  rt: PragmaRuntime,
@@ -143,6 +282,7 @@ export function makeLookupRun(
143
282
  return async (params, rt) => {
144
283
  const names = readNames(params);
145
284
  const level = await resolvePackDetail(rt, lookup.disclosure);
285
+ const scope = await resolveReadScope(rt, tierScope, params.tier, source);
146
286
  const output = await resolveLookup(
147
287
  rt,
148
288
  lookup,
@@ -151,6 +291,7 @@ export function makeLookupRun(
151
291
  source,
152
292
  prefixes,
153
293
  level,
294
+ lookupScope(tierScope, scope),
154
295
  );
155
296
  // A total miss (single or all-miss) exits non-zero; a partial batch renders
156
297
  // the results it found and notes the misses.
@@ -169,7 +310,36 @@ export function makeLookupRun(
169
310
  };
170
311
  }
171
312
 
172
- /** Build the run body for a sample verb (N random entities at the highest level). */
313
+ /**
314
+ * The resolver's view of a scope, or `undefined` when there is nothing to
315
+ * narrow by.
316
+ *
317
+ * `all` collapses to `undefined` rather than to an empty tier set: a resolver
318
+ * handed an empty set would treat every match as out of scope and report a
319
+ * fallback for a read that asked for every tier.
320
+ */
321
+ function lookupScope(
322
+ declaration: PackTierScope | undefined,
323
+ scope: TierScope | undefined,
324
+ ): LookupScope | undefined {
325
+ if (!declaration || scope === undefined || scope.kind === "all") {
326
+ return undefined;
327
+ }
328
+ const label = scopeLabel(scope);
329
+ if (label === undefined) return undefined;
330
+ return { via: declaration.via, tiers: scopeIris(scope), label };
331
+ }
332
+
333
+ /**
334
+ * Build the run body for a sample verb (N random entities at the highest level).
335
+ *
336
+ * UNSCOPED, deliberately, and it is the one read of a tiered noun that is. A
337
+ * sample is a shape probe — "show me what a block record looks like" — drawn at
338
+ * the highest disclosure level for exactly that reason, and the tier a specimen
339
+ * happens to belong to is not part of the shape. Narrowing it would also make
340
+ * the draw pool depend on a setting, so two agents reading the same store would
341
+ * see different exemplars for reasons neither asked about.
342
+ */
173
343
  export function makeSampleRun(
174
344
  lookup: PackLookup,
175
345
  noun: string,