@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,57 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { formatValidOptions, MAX_LISTED_OPTIONS } from "./validOptions.js";
3
+
4
+ /** `count` option names, distinct and ordered. */
5
+ function options(count: number): string[] {
6
+ return Array.from({ length: count }, (_, index) => `option-${index}`);
7
+ }
8
+
9
+ describe("formatValidOptions", () => {
10
+ it("names every option when the vocabulary is a choice", () => {
11
+ // Every real filter at or below the cap renders in full: the type filter
12
+ // at 7, the tier filter at 16, the coordinate set at 36.
13
+ const line = formatValidOptions(["color", "dimension", "duration"]);
14
+
15
+ expect(line).toBe("Valid options: color, dimension, duration");
16
+ });
17
+
18
+ it("names every option at exactly the cap", () => {
19
+ const line = formatValidOptions(options(MAX_LISTED_OPTIONS));
20
+
21
+ expect(line).toContain(`option-${MAX_LISTED_OPTIONS - 1}`);
22
+ expect(line).not.toContain("more");
23
+ });
24
+
25
+ it("withholds the tail one past the cap, and counts it", () => {
26
+ const line = formatValidOptions(options(MAX_LISTED_OPTIONS + 1));
27
+
28
+ expect(line).toContain(`(${MAX_LISTED_OPTIONS + 1})`);
29
+ expect(line).toContain("and 1 more");
30
+ expect(line).not.toContain(`option-${MAX_LISTED_OPTIONS}`);
31
+ });
32
+
33
+ it("keeps a catalogue-sized vocabulary to a readable line", () => {
34
+ // The case that forced this: the variable filter's vocabulary is 1,156
35
+ // names, and naming them all rendered roughly 90KB — unreadable in a
36
+ // terminal, and large enough over MCP to threaten the payload budget.
37
+ const line = formatValidOptions(options(1156));
38
+
39
+ expect(line).toContain("(1156)");
40
+ expect(line).toContain("and 1116 more");
41
+ expect(line.length).toBeLessThan(2000);
42
+ });
43
+
44
+ it("carries a caller's own prefix", () => {
45
+ // The prefix seam is how a caller that renders its OWN options line names
46
+ // the flag. Nothing in the error path passes one any more: the renderers
47
+ // print the vocabulary once, from `validOptions`, and a recovery that
48
+ // enumerated it too printed the same catalogue twice.
49
+ const line = formatValidOptions(
50
+ options(1156),
51
+ "Values allowed for --variable",
52
+ );
53
+
54
+ expect(line.startsWith("Values allowed for --variable (1156)")).toBe(true);
55
+ expect(line.length).toBeLessThan(2000);
56
+ });
57
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Naming a filter's admissible values without printing a catalogue.
3
+ *
4
+ * ONE error, ONE list. The vocabulary rides an error's `validOptions`, and the
5
+ * renderers here are the only place it is spelled out — a filter that also
6
+ * enumerated it in its `recovery` message printed the same 40 names twice,
7
+ * about 1.4KB of duplicate terminal output on a single mistyped `--symbol`. A
8
+ * recovery says what to DO; the options line says what the values ARE.
9
+ */
10
+
11
+ /**
12
+ * How many valid options a rendered error names before it stops listing them.
13
+ *
14
+ * Taken from the real vocabularies rather than picked round: the largest a
15
+ * reader can usefully scan is the coordinate set at 36, so every filter at or
16
+ * below this cap still renders in full. Above it sit the vocabularies that are
17
+ * catalogues rather than choices — variables at 1,156, symbols at 745, blocks
18
+ * at 252 — and naming those turned one mistyped filter value into roughly 90KB
19
+ * of terminal output, or an MCP response large enough to threaten its own
20
+ * payload budget. The JSON error envelope still carries every option, so a
21
+ * machine consumer loses nothing.
22
+ */
23
+ export const MAX_LISTED_OPTIONS = 40;
24
+
25
+ /**
26
+ * Render an admissible-value list, naming at most {@link MAX_LISTED_OPTIONS}.
27
+ *
28
+ * The count of what is withheld is the useful part of a long vocabulary: it
29
+ * tells the reader this is a catalogue to list, not a choice to read.
30
+ *
31
+ * @param options - Every accepted value, in the order the caller supplies.
32
+ * @param prefix - How the line introduces the values.
33
+ * @returns One line, with no trailing newline and no full stop.
34
+ */
35
+ export function formatValidOptions(
36
+ options: readonly string[],
37
+ prefix = "Valid options",
38
+ ): string {
39
+ if (options.length <= MAX_LISTED_OPTIONS) {
40
+ return `${prefix}: ${options.join(", ")}`;
41
+ }
42
+ const shown = options.slice(0, MAX_LISTED_OPTIONS).join(", ");
43
+ const rest = options.length - MAX_LISTED_OPTIONS;
44
+ return `${prefix} (${options.length}), first ${MAX_LISTED_OPTIONS}: ${shown}, and ${rest} more`;
45
+ }
@@ -259,6 +259,8 @@ describe("validateStories — package stories NEVER throw (PROTECTED)", () => {
259
259
  const before = capabilities.find((module) => module.name === "token");
260
260
  expect(before?.verbs.map((verb) => verb.path.join(" "))).toEqual([
261
261
  "token list",
262
+ "token values",
263
+ "token consumers",
262
264
  "token lookup",
263
265
  "token sample",
264
266
  ]);
@@ -10,11 +10,16 @@
10
10
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
11
11
  import { RECOVERY_CLI_PREFIX } from "../../constants.js";
12
12
  import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
13
+ import { PragmaError } from "../error/index.js";
13
14
  import type { PragmaRuntime } from "../runtime/types.js";
15
+ import { kebabCase } from "../spec/emitSurface.js";
16
+ import type { VerbSpec } from "../spec/types.js";
14
17
  import { compileListable, compilePack, compileStoryModule } from "./compile.js";
18
+ import { encodeCursor, pageFingerprint } from "./cursor.js";
19
+ import { DEFAULT_LIST_LIMIT, MAX_LIST_WINDOW } from "./paging.js";
15
20
  import type { LookupOutput } from "./resolveEntity.js";
16
21
  import { parsePackDefinition } from "./schema.js";
17
- import type { PackDefinition, PackRow } from "./types.js";
22
+ import type { PackDefinition, PackPage, PackRow } from "./types.js";
18
23
  import { distributionSource } from "./types.js";
19
24
  import { assertUniqueVerbs, verbKey } from "./uniqueness.js";
20
25
 
@@ -135,7 +140,14 @@ describe("pack compiler — round-trip + shape (PROTECTED, storeless)", () => {
135
140
  distributionSource("bundled:widget"),
136
141
  PREFIXES,
137
142
  );
138
- expect(list?.params.map((p) => p.name)).toEqual(["kind", "search"]);
143
+ // The page pair is the KERNEL's, on every list-shaped verb whether the
144
+ // story mentions it or not — see compile.ts#PAGE_PARAMS.
145
+ expect(list?.params.map((p) => p.name)).toEqual([
146
+ "kind",
147
+ "search",
148
+ "limit",
149
+ "after",
150
+ ]);
139
151
  const kind = list?.params.find((p) => p.name === "kind");
140
152
  expect(kind?.kind).toBe("enum");
141
153
  expect(list?.capability.needsStore).toBe(true);
@@ -248,8 +260,8 @@ describe("the grammar rejects what the compiler cannot build (PROTECTED)", () =>
248
260
 
249
261
  it("rejects a filter param declared twice, on list or on an extra verb", () => {
250
262
  const twice = [
251
- { param: "kind", variable: "uri" },
252
- { param: "kind", variable: "uri" },
263
+ { param: "kind", variable: "uri", values: ["a"] },
264
+ { param: "kind", variable: "uri", values: ["a"] },
253
265
  ];
254
266
  expect(
255
267
  parse({ noun: "widget", list: { ...listShape, filters: twice } }),
@@ -263,6 +275,51 @@ describe("the grammar rejects what the compiler cannot build (PROTECTED)", () =>
263
275
  ).toThrow(/filter param "kind" is declared twice/);
264
276
  });
265
277
 
278
+ it("admits a camelCase filter param, and it reaches the flag the projector spells", () => {
279
+ // The grammar and the projector have to agree about what a param name may
280
+ // be. `emitVerb` writes a flag as `--${kebabCase(param)}`, so a two-word
281
+ // dimension only has a readable spelling if the param may carry the hump —
282
+ // and the gate used to refuse the one spelling that projects well while
283
+ // admitting `channelof`, which projects to `--channelof`.
284
+ const definition = parsePackDefinition(
285
+ {
286
+ noun: "widget",
287
+ list: {
288
+ query:
289
+ "SELECT ?uri ?channelOf WHERE { ?uri ex:channelOf ?channelOf }",
290
+ columns: [{ field: "uri" }, { field: "channelOf" }],
291
+ filters: [
292
+ { param: "channelOf", variable: "channelOf", values: ["a"] },
293
+ ],
294
+ },
295
+ },
296
+ "pkg/stories/widget.json",
297
+ );
298
+ const list = compilePack(
299
+ definition,
300
+ distributionSource("pkg/stories/widget.json"),
301
+ PREFIXES,
302
+ ).find((verb) => verbKey(verb.path) === "widget list") as VerbSpec;
303
+ // The MCP key keeps the declared spelling; the CLI flag is its kebab form.
304
+ expect(list.params.map((param) => param.name)).toContain("channelOf");
305
+ expect(`--${kebabCase("channelOf")}`).toBe("--channel-of");
306
+ });
307
+
308
+ it("still refuses a filter param that does not start lowercase", () => {
309
+ // A leading capital would kebab to a LEADING DASH, and Commander would
310
+ // register a flag with an empty name — outside every error envelope the
311
+ // CLI owns. The widening is the hump, not the initial.
312
+ expect(
313
+ parse({
314
+ noun: "widget",
315
+ list: {
316
+ ...listShape,
317
+ filters: [{ param: "ChannelOf", variable: "uri", values: ["a"] }],
318
+ },
319
+ }),
320
+ ).toThrow();
321
+ });
322
+
266
323
  it("still accepts the distinct forms, and they compile to unique keys/params", () => {
267
324
  const definition = parsePackDefinition(
268
325
  {
@@ -270,8 +327,8 @@ describe("the grammar rejects what the compiler cannot build (PROTECTED)", () =>
270
327
  list: {
271
328
  ...listShape,
272
329
  filters: [
273
- { param: "kind", variable: "uri" },
274
- { param: "tier", variable: "uri" },
330
+ { param: "kind", variable: "uri", values: ["a"] },
331
+ { param: "tier", variable: "uri", values: ["b"] },
275
332
  ],
276
333
  },
277
334
  verbs: [{ ...listShape, verb: "categories" }],
@@ -291,7 +348,12 @@ describe("the grammar rejects what the compiler cannot build (PROTECTED)", () =>
291
348
  "widget sample",
292
349
  ]);
293
350
  expect(() => assertUniqueVerbs(verbs)).not.toThrow();
294
- expect(verbs.at(0)?.params.map((p) => p.name)).toEqual(["kind", "tier"]);
351
+ expect(verbs.at(0)?.params.map((p) => p.name)).toEqual([
352
+ "kind",
353
+ "tier",
354
+ "limit",
355
+ "after",
356
+ ]);
295
357
  });
296
358
 
297
359
  it("names the shape a noun must have, not the regex", () => {
@@ -331,6 +393,24 @@ describe("pack compiler — SPARQL fetch path (PROTECTED)", () => {
331
393
  (await rt.store.get()).store.dispose();
332
394
  });
333
395
 
396
+ /**
397
+ * The INVALID_INPUT recovery a call raises, which is where a pack error puts
398
+ * the sentence a caller acts on — the factory's own message is the uniform
399
+ * `Invalid <field> "<value>".`
400
+ */
401
+ const recoveryOf = async (call: () => Promise<unknown>): Promise<string> => {
402
+ try {
403
+ await call();
404
+ } catch (error) {
405
+ if (error instanceof PragmaError) {
406
+ expect(error.code).toBe("INVALID_INPUT");
407
+ return error.recovery?.message ?? "";
408
+ }
409
+ throw error;
410
+ }
411
+ throw new Error("expected the call to be refused");
412
+ };
413
+
334
414
  const run = <R>(verbLabel: string, params: Record<string, unknown>) => {
335
415
  const verb = compilePack(
336
416
  WIDGET_PACK,
@@ -342,19 +422,112 @@ describe("pack compiler — SPARQL fetch path (PROTECTED)", () => {
342
422
  };
343
423
 
344
424
  it("lists rows in the uniform pack shape", async () => {
345
- const rows = await run<PackRow[]>("list", {});
346
- expect(rows.map((r) => r.name)).toEqual(["Button", "Label"]);
347
- expect(rows[0]?.uri).toBe("https://example.org/widgets#button");
425
+ const page = await run<PackPage>("list", {});
426
+ expect(page.rows.map((r) => r.name)).toEqual(["Button", "Label"]);
427
+ expect(page.rows[0]?.uri).toBe("https://example.org/widgets#button");
428
+ // The whole population fits the default page, so nothing is withheld and
429
+ // there is no cursor to spend.
430
+ expect(page.nextAfter).toBeUndefined();
431
+ expect(page.limit).toBe(DEFAULT_LIST_LIMIT);
348
432
  });
349
433
 
350
- it("filters by an enum value (post-query row predicate)", async () => {
351
- const rows = await run<PackRow[]>("list", { kind: "input" });
352
- expect(rows.map((r) => r.name)).toEqual(["Button"]);
434
+ it("filters by an enum value, compiled into the query", async () => {
435
+ const page = await run<PackPage>("list", { kind: "input" });
436
+ expect(page.rows.map((r) => r.name)).toEqual(["Button"]);
353
437
  });
354
438
 
355
439
  it("searches case-insensitively", async () => {
356
- const rows = await run<PackRow[]>("list", { search: "lab" });
357
- expect(rows.map((r) => r.name)).toEqual(["Label"]);
440
+ const page = await run<PackPage>("list", { search: "lab" });
441
+ expect(page.rows.map((r) => r.name)).toEqual(["Label"]);
442
+ });
443
+
444
+ it("pages inside the query: a page, its successor, and their union", async () => {
445
+ const whole = await run<PackPage>("list", {});
446
+ const first = await run<PackPage>("list", { limit: 1 });
447
+ expect(first.rows).toEqual([whole.rows[0]]);
448
+ expect(first.nextAfter).toBeTypeOf("string");
449
+ const second = await run<PackPage>("list", {
450
+ limit: 1,
451
+ after: first.nextAfter,
452
+ });
453
+ expect(second.rows).toEqual([whole.rows[1]]);
454
+ // The last page reports no successor, and one past it is a calm empty page
455
+ // rather than an error.
456
+ expect(second.nextAfter).toBeUndefined();
457
+ const past = await run<PackPage>("list", {
458
+ limit: 1,
459
+ after: encodeCursor(
460
+ 9,
461
+ pageFingerprint([WIDGET_PACK.list?.query ?? "", "[]", "null"]),
462
+ ),
463
+ });
464
+ expect(past.rows).toEqual([]);
465
+ });
466
+
467
+ it("refuses a cursor issued for a different read", async () => {
468
+ const filtered = await run<PackPage>("list", { limit: 1, kind: "input" });
469
+ const unfiltered = await run<PackPage>("list", { limit: 1 });
470
+ // Spending the unfiltered read's cursor on the filtered one would answer a
471
+ // page of the wrong question, with nothing in the payload to show it.
472
+ await expect(
473
+ recoveryOf(() =>
474
+ run<PackPage>("list", {
475
+ limit: 1,
476
+ kind: "input",
477
+ after: unfiltered.nextAfter,
478
+ }),
479
+ ),
480
+ ).resolves.toMatch(/issued for a different read/);
481
+ expect(filtered.nextAfter).toBeUndefined();
482
+ });
483
+
484
+ it("refuses a limit that is not a whole number of rows", async () => {
485
+ await expect(
486
+ recoveryOf(() => run<PackPage>("list", { limit: 0 })),
487
+ ).resolves.toMatch(new RegExp(`1 to ${MAX_LIST_WINDOW}`));
488
+ });
489
+
490
+ it("refuses a limit above the ceiling, naming it", async () => {
491
+ // `--limit 9007199254740991` is the natural "give me everything" an agent
492
+ // writes, and it used to reach the store's own `LIMIT` — which must fit a
493
+ // 32-bit integer — and come back as a parse error dressed as
494
+ // INTERNAL_ERROR with "report this issue". A typed refusal that says what
495
+ // it would accept is the answer to a legitimate question asked too big.
496
+ for (const limit of [MAX_LIST_WINDOW + 1, 2 ** 32 - 1, 1e21, 2 ** 53 - 1]) {
497
+ await expect(
498
+ recoveryOf(() => run<PackPage>("list", { limit })),
499
+ ).resolves.toMatch(new RegExp(`1 to ${MAX_LIST_WINDOW}`));
500
+ }
501
+ // The ceiling itself is admitted: a refusal one row early would be a
502
+ // different bug.
503
+ await expect(
504
+ run<PackPage>("list", { limit: MAX_LIST_WINDOW }),
505
+ ).resolves.toBeDefined();
506
+ });
507
+
508
+ it("refuses a cursor whose offset is above the ceiling", async () => {
509
+ // The fingerprint covers the query and the arguments, not the offset — so
510
+ // a real cursor with only `o` edited is fingerprint-valid, and that is the
511
+ // path by which an unbounded offset reached the query's own `OFFSET`.
512
+ const fingerprint = pageFingerprint([
513
+ WIDGET_PACK.list?.query ?? "",
514
+ "[]",
515
+ "null",
516
+ ]);
517
+ await expect(
518
+ recoveryOf(() =>
519
+ run<PackPage>("list", {
520
+ limit: 1,
521
+ after: encodeCursor(2 ** 53 - 1, fingerprint),
522
+ }),
523
+ ),
524
+ ).resolves.toMatch(/cursor from a previous page/);
525
+ // An offset the kernel can serve is still served, empty page and all.
526
+ const edge = await run<PackPage>("list", {
527
+ limit: 1,
528
+ after: encodeCursor(MAX_LIST_WINDOW, fingerprint),
529
+ });
530
+ expect(edge.rows).toEqual([]);
358
531
  });
359
532
 
360
533
  it("looks up an entity by name with its fields and expands (detailed)", async () => {
@@ -15,6 +15,7 @@
15
15
  */
16
16
 
17
17
  import { BIN_NAME } from "../../constants.js";
18
+ import { PragmaError } from "../error/index.js";
18
19
  import { compactUri, DEFAULT_PREFIX_MAP } from "../render/index.js";
19
20
  import type { PragmaRuntime } from "../runtime/index.js";
20
21
  import { asVerb } from "../spec/asVerb.js";
@@ -25,6 +26,7 @@ import type {
25
26
  ParamSpec,
26
27
  VerbSpec,
27
28
  } from "../spec/types.js";
29
+ import { DEFAULT_LIST_LIMIT, MAX_LIST_WINDOW } from "./paging.js";
28
30
  import {
29
31
  listFormatters,
30
32
  lookupFormatters,
@@ -37,14 +39,18 @@ import {
37
39
  MIN_SAMPLE_COUNT,
38
40
  sampleDefaultCount,
39
41
  } from "./sample.js";
40
- import type {
41
- PackDefinition,
42
- PackFilter,
43
- PackList,
44
- PackLookup,
45
- PackRow,
46
- PackSearch,
47
- StorySource,
42
+ import { storyIssues } from "./storyRules.js";
43
+ import {
44
+ EVERY_TIER,
45
+ type PackDefinition,
46
+ type PackFilter,
47
+ type PackList,
48
+ type PackLookup,
49
+ type PackPage,
50
+ type PackSearch,
51
+ type PackTierScope,
52
+ type StorySource,
53
+ TIER_PARAM,
48
54
  } from "./types.js";
49
55
 
50
56
  // The run bodies pull the SPARQL/GraphQL fetch layer; they are dynamic-imported
@@ -78,6 +84,8 @@ export function compilePack(
78
84
  const { noun } = definition;
79
85
  const verbs: VerbSpec[] = [];
80
86
 
87
+ const tierScope = definition.tierScope;
88
+
81
89
  if (definition.list) {
82
90
  verbs.push(
83
91
  compileListVerb(definition.list, {
@@ -87,6 +95,7 @@ export function compilePack(
87
95
  doc: definition.toolDescription,
88
96
  source,
89
97
  prefixes,
98
+ ...(tierScope ? { tierScope } : {}),
90
99
  }),
91
100
  );
92
101
  }
@@ -100,12 +109,15 @@ export function compilePack(
100
109
  doc: verb.toolDescription,
101
110
  source,
102
111
  prefixes,
112
+ ...(tierScope ? { tierScope } : {}),
103
113
  }),
104
114
  );
105
115
  }
106
116
 
107
117
  if (definition.lookup) {
108
- verbs.push(compileLookupVerb(definition.lookup, noun, source, prefixes));
118
+ verbs.push(
119
+ compileLookupVerb(definition.lookup, noun, source, prefixes, tierScope),
120
+ );
109
121
  if (definition.lookup.sample) {
110
122
  verbs.push(compileSampleVerb(definition.lookup, noun, source, prefixes));
111
123
  }
@@ -171,16 +183,33 @@ export function compileListable(
171
183
  * through here alike, so a module-level projection derived from the story
172
184
  * (today: {@link compileListable}) cannot reach one tier and miss the other.
173
185
  *
186
+ * That door is also where the COMPILABILITY rules are checked
187
+ * ({@link ./storyRules.storyIssues}), for the same reason. The zod grammar runs
188
+ * for config- and package-declared stories only, so a rule stated only there
189
+ * was a declaration-time refusal for a third-party author and a first-call
190
+ * CONFIG_ERROR for the distribution's own stories. The rules are pure string
191
+ * work over text the story already carries, which is what makes them affordable
192
+ * on this path; a story that passed zod has passed them already, so the throw
193
+ * below is reachable only for a story that never saw zod.
194
+ *
174
195
  * @param definition - A validated pack definition.
175
196
  * @param source - Where the definition came from, for diagnostics.
176
197
  * @param prefixes - The merged prefix map used for display compaction.
177
198
  * @returns The module: the compiled verbs plus the story's module-level data.
199
+ * @throws PragmaError CONFIG_ERROR when the definition cannot be compiled,
200
+ * naming the field and the rule.
178
201
  */
179
202
  export function compileStoryModule(
180
203
  definition: PackDefinition,
181
204
  source: StorySource,
182
205
  prefixes: Readonly<Record<string, string>>,
183
206
  ): CapabilityModule {
207
+ const issue = storyIssues(definition)[0];
208
+ if (issue) {
209
+ throw PragmaError.configError(
210
+ `Invalid story in ${source.label} at ${issue.path.join(".")}: ${issue.message}`,
211
+ );
212
+ }
184
213
  const listable = compileListable(definition);
185
214
  return {
186
215
  name: definition.noun,
@@ -200,6 +229,8 @@ interface ListVerbMeta {
200
229
  readonly doc?: string;
201
230
  readonly source: StorySource;
202
231
  readonly prefixes: Readonly<Record<string, string>>;
232
+ /** The noun's declared tier hierarchy, when its entities are tiered. */
233
+ readonly tierScope?: PackTierScope;
203
234
  }
204
235
 
205
236
  /** Compile the `list` verb or an extra list-shaped verb. */
@@ -207,9 +238,11 @@ function compileListVerb(shape: PackList, meta: ListVerbMeta): VerbSpec {
207
238
  const params = [
208
239
  ...projectFilters(shape.filters),
209
240
  ...projectSearch(shape.search),
241
+ ...tierParams(meta.tierScope),
242
+ ...PAGE_PARAMS,
210
243
  ];
211
244
  const filterExample = shape.filters?.find((f) => f.values !== undefined);
212
- const verb: VerbSpec<Record<string, unknown>, PackRow[]> = {
245
+ const verb: VerbSpec<Record<string, unknown>, PackPage> = {
213
246
  path: [meta.noun, meta.verb],
214
247
  summary: meta.summary,
215
248
  ...(meta.doc ? { doc: meta.doc } : {}),
@@ -237,7 +270,10 @@ function compileListVerb(shape: PackList, meta: ListVerbMeta): VerbSpec {
237
270
  capability: READ_CAPABILITY,
238
271
  run: (params: Record<string, unknown>, rt: PragmaRuntime) =>
239
272
  runBodies().then((m) =>
240
- m.makeListRun(shape, { source: meta.source })(params, rt),
273
+ m.makeListRun(shape, {
274
+ source: meta.source,
275
+ ...(meta.tierScope ? { tierScope: meta.tierScope } : {}),
276
+ })(params, rt),
241
277
  ),
242
278
  };
243
279
  return asVerb(verb);
@@ -249,6 +285,7 @@ function compileLookupVerb(
249
285
  noun: string,
250
286
  source: StorySource,
251
287
  prefixes: Readonly<Record<string, string>>,
288
+ tierScope?: PackTierScope,
252
289
  ): VerbSpec {
253
290
  // Derive-by-default: every lookup completes its `<name>` from the pack index
254
291
  // (empty type = any, the reader handles it), UNLESS the pack opts out
@@ -282,21 +319,55 @@ function compileLookupVerb(
282
319
  summary:
283
320
  lookup.description ?? `Look up ${noun} details by name, IRI, or glob.`,
284
321
  ...(lookup.toolDescription ? { doc: lookup.toolDescription } : {}),
285
- params: [nameParam],
322
+ params: [nameParam, ...tierParams(tierScope)],
286
323
  output: { formatters: lookupFormatters(lookup, prefixes) },
287
- examples: [{ cmd: `${BIN_NAME} ${noun} lookup <name>` }],
324
+ examples: [
325
+ { cmd: `${BIN_NAME} ${noun} lookup <name>` },
326
+ ...(tierScope
327
+ ? [
328
+ {
329
+ cmd: `${BIN_NAME} ${noun} lookup --tier all <name>`,
330
+ note: "every tier, not just the ones in scope",
331
+ },
332
+ ]
333
+ : []),
334
+ ],
288
335
  ...(lookup.disclosure
289
336
  ? { disclosure: disclosureSpec(lookup.disclosure) }
290
337
  : {}),
291
338
  capability: READ_CAPABILITY,
292
339
  run: (params: Record<string, unknown>, rt: PragmaRuntime) =>
293
340
  runBodies().then((m) =>
294
- m.makeLookupRun(lookup, noun, source, prefixes)(params, rt),
341
+ m.makeLookupRun(lookup, noun, source, prefixes, tierScope)(params, rt),
295
342
  ),
296
343
  };
297
344
  return asVerb(verb);
298
345
  }
299
346
 
347
+ /**
348
+ * The `--tier` parameter a TIERED noun's reads carry (`tier` over MCP).
349
+ *
350
+ * Kernel-added, like the page: being tiered is declared, but the argument that
351
+ * steers the scope is the kernel's to name — one spelling across every tiered
352
+ * noun, so an agent that learned `--tier` on `block list` can use it on
353
+ * `concept list` without asking. The help text names the default and the
354
+ * escape, because a scope a caller cannot see is exactly the hidden behaviour
355
+ * CONSTITUTION §VI rules out.
356
+ *
357
+ * Not `repeatable`: a repeated `--tier` would be two chains, and the scope is
358
+ * one chain. The run body refuses an array rather than quietly taking the last.
359
+ */
360
+ function tierParams(tierScope: PackTierScope | undefined): ParamSpec[] {
361
+ if (!tierScope) return [];
362
+ return [
363
+ {
364
+ kind: "string",
365
+ name: TIER_PARAM,
366
+ doc: `Read this tier and its ancestors (default: the top-level tiers; "${EVERY_TIER}" for every tier).`,
367
+ },
368
+ ];
369
+ }
370
+
300
371
  /** Compile the `sample` verb (N random exemplars at the highest level). */
301
372
  function compileSampleVerb(
302
373
  lookup: PackLookup,
@@ -355,6 +426,30 @@ function disclosureSpec(disclosure: PackLookup["disclosure"]): DisclosureSpec {
355
426
  };
356
427
  }
357
428
 
429
+ /**
430
+ * The page parameters EVERY list-shaped verb carries — `list` and every extra
431
+ * verb alike.
432
+ *
433
+ * Not declared per story, and not declarable: a page is a property of a
434
+ * list-shaped read, so a story that forgot to declare one would be the one
435
+ * answer an agent could not bound. The default is named in the help text
436
+ * because a cap a caller cannot see is exactly the hidden behaviour
437
+ * CONSTITUTION §VI rules out — the number a caller gets when they pass nothing
438
+ * has to be readable in `--help`.
439
+ */
440
+ const PAGE_PARAMS: readonly ParamSpec[] = [
441
+ {
442
+ kind: "number",
443
+ name: "limit",
444
+ doc: `Maximum rows to return, 1 to ${MAX_LIST_WINDOW} (default ${DEFAULT_LIST_LIMIT}).`,
445
+ },
446
+ {
447
+ kind: "string",
448
+ name: "after",
449
+ doc: "Continue from a previous page: the cursor that page reported.",
450
+ },
451
+ ];
452
+
358
453
  /** Project declared filters onto verb params (enum for a value set, else string). */
359
454
  function projectFilters(
360
455
  filters: readonly PackFilter[] | undefined,