@canonical/pragma-cli 0.30.0 → 0.31.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 (254) hide show
  1. package/README.md +64 -35
  2. package/dist/pragma +0 -0
  3. package/package.json +15 -13
  4. package/src/completions/constants.ts +7 -0
  5. package/src/completions/handleQuery.test.ts +32 -0
  6. package/src/completions/handleQuery.ts +20 -1
  7. package/src/completions/queryCompletions.test.ts +5 -0
  8. package/src/completions/queryCompletions.ts +17 -3
  9. package/src/completions/querySocket.test.ts +31 -0
  10. package/src/completions/querySocket.ts +36 -9
  11. package/src/config/parseConfigValues.ts +2 -0
  12. package/src/config/readConfigLayers.test.ts +2 -0
  13. package/src/config/readConfigLayers.ts +3 -0
  14. package/src/config/types.ts +7 -0
  15. package/src/constants.ts +3 -1
  16. package/src/domains/block/commands/sample.ts +2 -1
  17. package/src/domains/block/formatters/list.test.ts +3 -3
  18. package/src/domains/block/formatters/lookup.test.ts +5 -5
  19. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  20. package/src/domains/block/stories.ts +9 -3
  21. package/src/domains/config/commands/channel.test.ts +16 -0
  22. package/src/domains/config/commands/channel.ts +14 -1
  23. package/src/domains/config/commands/framework.test.ts +14 -0
  24. package/src/domains/config/commands/framework.ts +13 -1
  25. package/src/domains/config/commands/tier.test.ts +7 -0
  26. package/src/domains/config/commands/tier.ts +14 -1
  27. package/src/domains/config/operations/validateTier.test.ts +9 -0
  28. package/src/domains/config/operations/validateTier.ts +10 -2
  29. package/src/domains/create/generatorPack/bundled.ts +49 -0
  30. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  31. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  32. package/src/domains/create/index.ts +13 -7
  33. package/src/domains/create/mcp/specs.ts +6 -161
  34. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  35. package/src/domains/create/renderGeneratorUi.ts +65 -0
  36. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  37. package/src/domains/doctor/formatters/doctor.ts +90 -38
  38. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
  39. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
  40. package/src/domains/doctor/operations/checks.test.ts +6 -4
  41. package/src/domains/doctor/operations/types.ts +23 -1
  42. package/src/domains/graph/commands/inspect.test.ts +2 -2
  43. package/src/domains/graph/commands/query.test.ts +5 -3
  44. package/src/domains/graph/formatters/inspect.ts +7 -7
  45. package/src/domains/graph/formatters/query.test.ts +87 -0
  46. package/src/domains/graph/formatters/query.ts +42 -5
  47. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  48. package/src/domains/graphql/commands/serve.test.ts +12 -6
  49. package/src/domains/graphql/commands/serve.ts +16 -6
  50. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  51. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  52. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  53. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  54. package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
  55. package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
  56. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  57. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  58. package/src/domains/graphql/operations/index.ts +5 -0
  59. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  60. package/src/domains/info/formatters/upgrade.ts +3 -1
  61. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  62. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  63. package/src/domains/info/operations/index.ts +1 -0
  64. package/src/domains/info/types.ts +13 -0
  65. package/src/domains/llm/capabilities.test.ts +23 -3
  66. package/src/domains/llm/commands/capabilities.ts +12 -3
  67. package/src/domains/llm/commands/llm.ts +5 -1
  68. package/src/domains/llm/data/commandReference.ts +20 -11
  69. package/src/domains/llm/data/decisionTrees.ts +10 -5
  70. package/src/domains/llm/data/toolCatalog.ts +49 -18
  71. package/src/domains/llm/formatters/orientation.ts +5 -1
  72. package/src/domains/llm/llmCommand.test.ts +11 -3
  73. package/src/domains/llm/mcp/specs.ts +3 -4
  74. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  75. package/src/domains/llm/operations/collectContext.ts +7 -17
  76. package/src/domains/modifier/commands/sample.ts +2 -1
  77. package/src/domains/modifier/operations/lookup.ts +5 -3
  78. package/src/domains/modifier/stories.ts +9 -0
  79. package/src/domains/ontology/commands/list.test.ts +5 -3
  80. package/src/domains/ontology/commands/show.test.ts +85 -9
  81. package/src/domains/ontology/formatters/index.ts +1 -0
  82. package/src/domains/ontology/formatters/list.ts +37 -12
  83. package/src/domains/ontology/formatters/show.ts +358 -67
  84. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  85. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  86. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  87. package/src/domains/ontology/helpers/index.ts +14 -0
  88. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  89. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  90. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  91. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  92. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  93. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  94. package/src/domains/ontology/operations/showOntology.ts +289 -7
  95. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  96. package/src/domains/ontology/stories.ts +53 -8
  97. package/src/domains/refs/operations/updateRefs.ts +0 -1
  98. package/src/domains/setup/commands/index.ts +1 -0
  99. package/src/domains/setup/commands/setup.ts +70 -0
  100. package/src/domains/setup/index.ts +7 -3
  101. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  102. package/src/domains/setup/operations/setupLsp.ts +7 -2
  103. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  104. package/src/domains/shared/bootStore.test.ts +82 -0
  105. package/src/domains/shared/bootStore.ts +56 -20
  106. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  107. package/src/domains/shared/extractLocalName.test.ts +3 -3
  108. package/src/domains/shared/formatters.ts +23 -2
  109. package/src/domains/shared/listTiers.ts +40 -0
  110. package/src/domains/shared/loaders/bundledLoader.ts +48 -0
  111. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  112. package/src/domains/shared/loaders/localLoader.ts +2 -18
  113. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  114. package/src/domains/shared/loaders/readPackageDir.ts +46 -8
  115. package/src/domains/shared/lookupMany.test.ts +15 -0
  116. package/src/domains/shared/lookupMany.ts +22 -6
  117. package/src/domains/shared/namespaces.ts +13 -5
  118. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  119. package/src/domains/shared/parseSampleCount.ts +45 -0
  120. package/src/domains/shared/prefixes.test.ts +211 -0
  121. package/src/domains/shared/prefixes.ts +182 -15
  122. package/src/domains/shared/renderers.ts +5 -5
  123. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  124. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  125. package/src/domains/shared/semanticPackage.test.ts +89 -0
  126. package/src/domains/shared/semanticPackage.ts +17 -2
  127. package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
  128. package/src/domains/shared/stories/compileLookupTool.ts +1 -1
  129. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
  130. package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
  131. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  132. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  133. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
  134. package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
  135. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  136. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  137. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  138. package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
  139. package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
  140. package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
  141. package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
  142. package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
  143. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
  144. package/src/domains/shared/stories/pack/index.ts +14 -0
  145. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  146. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  147. package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
  148. package/src/domains/shared/stories/pack/types.ts +123 -3
  149. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
  150. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
  151. package/src/domains/shared/stories/types.ts +9 -1
  152. package/src/domains/shared/types/index.ts +5 -9
  153. package/src/domains/shared/types/ontology.ts +119 -24
  154. package/src/domains/shared/types/standard.ts +9 -52
  155. package/src/domains/token/commands/lookup.test.ts +8 -0
  156. package/src/domains/token/commands/lookup.ts +12 -0
  157. package/src/domains/token/commands/sample.ts +2 -1
  158. package/src/domains/token/featureFlag.ts +17 -0
  159. package/src/domains/token/index.ts +5 -0
  160. package/src/domains/token/mcp/specs.ts +13 -1
  161. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  162. package/src/domains/trace/commands/show.ts +30 -4
  163. package/src/index.ts +1 -13
  164. package/src/mcp/registerResources.test.ts +2 -2
  165. package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
  166. package/src/mcp/tools/index.ts +18 -10
  167. package/src/mcp/tools/registerTools.test.ts +106 -30
  168. package/src/pipeline/collectCommands.test.ts +151 -1
  169. package/src/pipeline/collectCommands.ts +34 -19
  170. package/src/pipeline/createProgram.test.ts +5 -1
  171. package/src/pipeline/createProgram.ts +2 -2
  172. package/src/pipeline/firstRun.test.ts +80 -0
  173. package/src/pipeline/firstRun.ts +99 -0
  174. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  175. package/src/pipeline/parseGlobalFlags.ts +98 -9
  176. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  177. package/src/pipeline/resolveCommandKind.ts +4 -0
  178. package/src/pipeline/rootHelp.test.ts +87 -0
  179. package/src/pipeline/rootHelp.ts +265 -0
  180. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  181. package/src/pipeline/runCli.test.ts +17 -4
  182. package/src/pipeline/runCli.ts +162 -19
  183. package/src/testing/cli.ts +30 -2
  184. package/src/testing/dsFixtures.ts +21 -4
  185. package/src/testing/index.ts +1 -4
  186. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
  187. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  188. package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
  189. package/src/testing/integration/edge-cases.test.ts +62 -50
  190. package/src/testing/integration/executor-execution.test.ts +6 -4
  191. package/src/testing/integration/executor-golden.test.ts +2 -2
  192. package/src/testing/integration/mcp-surface.test.ts +40 -15
  193. package/src/testing/integration/parity.test.ts +119 -70
  194. package/src/testing/setupXdgIsolation.ts +16 -5
  195. package/src/testing/standardParityFixtures.ts +26 -83
  196. package/src/testing/store.ts +4 -4
  197. package/src/domains/create/commands/component.test.ts +0 -166
  198. package/src/domains/create/commands/component.ts +0 -123
  199. package/src/domains/create/commands/index.ts +0 -3
  200. package/src/domains/create/commands/package.test.ts +0 -35
  201. package/src/domains/create/commands/package.ts +0 -21
  202. package/src/domains/create/generators.ts +0 -8
  203. package/src/domains/standard/commands/categories.test.ts +0 -70
  204. package/src/domains/standard/commands/categories.ts +0 -48
  205. package/src/domains/standard/commands/index.ts +0 -6
  206. package/src/domains/standard/commands/list.test.ts +0 -105
  207. package/src/domains/standard/commands/list.ts +0 -11
  208. package/src/domains/standard/commands/lookup.test.ts +0 -135
  209. package/src/domains/standard/commands/lookup.ts +0 -14
  210. package/src/domains/standard/commands/sample.ts +0 -57
  211. package/src/domains/standard/formatters/categories.test.ts +0 -44
  212. package/src/domains/standard/formatters/categories.ts +0 -38
  213. package/src/domains/standard/formatters/index.ts +0 -10
  214. package/src/domains/standard/formatters/list.test.ts +0 -169
  215. package/src/domains/standard/formatters/list.ts +0 -184
  216. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  217. package/src/domains/standard/formatters/lookup.ts +0 -101
  218. package/src/domains/standard/formatters/sample.ts +0 -15
  219. package/src/domains/standard/formatters/types.ts +0 -31
  220. package/src/domains/standard/index.ts +0 -40
  221. package/src/domains/standard/mcp/index.ts +0 -2
  222. package/src/domains/standard/mcp/specs.ts +0 -89
  223. package/src/domains/standard/operations/categories.test.ts +0 -61
  224. package/src/domains/standard/operations/categories.ts +0 -38
  225. package/src/domains/standard/operations/index.ts +0 -6
  226. package/src/domains/standard/operations/list.test.ts +0 -75
  227. package/src/domains/standard/operations/list.ts +0 -64
  228. package/src/domains/standard/operations/lookup.test.ts +0 -58
  229. package/src/domains/standard/operations/lookup.ts +0 -151
  230. package/src/domains/standard/operations/sample.ts +0 -35
  231. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  232. package/src/domains/standard/orchestration/index.ts +0 -4
  233. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  234. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
  235. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  236. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  237. package/src/domains/standard/standardConfig.ts +0 -42
  238. package/src/domains/standard/stories.test.ts +0 -24
  239. package/src/domains/standard/stories.ts +0 -151
  240. package/src/domains/standard/types.ts +0 -15
  241. package/src/domains/tier/commands/index.ts +0 -3
  242. package/src/domains/tier/commands/list.test.ts +0 -67
  243. package/src/domains/tier/commands/list.ts +0 -9
  244. package/src/domains/tier/formatters/index.ts +0 -3
  245. package/src/domains/tier/formatters/list.test.ts +0 -33
  246. package/src/domains/tier/formatters/list.ts +0 -45
  247. package/src/domains/tier/index.ts +0 -17
  248. package/src/domains/tier/mcp/index.ts +0 -2
  249. package/src/domains/tier/mcp/specs.ts +0 -12
  250. package/src/domains/tier/operations/index.ts +0 -3
  251. package/src/domains/tier/operations/list.test.ts +0 -32
  252. package/src/domains/tier/operations/list.ts +0 -33
  253. package/src/domains/tier/stories.test.ts +0 -22
  254. package/src/domains/tier/stories.ts +0 -52
