@canonical/pragma-cli 0.35.0 → 0.37.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 (295) hide show
  1. package/README.md +2 -2
  2. package/bundled-skills/add-standard/SKILL.md +338 -0
  3. package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
  4. package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
  5. package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
  6. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
  7. package/bundled-skills/anatomy-author/SKILL.md +1226 -0
  8. package/bundled-skills/design-auditor/SKILL.md +455 -0
  9. package/bundled-skills/implement-component/SKILL.md +333 -0
  10. package/bundled-skills/specify-component/SKILL.md +331 -0
  11. package/bundled-skills/specify-pattern/SKILL.md +292 -0
  12. package/dist/package.json +24 -20
  13. package/dist/pragma.conf.js +266 -34
  14. package/dist/pragma.conf.js.map +1 -1
  15. package/dist/src/bin.js +1 -1
  16. package/dist/src/bin.js.map +1 -1
  17. package/dist/src/capabilities/capabilities/hints.js +1 -1
  18. package/dist/src/capabilities/capabilities/hints.js.map +1 -1
  19. package/dist/src/capabilities/colophon/colophon.render.js +1 -1
  20. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  21. package/dist/src/capabilities/create/create.verb.js +2 -1
  22. package/dist/src/capabilities/create/create.verb.js.map +1 -1
  23. package/dist/src/capabilities/create/createSurface.generated.js +23 -1
  24. package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
  25. package/dist/src/capabilities/create/mount.js +4 -3
  26. package/dist/src/capabilities/create/mount.js.map +1 -1
  27. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
  28. package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
  29. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  30. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  31. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  32. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  33. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  34. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  35. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  36. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
  37. package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
  38. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  39. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  40. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  41. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  42. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  43. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  44. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  45. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  46. package/dist/src/capabilities/graph/query.render.js +2 -2
  47. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  48. package/dist/src/capabilities/info/collectInfo.js +8 -2
  49. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  50. package/dist/src/capabilities/info/info.render.js +6 -2
  51. package/dist/src/capabilities/info/info.render.js.map +1 -1
  52. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  53. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  54. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  55. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  56. package/dist/src/capabilities/setup/mode.js +2 -2
  57. package/dist/src/capabilities/setup/operations/index.js +4 -4
  58. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  59. package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
  60. package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
  61. package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
  62. package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
  63. package/dist/src/capabilities/setup/operations/setupGenerator.js +156 -58
  64. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  65. package/dist/src/capabilities/setup/operations/setupLsp.js +136 -38
  66. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  67. package/dist/src/capabilities/setup/operations/setupMcp.js +101 -52
  68. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  69. package/dist/src/capabilities/setup/operations/setupSkills.js +329 -49
  70. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  71. package/dist/src/capabilities/setup/plan.js.map +1 -1
  72. package/dist/src/capabilities/setup/plan.render.js +144 -66
  73. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  74. package/dist/src/capabilities/setup/setup.render.js +1 -1
  75. package/dist/src/capabilities/setup/setup.verb.js +165 -63
  76. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  77. package/dist/src/capabilities/setup/targets.js +122 -51
  78. package/dist/src/capabilities/setup/targets.js.map +1 -1
  79. package/dist/src/capabilities/shared/index.js +7 -6
  80. package/dist/src/capabilities/shared/index.js.map +1 -1
  81. package/dist/src/capabilities/shared/packageManager.js +300 -54
  82. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  83. package/dist/src/capabilities/skill/discover.js +97 -11
  84. package/dist/src/capabilities/skill/discover.js.map +1 -1
  85. package/dist/src/capabilities/skill/render.js +2 -2
  86. package/dist/src/capabilities/skill/render.js.map +1 -1
  87. package/dist/src/capabilities/sources/installSkills.js +145 -3
  88. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  89. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  90. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  91. package/dist/src/capabilities/sources/update.verb.js +3 -3
  92. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  93. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  94. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  95. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  96. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  97. package/dist/src/constants.js +13 -2
  98. package/dist/src/constants.js.map +1 -1
  99. package/dist/src/kernel/index.js +11 -2
  100. package/dist/src/kernel/index.js.map +1 -1
  101. package/dist/src/kernel/interactivity.js +68 -0
  102. package/dist/src/kernel/interactivity.js.map +1 -0
  103. package/dist/src/kernel/packs/renderPack.js +3 -3
  104. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  105. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  106. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  107. package/dist/src/kernel/packs/runBodies.js +57 -4
  108. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  109. package/dist/src/kernel/packs/schema.js +54 -2
  110. package/dist/src/kernel/packs/schema.js.map +1 -1
  111. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  112. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  113. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +310 -43
  114. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  115. package/dist/src/kernel/packs/types.js.map +1 -1
  116. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  117. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  118. package/dist/src/kernel/project/cli/dispatch.js +46 -24
  119. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  120. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  121. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  122. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  123. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  124. package/dist/src/kernel/project/cli/rootHelp.js +42 -9
  125. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  126. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  127. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  128. package/dist/src/kernel/render/renderers.js +1 -1
  129. package/dist/src/kernel/render/style.js +29 -9
  130. package/dist/src/kernel/render/style.js.map +1 -1
  131. package/dist/src/kernel/render/vocabulary.js +174 -0
  132. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  133. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
  134. package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
  135. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
  136. package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
  137. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
  138. package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
  139. package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
  140. package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
  141. package/dist/src/kernel/runtime/graphpack/read.js +14 -9
  142. package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
  143. package/dist/src/kernel/runtime/loadSession.js +3 -1
  144. package/dist/src/kernel/runtime/loadSession.js.map +1 -1
  145. package/dist/src/kernel/runtime/probe.js +1 -1
  146. package/dist/src/kernel/runtime/probe.js.map +1 -1
  147. package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
  148. package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
  149. package/dist/src/kernel/runtime/refs/resolve.js +9 -3
  150. package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
  151. package/dist/src/kernel/spec/emitReference.js +1 -1
  152. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  153. package/package.json +24 -20
  154. package/pragma.conf.ts +272 -34
  155. package/src/bin.ts +8 -1
  156. package/src/capabilities/block.parity.test.ts +25 -11
  157. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  158. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  159. package/src/capabilities/capabilities/hints.ts +1 -1
  160. package/src/capabilities/colophon/colophon.render.ts +1 -2
  161. package/src/capabilities/colophon/colophon.test.ts +2 -2
  162. package/src/capabilities/config/collectConfigShow.test.ts +1 -0
  163. package/src/capabilities/create/byteEquality.test.ts +28 -1
  164. package/src/capabilities/create/create.test.ts +1 -0
  165. package/src/capabilities/create/create.verb.ts +2 -1
  166. package/src/capabilities/create/createGrammar.test.ts +5 -2
  167. package/src/capabilities/create/createSurface.generated.ts +23 -1
  168. package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
  169. package/src/capabilities/create/mount.ts +3 -3
  170. package/src/capabilities/distribution.parity.test.ts +4 -0
  171. package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
  172. package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
  173. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  174. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  175. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  176. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  177. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  178. package/src/capabilities/doctor/checks/index.ts +6 -6
  179. package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
  180. package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
  181. package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
  182. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  183. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  184. package/src/capabilities/doctor/doctor.render.ts +56 -80
  185. package/src/capabilities/doctor/doctor.test.ts +247 -35
  186. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  187. package/src/capabilities/doctor/runChecks.ts +14 -11
  188. package/src/capabilities/doctor/types.ts +15 -24
  189. package/src/capabilities/emptyRecovery.test.ts +10 -2
  190. package/src/capabilities/graph/query.render.ts +2 -2
  191. package/src/capabilities/info/collectInfo.ts +8 -1
  192. package/src/capabilities/info/info.render.ts +6 -2
  193. package/src/capabilities/info/info.test.ts +37 -1
  194. package/src/capabilities/info/types.ts +10 -2
  195. package/src/capabilities/lazy.test.ts +26 -2
  196. package/src/capabilities/listLookup.shipped.exec.test.ts +340 -0
  197. package/src/capabilities/prompt/prompt.render.ts +2 -2
  198. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  199. package/src/capabilities/resources/resources.test.ts +38 -2
  200. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  201. package/src/capabilities/setup/buildPlan.ts +45 -41
  202. package/src/capabilities/setup/index.ts +1 -1
  203. package/src/capabilities/setup/mode.ts +2 -2
  204. package/src/capabilities/setup/operations/index.ts +6 -1
  205. package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
  206. package/src/capabilities/setup/operations/setupConfig.ts +7 -2
  207. package/src/capabilities/setup/operations/setupGenerator.ts +227 -84
  208. package/src/capabilities/setup/operations/setupLsp.ts +203 -38
  209. package/src/capabilities/setup/operations/setupMcp.ts +139 -62
  210. package/src/capabilities/setup/operations/setupSkills.ts +399 -60
  211. package/src/capabilities/setup/plan.render.ts +171 -76
  212. package/src/capabilities/setup/plan.ts +17 -12
  213. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  214. package/src/capabilities/setup/setup.render.test.ts +50 -36
  215. package/src/capabilities/setup/setup.render.ts +1 -1
  216. package/src/capabilities/setup/setup.test.ts +1126 -50
  217. package/src/capabilities/setup/setup.verb.ts +174 -68
  218. package/src/capabilities/setup/targets.ts +138 -60
  219. package/src/capabilities/setup/types.ts +10 -18
  220. package/src/capabilities/shared/index.ts +18 -6
  221. package/src/capabilities/shared/packageManager.test.ts +421 -6
  222. package/src/capabilities/shared/packageManager.ts +394 -59
  223. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  224. package/src/capabilities/skill/discover.ts +100 -11
  225. package/src/capabilities/skill/render.ts +2 -2
  226. package/src/capabilities/skill/skill.test.ts +17 -4
  227. package/src/capabilities/sources/installSkills.ts +181 -3
  228. package/src/capabilities/sources/runUpdate.ts +78 -2
  229. package/src/capabilities/sources/sources.test.ts +228 -0
  230. package/src/capabilities/sources/update.verb.ts +3 -3
  231. package/src/capabilities/surface.test.ts +14 -6
  232. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  233. package/src/capabilities/upgrade/types.ts +10 -2
  234. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  235. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  236. package/src/constants.ts +14 -1
  237. package/src/identity.test.ts +18 -5
  238. package/src/kernel/completion/entitySource.test.ts +15 -2
  239. package/src/kernel/completion/safety.test.ts +16 -9
  240. package/src/kernel/config/defaults.test.ts +19 -3
  241. package/src/kernel/config/readConfig.test.ts +1 -1
  242. package/src/kernel/index.ts +11 -2
  243. package/src/kernel/interactivity.test.ts +76 -0
  244. package/src/kernel/interactivity.ts +69 -0
  245. package/src/kernel/packs/compile.test.ts +92 -0
  246. package/src/kernel/packs/renderPack.ts +3 -3
  247. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  248. package/src/kernel/packs/resolveEntity.ts +88 -24
  249. package/src/kernel/packs/runBodies.ts +84 -19
  250. package/src/kernel/packs/schema.ts +60 -4
  251. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  252. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  253. package/src/kernel/packs/sparql/buildLookupQuery.ts +327 -45
  254. package/src/kernel/packs/toolDescription.test.ts +35 -1
  255. package/src/kernel/packs/types.ts +125 -2
  256. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  257. package/src/kernel/project/cli/buildProgram.ts +8 -1
  258. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  259. package/src/kernel/project/cli/dispatch.ts +48 -25
  260. package/src/kernel/project/cli/globalFlags.ts +4 -1
  261. package/src/kernel/project/cli/helpFormat.ts +1 -2
  262. package/src/kernel/project/cli/rootHelp.ts +44 -8
  263. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  264. package/src/kernel/render/prefixes.test.ts +2 -0
  265. package/src/kernel/render/renderers.ts +1 -1
  266. package/src/kernel/render/style.ts +32 -9
  267. package/src/kernel/render/vocabulary.ts +276 -0
  268. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
  269. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
  270. package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
  271. package/src/kernel/runtime/graphpack/embedded.ts +14 -6
  272. package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
  273. package/src/kernel/runtime/graphpack/read.ts +14 -12
  274. package/src/kernel/runtime/loadSession.ts +3 -1
  275. package/src/kernel/runtime/probe.ts +1 -1
  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 +9 -3
  279. package/src/kernel/runtime/types.ts +17 -0
  280. package/src/kernel/spec/emitReference.ts +1 -1
  281. package/src/kernel/spec/types.ts +35 -6
  282. package/src/testing/PARITY_GAPS.ts +9 -3
  283. package/src/testing/behavioral/docExamples.test.ts +4 -2
  284. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  285. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  286. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  287. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  288. package/src/testing/eval/cases/readNouns.ts +245 -9
  289. package/src/testing/fixtures/blockGraph.ts +28 -13
  290. package/src/testing/fixtures/graph/canonical.ts +95 -12
  291. package/src/testing/helpers/runCli.test.ts +44 -0
  292. package/src/testing/helpers/runCli.ts +27 -3
  293. package/dist/src/capabilities/shared/bands.js +0 -16
  294. package/dist/src/capabilities/shared/bands.js.map +0 -1
  295. package/src/capabilities/shared/bands.ts +0 -16
