@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,3 @@
1
+ // AUTO-GENERATED by scripts/bundle.ts — do not edit by hand.
2
+ // Regenerate from the packs pragma.conf.ts declares: `bun run bundle`.
3
+ export const manifestJson = "{\"name\":\"pragma\",\"version\":\"0.37.0\",\"sourceRef\":\"@canonical/design-system@git:d65f8bacdf2a209b1b33746893c807e5370e1042, @canonical/anatomy-dsl@npm:0.6.0, @canonical/token-ontology@npm:0.10.1, @canonical/code-standards@git:fcd3ac2dea458775e5b79344b3237a3fcb37add9, @canonical/ds-implementations@self:v0.37.0\",\"contentHash\":\"e54849397af95dadd4b37e7d6ddf1d984c92ac27cac4c943db694c361feeb2c5\",\"prefixes\":{\"rdf\":\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\",\"rdfs\":\"http://www.w3.org/2000/01/rdf-schema#\",\"owl\":\"http://www.w3.org/2002/07/owl#\",\"xsd\":\"http://www.w3.org/2001/XMLSchema#\",\"skos\":\"http://www.w3.org/2004/02/skos/core#\",\"sh\":\"http://www.w3.org/ns/shacl#\",\"dcterms\":\"http://purl.org/dc/terms/\",\"ds\":\"https://ds.canonical.com/\",\"anatomy\":\"https://anatomy.canonical.com/\",\"dt\":\"https://dt.canonical.com/\",\"graphql\":\"https://pragma.canonical.com/graphql#\",\"w3c-tokens\":\"https://dt.canonical.com/w3c-tokens/\",\"dt-web\":\"https://dt.canonical.com/platform/web/\",\"cs\":\"http://pragma.canonical.com/codestandards#\"},\"createdAt\":\"2026-09-16T10:11:25.934Z\",\"tripleCount\":65298,\"entityCount\":4377}";
@@ -18,10 +18,12 @@
18
18
  *
19
19
  * The inlined strings are materialized into the ordinary content-addressed pack
20
20
  * cache on first use and then read back through {@link readPack}, so the
21
- * embedded pack and a built pack share one boot path. The story records live in
22
- * their OWN generated module (like the index): dispatch reads them on every
23
- * command, and pulling them out of `pack.generated.ts` would load its ~1.9 MB
24
- * of n-quads with them — a measured +28 ms on every invocation. Inlining as JS strings
21
+ * embedded pack and a built pack share one boot path. The story records, the
22
+ * index and the MANIFEST each live in their own generated module: every one of
23
+ * them is read on a path that does not need the n-quads, and leaving any of
24
+ * them beside `pack.generated.ts` would load its ~2.1 MB with them — a measured
25
+ * +28 ms on every invocation for the stories, and ~370 ms per CLI invocation
26
+ * for the manifest at a 30,340-triple pack. Inlining as JS strings
25
27
  * (rather than file assets) means the pack travels with the emitted modules —
26
28
  * it needs no asset step in any build, and no file lookup at run time.
27
29
  */
@@ -36,8 +38,8 @@ import {
36
38
  } from "node:fs";
37
39
  import { join } from "node:path";
38
40
  import { packDir, packsCacheDir } from "../paths.js";
39
- import { dataNq, manifestJson, schemaJson } from "./embedded/pack.generated.js";
40
41
  import { indexJson } from "./embedded/pack.index.generated.js";
42
+ import { manifestJson } from "./embedded/pack.manifest.generated.js";
41
43
  import { storiesJson } from "./embedded/pack.stories.generated.js";
42
44
  import { packIsComplete } from "./manifest.js";
43
45
  import { manifestSchema } from "./schemas.js";