@@ -1,7 +1,21 @@
1
1
  /** @module Barrel for ontology helpers. */
2
2
 
3
3
  export { default as extractLocalName } from "../../shared/extractLocalName.js";
4
+ export type { ClassTreeNode } from "./buildClassTree.js";
5
+ export {
6
+ default as buildClassTree,
7
+ flattenClassTree,
8
+ } from "./buildClassTree.js";
9
+ export { default as expandOntologyIris } from "./expandOntologyIris.js";
4
10
  export { default as findNamespace } from "./findNamespace.js";
11
+ export type { RawOntologyClass } from "./queryClasses.js";
5
12
  export { default as queryClasses } from "./queryClasses.js";
13
+ export type { RawOntologyProperty } from "./queryProperties.js";
6
14
  export { default as queryProperties } from "./queryProperties.js";
15
+ export {
16
+ queryConstraints,
17
+ queryInstanceCounts,
18
+ queryOntologyMeta,
19
+ querySampleInstances,
20
+ } from "./queryTboxFacts.js";
7
21
  export { default as resolvePrefix } from "./resolvePrefix.js";
@@ -2,49 +2,65 @@ import type { Store } from "@canonical/ke";
2
2
  import { buildQuery } from "../../shared/buildQuery.js";
3
3
  import extractLocalName from "../../shared/extractLocalName.js";
