@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,104 +2,395 @@ import chalk from "chalk";
2
2
  // Deep import, not #pipeline: the barrel re-exports collectCommands, which
3
3
  // imports every domain barrel — including this one — so #pipeline here forms
4
4
  // a module cycle that evaluates this domain's stories as undefined at load.
5
- import {
6
- formatHeading,
7
- formatSection,
8
- } from "../../../pipeline/formatTerminal.js";
9
- import compactUri from "../../shared/compactUri.js";
5
+ import { formatHeading } from "../../../pipeline/formatTerminal.js";
10
6
  import type { Formatters } from "../../shared/formatters.js";
11
- import { PREFIX_MAP } from "../../shared/prefixes.js";
12
- import type { OntologyDetailed } from "../../shared/types/index.js";
7
+ import type {
8
+ OntologyClassFocus,
9
+ OntologyDetailed,
10
+ OntologyProperty,
11
+ } from "../../shared/types/index.js";
12
+ import type { ClassTreeNode } from "../helpers/buildClassTree.js";
13
+ import buildClassTree from "../helpers/buildClassTree.js";
14
+
15
+ /**
16
+ * Formatter input for `ontology show`: the one complete structure plus the
17
+ * projection controls resolved from the story params. `--full-uris` is
18
+ * already applied to `ontology` by the story's `toOutput`, so the renderers
19
+ * below never re-encode IRIs.
20
+ */
21
+ export interface OntologyShowInput {
22
+ readonly ontology: OntologyDetailed;
23
+ /** Include datatype properties (attributes); default renders relations only. */
24
+ readonly showProperties: boolean;
25
+ }
13
26
 
14
27
  /**
15
28
  * Formatters for `pragma ontology show` output.
16
29
  *
17
- * - **plain**: chalk-styled heading with Classes and Properties sections.
18
- * - **llm**: condensed Markdown with `### Classes` and `### Properties` sub-headings.
19
- * - **json**: serialized {@link OntologyDetailed} as indented JSON.
30
+ * All three modes are projections of the same {@link OntologyDetailed}
31
+ * structure — they may style differently, but every fact they print comes
32
+ * from that one shape, and the hierarchy is derived through the single
33
+ * shared {@link buildClassTree} helper so it can never diverge between
34
+ * output modes.
35
+ *
36
+ * - **plain**: chalk-styled header (name, metadata, counts), class
37
+ * hierarchy tree with instance counts and inline relations, constraints,
38
+ * and a next-command footer. Attributes render only with `--properties`.
39
+ * Renders the class deep dive when `focus` is present.
40
+ * - **llm**: the same content as condensed Markdown.
41
+ * - **json**: the complete {@link OntologyDetailed} structure itself
42
+ * (always everything — display flags never subset the canonical data).
20
43
  */