@@ -72,10 +74,16 @@ export function embeddedManifest(): Manifest {
72
74
  * @returns The pack directory, ready for {@link readPack}.
73
75
  * @note Impure — writes the inlined pack files into the cache.
74
76
  */
75
- export function materializeEmbeddedPack(): string {
77
+ export async function materializeEmbeddedPack(): Promise<string> {
76
78
  const dir = packDir(embeddedManifest().contentHash);
77
79
  if (packIsComplete(dir)) return dir;
78
80
 
81
+ // Loaded HERE, not at module scope: this is the only branch that needs the
82
+ // payload, and it is the branch that does NOT run once the pack is cached.
83
+ // A static import would parse ~2.1 MB of inlined n-quads on every boot to
84
+ // reach a return statement six lines above.
85
+ const { dataNq, schemaJson } = await import("./embedded/pack.generated.js");
86
+
79
87
  mkdirSync(packsCacheDir(), { recursive: true });
80
88
  const temp = mkdtempSync(join(packsCacheDir(), ".embed-"));
81
89
  try {
@@ -144,15 +144,21 @@ describe("the committed embedded pack (PROTECTED)", () => {
144
144
  // directory holds — which the next build then reuses, silently dropping the
145
145
  // difference. Comparing the two directories catches that on the day it lands.
146
146
  const built = await build([{ path: "a.ttl", content: TTL }]);
147
- expect(readdirSync(materializeEmbeddedPack()).sort()).toEqual(
147
+ expect(readdirSync(await materializeEmbeddedPack()).sort()).toEqual(
148
148
  readdirSync(built.dir).sort(),
149
149
  );
150
- });
151
-
152
- it("is self-consistent: complete, content-addressed, and non-empty", () => {
150
+ // 60s, not the 5s default. This test does real work twice: it BUILDS a
151
+ // pack from source, and it materialises the embedded one — which on a cold
152
+ // cache takes the miss branch and imports the multi-megabyte payload. Both
153
+ // costs grow with the pack, and the pack grows whenever a source is added.
154
+ // A default that a pack size can outgrow turns a real assertion into an
155
+ // intermittent one.
156
+ }, 60_000);
157
+
158
+ it("is self-consistent: complete, content-addressed, and non-empty", async () => {
153
159
  // No network, so CI runs it: the committed strings really do materialize a
154
160
  // bootable pack whose parts agree with each other.
155
- const dir = materializeEmbeddedPack();
161
+ const dir = await materializeEmbeddedPack();
156
162
  const manifest = embeddedManifest();
157
163
  expect(packIsComplete(dir)).toBe(true);
158
164
  expect(basename(dir)).toBe(manifest.contentHash);
@@ -162,6 +168,26 @@ describe("the committed embedded pack (PROTECTED)", () => {
162
168
  expect(index.contentHash).toBe(manifest.contentHash);
163
169
  expect(manifest.tripleCount ?? 0).toBeGreaterThan(0);
164
170
  expect(manifest.entityCount ?? 0).toBeGreaterThan(0);
171
+
172
+ // EXACT, not "> 0", and it earns its keep twice.
173
+ //
174
+ // The manifest and the n-quads now live in SEPARATE generated modules, so
175
+ // a partial regeneration or a one-file `git add` can leave them describing
176
+ // different packs — a skew that could not exist while they shared a
177
+ // module. `read.ts` tolerates a superset deliberately, so it would serve
178
+ // the skew rather than refuse it.
179
+ //
180
+ // It also pins the equivalence `read.ts` depends on but cannot state:
181
+ // `store.size` (quads) against a `tripleCount` that `build.ts` computes
182
+ // with `SELECT (COUNT(*))` over the union graph. They agree on oxigraph
183
+ // 0.5.6; oxigraph is pinned by caret, so this is where a future version
184
+ // that deduplicates the union would be caught.
185
+ const session = await readPack(dir);
186
+ try {
187
+ expect(session.store.size).toBe(manifest.tripleCount);
188
+ } finally {
189
+ session.store.dispose();
190
+ }
165
191
  });
166
192
  });
167
193
 
@@ -39,7 +39,8 @@ function packUnavailable(reason: string): PragmaError {
39
39
  * Read a pack directory into a bootable store session.
40
40
  *
41
41
  * @param dir - The pack directory (must hold a complete pack).
42
- * @returns The store session: store, schema, context factory, prefixes, index.
42
+ * @returns The store session: store, schema, context factory, prefixes, index,
43
+ * and the pack's own manifest (its provenance, carried rather than re-read).
43
44
  * @throws PragmaError STORE_UNAVAILABLE when the pack is incomplete.
44
45
  * @note Impure — creates a store from the cached n-quads dump.
45
46
  */
@@ -80,15 +81,25 @@ export async function readPack(dir: string): Promise<StoreSession> {
80
81
  // "populated index but empty store" check (via the now-empty dump,
81
82
  // `packIsComplete` turns false so recovery rebuilds) rather than serving an
82
83
  // empty graph as the pack.
83
- const actualTriples = await countTriples(store);
84
+ // `store.size` is a field oxigraph maintains. The equivalent query —
85
+ // `SELECT (COUNT(*)) WHERE { ?s ?p ?o }`, which is what `build.ts` still runs
86
+ // to WRITE `tripleCount` — is a scan, and it ran on every boot to read a
87
+ // number the store already knew.
88
+ //
89
+ // The two agree: ke queries with `use_default_graph_as_union`, and oxigraph
90
+ // does not deduplicate across that union, so a quad count and the union's
91
+ // solution count are the same number even for a triple repeated across
92
+ // graphs. Verified against 0.5.6 rather than assumed — and the guard is `<`,
93
+ // which tolerates the direction a set-semantic union would drift.
94
+ const actualQuads = store.size;
84
95
  if (manifest.tripleCount !== undefined) {
85
- if (actualTriples < manifest.tripleCount) {
96
+ if (actualQuads < manifest.tripleCount) {
86
97
  store.dispose();
87
98
  throw packUnavailable(
88
- `The pack at ${dir} has a corrupt data cache (expected at least ${manifest.tripleCount} triples, loaded ${actualTriples}).`,
99
+ `The pack at ${dir} has a corrupt data cache (expected at least ${manifest.tripleCount} triples, loaded ${actualQuads}).`,
89
100
  );
90
101
  }
91
- } else if (index.entities.length > 0 && actualTriples === 0) {
102
+ } else if (index.entities.length > 0 && actualQuads === 0) {
92
103
  store.dispose();
93
104
  throw packUnavailable(`The pack at ${dir} has a corrupt data cache.`);
94
105
  }
@@ -99,13 +110,6 @@ export async function readPack(dir: string): Promise<StoreSession> {
99
110
  createContext: compiled.createContext,
100
111
  prefixes: manifest.prefixes,
101
112
  index,
113
+ manifest,
102
114
  };
103
115
  }
104
-
105
- /** Count the booted store's triples (a cheap aggregate over the union graph). */
106
- async function countTriples(store: StoreSession["store"]): Promise<number> {
107
- const result = (await store.query(
108
- "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" as never,
109
- )) as import("@canonical/ke").SelectResult;
110
- return Number(result.bindings.at(0)?.n ?? 0);
111
- }
@@ -43,6 +43,8 @@ export async function loadStoreSession(
43
43
  }
44
44
 
45
45
  const dir =
46
- decision.kind === "embedded" ? materializeEmbeddedPack() : decision.dir;
46
+ decision.kind === "embedded"
47
+ ? await materializeEmbeddedPack()
48
+ : decision.dir;
47
49
  return readPack(dir);
48
50
  }
@@ -21,7 +21,7 @@ export async function runStoreProbe(): Promise<string> {
21
21
  import("./graphpack/embedded.js"),
22
22
  import("./graphpack/read.js"),
23
23
  ]);
24
- const session = await readPack(materializeEmbeddedPack());
24
+ const session = await readPack(await materializeEmbeddedPack());
25
25
  try {
26
26
  const result = await session.store.query(
27
27
  "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" as never,
@@ -82,8 +82,13 @@ type Term = import("@canonical/ke").Term;
82
82
  * Membership edges behave identically — this graph's `ds:tier` fans in 141 deep
83
83
  * — and a rule keyed on `rdf:type` would have left a `detailed` read of one tier
84
84
  * at 20.9 KB while congratulating itself on having fixed classes.
85
+ *
86
+ * EXPORTED because it has a second consumer: `graph connect` decides which
87
+ * edges it may WALK by the same rule this reader decides what to list. One
88
+ * constant with two consumers, never two constants that agree today — a copied
89
+ * `20` would let the traversal drift from the doctrine it claims to obey.
85
90
  */
86
- const ROSTER_THRESHOLD = 20;
91
+ export const ROSTER_THRESHOLD = 20;
87
92
 
88
93
  /** How many subjects each level lists for a genuine ANSWER (fan-in ≤ threshold). */
89
94
  const ANSWER_CAP: Readonly<Record<DetailLevel, number>> = {
@@ -254,16 +259,26 @@ export interface InspectResult {
254
259
  * word twice. A graph whose labels are real prose (a concept at `ds:concept.pd`
255
260
  * named "Progressive disclosure") still keeps them, so this thins noise without
256
261
  * deciding that labels are worthless.
262
+ *
263
+ * Exported beside {@link nameIndex}: the two are ONE rule for naming a
264
+ * neighbour, and a consumer that took the map without the filter would print
265
+ * exactly the noise this function exists to drop.
257
266
  */
258
- function isInformativeName(name: string, short: string): boolean {
267
+ export function isInformativeName(name: string, short: string): boolean {
259
268
  const normalize = (value: string): string =>
260
269
  value.replace(/[\s._:-]/g, "").toLowerCase();
261
270
  const local = short.split(/[:.]/).pop() ?? short;
262
271
  return normalize(name) !== normalize(local);
263
272
  }
264
273
 
265
- /** Index lookup for a neighbour's human name (`label`, else the first alt name). */
266
- function nameIndex(index: PackIndex): ReadonlyMap<string, string> {
274
+ /**
275
+ * Index lookup for a neighbour's human name (`label`, else the first alt name).
276
+ *
277
+ * EXPORTED for the same reason as {@link ROSTER_THRESHOLD}: `graph connect`
278
+ * names the nodes on a path, and it must name them the way this reader does or
279
+ * the same neighbour reads differently depending on which verb showed it.
280
+ */
281
+ export function nameIndex(index: PackIndex): ReadonlyMap<string, string> {
267
282
  const names = new Map<string, string>();
268
283
  for (const entity of index.entities) {
269
284
  if (!entity.uri) continue;
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { redactUrl } from "./parseRef.js";
2
+ import { parsePackDeclaration, redactUrl } from "./parseRef.js";
3
3
 
4
4
  describe("redactUrl", () => {
5
5
  it("strips user:password userinfo so an inlined token never reaches output", () => {
@@ -32,3 +32,51 @@ describe("redactUrl", () => {
32
32
  );
33
33
  });
34
34
  });
35
+
36
+ describe("parsePackDeclaration — a git source may name a subdirectory", () => {
37
+ const url = "git+https://github.com/org/repo.git";
38
+
39
+ it("reaches a package nested in a monorepo", () => {
40
+ expect(
41
+ parsePackDeclaration({
42
+ name: "@org/thing",
43
+ source: `${url}#main:pkgs/thing`,
44
+ }),
45
+ ).toEqual({
46
+ kind: "git",
47
+ pkg: "@org/thing",
48
+ url: "https://github.com/org/repo.git",
49
+ ref: "main",
50
+ subdir: "pkgs/thing",
51
+ source: `${url}#main:pkgs/thing`,
52
+ });
53
+ });
54
+
55
+ it("leaves a plain ref untouched, with no subdir key", () => {
56
+ const ref = parsePackDeclaration({
57
+ name: "@org/thing",
58
+ source: `${url}#main`,
59
+ });
60
+ expect(ref).toEqual({
61
+ kind: "git",
62
+ pkg: "@org/thing",
63
+ url: "https://github.com/org/repo.git",
64
+ ref: "main",
65
+ source: `${url}#main`,
66
+ });
67
+ expect("subdir" in ref).toBe(false);
68
+ });
69
+
70
+ it("splits on the FIRST colon, so a nested path survives", () => {
71
+ // The ref cannot contain a colon; the subdirectory can contain slashes.
72
+ expect(
73
+ parsePackDeclaration({ name: "@org/t", source: `${url}#v1.2:a/b/c` }),
74
+ ).toMatchObject({ ref: "v1.2", subdir: "a/b/c" });
75
+ });
76
+
77
+ it("rejects an empty subdirectory rather than silently ignoring it", () => {
78
+ expect(() =>
79
+ parsePackDeclaration({ name: "@org/thing", source: `${url}#main:` }),
80
+ ).toThrow(/subdirectory after : is empty/);
81
+ });
82
+ });
@@ -25,6 +25,8 @@ export type PackageRef =
25
25
  readonly pkg: string;
26
26
  readonly url: string;
27
27
  readonly ref: string;
28
+ /** Package root WITHIN the clone, for a package nested in a monorepo. */
29
+ readonly subdir?: string;
28
30
  readonly source: string;
29
31
  };
30
32
 
@@ -72,11 +74,33 @@ export function parsePackDeclaration(entry: PackDeclaration): PackageRef {
72
74
  },
73
75
  );
74
76
  }
77
+ // `#<ref>` or `#<ref>:<subdir>`. A git ref cannot contain a colon
78
+ // (git check-ref-format), so the split is unambiguous.
79
+ const fragment = source.slice(hashIdx + 1);
80
+ const colonIdx = fragment.indexOf(":");
81
+ if (colonIdx !== -1 && colonIdx === fragment.length - 1) {
82
+ throw PragmaError.configError(
83
+ `Invalid source for "${name}": the subdirectory after : is empty.`,
84
+ {
85
+ recovery: {
86
+ message:
87
+ "Example: git+https://github.com/org/repo.git#main:packages/thing",
88
+ },
89
+ },
90
+ );
91
+ }
92
+ const ref = colonIdx === -1 ? fragment : fragment.slice(0, colonIdx);
93
+ if (ref.length === 0) {
94
+ throw PragmaError.configError(
95
+ `Invalid source for "${name}": git URL must include a ref after #.`,
96
+ );
97
+ }
75
98
  return {
76
99
  kind: "git",
77
100
  pkg: name,
78
101
  url: source.slice("git+".length, hashIdx),
79
- ref: source.slice(hashIdx + 1),
102
+ ref,
103
+ ...(colonIdx === -1 ? {} : { subdir: fragment.slice(colonIdx + 1) }),
80
104
  source,
81
105
  };
82
106
  }
@@ -184,6 +184,34 @@ function readStorySources(
184
184
  */
185
185
  const PREFIX_DECL = /(?:^|\s)@?prefix\s+([^\s:]+):\s*<([^>]*)>/gi;
186
186
 
187
+ /**
188
+ * Blank out every Turtle string literal, so a prefix scan reads declarations
189
+ * and not prose.
190
+ *
191
+ * A long literal can legally contain the text `@prefix ex: <iri> .`, and one
192
+ * genuinely does: the code-standards pack ships guidance ABOUT Turtle, whose
193
+ * `cs:code` examples include a deliberately discouraged declaration. Scanning
194
+ * raw text saw it as real, which made `sources update` report a `ds:` clash on
195
+ * every run for a file that asserts no `ds:` triple at all — and worse, let a
196
+ * code sample win the last-wins race in {@link harvestPrefixes} and rebind a
197
+ * real prefix.
198
+ *
199
+ * Long forms are blanked before short ones, so the `"""` delimiters are not
200
+ * mistaken for an empty `""` followed by prose. Newlines inside a literal are
201
+ * preserved so a later `^`-anchored match still sees the true line structure.
202
+ *
203
+ * @param content - Turtle source text.
204
+ * @returns The same text with literal contents replaced by blanks.
205
+ */
206
+ function blankLiterals(content: string): string {
207
+ const keepNewlines = (text: string): string => text.replace(/[^\n]/g, " ");
208
+ return content
209
+ .replace(/"""[\s\S]*?"""/g, keepNewlines)
210
+ .replace(/'''[\s\S]*?'''/g, keepNewlines)
211
+ .replace(/"(?:[^"\\\n]|\\.)*"/g, keepNewlines)
212
+ .replace(/'(?:[^'\\\n]|\\.)*'/g, keepNewlines);
213
+ }
214
+
187
215
  /**
188
216
  * Harvest a package's own `@prefix` / `PREFIX` declarations from its TTL text.
189
217
  *
@@ -201,7 +229,7 @@ export function harvestPrefixes(
201
229
  ): Record<string, string> {
202
230
  const prefixes: Record<string, string> = {};
203
231
  for (const { content } of sources) {
204
- for (const match of content.matchAll(PREFIX_DECL)) {
232
+ for (const match of blankLiterals(content).matchAll(PREFIX_DECL)) {
205
233
  const [, label, iri] = match;
206
234
  if (label !== undefined && iri !== undefined) prefixes[label] = iri;
207
235
  }
@@ -234,7 +262,7 @@ export function detectPrefixClashes(
234
262
  ): PrefixClash[] {
235
263
  const byLabel = new Map<string, string[]>();
236
264
  for (const { content } of sources) {
237
- for (const match of content.matchAll(PREFIX_DECL)) {
265
+ for (const match of blankLiterals(content).matchAll(PREFIX_DECL)) {
238
266
  const [, label, iri] = match;
239
267
  if (label === undefined || iri === undefined) continue;
240
268
  const iris = byLabel.get(label) ?? [];
@@ -383,13 +411,19 @@ export async function resolvePackage(
383
411
  },
384
412
  );
385
413
  }
414
+ // The clone lives at `dir`; the PACKAGE may be nested within it. A git
415
+ // source names a repository, so a package inside a monorepo is reached
416
+ // by its subdirectory — without which `readTtlSources` would scan a
417
+ // repository root that has no `definitions/` or `data/` and contribute
418
+ // nothing at all.
419
+ const packageRoot = ref.subdir ? join(dir, ref.subdir) : dir;
386
420
  return {
387
421
  name: ref.pkg,
388
422
  source: ref.source,
389
423
  resolved,
390
- root: dir,
391
- sources: readTtlSources(dir, ref.pkg),
392
- stories: readStorySources(dir, ref.pkg),
424
+ root: packageRoot,
425
+ sources: readTtlSources(packageRoot, ref.pkg),
426
+ stories: readStorySources(packageRoot, ref.pkg),
393
427
  };
394
428
  }
395
429
  }
@@ -8,6 +8,7 @@
8
8
  * that are actually populated by the time it runs.
9
9
  */
10
10
 
11
+ import type { UndoOutcome } from "@canonical/task/node";
11
12
  import type { DetailLevel, OutputFormat } from "../../constants.js";
12
13
  import type { ConfigLayers } from "../config/types.js";
13
14
 
@@ -53,6 +54,19 @@ export interface StoreSession {
53
54
  readonly prefixes: Readonly<Record<string, string>>;
54
55
  /** The storeless entity index shipped with the pack. */
55
56
  readonly index: import("./graphpack/types.js").PackIndex;
57
+ /**
58
+ * The provenance of the pack this session was booted from — the parsed
59
+ * `manifest.json` the boot ALREADY read to get {@link prefixes}.
60
+ *
61
+ * Carried rather than re-read because an answer that depends on WHICH packs
62
+ * were searched has to be able to say so from the session it was answered
63
+ * from. `graph connect`'s most common answer is "these two are not
64
+ * connected", and a missing pack makes the honest and the dishonest form of
65
+ * that answer byte-identical — so the answer names `sourceRef`, and it must
66
+ * be the loaded session's, not one re-resolved from config a caller could
67
+ * have changed since.
68
+ */
69
+ readonly manifest: import("./graphpack/types.js").Manifest;
56
70
  }
57
71
 
58
72
  /**
@@ -221,4 +235,20 @@ export interface PragmaRuntime {
221
235
  * plan of its own, which is what keeps the raw-effect render the default.
222
236
  */
223
237
  planData?: unknown;
238
+ /**
239
+ * The post-undo projection seam. `--undo` is two-phase: the undo
240
+ * interpreter WALKS the verb's task with every effect mocked to collect the
241
+ * reversals, and only then executes what it collected — so nothing the task
242
+ * body says while it is interpreted describes what the reversals did (the
243
+ * body only ever runs under the mocked walk). What DOES describe them is
244
+ * the interpreter's own per-undo outcomes, correlated by the `undoKey` each
245
+ * reversal was declared with. A verb that reports per-target results sets
246
+ * this (the same way it sets `exec`/`planData`, before returning its Task);
247
+ * the projector's undo branch awaits it with those outcomes AFTER the
248
+ * collected undos executed and before the `Undid N step(s).` line, so the
249
+ * verb projects them onto its own rows through the same path its forward
250
+ * run uses. A throw here is rendered as the run's error (nonzero exit) —
251
+ * the honest outcome when a reversal did not take.
252
+ */
253
+ undoReport?: (outcomes: readonly UndoOutcome[]) => void | Promise<void>;
224
254
  }
@@ -556,7 +556,7 @@ const CONFIG_FIELD_DOCS: Record<keyof RawConfig, ConfigFieldDoc> = {
556
556
  tier: {
557
557
  type: "string (optional)",
558
558
  notes:
559
- "Accepted by the validator and SCOPES NOTHING: since the block list became declared content, no read filters by tier — the value is only reported by `config show` and `info`. Set it with `config set tier <path>` and clear it with `config unset tier`; `none`, `default` and `-` are refused as values, so no string doubles as a remove-marker.",
559
+ "The DEFAULT TIER SCOPE for every read of a tiered entity (blocks, modifier families, concepts): reads answer from this tier and its ancestors — `apps_lxd` reads `global`, `apps` and `apps_lxd`. Unset, they answer from the top-level tiers; set to `all`, from every tier. A per-call `--tier <name>` (the `tier` tool parameter) replaces it for that call, and every scoped answer states the scope it used. Name a tier the way `tier list` or a `Tier` column spells it (`apps_lxd`, `Apps/LXD` and `apps/lxd` all resolve); an unknown name is an `INVALID_INPUT` listing the tiers that exist. Set it with `config set tier <name>` and clear it with `config unset tier`; `none`, `default` and `-` are refused as values, so no string doubles as a remove-marker.",
560
560
  },
561
561
  channel: {
562
562
  type: "`normal` | `experimental` | `prerelease` (optional)",
@@ -84,8 +84,8 @@ describe("surface conformance (PROTECTED)", () => {
84
84
  );
85
85
  });
86
86
 
87
- it("freezes exactly 42 designed MCP tools including info, the config family, colophon", () => {
88
- expect(golden.mcpSurface.tools).toHaveLength(42);
87
+ it("freezes exactly 49 designed MCP tools including info, the config family, colophon", () => {
88
+ expect(golden.mcpSurface.tools).toHaveLength(49);
89
89
  expect(golden.mcpSurface.tools).toContain("info");
90
90
  expect(golden.mcpSurface.tools).toContain("config_show");
91
91
  expect(golden.mcpSurface.tools).toContain("config_get");
@@ -192,6 +192,23 @@ export interface Formatters<T> {
192
192
  readonly llm: (d: T) => string;
193
193
  readonly json: (d: T) => string;
194
194
  readonly notice?: (d: T) => string | undefined;
195
+ /**
196
+ * The MACHINE half of the notice seam: facts about the read that belong in
197
+ * the envelope's `meta` as DATA, merged there by both projectors under the
198
+ * keys this returns.
199
+ *
200
+ * `notice` is a sentence, and a sentence is the right shape for a human
201
+ * reading stderr and the wrong shape for an agent deciding what to do next.
202
+ * The standing case is the tier scope: `meta.scope` names the tiers a list
203
+ * answered from, so a caller that wants the rest can widen the read without
204
+ * parsing prose for tier names. Both machine surfaces read this ONE seam, the
205
+ * way they already read one `notice`, so CLI-JSON and MCP stay byte-equal.
206
+ *
207
+ * Plain and llm ignore it — they carry the same facts in their own register
208
+ * (the notice on stderr, the heading in the condensed form). Return
209
+ * `undefined` (or omit the member) when the read has nothing to add.
210
+ */
211
+ readonly meta?: (d: T) => Record<string, unknown> | undefined;
195
212
  }
196
213
 
197
214
  /** A usage example shown in verb help. */
@@ -48,7 +48,7 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
48
48
  id: "no-empty-hook-on-free-filter",
49
49
  area: "pack list / empty-state",
50
50
  description:
51
- "STILL TRUE OF EMPTINESS, NARROWED ON BAD ARGUMENTS. A zero-row pack list — narrowed by a declared filter, by an unmatched `--search`, or unconditionally empty — is a SUCCESS: `ok:true, data:[]` at exit 0 with a calm notice (stderr in plain mode, `meta.notice` on the machine surfaces). pragma v2 still never raises EMPTY_RESULTS for an empty list, and U5's removal of the enum-filter-only `buildListEmptyError` hook stands. What is no longer true is that an UNMATCHED free-filter value stays successful: a value-free filter now rejects a value its vocabulary does not admit with INVALID_INPUT and the admissible values as `validOptions`, the same courtesy a declared-`values` filter always extended (`packs/sparql/applyFilters.ts`). The vocabulary is the graph's, not the returned rows' — a filter declares where it lives (`PackFilter.vocabulary`), so a real value that happens to match no row (a category with zero standards, a `ds:ConceptType` no concept uses) is a calm empty list, and only a value the graph does not know is an error.",
51
+ "STILL TRUE OF EMPTINESS, NARROWED ON BAD ARGUMENTS. A zero-row pack list — narrowed by a declared filter, by an unmatched `--search`, or unconditionally empty — is a SUCCESS: `ok:true, data:[]` at exit 0 with a calm notice (stderr in plain mode, `meta.notice` on the machine surfaces). pragma v2 still never raises EMPTY_RESULTS for an empty list, and U5's removal of the enum-filter-only `buildListEmptyError` hook stands. What is no longer true is that an UNMATCHED free-filter value stays successful: a value-free filter now rejects a value its vocabulary does not admit with INVALID_INPUT and the admissible values as `validOptions`, the same courtesy a declared-`values` filter always extended (`packs/sparql/filterValues.ts`). The vocabulary is the graph's, not the returned rows' — a filter declares where it lives (`PackFilter.vocabulary`), so a real value that happens to match no row (a category with zero standards, a `ds:ConceptType` no concept uses) is a calm empty list, and only a value the graph does not know is an error.",
52
52
  },
53
53
  {
54
54
  id: "raw-iri-in-data",
@@ -84,13 +84,13 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
84
84
  id: "block-lookup-not-tier-scoped",
85
85
  area: "block",
86
86
  description:
87
- "CLOSED, by REVERSAL of the entry it used to be. This gap read: `block lookup` resolves by `ds:name` globally, a same-named block at two tiers cannot be disambiguated by plain name, address it by prefixed IRI instead. That was accepted while the answer was merely arbitrary; it stopped being acceptable once measured — 25 live block names are carried by 2–3 blocks, and EVERY one of them was answered with whichever IRI sorted first and no sign the others existed (`block lookup button` returned Launchpad's Button, because `apps_launchpad…` precedes `global…`). 'Address it by IRI' is not a recovery an agent can reach when nothing tells it there is a second block to address. Two things replace it, and neither is a change of arity — one name still answers with one entity, and a glob is still the declared multi-entity form. (1) The discard is no longer SILENT: the resolve is unlimited, the winner is answered with, and the IRIs it outranked ride the verb's `notice` — the same seam a zero-record read uses (stderr in plain mode, `meta.notice` on `--format json` and MCP), because an ambiguous hit is the same failure as an empty one, a result that misrepresents itself. `LookupOutput.ambiguous` carries them in `data` for the machine surfaces, and is ABSENT (not `[]`) when nothing was ambiguous, so every unambiguous read keeps the payload it had. Note the recovery is not reachable any other way: a glob expands over DISTINCT names, so `block lookup 'Butt*'` returned Launchpad's Button and ButtonLink and the global Button appeared nowhere. (2) The order is a declared, derived RANKING (`PackLookup.scopeWeight` in `pragma.conf.ts`, generated by `packs/sparql/buildLookupQuery.ts#rankingClause`): a tier's depth comes from the `/` count in its OWN `ds:name` (`\"Apps/Launchpad\"` → 2, never from the `_` in `ds:apps_launchpad`), and it MULTIPLIES the existing type weight rather than tiebreaking above it, so a whole component in a nested tier still outranks a global SUBcomponent (`TextInput`). Nothing is enumerated, so a tier added upstream is placed without an edit here; an asserted `ds:tierRank` would take precedence over the derived depth and retire the config declaration with no code change. WHAT REMAINS a gap: `lookup` is still not tier-SCOPED — there is no way to ask for one tier's answer only, the ranking merely decides which comes first — and since L-OPEN-9 nothing is tier/channel-FILTERED (the hand-written `block list` went with its `tierChain.ts`; see `block-list-unfiltered`). Two top-level tiers still cannot be ordered against each other by depth alone, which today leaves `Navigation` (`global` vs `apps`) and `InfoTable` (`global` vs `sites`) decided by the IRI fallback; naming a precedence among the depth-1 tiers is an ontology question, deliberately NOT invented here.",
87
+ "CLOSED, by REVERSAL of the entry it used to be. This gap read: `block lookup` resolves by `ds:name` globally, a same-named block at two tiers cannot be disambiguated by plain name, address it by prefixed IRI instead. That was accepted while the answer was merely arbitrary; it stopped being acceptable once measured — 25 live block names are carried by 2–3 blocks, and EVERY one of them was answered with whichever IRI sorted first and no sign the others existed (`block lookup button` returned Launchpad's Button, because `apps_launchpad…` precedes `global…`). 'Address it by IRI' is not a recovery an agent can reach when nothing tells it there is a second block to address. Two things replace it, and neither is a change of arity — one name still answers with one entity, and a glob is still the declared multi-entity form. (1) The discard is no longer SILENT: the resolve is unlimited, the winner is answered with, and the IRIs it outranked ride the verb's `notice` — the same seam a zero-record read uses (stderr in plain mode, `meta.notice` on `--format json` and MCP), because an ambiguous hit is the same failure as an empty one, a result that misrepresents itself. `LookupOutput.ambiguous` carries them in `data` for the machine surfaces, and is ABSENT (not `[]`) when nothing was ambiguous, so every unambiguous read keeps the payload it had. Note the recovery is not reachable any other way: a glob expands over DISTINCT names, so `block lookup 'Butt*'` returned Launchpad's Button and ButtonLink and the global Button appeared nowhere. (2) The order is a declared, derived RANKING (`PackLookup.scopeWeight` in `pragma.conf.ts`, generated by `packs/sparql/buildLookupQuery.ts#rankingClause`): a tier's depth comes from the `/` count in its OWN `ds:name` (`\"Apps/Launchpad\"` → 2, never from the `_` in `ds:apps_launchpad`), and it MULTIPLIES the existing type weight rather than tiebreaking above it, so a whole component in a nested tier still outranks a global SUBcomponent (`TextInput`). Nothing is enumerated, so a tier added upstream is placed without an edit here; an asserted `ds:tierRank` would take precedence over the derived depth and retire the config declaration with no code change. WHAT USED TO REMAIN, now closed: `lookup` was not tier-SCOPED — the ranking decided which answer came first, and there was no way to ask for one tier's answer only. The TIER SCOPE is that ask: `block lookup button` answers with the global Button alone under the default scope, `--tier apps_launchpad` answers with Launchpad's, `--tier all` answers with both (the ranking still orders whatever the scope admits, so the two rules compose rather than compete). A name whose every match is out of scope is still ANSWERED, from the tiers it does live in, with one line saying so — a scope decides which of several blocks a name means, never whether the block exists. `config.channel` is still not a filter. Two top-level tiers still cannot be ordered against each other by depth alone, which today leaves `Navigation` (`global` vs `apps`) and `InfoTable` (`global` vs `sites`) decided by the IRI fallback; naming a precedence among the depth-1 tiers is an ontology question, deliberately NOT invented here.",
88
88
  },
89
89
  {
90
90
  id: "block-list-unfiltered",
91
91
  area: "block",
92
92
  description:
93
- "L-OPEN-9 (owner-signed): `block list` is the block story's compiled, UNFILTERED list — it shows every block, including experimental/alpha ones, for everyone, until filtering returns in declared form. The old tier-chain inheritance, channel visibility, and `--all-tiers` escape were hand-written filtering the pack grammar has no term for, and the ruling removed them rather than growing the grammar to keep them. `config.tier` consequently scopes nothing (reported by `config show`/`info` only); `config.channel` keeps only its npm dist-tag role for `upgrade`/`info`.",
93
+ "HALF CLOSED, and the half that closed is the tier. This gap read: `block list` is the block story's compiled, UNFILTERED list — every block, experimental ones included, for everyone, until filtering returns in declared form; the old tier-chain inheritance, channel visibility and `--all-tiers` escape were hand-written filtering the pack grammar had no term for, and the ruling removed them rather than grow the grammar to keep them. Filtering has now returned for the TIER, and in declared form: the `block` story declares its tier hierarchy (`PackDefinition.tierScope`) and the kernel compiles a scope into the same query the filters ride, so the list answers from the top-level tiers by default, from a chosen tier plus its ancestors with `--tier <name>`, and from every tier with `--tier all` (which is this entry's old behaviour, on request). `config.tier` is what sets that default — no longer inert, and `config show` reports the scope it puts reads under. An UNTIERED block stays visible under every scope (A2 — `ds:button.icon`), because an entity the scope cannot place is not one it may hide. WHAT REMAINS: `config.channel` still scopes nothing and keeps only its npm dist-tag role for `upgrade`/`info`, so an experimental block is still visible to everyone in whatever tier it sits.",
94
94
  },
95
95
  {
96
96
  id: "graph-query-deferred",
@@ -120,7 +120,7 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
120
120
  id: "config-tier-no-ontology-validation",
121
121
  area: "config setters",
122
122
  description:
123
- "The covenant `config_tier` has no `needsStore`, so it is storeless — the old store-backed `validateTier` (which queried the ontology for valid tier paths) is DROPPED. `config tier <anything>` writes the value verbatim; an unknown tier simply yields empty scoped reads later. `config/fields.ts`.",
123
+ "`config set tier <anything>` is STORELESS and writes the value verbatim: the old store-backed `validateTier` (which queried the ontology for valid tier paths) is dropped, and the covenant's `config_set` declares no `needsStore` to bring it back. What changed with the tier scope is the CONSEQUENCE of a bad value — it used to yield nothing at all (the key scoped nothing), and it is now refused at the first tiered READ, with an INVALID_INPUT naming the tiers the pack does carry and a `config unset tier` recovery. Validation at the read rather than at the write is deliberate: the write has no store, and the pack a value is valid against can change under a machine that set it months ago. `config/fields.ts`, `kernel/packs/tierScope.ts`.",
124
124
  },
125
125
  {
126
126
  id: "doctor-exit-zero-with-failures",