@canonical/pragma-cli 0.32.0 → 0.34.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 +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +19 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +15 -6
  6. package/src/capabilities/block.parity.test.ts +250 -0
  7. package/src/capabilities/capabilities/capabilities.render.ts +3 -2
  8. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  9. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  10. package/src/capabilities/capabilities/catalog.ts +20 -8
  11. package/src/capabilities/capabilities/hints.ts +21 -36
  12. package/src/capabilities/capabilities/types.ts +2 -0
  13. package/src/capabilities/colophon/collectColophon.ts +34 -19
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +11 -4
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/types.ts +16 -7
  20. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  21. package/src/capabilities/config/collectConfigShow.ts +41 -4
  22. package/src/capabilities/config/fields.ts +10 -108
  23. package/src/capabilities/config/set.test.ts +6 -6
  24. package/src/capabilities/config/set.verb.ts +12 -8
  25. package/src/capabilities/config/show.render.test.ts +137 -0
  26. package/src/capabilities/config/show.render.ts +57 -16
  27. package/src/capabilities/config/show.verb.ts +9 -8
  28. package/src/capabilities/config/types.ts +8 -0
  29. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/byteEquality.test.ts +82 -101
  31. package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
  32. package/src/capabilities/create/constants.ts +75 -0
  33. package/src/capabilities/create/create.mcp.test.ts +2 -2
  34. package/src/capabilities/create/create.test.ts +192 -16
  35. package/src/capabilities/create/create.verb.ts +208 -79
  36. package/src/capabilities/create/index.ts +2 -10
  37. package/src/capabilities/create/pickGenerator.ts +34 -28
  38. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  39. package/src/capabilities/create/types.ts +10 -6
  40. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  41. package/src/capabilities/distribution.test.ts +120 -0
  42. package/src/capabilities/distribution.ts +59 -0
  43. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  44. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  45. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  46. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
  47. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  48. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  49. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  50. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  51. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  52. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  53. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  54. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  55. package/src/capabilities/doctor/doctor.render.ts +117 -34
  56. package/src/capabilities/doctor/doctor.test.ts +68 -9
  57. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  58. package/src/capabilities/doctor/runChecks.ts +33 -9
  59. package/src/capabilities/doctor/types.ts +16 -1
  60. package/src/capabilities/emptyRecovery.test.ts +32 -15
  61. package/src/capabilities/graph/inspect.verb.ts +3 -2
  62. package/src/capabilities/graph/query.verb.ts +3 -2
  63. package/src/capabilities/graph/runQuery.ts +8 -5
  64. package/src/capabilities/index.ts +22 -18
  65. package/src/capabilities/info/collectInfo.ts +10 -6
  66. package/src/capabilities/info/info.render.ts +3 -2
  67. package/src/capabilities/info/info.verb.ts +3 -2
  68. package/src/capabilities/lazy.test.ts +93 -2
  69. package/src/capabilities/ontology/index.ts +10 -3
  70. package/src/capabilities/ontology/ontology.test.ts +157 -5
  71. package/src/capabilities/ontology/verbs.ts +203 -104
  72. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  73. package/src/capabilities/prompt/prompt.render.ts +23 -6
  74. package/src/capabilities/prompt/prompt.test.ts +68 -1
  75. package/src/capabilities/prompt/verbs.ts +24 -11
  76. package/src/capabilities/reference.test.ts +32 -0
  77. package/src/capabilities/resources/provider.ts +58 -25
  78. package/src/capabilities/resources/resources.test.ts +176 -21
  79. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  80. package/src/capabilities/setup/index.ts +7 -1
  81. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  82. package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
  83. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  84. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  85. package/src/capabilities/setup/operations/setupSkills.ts +10 -4
  86. package/src/capabilities/setup/setup.render.test.ts +109 -0
  87. package/src/capabilities/setup/setup.render.ts +61 -14
  88. package/src/capabilities/setup/setup.test.ts +352 -1
  89. package/src/capabilities/setup/setup.verb.ts +68 -9
  90. package/src/capabilities/setup/shell.ts +29 -3
  91. package/src/capabilities/setup/types.ts +59 -2
  92. package/src/capabilities/shared/bands.ts +16 -0
  93. package/src/capabilities/skill/discover.ts +11 -5
  94. package/src/capabilities/skill/render.ts +4 -5
  95. package/src/capabilities/skill/verbs.ts +7 -7
  96. package/src/capabilities/sources/collectStatus.ts +69 -49
  97. package/src/capabilities/sources/index.ts +1 -1
  98. package/src/capabilities/sources/installSkills.ts +2 -2
  99. package/src/capabilities/sources/runUpdate.ts +146 -98
  100. package/src/capabilities/sources/sources.test.ts +264 -84
  101. package/src/capabilities/sources/status.render.test.ts +94 -0
  102. package/src/capabilities/sources/status.render.ts +55 -33
  103. package/src/capabilities/sources/status.verb.ts +15 -8
  104. package/src/capabilities/sources/types.ts +18 -25
  105. package/src/capabilities/sources/update.render.ts +9 -2
  106. package/src/capabilities/sources/update.verb.ts +12 -23
  107. package/src/capabilities/surface.test.ts +65 -17
  108. package/src/capabilities/tier.parity.test.ts +144 -0
  109. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  110. package/src/constants.test.ts +9 -7
  111. package/src/constants.ts +49 -23
  112. package/src/identity.test.ts +333 -0
  113. package/src/index.ts +2 -2
  114. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
  115. package/src/kernel/completion/complete.test.ts +3 -5
  116. package/src/kernel/completion/emitScripts.test.ts +1 -1
  117. package/src/kernel/completion/emitScripts.ts +4 -2
  118. package/src/kernel/completion/entitySource.test.ts +185 -41
  119. package/src/kernel/completion/entitySource.ts +131 -113
  120. package/src/kernel/completion/model.test.ts +3 -3
  121. package/src/kernel/completion/model.ts +11 -1
  122. package/src/kernel/completion/parse.test.ts +2 -4
  123. package/src/kernel/completion/safety.test.ts +82 -36
  124. package/src/kernel/completion/shellDrive.test.ts +592 -0
  125. package/src/kernel/completion/templates/fish.ts +28 -6
  126. package/src/kernel/completion/types.ts +3 -3
  127. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  128. package/src/kernel/config/defaults.test.ts +85 -0
  129. package/src/kernel/config/defaults.ts +12 -23
  130. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  131. package/src/kernel/config/findProjectConfig.ts +4 -3
  132. package/src/kernel/config/firstRun.ts +12 -5
  133. package/src/kernel/config/globalConfig.ts +2 -2
  134. package/src/kernel/config/index.ts +4 -5
  135. package/src/kernel/config/paths.ts +17 -17
  136. package/src/kernel/config/readConfig.test.ts +242 -6
  137. package/src/kernel/config/readConfig.ts +8 -8
  138. package/src/kernel/config/schema.test.ts +50 -0
  139. package/src/kernel/config/schema.ts +97 -10
  140. package/src/kernel/config/types.ts +73 -19
  141. package/src/kernel/copy.test.ts +453 -0
  142. package/src/kernel/error/PragmaError.test.ts +5 -5
  143. package/src/kernel/error/constants.ts +3 -2
  144. package/src/kernel/error/index.ts +1 -1
  145. package/src/kernel/error/recovery.test.ts +22 -45
  146. package/src/kernel/error/recovery.ts +22 -27
  147. package/src/kernel/error/renderError.ts +1 -1
  148. package/src/kernel/error/types.ts +5 -4
  149. package/src/kernel/packs/collect.test.ts +195 -11
  150. package/src/kernel/packs/collect.ts +213 -58
  151. package/src/kernel/packs/compile.test.ts +163 -7
  152. package/src/kernel/packs/compile.ts +15 -12
  153. package/src/kernel/packs/disclosure.test.ts +5 -2
  154. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  155. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  156. package/src/kernel/packs/iri.ts +5 -7
  157. package/src/kernel/packs/renderPack.ts +5 -3
  158. package/src/kernel/packs/resolveEntity.ts +21 -17
  159. package/src/kernel/packs/runBodies.ts +15 -14
  160. package/src/kernel/packs/schema.ts +96 -10
  161. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  162. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  163. package/src/kernel/packs/toolDescription.test.ts +10 -2
  164. package/src/kernel/packs/types.ts +39 -4
  165. package/src/kernel/packs/uniqueness.ts +4 -4
  166. package/src/kernel/project/cli/buildProgram.test.ts +60 -11
  167. package/src/kernel/project/cli/buildProgram.ts +11 -0
  168. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  169. package/src/kernel/project/cli/dispatch.ts +34 -12
  170. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  171. package/src/kernel/project/cli/globalFlags.ts +14 -14
  172. package/src/kernel/project/cli/helpFormat.ts +1 -1
  173. package/src/kernel/project/cli/rootHelp.ts +109 -107
  174. package/src/kernel/project/mcp/buildServer.ts +11 -6
  175. package/src/kernel/project/mcp/instructions.ts +12 -7
  176. package/src/kernel/project/mcp/mcpError.ts +52 -0
  177. package/src/kernel/project/mcp/parity.test.ts +39 -1
  178. package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
  179. package/src/kernel/project/mcp/prompts/provider.ts +51 -7
  180. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  181. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  182. package/src/kernel/project/mcp/serve.ts +11 -3
  183. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  184. package/src/kernel/render/disclosure.ts +4 -2
  185. package/src/kernel/render/formatters.ts +5 -4
  186. package/src/kernel/render/prefixes.test.ts +36 -0
  187. package/src/kernel/render/prefixes.ts +40 -13
  188. package/src/kernel/render/renderers.test.ts +35 -1
  189. package/src/kernel/render/renderers.ts +25 -7
  190. package/src/kernel/render/style.test.ts +31 -0
  191. package/src/kernel/render/style.ts +74 -0
  192. package/src/kernel/runtime/graphpack/build.ts +57 -3
  193. package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
  194. package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
  195. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
  196. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
  197. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  198. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  199. package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
  200. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  201. package/src/kernel/runtime/graphpack/index.ts +1 -1
  202. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  203. package/src/kernel/runtime/graphpack/read.ts +32 -22
  204. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  205. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  206. package/src/kernel/runtime/graphpack/types.ts +72 -45
  207. package/src/kernel/runtime/loadSession.ts +4 -12
  208. package/src/kernel/runtime/paths.ts +64 -15
  209. package/src/kernel/runtime/probe.test.ts +8 -4
  210. package/src/kernel/runtime/readEntity.ts +6 -5
  211. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  212. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  213. package/src/kernel/runtime/refs/resolve.ts +141 -65
  214. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  215. package/src/kernel/runtime/resolveSources.ts +36 -25
  216. package/src/kernel/runtime/storeReadiness.ts +62 -0
  217. package/src/kernel/runtime/storeless.test.ts +1 -1
  218. package/src/kernel/runtime/types.ts +1 -1
  219. package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
  220. package/src/kernel/spec/emitReference.test.ts +196 -0
  221. package/src/kernel/spec/emitReference.ts +624 -0
  222. package/src/kernel/spec/emitSurface.ts +13 -7
  223. package/src/kernel/spec/index.ts +2 -0
  224. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  225. package/src/kernel/spec/surfaceConformance.ts +8 -0
  226. package/src/kernel/spec/types.ts +29 -9
  227. package/src/kernel/spec/validate.ts +2 -1
  228. package/src/kernel/vocabulary.test.ts +164 -0
  229. package/src/kernel/vocabulary.ts +146 -0
  230. package/src/testing/PARITY_GAPS.ts +9 -3
  231. package/src/testing/behavioral/README.md +3 -3
  232. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  233. package/src/testing/behavioral/completion.test.ts +21 -8
  234. package/src/testing/behavioral/disclosure.test.ts +12 -0
  235. package/src/testing/behavioral/docExamples.test.ts +283 -0
  236. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  237. package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
  238. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  239. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  240. package/src/testing/behavioral/journeys.cli.test.ts +116 -73
  241. package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
  242. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  243. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  244. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  245. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
  246. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  247. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  248. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  249. package/src/testing/behavioral/staleCommands.test.ts +86 -0
  250. package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
  251. package/src/testing/eval/cases/assembly.ts +16 -9
  252. package/src/testing/eval/cases/effectful.ts +31 -16
  253. package/src/testing/eval/cases/readNouns.ts +23 -18
  254. package/src/testing/eval/cases/stable.ts +5 -1
  255. package/src/testing/eval/eval.test.ts +7 -1
  256. package/src/testing/fixtures/fixtureCapability.ts +201 -1
  257. package/src/testing/fixtures/graph/canonical.ts +6 -3
  258. package/src/testing/fixtures/graph/defaultPack.ts +179 -0
  259. package/src/testing/fixtures/graph/owlExport.ts +97 -0
  260. package/src/testing/helpers/extractSnippets.ts +29 -0
  261. package/src/testing/helpers/fixtureGraph.ts +33 -16
  262. package/src/testing/helpers/golden.ts +1 -1
  263. package/src/testing/helpers/packRuntime.ts +2 -3
  264. package/src/testing/helpers/projectCli.ts +2 -1
  265. package/src/testing/helpers/projectMcp.ts +6 -0
  266. package/src/testing/perf/budgets.test.ts +4 -0
  267. package/src/testing/perf/budgets.ts +22 -7
  268. package/src/testing/perf/globalSetup.ts +121 -6
  269. package/src/types.ts +2 -2
  270. package/src/capabilities/block/blockList.verb.ts +0 -150
  271. package/src/capabilities/block/index.ts +0 -25
  272. package/src/capabilities/block/pack.ts +0 -155
  273. package/src/capabilities/block/parity.test.ts +0 -180
  274. package/src/capabilities/block/tierChain.ts +0 -78
  275. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  276. package/src/capabilities/config/field.test.ts +0 -188
  277. package/src/capabilities/modifier/index.ts +0 -15
  278. package/src/capabilities/modifier/pack.ts +0 -64
  279. package/src/capabilities/standard/index.ts +0 -18
  280. package/src/capabilities/standard/pack.ts +0 -128
  281. package/src/capabilities/tier/index.ts +0 -19
  282. package/src/capabilities/tier/lookup.render.ts +0 -34
  283. package/src/capabilities/tier/lookup.test.ts +0 -68
  284. package/src/capabilities/tier/lookup.verb.ts +0 -44
  285. package/src/capabilities/tier/pack.ts +0 -28
  286. package/src/capabilities/tier/runLookup.ts +0 -59
  287. package/src/capabilities/token/addConfig.render.ts +0 -27
  288. package/src/capabilities/token/addConfig.test.ts +0 -62
  289. package/src/capabilities/token/addConfig.verb.ts +0 -40
  290. package/src/capabilities/token/index.ts +0 -19
  291. package/src/capabilities/token/pack.ts +0 -58
  292. package/src/capabilities/token/runAddConfig.ts +0 -95
  293. package/src/kernel/completion/bashDrive.test.ts +0 -73
  294. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  295. package/src/kernel/runtime/lock.ts +0 -90