@@ -5,15 +5,47 @@
5
5
  * and which properties to read; the query text is generated here so
6
6
  * user-supplied names are always escaped SPARQL string literals, and level-gated
7
7
  * fields below the active level are excluded from the projection (fetch-gating).
8
- * Matching on the `by` property is exact and case-insensitive.
8
+ * Matching on the name is exact and case-insensitive.
9
9
  *
10
- * Every name-addressed resolve is TOTALLY ORDERED before its `LIMIT 1`. A name
11
- * can recur across tiers, and an unordered `LIMIT 1` hands the tie to the
12
- * store's enumeration order — a different answer on a different machine, or
13
- * after a repack. `ORDER BY STR(?uri)` is a total, engine-independent order over
14
- * a variable that is always bound (`?uri` carries the `by` triple), so it needs
15
- * no unbound-value sentinel. It fixes WHICH answer is arbitrary, not WHETHER:
16
- * declaring a precedence between tiers is an ontology change, tracked separately.
10
+ * WHAT an entity's name IS lives in one place — {@link nameBinding} — and every
11
+ * form here uses it: the asserted `by` value, plus, for a lookup that DECLARES
12
+ * `nameFallback: "iri"`, the IRI-derived name its class constraint can vouch
13
+ * for. That single binding is what makes the population a `list` publishes and
14
+ * the population a `lookup` answers to the same population.
15
+ *
16
+ * A name-addressed resolve RANKS, and returns every row it ranked. A name can
17
+ * reach several entities (25 of the live design system's block names do), and
18
+ * the `LIMIT 1` that used to sit under the ordering discarded all but the first
19
+ * inside the STORE, where nothing downstream could see that it had happened —
20
+ * `block lookup button` answered with Launchpad's Button and gave no sign the
21
+ * global one existed, because `apps_launchpad…` sorts before `global…`.
22
+ *
23
+ * The limit is gone from the NAME forms so the resolver can see what it is
24
+ * choosing between. It still answers with one entity — the arity is the tool's
25
+ * contract — but the rows it did not take become the IRIs of its notice, which
26
+ * is the only address that reaches them. (An IRI-addressed form keeps its
27
+ * `LIMIT 1`: an IRI is one entity by construction, so there is nothing to rank
28
+ * and nothing set aside.)
29
+ *
30
+ * The losers are NOT free, and an earlier draft of this comment claimed they
31
+ * were. `lookupProjection`'s optionals sit in the same SELECT as the ranking, so
32
+ * every matching URI has its declared fields projected, and a multi-valued field
33
+ * multiplies rows per entity before `firstRowPerEntity` throws them away. On the
34
+ * live graph that is small — 25 shared names, two entities each — but it is a
35
+ * product, not a constant, and the honest shape is a two-phase resolve: rank the
36
+ * URIs in a minimal SELECT, then project fields for the ones actually answered
37
+ * with. That restructure is deliberately not done here: it is the same query the
38
+ * pending no-limit change would rewrite again, and doing it twice would land two
39
+ * migrations on one seam.
40
+ *
41
+ * The order is {@link rankingClause}'s `?score`, then `STR(?uri)`. The score
42
+ * multiplies the two things a pack may declare about which entity a bare name
43
+ * MEANS — `weights` (which KIND of thing) and `scopeWeight` (WHOSE) — and a
44
+ * story that declares neither falls through to `STR(?uri)` alone, a total,
45
+ * engine-independent order over a variable that is always bound (`?uri` carries
46
+ * the class constraint, or the `by` triple when the name is not derived), so it
47
+ * needs no unbound-value sentinel. That final key is what makes the ranking a
48
+ * TOTAL order rather than a better arbitrary one.
17
49
  */
18
50
 
19
51
  import { activeExpands, activeFields } from "../disclosure.js";
@@ -43,11 +75,15 @@ function buildTypeConstraint(lookup: PackLookup): string {
43
75
  * differ ONLY in how `?uri` is bound (a `FILTER` on the escaped name vs a `BIND`
44
76
  * of the resolved IRI), so both thread the active level through here and gate
45
77
  * identically — an IRI-addressed lookup honours `--detail` just like a name one.
78
+ *
79
+ * `constraint` and `optionals` are handed back SEPARATELY because the `?name`
80
+ * binding has to sit between them: it reads `?uri`, which the class constraint
81
+ * binds, and a SPARQL `BIND` may only name variables already in scope.
46
82
  */
47
83
  function lookupProjection(
48
84
  lookup: PackLookup,
49
85
  level: string | undefined,
50
- ): { header: string; body: string } {
86
+ ): { header: string; constraint: string; optionals: string } {
51
87
  const fields = activeFields(lookup, level);
52
88
  const vars = fields.map((field) => `?${field.name}`).join(" ");
53
89
  const optionals = fields
@@ -58,56 +94,290 @@ function lookupProjection(
58
94
  .join("\n");
59
95
  return {
60
96
  header: `SELECT ?uri ?name${vars.length > 0 ? ` ${vars}` : ""} WHERE {`,
61
- body: buildTypeConstraint(lookup) + optionals,
97
+ constraint: buildTypeConstraint(lookup).trimEnd(),
98
+ optionals,
62
99
  };
63
100
  }
64
101
 
65
102
  /**
66
- * Build the SELECT retrieving one named entity with its declared fields.
103
+ * The IRI-derived display name, in ONE spelling every relaxed form shares.
104
+ *
105
+ * The local name is everything after the last `#` or `/`, and its dot-separated
106
+ * hierarchy segments are published with slashes — `cs:react.component.props` →
107
+ * `react/component/props`. That is not an invention here: it is the derivation
108
+ * the `standard` list story already performs to PUBLISH a row's name, so this is
109
+ * the same rule read from the other side. Written once, so the name a list hands
110
+ * out and the name a lookup answers to cannot drift apart.
111
+ */
112
+ const DERIVED_NAME = 'REPLACE(REPLACE(STR(?uri), "^.*[#/]", ""), "\\\\.", "/")';
113
+
114
+ /**
115
+ * The `?name` binding shared by every lookup form.
116
+ *
117
+ * By DEFAULT the `by` triple is required, and that is the honest default: `by`
118
+ * is documented as "the property whose value names the entity", so an entity
119
+ * without one has no name, is not addressable, and is not drawn by `sample`.
120
+ * It is also the one thing standing between a typo'd IRI and an empty entity.
121
+ *
122
+ * A story whose `list` SYNTHESIZES a name instead of reading one declares
123
+ * `nameFallback: "iri"`, and then the triple becomes OPTIONAL with
124
+ * {@link DERIVED_NAME} behind it. `standard` is the case that exists. When the
125
+ * defect was measured, 22 of the then-156 live code standards carried a
126
+ * `cs:name`, and the code-standards ontology said so deliberately — "an
127
+ * optional human-readable display title" that "never participates in
128
+ * identity". `standard list` published `react/component/tsdoc` while
129
+ * `standard lookup react/component/tsdoc` answered ENTITY_NOT_FOUND with
130
+ * empty suggestions, because the only addressable population was the ~13%
131
+ * carrying an asserted name. (Upstream has since retired `cs:name` for
132
+ * `rdfs:label`, which every standard carries; the fallback stays because a
133
+ * pack is data, and a dropped label must degrade to the derived name rather
134
+ * than become unaddressable.)
135
+ *
136
+ * INFERRING the fallback from the mere PRESENCE of a class constraint is what
137
+ * this option replaced, and it was over-reach in both directions of the same
138
+ * defect: `token list` requires `ds:tokenId`, so an inferred fallback made a
139
+ * `ds:Token` without one addressable and sampleable under a name `token list`
140
+ * never publishes. The declaration is per story because the list/lookup
141
+ * agreement is per story.
142
+ *
143
+ * The class constraint is still required for the fallback (the schema rejects
144
+ * the pairing, and this guard keeps a statically-compiled story from generating
145
+ * a query with nothing bounding `?uri`): a derived name is only as trustworthy
146
+ * as the class vouching for the entity it came from.
147
+ *
148
+ * One binding then serves the name resolve, the miss-suggestion pool, glob
149
+ * expansion and `sample`'s draw pool alike — every form that decides WHICH
150
+ * entities a name can reach. (The IRI-addressed forms reach an entity without
151
+ * going through a name at all: see {@link iriNameBinding}.)
152
+ */
153
+ function nameBinding(lookup: PackLookup): string {
154
+ if (!derivesNames(lookup)) return ` ?uri ${formatTerm(lookup.by)} ?name .`;
155
+ return [
156
+ ` OPTIONAL { ?uri ${formatTerm(lookup.by)} ?byName . }`,
157
+ ` BIND(COALESCE(?byName, ${DERIVED_NAME}) AS ?name)`,
158
+ ].join("\n");
159
+ }
160
+
161
+ /**
162
+ * Whether this lookup names an entity that carries no `by` value.
163
+ *
164
+ * The class constraint is part of the condition, not merely a schema rule the
165
+ * builders trust: without one there is no triple bounding `?uri`, so a derived
166
+ * name would be scanned over the whole graph. The schema rejects the pairing
167
+ * for declared packs; this keeps a statically-compiled story from generating
168
+ * that query.
169
+ */
170
+ function derivesNames(lookup: PackLookup): boolean {
171
+ return (
172
+ lookup.nameFallback === "iri" &&
173
+ Boolean(lookup.type ?? lookup.types?.length)
174
+ );
175
+ }
176
+
177
+ /**
178
+ * The `?name` binding for an IRI-ADDRESSED form.
179
+ *
180
+ * An IRI names the entity by itself, so here the `by` value is a LABEL to
181
+ * project, not the thing that identifies it — and the class constraint is
182
+ * already a sufficient existence check. So the triple is OPTIONAL wherever a
183
+ * class vouches for the entity, and `?name` is simply left unbound when there is
184
+ * no label; it is REQUIRED only for a lookup constraining no class, where it is
185
+ * the one thing standing between a typo'd IRI and an empty entity.
186
+ *
187
+ * A story that {@link derivesNames} gets the same COALESCE the name forms use,
188
+ * so an entity reached by IRI reports the name `list` published for it rather
189
+ * than a blank.
190
+ */
191
+ function iriNameBinding(lookup: PackLookup): string {
192
+ if (derivesNames(lookup)) return nameBinding(lookup);
193
+ const named = `?uri ${formatTerm(lookup.by)} ?name .`;
194
+ const constrained = Boolean(lookup.type ?? lookup.types?.length);
195
+ return constrained ? ` OPTIONAL { ${named} }` : ` ${named}`;
196
+ }
197
+
198
+ /**
199
+ * Render a declared weight as a SPARQL numeric literal (never exponential).
200
+ *
201
+ * `toFixed` is the wrong instrument twice over, and both bite inside the 0–1
202
+ * range the schema allows. It ROUNDS — `0.1234567` became `0.123457`, silently
203
+ * altering a ranking a pack author declared — and stripping the trailing zeros
204
+ * off its output turns `0.0000001` into `0.`, which is not a numeric literal at
205
+ * all and makes the query a parse error rather than a wrong answer.
206
+ *
207
+ * `String` already prints the shortest exact round-trip, so it is the right
208
+ * source of digits; the only thing it does that SPARQL cannot read is
209
+ * exponent notation, which xsd:decimal's grammar has no form for. Expand that
210
+ * case and pass everything else through untouched.
211
+ */
212
+ function sparqlNumber(value: number): string {
213
+ if (Number.isInteger(value)) return String(value);
214
+ const text = String(value);
215
+ const exponent = /e-(\d+)$/i.exec(text);
216
+ if (exponent === null) return text;
217
+ // `1e-7` / `1.5e-7`: the decimal places needed are the exponent plus whatever
218
+ // the mantissa already carried, which is exact — no rounding can occur.
219
+ const mantissaPlaces = text.split("e")[0]?.split(".")[1]?.length ?? 0;
220
+ return value.toFixed(Number(exponent[1]) + mantissaPlaces);
221
+ }
222
+
223
+ /**
224
+ * `left * right`, guarded where an operand may be ZERO.
225
+ *
226
+ * Not defensiveness — a MEASURED engine defect, and the top scope hits it every
227
+ * time. oxigraph raises on an xsd:decimal times a zero (`0.2 * 0` errors, while
228
+ * `0.2 * 1` is `0.2`), and a raising expression inside a `BIND` leaves its
229
+ * variable unbound. Written the arithmetic way, the depth-0 scope — the one that
230
+ * is supposed to WIN — is the only one whose weight never binds, and what
231
+ * happens next is decided by whatever the surrounding `COALESCE` falls back to.
232
+ * The same expression on the `block list` side fell back to 0 and sorted every
233
+ * top-tier row LAST, which is how this was found. Both sides now say what they
234
+ * mean. Only VARIABLE operands are tested; a declared literal is resolved at
235
+ * build time by the callers.
236
+ */
237
+ function sparqlProduct(left: string, right: string): string {
238
+ const guards = [left, right]
239
+ .filter((operand) => operand.startsWith("?"))
240
+ .map((operand) => `${operand} = 0`);
241
+ const product = `${left} * ${right}`;
242
+ return guards.length === 0
243
+ ? product
244
+ : `IF(${guards.join(" || ")}, 0, ${product})`;
245
+ }
246
+
247
+ /**
248
+ * The type factor: the LOWEST weight declared for any class the entity belongs
249
+ * to, 1 when none is.
250
+ *
251
+ * An IF-ladder over `EXISTS` rather than a weight column on the class VALUES,
252
+ * because a join would multiply rows for an entity in two weighted classes and
253
+ * the resolve now returns every row it finds. Lowest weight tested FIRST, so the
254
+ * ladder means MIN — the same rule the MCP listing's `effectiveWeight` states in
255
+ * prose: a demotion any membership asks for is not cancelled by a membership
256
+ * that asked for nothing.
257
+ */
258
+ function typeWeightBind(lookup: PackLookup): string {
259
+ const declared = Object.entries(lookup.weights ?? {});
260
+ if (declared.length === 0) return "";
261
+ const ladder = [...declared]
262
+ .sort(([aType, aWeight], [bType, bWeight]) =>
263
+ aWeight === bWeight ? aType.localeCompare(bType) : aWeight - bWeight,
264
+ )
265
+ .reduceRight(
266
+ (fallback, [type, weight]) =>
267
+ `IF(EXISTS { ?uri a ${formatTerm(type)} }, ${sparqlNumber(weight)}, ${fallback})`,
268
+ "1",
269
+ );
270
+ return ` BIND(${ladder} AS ?rankType)`;
271
+ }
272
+
273
+ /**
274
+ * The scope factor: how much the entity's containing scope is worth, DERIVED
275
+ * from the depth of that scope's own path-shaped name.
276
+ *
277
+ * Nested OPTIONALs, not a flat sequence: `?rankScope` is unbound for an entity
278
+ * outside any scope, and a following top-level `OPTIONAL { ?rankScope <by> ?n }`
279
+ * would then match every such triple in the graph and multiply the row.
280
+ *
281
+ * The depth is the separator count, so nothing is enumerated and a scope the
282
+ * ontology adds tomorrow inherits its place. `COALESCE` states the retirement
283
+ * path in the query itself: an ASSERTED weight wins, the derived one answers
284
+ * until there is one, and an entity with no scope at all scores a neutral 1
285
+ * rather than dropping below entities that have one.
286
+ */
287
+ function scopeWeightBinds(lookup: PackLookup): string {
288
+ const scope = lookup.scopeWeight;
289
+ if (!scope) return "";
290
+ const name = "?rankScopeName";
291
+ const asserted = scope.asserted
292
+ ? [
293
+ ` OPTIONAL { ?rankScope ${formatTerm(scope.asserted)} ?rankAsserted . }`,
294
+ ]
295
+ : [];
296
+ // A falloff of 0 ranks every scope alike, so the derived weight is the
297
+ // constant — and the depth it would multiply is never computed.
298
+ const derived =
299
+ scope.falloff === 0
300
+ ? "1"
301
+ : `1 - ${sparqlProduct(sparqlNumber(scope.falloff), "?rankDepth")}`;
302
+ return [
303
+ " OPTIONAL {",
304
+ ` ?uri ${formatTerm(scope.via)} ?rankScope .`,
305
+ ` OPTIONAL { ?rankScope ${formatTerm(scope.by)} ${name} . }`,
306
+ ...asserted,
307
+ " }",
308
+ ` BIND(STRLEN(${name}) - STRLEN(REPLACE(${name}, "/", "")) AS ?rankDepth)`,
309
+ ` BIND(${derived} AS ?rankDerived)`,
310
+ // Floored at 0: a scope deep enough to drive the factor negative would
311
+ // otherwise INVERT the type factor it multiplies, promoting a part of a
312
+ // block above a whole one.
313
+ ` BIND(COALESCE(${scope.asserted ? "?rankAsserted, " : ""}IF(?rankDerived < 0, 0, ?rankDerived), 1) AS ?rankScopeWeight)`,
314
+ ].join("\n");
315
+ }
316
+
317
+ /**
318
+ * The `?score` a name resolve orders by, and the ORDER BY that reads it.
319
+ *
320
+ * The two factors MULTIPLY rather than tiebreak, and that is the editorial
321
+ * ruling, not an implementation convenience: neither question subsumes the
322
+ * other. A whole component in a nested scope (1 × 0.8) outranks a mere PART of a
323
+ * block in the widest one (0.6 × 1), while two components of equal kind are
324
+ * separated by their scopes alone (1 × 1 beats 1 × 0.8). Both live cases fall
325
+ * out of one product.
326
+ *
327
+ * @returns The BIND lines to splice into the WHERE clause, and the ORDER BY
328
+ * line — a story declaring neither factor gets no BINDs and `STR(?uri)` alone.
329
+ */
330
+ function rankingClause(lookup: PackLookup): {
331
+ binds: string;
332
+ orderBy: string;
333
+ } {
334
+ const factors = [typeWeightBind(lookup), scopeWeightBinds(lookup)].filter(
335
+ (line) => line !== "",
336
+ );
337
+ if (factors.length === 0) return { binds: "", orderBy: "ORDER BY STR(?uri)" };
338
+ const terms = [
339
+ lookup.weights && Object.keys(lookup.weights).length > 0 ? "?rankType" : "",
340
+ lookup.scopeWeight ? "?rankScopeWeight" : "",
341
+ ].filter((term) => term !== "");
342
+ return {
343
+ binds: [
344
+ ...factors,
345
+ ` BIND(${terms.length === 2 ? sparqlProduct(terms[0] as string, terms[1] as string) : terms.join("")} AS ?score)`,
346
+ ].join("\n"),
347
+ orderBy: "ORDER BY DESC(?score) STR(?uri)",
348
+ };
349
+ }
350
+
351
+ /**
352
+ * Build the SELECT retrieving the entities a name reaches, with their declared
353
+ * fields, best first. The caller answers with the first and names the rest.
67
354
  *
68
355
  * @param lookup - The pack's lookup declaration.
69
356
  * @param name - User-supplied entity name (escaped here).
70
357
  * @param level - Active canonical level; gated fields below it are excluded.
71
- * @returns SPARQL SELECT text.
358
+ * @returns SPARQL SELECT text, ranked and unlimited.
72
359
  */
73
360
  export function buildLookupQuery(
74
361
  lookup: PackLookup,
75
362
  name: string,
76
363
  level?: string,
77
364
  ): string {
78
- const { header, body } = lookupProjection(lookup, level);
365
+ const { header, constraint, optionals } = lookupProjection(lookup, level);
366
+ const ranking = rankingClause(lookup);
79
367
  return [
80
368
  header,
81
- ` ?uri ${formatTerm(lookup.by)} ?name .`,
82
- body,
369
+ constraint,
370
+ nameBinding(lookup),
371
+ ranking.binds,
372
+ optionals,
83
373
  ` FILTER (LCASE(STR(?name)) = LCASE("${escapeSparqlString(name)}"))`,
84
374
  "}",
85
- "ORDER BY STR(?uri)",
86
- "LIMIT 1",
375
+ ranking.orderBy,
87
376
  ]
88
377
  .filter((line) => line !== "")
89
378
  .join("\n");
90
379
  }
91
380
 
92
- /**
93
- * The `?name` clause for an IRI-addressed form.
94
- *
95
- * An IRI names the entity by itself, so the `by` value is a LABEL to project,
96
- * not the thing that identifies it — and a class constraint is already a
97
- * sufficient existence check. Requiring the `by` triple here made every entity
98
- * that carries no name unaddressable by any means at all: 131 of the 144 live
99
- * code standards have no `cs:name` (their displayed name is synthesized from the
100
- * IRI by the list story), and shell completion offers their IRIs. So the triple
101
- * is OPTIONAL wherever a class constraint can vouch for the entity, and
102
- * REQUIRED only for a lookup that declares no `type`/`types` — where it is the
103
- * one thing standing between a typo'd IRI and an empty entity.
104
- */
105
- function iriNameClause(lookup: PackLookup): string {
106
- const named = `?uri ${formatTerm(lookup.by)} ?name .`;
107
- const constrained = Boolean(lookup.type ?? lookup.types?.length);
108
- return constrained ? ` OPTIONAL { ${named} }` : ` ${named}`;
109
- }
110
-
111
381
  /**
112
382
  * Build the SELECT retrieving one entity addressed directly by IRI.
113
383
  *
@@ -126,12 +396,13 @@ export function buildLookupByIriQuery(
126
396
  iri: string,
127
397
  level?: string,
128
398
  ): string {
129
- const { header, body } = lookupProjection(lookup, level);
399
+ const { header, constraint, optionals } = lookupProjection(lookup, level);
130
400
  return [
131
401
  header,
132
402
  ` BIND(<${iri}> AS ?uri)`,
133
- iriNameClause(lookup),
134
- body,
403
+ constraint,
404
+ iriNameBinding(lookup),
405
+ optionals,
135
406
  "}",
136
407
  "LIMIT 1",
137
408
  ]
@@ -142,21 +413,28 @@ export function buildLookupByIriQuery(
142
413
  /**
143
414
  * Build the minimal name→URI resolve for a graphql-sourced lookup (and the
144
415
  * shared entry point for the sparql path's name form): maps the user-supplied
145
- * name to the entity IRI, everything else comes from the field fetch. The name
416
+ * name to the entity IRIs, everything else comes from the field fetch. The name
146
417
  * is an escaped literal; all terms are validated pack terms.
418
+ *
419
+ * Ranked and unlimited, exactly like {@link buildLookupQuery} — the two paths
420
+ * differ in where the VALUES come from, never in which entities a name reaches.
421
+ * `?score` is deliberately NOT projected: it orders the rows and is none of the
422
+ * caller's business, and projecting it would leak a ranking artefact into
423
+ * `--format json`.
147
424
  */
148
425
  export function buildNameResolveQuery(
149
426
  lookup: PackLookup,
150
427
  name: string,
151
428
  ): string {
429
+ const ranking = rankingClause(lookup);
152
430
  return [
153
431
  "SELECT ?uri ?name WHERE {",
154
- ` ?uri ${formatTerm(lookup.by)} ?name .`,
155
432
  buildTypeConstraint(lookup).trimEnd(),
433
+ nameBinding(lookup),
434
+ ranking.binds,
156
435
  ` FILTER (LCASE(STR(?name)) = LCASE("${escapeSparqlString(name)}"))`,
157
436
  "}",
158
- "ORDER BY STR(?uri)",
159
- "LIMIT 1",
437
+ ranking.orderBy,
160
438
  ]
161
439
  .filter((line) => line !== "")
162
440
  .join("\n");
@@ -175,8 +453,8 @@ export function buildIriResolveQuery(lookup: PackLookup, iri: string): string {
175
453
  return [
176
454
  "SELECT ?uri ?name WHERE {",
177
455
  ` BIND(<${iri}> AS ?uri)`,
178
- iriNameClause(lookup),
179
456
  buildTypeConstraint(lookup).trimEnd(),
457
+ iriNameBinding(lookup),
180
458
  "}",
181
459
  "LIMIT 1",
182
460
  ]
@@ -239,8 +517,12 @@ export function buildLookupNamesQuery(lookup: PackLookup): string {
239
517
  // `block lookup 'Butt*'` listed one Button twice while the other Button
240
518
  // never appeared at all.
241
519
  "SELECT DISTINCT ?name WHERE {",
242
- ` ?uri ${formatTerm(lookup.by)} ?name .`,
243
520
  buildTypeConstraint(lookup).trimEnd(),
521
+ // Same {@link nameBinding} the resolves use, so the addressable population
522
+ // and the resolvable population are the same population. When they were
523
+ // not, a name a miss-suggestion offered could itself miss, and a glob
524
+ // expanded over a pool the resolve could not answer from.
525
+ nameBinding(lookup),
244
526
  "}",
245
527
  ]
246
528
  .filter((line) => line !== "")
@@ -68,7 +68,11 @@ describe("pack toolDescription wiring (PROTECTED)", () => {
68
68
  (t) => t.name === "standard_categories",
69
69
  )?.description;
70
70
  await mcp.cleanup();
71
- expect(desc).toBe("List all code standard categories.");
71
+ // The authored prose reaches the extra verb's tool, whole. Asserted by
72
+ // fragment rather than by exact string: the wiring is what this pins, and
73
+ // tool descriptions are explicitly not frozen (`registerVerb.ts:324-326`).
74
+ expect(desc).toContain("List all code standard categories");
75
+ expect(desc).toContain("Example: standard_categories {}");
72
76
  });
73
77
 
74
78
  it("CLI --help shows the rich prose but NEVER the MCP tool-call syntax", () => {
@@ -126,3 +130,33 @@ describe("a tool-call example names a parameter the tool ACCEPTS (PROTECTED)", (
126
130
  }
127
131
  });
128
132
  });
133
+
134
+ describe("every story tool CARRIES a call example (PROTECTED)", () => {
135
+ // The check above only polices examples that exist. `standard` authored NONE,
136
+ // and the auto-generated one fires only for a filter declaring `values` —
137
+ // which the category filter deliberately does not, because the graph is the
138
+ // vocabulary. So the four `standard_*` tools shipped with no worked call at
139
+ // all, and the model that could not form one had nothing to copy.
140
+ //
141
+ // This asserts the gap cannot reappear on a future story: a compiled story
142
+ // tool must show an agent one call it can make, spelled with its OWN name so
143
+ // the example cannot be copied from a sibling tool and left stale.
144
+ it("names its own tool in an `Example: <tool_name> {…}` fragment", async () => {
145
+ const mcp = await projectMcp([...storyModules.values()]);
146
+ try {
147
+ const tools = await mcp.listTools();
148
+ expect(tools.length).toBeGreaterThan(0);
149
+ const missing = tools
150
+ .filter(
151
+ (tool) =>
152
+ !new RegExp(`Example:\\s*${tool.name}\\s*\\{`).test(
153
+ tool.description ?? "",
154
+ ),
155
+ )
156
+ .map((tool) => tool.name);
157
+ expect(missing).toEqual([]);
158
+ } finally {
159
+ await mcp.cleanup();
160
+ }
161
+ });
162
+ });
@@ -150,10 +150,56 @@ export interface PackFilter {
150
150
  * string matched case-insensitively against the variable).
151
151
  */
152
152
  readonly values?: readonly string[];
153
+ /**
154
+ * How a provided value is compared to the row's cell.
155
+ *
156
+ * `"exact"` (the default) compares the whole cell. `"set"` reads the cell as
157
+ * a SPACE-SEPARATED set and matches when ANY member equals the provided
158
+ * value — the shape a `GROUP_CONCAT` produces, for a dimension where one row
159
+ * legitimately belongs to several values at once. The standing case is a
160
+ * hierarchy: a standard filed under `testing-unit` is, by the definition of
161
+ * the hierarchy, also a `testing` standard, and a filter that compared only
162
+ * the leaf answered `--category testing` with 1 of 8 — a silently wrong
163
+ * answer, exit 0.
164
+ */
165
+ readonly match?: "exact" | "set";
166
+ /**
167
+ * Where the dimension's VOCABULARY lives in the graph, for a filter with no
168
+ * declared {@link values}.
169
+ *
170
+ * A value-free filter rejects a value the data does not carry. The question is
171
+ * what "the data" means. The rows a list just returned are the wrong answer:
172
+ * they are a POPULATION, and a value can be real while no row carries it — a
173
+ * category the graph declares with zero standards filed under it, a
174
+ * `ds:ConceptType` no concept uses yet. Validating against rows turns those
175
+ * into `INVALID_INPUT`, when the honest answer is a calm empty list.
176
+ *
177
+ * So the vocabulary is read from the graph directly, from the same terms the
178
+ * surface that ENUMERATES it reads (`standard categories` reads `cs:Category`
179
+ * / `cs:slug`; so does the `category` filter's vocabulary query). The ruling
180
+ * this honours is "the graph is the vocabulary, don't hard-code the slugs" —
181
+ * rows were never the graph, just the part of it that answered.
182
+ *
183
+ * Without it a value-free filter falls back to the observed rows, which is the
184
+ * only evidence available; that is a NARROWER vocabulary than the truth, and a
185
+ * story whose dimension has an authoritative source should declare it.
186
+ */
187
+ readonly vocabulary?: PackFilterVocabulary;
153
188
  /** Help text (defaults to a generated description). */
154
189
  readonly description?: string;
155
190
  }
156
191
 
192
+ /** The authoritative value set for a value-free {@link PackFilter}. */
193
+ export interface PackFilterVocabulary {
194
+ /** SPARQL SELECT producing one row per admissible value. */
195
+ readonly query: string;
196
+ /**
197
+ * SELECT variable carrying the value (without `?`). Defaults to the filter's
198
+ * own {@link PackFilter.variable}.
199
+ */
200
+ readonly variable?: string;
201
+ }
202
+
157
203
  /**
158
204
  * Free-text search over list rows: a `--search` string keeping a row when ANY
159
205
  * named SELECT variable contains the term (case-insensitive substring). Applied
@@ -239,6 +285,49 @@ export interface PackCompletion {
239
285
  readonly minChars?: number;
240
286
  }
241
287
 
288
+ /**
289
+ * How much an entity's containing SCOPE is worth when a name reaches several
290
+ * entities at once — the ranking factor {@link PackLookup.scopeWeight} declares.
291
+ *
292
+ * DERIVED, never enumerated. The scope's own name is a path (`Global`,
293
+ * `Apps/Launchpad`), so its DEPTH is a fact the graph already states, and a
294
+ * scope nobody has heard of yet inherits its place the day it appears. An
295
+ * enumerated ranking would have to be edited for each new one, and the one
296
+ * nobody edited would silently rank first.
297
+ *
298
+ * The weight falls by {@link falloff} per level below the top, so the top level
299
+ * is worth 1 and each nesting step costs the same again; floored at 0, so a very
300
+ * deep scope can never turn negative and invert the type factor it multiplies.
301
+ *
302
+ * {@link asserted} is the retirement path for the DERIVATION, not for this
303
+ * declaration: an asserted value wins over the derived one
304
+ * (`COALESCE(?asserted, ?derived)`), so the day the ontology states the ranking
305
+ * itself, the depth heuristic stops mattering and `falloff` becomes inert.
306
+ *
307
+ * The declaration itself must stay. Every read of {@link asserted} is reached
308
+ * through this object, so deleting it does not fall back to the ontology's rank
309
+ * — it removes scope ranking from the query altogether and silently returns the
310
+ * lookup to `STR(?uri)` order, which is the defect this exists to fix. What
311
+ * retires is the derivation, and what remains is `via` plus `asserted`.
312
+ */
313
+ export interface PackScopeWeight {
314
+ /** The entity → scope edge (e.g. `ds:tier`). */
315
+ readonly via: string;
316
+ /**
317
+ * The scope property holding its path-shaped name (e.g. `ds:name`), whose
318
+ * `/`-separated depth derives the weight.
319
+ *
320
+ * It must be the NAME, not the IRI: the live tiers spell the same scope
321
+ * `ds:apps_launchpad` and `"Apps/Launchpad"`, and only the name carries the
322
+ * separator that makes the hierarchy legible.
323
+ */
324
+ readonly by: string;
325
+ /** Score cost per level below the top, 0–1 (e.g. 0.2 → 1, 0.8, 0.6, …). */
326
+ readonly falloff: number;
327
+ /** An asserted weight on the scope, which WINS over the derived one. */
328
+ readonly asserted?: string;
329
+ }
330
+
242
331
  /**
243
332
  * The lookup half of a pack. The query is generated from `by` and `type`/`types`
244
333
  * — user-supplied names are escaped by the generator, never interpolated by the
@@ -256,6 +345,29 @@ export interface PackLookup {
256
345
  readonly source?: "sparql" | "graphql";
257
346
  /** Property whose value names the entity — prefixed name or IRI. */
258
347
  readonly by: string;
348
+ /**
349
+ * What names an entity that carries no {@link by} value. Absent (the default)
350
+ * means NOTHING does: the `by` triple is required, and an entity without one
351
+ * is not addressable by name and never drawn by `sample`.
352
+ *
353
+ * `"iri"` opts the family into an IRI-DERIVED name — the local name after the
354
+ * last `#`/`/`, with dot-separated hierarchy segments published as slashes
355
+ * (`cs:react.component.props` → `react/component/props`).
356
+ *
357
+ * DECLARE IT ONLY WHEN THE STORY'S `list` PUBLISHES THE SAME DERIVED NAME.
358
+ * The option exists to keep the two halves of the two-step grammar over ONE
359
+ * population: `standard list` synthesizes a name for the ~87% of code
360
+ * standards carrying no `cs:name`, so `standard lookup` must answer to it.
361
+ * Turning it on where the list does NOT publish such a name is the mirror
362
+ * defect — `token list` requires `ds:tokenId`, so a `ds:Token` without one
363
+ * would become addressable and sampleable under a name the list never
364
+ * published.
365
+ *
366
+ * Requires a class constraint ({@link type}/{@link types}): the derived name
367
+ * is only as trustworthy as the class that vouches for the entity, and
368
+ * without one there is nothing to bound the scan with either.
369
+ */
370
+ readonly nameFallback?: "iri";
259
371
  /** Optional single class constraint — prefixed name or IRI. */
260
372
  readonly type?: string;
261
373
  /** CLI description for the lookup command (defaults to a generated one). */
@@ -271,8 +383,9 @@ export interface PackLookup {
271
383
  * Relative importance per addressed type, prefixed type → 0–1 (unlisted types
272
384
  * default to 1). Editorial judgement about which of a noun's classes matters
273
385
  * most, declared as DATA rather than frozen into the kernel — it feeds the MCP
274
- * listing's `annotations.priority` AND breaks ties in URI-completion ranking,
275
- * so a `ds:Subcomponent` at 0.6 sinks below every component at equal match.
386
+ * listing's `annotations.priority`, breaks ties in URI-completion ranking, AND
387
+ * is one factor of the score a name resolve ORDERS BY, so a `ds:Subcomponent`
388
+ * at 0.6 sinks below every component at equal match.
276
389
  *
277
390
  * A separate map rather than an entry shape on `types` so `types` keeps its
278
391
  * flat `readonly string[]` form and the addition is non-breaking. A key naming
@@ -280,6 +393,16 @@ export interface PackLookup {
280
393
  * is how a weight that never applied survives review.
281
394
  */
282
395
  readonly weights?: Readonly<Record<string, number>>;
396
+ /**
397
+ * The OTHER factor of that score: how much an entity's containing scope is
398
+ * worth, derived from how DEEP that scope sits in its own hierarchy.
399
+ *
400
+ * {@link weights} answers "which KIND of thing did they mean"; this answers
401
+ * "whose?". They multiply rather than tiebreak, because neither subsumes the
402
+ * other: a whole component in a narrow scope still beats a mere part of a
403
+ * block in the widest one.
404
+ */
405
+ readonly scopeWeight?: PackScopeWeight;
283
406
  /**
284
407
  * GraphQL type or interface the generated document's inline fragment targets
285
408
  * (`source: "graphql"` only). Defaults to the local name of `type`; required