4
4
  import { P } from "../../shared/prefixes.js";
5
- import type { OntologyClass } from "../../shared/types/index.js";
5
+
6
+ /** A class row as stored — full URIs, all superclass edges preserved. */
7
+ export interface RawOntologyClass {
8
+ readonly uri: string;
9
+ readonly label: string;
10
+ readonly comment?: string;
11
+ readonly subClassOf: readonly string[];
12
+ }
6
13
 
7
14
  /**
8
- * Queries all `owl:Class` definitions within a namespace.
9
- *
10
- * Deduplicates by class URI -- when a class has multiple `rdfs:subClassOf`
11
- * values, only the first superclass encountered is kept.
15
+ * Queries all `owl:Class` definitions within a namespace, including their
16
+ * `rdfs:comment` and **every** `rdfs:subClassOf` edge (multiple inheritance
17
+ * is preserved; downstream renderers decide tree placement).
12
18
  *
13
19
  * @note Queries ke store
14
20
  *
15
21
  * @param store - The ke store to query.
16
22
  * @param namespace - The namespace URI to filter classes by.
17
- * @returns An array of {@link OntologyClass} entries.
23
+ * @returns An array of {@link RawOntologyClass} entries, one per class.
18
24
  */
19
25
  export default async function queryClasses(
20
26
  store: Store,
21
27
  namespace: string,
22
- ): Promise<OntologyClass[]> {
28
+ ): Promise<RawOntologyClass[]> {
23
29
  const result = await store.query(
24
30
  buildQuery(`
25
- SELECT ?class ?label ?superclass
31
+ SELECT ?class ?label ?comment ?superclass
26
32
  WHERE {
27
33
  ?class a ${P.owl}Class .
28
34
  FILTER(STRSTARTS(STR(?class), "${namespace}"))
29
35
  OPTIONAL { ?class ${P.rdfs}label ?label }
36
+ OPTIONAL { ?class ${P.rdfs}comment ?comment }
30
37
  OPTIONAL { ?class ${P.rdfs}subClassOf ?superclass }
31
38
  }
32
- ORDER BY ?class
39
+ ORDER BY ?class ?superclass
33
40
  `),
34
41
  );
35
42
 
36
43
  if (result.type !== "select") return [];
37
44
 
38
- const seen = new Map<string, OntologyClass>();
45
+ const seen = new Map<
46
+ string,
47
+ { label: string; comment?: string; supers: Set<string> }
48
+ >();
39
49
  for (const b of result.bindings) {
40
50
  const uri = b.class ?? "";
41
- if (seen.has(uri)) continue;
42
- seen.set(uri, {
43
- uri,
51
+ const entry = seen.get(uri) ?? {
44
52
  label: b.label ?? extractLocalName(uri),
45
- ...(b.superclass ? { superclass: b.superclass } : {}),
46
- });
53
+ ...(b.comment ? { comment: b.comment } : {}),
54
+ supers: new Set<string>(),
55
+ };
56
+ if (b.superclass) entry.supers.add(b.superclass);
57
+ seen.set(uri, entry);
47
58
  }
48
59
 
49
- return [...seen.values()];
60
+ return [...seen.entries()].map(([uri, entry]) => ({
61
+ uri,
62
+ label: entry.label,
63
+ ...(entry.comment ? { comment: entry.comment } : {}),
64
+ subClassOf: [...entry.supers].sort(),
65
+ }));
50
66
  }