@@ -1,17 +1,36 @@
1
1
  /**
2
2
  * Build the storeless entity index (`index.json`) from a populated store.
3
3
  *
4
- * A lean port of the v1 `buildGraphIndex` (#856): three bulk SPARQL queries —
5
- * types, labels, and instance counts — joined in memory, rather than a query
6
- * per subject. Only typed named-node subjects are indexed (untyped subjects and
7
- * blank nodes are reachable via `graph query` but never listed). Each entity
4
+ * A lean port of the v1 `buildGraphIndex` (#856): a few bulk SPARQL queries —
5
+ * types (which also yield the instance counts), labels, descriptions and
6
+ * alternative names — joined in memory, rather than a query per subject. Only
7
+ * the last names a DOMAIN property, and it is read from the distribution's
8
+ * declaration rather than spelled out here; see {@link resolveAltNamePredicate} for
9
+ * the pack that binds no such prefix.
10
+ *
11
+ * Only typed named-node subjects are indexed (untyped subjects and blank-node
12
+ * subjects are reachable via `graph query` but never listed). Each entity
8
13
  * carries the FROZEN `{ name, type }` minimum plus the enrichment fields
9
14
  * (`uri`, `prefixed`, `types`, `label`, `box`) the completion tier and reads
10
15
  * use. The output is pure JSON — no store handle survives — so the completion
11
16
  * tier reads it without ever booting oxigraph.
17
+ *
18
+ * Real-data correctness (things a single-typed toy graph never exercises):
19
+ * - Blank-node `rdf:type` objects (SHACL shapes, RDF lists) are ignored — a
20
+ * `_:b0` is never a domain class and must not become a `type:"_:b0"` key.
21
+ * - The abox primary type is the LEXICALLY-SMALLEST domain class, a
22
+ * deterministic tie-break: SPARQL returns a subject's types in unspecified
23
+ * order, so a multi-domain individual would otherwise get an arbitrary
24
+ * completion key that flips across builds/engines.
25
+ * - Multilingual labels prefer untagged / `@en`, then the lexically smallest —
26
+ * never the store's arbitrary first row.
27
+ * - An OWL-punned subject (a class/property IRI ALSO asserted as a domain
28
+ * individual) emits BOTH its tbox and abox facet, so its abox membership
29
+ * stays completable instead of vanishing behind the tbox facet.
12
30
  */