21
- const formatters: Formatters<OntologyDetailed> = {
22
- plain(data) {
23
- const lines: string[] = [];
44
+ const formatters: Formatters<OntologyShowInput> = {
45
+ plain({ ontology, showProperties }) {
46
+ if (ontology.focus) return renderFocusPlain(ontology.focus);
24
47
 
48
+ const lines: string[] = [];
25
49
  lines.push(
26
- formatHeading(
27
- `${data.prefix}: ${compactUri(data.namespace, PREFIX_MAP)}`,
28
- ),
50
+ formatHeading(`Ontology ${ontology.prefix}: — ${ontology.namespace}`),
29
51
  );
52
+ lines.push(chalk.dim(summaryLine(ontology)));
53
+ if (ontology.meta?.imports?.length) {
54
+ lines.push(chalk.dim(`imports: ${ontology.meta.imports.join(", ")}`));
55
+ }
30
56
 
31
- if (data.classes.length > 0) {
32
- const classLines = data.classes
33
- .map((c) => {
34
- const sup = c.superclass
35
- ? chalk.dim(` extends ${compactUri(c.superclass, PREFIX_MAP)}`)
36
- : "";
37
- return ` ${chalk.bold(c.label)} ${chalk.dim(`(${compactUri(c.uri, PREFIX_MAP)})`)}${sup}`;
38
- })
39
- .join("\n");
57
+ if (ontology.classes.length > 0) {
40
58
  lines.push("");
41
- lines.push(formatSection("Classes", classLines));
59
+ for (const root of buildClassTree(ontology.classes)) {
60
+ renderTreePlain(root, "", "", showProperties, lines);
61
+ }
42
62
  }
43
63
 
44
- if (data.properties.length > 0) {
45
- const propLines = data.properties
46
- .map((p) => {
47
- const domain = p.domain
48
- ? chalk.dim(` domain: ${compactUri(p.domain, PREFIX_MAP)}`)
49
- : "";
50
- const range = p.range
51
- ? chalk.dim(` range: ${compactUri(p.range, PREFIX_MAP)}`)
52
- : "";
53
- return ` ${chalk.bold(p.label)} ${chalk.dim(`(${compactUri(p.uri, PREFIX_MAP)})`)} (${p.type})${domain}${range}`;
54
- })
55
- .join("\n");
64
+ const looseRelations = unattachedToShow(ontology, showProperties);
65
+ if (looseRelations.length > 0) {
56
66
  lines.push("");
57
- lines.push(formatSection("Properties", propLines));
67
+ lines.push(
68
+ `${chalk.bold("Unattached:")} ${looseRelations
69
+ .map((p) => propLine(p, showProperties))
70
+ .join(", ")}`,
71
+ );
58
72
  }
59
73
 
74
+ if (ontology.constraints && ontology.constraints.length > 0) {
75
+ lines.push("");
76
+ lines.push(chalk.bold("Constraints:"));
77
+ for (const c of ontology.constraints) {
78
+ const target = c.targetClass ? ` on ${c.targetClass}` : "";
79
+ lines.push(
80
+ ` ${c.shape}${target} ${chalk.dim(
81
+ `(${c.propertyCount} property constraints)`,
82
+ )}`,
83
+ );
84
+ }
85
+ }
86
+
87
+ lines.push("");
88
+ lines.push(chalk.dim(footer(ontology)));
89
+
60
90
  return lines.join("\n");
61
91
  },
62
92
 
63
- llm(data) {
64
- const lines = [
65
- `## ${data.prefix}: ${compactUri(data.namespace, PREFIX_MAP)}`,
66
- "",
67
- ];
93
+ llm({ ontology, showProperties }) {
94
+ if (ontology.focus) return renderFocusLlm(ontology.focus);
68
95
 
69
- if (data.classes.length > 0) {
96
+ const lines: string[] = [];
97
+ lines.push(`## Ontology ${ontology.prefix}: — ${ontology.namespace}`);
98
+ lines.push(summaryLine(ontology));
99
+ if (ontology.meta?.imports?.length) {
100
+ lines.push(`imports: ${ontology.meta.imports.join(", ")}`);
101
+ }
102
+ lines.push("");
103
+
104
+ if (ontology.classes.length > 0) {
70
105
  lines.push("### Classes");
71
- for (const c of data.classes) {
72
- const sup = c.superclass
73
- ? ` extends ${compactUri(c.superclass, PREFIX_MAP)}`
74
- : "";
75
- lines.push(
76
- `- \`${compactUri(c.uri, PREFIX_MAP)}\` — **${c.label}**${sup}`,
77
- );
106
+ lines.push("```");
107
+ for (const root of buildClassTree(ontology.classes)) {
108
+ renderTreeLlm(root, 0, showProperties, lines);
78
109
  }
79
- lines.push("");
110
+ lines.push("```");
80
111
  }
81
112
 
82
- if (data.properties.length > 0) {
83
- lines.push("### Properties");
84
- for (const p of data.properties) {
85
- const domain = p.domain
86
- ? ` domain: ${compactUri(p.domain, PREFIX_MAP)}`
87
- : "";
88
- const range = p.range
89
- ? ` range: ${compactUri(p.range, PREFIX_MAP)}`
90
- : "";
91
- lines.push(
92
- `- \`${compactUri(p.uri, PREFIX_MAP)}\` — **${p.label}** (${p.type})${domain}${range}`,
93
- );
113
+ const looseRelations = unattachedToShow(ontology, showProperties);
114
+ if (looseRelations.length > 0) {
115
+ lines.push(
116
+ `Unattached: ${looseRelations
117
+ .map((p) => propLine(p, showProperties))
118
+ .join(", ")}`,
119
+ );
120
+ }
121
+
122
+ if (ontology.constraints && ontology.constraints.length > 0) {
123
+ lines.push("### Constraints");
124
+ for (const c of ontology.constraints) {
125
+ const target = c.targetClass ? ` targets ${c.targetClass}` : "";
126
+ lines.push(`- ${c.shape}${target} (${c.propertyCount} constraints)`);
94
127
  }
95
128
  }
96
129
 
130
+ lines.push("");
131
+ lines.push(footer(ontology));
132
+
97
133
  return lines.join("\n");
98
134
  },
99
135
 
100
- json(data) {
101
- return JSON.stringify(data, null, 2);
136
+ json({ ontology }) {
137
+ return JSON.stringify(ontology, null, 2);
102
138
  },
103
139
  };
104
140
 
105
141
  export default formatters;
142
+
143
+ // ---------------------------------------------------------------------------
144
+ // Shared fragments
145
+ // ---------------------------------------------------------------------------
146
+
147
+ function allProperties(ontology: OntologyDetailed): OntologyProperty[] {
148
+ return [
149
+ ...ontology.classes.flatMap((c) => c.properties),
150
+ ...ontology.unattached,
151
+ ];
152
+ }
153
+
154
+ function summaryLine(ontology: OntologyDetailed): string {
155
+ const props = allProperties(ontology);
156
+ const relations = props.filter((p) => p.kind === "object").length;
157
+ const attributes = props.length - relations;
158
+ const parts = [
159
+ `${ontology.classes.length} classes`,
160
+ `${relations} relations`,
161
+ `${attributes} attributes`,
162
+ ];
163
+ if (ontology.constraints?.length) {
164
+ parts.push(`${ontology.constraints.length} shapes`);
165
+ }
166
+ if (ontology.meta?.version) parts.push(`v${ontology.meta.version}`);
167
+ if (ontology.meta?.title) parts.push(ontology.meta.title);
168
+ return parts.join(" · ");
169
+ }
170
+
171
+ /**
172
+ * The next-step footer: the class deep dive, plus a runnable query seeded
173
+ * with a real class from this namespace so follow-up querying starts from
174
+ * copy-paste, not from reading SPARQL docs.
175
+ */
176
+ function footer(ontology: OntologyDetailed): string {
177
+ const seed =
178
+ ontology.classes.find((c) => (c.instances ?? 0) > 0) ?? ontology.classes[0];
179
+ const query = seed
180
+ ? ` · pragma graph query "SELECT ?s WHERE { ?s a ${sparqlTerm(seed.iri)} } LIMIT 10"`
181
+ : "";
182
+ return `Next: pragma ontology show ${ontology.prefix} --class <Class>${query}`;
183
+ }
184
+
185
+ /**
186
+ * Render an IRI as a valid SPARQL term: full URIs (present under
187
+ * `--full-uris`) need `<...>`; compact prefixed IRIs are usable as-is.
188
+ */
189
+ function sparqlTerm(iri: string): string {
190
+ return iri.startsWith("http://") || iri.startsWith("https://")
191
+ ? `<${iri}>`
192
+ : iri;
193
+ }
194
+
195
+ /** Properties a class renders inline: relations always, attributes on demand. */
196
+ function inlineProperties(
197
+ props: readonly OntologyProperty[],
198
+ showProperties: boolean,
199
+ ): readonly OntologyProperty[] {
200
+ return showProperties ? props : props.filter((p) => p.kind === "object");
201
+ }
202
+
203
+ /** Unattached entries to display under the current verbosity. */
204
+ function unattachedToShow(
205
+ ontology: OntologyDetailed,
206
+ showProperties: boolean,
207
+ ): readonly OntologyProperty[] {
208
+ return inlineProperties(ontology.unattached, showProperties);
209
+ }
210
+
211
+ /** One class's inline property summary: `tier →ds:Tier, …(+2 more)`. */
212
+ function inlineProps(
213
+ props: readonly OntologyProperty[],
214
+ showProperties: boolean,
215
+ max = 4,
216
+ ): string {
217
+ const shown = inlineProperties(props, showProperties);
218
+ if (shown.length === 0) return "";
219
+ const rendered = shown.slice(0, max).map((p) => propLine(p, showProperties));
220
+ const extra = shown.length - max;
221
+ return ` — ${rendered.join(", ")}${extra > 0 ? `, …(+${extra} more)` : ""}`;
222
+ }
223
+
224
+ function propLine(p: OntologyProperty, showProperties: boolean): string {
225
+ const range =
226
+ p.kind === "object" && p.range
227
+ ? ` →${p.range}`
228
+ : showProperties && p.range
229
+ ? `: ${p.range}`
230
+ : "";
231
+ return `${p.label}${range}`;
232
+ }
233
+
234
+ function classSuffix(node: ClassTreeNode): string {
235
+ const count = node.cls.instances ? ` (${node.cls.instances})` : "";
236
+ const also =
237
+ node.alsoExtends.length > 0
238
+ ? ` [also extends ${node.alsoExtends.join(", ")}]`
239
+ : "";
240
+ return `${count}${also}`;
241
+ }
242
+
243
+ // ---------------------------------------------------------------------------
244
+ // Tree renderers (both derive from the same buildClassTree forest)
245
+ // ---------------------------------------------------------------------------
246
+
247
+ function renderTreePlain(
248
+ node: ClassTreeNode,
249
+ prefix: string,
250
+ childIndent: string,
251
+ showProperties: boolean,
252
+ lines: string[],
253
+ ): void {
254
+ const suffix = chalk.dim(
255
+ `${classSuffix(node)}${inlineProps(node.cls.properties, showProperties)}`,
256
+ );
257
+ lines.push(`${prefix}${chalk.bold(node.cls.label)}${suffix}`);
258
+
259
+ node.children.forEach((child, i) => {
260
+ const last = i === node.children.length - 1;
261
+ renderTreePlain(
262
+ child,
263
+ `${childIndent}${last ? "└─ " : "├─ "}`,
264
+ `${childIndent}${last ? " " : "│ "}`,
265
+ showProperties,
266
+ lines,
267
+ );
268
+ });
269
+ }
270
+
271
+ function renderTreeLlm(
272
+ node: ClassTreeNode,
273
+ depth: number,
274
+ showProperties: boolean,
275
+ lines: string[],
276
+ ): void {
277
+ const pad = " ".repeat(depth);
278
+ lines.push(
279
+ `${pad}${node.cls.iri}${classSuffix(node)}${inlineProps(node.cls.properties, showProperties)}`,
280
+ );
281
+ for (const child of node.children) {
282
+ renderTreeLlm(child, depth + 1, showProperties, lines);
283
+ }
284
+ }
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Focus (per-class deep dive) renderers — always full detail
288
+ // ---------------------------------------------------------------------------
289
+
290
+ function renderFocusPlain(f: OntologyClassFocus): string {
291
+ const lines: string[] = [];
292
+
293
+ lines.push(formatHeading(`${f.label} — ${f.iri}`));
294
+ if (f.comment) lines.push(f.comment);
295
+ const chain =
296
+ f.superChain.length > 0 ? f.superChain.join(" < ") : "(root class)";
297
+ lines.push(chalk.dim(`extends: ${chain} · instances: ${f.instances}`));
298
+
299
+ pushPropSection(lines, "Relations", relationsOf(f.directProperties));
300
+ pushPropSection(lines, "Attributes", attributesOf(f.directProperties));
301
+ pushPropSection(lines, "Inherited", f.inheritedProperties);
302
+ pushPropSection(lines, "Referenced by", f.referencedBy);
303
+
304
+ if (f.subclasses.length > 0) {
305
+ lines.push("");
306
+ lines.push(`${chalk.bold("Subclasses:")} ${f.subclasses.join(", ")}`);
307
+ }
308
+
309
+ if (f.sampleInstances.length > 0) {
310
+ lines.push("");
311
+ lines.push(`${chalk.bold("Instances:")} ${f.sampleInstances.join(", ")}`);
312
+ }
313
+
314
+ if (f.queries.length > 0) {
315
+ lines.push("");
316
+ lines.push(chalk.bold("Queries:"));
317
+ for (const q of f.queries) {
318
+ lines.push(chalk.dim(` # ${q.label}`));
319
+ lines.push(` pragma graph query "${q.sparql}"`);
320
+ }
321
+ }
322
+
323
+ return lines.join("\n");
324
+ }
325
+
326
+ function renderFocusLlm(f: OntologyClassFocus): string {
327
+ const lines: string[] = [];
328
+
329
+ lines.push(`## ${f.iri} — ${f.label}`);
330
+ if (f.comment) lines.push(f.comment);
331
+ const chain =
332
+ f.superChain.length > 0 ? f.superChain.join(" < ") : "(root class)";
333
+ lines.push(`- extends: ${chain}`);
334
+ lines.push(`- instances: ${f.instances}`);
335
+ if (f.subclasses.length > 0) {
336
+ lines.push(`- subclasses: ${f.subclasses.join(", ")}`);
337
+ }
338
+
339
+ const section = (title: string, props: readonly OntologyProperty[]) => {
340
+ if (props.length === 0) return;
341
+ lines.push(`### ${title}`);
342
+ for (const p of props) {
343
+ lines.push(`- ${fullPropLine(p)}`);
344
+ }
345
+ };
346
+ section("Relations", relationsOf(f.directProperties));
347
+ section("Attributes", attributesOf(f.directProperties));
348
+ section("Inherited", f.inheritedProperties);
349
+ section("Referenced by", f.referencedBy);
350
+
351
+ if (f.sampleInstances.length > 0) {
352
+ lines.push("### Instances");
353
+ for (const iri of f.sampleInstances) lines.push(`- ${iri}`);
354
+ }
355
+
356
+ if (f.queries.length > 0) {
357
+ lines.push("### Queries");
358
+ for (const q of f.queries) {
359
+ lines.push(`- ${q.label}: \`${q.sparql}\``);
360
+ }
361
+ }
362
+
363
+ return lines.join("\n");
364
+ }
365
+
366
+ function relationsOf(
367
+ props: readonly OntologyProperty[],
368
+ ): readonly OntologyProperty[] {
369
+ return props.filter((p) => p.kind === "object");
370
+ }
371
+
372
+ function attributesOf(
373
+ props: readonly OntologyProperty[],
374
+ ): readonly OntologyProperty[] {
375
+ return props.filter((p) => p.kind === "datatype");
376
+ }
377
+
378
+ function pushPropSection(
379
+ lines: string[],
380
+ title: string,
381
+ props: readonly OntologyProperty[],
382
+ ): void {
383
+ if (props.length === 0) return;
384
+ lines.push("");
385
+ lines.push(chalk.bold(`${title}:`));
386
+ for (const p of props) {
387
+ lines.push(` ${fullPropLine(p)}`);
388
+ }
389
+ }
390
+
391
+ function fullPropLine(p: OntologyProperty): string {
392
+ const range = p.range ? ` → ${p.range}` : "";
393
+ const from = p.domain ? ` (on ${p.domain})` : "";
394
+ const functional = p.functional ? " [functional]" : "";
395
+ return `${p.iri} (${p.kind})${range}${from}${functional}`;
396
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Pure class-hierarchy builder shared by every `ontology show` projection.
3
+ *
4
+ * The {@link OntologyDetailed} data shape keeps classes as a flat,
5
+ * topologically ordered array with `subClassOf` edges (canonical for JSON
6
+ * and MCP consumers); the plain and llm renderers both derive their tree
7
+ * from this single implementation so the hierarchy can never diverge
8
+ * between output modes.
9
+ */
10
+
11
+ import type { OntologyClass } from "../../shared/types/index.js";
12
+
13
+ /** A class positioned in the rendered hierarchy. */
14
+ export interface ClassTreeNode {
15
+ readonly cls: OntologyClass;
16
+ readonly children: readonly ClassTreeNode[];
17
+ /**
18
+ * Superclasses beyond the one used for tree placement (multiple
19
+ * inheritance) — renderers annotate these as "also extends".
20
+ */
21
+ readonly alsoExtends: readonly string[];
22
+ }
23
+
24
+ /**
25
+ * Arrange classes into a forest.
26
+ *
27
+ * Roots are classes with no in-set parent; children sort alphabetically by
28
+ * label. A class with several in-set parents is placed under its first
29
+ * (alphabetically sorted) parent and carries the rest in `alsoExtends`.
30
+ * Cycle-safe: an edge that would revisit a placed class is ignored, so a
31
+ * cyclic ontology degrades to a flat listing rather than recursing.
32
+ *
33
+ * @param classes - Flat class array (any order; `subClassOf` holds compact
34
+ * or full IRIs — matching whatever `iri` uses).
35
+ * @returns Root nodes in alphabetical label order.
36
+ */
37
+ export default function buildClassTree(
38
+ classes: readonly OntologyClass[],
39
+ ): ClassTreeNode[] {
40
+ const byIri = new Map(classes.map((c) => [c.iri, c]));
41
+ const placed = new Set<string>();
42
+
43
+ const inSetParents = (cls: OntologyClass): string[] =>
44
+ cls.subClassOf.filter((parent) => byIri.has(parent));
45
+
46
+ const childrenOf = (parentIri: string): OntologyClass[] =>
47
+ classes
48
+ .filter((c) => inSetParents(c)[0] === parentIri)
49
+ .sort((a, b) => a.label.localeCompare(b.label));
50
+
51
+ const build = (cls: OntologyClass): ClassTreeNode => {
52
+ placed.add(cls.iri);
53
+ const parents = inSetParents(cls);
54
+ return {
55
+ cls,
56
+ alsoExtends: parents.slice(1),
57
+ children: childrenOf(cls.iri)
58
+ .filter((child) => !placed.has(child.iri))
59
+ .map(build),
60
+ };
61
+ };
62
+
63
+ const roots = classes
64
+ .filter((c) => inSetParents(c).length === 0)
65
+ .sort((a, b) => a.label.localeCompare(b.label))
66
+ .map(build);
67
+
68
+ // Cycle fallback: anything unreachable from a root (e.g. mutually
69
+ // extending classes) is appended as an additional root.
70
+ const leftovers = classes
71
+ .filter((c) => !placed.has(c.iri))
72
+ .sort((a, b) => a.label.localeCompare(b.label))
73
+ .map(build);
74
+
75
+ return [...roots, ...leftovers];
76
+ }
77
+
78
+ /** Flatten a forest to pre-order (the shape's canonical class ordering). */
79
+ export function flattenClassTree(
80
+ nodes: readonly ClassTreeNode[],
81
+ ): OntologyClass[] {
82
+ const out: OntologyClass[] = [];
83
+ const walk = (node: ClassTreeNode): void => {
84
+ out.push(node.cls);
85
+ for (const child of node.children) walk(child);
86
+ };
87
+ for (const node of nodes) walk(node);
88
+ return out;
89
+ }
@@ -0,0 +1,43 @@
1
+ import type { Store } from "@canonical/ke";
2
+ import { buildQuery } from "../../shared/buildQuery.js";
3
+ import extractLocalName from "../../shared/extractLocalName.js";
4
+ import { P } from "../../shared/prefixes.js";
5
+
6
+ /**
7
+ * Tab-completion candidates for the `--class` parameter of
8
+ * `ontology show`: the local names of every `owl:Class` in the store,
9
+ * filtered by the typed partial (case-insensitive prefix match).
10
+ *
11
+ * Completion runs before the prefix positional is resolvable from the
12
+ * completion context, so candidates span all namespaces — the operation
13
+ * scopes the actual lookup to the requested namespace. Results are capped
14
+ * at `limit` so a huge graph cannot flood the shell; an empty partial
15
+ * still completes (class sets are small and the query is one indexed
16
+ * type-scan).
17
+ *
18
+ * @note Queries ke store
19
+ */
20
+ export default async function completeClassNames(
21
+ store: Store,
22
+ partial: string,
23
+ limit = 50,
24
+ ): Promise<string[]> {
25
+ const result = await store.query(
26
+ buildQuery(`
27
+ SELECT ?class
28
+ WHERE { ?class a ${P.owl}Class }
29
+ `),
30
+ );
31
+
32
+ if (result.type !== "select") return [];
33
+
34
+ const needle = partial.toLowerCase();
35
+ const names = new Set<string>();
36
+ for (const b of result.bindings) {
37
+ if (!b.class) continue;
38
+ const local = extractLocalName(b.class);
39
+ if (local.toLowerCase().startsWith(needle)) names.add(local);
40
+ }
41
+
42
+ return [...names].sort().slice(0, limit);
43
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Expand every compact IRI in an {@link OntologyDetailed} structure back to
3
+ * its full URI using the structure's own `prefixes` map.
4
+ *
5
+ * The data shape keeps compact IRIs as its canonical encoding; `--full-uris`
6
+ * applies this pure transform once in the story's `toOutput`, so every
7
+ * projection (plain, llm, json, MCP condensed) inherits the expansion from
8
+ * the same place. Query hints are left untouched: their compact IRIs are
9
+ * resolvable by `pragma graph query` regardless of the display encoding.
10
+ */
11
+
12
+ import type {
13
+ OntologyClass,
14
+ OntologyClassFocus,
15
+ OntologyDetailed,
16
+ OntologyProperty,
17
+ } from "../../shared/types/index.js";
18
+
19
+ export default function expandOntologyIris(
20
+ ontology: OntologyDetailed,
21
+ ): OntologyDetailed {
22
+ const expand = (iri: string): string => {
23
+ const colon = iri.indexOf(":");
24
+ if (colon <= 0) return iri;
25
+ const namespace = ontology.prefixes[iri.slice(0, colon)];
26
+ return namespace === undefined
27
+ ? iri
28
+ : `${namespace}${iri.slice(colon + 1)}`;
29
+ };
30
+
31
+ const expandProperty = (p: OntologyProperty): OntologyProperty => ({
32
+ ...p,
33
+ iri: expand(p.iri),
34
+ ...(p.domain ? { domain: expand(p.domain) } : {}),
35
+ ...(p.range ? { range: expand(p.range) } : {}),
36
+ });
37
+
38
+ const expandClass = (c: OntologyClass): OntologyClass => ({
39
+ ...c,
40
+ iri: expand(c.iri),
41
+ subClassOf: c.subClassOf.map(expand),
42
+ properties: c.properties.map(expandProperty),
43
+ });
44
+
45
+ const expandFocus = (f: OntologyClassFocus): OntologyClassFocus => ({
46
+ ...f,
47
+ iri: expand(f.iri),
48
+ superChain: f.superChain.map(expand),
49
+ subclasses: f.subclasses.map(expand),
50
+ directProperties: f.directProperties.map(expandProperty),
51
+ inheritedProperties: f.inheritedProperties.map(expandProperty),
52
+ referencedBy: f.referencedBy.map(expandProperty),
53
+ sampleInstances: f.sampleInstances.map(expand),
54
+ });
55
+
56
+ return {
57
+ ...ontology,
58
+ ...(ontology.meta?.imports
59
+ ? {
60
+ meta: {
61
+ ...ontology.meta,
62
+ imports: ontology.meta.imports.map(expand),
63
+ },
64
+ }
65
+ : {}),
66
+ classes: ontology.classes.map(expandClass),
67
+ unattached: ontology.unattached.map(expandProperty),
68
+ ...(ontology.constraints
69
+ ? {
70
+ constraints: ontology.constraints.map((c) => ({
71
+ ...c,
72
+ shape: expand(c.shape),
73
+ ...(c.targetClass ? { targetClass: expand(c.targetClass) } : {}),
74
+ })),
75
+ }
76
+ : {}),
77
+ ...(ontology.focus ? { focus: expandFocus(ontology.focus) } : {}),
78
+ };
79
+ }