@canonical/pragma-cli 0.29.0 → 0.30.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 (241) hide show
  1. package/README.md +67 -15
  2. package/dist/pragma +0 -0
  3. package/package.json +13 -13
  4. package/src/config/defaultConfig.test.ts +33 -0
  5. package/src/config/defaultConfig.ts +59 -0
  6. package/src/config/defaults.json +17 -0
  7. package/src/config/findProjectConfigPath.test.ts +53 -0
  8. package/src/config/findProjectConfigPath.ts +36 -0
  9. package/src/config/index.ts +17 -2
  10. package/src/config/mergeConfigFileUpdate.ts +58 -0
  11. package/src/config/parseConfigValues.ts +215 -0
  12. package/src/config/readConfig.test.ts +104 -13
  13. package/src/config/readConfig.ts +12 -153
  14. package/src/config/readConfigLayers.test.ts +107 -0
  15. package/src/config/readConfigLayers.ts +115 -0
  16. package/src/config/resolveGlobalConfigPath.ts +13 -0
  17. package/src/config/resolveWriteConfigPath.test.ts +53 -0
  18. package/src/config/resolveWriteConfigPath.ts +31 -0
  19. package/src/config/types.ts +76 -1
  20. package/src/config/writeConfig.ts +25 -55
  21. package/src/domains/block/commands/list.ts +5 -90
  22. package/src/domains/block/commands/lookup.ts +8 -167
  23. package/src/domains/block/mcp/specs.ts +16 -196
  24. package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
  25. package/src/domains/block/stories.test.ts +28 -0
  26. package/src/domains/block/stories.ts +254 -0
  27. package/src/domains/config/commands/channel.test.ts +14 -3
  28. package/src/domains/config/commands/channel.ts +11 -7
  29. package/src/domains/config/commands/configScope.ts +46 -0
  30. package/src/domains/config/commands/framework.test.ts +18 -1
  31. package/src/domains/config/commands/framework.ts +13 -7
  32. package/src/domains/config/commands/show.ts +5 -45
  33. package/src/domains/config/commands/tier.test.ts +13 -2
  34. package/src/domains/config/commands/tier.ts +11 -7
  35. package/src/domains/config/commands/trace.test.ts +19 -2
  36. package/src/domains/config/commands/trace.ts +6 -3
  37. package/src/domains/config/formatters/channel.test.ts +20 -6
  38. package/src/domains/config/formatters/channel.ts +14 -11
  39. package/src/domains/config/formatters/framework.ts +14 -11
  40. package/src/domains/config/formatters/show.test.ts +27 -0
  41. package/src/domains/config/formatters/show.ts +27 -4
  42. package/src/domains/config/formatters/tier.test.ts +26 -10
  43. package/src/domains/config/formatters/tier.ts +19 -11
  44. package/src/domains/config/formatters/trace.ts +7 -7
  45. package/src/domains/config/mcp/specs.ts +33 -53
  46. package/src/domains/config/operations/show.test.ts +18 -0
  47. package/src/domains/config/operations/show.ts +7 -1
  48. package/src/domains/config/operations/types.ts +7 -0
  49. package/src/domains/config/stories.ts +42 -0
  50. package/src/domains/config/tasks/index.ts +1 -0
  51. package/src/domains/config/tasks/setChannel.ts +13 -28
  52. package/src/domains/config/tasks/setFramework.ts +11 -30
  53. package/src/domains/config/tasks/setTier.ts +10 -29
  54. package/src/domains/config/tasks/setTrace.ts +10 -28
  55. package/src/domains/config/tasks/writeConfigField.ts +41 -0
  56. package/src/domains/create/commands/component.ts +5 -5
  57. package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
  58. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
  59. package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
  60. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +2 -14
  61. package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
  62. package/src/domains/doctor/operations/checks/index.ts +1 -0
  63. package/src/domains/doctor/operations/checks.test.ts +158 -7
  64. package/src/domains/doctor/operations/runChecks.test.ts +15 -6
  65. package/src/domains/doctor/operations/runChecks.ts +2 -0
  66. package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
  67. package/src/domains/graph/helpers/resolveUri.ts +28 -21
  68. package/src/domains/graphql/commands/build.test.ts +5 -3
  69. package/src/domains/graphql/commands/build.ts +8 -16
  70. package/src/domains/graphql/commands/check.test.ts +5 -3
  71. package/src/domains/graphql/commands/check.ts +7 -13
  72. package/src/domains/graphql/commands/index.ts +1 -0
  73. package/src/domains/graphql/commands/serve.test.ts +40 -0
  74. package/src/domains/graphql/commands/serve.ts +131 -0
  75. package/src/domains/graphql/constants.ts +10 -0
  76. package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
  77. package/src/domains/graphql/helpers/gatherSources.ts +58 -0
  78. package/src/domains/graphql/helpers/index.ts +2 -0
  79. package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
  80. package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
  81. package/src/domains/graphql/index.ts +9 -6
  82. package/src/domains/graphql/operations/compileSchema.test.ts +21 -45
  83. package/src/domains/graphql/operations/compileSchema.ts +23 -25
  84. package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
  85. package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
  86. package/src/domains/graphql/operations/index.ts +5 -0
  87. package/src/domains/info/operations/collectInfo.test.ts +14 -3
  88. package/src/domains/info/operations/collectInfo.ts +12 -15
  89. package/src/domains/llm/capabilities.test.ts +4 -4
  90. package/src/domains/llm/data/toolCatalog.ts +6 -0
  91. package/src/domains/modifier/commands/list.ts +5 -38
  92. package/src/domains/modifier/commands/lookup.ts +8 -79
  93. package/src/domains/modifier/mcp/specs.ts +15 -94
  94. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
  95. package/src/domains/modifier/stories.test.ts +20 -0
  96. package/src/domains/modifier/stories.ts +77 -0
  97. package/src/domains/ontology/commands/list.ts +5 -58
  98. package/src/domains/ontology/commands/show.ts +6 -58
  99. package/src/domains/ontology/formatters/show.ts +7 -1
  100. package/src/domains/ontology/mcp/specs.ts +7 -84
  101. package/src/domains/ontology/stories.test.ts +36 -0
  102. package/src/domains/ontology/stories.ts +108 -0
  103. package/src/domains/refs/operations/updateRefs.ts +2 -37
  104. package/src/domains/setup/commands/skills.test.ts +6 -2
  105. package/src/domains/setup/commands/skills.ts +6 -2
  106. package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
  107. package/src/domains/setup/helpers/runSetupTask.ts +62 -70
  108. package/src/domains/setup/operations/setupMcp.test.ts +6 -1
  109. package/src/domains/shared/bootStore.ts +6 -3
  110. package/src/domains/shared/compactUri.test.ts +11 -0
  111. package/src/domains/shared/compactUri.ts +10 -5
  112. package/src/domains/shared/context.ts +2 -2
  113. package/src/domains/shared/contracts.ts +11 -0
  114. package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
  115. package/src/domains/shared/createInteractivePromptSession.ts +236 -0
  116. package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
  117. package/src/domains/shared/loaders/bundledLoader.ts +110 -12
  118. package/src/domains/shared/loaders/gitLoader.ts +2 -2
  119. package/src/domains/shared/loaders/index.ts +4 -1
  120. package/src/domains/shared/loaders/localLoader.ts +18 -4
  121. package/src/domains/shared/loaders/readPackageDir.ts +36 -1
  122. package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
  123. package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
  124. package/src/domains/shared/lookupMany.test.ts +120 -0
  125. package/src/domains/shared/lookupMany.ts +34 -2
  126. package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
  127. package/src/domains/shared/mergeAndParseRefs.ts +44 -0
  128. package/src/domains/shared/packages.ts +10 -17
  129. package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
  130. package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
  131. package/src/domains/shared/runtime.ts +4 -63
  132. package/src/domains/shared/semanticPackage.ts +17 -1
  133. package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
  134. package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
  135. package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
  136. package/src/domains/shared/stories/compileLookupTool.ts +96 -0
  137. package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
  138. package/src/domains/shared/stories/compileReadCommand.ts +48 -0
  139. package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
  140. package/src/domains/shared/stories/compileReadTool.ts +38 -0
  141. package/src/domains/shared/stories/condense.test.ts +18 -0
  142. package/src/domains/shared/stories/condense.ts +15 -0
  143. package/src/domains/shared/stories/index.ts +20 -0
  144. package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
  145. package/src/domains/shared/stories/normalizeNames.ts +22 -0
  146. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
  147. package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
  148. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
  149. package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
  150. package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
  151. package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
  152. package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
  153. package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
  154. package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
  155. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
  156. package/src/domains/shared/stories/pack/index.ts +28 -0
  157. package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
  158. package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
  159. package/src/domains/shared/stories/pack/types.ts +104 -0
  160. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
  161. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
  162. package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
  163. package/src/domains/shared/stories/projectCliParameters.ts +57 -0
  164. package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
  165. package/src/domains/shared/stories/projectToolParams.ts +32 -0
  166. package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
  167. package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
  168. package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
  169. package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
  170. package/src/domains/shared/stories/types.ts +171 -0
  171. package/src/domains/shared/suggestions/expandGlob.ts +1 -0
  172. package/src/domains/shared/suggestions/suggestNames.ts +43 -10
  173. package/src/domains/skill/commands/index.ts +3 -1
  174. package/src/domains/skill/commands/list.test.ts +6 -2
  175. package/src/domains/skill/commands/list.ts +5 -63
  176. package/src/domains/skill/commands/lookup.test.ts +118 -0
  177. package/src/domains/skill/commands/lookup.ts +14 -0
  178. package/src/domains/skill/formatters/index.ts +1 -0
  179. package/src/domains/skill/formatters/lookup.test.ts +46 -0
  180. package/src/domains/skill/formatters/lookup.ts +57 -0
  181. package/src/domains/skill/helpers/resolveSkillSources.ts +0 -1
  182. package/src/domains/skill/index.ts +7 -3
  183. package/src/domains/skill/mcp/specs.ts +10 -29
  184. package/src/domains/skill/operations/discover.test.ts +61 -30
  185. package/src/domains/skill/operations/discover.ts +28 -37
  186. package/src/domains/skill/operations/index.ts +2 -1
  187. package/src/domains/skill/operations/list.test.ts +15 -7
  188. package/src/domains/skill/operations/lookup.test.ts +91 -0
  189. package/src/domains/skill/operations/lookup.ts +70 -0
  190. package/src/domains/skill/stories.test.ts +43 -0
  191. package/src/domains/skill/stories.ts +103 -0
  192. package/src/domains/skill/types.ts +8 -0
  193. package/src/domains/standard/commands/list.ts +5 -84
  194. package/src/domains/standard/commands/lookup.ts +8 -93
  195. package/src/domains/standard/mcp/specs.ts +16 -171
  196. package/src/domains/standard/orchestration/resolveStandardLookup.ts +2 -1
  197. package/src/domains/standard/stories.test.ts +24 -0
  198. package/src/domains/standard/stories.ts +151 -0
  199. package/src/domains/tier/commands/list.ts +5 -55
  200. package/src/domains/tier/mcp/specs.ts +5 -31
  201. package/src/domains/tier/stories.test.ts +22 -0
  202. package/src/domains/tier/stories.ts +52 -0
  203. package/src/domains/token/mcp/specs.ts +5 -1
  204. package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
  205. package/src/domains/trace/tracePlugin.test.ts +1 -1
  206. package/src/mcp/registerResources.test.ts +382 -74
  207. package/src/mcp/registerResources.ts +5 -348
  208. package/src/mcp/resources/buildGraphIndex.ts +167 -0
  209. package/src/mcp/resources/buildResourceList.test.ts +97 -0
  210. package/src/mcp/resources/buildResourceList.ts +176 -0
  211. package/src/mcp/resources/classifyEntity.test.ts +40 -0
  212. package/src/mcp/resources/classifyEntity.ts +27 -0
  213. package/src/mcp/resources/constants.ts +105 -0
  214. package/src/mcp/resources/displayPredicates.ts +62 -0
  215. package/src/mcp/resources/pickFirstValue.test.ts +29 -0
  216. package/src/mcp/resources/pickFirstValue.ts +26 -0
  217. package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
  218. package/src/mcp/resources/pickPrimaryType.ts +26 -0
  219. package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
  220. package/src/mcp/resources/rankUriCompletions.ts +64 -0
  221. package/src/mcp/resources/readEntity.ts +169 -0
  222. package/src/mcp/resources/registerResources.ts +126 -0
  223. package/src/mcp/resources/types.ts +128 -0
  224. package/src/mcp/tools/index.ts +9 -0
  225. package/src/mcp/tools/registerTools.test.ts +65 -8
  226. package/src/mcp/types.ts +4 -0
  227. package/src/pipeline/collectCommands.ts +8 -1
  228. package/src/pipeline/runCli.ts +40 -6
  229. package/src/testing/createTestMcpClient.ts +4 -1
  230. package/src/testing/index.ts +10 -0
  231. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
  232. package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
  233. package/src/testing/integration/executor-execution.test.ts +101 -0
  234. package/src/testing/integration/executor-golden.test.ts +145 -0
  235. package/src/testing/integration/mcp-surface.test.ts +1 -1
  236. package/src/testing/integration/parity.test.ts +17 -12
  237. package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
  238. package/src/testing/setupXdgIsolation.ts +15 -0
  239. package/src/testing/standardParityFixtures.ts +92 -0
  240. package/src/testing/storyFixtures.ts +63 -0
  241. package/src/pipeline/runInteractiveCommand.ts +0 -198
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Route an incoming request for the graphql dev server: `/graphql` to the
3
+ * GraphQL + GraphiQL handler, `/` to a redirect there, and everything else
4
+ * to 404. Mounting is intentionally the host's job, which keeps the handler
5
+ * itself path-agnostic.
6
+ *
7
+ * @param request - The incoming fetch Request.
8
+ * @param handler - The GraphQL fetch handler for `/graphql`.
9
+ * @param port - The listen port, used to build the root redirect target.
10
+ * @returns The response (or a promise of one) for the route.
11
+ */
12
+
13
+ type GraphQLHandler = (request: Request) => Response | Promise<Response>;
14
+
15
+ export default function routeGraphql(
16
+ request: Request,
17
+ handler: GraphQLHandler,
18
+ port: number,
19
+ ): Response | Promise<Response> {
20
+ const { pathname } = new URL(request.url);
21
+ if (pathname === "/graphql") {
22
+ return handler(request);
23
+ }
24
+ if (pathname === "/") {
25
+ return Response.redirect(`http://localhost:${port}/graphql`, 302);
26
+ }
27
+ return new Response(null, { status: 404 });
28
+ }
@@ -1,21 +1,24 @@
1
1
  /**
2
2
  * @module GraphQL domain barrel.
3
3
  *
4
- * Compiles TTL ontologies into GraphQL schema artifacts via the
5
- * ke-graphql pipeline. Commands boot their own ke store from explicit
6
- * TTL sources, so they run on the store-skip pipeline path.
4
+ * Compiles TTL ontologies into GraphQL schema artifacts via the ke-graphql
5
+ * pipeline, and serves them locally. Commands boot their own ke store —
6
+ * from explicit TTL sources or, when none are given, the semantic packages
7
+ * configured in `pragma.config.json` — so they run on the store-skip
8
+ * pipeline path.
7
9
  */
