@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
@@ -2,7 +2,7 @@ import type { Store } from "@canonical/ke";
2
2
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
3
  import { PragmaError } from "#error";
4
4
  import { createTestStore, DS_ALL_TTL } from "#testing";
5
- import { PREFIX_MAP } from "../../shared/prefixes.js";
5
+ import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
6
6
  import showOntology from "./showOntology.js";
7
7
 
8
8
  let store: Store;
@@ -17,31 +17,85 @@ beforeAll(async () => {
17
17
  afterAll(() => cleanup());
18
18
 
19
19
  describe("showOntology", () => {
20
- it("returns classes and properties for a prefix", async () => {
20
+ it("returns classes with attached properties for a prefix", async () => {
21
21
  const result = await showOntology(store, "ds");
22
22
  expect(result.prefix).toBe("ds");
23
- expect(result.namespace).toBe(PREFIX_MAP.ds);
23
+ expect(result.namespace).toBe(DEFAULT_PREFIX_MAP.ds);
24
24
  expect(result.classes.length).toBeGreaterThan(0);
25
- expect(result.properties.length).toBeGreaterThan(0);
25
+
26
+ const uiBlock = result.classes.find((c) => c.iri === "ds:UIBlock");
27
+ expect(uiBlock).toBeDefined();
28
+ expect(uiBlock?.properties.map((p) => p.label)).toContain("name");
26
29
  });
27
30
 
28
- it("includes class labels and superclass relationships", async () => {
31
+ it("preserves all subClassOf edges as compact IRIs", async () => {
29
32
  const result = await showOntology(store, "ds");
30
33
  const component = result.classes.find((c) => c.label === "Component");
31
34
  expect(component).toBeDefined();
32
- expect(component?.superclass).toContain("UIBlock");
35
+ expect(component?.subClassOf).toContain("ds:UIBlock");
36
+ });
37
+
38
+ it("orders classes topologically (parents before children)", async () => {
39
+ const result = await showOntology(store, "ds");
40
+ const order = result.classes.map((c) => c.iri);
41
+ expect(order.indexOf("ds:UIBlock")).toBeLessThan(
42
+ order.indexOf("ds:Component"),
43
+ );
44
+ });
45
+
46
+ it("includes rdfs:comment when declared", async () => {
47
+ const result = await showOntology(store, "ds");
48
+ const component = result.classes.find((c) => c.iri === "ds:Component");
49
+ expect(component?.comment).toBe("A reusable UI building block.");
50
+ });
51
+
52
+ it("counts instances per class", async () => {
53
+ const result = await showOntology(store, "ds");
54
+ const component = result.classes.find((c) => c.iri === "ds:Component");
55
+ expect(component?.instances).toBeGreaterThan(0);
56
+ });
57
+
58
+ it("marks functional properties", async () => {
59
+ const result = await showOntology(store, "ds");
60
+ const uiBlock = result.classes.find((c) => c.iri === "ds:UIBlock");
61
+ const tier = uiBlock?.properties.find((p) => p.iri === "ds:tier");
62
+ expect(tier?.functional).toBe(true);
33
63
  });
34
64
 
35
- it("includes property domain and range", async () => {
65
+ it("attaches property domain and range as compact IRIs", async () => {
36
66
  const result = await showOntology(store, "ds");
37
- const nameProp = result.properties.find((p) => p.uri.endsWith("name"));
67
+ const uiBlock = result.classes.find((c) => c.iri === "ds:UIBlock");
68
+ const nameProp = uiBlock?.properties.find((p) => p.iri === "ds:name");
38
69
  expect(nameProp).toBeDefined();
39
- expect(nameProp?.domain).toContain("UIBlock");
40
- expect(nameProp?.type).toBe("datatype");
70
+ expect(nameProp?.domain).toBe("ds:UIBlock");
71
+ expect(nameProp?.kind).toBe("datatype");
72
+ expect(nameProp?.range).toBe("xsd:string");
73
+ });
74
+
75
+ it("emits a prefixes map covering the compact IRIs used", async () => {
76
+ const result = await showOntology(store, "ds");
77
+ expect(result.prefixes.ds).toBe(DEFAULT_PREFIX_MAP.ds);
78
+ expect(result.prefixes.xsd).toBeDefined();
79
+ });
80
+
81
+ it("surfaces the owl:Ontology header", async () => {
82
+ const result = await showOntology(store, "ds");
83
+ expect(result.meta?.title).toBe("Design System Ontology");
84
+ expect(result.meta?.version).toBe("0.1.0-test");
85
+ });
86
+
87
+ it("summarizes SHACL node shapes", async () => {
88
+ const result = await showOntology(store, "ds");
89
+ const shape = result.constraints?.find(
90
+ (c) => c.shape === "ds:ComponentShape",
91
+ );
92
+ expect(shape).toBeDefined();
93
+ expect(shape?.targetClass).toBe("ds:Component");
94
+ expect(shape?.propertyCount).toBe(2);
41
95
  });
42
96
 
43
97
  it("resolves full namespace URI", async () => {
44
- const result = await showOntology(store, PREFIX_MAP.ds);
98
+ const result = await showOntology(store, DEFAULT_PREFIX_MAP.ds);
45
99
  expect(result.prefix).toBe("ds");
46
100
  expect(result.classes.length).toBeGreaterThan(0);
47
101
  });
@@ -59,7 +113,72 @@ describe("showOntology", () => {
59
113
  it("returns cs ontology with standard classes", async () => {
60
114
  const result = await showOntology(store, "cs");
61
115
  expect(result.prefix).toBe("cs");
62
- const standard = result.classes.find((c) => c.uri.includes("CodeStandard"));
116
+ const standard = result.classes.find((c) => c.iri.includes("CodeStandard"));
63
117
  expect(standard).toBeDefined();
64
118
  });
65
119
  });
120
+
121
+ describe("showOntology — class focus", () => {
122
+ it("deep-dives into a class by label", async () => {
123
+ const result = await showOntology(store, "ds", { class: "Component" });
124
+ const focus = result.focus;
125
+ expect(focus).toBeDefined();
126
+ expect(focus?.iri).toBe("ds:Component");
127
+ expect(focus?.superChain).toEqual(["ds:UIBlock"]);
128
+ expect(focus?.instances).toBeGreaterThan(0);
129
+ });
130
+
131
+ it("separates direct and inherited properties", async () => {
132
+ const result = await showOntology(store, "ds", { class: "Component" });
133
+ const inherited = result.focus?.inheritedProperties.map((p) => p.iri);
134
+ expect(inherited).toContain("ds:name");
135
+ });
136
+
137
+ it("finds reverse references (properties ranging over the class)", async () => {
138
+ const result = await showOntology(store, "ds", { class: "UIBlock" });
139
+ const refs = result.focus?.referencedBy.map((p) => p.iri);
140
+ expect(refs).toContain("ds:implementsBlock");
141
+ });
142
+
143
+ it("returns sample instance IRIs", async () => {
144
+ const result = await showOntology(store, "ds", { class: "Component" });
145
+ expect(result.focus?.sampleInstances.length).toBeGreaterThan(0);
146
+ });
147
+
148
+ it("emits runnable follow-up queries with compact IRIs", async () => {
149
+ const result = await showOntology(store, "ds", { class: "Component" });
150
+ const queries = result.focus?.queries ?? [];
151
+ expect(queries.length).toBeGreaterThan(0);
152
+ expect(queries[0]?.sparql).toBe(
153
+ "SELECT ?instance WHERE { ?instance a ds:Component } LIMIT 20",
154
+ );
155
+ // A sample-instance inspection query is included when instances exist.
156
+ expect(queries.some((q) => q.sparql.includes("?predicate ?object"))).toBe(
157
+ true,
158
+ );
159
+ });
160
+
161
+ it("accepts a compact IRI and a local name", async () => {
162
+ const byIri = await showOntology(store, "ds", { class: "ds:Component" });
163
+ expect(byIri.focus?.iri).toBe("ds:Component");
164
+
165
+ const byLocal = await showOntology(store, "ds", { class: "component" });
166
+ expect(byLocal.focus?.iri).toBe("ds:Component");
167
+ });
168
+
169
+ it("throws not-found with suggestions for an unknown class", async () => {
170
+ try {
171
+ await showOntology(store, "ds", { class: "Comp" });
172
+ expect.unreachable("should have thrown");
173
+ } catch (e) {
174
+ expect((e as PragmaError).code).toBe("ENTITY_NOT_FOUND");
175
+ expect((e as PragmaError).suggestions).toContain("Component");
176
+ }
177
+ });
178
+
179
+ it("still returns the full namespace structure alongside the focus", async () => {
180
+ const result = await showOntology(store, "ds", { class: "Component" });
181
+ expect(result.classes.length).toBeGreaterThan(0);
182
+ expect(result.focus).toBeDefined();
183
+ });
184
+ });
@@ -1,12 +1,44 @@
1
1
  import type { Store } from "@canonical/ke";
2
2
  import { PragmaError } from "#error";
3
- import type { OntologyDetailed } from "../../shared/types/index.js";
3
+ import compactUri from "../../shared/compactUri.js";
4
+ import { PREFIX_MAP } from "../../shared/prefixes.js";
5
+ import type {
6
+ OntologyClass,
7
+ OntologyClassFocus,
8
+ OntologyConstraint,
9
+ OntologyDetailed,
10
+ OntologyMeta,
11
+ OntologyProperty,
12
+ OntologyQueryHint,
13
+ } from "../../shared/types/index.js";
14
+ import buildClassTree, { flattenClassTree } from "../helpers/buildClassTree.js";
4
15
  import queryClasses from "../helpers/queryClasses.js";
16
+ import type { RawOntologyProperty } from "../helpers/queryProperties.js";
5
17
  import queryProperties from "../helpers/queryProperties.js";
18
+ import {
19
+ queryConstraints,
20
+ queryInstanceCounts,
21
+ queryOntologyMeta,
22
+ querySampleInstances,
23
+ } from "../helpers/queryTboxFacts.js";
6
24
  import resolvePrefix from "../helpers/resolvePrefix.js";
7
25
 
26
+ /** Options for {@link showOntology}. */
27
+ export interface ShowOntologyOptions {
28
+ /** Class to deep-dive into (label, local name, or compact IRI). */
29
+ readonly class?: string;
30
+ }
31
+
8
32
  /**
9
- * Returns detailed schema for a namespace, including its classes and properties.
33
+ * Returns the complete TBox structure for a namespace: the class hierarchy
34
+ * (topologically ordered, direct properties attached, instance counts),
35
+ * unattached properties, SHACL constraint summaries, the owl:Ontology
36
+ * header, and — when `options.class` is given — a per-class deep dive.
37
+ *
38
+ * This is the **single** structure every output mode projects; plain,
39
+ * `--llm`, `--format json`, and the MCP envelope must render from it and
40
+ * nothing else. All IRIs are compacted against the store's prefix map,
41
+ * which is included in the result for lossless expansion.
10
42
  *
11
43
  * Accepts a short prefix (e.g. `ds`) or a full namespace URI.
12
44
  *
@@ -14,20 +46,29 @@ import resolvePrefix from "../helpers/resolvePrefix.js";
14
46
  *
15
47
  * @param store - The ke store to query.
16
48
  * @param prefixOrUri - A short prefix or full namespace URI.
49
+ * @param options - Optional class focus.
17
50
  * @returns An {@link OntologyDetailed} with classes and properties.
18
51
  * @throws PragmaError.invalidInput if the prefix or namespace is unknown.
19
- * @throws PragmaError.notFound if the namespace has no classes or properties.
52
+ * @throws PragmaError.notFound if the namespace has no classes or
53
+ * properties, or the focused class does not exist.
20
54
  */
21
55
  export default async function showOntology(
22
56
  store: Store,
23
57
  prefixOrUri: string,
58
+ options: ShowOntologyOptions = {},
24
59
  ): Promise<OntologyDetailed> {
25
60
  const { prefix, namespace } = resolvePrefix(prefixOrUri, store.prefixes);
26
61
 
27
- const classes = await queryClasses(store, namespace);
28
- const properties = await queryProperties(store, namespace);
62
+ const [rawClasses, rawProperties, instanceCounts, rawConstraints, rawMeta] =
63
+ await Promise.all([
64
+ queryClasses(store, namespace),
65
+ queryProperties(store, namespace),
66
+ queryInstanceCounts(store, namespace),
67
+ queryConstraints(store, namespace),
68
+ queryOntologyMeta(store, namespace),
69
+ ]);
29
70
 
30
- if (classes.length === 0 && properties.length === 0) {
71
+ if (rawClasses.length === 0 && rawProperties.length === 0) {
31
72
  throw PragmaError.notFound("ontology", prefixOrUri, {
32
73
  recovery: {
33
74
  message: "List loaded ontologies.",
@@ -37,5 +78,246 @@ export default async function showOntology(
37
78
  });
38
79
  }
39
80
 
40
- return { prefix, namespace, classes, properties };
81
+ // Core RDF vocabularies (xsd etc.) plus the store's resolved package
82
+ // prefixes; the store map wins on collisions.
83
+ const compactionMap: Readonly<Record<string, string>> = {
84
+ ...PREFIX_MAP,
85
+ ...store.prefixes,
86
+ };
87
+ const usedPrefixes = new Set<string>([prefix]);
88
+ const compact = (uri: string): string => {
89
+ const compacted = compactUri(uri, compactionMap);
90
+ const colon = compacted.indexOf(":");
91
+ if (compacted !== uri && colon > 0) {
92
+ usedPrefixes.add(compacted.slice(0, colon));
93
+ }
94
+ return compacted;
95
+ };
96
+
97
+ const toProperty = (raw: RawOntologyProperty): OntologyProperty => ({
98
+ iri: compact(raw.uri),
99
+ label: raw.label,
100
+ kind: raw.kind,
101
+ ...(raw.domain ? { domain: compact(raw.domain) } : {}),
102
+ ...(raw.range ? { range: compact(raw.range) } : {}),
103
+ ...(raw.functional ? { functional: true as const } : {}),
104
+ });
105
+
106
+ // Attach properties to their domain class; the rest are unattached.
107
+ const classUris = new Set(rawClasses.map((c) => c.uri));
108
+ const byDomain = new Map<string, OntologyProperty[]>();
109
+ const unattached: OntologyProperty[] = [];
110
+ for (const raw of rawProperties) {
111
+ if (raw.domain !== undefined && classUris.has(raw.domain)) {
112
+ const attached = byDomain.get(raw.domain) ?? [];
113
+ attached.push(toProperty(raw));
114
+ byDomain.set(raw.domain, attached);
115
+ } else {
116
+ unattached.push(toProperty(raw));
117
+ }
118
+ }
119
+
120
+ const unordered: OntologyClass[] = rawClasses.map((raw) => {
121
+ const instances = instanceCounts.get(raw.uri) ?? 0;
122
+ return {
123
+ iri: compact(raw.uri),
124
+ label: raw.label,
125
+ ...(raw.comment ? { comment: raw.comment } : {}),
126
+ subClassOf: raw.subClassOf.map(compact),
127
+ ...(instances > 0 ? { instances } : {}),
128
+ properties: (byDomain.get(raw.uri) ?? []).sort((a, b) =>
129
+ a.label.localeCompare(b.label),
130
+ ),
131
+ };
132
+ });
133
+
134
+ // Canonical ordering: pre-order walk of the tree every renderer shares.
135
+ const classes = flattenClassTree(buildClassTree(unordered));
136
+
137
+ const constraints: OntologyConstraint[] = rawConstraints.map((raw) => ({
138
+ shape: compact(raw.shape),
139
+ ...(raw.targetClass ? { targetClass: compact(raw.targetClass) } : {}),
140
+ propertyCount: raw.propertyCount,
141
+ }));
142
+
143
+ const meta: OntologyMeta | undefined = rawMeta
144
+ ? {
145
+ ...(rawMeta.title ? { title: rawMeta.title } : {}),
146
+ ...(rawMeta.version ? { version: rawMeta.version } : {}),
147
+ ...(rawMeta.imports.length > 0
148
+ ? { imports: rawMeta.imports.map(compact) }
149
+ : {}),
150
+ }
151
+ : undefined;
152
+
153
+ const focus = options.class
154
+ ? await buildFocus(store, options.class, {
155
+ prefix,
156
+ namespace,
157
+ classes,
158
+ unattached,
159
+ constraints,
160
+ instanceCounts,
161
+ rawClassUris: rawClasses.map((c) => c.uri),
162
+ compactionMap,
163
+ })
164
+ : undefined;
165
+
166
+ const prefixes = Object.fromEntries(
167
+ [...usedPrefixes]
168
+ .sort()
169
+ .map((p) => [p, compactionMap[p]])
170
+ .filter(([, ns]) => ns !== undefined),
171
+ );
172
+
173
+ return {
174
+ prefix,
175
+ namespace,
176
+ prefixes,
177
+ ...(meta ? { meta } : {}),
178
+ classes,
179
+ unattached: unattached.sort((a, b) => a.label.localeCompare(b.label)),
180
+ ...(constraints.length > 0 ? { constraints } : {}),
181
+ ...(focus ? { focus } : {}),
182
+ };
183
+ }
184
+
185
+ interface FocusContext {
186
+ readonly prefix: string;
187
+ readonly namespace: string;
188
+ readonly classes: readonly OntologyClass[];
189
+ readonly unattached: readonly OntologyProperty[];
190
+ readonly constraints: readonly OntologyConstraint[];
191
+ readonly instanceCounts: ReadonlyMap<string, number>;
192
+ readonly rawClassUris: readonly string[];
193
+ readonly compactionMap: Readonly<Record<string, string>>;
194
+ }
195
+
196
+ /**
197
+ * Assemble the per-class deep dive from the already-loaded structure plus
198
+ * one sample-instances query. Everything else (chain, subclasses, reverse
199
+ * references, inherited properties) derives from the namespace data.
200
+ */
201
+ async function buildFocus(
202
+ store: Store,
203
+ classQuery: string,
204
+ ctx: FocusContext,
205
+ ): Promise<OntologyClassFocus> {
206
+ const needle = classQuery.toLowerCase();
207
+ const target = ctx.classes.find(
208
+ (c) =>
209
+ c.iri.toLowerCase() === needle ||
210
+ c.label.toLowerCase() === needle ||
211
+ localName(c.iri).toLowerCase() === needle,
212
+ );
213
+
214
+ if (!target) {
215
+ throw PragmaError.notFound("class", classQuery, {
216
+ suggestions: ctx.classes
217
+ .map((c) => localName(c.iri))
218
+ .filter((name) => name.toLowerCase().includes(needle))
219
+ .slice(0, 5),
220
+ recovery: {
221
+ message: "Show the namespace's class hierarchy.",
222
+ cli: `pragma ontology show ${ctx.prefix}`,
223
+ mcp: { tool: "ontology_show", params: { prefix: ctx.prefix } },
224
+ },
225
+ });
226
+ }
227
+
228
+ const byIri = new Map(ctx.classes.map((c) => [c.iri, c]));
229
+
230
+ // Superclass chain within the namespace, nearest first, cycle-safe.
231
+ const superChain: string[] = [];
232
+ let cursor = target.subClassOf.find((p) => byIri.has(p));
233
+ while (cursor !== undefined && !superChain.includes(cursor)) {
234
+ superChain.push(cursor);
235
+ cursor = byIri.get(cursor)?.subClassOf.find((p) => byIri.has(p));
236
+ }
237
+
238
+ const inheritedProperties = superChain.flatMap(
239
+ (iri) => byIri.get(iri)?.properties ?? [],
240
+ );
241
+
242
+ const allProperties = [
243
+ ...ctx.classes.flatMap((c) => c.properties),
244
+ ...ctx.unattached,
245
+ ];
246
+
247
+ // The focused class's full URI is recoverable from the raw list (same
248
+ // index space as ctx.classes before ordering is irrelevant — match by
249
+ // compacted form).
250
+ const fullUri =
251
+ ctx.rawClassUris.find(
252
+ (uri) => compactUri(uri, ctx.compactionMap) === target.iri,
253
+ ) ?? target.iri;
254
+
255
+ const sampleInstances = (await querySampleInstances(store, fullUri)).map(
256
+ (uri) => compactUri(uri, ctx.compactionMap),
257
+ );
258
+
259
+ const referencedBy = allProperties
260
+ .filter((p) => p.range === target.iri && p.domain !== target.iri)
261
+ .sort((a, b) => a.label.localeCompare(b.label));
262
+
263
+ return {
264
+ iri: target.iri,
265
+ label: target.label,
266
+ ...(target.comment ? { comment: target.comment } : {}),
267
+ superChain,
268
+ subclasses: ctx.classes
269
+ .filter((c) => c.subClassOf.includes(target.iri))
270
+ .map((c) => c.iri)
271
+ .sort(),
272
+ instances: ctx.instanceCounts.get(fullUri) ?? 0,
273
+ directProperties: target.properties,
274
+ inheritedProperties,
275
+ referencedBy,
276
+ sampleInstances,
277
+ queries: buildQueryHints(target, referencedBy, sampleInstances),
278
+ };
279
+ }
280
+
281
+ /**
282
+ * Ready-to-run SPARQL for the natural follow-ups from a class deep dive.
283
+ * Compact IRIs are used throughout — `pragma graph query` resolves them
284
+ * against the store's prefix map, so each hint is directly pasteable.
285
+ */
286
+ function buildQueryHints(
287
+ target: OntologyClass,
288
+ referencedBy: readonly OntologyProperty[],
289
+ sampleInstances: readonly string[],
290
+ ): OntologyQueryHint[] {
291
+ const hints: OntologyQueryHint[] = [
292
+ {
293
+ label: `instances of ${target.iri}`,
294
+ sparql: `SELECT ?instance WHERE { ?instance a ${target.iri} } LIMIT 20`,
295
+ },
296
+ ];
297
+
298
+ const relation =
299
+ target.properties.find((p) => p.kind === "object") ??
300
+ referencedBy.find((p) => p.kind === "object");
301
+ if (relation) {
302
+ hints.push({
303
+ label: `traverse ${relation.iri}`,
304
+ sparql: `SELECT ?from ?to WHERE { ?from ${relation.iri} ?to } LIMIT 10`,
305
+ });
306
+ }
307
+
308
+ const sample = sampleInstances[0];
309
+ if (sample !== undefined) {
310
+ hints.push({
311
+ label: `everything about ${sample}`,
312
+ sparql: `SELECT ?predicate ?object WHERE { ${sample} ?predicate ?object }`,
313
+ });
314
+ }
315
+
316
+ return hints;
317
+ }
318
+
319
+ function localName(iri: string): string {
320
+ const colon = iri.lastIndexOf(":");
321
+ const slash = iri.lastIndexOf("/");
322
+ return iri.slice(Math.max(colon, slash) + 1);
41
323
  }
@@ -2,7 +2,7 @@ import type { Store } from "@canonical/ke";
2
2
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
3
  import { PragmaError } from "#error";
4
4
  import { createTestStore, DS_ALL_TTL } from "#testing";
5
- import { PREFIX_MAP } from "../../shared/prefixes.js";
5
+ import { DEFAULT_PREFIX_MAP } from "../../shared/prefixes.js";
6
6
  import showOntologyRaw from "./showOntologyRaw.js";
7
7
 
8
8
  let store: Store;
@@ -21,7 +21,9 @@ describe("showOntologyRaw", () => {
21
21
  const triples = await showOntologyRaw(store, "ds");
22
22
  expect(triples.length).toBeGreaterThan(0);
23
23
 
24
- const hasDs = triples.some((t) => t.subject.startsWith(PREFIX_MAP.ds));
24
+ const hasDs = triples.some((t) =>
25
+ t.subject.startsWith(DEFAULT_PREFIX_MAP.ds),
26
+ );
25
27
  expect(hasDs).toBe(true);
26
28
  });
27
29
 
@@ -19,15 +19,20 @@ import type {
19
19
  OntologyDetailed,
20
20
  OntologySummary,
21
21
  } from "../shared/types/index.js";
22
+ import type { OntologyShowInput } from "./formatters/index.js";
22
23
  import { listFormatters, showFormatters } from "./formatters/index.js";
24
+ import completeClassNames from "./helpers/completeClassNames.js";
25
+ import expandOntologyIris from "./helpers/expandOntologyIris.js";
23
26
  import { listOntologies, showOntology } from "./operations/index.js";
24
27
 
25
28
  const ontologyListColumns: readonly ColumnDef<OntologySummary>[] = [
26
29
  { key: "prefix", label: "Prefix" },
27
30
  { key: "namespace", label: "Namespace" },
31
+ { key: "version", label: "Version" },
28
32
  { key: "classCount", label: "Classes" },
29
- { key: "propertyCount", label: "Properties" },
30
- { key: "anatomyCount", label: "Anatomies" },
33
+ { key: "relationCount", label: "Relations" },
34
+ { key: "attributeCount", label: "Attributes" },
35
+ { key: "shapeCount", label: "Shapes" },
31
36
  ];
32
37
 
33
38
  /** The `ontology list` / `ontology_list` read story. */
@@ -67,13 +72,13 @@ export const ontologyListStory: ReadStory<
67
72
  };
68
73
 
69
74
  /** The `ontology show <prefix>` / `ontology_show` read story. */
70
- export const ontologyShowStory: ReadStory<OntologyDetailed, OntologyDetailed> =
75
+ export const ontologyShowStory: ReadStory<OntologyDetailed, OntologyShowInput> =
71
76
  {
72
77
  noun: "ontology",
73
78
  verb: "show",
74
79
  description: "Show ontology schema details",
75
80
  toolDescription:
76
- "Show detailed schema for an ontology including classes and properties.",
81
+ "Show the TBox for a namespace: class hierarchy with instance counts and relations, plus metadata and constraints.",
77
82
  params: [
78
83
  {
79
84
  name: "prefix",
@@ -86,13 +91,53 @@ export const ontologyShowStory: ReadStory<OntologyDetailed, OntologyDetailed> =
86
91
  const all = await listOntologies(ctx.store);
87
92
  return all
88
93
  .map((o) => o.prefix)
89
- .filter((p) => p.toLowerCase().startsWith(partial.toLowerCase()));
94
+ .filter(
95
+ (p) =>
96
+ p.length > 0 &&
97
+ p.toLowerCase().startsWith(partial.toLowerCase()),
98
+ );
90
99
  },
91
100
  },
101
+ {
102
+ name: "class",
103
+ type: "string",
104
+ description:
105
+ "Deep-dive into one class (label, local name, or compact IRI)",
106
+ toolDescription:
107
+ "Class to deep-dive into: super chain, direct + inherited properties, reverse references, sample instances, and follow-up queries",
108
+ complete: async (partial, cmdCtx) => {
109
+ const ctx = requirePragmaContext(cmdCtx);
110
+ return completeClassNames(ctx.store, partial);
111
+ },
112
+ },
113
+ {
114
+ name: "properties",
115
+ type: "boolean",
116
+ description:
117
+ "Include datatype properties (attributes); default shows relations only",
118
+ },
119
+ {
120
+ name: "fullUris",
121
+ type: "boolean",
122
+ description: "Show full URIs instead of compact prefixed IRIs",
123
+ },
124
+ ],
125
+ examples: [
126
+ "pragma ontology show ds",
127
+ "pragma ontology show ds --properties",
128
+ "pragma ontology show ds --full-uris --format json",
129
+ "pragma ontology show ds --class Component",
92
130
  ],
93
- examples: ["pragma ontology show ds", "pragma ontology show cs --llm"],
94
- resolve: (rt, params) => showOntology(rt.store, params.prefix as string),
95
- toOutput: (ontology) => ontology,
131
+ resolve: (rt, params) =>
132
+ showOntology(rt.store, params.prefix as string, {
133
+ class: params.class as string | undefined,
134
+ }),
135
+ // --full-uris is applied here, once, so every projection (plain, llm,
136
+ // json, MCP condensed) inherits the same encoding from the same place.
137
+ toOutput: (ontology, params) => ({
138
+ ontology: params.fullUris ? expandOntologyIris(ontology) : ontology,
139
+ showProperties: Boolean(params.properties),
140
+ }),
96
141
  formatters: showFormatters,
97
142
  toEnvelope: (ontology) => ({ data: ontology }),
98
143
  guardParams: (params) =>
@@ -11,7 +11,6 @@ import { readConfig } from "#config";
11
11
  import { mergeAndParseRefs } from "../../shared/mergeAndParseRefs.js";
12
12
  import { cloneRef, fetchRef, pruneCache } from "./gitOps.js";
13
13
  import type { PackageRef, RawPackageEntry } from "./parseRef.js";
14
- import { parsePackageEntry } from "./parseRef.js";
15
14
  import { cacheRoot, gitCacheDir } from "./paths.js";
16
15
 
17
16
  // ---------------------------------------------------------------------------
@@ -3,4 +3,5 @@ export { default as allCommand } from "./all.js";
3
3
  export { default as completionsCommand } from "./completions.js";
4
4
  export { default as lspCommand } from "./lsp.js";
5
5
  export { default as mcpCommand } from "./mcp.js";
6
+ export { default as setupCommand } from "./setup.js";
6
7
  export { default as skillsCommand } from "./skills.js";