@@ -2,11 +2,20 @@ import type { Store } from "@canonical/ke";
2
2
  import { buildQuery } from "../../shared/buildQuery.js";
3
3
  import extractLocalName from "../../shared/extractLocalName.js";
4
4
  import { P } from "../../shared/prefixes.js";
5
- import type { OntologyProperty } from "../../shared/types/index.js";
5
+
6
+ /** A property row as stored — full URIs. */
7
+ export interface RawOntologyProperty {
8
+ readonly uri: string;
9
+ readonly label: string;
10
+ readonly kind: "object" | "datatype";
11
+ readonly domain?: string;
12
+ readonly range?: string;
13
+ readonly functional: boolean;
14
+ }
6
15
 
7
16
  /**
8
17
  * Queries all `owl:ObjectProperty` and `owl:DatatypeProperty` definitions
9
- * within a namespace.
18
+ * within a namespace, marking those also typed `owl:FunctionalProperty`.
10
19
  *
11
20
  * Deduplicates by property URI -- when a property has multiple `rdfs:domain`
12
21
  * or `rdfs:range` values, only the first encountered is kept.
@@ -15,12 +24,12 @@ import type { OntologyProperty } from "../../shared/types/index.js";
15
24
  *
16
25
  * @param store - The ke store to query.
17
26
  * @param namespace - The namespace URI to filter properties by.
18
- * @returns An array of {@link OntologyProperty} entries.
27
+ * @returns An array of {@link RawOntologyProperty} entries.
19
28
  */
