@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
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Wires the top-level `pragma setup` command.
3
+ *
4
+ * `setup` is one level more general than `create <thing>`: it configures the
5
+ * whole local environment (completions, LSP, MCP). Unlike `create`, its steps
6
+ * perform *read* effects whose results drive control flow — detecting AI
7
+ * harnesses, checking what is already installed — so it must run for real and
8
+ * report the real outcome. It therefore executes through {@link runSetupTask}
9
+ * (real execution, streamed output) rather than the generator preview-then-
10
+ * execute path, which would render a dry-run preview whose stubbed reads report
11
+ * the wrong state (e.g. "no harnesses detected").
12
+ */
13
+
14
+ import type { CommandDefinition, CommandResult } from "@canonical/cli-core";
15
+ import runSetupTask from "../helpers/runSetupTask.js";
16
+ import setupAll from "../operations/setupAll.js";
17
+
18
+ /**
19
+ * Build the top-level `pragma setup` command definition.
20
+ *
21
+ * @returns The command definition for `pragma setup`.
22
+ * @note Impure
23
+ */
24
+ export default function buildSetupCommand(): CommandDefinition {
25
+ return {
26
+ path: ["setup"],
27
+ description:
28
+ "Configure this project's environment: completions, LSP, and MCP",
29
+ parameters: [
30
+ {
31
+ name: "dryRun",
32
+ description: "Preview changes without writing",
33
+ type: "boolean",
34
+ default: false,
35
+ },
36
+ {
37
+ name: "undo",
38
+ description: "Reverse a previous setup",
39
+ type: "boolean",
40
+ default: false,
41
+ },
42
+ {
43
+ name: "yes",
44
+ description: "Skip all confirmation prompts",
45
+ type: "boolean",
46
+ default: false,
47
+ },
48
+ ],
49
+ meta: {
50
+ examples: [
51
+ "pragma setup",
52
+ "pragma setup --yes",
53
+ "pragma setup --dry-run",
54
+ "pragma setup --undo",
55
+ ],
56
+ },
57
+ execute: async (
58
+ params: Record<string, unknown>,
59
+ ctx,
60
+ ): Promise<CommandResult> =>
61
+ runSetupTask(setupAll(ctx.cwd), {
62
+ dryRun: params.dryRun === true,
63
+ undo: params.undo === true,
64
+ yes: params.yes === true,
65
+ verbose: ctx.globalFlags.verbose,
66
+ llm: ctx.globalFlags.llm,
67
+ format: ctx.globalFlags.format,
68
+ }),
69
+ };
70
+ }
@@ -2,21 +2,25 @@
2
2
 
3
3
  import type { CommandDefinition } from "@canonical/cli-core";
4
4
  import {
5
- allCommand,
6
5
  completionsCommand,
7
6
  lspCommand,
8
7
  mcpCommand,
8
+ setupCommand,
9
9
  skillsCommand,
10
10
  } from "./commands/index.js";
11
11
 
12
12
  /**
13
13
  * Return all setup command definitions.
14
14
  *
15
- * @returns An array of CommandDefinitions for the setup subcommands.
15
+ * The top-level `pragma setup` is a summon-generator-backed command (replacing
16
+ * the former `setup all`); the granular `setup <step>` verbs remain for
17
+ * targeted, scriptable configuration of a single concern.
18
+ *
19
+ * @returns An array of CommandDefinitions for the setup command and its verbs.
16
20
  */