13
31
 
14
32
  import { compactUri } from "../../render/compactUri.js";
33
+ import { VOCABULARY } from "../../vocabulary.js";
15
34
  import type { PackIndex, PackIndexEntity } from "./types.js";
16
35
 
17
36
  type Store = import("@canonical/ke").Store;
@@ -62,62 +81,143 @@ const DESCRIPTION_PREDICATES = [
62
81
  "http://schema.org/description",
63
82
  ];
64
83
 
65
- /**
66
- * Alternative-name predicates: `ds:name`, the property `tier lookup` (and other
67
- * `ds:name`-addressed families) match on. Projected into `altNames` so the
68
- * storeless name-completion sources can offer those tokens exactly, without a
69
- * store boot. Distinct from the display `label` — a subject may carry both.
70
- */
71
- const ALT_NAME_PREDICATES = ["https://ds.canonical.com/name"];
72
-
73
84
  const isStdVocab = (uri: string): boolean =>
74
85
  STD_VOCAB_PREFIXES.some((ns) => uri.startsWith(ns));
75
86
 
76
- /** Classify a subject's full types into a box and a primary (filter) type. */
77
- function classify(
78
- fullTypes: readonly string[],
79
- ): { box: "tbox" | "abox"; primary: string } | null {
80
- if (fullTypes.length === 0) return null;
87
+ /** One classification facet: which box a subject occupies + its primary type. */
88
+ interface Facet {
89
+ readonly box: "tbox" | "abox";
90
+ readonly primary: string;
91
+ }
92
+
93
+ /**
94
+ * Classify a subject's full types into its facets — one per box it occupies.
95
+ *
96
+ * A plain individual or a plain class yields ONE facet. An OWL-punned subject
97
+ * (a class/property IRI ALSO asserted as a domain individual) yields BOTH its
98
+ * tbox and abox facet (A8), so its abox membership stays completable rather than
99
+ * disappearing behind the tbox one. The abox primary is the lexically-smallest
100
+ * domain class — a deterministic tie-break (A3), not the store's first row.
101
+ *
102
+ * @param fullTypes - The subject's asserted `rdf:type` values (blank nodes
103
+ * already dropped), in store order.
104
+ * @returns The subject's facets (empty when it has no usable type).
105
+ */
106
+ function classify(fullTypes: readonly string[]): Facet[] {
107
+ if (fullTypes.length === 0) return [];
108
+ const facets: Facet[] = [];
81
109
  const classMeta = fullTypes.find((t) => CLASS_METATYPES.has(t));
82
- if (classMeta) return { box: "tbox", primary: classMeta };
83
110
  const propMeta = fullTypes.find((t) => PROPERTY_METATYPES.has(t));
84
- if (propMeta) return { box: "tbox", primary: propMeta };
85
- // An individual: its filter key is the most specific domain class.
86
- const domain =
87
- fullTypes.find((t) => t !== NAMED_INDIVIDUAL && !isStdVocab(t)) ??
88
- fullTypes.find((t) => t !== NAMED_INDIVIDUAL) ??
89
- fullTypes[0];
90
- return { box: "abox", primary: domain as string };
111
+ if (classMeta !== undefined) facets.push({ box: "tbox", primary: classMeta });
112
+ else if (propMeta !== undefined)
113
+ facets.push({ box: "tbox", primary: propMeta });
114
+
115
+ // A subject's domain classes: its own types minus the structural std vocab
116
+ // and the `owl:NamedIndividual` typing artifact. Sorted, so the abox primary
117
+ // (the smallest) is stable across builds/engines regardless of store order.
118
+ const domainClasses = fullTypes
119
+ .filter((t) => t !== NAMED_INDIVIDUAL && !isStdVocab(t))
120
+ .sort();
121
+ const primaryDomain = domainClasses.at(0);
122
+ if (primaryDomain !== undefined) {
123
+ facets.push({ box: "abox", primary: primaryDomain });
124
+ } else if (facets.length === 0) {
125
+ // No schema facet and no domain class — a bare individual (only
126
+ // `owl:NamedIndividual`, or only std-vocab types). Keep it indexed under a
127
+ // deterministic primary rather than dropping it silently.
128
+ const fallback =
129
+ fullTypes
130
+ .filter((t) => t !== NAMED_INDIVIDUAL)
131
+ .sort()
132
+ .at(0) ?? [...fullTypes].sort().at(0);
133
+ if (fallback !== undefined) facets.push({ box: "abox", primary: fallback });
134
+ }
135
+ return facets;
91
136
  }
92
137
 
93
138
  /** A `VALUES ?p { … }` body from a predicate list. */
94
139
  const valuesList = (uris: readonly string[]): string =>
95
140
  uris.map((uri) => `<${uri}>`).join(" ");
96
141
 
142
+ /**
143
+ * Expand the declared alternative-name property against a pack's prefix map.
144
+ *
145
+ * That property is what a family is addressed by, so the storeless completion
146
+ * sources can offer those tokens exactly and a bespoke lookup can match them,
147
+ * without a store boot. Distinct from the display `label`: a subject may carry
148
+ * both.
149
+ *
150
+ * The distribution DECLARES it as a prefixed name; this resolves it against the
151
+ * PACK's own prefixes, which are not the distribution's. A pack that binds no
152
+ * such prefix simply carries no alternative names — the normal case for a
153
+ * third-party pack outside the distribution's namespace. Expanding through
154
+ * `packs/iri.ts` instead would THROW on exactly that pack and take its whole
155
+ * build down, so this degrades and the caller skips the query.
156
+ *
157
+ * The split needs no guard: `kernel/vocabulary.ts` has already held the term to
158
+ * `prefix:local`, so the colon is there and both sides are non-empty.
159
+ *
160
+ * @param prefixes - The store's merged prefix map.
161
+ * @returns The full IRI to collect, or `undefined` when the prefix is unbound.
162
+ */
163
+ function resolveAltNamePredicate(
164
+ prefixes: Readonly<Record<string, string>>,
165
+ ): string | undefined {
166
+ const colon = VOCABULARY.altName.indexOf(":");
167
+ const namespace = prefixes[VOCABULARY.altName.slice(0, colon)];
168
+ return namespace === undefined
169
+ ? undefined
170
+ : `${namespace}${VOCABULARY.altName.slice(colon + 1)}`;
171
+ }
172
+
173
+ /** Language rank for label selection: untagged (0) < `@en` (1) < any other (2). */
174
+ function rankLanguage(language: string): number {
175
+ if (language === "") return 0;
176
+ const lower = language.toLowerCase();
177
+ return lower === "en" || lower.startsWith("en-") ? 1 : 2;
178
+ }
179
+
97
180
  /**
98
181
  * Per subject, keep the object whose predicate ranks HIGHEST (lowest index) in
99
182
  * `preference`. The `?p` binding is projected so the winner is chosen by declared
100
183
  * preference order: SPARQL returns solutions in an unspecified order, so keeping
101
184
  * the first row per subject would be store-order-dependent (an entity carrying
102
185
  * both `rdfs:label` and `skos:prefLabel` could index either), not ranked.
186
+ *
187
+ * Ties within a predicate (a subject with several `rdfs:label` values — the
188
+ * multilingual case) are broken deterministically: untagged / `@en` first, then
189
+ * the lexically smallest value. So `"Button"@en`/`"Bouton"@fr` always resolves
190
+ * to `Button`, never whichever row the store happened to return first (A7).
103
191
  */
104
192
  function preferredBySubject(
105
193
  result: import("@canonical/ke").SelectResult,
106
194
  valueVar: string,
107
195
  preference: readonly string[],
108
196
  ): Map<string, string> {
109
- const best = new Map<string, { value: string; rank: number }>();
197
+ const best = new Map<
198
+ string,
199
+ { value: string; rank: number; langRank: number }
200
+ >();
110
201
  for (const term of result.termBindings) {
111
202
  if (term.s?.termType !== "NamedNode") continue;
112
- const value = term[valueVar]?.value;
203
+ const valueTerm = term[valueVar];
113
204
  const predicate = term.p?.value;
114
- if (!value || predicate === undefined) continue;
205
+ if (valueTerm === undefined || predicate === undefined) continue;
206
+ const value = valueTerm.value;
207
+ if (!value) continue;
115
208
  const rank = preference.indexOf(predicate);
116
209
  if (rank === -1) continue;
210
+ const language =
211
+ valueTerm.termType === "Literal" ? (valueTerm.language ?? "") : "";
212
+ const langRank = rankLanguage(language);
117
213
  const current = best.get(term.s.value);
118
- if (current === undefined || rank < current.rank) {
119
- best.set(term.s.value, { value, rank });
120
- }
214
+ const better =
215
+ current === undefined ||
216
+ rank < current.rank ||
217
+ (rank === current.rank &&
218
+ (langRank < current.langRank ||
219
+ (langRank === current.langRank && value < current.value)));
220
+ if (better) best.set(term.s.value, { value, rank, langRank });
121
221
  }
122
222
  const picked = new Map<string, string>();
123
223
  for (const [subject, entry] of best) picked.set(subject, entry.value);
@@ -159,6 +259,7 @@ export async function buildIndex(
159
259
  prefixes: Readonly<Record<string, string>>,
160
260
  contentHash: string,
161
261
  ): Promise<PackIndex> {
262
+ const altNameUri = resolveAltNamePredicate(prefixes);
162
263
  const [typesResult, labelResult, descResult, altNameResult] =
163
264
  await Promise.all([
164
265
  store.query(
@@ -170,17 +271,24 @@ export async function buildIndex(
170
271
  store.query(
171
272
  `SELECT ?s ?p ?desc WHERE { ?s ?p ?desc . VALUES ?p { ${valuesList(DESCRIPTION_PREDICATES)} } }` as never,
172
273
  ) as Promise<import("@canonical/ke").SelectResult>,
173
- store.query(
174
- `SELECT ?s ?p ?alt WHERE { ?s ?p ?alt . VALUES ?p { ${valuesList(ALT_NAME_PREDICATES)} } }` as never,
175
- ) as Promise<import("@canonical/ke").SelectResult>,
274
+ altNameUri === undefined
275
+ ? undefined
276
+ : (store.query(
277
+ `SELECT ?s ?p ?alt WHERE { ?s ?p ?alt . VALUES ?p { <${altNameUri}> } }` as never,
278
+ ) as Promise<import("@canonical/ke").SelectResult>),
176
279
  ]);
177
280
 
178
281
  const typesBySubject = new Map<string, string[]>();
179
282
  const instanceCountByType: Record<string, number> = {};
180
283
  for (const term of typesResult.termBindings) {
181
284
  if (term.s?.termType !== "NamedNode") continue;
285
+ const typeTerm = term.type;
286
+ // Ignore blank-node `rdf:type` objects (SHACL shapes / RDF lists): a `_:b0`
287
+ // is never a domain class, so it must not become a garbage primary type or
288
+ // inflate the per-type instance counts (A6).
289
+ if (typeTerm === undefined || typeTerm.termType === "BlankNode") continue;
182
290
  const subject = term.s.value;
183
- const typeUri = term.type?.value ?? "";
291
+ const typeUri = typeTerm.value;
184
292
  const bucket = typesBySubject.get(subject) ?? [];
185
293
  bucket.push(typeUri);
186
294
  typesBySubject.set(subject, bucket);
@@ -197,27 +305,42 @@ export async function buildIndex(
197
305
  "desc",
198
306
  DESCRIPTION_PREDICATES,
199
307
  );
200
- const altNamesBySubject = allBySubject(altNameResult, "alt");
308
+ const altNamesBySubject =
309
+ altNameResult === undefined
310
+ ? new Map<string, string[]>()
311
+ : allBySubject(altNameResult, "alt");
201
312
 
202
313
  const entities: PackIndexEntity[] = [];
203
314
  for (const [subject, fullTypes] of typesBySubject) {
204
- const classification = classify(fullTypes);
205
- if (classification === null) continue;
315
+ const facets = classify(fullTypes);
316
+ if (facets.length === 0) continue;
206
317
  const prefixed = compactUri(subject, prefixes);
207
318
  const altNames = altNamesBySubject.get(subject);
208
- entities.push({
209
- name: prefixed,
210
- type: compactUri(classification.primary, prefixes),
211
- uri: subject,
212
- prefixed,
213
- types: fullTypes.map((t) => compactUri(t, prefixes)),
214
- label: labelBySubject.get(subject) ?? null,
215
- ...(altNames && altNames.length > 0 ? { altNames } : {}),
216
- box: classification.box,
217
- description: descBySubject.get(subject) ?? null,
218
- });
319
+ const typesCompact = fullTypes.map((t) => compactUri(t, prefixes));
320
+ const label = labelBySubject.get(subject) ?? null;
321
+ const description = descBySubject.get(subject) ?? null;
322
+ // A punned subject emits one entity per facet (tbox class + abox individual).
323
+ for (const facet of facets) {
324
+ entities.push({
325
+ name: prefixed,
326
+ type: compactUri(facet.primary, prefixes),
327
+ uri: subject,
328
+ prefixed,
329
+ types: typesCompact,
330
+ label,
331
+ ...(altNames && altNames.length > 0 ? { altNames } : {}),
332
+ box: facet.box,
333
+ description,
334
+ });
335
+ }
219
336
  }
220
- entities.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
337
+ // Sort by name, then a deterministic tie-break (box, then type) so a punned
338
+ // subject's two facets keep a stable order across builds.
339
+ entities.sort((a, b) => {
340
+ if (a.name !== b.name) return a.name < b.name ? -1 : 1;
341
+ if (a.box !== b.box) return a.box === "tbox" ? -1 : 1;
342
+ return a.type < b.type ? -1 : a.type > b.type ? 1 : 0;
343
+ });
221
344
 
222
345
  return {
223
346
  version: 2,