20
29
  export default async function queryProperties(
21
30
  store: Store,
22
31
  namespace: string,
23
- ): Promise<OntologyProperty[]> {
32
+ ): Promise<RawOntologyProperty[]> {
24
33
  const result = await store.query(
25
34
  buildQuery(`
26
35
  SELECT ?prop ?label ?domain ?range ?propType
@@ -38,7 +47,9 @@ export default async function queryProperties(
38
47
 
39
48
  if (result.type !== "select") return [];
40
49
 
41
- const seen = new Map<string, OntologyProperty>();
50
+ const functional = await queryFunctionalProperties(store, namespace);
51
+
52
+ const seen = new Map<string, RawOntologyProperty>();
42
53
  for (const b of result.bindings) {
43
54
  const uri = b.prop ?? "";
44
55
  if (seen.has(uri)) continue;
@@ -47,11 +58,33 @@ export default async function queryProperties(
47
58
  label: b.label ?? extractLocalName(uri),
48
59
  ...(b.domain ? { domain: b.domain } : {}),
49
60
  ...(b.range ? { range: b.range } : {}),
50
- type: b.propType?.includes("ObjectProperty")
61
+ kind: b.propType?.includes("ObjectProperty")
51
62
  ? ("object" as const)
52
63
  : ("datatype" as const),
64
+ functional: functional.has(uri),
53
65
  });
54
66
  }
55
67
 
56
68
  return [...seen.values()];
57
69
  }
70
+
71
+ /** URIs within the namespace typed `owl:FunctionalProperty`. */
72
+ async function queryFunctionalProperties(
73
+ store: Store,
74
+ namespace: string,
75
+ ): Promise<Set<string>> {
76
+ const result = await store.query(
77
+ buildQuery(`
78
+ SELECT ?prop
79
+ WHERE {
80
+ ?prop a ${P.owl}FunctionalProperty .
81
+ FILTER(STRSTARTS(STR(?prop), "${namespace}"))
82
+ }
83
+ `),
84
+ );
85
+
86
+ if (result.type !== "select") return new Set();
87
+ return new Set(
88
+ result.bindings.map((b) => b.prop).filter((p): p is string => Boolean(p)),
89
+ );
90
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Supplementary TBox queries for `ontology show` — instance counts, SHACL
3
+ * shape summaries, the owl:Ontology header, and per-class sample instances.
4
+ *
5
+ * Kept together because each is a single small SELECT feeding the one
6
+ * {@link OntologyDetailed} structure the show operation assembles.
7
+ *
8
+ * @note Every function queries the ke store.
9
+ */
10
+
11
+ import type { Store } from "@canonical/ke";
12
+ import { buildQuery } from "../../shared/buildQuery.js";
13
+ import { P } from "../../shared/prefixes.js";
14
+
15
+ const SH = "http://www.w3.org/ns/shacl#";
16
+
17
+ /**
18
+ * Count instances per class within a namespace.
19
+ *
20
+ * One `GROUP BY ?type` query for the whole namespace — never per class.
21
+ *
22
+ * @returns Map from full class URI to instance count.
23
+ */
24
+ export async function queryInstanceCounts(
25
+ store: Store,
26
+ namespace: string,
27
+ ): Promise<ReadonlyMap<string, number>> {
28
+ const result = await store.query(
29
+ buildQuery(`
30
+ SELECT ?type (COUNT(?s) AS ?n)
31
+ WHERE {
32
+ ?s a ?type .
33
+ FILTER(STRSTARTS(STR(?type), "${namespace}"))
34
+ }
35
+ GROUP BY ?type
36
+ `),
37
+ );
38
+
39
+ if (result.type !== "select") return new Map();
40
+
41
+ const counts = new Map<string, number>();
42
+ for (const b of result.bindings) {
43
+ if (b.type && b.n) counts.set(b.type, Number(b.n));
44
+ }
45
+ return counts;
46
+ }
47
+
48
+ /** A SHACL node-shape summary row — full URIs. */
49
+ export interface RawConstraint {
50
+ readonly shape: string;
51
+ readonly targetClass?: string;
52
+ readonly propertyCount: number;
53
+ }
54
+
55
+ /**
56
+ * Summarize `sh:NodeShape`s whose shape or target class lives in the
57
+ * namespace: target class and number of property constraints.
58
+ */
59
+ export async function queryConstraints(
60
+ store: Store,
61
+ namespace: string,
62
+ ): Promise<RawConstraint[]> {
63
+ const result = await store.query(
64
+ buildQuery(`
65
+ SELECT ?shape ?target (COUNT(?p) AS ?n)
66
+ WHERE {
67
+ ?shape a <${SH}NodeShape> .
68
+ OPTIONAL { ?shape <${SH}targetClass> ?target }
69
+ OPTIONAL { ?shape <${SH}property> ?p }
70
+ FILTER(
71
+ STRSTARTS(STR(?shape), "${namespace}") ||
72
+ STRSTARTS(STR(?target), "${namespace}")
73
+ )
74
+ }
75
+ GROUP BY ?shape ?target
76
+ ORDER BY ?shape
77
+ `),
78
+ );
79
+
80
+ if (result.type !== "select") return [];
81
+
82
+ return result.bindings
83
+ .filter((b) => Boolean(b.shape))
84
+ .map((b) => ({
85
+ shape: b.shape as string,
86
+ ...(b.target ? { targetClass: b.target } : {}),
87
+ propertyCount: Number(b.n ?? 0),
88
+ }));
89
+ }
90
+
91
+ /** owl:Ontology header row — full URIs. */
92
+ export interface RawOntologyMeta {
93
+ readonly title?: string;
94
+ readonly version?: string;
95
+ readonly imports: readonly string[];
96
+ }
97
+
98
+ /**
99
+ * Read the `owl:Ontology` header declared in a namespace, when present:
100
+ * title (rdfs:label), version (owl:versionInfo), and owl:imports.
101
+ */
102
+ export async function queryOntologyMeta(
103
+ store: Store,
104
+ namespace: string,
105
+ ): Promise<RawOntologyMeta | undefined> {
106
+ const result = await store.query(
107
+ buildQuery(`
108
+ SELECT ?ont ?title ?version ?import
109
+ WHERE {
110
+ ?ont a ${P.owl}Ontology .
111
+ FILTER(STRSTARTS(STR(?ont), "${namespace}"))
112
+ OPTIONAL { ?ont ${P.rdfs}label ?title }
113
+ OPTIONAL { ?ont ${P.owl}versionInfo ?version }
114
+ OPTIONAL { ?ont ${P.owl}imports ?import }
115
+ }
116
+ ORDER BY ?ont
117
+ `),
118
+ );
119
+
120
+ if (result.type !== "select" || result.bindings.length === 0) {
121
+ return undefined;
122
+ }
123
+
124
+ const first = result.bindings[0] as (typeof result.bindings)[number];
125
+ const imports = [
126
+ ...new Set(
127
+ result.bindings
128
+ .map((b) => b.import)
129
+ .filter((i): i is string => Boolean(i)),
130
+ ),
131
+ ].sort();
132
+
133
+ return {
134
+ ...(first.title ? { title: first.title } : {}),
135
+ ...(first.version ? { version: first.version } : {}),
136
+ imports,
137
+ };
138
+ }
139
+
140
+ /**
141
+ * Fetch up to `limit` instance URIs of a class, as concrete entry points
142
+ * for `graph inspect` and domain lookups.
143
+ */
144
+ export async function querySampleInstances(
145
+ store: Store,
146
+ classUri: string,
147
+ limit = 3,
148
+ ): Promise<string[]> {
149
+ const result = await store.query(
150
+ buildQuery(`
151
+ SELECT ?s
152
+ WHERE { ?s a <${classUri}> }
153
+ ORDER BY ?s
154
+ LIMIT ${limit}
155
+ `),
156
+ );
157
+
158
+ if (result.type !== "select") return [];
159
+ return result.bindings.map((b) => b.s).filter((s): s is string => Boolean(s));
160
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Store } from "@canonical/ke";
2
2
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
3
  import { createTestStore, DS_ALL_TTL } from "#testing";
4
- import { PREFIX_MAP } from "../../shared/prefixes.js";
4
+ import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
5
5
  import listOntologies from "./listOntologies.js";
6
6
 
7
7
  let store: Store;
@@ -22,12 +22,36 @@ describe("listOntologies", () => {
22
22
 
23
23
  const ds = result.find((o) => o.prefix === "ds");
24
24
  expect(ds).toBeDefined();
25
- expect(ds?.namespace).toBe(PREFIX_MAP.ds);
25
+ expect(ds?.namespace).toBe(DEFAULT_PREFIX_MAP.ds);
26
26
  expect(ds?.classCount).toBeGreaterThan(0);
27
- expect(ds?.propertyCount).toBeGreaterThan(0);
27
+ expect(ds?.relationCount).toBeGreaterThan(0);
28
+ expect(ds?.attributeCount).toBeGreaterThan(0);
28
29
  expect(ds?.anatomyCount).toBeGreaterThan(0);
29
30
  });
30
31
 
32
+ it("splits relations from attributes", async () => {
33
+ const result = await listOntologies(store);
34
+ const ds = result.find((o) => o.prefix === "ds");
35
+ // ds:tier is an object property; ds:name is a datatype property.
36
+ expect(ds?.relationCount).toBeGreaterThan(0);
37
+ expect(ds?.attributeCount).toBeGreaterThan(0);
38
+ });
39
+
40
+ it("carries the owl:Ontology header and SHACL shape count", async () => {
41
+ const result = await listOntologies(store);
42
+ const ds = result.find((o) => o.prefix === "ds");
43
+ expect(ds?.title).toBe("Design System Ontology");
44
+ expect(ds?.version).toBe("0.1.0-test");
45
+ expect(ds?.shapeCount).toBeGreaterThan(0);
46
+ });
47
+
48
+ it("omits header fields for namespaces without an owl:Ontology node", async () => {
49
+ const result = await listOntologies(store);
50
+ const cs = result.find((o) => o.prefix === "cs");
51
+ expect(cs?.title).toBeUndefined();
52
+ expect(cs?.version).toBeUndefined();
53
+ });
54
+
31
55
  it("includes cs namespace", async () => {
32
56
  const result = await listOntologies(store);
33
57
  const cs = result.find((o) => o.prefix === "cs");
@@ -36,6 +60,59 @@ describe("listOntologies", () => {
36
60
  expect(cs?.anatomyCount).toBe(0);
37
61
  });
38
62
 
63
+ it("lists namespaces without a registered prefix", async () => {
64
+ const scoped = await createTestStore({
65
+ ttl: `@prefix owl: <http://www.w3.org/2002/07/owl#> .
66
+ <http://unprefixed.example.org/ontology#Widget> a owl:Class .
67
+ <http://unprefixed.example.org/ontology#partOf> a owl:ObjectProperty .`,
68
+ });
69
+ try {
70
+ const result = await listOntologies(scoped.store);
71
+ const anon = result.find(
72
+ (o) => o.namespace === "http://unprefixed.example.org/ontology#",
73
+ );
74
+ expect(anon).toBeDefined();
75
+ expect(anon?.prefix).toBe("");
76
+ expect(anon?.classCount).toBe(1);
77
+ expect(anon?.relationCount).toBe(1);
78
+ } finally {
79
+ scoped.cleanup();
80
+ }
81
+ });
82
+
83
+ it("derives namespaces from short path-only URIs", async () => {
84
+ const scoped = await createTestStore({
85
+ ttl: `@prefix owl: <http://www.w3.org/2002/07/owl#> .
86
+ <http://a/b> a owl:Class .`,
87
+ });
88
+ try {
89
+ const result = await listOntologies(scoped.store);
90
+ expect(result.find((o) => o.namespace === "http://a/")?.classCount).toBe(
91
+ 1,
92
+ );
93
+ } finally {
94
+ scoped.cleanup();
95
+ }
96
+ });
97
+
98
+ it("lists a namespace that only contributes SHACL shapes", async () => {
99
+ const scoped = await createTestStore({
100
+ ttl: `@prefix sh: <http://www.w3.org/ns/shacl#> .
101
+ <http://shapes.example.org/ontology#OnlyShape> a sh:NodeShape .`,
102
+ });
103
+ try {
104
+ const result = await listOntologies(scoped.store);
105
+ const shapeOnly = result.find(
106
+ (o) => o.namespace === "http://shapes.example.org/ontology#",
107
+ );
108
+ expect(shapeOnly).toBeDefined();
109
+ expect(shapeOnly?.shapeCount).toBe(1);
110
+ expect(shapeOnly?.classCount).toBe(0);
111
+ } finally {
112
+ scoped.cleanup();
113
+ }
114
+ });
115
+
39
116
  it("returns sorted by prefix", async () => {
40
117
  const result = await listOntologies(store);
41
118
  const prefixes = result.map((o) => o.prefix);
@@ -4,17 +4,51 @@ import { P } from "../../shared/prefixes.js";
4
4
  import type { OntologySummary } from "../../shared/types/index.js";
5
5
  import findNamespace from "../helpers/findNamespace.js";
6
6
 
7
+ const SH = "http://www.w3.org/ns/shacl#";
8
+
7
9
  /** Check whether a URI belongs to the anatomy namespace. */
8
10
  function isAnatomyTerm(uri: string): boolean {
9
11
  return uri.toLowerCase().includes("anatomy");
10
12
  }
11
13
 
14
+ /** Per-namespace URI accumulator. */
15
+ function add(
16
+ map: Map<string, Set<string>>,
17
+ namespace: string,
18
+ uri: string,
19
+ ): void {
20
+ const set = map.get(namespace) ?? new Set();
21
+ set.add(uri);
22
+ map.set(namespace, set);
23
+ }
24
+
25
+ /**
26
+ * Derive a namespace from a URI when no registered prefix matches: cut
27
+ * after the last `#`, else after the last `/`. Keeps ontologies loaded
28
+ * without a declared prefix (e.g. a package that has not yet published
29
+ * its `pragma.prefixes`) visible in the listing instead of silently
30
+ * dropping their whole TBox.
31
+ */
32
+ function deriveNamespace(uri: string): string | undefined {
33
+ const hash = uri.lastIndexOf("#");
34
+ if (hash > 0) return uri.slice(0, hash + 1);
35
+ // Scheme-agnostic: any slash after the `scheme://` authority separator
36
+ // marks a path segment; a URI with no path (http://host) has none.
37
+ const authority = uri.indexOf("://");
38
+ if (authority < 0) return undefined;
39
+ const slash = uri.lastIndexOf("/");
40
+ return slash > authority + 2 ? uri.slice(0, slash + 1) : undefined;
41
+ }
42
+
12
43
  /**
13
- * List all loaded ontology namespaces with their class, property, and anatomy counts.
44
+ * List all loaded ontology namespaces with their header metadata (title,
45
+ * version) and counts: classes, relations (object properties), attributes
46
+ * (datatype properties), SHACL shapes, and anatomy terms.
14
47
  *
15
48
  * Discovers namespaces by querying for `owl:Class` and
16
49
  * `owl:ObjectProperty`/`owl:DatatypeProperty` definitions, then maps
17
- * namespace URIs back to prefixes via `store.prefixes`.
50
+ * namespace URIs back to prefixes via `store.prefixes`. Four whole-store
51
+ * queries regardless of how many namespaces are loaded.
18
52
  *
19
53
  * @param store - The ke store to query.
20
54
  * @returns An array of {@link OntologySummary} sorted alphabetically by prefix.
@@ -41,25 +75,47 @@ export default async function listOntologies(
41
75
  `),
42
76
  );
43
77
 
78
+ const shapeResult = await store.query(
79
+ buildQuery(`
80
+ SELECT ?shape ?target
81
+ WHERE {
82
+ ?shape a <${SH}NodeShape> .
83
+ OPTIONAL { ?shape <${SH}targetClass> ?target }
84
+ }
85
+ `),
86
+ );
87
+
88
+ const metaResult = await store.query(
89
+ buildQuery(`
90
+ SELECT ?ont ?title ?version
91
+ WHERE {
92
+ ?ont a ${P.owl}Ontology .
93
+ OPTIONAL { ?ont ${P.rdfs}label ?title }
94
+ OPTIONAL { ?ont ${P.owl}versionInfo ?version }
95
+ }
96
+ ORDER BY ?ont
97
+ `),
98
+ );
99
+
44
100
  const prefixEntries = Object.entries(store.prefixes);
101
+ // Everything below is keyed by namespace URI so ontologies without a
102
+ // registered prefix still surface in the listing.
103
+ const nsOf = (uri: string): string | undefined =>
104
+ findNamespace(uri, prefixEntries)?.namespace ?? deriveNamespace(uri);
45
105
  const classCounts = new Map<string, Set<string>>();
46
- const propCounts = new Map<string, Set<string>>();
106
+ const relationCounts = new Map<string, Set<string>>();
107
+ const attributeCounts = new Map<string, Set<string>>();
108
+ const shapeCounts = new Map<string, Set<string>>();
47
109
  const anatomyCounts = new Map<string, Set<string>>();
110
+ const meta = new Map<string, { title?: string; version?: string }>();
48
111
 
49
112
  if (classResult.type === "select") {
50
113
  for (const b of classResult.bindings) {
51
114
  const uri = b.class ?? "";
52
- const ns = findNamespace(uri, prefixEntries);
115
+ const ns = nsOf(uri);
53
116
  if (ns) {
54
- const set = classCounts.get(ns.prefix) ?? new Set();
55
- set.add(uri);
56
- classCounts.set(ns.prefix, set);
57
-
58
- if (isAnatomyTerm(uri)) {
59
- const anatomySet = anatomyCounts.get(ns.prefix) ?? new Set();
60
- anatomySet.add(uri);
61
- anatomyCounts.set(ns.prefix, anatomySet);
62
- }
117
+ add(classCounts, ns, uri);
118
+ if (isAnatomyTerm(uri)) add(anatomyCounts, ns, uri);
63
119
  }
64
120
  }
65
121
  }
@@ -67,35 +123,75 @@ export default async function listOntologies(
67
123
  if (propResult.type === "select") {
68
124
  for (const b of propResult.bindings) {
69
125
  const uri = b.prop ?? "";
70
- const ns = findNamespace(uri, prefixEntries);
126
+ const ns = nsOf(uri);
71
127
  if (ns) {
72
- const set = propCounts.get(ns.prefix) ?? new Set();
73
- set.add(uri);
74
- propCounts.set(ns.prefix, set);
75
-
76
- if (isAnatomyTerm(uri)) {
77
- const anatomySet = anatomyCounts.get(ns.prefix) ?? new Set();
78
- anatomySet.add(uri);
79
- anatomyCounts.set(ns.prefix, anatomySet);
80
- }
128
+ const kind = b.propType?.includes("ObjectProperty")
129
+ ? relationCounts
130
+ : attributeCounts;
131
+ add(kind, ns, uri);
132
+ if (isAnatomyTerm(uri)) add(anatomyCounts, ns, uri);
133
+ }
134
+ }
135
+ }
136
+
137
+ if (shapeResult.type === "select") {
138
+ for (const b of shapeResult.bindings) {
139
+ // Same attribution as `ontology show`: a shape counts for the
140
+ // namespace of the shape itself or of its target class.
141
+ const ns = b.shape ? nsOf(b.shape) : undefined;
142
+ const targetNs = b.target ? nsOf(b.target) : undefined;
143
+ const owner = ns ?? targetNs;
144
+ if (owner && b.shape) add(shapeCounts, owner, b.shape);
145
+ }
146
+ }
147
+
148
+ if (metaResult.type === "select") {
149
+ for (const b of metaResult.bindings) {
150
+ const ns = b.ont ? nsOf(b.ont) : undefined;
151
+ if (ns && !meta.has(ns)) {
152
+ meta.set(ns, {
153
+ ...(b.title ? { title: b.title } : {}),
154
+ ...(b.version ? { version: b.version } : {}),
155
+ });
81
156
  }
82
157
  }
83
158
  }
84
159
 
85
- const allPrefixes = new Set([
160
+ // A namespace that only contributes SHACL shapes still surfaces; one
161
+ // that only declares an owl:Ontology header (no terms at all) does not —
162
+ // an empty TBox is not a listable ontology.
163
+ const allNamespaces = new Set([
86
164
  ...classCounts.keys(),
87
- ...propCounts.keys(),
165
+ ...relationCounts.keys(),
166
+ ...attributeCounts.keys(),
88
167
  ...anatomyCounts.keys(),
168
+ ...shapeCounts.keys(),
89
169
  ]);
90
170
 
91
- return [...allPrefixes].sort().map((prefix) => {
92
- const namespace = prefixEntries.find(([p]) => p === prefix)?.[1] ?? "";
171
+ const summaries = [...allNamespaces].map((namespace) => {
172
+ const prefix = prefixEntries.find(([, ns]) => ns === namespace)?.[0] ?? "";
173
+ const header = meta.get(namespace);
93
174
  return {
94
175
  prefix,
95
176
  namespace,
96
- classCount: classCounts.get(prefix)?.size ?? 0,
97
- propertyCount: propCounts.get(prefix)?.size ?? 0,
98
- anatomyCount: anatomyCounts.get(prefix)?.size ?? 0,
177
+ ...(header?.title ? { title: header.title } : {}),
178
+ ...(header?.version ? { version: header.version } : {}),
179
+ classCount: classCounts.get(namespace)?.size ?? 0,
180
+ relationCount: relationCounts.get(namespace)?.size ?? 0,
181
+ attributeCount: attributeCounts.get(namespace)?.size ?? 0,
182
+ shapeCount: shapeCounts.get(namespace)?.size ?? 0,
183
+ anatomyCount: anatomyCounts.get(namespace)?.size ?? 0,
99
184
  };
100
185
  });
186
+
187
+ // Prefixed namespaces first (alphabetical), unprefixed after by URI.
188
+ return summaries.sort((a, b) =>
189
+ a.prefix && b.prefix
190
+ ? a.prefix.localeCompare(b.prefix)
191
+ : a.prefix
192
+ ? -1
193
+ : b.prefix
194
+ ? 1
195
+ : a.namespace.localeCompare(b.namespace),
196
+ );
101
197
  }