8
10
 
9
11
  import type { CommandDefinition } from "@canonical/cli-core";
10
- import { buildCommand, checkCommand } from "./commands/index.js";
12
+ import { buildCommand, checkCommand, serveCommand } from "./commands/index.js";
11
13
 
12
14
  /**
13
15
  * Returns all command definitions for the graphql domain.
14
16
  *
15
- * @returns An array of command definitions (`graphql build`, `graphql check`).
17
+ * @returns The `graphql build`, `graphql check`, and `graphql serve`
18
+ * command definitions.
16
19
  */
17
20
  export function commands(): CommandDefinition[] {
18
- return [buildCommand, checkCommand];
21
+ return [buildCommand, checkCommand, serveCommand];
19
22
  }
20
23
 
21
24
  export { compileSchema } from "./operations/index.js";
@@ -1,8 +1,5 @@
1
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import { join } from "node:path";
4
1
  import { hashSources } from "@canonical/ke-graphql";
5
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import { describe, expect, it } from "vitest";
6
3
  import {
7
4
  EX_NAMESPACE,
8
5
  GRAPHQL_CLEAN_TTL,
@@ -14,60 +11,47 @@ import compileSchema from "./compileSchema.js";
14
11
 
15
12
  const PREFIXES = { ...PREFIX_MAP, ex: EX_NAMESPACE };
16
13
 
17
- describe("compileSchema", () => {
18
- let dir: string;
19
-
20
- beforeEach(() => {
21
- dir = mkdtempSync(join(tmpdir(), "pragma-graphql-compile-"));
22
- writeFileSync(join(dir, "clean.ttl"), GRAPHQL_CLEAN_TTL);
23
- writeFileSync(join(dir, "error.ttl"), GRAPHQL_ERROR_TTL);
24
- writeFileSync(join(dir, "fatal.ttl"), GRAPHQL_FATAL_TTL);
25
- });
26
-
27
- afterEach(() => {
28
- rmSync(dir, { recursive: true, force: true });
29
- });
14
+ const inline = (content: string, path: string) => ({
15
+ content,
16
+ format: "turtle" as const,
17
+ path,
18
+ });
30
19
 
20
+ describe("compileSchema", () => {
31
21
  it("compiles a clean ontology into SDL", async () => {
32
22
  const outcome = await compileSchema({
33
- sources: ["clean.ttl"],
23
+ sources: [inline(GRAPHQL_CLEAN_TTL, "clean.ttl")],
34
24
  prefixes: PREFIXES,
35
- cwd: dir,
36
25
  });
37
26
 
38
27
  expect(outcome.status).toBe("ok");
39
28
  expect(outcome.compiled?.sdl).toContain("type Thing");
40
29
  expect(outcome.diagnostics).toEqual([]);
41
- expect(outcome.files).toEqual([join(dir, "clean.ttl")]);
30
+ expect(outcome.files).toEqual(["clean.ttl"]);
42
31
  });
43
32
 
44
- it("hashes the exact raw file contents", async () => {
33
+ it("hashes the exact raw source contents", async () => {
45
34
  const outcome = await compileSchema({
46
- sources: ["clean.ttl"],
35
+ sources: [inline(GRAPHQL_CLEAN_TTL, "clean.ttl")],
47
36
  prefixes: PREFIXES,
48
- cwd: dir,
49
37
  });
50
38
 
51
- const raw = readFileSync(join(dir, "clean.ttl"), "utf-8");
52
- expect(outcome.sourcesHash).toBe(hashSources([raw]));
39
+ expect(outcome.sourcesHash).toBe(hashSources([GRAPHQL_CLEAN_TTL]));
53
40
  });
54
41
 
55
- it("resolves glob patterns to source files", async () => {
42
+ it("labels a source without a path as 'inline'", async () => {
56
43
  const outcome = await compileSchema({
57
- sources: ["clean*.ttl"],
44
+ sources: [{ content: GRAPHQL_CLEAN_TTL, format: "turtle" }],
58
45
  prefixes: PREFIXES,
59
- cwd: dir,
60
46
  });
61
47
 
62
- expect(outcome.status).toBe("ok");
63
- expect(outcome.files).toEqual([join(dir, "clean.ttl")]);
48
+ expect(outcome.files).toEqual(["inline"]);
64
49
  });
65
50
 
66
51
  it("surfaces error diagnostics without failing the compile", async () => {
67
52
  const outcome = await compileSchema({
68
- sources: ["error.ttl"],
53
+ sources: [inline(GRAPHQL_ERROR_TTL, "error.ttl")],
69
54
  prefixes: PREFIXES,
70
- cwd: dir,
71
55
  });
72
56
 
73
57
  expect(outcome.status).toBe("ok");
@@ -81,9 +65,8 @@ describe("compileSchema", () => {
81
65
 
82
66
  it("converts CompilationError into a failed result with diagnostics", async () => {
83
67
  const outcome = await compileSchema({
84
- sources: ["fatal.ttl"],
68
+ sources: [inline(GRAPHQL_FATAL_TTL, "fatal.ttl")],
85
69
  prefixes: PREFIXES,
86
- cwd: dir,
87
70
  });
88
71
 
89
72
  expect(outcome.status).toBe("failed");
@@ -95,24 +78,17 @@ describe("compileSchema", () => {
95
78
  ).toBe(true);
96
79
  });
97
80
 
98
- it("throws structured error when no sources resolve", async () => {
81
+ it("throws a structured error when given no sources", async () => {
99
82
  await expect(
100
- compileSchema({
101
- sources: ["missing.ttl"],
102
- prefixes: PREFIXES,
103
- cwd: dir,
104
- }),
83
+ compileSchema({ sources: [], prefixes: PREFIXES }),
105
84
  ).rejects.toMatchObject({ code: "INVALID_INPUT" });
106
85
  });
107
86
 
108
- it("throws structured error on invalid TTL syntax", async () => {
109
- writeFileSync(join(dir, "broken.ttl"), "this is not turtle .");
110
-
87
+ it("throws a structured error on invalid TTL syntax", async () => {
111
88
  await expect(
112
89
  compileSchema({
113
- sources: ["broken.ttl"],
90
+ sources: [inline("this is not turtle .", "broken.ttl")],
114
91
  prefixes: PREFIXES,
115
- cwd: dir,
116
92
  }),
117
93
  ).rejects.toMatchObject({ code: "STORE_ERROR" });
118
94
  });
@@ -1,5 +1,4 @@
1
- import { readFileSync } from "node:fs";
2
- import { createStore } from "@canonical/ke";
1
+ import { createStore, type InlineSource } from "@canonical/ke";
3
2
  import {
4
3
  CompilationError,
5
4
  type CompilerResult,
@@ -9,15 +8,14 @@ import {
9
8
  hashSources,
10
9
  } from "@canonical/ke-graphql";
11
10
  import { PragmaError } from "#error";
12
- import { resolveSourceFiles } from "../helpers/index.js";
13
11
 
14
12
  export interface CompileSchemaOptions {
15
- /** TTL file paths or glob patterns, resolved from `cwd`. */
16
- readonly sources: readonly string[];
13
+ /** In-memory TTL sources (content + provenance path) to compile. */
14
+ readonly sources: readonly InlineSource[];
17
15
  /** Prefix map handed to both the ke store and the compiler. */
18
16
  readonly prefixes: Readonly<Record<string, string>>;
19
- /** Working directory for resolving relative source paths. */
20
- readonly cwd: string;
17
+ /** Working directory, passed through to the store (unused for inline). */
18
+ readonly cwd?: string;
21
19
  }
22
20
 
23
21
  export interface CompileSchemaResult {
@@ -25,7 +23,7 @@ export interface CompileSchemaResult {
25
23
  readonly status: "ok" | "failed";
26
24
  /** All compiler diagnostics, including those carried by a CompilationError. */
27
25
  readonly diagnostics: readonly Diagnostic[];
28
- /** Resolved absolute TTL file paths that were loaded. */
26
+ /** Provenance paths of the sources that were loaded. */
29
27
  readonly files: readonly string[];
30
28
  /** Fingerprint of the raw source contents (matches ke's onLoad hashing). */
31
29
  readonly sourcesHash: string;
@@ -34,42 +32,42 @@ export interface CompileSchemaResult {
34
32
  }
35
33
 
36
34
  /**
37
- * Compile TTL sources into a GraphQL schema via the ke-graphql pipeline.
35
+ * Compile in-memory TTL sources into a GraphQL schema via the ke-graphql
36
+ * pipeline.
38
37
  *
39
- * Resolves source patterns, hashes the raw file contents (the exact bytes
40
- * ke loads, so the artifact's `sourcesHash` matches what
41
- * `createSchemaPlugin` computes from ke's onLoad), boots a throwaway ke
42
- * store, and runs the compiler. A {@link CompilationError} (schema
43
- * composition failed) is converted into a `"failed"` result carrying its
44
- * diagnostics; the store is always disposed.
38
+ * Hashes the raw source contents (the exact bytes ke loads, so the
39
+ * artifact's `sourcesHash` matches what `createSchemaPlugin` computes from
40
+ * ke's onLoad), boots a throwaway ke store, and runs the compiler. A
41
+ * {@link CompilationError} (schema composition failed) is converted into a
42
+ * `"failed"` result carrying its diagnostics; the store is always disposed.
45
43
  *
46
- * @note Impure — reads filesystem and boots a ke store (WASM + TTL).
44
+ * @note Impure — boots a ke store (WASM + TTL).
47
45
  *
48
- * @param options - Sources, prefixes, and working directory.
46
+ * @param options - In-memory sources and prefixes.
49
47
  * @returns The compile outcome with diagnostics, files, and sources hash.
50
- * @throws PragmaError with code `INVALID_INPUT` when no sources resolve.
48
+ * @throws PragmaError with code `INVALID_INPUT` when given no sources.
51
49
  * @throws PragmaError with code `STORE_ERROR` when the store fails to load.
52
50
  */
53
51
  export default async function compileSchema(
54
52
  options: CompileSchemaOptions,
55
53
  ): Promise<CompileSchemaResult> {
56
- const files = resolveSourceFiles(options.sources, options.cwd);
54
+ const { sources } = options;
57
55
 
58
- if (files.length === 0) {
59
- throw PragmaError.invalidInput("sources", options.sources.join(", "), {
56
+ if (sources.length === 0) {
57
+ throw PragmaError.invalidInput("sources", "(empty)", {
60
58
  recovery: {
61
- message: "Provide at least one existing TTL file or glob pattern.",
59
+ message: "Provide at least one TTL source.",
62
60
  },
63
61
  });
64
62
  }
65
63
 
66
- const contents = files.map((file) => readFileSync(file, "utf-8"));
67
- const sourcesHash = hashSources(contents);
64
+ const sourcesHash = hashSources(sources.map((source) => source.content));
65
+ const files = sources.map((source) => source.path ?? "inline");
68
66
 
69
67
  let store: Awaited<ReturnType<typeof createStore>>;
70
68
  try {
71
69
  store = await createStore({
72
- sources: [...files],
70
+ sources: [...sources],
73
71
  prefixes: options.prefixes,
74
72
  cwd: options.cwd,
75
73
  });
@@ -0,0 +1,50 @@
1
+ import { afterEach, describe, expect, it } from "vitest";
2
+ import { EX_NAMESPACE, GRAPHQL_CLEAN_TTL } from "#testing";
3
+ import { PREFIX_MAP } from "../../shared/prefixes.js";
4
+ import createSchemaServer, { type SchemaServer } from "./createSchemaServer.js";
5
+
6
+ const PREFIXES = { ...PREFIX_MAP, ex: EX_NAMESPACE };
7
+
8
+ describe("createSchemaServer", () => {
9
+ let server: SchemaServer | undefined;
10
+
11
+ afterEach(() => {
12
+ server?.dispose();
13
+ server = undefined;
14
+ });
15
+
16
+ it("compiles inline sources and serves an introspectable schema", async () => {
17
+ server = await createSchemaServer({
18
+ sources: [
19
+ { content: GRAPHQL_CLEAN_TTL, format: "turtle", path: "clean.ttl" },
20
+ ],
21
+ prefixes: PREFIXES,
22
+ });
23
+
24
+ const response = await server.handler(
25
+ new Request("http://localhost/graphql", {
26
+ method: "POST",
27
+ headers: { "content-type": "application/json" },
28
+ body: JSON.stringify({
29
+ query: "{ __schema { queryType { name } } }",
30
+ }),
31
+ }),
32
+ );
33
+
34
+ expect(response.status).toBe(200);
35
+ const body = (await response.json()) as {
36
+ data?: { __schema?: { queryType?: { name?: string } } };
37
+ };
38
+ expect(body.data?.__schema?.queryType?.name).toBe("Query");
39
+ expect(Array.isArray(server.diagnostics)).toBe(true);
40
+ });
41
+
42
+ it("throws a STORE_ERROR on invalid TTL", async () => {
43
+ await expect(
44
+ createSchemaServer({
45
+ sources: [{ content: "not turtle .", format: "turtle", path: "x.ttl" }],
46
+ prefixes: PREFIXES,
47
+ }),
48
+ ).rejects.toMatchObject({ code: "STORE_ERROR" });
49
+ });
50
+ });
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Boot a persistent ke store over in-memory TTL sources with the
3
+ * ke-graphql schema plugin attached, and build the `/http` fetch handler
4
+ * (GraphiQL + GraphQL, CORS, incremental delivery) over the compiled
5
+ * schema. The caller owns the returned `dispose`.
6
+ *
7
+ * @note Impure — boots a ke store (WASM + TTL) and compiles the schema.
8
+ */
9
+
10
+ import { createStore, type InlineSource, type Plugin } from "@canonical/ke";
11
+ import {
12
+ createSchemaPlugin,
13
+ type Diagnostic,
14
+ type SchemaPluginApi,
15
+ } from "@canonical/ke-graphql";
16
+ import { createGraphQLHandler } from "@canonical/ke-graphql/http";
17
+ import { PragmaError } from "#error";
18
+
19
+ export interface CreateSchemaServerOptions {
20
+ /** In-memory TTL sources to load into the store. */
21
+ readonly sources: readonly InlineSource[];
22
+ /** Prefix map handed to both the ke store and the compiler. */
23
+ readonly prefixes: Readonly<Record<string, string>>;
24
+ }
25
+
26
+ export interface SchemaServer {
27
+ /** Runtime-neutral fetch handler over the compiled schema. */
28
+ readonly handler: ReturnType<typeof createGraphQLHandler>;
29
+ /** Compiler diagnostics produced while building the schema. */
30
+ readonly diagnostics: readonly Diagnostic[];
31
+ /** Releases the underlying ke store. */
32
+ readonly dispose: () => void;
33
+ }
34
+
35
+ export default async function createSchemaServer(
36
+ options: CreateSchemaServerOptions,
37
+ ): Promise<SchemaServer> {
38
+ const plugin = createSchemaPlugin({ incremental: true });
39
+
40
+ let store: Awaited<ReturnType<typeof createStore>>;
41
+ try {
42
+ store = await createStore({
43
+ sources: [...options.sources],
44
+ prefixes: options.prefixes,
45
+ // biome-ignore lint/suspicious/noExplicitAny: Plugin generic variance
46
+ plugins: [plugin as Plugin<any>],
47
+ });
48
+ } catch (error) {
49
+ throw PragmaError.storeError(
50
+ error instanceof Error ? error.message : String(error),
51
+ { recovery: { message: "Check the TTL syntax of the sources." } },
52
+ );
53
+ }
54
+
55
+ const api = store.api<SchemaPluginApi>("ke-graphql");
56
+ if (!api) {
57
+ store.dispose();
58
+ throw PragmaError.storeError("ke-graphql plugin did not register its API", {
59
+ recovery: { message: "This is an internal error; please report it." },
60
+ });
61
+ }
62
+
63
+ const handler = createGraphQLHandler(api.schema, {
64
+ context: () => api.createContext(store),
65
+ graphiql: true,
66
+ cors: true,
67
+ incremental: true,
68
+ });
69
+
70
+ return {
71
+ handler,
72
+ diagnostics: api.diagnostics,
73
+ dispose: () => store.dispose(),
74
+ };
75
+ }
@@ -5,3 +5,8 @@ export type {
5
5
  CompileSchemaResult,
6
6
  } from "./compileSchema.js";
7
7
  export { default as compileSchema } from "./compileSchema.js";
8
+ export type {
9
+ CreateSchemaServerOptions,
10
+ SchemaServer,
11
+ } from "./createSchemaServer.js";
12
+ export { default as createSchemaServer } from "./createSchemaServer.js";
@@ -1,6 +1,6 @@
1
1
  import { beforeEach, describe, expect, it, vi } from "vitest";
2
2
 
3
- const readConfigMock = vi.fn();
3
+ const readConfigLayersMock = vi.fn();
4
4
  const detectInstallSourceMock = vi.fn();
5
5
  const bootStoreMock = vi.fn();
6
6
  const checkRegistryVersionMock = vi.fn();
@@ -8,7 +8,7 @@ const collectStoreSummaryMock = vi.fn();
8
8
 
9
9
  vi.mock("#config", async (importOriginal) => ({
10
10
  ...(await importOriginal<typeof import("#config")>()),
11
- readConfig: readConfigMock,
11
+ readConfigLayers: readConfigLayersMock,
12
12
  }));
13
13
 
14
14
  vi.mock("#package-manager", async (importOriginal) => {
@@ -46,7 +46,18 @@ describe("collectInfo", () => {
46
46
  scope: "global",
47
47
  label: "bun (global)",
48
48
  });
49
- readConfigMock.mockReturnValue({ tier: "apps/lxd", channel: "normal" });
49
+ readConfigLayersMock.mockReturnValue({
50
+ config: { tier: "apps/lxd", channel: "normal" },
51
+ origins: {
52
+ tier: "project",
53
+ channel: "default",
54
+ packages: "default",
55
+ trace: "default",
56
+ framework: "default",
57
+ },
58
+ global: { path: "/home/u/.config/pragma/config.json", exists: false },
59
+ project: { path: "/repo/pragma.config.json", exists: true },
60
+ });
50
61
  });
51
62
 
52
63
  it("includes update information and store summary when available", async () => {
@@ -1,15 +1,14 @@
1
- import { readConfig } from "#config";
1
+ import { readConfigLayers } from "#config";
2
2
  import { VERSION } from "#constants";
3
3
  import { detectInstallSource, PM_COMMANDS } from "#package-manager";
4
- import {
5
- type PackageRef,
6
- parsePackageEntry,
4
+ import type {
5
+ PackageRef,
6
+ RawPackageEntry,
7
7
  } from "../../refs/operations/parseRef.js";
8
- import readGlobalRefs from "../../refs/operations/readGlobalRefs.js";
9
8
  import { bootStore } from "../../shared/bootStore.js";
10
9
  import { CHANNEL_RELEASES } from "../../shared/filters/buildChannelFilter.js";
11
10
  import { resolveTierChain } from "../../shared/filters/buildTierFilter.js";
12
- import { DEFAULT_PACKAGES } from "../../shared/packages.js";
11
+ import { mergeAndParseRefs } from "../../shared/mergeAndParseRefs.js";
13
12
  import type { InfoData, PackageRefSummary } from "../types.js";
14
13
  import checkRegistryVersion from "./checkRegistryVersion.js";
15
14
  import { collectStoreSummary } from "./collectStoreSummary.js";
@@ -28,7 +27,8 @@ import { collectStoreSummary } from "./collectStoreSummary.js";
28
27
  export default async function collectInfo(cwd: string): Promise<InfoData> {
29
28
  const install = detectInstallSource();
30
29
  const pm = install.packageManager;
31
- const config = readConfig(cwd);
30
+ const layers = readConfigLayers(cwd);
31
+ const config = layers.config;
32
32
  const tierChain = resolveTierChain(config.tier);
33
33
  const channelReleases = CHANNEL_RELEASES[config.channel];
34
34
 
@@ -65,7 +65,9 @@ export default async function collectInfo(cwd: string): Promise<InfoData> {
65
65
  version: VERSION,
66
66
  pm,
67
67
  installSource: install.label,
68
- configPath: "pragma.config.json",
68
+ configPath: layers.project.exists
69
+ ? layers.project.path
70
+ : layers.global.path,
69
71
  tier: config.tier,
70
72
  tierChain,
71
73
  channel: config.channel,
@@ -78,14 +80,9 @@ export default async function collectInfo(cwd: string): Promise<InfoData> {
78
80
  }
79
81
 
80
82
  function collectPackageRefSummaries(
81
- projectPackages?: ReadonlyArray<
82
- string | { readonly name: string; readonly source?: string }
83
- >,
83
+ configPackages?: ReadonlyArray<RawPackageEntry>,
84
84
  ): PackageRefSummary[] {
85
- const entries = projectPackages ?? readGlobalRefs();
86
- const raw = entries.length > 0 ? entries : DEFAULT_PACKAGES.map((pkg) => pkg);
87
-
88
- return raw.map((entry) => refToSummary(parsePackageEntry(entry)));
85
+ return mergeAndParseRefs(configPackages).map(refToSummary);
89
86
  }
90
87
 
91
88
  function refToSummary(ref: PackageRef): PackageRefSummary {
@@ -26,10 +26,10 @@ describe("buildCapabilitiesData", () => {
26
26
  expect(data.discovery_sequence[2]?.tool).toBe("domain tools");
27
27
  });
28
28
 
29
- it("includes all 29 tools from TOOL_CATALOG", () => {
29
+ it("includes every tool from TOOL_CATALOG", () => {
30
30
  const data = buildCapabilitiesData();
31
31
  expect(data.tools).toHaveLength(TOOL_CATALOG.length);
32
- expect(data.tools).toHaveLength(29);
32
+ expect(data.tools).toHaveLength(30);
33
33
  });
34
34
 
35
35
  it("every tool has a non-empty use_when", () => {
@@ -41,7 +41,7 @@ describe("buildCapabilitiesData", () => {
41
41
 
42
42
  it("counts match tool catalog category lengths", () => {
43
43
  const data = buildCapabilitiesData();
44
- expect(data.counts.total).toBe(29);
44
+ expect(data.counts.total).toBe(30);
45
45
  expect(data.counts.read).toBe(
46
46
  TOOL_CATALOG.filter((t) => t.category === "read").length,
47
47
  );
@@ -79,7 +79,7 @@ describe("buildCapabilitiesCommand", () => {
79
79
  expect(text).toContain("Discovery Sequence");
80
80
  expect(text).toContain("block_list");
81
81
  expect(text).toContain("read");
82
- expect(text).toContain("29 tools");
82
+ expect(text).toContain("30 tools");
83
83
  });
84
84
 
85
85
  it("does not require a store or runtime", async () => {
@@ -100,6 +100,12 @@ export const TOOL_CATALOG: readonly ToolEntry[] = [
100
100
  category: "read",
101
101
  use_when: "Discovering agent skills provided by installed packages",
102
102
  },
103
+ {
104
+ name: "skill_lookup",
105
+ category: "read",
106
+ use_when:
107
+ "Loading full SKILL.md instructions for a specific agent skill by name",
108
+ },
103
109
  {
104
110
  name: "block_sample",
105
111
  category: "read",
@@ -1,42 +1,9 @@
1
- /**
2
- * Wires the `pragma modifier list` CLI command.
3
- *
4
- * Lists all modifier families with their allowed values. Throws a
5
- * structured recovery error when no families are found.
6
- */
7
-
8
- import {
9
- type CommandDefinition,
10
- createOutputResult,
11
- } from "@canonical/cli-core";
12
- import { createListView } from "#tui";
1
+ import type { CommandDefinition } from "@canonical/cli-core";
13
2
  import type { PragmaContext } from "../../shared/context.js";
14
- import { selectFormatter } from "../../shared/formatters.js";
15
- import { listFormatters } from "../formatters/index.js";
16
- import { modifierConfig } from "../modifierConfig.js";
17
- import { resolveModifierList } from "../orchestration/index.js";
3
+ import { compileReadCommand } from "../../shared/stories/index.js";
4
+ import { modifierListStory } from "../stories.js";
18
5
 
6
+ /** Wire the `pragma modifier list` CLI command from the modifier list story. */
19
7
  export default function listCommand(ctx: PragmaContext): CommandDefinition {
20
- return {
21
- path: ["modifier", "list"],
22
- description: "List all modifier families",
23
- parameters: [],
24
- meta: {
25
- examples: ["pragma modifier list", "pragma modifier list --llm"],
26
- },
27
- async execute() {
28
- const resolution = await resolveModifierList(ctx);
29
-
30
- return createOutputResult([...resolution.items], {
31
- plain: selectFormatter(ctx, listFormatters),
32
- ink: (data) =>
33
- createListView({
34
- heading: "Modifiers",
35
- domain: "modifier",
36
- items: data,
37
- columns: modifierConfig.listColumns,
38
- }),
39
- });
40
- },
41
- };
8
+ return compileReadCommand(ctx, modifierListStory);
42
9
  }