@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,141 @@
1
+ /**
2
+ * Formatters for `graph connect` — the same answer in two registers.
3
+ *
4
+ * A person gets one ROW PER STEP grouped by path, with each node's human name
5
+ * from the pack index, because what a reader wants from "how do these two
6
+ * connect" is the chain, read left to right, with the direction of every edge
7
+ * visible.
8
+ *
9
+ * An agent gets ONE OBJECT. Not Markdown: the answer is structured data — two
10
+ * endpoints, a verdict, the paths, and the policy the walk ran under — and
11
+ * flattening it into prose would make a caller parse back out what it already
12
+ * had (Constitution XI). This is also why neither format ever explains WHY two
13
+ * things relate: the predicates on the path are the explanation, and a sentence
14
+ * synthesised from them would be pragma asserting a meaning the graph does not
15
+ * carry.
16
+ *
17
+ * Terms arrive already compacted and named by the relation index, against the
18
+ * store's own merged prefix map — so these formatters carry no prefix map and
19
+ * re-derive nothing.
20
+ */
21
+
22
+ import type { Formatters } from "../../kernel/spec/index.js";
23
+ import type {
24
+ ConnectPath,
25
+ ConnectRefusal,
26
+ ConnectResult,
27
+ ConnectTerm,
28
+ SharedRoster,
29
+ } from "./connect.types.js";
30
+
31
+ /** The short form of a term: prefixed when there is one, else the raw value. */
32
+ const short = (term: ConnectTerm): string => term.prefixed ?? term.value;
33
+
34
+ /** A term as a reader should see it — short form, with its name when known. */
35
+ const named = (term: ConnectTerm): string =>
36
+ term.title ? `${short(term)} (${term.title})` : short(term);
37
+
38
+ /** A blank node has no address, and saying so beats printing a dead handle. */
39
+ const node = (term: ConnectTerm): string =>
40
+ term.addressable === false ? `[record ${term.value}]` : named(term);
41
+
42
+ /** `1 hop` / `2 hops` — a count a reader does not have to forgive. */
43
+ const count = (n: number, noun: string): string =>
44
+ `${n} ${noun}${n === 1 ? "" : "s"}`;
45
+
46
+ /** The sentence for each way of being unconnected — one per case, never shared. */
47
+ const REFUSALS: Readonly<Record<ConnectRefusal, string>> = {
48
+ "endpoint-carries-no-relation":
49
+ "Not connectable: one endpoint carries no relation the model uses — it is only classified.",
50
+ "shares-only-rosters":
51
+ "Not connected in the model. The two share membership, which is not a relation.",
52
+ "no-path-within-hop-limit":
53
+ "Not connected: no relation path between them within the hop limit.",
54
+ };
55
+
56
+ /** One roster group, as a line a reader can act on. */
57
+ function rosterLine(roster: SharedRoster): string {
58
+ const shape =
59
+ roster.side === "subjects"
60
+ ? `${short(roster.predicate)} -> ${node(roster.node)}`
61
+ : `${node(roster.node)} -> ${short(roster.predicate)}`;
62
+ return ` ${shape} (${roster.members} members)`;
63
+ }
64
+
65
+ /** One path, as a header plus one row per step. */
66
+ function pathLines(path: ConnectPath, ordinal: number): string[] {
67
+ const lines = [` Path ${ordinal} (${count(path.steps.length, "hop")})`];
68
+ for (const step of path.steps) {
69
+ const arrow =
70
+ step.direction === "forward"
71
+ ? `--${short(step.predicate)}->`
72
+ : `<-${short(step.predicate)}--`;
73
+ lines.push(` ${node(step.from)} ${arrow} ${node(step.to)}`);
74
+ }
75
+ return lines;
76
+ }
77
+
78
+ /** The policy block every answer carries, positive or negative. */
79
+ function policyLines(data: ConnectResult): string[] {
80
+ return [
81
+ "",
82
+ ` Hop limit ${data.hopLimit}; edges whose fan-in exceeds ${data.rosterThreshold} are rosters, not relations.`,
83
+ ` Packs searched: ${data.packs.length > 0 ? data.packs.join(", ") : "none recorded"}`,
84
+ ];
85
+ }
86
+
87
+ export const connectFormatters: Formatters<ConnectResult> = {
88
+ plain(data) {
89
+ const title = `${node(data.a.term)} <-> ${node(data.b.term)}`;
90
+ const lines = [title, "=".repeat(Math.min(Math.max(title.length, 24), 96))];
91
+
92
+ if (data.connected) {
93
+ lines.push(
94
+ ` Connected in ${count(data.hops ?? 0, "relation hop")}, by ${count(data.pathCount, "shortest path")}.`,
95
+ "",
96
+ );
97
+ for (const [index, path] of data.paths.entries()) {
98
+ lines.push(...pathLines(path, index + 1));
99
+ lines.push("");
100
+ }
101
+ if (data.truncated) {
102
+ lines.push(
103
+ ` Showing ${data.paths.length} of ${data.pathCount} shortest paths — raise --paths for the rest.`,
104
+ );
105
+ }
106
+ } else {
107
+ lines.push(
108
+ ` ${REFUSALS[data.unconnectedBecause ?? "no-path-within-hop-limit"]}`,
109
+ );
110
+ for (const endpoint of [data.a, data.b]) {
111
+ if (endpoint.relatable) continue;
112
+ lines.push(
113
+ "",
114
+ ` ${node(endpoint.term)} carries no relation. It is classified by:`,
115
+ ...(endpoint.classifiedBy ?? []).map(rosterLine),
116
+ );
117
+ }
118
+ if (data.sharedRosters.length > 0) {
119
+ lines.push("", " Rosters they both belong to:");
120
+ lines.push(...data.sharedRosters.map(rosterLine));
121
+ lines.push(
122
+ "",
123
+ " A roster is answered by the noun's own list verb, not by a path.",
124
+ );
125
+ }
126
+ }
127
+
128
+ lines.push(...policyLines(data));
129
+ return lines.join("\n").trimEnd();
130
+ },
131
+
132
+ /**
133
+ * The agent-facing format IS the object — one JSON value on one line.
134
+ *
135
+ * `json` pretty-prints the same payload for a human reading a pipe; the two
136
+ * are the same data by construction, so an agent and a reader never see
137
+ * different answers to the same question.
138
+ */
139
+ llm: (data) => JSON.stringify(data),
140
+ json: (data) => JSON.stringify(data, null, 2),
141
+ };
@@ -0,0 +1,370 @@
1
+ /**
2
+ * `graph connect` against the pack this CLI actually ships (PROTECTED).
3
+ *
4
+ * Nothing here pins a number the graph produces, and nothing snapshots an
5
+ * answer. That is deliberate, and it is the whole design of this file. This
6
+ * graph is about to change: new channel symbols will become addressable and
7
+ * hundreds of binding records will be written, and every one of those moves a
8
+ * hop count and a path set. A test that pinned "button is exactly 2 hops from
9
+ * `color.text`" would go red on a correct change, and a snapshot of an answer's
10
+ * bytes would go red on a formatting fix. Both teach a maintainer to update the
11
+ * fixture rather than to think.
12
+ *
13
+ * So what is asserted here are INVARIANTS — properties that must hold whatever
14
+ * the data says:
15
+ *
16
+ * - the relation rule holds over the verb's OWN output: no returned step is an
17
+ * edge the rule would have refused;
18
+ * - every returned path is the same length, and that length is minimal;
19
+ * - the returned paths are distinct, and two runs agree on their order;
20
+ * - every step is an edge the store actually asserts, in the direction claimed,
21
+ * and the steps join end to end from one endpoint to the other;
22
+ * - the limit and threshold reported are the ones applied;
23
+ * - the packs reported are the packs the session loaded;
24
+ * - asking the question the other way round returns the same paths mirrored.
25
+ *
26
+ * Every check asks the STORE, never the verb's own index — an answer checked
27
+ * against the structure that produced it only proves the verb agrees with
28
+ * itself. The questions are targeted at the nodes on the answer rather than
29
+ * re-scanning the graph, which is both cheaper and a more direct question.
30
+ *
31
+ * Exact numbers live in `connect.test.ts`, over a fixture this repo authors.
32
+ */
33
+
34
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
35
+ import { verbKey } from "../../kernel/packs/uniqueness.js";
36
+ import { bootRuntime } from "../../kernel/runtime/boot.js";
37
+ import { ROSTER_THRESHOLD } from "../../kernel/runtime/readEntity.js";
38
+ import type {
39
+ PragmaRuntime,
40
+ StoreSession,
41
+ } from "../../kernel/runtime/types.js";
42
+ import type { VerbSpec } from "../../kernel/spec/types.js";
43
+ import { TEST_FLAGS } from "../../testing/helpers/projectCli.js";
44
+ import type { ConnectPath, ConnectResult } from "./connect.types.js";
45
+ import { graphModule } from "./index.js";
46
+
47
+ const connect = graphModule.verbs.find(
48
+ (v) => verbKey(v.path) === "graph connect",
49
+ ) as VerbSpec;
50
+
51
+ /**
52
+ * A pair that is connected on the shipped pack. Asserted to ANSWER, never to
53
+ * answer with a particular number — the block subscribes to a modifier family
54
+ * and that family rebinds the symbol, and if that route is ever rewritten the
55
+ * properties below still describe a correct answer.
56
+ */
57
+ const CONNECTED_PAIR = ["ds:global.component.button", "dt:color.text"] as const;
58
+
59
+ /** One store for the file — booting per case triples the cost for no coverage. */
60
+ let rt: PragmaRuntime;
61
+ let session: StoreSession;
62
+
63
+ beforeAll(async () => {
64
+ rt = bootRuntime(TEST_FLAGS);
65
+ session = await rt.store.get();
66
+ });
67
+ afterAll(() => {
68
+ session.store.dispose();
69
+ });
70
+
71
+ /** Ask one connect question through the verb, as a projector would. */
72
+ async function ask(
73
+ a: string,
74
+ b: string,
75
+ options: { hops?: number; paths?: number } = {},
76
+ ): Promise<ConnectResult> {
77
+ return (await connect.run({ a, b, ...options }, rt)) as ConnectResult;
78
+ }
79
+
80
+ /** One step of a path, as the answer reports it. */
81
+ type Step = ConnectPath["steps"][number];
82
+
83
+ /** The triple a step claims, as subject, predicate, object. */
84
+ function claimed(step: Step): {
85
+ subject: Step["from"];
86
+ predicate: string;
87
+ object: Step["to"];
88
+ } {
89
+ const forward = step.direction === "forward";
90
+ return {
91
+ subject: forward ? step.from : step.to,
92
+ predicate: step.predicate.value,
93
+ object: forward ? step.to : step.from,
94
+ };
95
+ }
96
+
97
+ /**
98
+ * Whether a term can be named in a query.
99
+ *
100
+ * A blank node cannot: its label is store-local and is not a legal IRI, which
101
+ * is the very fact that rules out a per-layer SPARQL frontier and makes the
102
+ * in-code walk necessary. A step with a blank node at one end is therefore
103
+ * checked from its other end, and one blank at BOTH ends cannot be checked from
104
+ * outside the index at all — recorded here rather than left as a silent pass.
105
+ */
106
+ const nameable = (term: { addressable?: false }): boolean =>
107
+ term.addressable !== false;
108
+
109
+ /** The number a `?n` counting query returns. */
110
+ async function count(query: string): Promise<number> {
111
+ const result = await rt.query.sparql(query);
112
+ if (result.type !== "select") return 0;
113
+ const value = result.termBindings[0]?.n?.value;
114
+ return value === undefined ? 0 : Number(value);
115
+ }
116
+
117
+ describe("graph connect over the shipped pack — invariants (PROTECTED)", () => {
118
+ it("returns no step the relation rule would have refused", async () => {
119
+ // The rule enforced against the verb's own output, from group sizes counted
120
+ // by the STORE rather than taken from the index that produced the paths.
121
+ const result = await ask(...CONNECTED_PAIR, { paths: 99 });
122
+ expect(result.connected).toBe(true);
123
+ let checked = 0;
124
+ for (const path of result.paths) {
125
+ for (const step of path.steps) {
126
+ const { subject, predicate, object } = claimed(step);
127
+ if (nameable(object)) {
128
+ const subjectsSharing = await count(
129
+ `SELECT (COUNT(*) AS ?n) WHERE { ?s <${predicate}> <${object.value}> }`,
130
+ );
131
+ expect(
132
+ subjectsSharing,
133
+ `(${predicate}, ${object.value}) is a roster of subjects, so this edge is not walkable`,
134
+ ).toBeLessThanOrEqual(ROSTER_THRESHOLD);
135
+ checked++;
136
+ }
137
+ if (nameable(subject)) {
138
+ const objectsSharing = await count(
139
+ `SELECT (COUNT(*) AS ?n) WHERE { <${subject.value}> <${predicate}> ?o . FILTER(!isLiteral(?o)) }`,
140
+ );
141
+ expect(
142
+ objectsSharing,
143
+ `(${subject.value}, ${predicate}) is a roster of objects, so this edge is not walkable`,
144
+ ).toBeLessThanOrEqual(ROSTER_THRESHOLD);
145
+ checked++;
146
+ }
147
+ }
148
+ }
149
+ expect(checked, "no group was actually checked").toBeGreaterThan(0);
150
+ });
151
+
152
+ it("walks only edges the store asserts, in the direction it asserts them", async () => {
153
+ const result = await ask(...CONNECTED_PAIR, { paths: 99 });
154
+ let checked = 0;
155
+ for (const path of result.paths) {
156
+ for (const step of path.steps) {
157
+ const { subject, predicate, object } = claimed(step);
158
+ // Asked from whichever end can be named, and the OTHER end's value is
159
+ // read back out of the store — so a blank node is verified too, by the
160
+ // label the store itself hands back.
161
+ if (nameable(subject)) {
162
+ const rows = await rt.query.sparql(
163
+ `SELECT ?o WHERE { <${subject.value}> <${predicate}> ?o }`,
164
+ );
165
+ const objects =
166
+ rows.type === "select"
167
+ ? rows.termBindings.map((binding) => binding.o?.value)
168
+ : [];
169
+ expect(
170
+ objects,
171
+ `the store asserts no <${subject.value}> <${predicate}> <${object.value}>`,
172
+ ).toContain(object.value);
173
+ checked++;
174
+ } else if (nameable(object)) {
175
+ const rows = await rt.query.sparql(
176
+ `SELECT ?s WHERE { ?s <${predicate}> <${object.value}> }`,
177
+ );
178
+ const subjects =
179
+ rows.type === "select"
180
+ ? rows.termBindings.map((binding) => binding.s?.value)
181
+ : [];
182
+ expect(subjects).toContain(subject.value);
183
+ checked++;
184
+ }
185
+ }
186
+ }
187
+ expect(checked, "no step was actually checked").toBeGreaterThan(0);
188
+ });
189
+
190
+ it("returns paths that actually join one endpoint to the other", async () => {
191
+ const result = await ask(...CONNECTED_PAIR, { paths: 99 });
192
+ for (const path of result.paths) {
193
+ expect(path.steps[0]?.from.value).toBe(result.a.term.value);
194
+ expect(path.steps.at(-1)?.to.value).toBe(result.b.term.value);
195
+ // Each step starts where the previous one ended: a list of edges that
196
+ // does not join up is not a path.
197
+ for (const [index, step] of path.steps.entries()) {
198
+ if (index === 0) continue;
199
+ expect(step.from.value).toBe(path.steps[index - 1]?.to.value);
200
+ }
201
+ }
202
+ });
203
+
204
+ it("returns SHORTEST paths only — all of one length, and that length minimal", async () => {
205
+ const result = await ask(...CONNECTED_PAIR, { paths: 99 });
206
+ const lengths = new Set(result.paths.map((path) => path.steps.length));
207
+ expect(lengths.size).toBe(1);
208
+ expect([...lengths][0]).toBe(result.hops);
209
+ // Minimal, proved against the verb itself: one hop short of the reported
210
+ // distance, the same question has no answer. Only askable when there IS a
211
+ // tighter limit — a one-hop answer is minimal by construction, and the
212
+ // limit's own floor is 1.
213
+ const hops = result.hops ?? 0;
214
+ expect(hops).toBeGreaterThan(0);
215
+ if (hops > 1) {
216
+ const tighter = await ask(...CONNECTED_PAIR, { hops: hops - 1 });
217
+ expect(tighter.connected).toBe(false);
218
+ }
219
+ });
220
+
221
+ it("returns distinct paths, in an order two runs agree on", async () => {
222
+ const once = await ask(...CONNECTED_PAIR, { paths: 99 });
223
+ const twice = await ask(...CONNECTED_PAIR, { paths: 99 });
224
+ const key = (path: ConnectPath): string =>
225
+ path.steps
226
+ .map(
227
+ (step) =>
228
+ `${step.direction}:${step.predicate.value}:${step.to.value}`,
229
+ )
230
+ .join("|");
231
+ const keys = once.paths.map(key);
232
+ expect(new Set(keys).size, "duplicate paths in one answer").toBe(
233
+ keys.length,
234
+ );
235
+ expect(twice.paths.map(key)).toEqual(keys);
236
+ // Deterministic BY THE STATED KEY: hop count, then fewest distinct
237
+ // predicates, then the path's canonical form.
238
+ const families = once.paths.map(
239
+ (path) => new Set(path.steps.map((step) => step.predicate.value)).size,
240
+ );
241
+ expect([...families]).toEqual([...families].sort((x, y) => x - y));
242
+ });
243
+
244
+ it("reports the hop limit and roster threshold it actually applied", async () => {
245
+ const raised = await ask(...CONNECTED_PAIR, { hops: 6 });
246
+ expect(raised.hopLimit).toBe(6);
247
+ expect(raised.rosterThreshold).toBe(ROSTER_THRESHOLD);
248
+ const tight = await ask(...CONNECTED_PAIR, { hops: 1 });
249
+ expect(tight.hopLimit).toBe(1);
250
+ // The limit is not decoration: a path longer than it is not returned.
251
+ for (const path of tight.paths) {
252
+ expect(path.steps.length).toBeLessThanOrEqual(1);
253
+ }
254
+ });
255
+
256
+ it("names the packs the session it answered from was built with", async () => {
257
+ const result = await ask(...CONNECTED_PAIR);
258
+ expect(result.packs).toEqual(
259
+ session.manifest.sourceRef
260
+ .split(",")
261
+ .map((ref) => ref.trim())
262
+ .filter((ref) => ref.length > 0),
263
+ );
264
+ expect(result.packs.length).toBeGreaterThan(0);
265
+ });
266
+
267
+ it("answers the same path set whichever endpoint is asked first", async () => {
268
+ const forwards = await ask(...CONNECTED_PAIR, { paths: 99 });
269
+ const backwards = await ask(CONNECTED_PAIR[1], CONNECTED_PAIR[0], {
270
+ paths: 99,
271
+ });
272
+ expect(backwards.hops).toBe(forwards.hops);
273
+ expect(backwards.pathCount).toBe(forwards.pathCount);
274
+ // Modulo direction: reverse the order of the steps, swap each step's ends,
275
+ // and flip the direction it claims.
276
+ const mirror = (result: ConnectResult): string[] =>
277
+ result.paths
278
+ .map((path) =>
279
+ [...path.steps]
280
+ .reverse()
281
+ .map(
282
+ (step) =>
283
+ `${step.direction === "forward" ? "reverse" : "forward"}:${step.predicate.value}:${step.from.value}`,
284
+ )
285
+ .join("|"),
286
+ )
287
+ .sort();
288
+ const plain = (result: ConnectResult): string[] =>
289
+ result.paths
290
+ .map((path) =>
291
+ path.steps
292
+ .map(
293
+ (step) =>
294
+ `${step.direction}:${step.predicate.value}:${step.to.value}`,
295
+ )
296
+ .join("|"),
297
+ )
298
+ .sort();
299
+ expect(mirror(backwards)).toEqual(plain(forwards));
300
+ });
301
+
302
+ it("answers a real pair in the shape the contract promises", async () => {
303
+ // A SHAPE assertion, not a value one: it parses, it carries at least one
304
+ // path, and every step names a predicate and a direction — without saying
305
+ // which predicate or which direction, because that is the graph's business
306
+ // and the graph is about to change.
307
+ const result = await ask(...CONNECTED_PAIR);
308
+ const round = JSON.parse(
309
+ connect.output.formatters.json?.(result) ?? "null",
310
+ ) as ConnectResult;
311
+ expect(round.connected).toBe(true);
312
+ expect(round.paths.length).toBeGreaterThan(0);
313
+ for (const path of round.paths) {
314
+ expect(path.steps.length).toBeGreaterThan(0);
315
+ for (const step of path.steps) {
316
+ expect(step.predicate.value).toMatch(/^\w+:/);
317
+ expect(["forward", "reverse"]).toContain(step.direction);
318
+ expect(step.from.value.length).toBeGreaterThan(0);
319
+ expect(step.to.value.length).toBeGreaterThan(0);
320
+ }
321
+ }
322
+ });
323
+
324
+ it("runs every documented example as WRITTEN, and each one answers", async () => {
325
+ // The promise an example makes to a stranger. It lives here rather than in
326
+ // `examples.exec.test.ts` because that file's cases are already the most
327
+ // contended in the package and this one needs the same shipped store this
328
+ // file has already booted — one store per concern, not two.
329
+ //
330
+ // That it ANSWERS is what is checked. An endpoint that is not in the graph
331
+ // THROWS, which is exactly the broken example worth catching. Whether a
332
+ // pair turns out to be connected is the graph's business and deliberately
333
+ // not pinned: new symbols and binding records will move that, and an
334
+ // example is not wrong for demonstrating a real answer that changed.
335
+ expect(connect.examples?.length).toBeGreaterThan(0);
336
+ const positionals = connect.params
337
+ .filter((param) => param.positional)
338
+ .map((param) => param.name);
339
+ expect(positionals).toHaveLength(2);
340
+
341
+ for (const [index, example] of (connect.examples ?? []).entries()) {
342
+ // Both endpoints, read from the example's own `cmd` rather than retyped:
343
+ // the tokens after the verb, up to the first flag.
344
+ const endpoints: string[] = [];
345
+ for (const token of example.cmd.trim().split(/\s+/).slice(3)) {
346
+ if (token.startsWith("--")) break;
347
+ endpoints.push(token);
348
+ }
349
+ expect(endpoints, `example ${index} names two endpoints`).toHaveLength(2);
350
+ const result = await ask(endpoints[0] as string, endpoints[1] as string);
351
+ // Coherent with itself, whichever answer it gave.
352
+ expect(result.connected).toBe(result.paths.length > 0);
353
+ expect(result.connected).toBe(result.hops !== null);
354
+ expect(result.connected).toBe(result.unconnectedBecause === undefined);
355
+ expect(result.packs.length).toBeGreaterThan(0);
356
+ }
357
+ });
358
+
359
+ it("builds its index once per session and reuses it", async () => {
360
+ const { relationIndex } = await import("./relationIndex.js");
361
+ const first = await relationIndex({ session, sparql: rt.query.sparql });
362
+ const second = await relationIndex({ session, sparql: rt.query.sparql });
363
+ // The SAME object, not an equal one: a second scan of a 33k-edge graph per
364
+ // question is the cost this memo exists to avoid.
365
+ expect(second).toBe(first);
366
+ expect(first.relationEdges).toBeGreaterThan(0);
367
+ expect(first.relationEdges).toBeLessThan(first.scannedEdges);
368
+ expect(first.relationNodes).toBeGreaterThan(0);
369
+ });
370
+ });