@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,372 @@
1
+ /**
2
+ * The relation graph: which edges `graph connect` may walk, and the rosters it
3
+ * may not.
4
+ *
5
+ * ## What a relation is
6
+ *
7
+ * The kernel's entity reader already draws this distinction, and defends it in
8
+ * its own source (`readEntity.ts:37-50`): a RELATION "fans in narrowly and
9
+ * every subject is part of the answer", a ROSTER "fans in without bound because
10
+ * it grows with the data rather than the model" and "is already answered by
11
+ * `pragma/instanceCount` and by the noun's list verb". Then the sentence that
12
+ * makes it reusable here: "The two are told apart by FAN-IN, not by predicate
13
+ * name, so the kernel names no vocabulary."
14
+ *
15
+ * This module applies that same rule to TRAVERSAL. An edge `(s, p, o)` is
16
+ * walkable only if neither of its groups is a roster:
17
+ *
18
+ * - at most {@link ROSTER_THRESHOLD} subjects share its `(p, o)` pair, and
19
+ * - at most that many objects share its `(s, p)` pair.
20
+ *
21
+ * The threshold is IMPORTED, never restated — one constant with two consumers,
22
+ * so the traversal cannot drift from the doctrine it claims to obey.
23
+ *
24
+ * ## Why not a list of predicates
25
+ *
26
+ * Because the rule is per edge, per node and per direction, and a list cannot
27
+ * be. On the pack this CLI ships, twelve predicates carry both kinds of edge:
28
+ * `ds:tier` fans in 218-deep for the global tier and 1-to-a-handful for a small
29
+ * one, so the same predicate is a roster at one node and a real relation at
30
+ * another. A list would also be an editorial judgement about five vocabularies
31
+ * frozen into this file, stale the day a pack ships a predicate nobody listed.
32
+ * Nothing in this module names a predicate, and nothing may start to: the rule
33
+ * has to hold for a graph whose rosters hang off terms this code never saw.
34
+ *
35
+ * Nor can the distinction be read off the ontology. It is not asserted there:
36
+ * 72 predicates are declared `owl:ObjectProperty` and every one of the eleven
37
+ * classifiers is among them. The fan-in rule is a measurement standing in for a
38
+ * statement the graph does not make — which is exactly why it is computed here,
39
+ * fresh, from whatever is loaded, rather than written down anywhere.
40
+ *
41
+ * ## What the scan covers, and what it costs
42
+ *
43
+ * ONE scan of every triple with a non-literal object. Both group tables are
44
+ * counted over that same edge set, because a literal object is not an edge and
45
+ * cannot be walked — counting attributes into a fan-in would refuse edges on
46
+ * the strength of triples no traversal could ever use. The counts must be taken
47
+ * over the whole edge set before any edge is admitted, so the two tests cannot
48
+ * be applied in sequence: dropping a hub edge first would shrink the group its
49
+ * sibling is measured against and quietly admit an edge the rule refuses.
50
+ *
51
+ * The index is built ONCE per store session and memoised on it, and nothing
52
+ * here is written into the pack (`C.05` forbids a pre-joined view artifact, and
53
+ * prescribes exactly this: a thin loader that joins at runtime, because "the
54
+ * join is milliseconds at this scale — code, not data"). It is reached only
55
+ * through the connect verb's dynamically-imported run body, behind
56
+ * `needsStore`, so a process that never asks a connect question never loads
57
+ * this module, let alone runs the scan.
58
+ */
59
+
60
+ import { compactUri } from "../../kernel/render/index.js";
61
+ import {
62
+ isInformativeName,
63
+ nameIndex,
64
+ ROSTER_THRESHOLD,
65
+ } from "../../kernel/runtime/readEntity.js";
66
+ import type { QueryFacade, StoreSession } from "../../kernel/runtime/types.js";
67
+ import type { ConnectTerm, SharedRoster } from "./connect.types.js";
68
+
69
+ /** Every triple whose object is a node rather than an attribute — one scan. */
70
+ const EDGE_SCAN = "SELECT ?s ?p ?o WHERE { ?s ?p ?o . FILTER(!isLiteral(?o)) }";
71
+
72
+ /**
73
+ * Field separator for composite map keys.
74
+ *
75
+ * A NUL, written as an ESCAPE so this file stays text: a literal NUL byte in
76
+ * the source makes git classify the module as binary, and a reviewer then gets
77
+ * no diff at all. It is the one character that cannot appear in an IRI or in a
78
+ * blank-node label, so a composite key is unambiguous.
79
+ */
80
+ const SEP = "\u0000";
81
+
82
+ /** One walkable edge, as seen from one of its two ends. */
83
+ export interface RelationEdge {
84
+ /** The node at the other end. */
85
+ readonly to: string;
86
+ readonly predicate: string;
87
+ /** True when the asserted triple is `(this node, predicate, to)`. */
88
+ readonly forward: boolean;
89
+ }
90
+
91
+ /** A roster group a node belongs to, decoded from its key. */
92
+ interface RosterGroup {
93
+ readonly side: "subjects" | "objects";
94
+ readonly predicate: string;
95
+ readonly node: string;
96
+ readonly members: number;
97
+ }
98
+
99
+ /** The relation graph of one store session. */
100
+ export interface RelationIndex {
101
+ /** Walkable edges by node, both directions. */
102
+ readonly adjacency: ReadonlyMap<string, readonly RelationEdge[]>;
103
+ /** Roster-group keys by node — the memberships an edge was refused for. */
104
+ readonly rosters: ReadonlyMap<string, readonly string[]>;
105
+ /** Every roster group, by key. */
106
+ readonly groups: ReadonlyMap<string, RosterGroup>;
107
+ /** Distinct triples the scan returned (edges, before the rule). */
108
+ readonly scannedEdges: number;
109
+ /** Edges the rule admitted. */
110
+ readonly relationEdges: number;
111
+ /** Nodes carrying at least one admitted edge. */
112
+ readonly relationNodes: number;
113
+ /** Milliseconds the scan took, and milliseconds the build took. */
114
+ readonly scanMs: number;
115
+ readonly buildMs: number;
116
+ /** Short form + human name for any node the index has seen. */
117
+ term(value: string, blank?: boolean): ConnectTerm;
118
+ }
119
+
120
+ /**
121
+ * The memo. Keyed on the session OBJECT, so it lives exactly as long as the
122
+ * session does: the lazy store hands out one immutable session per boot and
123
+ * `invalidate()` replaces it, which means a rebuilt store gets a rebuilt index
124
+ * with no invalidation hook of its own to forget to call. A WeakMap rather than
125
+ * a field on the session, because the index is this capability's concern and
126
+ * the kernel should not carry a structure only one verb reads.
127
+ */
128
+ const memo = new WeakMap<StoreSession, Promise<RelationIndex>>();
129
+
130
+ /** What the index build needs: the session, and a way to query it. */
131
+ export interface IndexContext {
132
+ readonly session: StoreSession;
133
+ readonly sparql: QueryFacade["sparql"];
134
+ }
135
+
136
+ /**
137
+ * The relation index for a session, built on first use and reused after.
138
+ *
139
+ * @param ctx - The booted session and its query facade.
140
+ * @returns The session's relation index.
141
+ * @note Impure — scans the store on the first call for a given session.
142
+ */
143
+ export function relationIndex(ctx: IndexContext): Promise<RelationIndex> {
144
+ const cached = memo.get(ctx.session);
145
+ if (cached) return cached;
146
+ // Memoise the PROMISE, not the result, so two concurrent questions on one
147
+ // session share a single scan instead of racing two.
148
+ const building = buildRelationIndex(ctx).catch((error: unknown) => {
149
+ memo.delete(ctx.session);
150
+ throw error;
151
+ });
152
+ memo.set(ctx.session, building);
153
+ return building;
154
+ }
155
+
156
+ /** One scanned triple, with its two ends flagged. */
157
+ interface ScannedEdge {
158
+ readonly s: string;
159
+ readonly p: string;
160
+ readonly o: string;
161
+ readonly sBlank: boolean;
162
+ readonly oBlank: boolean;
163
+ }
164
+
165
+ /** Read every edge out of the store, deduplicated. */
166
+ async function scanEdges(ctx: IndexContext): Promise<ScannedEdge[]> {
167
+ const result = await ctx.sparql(EDGE_SCAN);
168
+ if (result.type !== "select") return [];
169
+ const edges: ScannedEdge[] = [];
170
+ // Deduplicate by triple: ke queries with `use_default_graph_as_union` and
171
+ // oxigraph does not deduplicate across that union, so one triple asserted in
172
+ // two named graphs arrives twice. Left in, it would inflate every fan-in
173
+ // count it touches AND return the same path twice.
174
+ const seen = new Set<string>();
175
+ for (const binding of result.termBindings) {
176
+ const s = binding.s;
177
+ const p = binding.p;
178
+ const o = binding.o;
179
+ if (!s || !p || !o) continue;
180
+ const key = `${s.value}${SEP}${p.value}${SEP}${o.value}`;
181
+ if (seen.has(key)) continue;
182
+ seen.add(key);
183
+ edges.push({
184
+ s: s.value,
185
+ p: p.value,
186
+ o: o.value,
187
+ sBlank: s.termType === "BlankNode",
188
+ oBlank: o.termType === "BlankNode",
189
+ });
190
+ }
191
+ return edges;
192
+ }
193
+
194
+ /**
195
+ * Build the relation index: one scan, two group tables, then the rule.
196
+ *
197
+ * @param ctx - The booted session and its query facade.
198
+ * @returns The relation index, with the cost of producing it recorded.
199
+ * @note Impure — queries the store.
200
+ */
201
+ async function buildRelationIndex(ctx: IndexContext): Promise<RelationIndex> {
202
+ const scanStart = performance.now();
203
+ const edges = await scanEdges(ctx);
204
+ const scanMs = performance.now() - scanStart;
205
+
206
+ const buildStart = performance.now();
207
+ // Both tables over the SAME edge set, before anything is admitted.
208
+ const bySubjectSide = new Map<string, number>();
209
+ const byObjectSide = new Map<string, number>();
210
+ for (const edge of edges) {
211
+ const inbound = `${edge.p}${SEP}${edge.o}`;
212
+ const outbound = `${edge.s}${SEP}${edge.p}`;
213
+ bySubjectSide.set(inbound, (bySubjectSide.get(inbound) ?? 0) + 1);
214
+ byObjectSide.set(outbound, (byObjectSide.get(outbound) ?? 0) + 1);
215
+ }
216
+
217
+ const adjacency = new Map<string, RelationEdge[]>();
218
+ const rosters = new Map<string, string[]>();
219
+ const groups = new Map<string, RosterGroup>();
220
+ const blanks = new Set<string>();
221
+ let relationEdges = 0;
222
+
223
+ const link = (from: string, edge: RelationEdge): void => {
224
+ const list = adjacency.get(from);
225
+ if (list) list.push(edge);
226
+ else adjacency.set(from, [edge]);
227
+ };
228
+ const enrol = (node: string, key: string, group: RosterGroup): void => {
229
+ if (!groups.has(key)) groups.set(key, group);
230
+ const list = rosters.get(node);
231
+ if (list) {
232
+ if (!list.includes(key)) list.push(key);
233
+ } else rosters.set(node, [key]);
234
+ };
235
+
236
+ for (const edge of edges) {
237
+ if (edge.sBlank) blanks.add(edge.s);
238
+ if (edge.oBlank) blanks.add(edge.o);
239
+ const inboundKey = `${edge.p}${SEP}${edge.o}`;
240
+ const outboundKey = `${edge.s}${SEP}${edge.p}`;
241
+ const subjectsSharing = bySubjectSide.get(inboundKey) ?? 0;
242
+ const objectsSharing = byObjectSide.get(outboundKey) ?? 0;
243
+ const inboundRoster = subjectsSharing > ROSTER_THRESHOLD;
244
+ const outboundRoster = objectsSharing > ROSTER_THRESHOLD;
245
+
246
+ if (!inboundRoster && !outboundRoster) {
247
+ relationEdges++;
248
+ link(edge.s, { to: edge.o, predicate: edge.p, forward: true });
249
+ link(edge.o, { to: edge.s, predicate: edge.p, forward: false });
250
+ continue;
251
+ }
252
+ // A refused edge still says something true: it records the node's
253
+ // MEMBERSHIP of the group that refused it, which is the whole content of a
254
+ // negative answer about two things that share only a classification.
255
+ if (inboundRoster) {
256
+ enrol(edge.s, `subjects${SEP}${inboundKey}`, {
257
+ side: "subjects",
258
+ predicate: edge.p,
259
+ node: edge.o,
260
+ members: subjectsSharing,
261
+ });
262
+ }
263
+ if (outboundRoster) {
264
+ enrol(edge.o, `objects${SEP}${outboundKey}`, {
265
+ side: "objects",
266
+ predicate: edge.p,
267
+ node: edge.s,
268
+ members: objectsSharing,
269
+ });
270
+ }
271
+ }
272
+
273
+ const names = nameIndex(ctx.session.index);
274
+ const prefixes = ctx.session.prefixes;
275
+ const term = (value: string, blank?: boolean): ConnectTerm => {
276
+ if (blank ?? blanks.has(value)) {
277
+ return { value, addressable: false };
278
+ }
279
+ const prefixed = compactUri(value, prefixes);
280
+ const name = names.get(value);
281
+ return {
282
+ value,
283
+ // `compactUri` returns its input when no namespace matches, so an
284
+ // unmatched IRI carries no `prefixed` echoing `value`.
285
+ ...(prefixed === value ? {} : { prefixed }),
286
+ ...(name && isInformativeName(name, prefixed) ? { title: name } : {}),
287
+ };
288
+ };
289
+
290
+ return {
291
+ adjacency,
292
+ rosters,
293
+ groups,
294
+ scannedEdges: edges.length,
295
+ relationEdges,
296
+ relationNodes: adjacency.size,
297
+ scanMs,
298
+ buildMs: performance.now() - buildStart,
299
+ term,
300
+ };
301
+ }
302
+
303
+ /**
304
+ * The roster groups two nodes both belong to, ordered deterministically.
305
+ *
306
+ * Scoped to one hop in each direction, which is what makes it cost nothing —
307
+ * and what it cannot see: two blocks whose shared library is attributed to
308
+ * their implementation objects rather than to themselves share that roster one
309
+ * step removed, and this will not report it.
310
+ *
311
+ * @param index - The session's relation index.
312
+ * @param a - One node's IRI or blank label.
313
+ * @param b - The other node's IRI or blank label.
314
+ * @returns The shared roster groups, largest first then by predicate.
315
+ */
316
+ export function sharedRosters(
317
+ index: RelationIndex,
318
+ a: string,
319
+ b: string,
320
+ ): SharedRoster[] {
321
+ const mine = index.rosters.get(a) ?? [];
322
+ const theirs = new Set(index.rosters.get(b) ?? []);
323
+ const shared: SharedRoster[] = [];
324
+ for (const key of mine) {
325
+ if (!theirs.has(key)) continue;
326
+ const group = index.groups.get(key);
327
+ if (!group) continue;
328
+ shared.push({
329
+ side: group.side,
330
+ predicate: index.term(group.predicate),
331
+ node: index.term(group.node),
332
+ members: group.members,
333
+ });
334
+ }
335
+ return sortRosters(shared);
336
+ }
337
+
338
+ /**
339
+ * The roster groups one node belongs to — what classifies an entity that
340
+ * carries no relation at all.
341
+ *
342
+ * @param index - The session's relation index.
343
+ * @param node - The node's IRI or blank label.
344
+ * @returns The node's roster groups, largest first then by predicate.
345
+ */
346
+ export function rosterMemberships(
347
+ index: RelationIndex,
348
+ node: string,
349
+ ): SharedRoster[] {
350
+ const groups: SharedRoster[] = [];
351
+ for (const key of index.rosters.get(node) ?? []) {
352
+ const group = index.groups.get(key);
353
+ if (!group) continue;
354
+ groups.push({
355
+ side: group.side,
356
+ predicate: index.term(group.predicate),
357
+ node: index.term(group.node),
358
+ members: group.members,
359
+ });
360
+ }
361
+ return sortRosters(groups);
362
+ }
363
+
364
+ /** Order rosters by size then by name, so two runs agree. */
365
+ function sortRosters(groups: SharedRoster[]): SharedRoster[] {
366
+ return groups.sort(
367
+ (x, y) =>
368
+ y.members - x.members ||
369
+ x.predicate.value.localeCompare(y.predicate.value) ||
370
+ x.node.value.localeCompare(y.node.value),
371
+ );
372
+ }