17
21
  export function commands(): CommandDefinition[] {
18
22
  return [
19
- allCommand,
23
+ setupCommand(),
20
24
  lspCommand,
21
25
  mcpCommand,
22
26
  completionsCommand,
@@ -18,8 +18,9 @@ describe("setupLsp", () => {
18
18
  it("logs info messages before and after exec", () => {
19
19
  const result = dryRun(setupLsp("/project"));
20
20
  const logs = result.effects.filter((e) => e._tag === "Log");
21
- expect(logs.some((l) => l.message.includes("Installing"))).toBe(true);
22
- expect(logs.some((l) => l.message.includes("✓"))).toBe(true);
21
+ expect(logs.some((l) => l.message.includes("Ensuring"))).toBe(true);
22
+ // Reports an idempotent "up to date" outcome, not a fresh install claim.
23
+ expect(logs.some((l) => l.message.includes("up to date"))).toBe(true);
23
24
  });
24
25
 
25
26
  it("collects effects without execution", () => {
@@ -10,8 +10,13 @@ import { $, exec, gen, info, type Task } from "@canonical/task";
10
10
  */
11
11
  export default function setupLsp(root: string): Task<void> {
12
12
  return gen(function* () {
13
- yield* $(info("Installing Terrazzo LSP extension for VS Code..."));
13
+ yield* $(
14
+ info("Ensuring the Terrazzo LSP VS Code extension is installed..."),
15
+ );
16
+ // The extension installer is idempotent — it installs if missing and is a
17
+ // no-op if already present — so this reports "ensured", not a fresh install
18
+ // (pragma does not inspect VS Code's extension state to tell them apart).
14
19
  yield* $(exec("bunx", ["@canonical/terrazzo-lsp-extension"], root));
15
- yield* $(info("✓ Terrazzo LSP extension installed."));
20
+ yield* $(info("✓ Terrazzo LSP extension is installed (up to date)."));
16
21
  });
17
22
  }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Bare-core boot — pragma must serve the generic `graph`, `graphql`, and
3
+ * `ontology` surfaces with zero semantic packages loaded. This pins the P0
4
+ * invariant that the core no longer depends on the design-system packages:
5
+ * a store booted with no packages resolves generic queries and each
6
+ * generic operation returns cleanly instead of crashing.
7
+ */
8
+
9
+ import type { Store } from "@canonical/ke";
10
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
11
+ import { EX_NAMESPACE, GRAPHQL_CLEAN_TTL } from "#testing";
12
+ import inspectUri from "../graph/operations/inspectUri.js";
13
+ import compileSchema from "../graphql/operations/compileSchema.js";
14
+ import listOntologies from "../ontology/operations/listOntologies.js";
15
+ import { bootStore } from "./bootStore.js";
16
+ import { PREFIX_MAP, resolvePrefixes } from "./prefixes.js";
17
+
18
+ let store: Store;
19
+
20
+ beforeAll(async () => {
21
+ // The `sources` override boots with zero resolved packages (bare core).
22
+ const boot = await bootStore({ sources: [] });
23
+ store = boot.store;
24
+ expect(boot.packages).toEqual([]);
25
+ });
26
+
27
+ afterAll(() => store?.dispose());
28
+
29
+ describe("bare-core boot", () => {
30
+ it("carries the generic core prefixes on the store", () => {
31
+ expect(store.prefixes.rdf).toBe(PREFIX_MAP.rdf);
32
+ expect(store.prefixes.owl).toBe(PREFIX_MAP.owl);
33
+ });
34
+
35
+ it("serves generic SPARQL without any packages loaded", async () => {
36
+ const result = await store.query(
37
+ "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }",
38
+ );
39
+ expect(result.type).toBe("select");
40
+ });
41
+
42
+ it("serves `ontology` — listOntologies returns cleanly, not a crash", async () => {
43
+ await expect(listOntologies(store)).resolves.toEqual([]);
44
+ });
45
+
46
+ it("serves `graph` — inspecting an unknown URI reports not-found, not a crash", async () => {
47
+ await expect(
48
+ inspectUri(store, "https://example.org/nothing"),
49
+ ).rejects.toMatchObject({ code: "ENTITY_NOT_FOUND" });
50
+ });
51
+
52
+ it("serves `graphql` — compiles a generic ontology with core-only prefixes", async () => {
53
+ const outcome = await compileSchema({
54
+ sources: [
55
+ { content: GRAPHQL_CLEAN_TTL, format: "turtle", path: "x.ttl" },
56
+ ],
57
+ // Package-style prefix layered over the bare core — no DS packages.
58
+ prefixes: resolvePrefixes([{ prefixes: { ex: EX_NAMESPACE } }]),
59
+ });
60
+ expect(outcome.status).toBe("ok");
61
+ expect(outcome.compiled?.sdl).toContain("type Thing");
62
+ });
63
+ });
@@ -0,0 +1,82 @@
1
+ import { createStore } from "@canonical/ke";
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
3
+ import { createGraphLoaderPlugin } from "./bootStore.js";
4
+
5
+ const PREFIXES = { ds: "https://ds.canonical.com/" };
6
+
7
+ const goodGraph = (path: string, subject: string) => ({
8
+ path,
9
+ content: `@prefix ds: <https://ds.canonical.com/>.\nds:${subject} a ds:Component.\n`,
10
+ format: "turtle" as const,
11
+ });
12
+
13
+ /** Mirrors the malformed `ds:hasSubcomponent ds:global..` shipped by upstream. */
14
+ const badGraph = (path: string) => ({
15
+ path,
16
+ content:
17
+ "@prefix ds: <https://ds.canonical.com/>.\nds:global.component.broken a ds:Component;\n ds:hasSubcomponent ds:global...\n",
18
+ format: "turtle" as const,
19
+ });
20
+
21
+ async function countTriples(
22
+ graphs: readonly ReturnType<typeof goodGraph>[],
23
+ ): Promise<number> {
24
+ const store = await createStore({
25
+ sources: [],
26
+ prefixes: PREFIXES,
27
+ plugins: [createGraphLoaderPlugin(graphs)],
28
+ });
29
+ // Always dispose the WASM-backed store so it can't leak across tests.
30
+ try {
31
+ const result = await store.query(
32
+ "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }",
33
+ );
34
+ return Number(
35
+ result.type === "select" ? (result.bindings[0]?.n ?? "0") : "0",
36
+ );
37
+ } finally {
38
+ store.dispose();
39
+ }
40
+ }
41
+
42
+ let stderrSpy: ReturnType<typeof vi.spyOn>;
43
+
44
+ beforeEach(() => {
45
+ stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
46
+ });
47
+
48
+ afterEach(() => {
49
+ stderrSpy.mockRestore();
50
+ });
51
+
52
+ const stderrText = (): string =>
53
+ stderrSpy.mock.calls.map((call) => String(call[0])).join("");
54
+
55
+ describe("createGraphLoaderPlugin", () => {
56
+ it("loads every graph when all are well-formed", async () => {
57
+ const count = await countTriples([
58
+ goodGraph("a.ttl", "global.component.a"),
59
+ goodGraph("b.ttl", "global.component.b"),
60
+ ]);
61
+ expect(count).toBe(2);
62
+ expect(stderrText()).toBe("");
63
+ });
64
+
65
+ it("skips a malformed graph but still boots and loads the good ones", async () => {
66
+ const count = await countTriples([
67
+ goodGraph("a.ttl", "global.component.a"),
68
+ badGraph("data/global/component/broken.ttl"),
69
+ goodGraph("b.ttl", "global.component.b"),
70
+ ]);
71
+ // The two good graphs load; the malformed one is skipped, not fatal.
72
+ expect(count).toBe(2);
73
+ });
74
+
75
+ it("warns with the offending file path and parse error", async () => {
76
+ await countTriples([badGraph("data/global/component/broken.ttl")]);
77
+ const text = stderrText();
78
+ expect(text).toContain("skipping malformed graph");
79
+ expect(text).toContain("data/global/component/broken.ttl");
80
+ expect(text).toContain("Check the TTL syntax");
81
+ });
82
+ });
@@ -23,8 +23,8 @@ import {
23
23
  createLocalLoader,
24
24
  } from "./loaders/index.js";
25
25
  import { DEFAULT_PACKAGES } from "./packages.js";
26
- import { PREFIX_MAP } from "./prefixes.js";
27
- import type { SemanticPackage } from "./semanticPackage.js";
26
+ import { resolvePrefixes } from "./prefixes.js";
27
+ import type { GraphContent, SemanticPackage } from "./semanticPackage.js";
28
28
  import { resolveSemanticPackages } from "./semanticPackage.js";
29
29
 
30
30
  export interface BootStoreOptions {
@@ -61,13 +61,14 @@ export interface BootResult {
61
61
  export async function bootStore(
62
62
  options: BootStoreOptions = {},
63
63
  ): Promise<BootResult> {
64
- const prefixes = { ...PREFIX_MAP, ...options.prefixes };
65
64
  try {
66
- // Testing/programmatic override — use explicit sources
65
+ // Testing/programmatic override — use explicit sources. No packages are
66
+ // resolved, so the prefix map is the core base plus the transitional DS
67
+ // fallback and any caller override.
67
68
  if (options.sources) {
68
69
  const store = await createStore({
69
70
  sources: options.sources,
70
- prefixes,
71
+ prefixes: resolvePrefixes([], options.prefixes),
71
72
  cache: options.cache,
72
73
  cwd: options.cwd,
73
74
  });
@@ -90,19 +91,11 @@ export async function bootStore(
90
91
  // Collect all graph content from resolved packages
91
92
  const allGraphs = packages.flatMap((pkg) => pkg.graphs);
92
93
 
93
- // Use a ke plugin to load graph content during store creation.
94
- // The onReady hook has access to ctx.load() which loads raw content.
95
- const graphLoaderPlugin = definePlugin({
96
- name: "pragma-graph-loader",
97
- onReady(ctx) {
98
- for (const graph of allGraphs) {
99
- ctx.load(graph.content, { format: graph.format });
100
- }
101
- },
102
- });
103
-
104
- // biome-ignore lint: Plugin generic variance requires explicit unknown
105
- const plugins: Plugin<any>[] = [graphLoaderPlugin];
94
+ // `Plugin<T>` is invariant in its API type and createStore expects
95
+ // `Plugin<void>[]`, so heterogeneous plugins (graph loader + trace) can only
96
+ // share an array via `any`. `unknown` does not satisfy `Plugin<void>`.
97
+ // biome-ignore lint/suspicious/noExplicitAny: intentional — see above
98
+ const plugins: Plugin<any>[] = [createGraphLoaderPlugin(allGraphs)];
106
99
  const traceEnabled =
107
100
  process.env.PRAGMA_TRACE === "1" || options.trace === true;
108
101
  if (traceEnabled) {
@@ -111,9 +104,12 @@ export async function bootStore(
111
104
  plugins.push(createTracePlugin({ traceDir: traceDir() }));
112
105
  }
113
106
 
107
+ // Merge every resolved package's declared prefixes (and the transitional
108
+ // DS fallback) over the generic core, then config overrides — so the
109
+ // store and every query see the packages' own namespaces.
114
110
  const store = await createStore({
115
111
  sources: [],
116
- prefixes,
112
+ prefixes: resolvePrefixes(packages, options.prefixes),
117
113
  cache: options.cache,
118
114
  cwd: options.cwd,
119
115
  plugins,
@@ -127,9 +123,49 @@ export async function bootStore(
127
123
  {
128
124
  recovery: {
129
125
  message:
130
- "Ensure design system packages are installed: bun add -D @canonical/design-system @canonical/code-standards @canonical/anatomy-dsl",
126
+ "If this is a parser error, check the TTL syntax of the source graphs " +
127
+ "(the message above names the line and column). Otherwise ensure design " +
128
+ "system packages are installed: bun add -D @canonical/design-system " +
129
+ "@canonical/code-standards @canonical/anatomy-dsl",
131
130
  },
132
131
  },
133
132
  );
134
133
  }
135
134
  }
135
+
136
+ /**
137
+ * Build the ke plugin that loads resolved graph content into the store.
138
+ *
139
+ * Each graph is loaded independently inside its own try/catch: a single
140
+ * malformed TTL file (e.g. invalid Turtle emitted by an upstream authoring
141
+ * tool) must not abort the entire boot and take the whole CLI down with it.
142
+ * Failed graphs are skipped and surfaced as warnings, mirroring the
143
+ * resilience the story loader already provides ("one bad file cannot break
144
+ * boot"). A store built from N-1 good graphs is far more useful than a store
145
+ * that refuses to boot because of one bad graph.
146
+ *
147
+ * @note Impure — writes warnings to stderr on parse failure.
148
+ */
149
+ export function createGraphLoaderPlugin(
150
+ graphs: readonly GraphContent[],
151
+ ): Plugin {
152
+ return definePlugin({
153
+ name: "pragma-graph-loader",
154
+ onReady(ctx) {
155
+ for (const graph of graphs) {
156
+ try {
157
+ ctx.load(graph.content, { format: graph.format });
158
+ } catch (error) {
159
+ const reason = error instanceof Error ? error.message : String(error);
160
+ // Name the offending file and the parse error, and point at the fix.
161
+ // The store still boots from the remaining graphs, so this is a
162
+ // warning, not a fatal error.
163
+ process.stderr.write(
164
+ `Warning: skipping malformed graph "${graph.path}" — ${reason}. ` +
165
+ "Check the TTL syntax of this source file; the remaining graphs still load.\n",
166
+ );
167
+ }
168
+ }
169
+ },
170
+ });
171
+ }
@@ -4,8 +4,7 @@ import blockSpecs from "../block/mcp/specs.js";
4
4
  import blockEmptyError from "../block/orchestration/blockEmptyError.js";
5
5
  import modifierSpecs from "../modifier/mcp/specs.js";
6
6
  import modifierEmptyError from "../modifier/orchestration/modifierEmptyError.js";
7
- import standardSpecs from "../standard/mcp/specs.js";
8
- import standardEmptyError from "../standard/orchestration/standardEmptyError.js";
7
+ import { TOKEN_READ_SURFACE_ENABLED } from "../token/featureFlag.js";
9
8
  import tokenSpecs from "../token/mcp/specs.js";
10
9
  import tokenEmptyError from "../token/orchestration/tokenEmptyError.js";
11
10
  import type { FilterConfig } from "./types/index.js";
@@ -40,36 +39,34 @@ const cases: ReadonlyArray<{
40
39
  toolNames: new Set(blockSpecs.map((s) => s.name)),
41
40
  storeEmpty: false,
42
41
  },
43
- {
44
- domain: "token (category active)",
45
- error: tokenEmptyError("color"),
46
- toolNames: new Set(tokenSpecs.map((s) => s.name)),
47
- storeEmpty: false,
48
- },
49
- {
50
- domain: "token (store-empty)",
51
- error: tokenEmptyError(),
52
- toolNames: new Set(tokenSpecs.map((s) => s.name)),
53
- storeEmpty: true,
54
- },
42
+ // The token read surface (and with it the paths that raise
43
+ // tokenEmptyError) is gated behind the token feature flag; its recovery
44
+ // hints can only point at registered tools while the surface is live.
45
+ ...(TOKEN_READ_SURFACE_ENABLED
46
+ ? [
47
+ {
48
+ domain: "token (category active)",
49
+ error: tokenEmptyError("color"),
50
+ toolNames: new Set(tokenSpecs.map((s) => s.name)),
51
+ storeEmpty: false,
52
+ },
53
+ {
54
+ domain: "token (store-empty)",
55
+ error: tokenEmptyError(),
56
+ toolNames: new Set(tokenSpecs.map((s) => s.name)),
57
+ storeEmpty: true,
58
+ },
59
+ ]
60
+ : []),
55
61
  {
56
62
  domain: "modifier (store-empty)",
57
63
  error: modifierEmptyError(),
58
64
  toolNames: new Set(modifierSpecs.map((s) => s.name)),
59
65
  storeEmpty: true,
60
66
  },
61
- {
62
- domain: "standard (filter active)",
63
- error: standardEmptyError({ category: "react" }),
64
- toolNames: new Set(standardSpecs.map((s) => s.name)),
65
- storeEmpty: false,
66
- },
67
- {
68
- domain: "standard (store-empty)",
69
- error: standardEmptyError({}),
70
- toolNames: new Set(standardSpecs.map((s) => s.name)),
71
- storeEmpty: true,
72
- },
67
+ // `standard` left this contract with its domain: the bundled pack has no
68
+ // emptyError hook (a pinned PARITY_GAP), so an empty standard list renders
69
+ // zero rows instead of an EMPTY_RESULTS recovery.
73
70
  ];
74
71
 
75
72
  describe("empty-error recovery contract (cross-domain)", () => {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import extractLocalName from "./extractLocalName.js";
3
- import { PREFIX_MAP } from "./prefixes.js";
3
+ import { DEFAULT_PREFIX_MAP } from "./prefixes.js";
4
4
 
5
5
  describe("extractLocalName", () => {
6
6
  it("extracts after hash", () => {
@@ -8,7 +8,7 @@ describe("extractLocalName", () => {
8
8
  });
9
9
 
10
10
  it("extracts after last slash", () => {
11
- expect(extractLocalName(`${PREFIX_MAP.ds}global`)).toBe("global");
11
+ expect(extractLocalName(`${DEFAULT_PREFIX_MAP.ds}global`)).toBe("global");
12
12
  });
13
13
 
14
14
  it("returns full string when no separator", () => {
@@ -16,6 +16,6 @@ describe("extractLocalName", () => {
16
16
  });
17
17
 
18
18
  it("handles nested path", () => {
19
- expect(extractLocalName(`${PREFIX_MAP.ds}apps/lxd`)).toBe("lxd");
19
+ expect(extractLocalName(`${DEFAULT_PREFIX_MAP.ds}apps/lxd`)).toBe("lxd");
20
20
  });
21
21
  });
@@ -70,13 +70,34 @@ export function renderLookupResults<TResult, TInput>(
70
70
 
71
71
  const parts = result.results.map((item) => formatOne(mapResult(item)));
72
72
  if (result.errors.length > 0) {
73
+ const heading = ctx.globalFlags.llm ? "**Errors:**" : "Errors:";
73
74
  parts.push(
74
75
  [
75
- "Errors:",
76
- ...result.errors.map((error) => `- ${error.query}: ${error.message}`),
76
+ heading,
77
+ ...result.errors.flatMap((error) => formatErrorLines(error)),
77
78
  ].join("\n"),
78
79
  );
79
80
  }
80
81
 
81
82
  return parts.join("\n\n");
82
83
  }
84
+
85
+ /**
86
+ * Render one lookup error as display lines: the query and message, plus any
87
+ * fuzzy-match suggestions carried on the error (which the JSON mode surfaces
88
+ * but the text modes previously dropped).
89
+ *
90
+ * @param error - A single lookup error entry.
91
+ * @returns One or more display lines for this error.
92
+ */
93
+ function formatErrorLines(error: {
94
+ query: string;
95
+ message: string;
96
+ suggestions?: readonly string[];
97
+ }): string[] {
98
+ const lines = [`- ${error.query}: ${error.message}`];
99
+ if (error.suggestions && error.suggestions.length > 0) {
100
+ lines.push(` Did you mean: ${error.suggestions.join(", ")}?`);
101
+ }
102
+ return lines;
103
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * List the tiers defined in the loaded ontology, ordered by name.
3
+ *
4
+ * Shared operation: the `tier list` CLI/MCP surface is served by the bundled
5
+ * `tier` story pack, but `config tier` validation still needs the tier set
6
+ * programmatically, and this stays part of the package's public API. Kept in
7
+ * the shared layer so it has no dependency on any DS-specific domain module.
8
+ *
9
+ * @param store - ke store to query.
10
+ * @returns Array of tier entries, empty when none exist.
11
+ * @note Impure — queries the ke store.
12
+ */
13
+
14
+ import type { Store, URI } from "@canonical/ke";
15
+ import { buildQuery } from "./buildQuery.js";
16
+ import { P } from "./prefixes.js";
17
+ import type { TierEntry } from "./types/index.js";
18
+
19
+ export async function listTiers(store: Store): Promise<TierEntry[]> {
20
+ const result = await store.query(
21
+ buildQuery(`
22
+ SELECT ?tier ?name
23
+ WHERE {
24
+ ?tier a ${P.ds}Tier ;
25
+ ${P.ds}name ?name .
26
+ }
27
+ ORDER BY ?name
28
+ `),
29
+ );
30
+
31
+ if (result.type !== "select") return [];
32
+
33
+ return result.bindings.map((b) => ({
34
+ uri: (b.tier ?? "") as URI,
35
+ path: b.name ?? "",
36
+ depth: 0,
37
+ }));
38
+ }
39
+
40
+ export default listTiers;
@@ -96,12 +96,14 @@ export default function createBundledLoader(): PackageLoader {
96
96
  if (graphs.length === 0 && stories.length === 0) return undefined;
97
97
 
98
98
  const version = await readEmbeddedVersion(blobs);
99
+ const prefixes = await readEmbeddedPrefixes(blobs);
99
100
 
100
101
  cached = {
101
102
  name: "(bundled)",
102
103
  version,
103
104
  source: "bundled",
104
105
  graphs,
106
+ ...(prefixes ? { prefixes } : {}),
105
107
  skills: [],
106
108
  stories,
107
109
  };
@@ -179,6 +181,52 @@ async function readEmbeddedStories(
179
181
  // Version reading
180
182
  // ---------------------------------------------------------------------------
181
183
 
184
+ /**
185
+ * Aggregate `pragma.prefixes` declared across embedded package manifests.
186
+ *
187
+ * The bundled loader collapses every embedded package into one aggregate
188
+ * SemanticPackage, so their prefix declarations are merged here (later
189
+ * manifests win on key collisions). Only string→string entries are kept;
190
+ * malformed shapes are ignored so one bad manifest cannot break boot.
191
+ *
192
+ * @returns The merged prefix map, or `undefined` when none is declared.
193
+ */
194
+ async function readEmbeddedPrefixes(
195
+ blobs: ReadonlyArray<Blob & { name: string }>,
196
+ ): Promise<Readonly<Record<string, string>> | undefined> {
197
+ const prefixes: Record<string, string> = {};
198
+
199
+ for (const blob of blobs) {
200
+ if (!isPackageManifest(blob.name)) continue;
201
+
202
+ try {
203
+ const parsed = JSON.parse(await blob.text()) as { pragma?: unknown };
204
+ const pragma = parsed.pragma;
205
+ if (typeof pragma !== "object" || pragma === null) continue;
206
+ const raw = (pragma as { prefixes?: unknown }).prefixes;
207
+ if (typeof raw !== "object" || raw === null) continue;
208
+ for (const [name, namespace] of Object.entries(raw)) {
209
+ if (typeof namespace !== "string") continue;
210
+ // These collapse into one aggregate package, so a cross-manifest
211
+ // collision would never reach resolvePrefixes' warning — surface it
212
+ // here, naming the manifest, instead of silently last-winning.
213
+ if (name in prefixes && prefixes[name] !== namespace) {
214
+ warn(
215
+ `embedded manifest "${blob.name}" overrides prefix "${name}" ` +
216
+ `(was <${prefixes[name]}>, now <${namespace}>)`,
217
+ );
218
+ }
219
+ prefixes[name] = namespace;
220
+ }
221
+ } catch (error) {
222
+ const reason = error instanceof Error ? error.message : String(error);
223
+ warn(`skipping prefixes in embedded manifest "${blob.name}": ${reason}`);
224
+ }
225
+ }
226
+
227
+ return Object.keys(prefixes).length > 0 ? prefixes : undefined;
228
+ }
229
+
182
230
  async function readEmbeddedVersion(
183
231
  blobs: ReadonlyArray<Blob & { name: string }>,
184
232
  ): Promise<string> {
@@ -24,8 +24,7 @@ export default function createGitLoader(): PackageLoader {
24
24
  const dir = gitCacheDir(ref.pkg, ref.ref);
25
25
  if (!existsSync(dir)) return undefined;
26
26
 
27
- const { version, graphs, skills, stories } = readPackageDir(dir);
28
- return { name: ref.pkg, version, source: "git", graphs, skills, stories };
27
+ return { name: ref.pkg, source: "git", ...readPackageDir(dir) };
29
28
  },
30
29
  };
31
30
  }