@canonical/pragma-cli 0.27.0 → 0.29.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 (169) hide show
  1. package/dist/pragma +0 -0
  2. package/package.json +19 -17
  3. package/src/config/readConfig.ts +38 -2
  4. package/src/config/types.ts +12 -1
  5. package/src/config/writeConfig.ts +12 -0
  6. package/src/constants.ts +12 -1
  7. package/src/domains/block/commands/index.ts +2 -1
  8. package/src/domains/block/commands/sample.ts +55 -0
  9. package/src/domains/block/formatters/index.ts +2 -1
  10. package/src/domains/block/formatters/sample.ts +21 -0
  11. package/src/domains/block/index.ts +3 -3
  12. package/src/domains/block/mcp/specs.ts +48 -0
  13. package/src/domains/block/operations/index.ts +2 -1
  14. package/src/domains/block/operations/lookup.ts +9 -0
  15. package/src/domains/block/operations/sample.ts +41 -0
  16. package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
  17. package/src/domains/block/orchestration/resolveBlockLookup.ts +10 -3
  18. package/src/domains/config/commands/framework.test.ts +88 -0
  19. package/src/domains/config/commands/framework.ts +85 -0
  20. package/src/domains/config/commands/index.ts +2 -0
  21. package/src/domains/config/commands/trace.test.ts +148 -0
  22. package/src/domains/config/commands/trace.ts +80 -0
  23. package/src/domains/config/formatters/framework.ts +34 -0
  24. package/src/domains/config/formatters/index.ts +2 -0
  25. package/src/domains/config/formatters/trace.ts +28 -0
  26. package/src/domains/config/index.ts +15 -2
  27. package/src/domains/config/operations/index.ts +1 -0
  28. package/src/domains/config/operations/validateFramework.ts +24 -0
  29. package/src/domains/config/tasks/index.ts +2 -0
  30. package/src/domains/config/tasks/setChannel.ts +7 -1
  31. package/src/domains/config/tasks/setFramework.ts +40 -0
  32. package/src/domains/config/tasks/setTier.ts +7 -1
  33. package/src/domains/config/tasks/setTrace.ts +38 -0
  34. package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
  35. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +35 -41
  36. package/src/domains/doctor/operations/checks.test.ts +4 -1
  37. package/src/domains/doctor/operations/runChecks.test.ts +72 -0
  38. package/src/domains/doctor/operations/runChecks.ts +48 -12
  39. package/src/domains/graphql/commands/build.test.ts +194 -0
  40. package/src/domains/graphql/commands/build.ts +125 -0
  41. package/src/domains/graphql/commands/check.test.ts +139 -0
  42. package/src/domains/graphql/commands/check.ts +87 -0
  43. package/src/domains/graphql/commands/index.ts +4 -0
  44. package/src/domains/graphql/formatters/index.ts +3 -0
  45. package/src/domains/graphql/formatters/report.ts +78 -0
  46. package/src/domains/graphql/helpers/index.ts +4 -0
  47. package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
  48. package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
  49. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
  50. package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
  51. package/src/domains/graphql/index.ts +21 -0
  52. package/src/domains/graphql/operations/compileSchema.test.ts +119 -0
  53. package/src/domains/graphql/operations/compileSchema.ts +109 -0
  54. package/src/domains/graphql/operations/index.ts +7 -0
  55. package/src/domains/graphql/types.ts +29 -0
  56. package/src/domains/info/operations/collectInfo.test.ts +2 -2
  57. package/src/domains/info/operations/collectInfo.ts +4 -4
  58. package/src/domains/llm/capabilities.test.ts +91 -0
  59. package/src/domains/llm/commands/capabilities.ts +28 -0
  60. package/src/domains/llm/commands/index.ts +1 -0
  61. package/src/domains/llm/data/commandReference.ts +90 -18
  62. package/src/domains/llm/data/decisionTrees.ts +15 -13
  63. package/src/domains/llm/data/index.ts +2 -0
  64. package/src/domains/llm/data/toolCatalog.ts +184 -0
  65. package/src/domains/llm/formatters/capabilities.ts +50 -0
  66. package/src/domains/llm/formatters/index.ts +1 -0
  67. package/src/domains/llm/index.ts +3 -2
  68. package/src/domains/llm/mcp/index.ts +1 -1
  69. package/src/domains/llm/mcp/specs.ts +59 -52
  70. package/src/domains/llm/types.ts +40 -0
  71. package/src/domains/modifier/commands/index.ts +2 -1
  72. package/src/domains/modifier/commands/sample.ts +53 -0
  73. package/src/domains/modifier/formatters/index.ts +2 -1
  74. package/src/domains/modifier/formatters/sample.ts +15 -0
  75. package/src/domains/modifier/index.ts +7 -3
  76. package/src/domains/modifier/mcp/specs.ts +47 -0
  77. package/src/domains/modifier/operations/index.ts +2 -1
  78. package/src/domains/modifier/operations/lookup.ts +9 -0
  79. package/src/domains/modifier/operations/sample.ts +32 -0
  80. package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
  81. package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
  82. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +14 -2
  83. package/src/domains/refs/operations/index.ts +1 -1
  84. package/src/domains/refs/operations/paths.ts +29 -1
  85. package/src/domains/refs/operations/updateRefs.test.ts +6 -3
  86. package/src/domains/refs/operations/updateRefs.ts +3 -2
  87. package/src/domains/shared/bootStore.ts +88 -49
  88. package/src/domains/shared/contracts.ts +1 -0
  89. package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
  90. package/src/domains/shared/loaders/bundledLoader.ts +108 -0
  91. package/src/domains/shared/loaders/gitLoader.ts +31 -0
  92. package/src/domains/shared/loaders/index.ts +6 -0
  93. package/src/domains/shared/loaders/localLoader.ts +73 -0
  94. package/src/domains/shared/loaders/readPackageDir.ts +112 -0
  95. package/src/domains/shared/lookupMany.ts +9 -1
  96. package/src/domains/shared/packages.ts +21 -120
  97. package/src/domains/shared/pickRandom.ts +22 -0
  98. package/src/domains/shared/runtime.ts +6 -3
  99. package/src/domains/shared/sampleFormatters.ts +47 -0
  100. package/src/domains/shared/semanticPackage.ts +96 -0
  101. package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
  102. package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
  103. package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
  104. package/src/domains/shared/suggestions/expandGlob.ts +51 -0
  105. package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
  106. package/src/domains/shared/suggestions/index.ts +7 -0
  107. package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
  108. package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
  109. package/src/domains/shared/suggestions/suggestNames.ts +97 -0
  110. package/src/domains/shared/types/index.ts +1 -0
  111. package/src/domains/shared/types/runtime.ts +4 -1
  112. package/src/domains/shared/types/sample.ts +17 -0
  113. package/src/domains/skill/helpers/resolveSkillSources.ts +49 -16
  114. package/src/domains/standard/commands/index.ts +2 -1
  115. package/src/domains/standard/commands/sample.ts +57 -0
  116. package/src/domains/standard/formatters/index.ts +2 -1
  117. package/src/domains/standard/formatters/sample.ts +15 -0
  118. package/src/domains/standard/index.ts +8 -1
  119. package/src/domains/standard/mcp/specs.ts +48 -1
  120. package/src/domains/standard/operations/index.ts +2 -1
  121. package/src/domains/standard/operations/lookup.ts +9 -0
  122. package/src/domains/standard/operations/sample.ts +35 -0
  123. package/src/domains/standard/orchestration/resolveStandardLookup.ts +14 -2
  124. package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
  125. package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
  126. package/src/domains/token/commands/index.ts +2 -1
  127. package/src/domains/token/commands/sample.ts +53 -0
  128. package/src/domains/token/formatters/index.ts +2 -1
  129. package/src/domains/token/formatters/sample.ts +16 -0
  130. package/src/domains/token/index.ts +8 -1
  131. package/src/domains/token/mcp/specs.ts +47 -1
  132. package/src/domains/token/operations/index.ts +2 -1
  133. package/src/domains/token/operations/lookup.ts +9 -0
  134. package/src/domains/token/operations/sample.ts +32 -0
  135. package/src/domains/token/orchestration/resolveTokenLookup.ts +14 -2
  136. package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
  137. package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
  138. package/src/domains/trace/commands/clear.ts +71 -0
  139. package/src/domains/trace/commands/index.ts +5 -0
  140. package/src/domains/trace/commands/sessions.ts +32 -0
  141. package/src/domains/trace/commands/show.ts +145 -0
  142. package/src/domains/trace/formatters/index.ts +9 -0
  143. package/src/domains/trace/formatters/sessions.ts +80 -0
  144. package/src/domains/trace/formatters/show.ts +122 -0
  145. package/src/domains/trace/index.ts +17 -0
  146. package/src/domains/trace/operations/index.ts +8 -0
  147. package/src/domains/trace/operations/listSessions.test.ts +55 -0
  148. package/src/domains/trace/operations/listSessions.ts +52 -0
  149. package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
  150. package/src/domains/trace/operations/readTraceLog.ts +104 -0
  151. package/src/domains/trace/tracePlugin.test.ts +171 -0
  152. package/src/domains/trace/tracePlugin.ts +159 -0
  153. package/src/domains/trace/types.ts +65 -0
  154. package/src/error/PragmaError.ts +15 -2
  155. package/src/error/types.ts +15 -1
  156. package/src/mcp/error/serializeErrorPayload.ts +7 -0
  157. package/src/mcp/error/types.ts +8 -0
  158. package/src/mcp/registerResources.test.ts +38 -16
  159. package/src/mcp/registerResources.ts +57 -8
  160. package/src/mcp/tools/registerTools.test.ts +22 -7
  161. package/src/mcp/utils/wrapTool.test.ts +1 -0
  162. package/src/pipeline/collectCommands.ts +7 -1
  163. package/src/pipeline/renderError.ts +15 -0
  164. package/src/pipeline/resolveCommandKind.ts +8 -1
  165. package/src/pipeline/runCli.ts +12 -1
  166. package/src/testing/createTestMcpClient.ts +1 -0
  167. package/src/testing/graphqlFixtures.ts +72 -0
  168. package/src/testing/index.ts +7 -0
  169. package/src/testing/integration/mcp-surface.test.ts +11 -5
@@ -0,0 +1,139 @@
1
+ import { mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import {
6
+ EX_PREFIX_ENTRY,
7
+ GRAPHQL_CLEAN_TTL,
8
+ GRAPHQL_ERROR_TTL,
9
+ GRAPHQL_FATAL_TTL,
10
+ } from "#testing";
11
+ import type { PragmaContext } from "../../shared/context.js";
12
+ import type { GraphqlCompileReport } from "../types.js";
13
+ import checkCommand from "./check.js";
14
+
15
+ let dir: string;
16
+ let stdoutSpy: ReturnType<typeof vi.spyOn>;
17
+
18
+ beforeEach(() => {
19
+ dir = mkdtempSync(join(tmpdir(), "pragma-graphql-check-"));
20
+ writeFileSync(join(dir, "clean.ttl"), GRAPHQL_CLEAN_TTL);
21
+ writeFileSync(join(dir, "error.ttl"), GRAPHQL_ERROR_TTL);
22
+ writeFileSync(join(dir, "fatal.ttl"), GRAPHQL_FATAL_TTL);
23
+ stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
24
+ });
25
+
26
+ afterEach(() => {
27
+ stdoutSpy.mockRestore();
28
+ rmSync(dir, { recursive: true, force: true });
29
+ });
30
+
31
+ function makeCtx(overrides: Partial<PragmaContext> = {}): PragmaContext {
32
+ return {
33
+ cwd: dir,
34
+ globalFlags: { llm: false, format: "text" as const, verbose: false },
35
+ store: {} as PragmaContext["store"],
36
+ config: { tier: undefined, channel: "normal" },
37
+ ...overrides,
38
+ } as PragmaContext;
39
+ }
40
+
41
+ function printedOutput(): string {
42
+ return stdoutSpy.mock.calls.map((call) => call[0]).join("");
43
+ }
44
+
45
+ describe("graphql check command", () => {
46
+ it("passes a clean ontology with a summary line", async () => {
47
+ const ctx = makeCtx();
48
+ const result = await checkCommand.execute(
49
+ { sources: ["clean.ttl"], prefix: [EX_PREFIX_ENTRY] },
50
+ ctx,
51
+ );
52
+
53
+ expect(result.tag).toBe("output");
54
+ if (result.tag !== "output") throw new Error("Expected output result");
55
+ const text = result.render.plain(result.value);
56
+ expect(text).toContain("1 source file: 0 errors, 0 warnings, 0 info");
57
+ });
58
+
59
+ it("exits non-zero and prints diagnostics on error diagnostics", async () => {
60
+ const ctx = makeCtx();
61
+ const result = await checkCommand.execute(
62
+ { sources: ["error.ttl"], prefix: [EX_PREFIX_ENTRY] },
63
+ ctx,
64
+ );
65
+
66
+ expect(result.tag).toBe("exit");
67
+ if (result.tag !== "exit") throw new Error("Expected exit result");
68
+ expect(result.code).toBe(1);
69
+
70
+ const printed = printedOutput();
71
+ expect(printed).toContain("M001");
72
+ expect(printed).toContain("error");
73
+ expect(printed).toContain("1 error, 2 warnings, 0 info");
74
+ });
75
+
76
+ it("exits non-zero when compilation fails entirely", async () => {
77
+ const ctx = makeCtx();
78
+ const result = await checkCommand.execute(
79
+ { sources: ["fatal.ttl"], prefix: [EX_PREFIX_ENTRY] },
80
+ ctx,
81
+ );
82
+
83
+ expect(result.tag).toBe("exit");
84
+ if (result.tag !== "exit") throw new Error("Expected exit result");
85
+ expect(result.code).toBe(1);
86
+ expect(printedOutput()).toContain("C003");
87
+ });
88
+
89
+ it("passes with warnings when prefixes are not registered", async () => {
90
+ const ctx = makeCtx();
91
+ const result = await checkCommand.execute({ sources: ["clean.ttl"] }, ctx);
92
+
93
+ expect(result.tag).toBe("output");
94
+ if (result.tag !== "output") throw new Error("Expected output result");
95
+ const text = result.render.plain(result.value);
96
+ expect(text).toContain("1 warning");
97
+ expect(text).toContain("0 errors");
98
+ });
99
+
100
+ it("writes no artifacts", async () => {
101
+ const ctx = makeCtx();
102
+ const before = readdirSync(dir).sort();
103
+ await checkCommand.execute(
104
+ { sources: ["clean.ttl"], prefix: [EX_PREFIX_ENTRY] },
105
+ ctx,
106
+ );
107
+
108
+ expect(readdirSync(dir).sort()).toEqual(before);
109
+ });
110
+
111
+ it("renders json output", async () => {
112
+ const ctx = makeCtx({
113
+ globalFlags: { llm: false, format: "json" as const, verbose: false },
114
+ });
115
+ const result = await checkCommand.execute(
116
+ { sources: ["clean.ttl"], prefix: [EX_PREFIX_ENTRY] },
117
+ ctx,
118
+ );
119
+
120
+ expect(result.tag).toBe("output");
121
+ if (result.tag !== "output") throw new Error("Expected output result");
122
+ const parsed = JSON.parse(
123
+ result.render.plain(result.value),
124
+ ) as GraphqlCompileReport;
125
+ expect(parsed.diagnostics).toEqual([]);
126
+ expect(parsed.artifacts).toBeUndefined();
127
+ });
128
+
129
+ it("throws structured error when sources are missing", async () => {
130
+ const ctx = makeCtx();
131
+
132
+ await expect(checkCommand.execute({}, ctx)).rejects.toMatchObject({
133
+ code: "INVALID_INPUT",
134
+ recovery: {
135
+ message: "Provide at least one TTL file or glob pattern.",
136
+ },
137
+ });
138
+ });
139
+ });
@@ -0,0 +1,87 @@
1
+ import {
2
+ type CommandDefinition,
3
+ type CommandResult,
4
+ createExitResult,
5
+ createOutputResult,
6
+ } from "@canonical/cli-core";
7
+ import { PragmaError } from "#error";
8
+ import { selectFormatter } from "../../shared/formatters.js";
9
+ import { reportFormatters } from "../formatters/index.js";
10
+ import { parsePrefixes } from "../helpers/index.js";
11
+ import { compileSchema } from "../operations/index.js";
12
+ import type { GraphqlCompileReport } from "../types.js";
13
+
14
+ /** Narrow an unknown param to a string array. */
15
+ function readStringArray(value: unknown): string[] {
16
+ return Array.isArray(value)
17
+ ? value.filter((item): item is string => typeof item === "string")
18
+ : [];
19
+ }
20
+
21
+ /**
22
+ * Builds the `pragma graphql check` command definition.
23
+ *
24
+ * Runs the same compile as `graphql build` but writes no artifacts —
25
+ * a CI gate. Prints all compiler diagnostics and exits non-zero when
26
+ * compilation fails or any error-severity diagnostic is produced.
27
+ */
28
+ const checkCommand: CommandDefinition = {
29
+ path: ["graphql", "check"],
30
+ description: "Compile TTL sources and fail on error diagnostics",
31
+ parameters: [
32
+ {
33
+ name: "sources",
34
+ description: "TTL source files or glob patterns",
35
+ type: "multiselect",
36
+ positional: true,
37
+ required: true,
38
+ },
39
+ {
40
+ name: "prefix",
41
+ description: "Ontology prefix as name=namespace (repeatable)",
42
+ type: "multiselect",
43
+ },
44
+ ],
45
+ meta: {
46
+ examples: [
47
+ "pragma graphql check ontology.ttl",
48
+ 'pragma graphql check "data/*.ttl" --prefix ds=https://ds.canonical.com/',
49
+ ],
50
+ },
51
+ async execute(params, ctx): Promise<CommandResult> {
52
+ const sources = readStringArray(params.sources);
53
+ if (sources.length === 0) {
54
+ throw PragmaError.invalidInput("sources", "(empty)", {
55
+ recovery: {
56
+ message: "Provide at least one TTL file or glob pattern.",
57
+ },
58
+ });
59
+ }
60
+
61
+ const prefixes = parsePrefixes(readStringArray(params.prefix));
62
+ const outcome = await compileSchema({ sources, prefixes, cwd: ctx.cwd });
63
+
64
+ const report: GraphqlCompileReport = {
65
+ diagnostics: outcome.diagnostics,
66
+ files: outcome.files,
67
+ sourcesHash: outcome.sourcesHash,
68
+ };
69
+
70
+ const hasErrors =
71
+ outcome.status === "failed" ||
72
+ outcome.diagnostics.some((d) => d.severity === "error");
73
+
74
+ if (hasErrors) {
75
+ process.stdout.write(
76
+ `${selectFormatter(ctx, reportFormatters)(report)}\n`,
77
+ );
78
+ return createExitResult(1);
79
+ }
80
+
81
+ return createOutputResult(report, {
82
+ plain: selectFormatter(ctx, reportFormatters),
83
+ });
84
+ },
85
+ };
86
+
87
+ export default checkCommand;
@@ -0,0 +1,4 @@
1
+ /** @module Barrel for graphql command builders. */
2
+
3
+ export { default as buildCommand } from "./build.js";
4
+ export { default as checkCommand } from "./check.js";
@@ -0,0 +1,3 @@
1
+ /** @module Barrel for graphql formatters. */
2
+
3
+ export { default as reportFormatters } from "./report.js";
@@ -0,0 +1,78 @@
1
+ import type { Diagnostic } from "@canonical/ke-graphql";
2
+ import chalk from "chalk";
3
+ import type { Formatters } from "../../shared/formatters.js";
4
+ import type { GraphqlCompileReport } from "../types.js";
5
+
6
+ /** Count diagnostics of one severity. */
7
+ function countSeverity(
8
+ diagnostics: readonly Diagnostic[],
9
+ severity: Diagnostic["severity"],
10
+ ): number {
11
+ return diagnostics.filter((d) => d.severity === severity).length;
12
+ }
13
+
14
+ /** Pluralize a count: `1 error`, `2 errors`. */
15
+ function plural(count: number, word: string): string {
16
+ return `${count} ${word}${count !== 1 ? "s" : ""}`;
17
+ }
18
+
19
+ /** One-line summary of diagnostic counts and compiled sources. */
20
+ function summaryLine(report: GraphqlCompileReport): string {
21
+ const errors = countSeverity(report.diagnostics, "error");
22
+ const warnings = countSeverity(report.diagnostics, "warning");
23
+ const infos = countSeverity(report.diagnostics, "info");
24
+ return `${plural(report.files.length, "source file")}: ${plural(errors, "error")}, ${plural(warnings, "warning")}, ${infos} info`;
25
+ }
26
+
27
+ function colorSeverity(severity: Diagnostic["severity"]): string {
28
+ if (severity === "error") return chalk.red(severity);
29
+ if (severity === "warning") return chalk.yellow(severity);
30
+ return chalk.dim(severity);
31
+ }
32
+
33
+ /**
34
+ * Formatters for `pragma graphql build` and `pragma graphql check` output.
35
+ *
36
+ * - **plain**: one line per diagnostic (severity, code, message, source),
37
+ * a summary line with counts, and `Wrote <path>` lines for artifacts.
38
+ * - **llm**: condensed Markdown under a `## GraphQL Compile` heading.
39
+ * - **json**: serialized {@link GraphqlCompileReport} as indented JSON.
40
+ */
41
+ const formatters: Formatters<GraphqlCompileReport> = {
42
+ plain(report) {
43
+ const lines = report.diagnostics.map(
44
+ (d) =>
45
+ `${colorSeverity(d.severity)} ${chalk.bold(d.code)}: ${d.message}${d.source ? ` (${d.source})` : ""}`,
46
+ );
47
+ lines.push(summaryLine(report));
48
+ if (report.artifacts) {
49
+ lines.push(`Wrote ${report.artifacts.sdl}`);
50
+ lines.push(`Wrote ${report.artifacts.extraction}`);
51
+ }
52
+ return lines.join("\n");
53
+ },
54
+
55
+ llm(report) {
56
+ const lines = ["## GraphQL Compile", ""];
57
+ for (const d of report.diagnostics) {
58
+ lines.push(
59
+ `- **${d.severity}** \`${d.code}\`: ${d.message}${d.source ? ` (\`${d.source}\`)` : ""}`,
60
+ );
61
+ }
62
+ if (report.diagnostics.length > 0) {
63
+ lines.push("");
64
+ }
65
+ lines.push(`**Summary:** ${summaryLine(report)}`);
66
+ if (report.artifacts) {
67
+ lines.push(`**SDL:** \`${report.artifacts.sdl}\``);
68
+ lines.push(`**Extraction:** \`${report.artifacts.extraction}\``);
69
+ }
70
+ return lines.join("\n");
71
+ },
72
+
73
+ json(report) {
74
+ return JSON.stringify(report, null, 2);
75
+ },
76
+ };
77
+
78
+ export default formatters;
@@ -0,0 +1,4 @@
1
+ /** @module Barrel for graphql helpers. */
2
+
3
+ export { default as parsePrefixes } from "./parsePrefixes.js";
4
+ export { default as resolveSourceFiles } from "./resolveSourceFiles.js";
@@ -0,0 +1,44 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { PREFIX_MAP } from "../../shared/prefixes.js";
3
+ import parsePrefixes from "./parsePrefixes.js";
4
+
5
+ describe("parsePrefixes", () => {
6
+ it("returns the default prefix map for no entries", () => {
7
+ const prefixes = parsePrefixes([]);
8
+ expect(prefixes).toEqual(PREFIX_MAP);
9
+ });
10
+
11
+ it("merges entries on top of the defaults", () => {
12
+ const prefixes = parsePrefixes(["ex=http://example.org/"]);
13
+ expect(prefixes.ex).toBe("http://example.org/");
14
+ expect(prefixes.ds).toBe(PREFIX_MAP.ds);
15
+ });
16
+
17
+ it("lets entries override default prefixes", () => {
18
+ const prefixes = parsePrefixes(["ds=http://other.example/"]);
19
+ expect(prefixes.ds).toBe("http://other.example/");
20
+ });
21
+
22
+ it("splits on the first equals sign only", () => {
23
+ const prefixes = parsePrefixes(["ex=http://example.org/?q=1"]);
24
+ expect(prefixes.ex).toBe("http://example.org/?q=1");
25
+ });
26
+
27
+ it("throws structured error for an entry without equals sign", () => {
28
+ expect(() => parsePrefixes(["nonsense"])).toThrowError(
29
+ expect.objectContaining({ code: "INVALID_INPUT" }),
30
+ );
31
+ });
32
+
33
+ it("throws structured error for an entry without a name", () => {
34
+ expect(() => parsePrefixes(["=http://example.org/"])).toThrowError(
35
+ expect.objectContaining({ code: "INVALID_INPUT" }),
36
+ );
37
+ });
38
+
39
+ it("throws structured error for an entry without a namespace", () => {
40
+ expect(() => parsePrefixes(["ex="])).toThrowError(
41
+ expect.objectContaining({ code: "INVALID_INPUT" }),
42
+ );
43
+ });
44
+ });
@@ -0,0 +1,38 @@
1
+ import { PragmaError } from "#error";
2
+ import { PREFIX_MAP } from "../../shared/prefixes.js";
3
+
4
+ /**
5
+ * Parse repeated `--prefix name=namespace` entries into a prefix map.
6
+ *
7
+ * User-supplied prefixes are merged on top of pragma's default
8
+ * {@link PREFIX_MAP}, so `ds:` and the W3C vocabularies stay available
9
+ * unless explicitly overridden.
10
+ *
11
+ * @param entries - Raw `name=namespace` strings from the CLI.
12
+ * @returns The merged prefix map for store creation and compilation.
13
+ * @throws PragmaError with code `INVALID_INPUT` when an entry is malformed.
14
+ */
15
+ export default function parsePrefixes(
16
+ entries: readonly string[],
17
+ ): Record<string, string> {
18
+ const prefixes: Record<string, string> = { ...PREFIX_MAP };
19
+
20
+ for (const entry of entries) {
21
+ const separator = entry.indexOf("=");
22
+ const name = separator === -1 ? "" : entry.slice(0, separator).trim();
23
+ const namespace = separator === -1 ? "" : entry.slice(separator + 1).trim();
24
+
25
+ if (!name || !namespace) {
26
+ throw PragmaError.invalidInput("prefix", entry, {
27
+ recovery: {
28
+ message:
29
+ "Use name=namespace, e.g. --prefix ds=https://ds.canonical.com/.",
30
+ },
31
+ });
32
+ }
33
+
34
+ prefixes[name] = namespace;
35
+ }
36
+
37
+ return prefixes;
38
+ }
@@ -0,0 +1,56 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import resolveSourceFiles from "./resolveSourceFiles.js";
6
+
7
+ describe("resolveSourceFiles", () => {
8
+ let dir: string;
9
+
10
+ beforeEach(() => {
11
+ dir = mkdtempSync(join(tmpdir(), "pragma-graphql-sources-"));
12
+ writeFileSync(join(dir, "a.ttl"), "# a");
13
+ writeFileSync(join(dir, "b.ttl"), "# b");
14
+ writeFileSync(join(dir, "notes.txt"), "ignore me");
15
+ mkdirSync(join(dir, "nested"));
16
+ writeFileSync(join(dir, "nested", "c.ttl"), "# c");
17
+ });
18
+
19
+ afterEach(() => {
20
+ rmSync(dir, { recursive: true, force: true });
21
+ });
22
+
23
+ it("resolves relative literal paths against cwd", () => {
24
+ const files = resolveSourceFiles(["a.ttl"], dir);
25
+ expect(files).toEqual([join(dir, "a.ttl")]);
26
+ });
27
+
28
+ it("keeps absolute literal paths", () => {
29
+ const files = resolveSourceFiles([join(dir, "b.ttl")], dir);
30
+ expect(files).toEqual([join(dir, "b.ttl")]);
31
+ });
32
+
33
+ it("skips missing literal paths", () => {
34
+ const files = resolveSourceFiles(["missing.ttl"], dir);
35
+ expect(files).toEqual([]);
36
+ });
37
+
38
+ it("expands glob patterns", () => {
39
+ const files = resolveSourceFiles(["*.ttl"], dir);
40
+ expect(files.sort()).toEqual([join(dir, "a.ttl"), join(dir, "b.ttl")]);
41
+ });
42
+
43
+ it("expands recursive glob patterns", () => {
44
+ const files = resolveSourceFiles(["**/*.ttl"], dir);
45
+ expect(files.sort()).toEqual([
46
+ join(dir, "a.ttl"),
47
+ join(dir, "b.ttl"),
48
+ join(dir, "nested", "c.ttl"),
49
+ ]);
50
+ });
51
+
52
+ it("combines multiple patterns in order", () => {
53
+ const files = resolveSourceFiles(["b.ttl", "nested/*.ttl"], dir);
54
+ expect(files).toEqual([join(dir, "b.ttl"), join(dir, "nested", "c.ttl")]);
55
+ });
56
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Resolve TTL source patterns to absolute file paths.
3
+ *
4
+ * Mirrors ke's own source resolution (wildcard detection + `globSync`,
5
+ * literal paths checked for existence) so the files the CLI reads and
6
+ * hashes are exactly the files `createStore` loads.
7
+ *
8
+ * @note Impure — reads filesystem to expand globs and check existence.
9
+ */
10
+
11
+ import { existsSync, globSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+
14
+ /**
15
+ * Expand file paths and glob patterns into absolute file paths.
16
+ *
17
+ * @param patterns - Literal paths or glob patterns (`*`, `?`, `[`).
18
+ * @param cwd - Base directory for resolving relative patterns.
19
+ * @returns Absolute paths of all matched, existing files.
20
+ */
21
+ export default function resolveSourceFiles(
22
+ patterns: readonly string[],
23
+ cwd: string,
24
+ ): string[] {
25
+ const files: string[] = [];
26
+
27
+ for (const pattern of patterns) {
28
+ if (
29
+ pattern.includes("*") ||
30
+ pattern.includes("?") ||
31
+ pattern.includes("[")
32
+ ) {
33
+ for (const file of globSync(pattern, { cwd })) {
34
+ files.push(resolve(cwd, file));
35
+ }
36
+ } else {
37
+ const absPath = resolve(cwd, pattern);
38
+ if (existsSync(absPath)) {
39
+ files.push(absPath);
40
+ }
41
+ }
42
+ }
43
+
44
+ return files;
45
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * @module GraphQL domain barrel.
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.
7
+ */
8
+
9
+ import type { CommandDefinition } from "@canonical/cli-core";
10
+ import { buildCommand, checkCommand } from "./commands/index.js";
11
+
12
+ /**
13
+ * Returns all command definitions for the graphql domain.
14
+ *
15
+ * @returns An array of command definitions (`graphql build`, `graphql check`).
16
+ */
17
+ export function commands(): CommandDefinition[] {
18
+ return [buildCommand, checkCommand];
19
+ }
20
+
21
+ export { compileSchema } from "./operations/index.js";
@@ -0,0 +1,119 @@
1
+ import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { hashSources } from "@canonical/ke-graphql";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import {
7
+ EX_NAMESPACE,
8
+ GRAPHQL_CLEAN_TTL,
9
+ GRAPHQL_ERROR_TTL,
10
+ GRAPHQL_FATAL_TTL,
11
+ } from "#testing";
12
+ import { PREFIX_MAP } from "../../shared/prefixes.js";
13
+ import compileSchema from "./compileSchema.js";
14
+
15
+ const PREFIXES = { ...PREFIX_MAP, ex: EX_NAMESPACE };
16
+
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
+ });
30
+
31
+ it("compiles a clean ontology into SDL", async () => {
32
+ const outcome = await compileSchema({
33
+ sources: ["clean.ttl"],
34
+ prefixes: PREFIXES,
35
+ cwd: dir,
36
+ });
37
+
38
+ expect(outcome.status).toBe("ok");
39
+ expect(outcome.compiled?.sdl).toContain("type Thing");
40
+ expect(outcome.diagnostics).toEqual([]);
41
+ expect(outcome.files).toEqual([join(dir, "clean.ttl")]);
42
+ });
43
+
44
+ it("hashes the exact raw file contents", async () => {
45
+ const outcome = await compileSchema({
46
+ sources: ["clean.ttl"],
47
+ prefixes: PREFIXES,
48
+ cwd: dir,
49
+ });
50
+
51
+ const raw = readFileSync(join(dir, "clean.ttl"), "utf-8");
52
+ expect(outcome.sourcesHash).toBe(hashSources([raw]));
53
+ });
54
+
55
+ it("resolves glob patterns to source files", async () => {
56
+ const outcome = await compileSchema({
57
+ sources: ["clean*.ttl"],
58
+ prefixes: PREFIXES,
59
+ cwd: dir,
60
+ });
61
+
62
+ expect(outcome.status).toBe("ok");
63
+ expect(outcome.files).toEqual([join(dir, "clean.ttl")]);
64
+ });
65
+
66
+ it("surfaces error diagnostics without failing the compile", async () => {
67
+ const outcome = await compileSchema({
68
+ sources: ["error.ttl"],
69
+ prefixes: PREFIXES,
70
+ cwd: dir,
71
+ });
72
+
73
+ expect(outcome.status).toBe("ok");
74
+ expect(outcome.compiled).toBeDefined();
75
+ expect(
76
+ outcome.diagnostics.some(
77
+ (d) => d.severity === "error" && d.code === "M001",
78
+ ),
79
+ ).toBe(true);
80
+ });
81
+
82
+ it("converts CompilationError into a failed result with diagnostics", async () => {
83
+ const outcome = await compileSchema({
84
+ sources: ["fatal.ttl"],
85
+ prefixes: PREFIXES,
86
+ cwd: dir,
87
+ });
88
+
89
+ expect(outcome.status).toBe("failed");
90
+ expect(outcome.compiled).toBeUndefined();
91
+ expect(
92
+ outcome.diagnostics.some(
93
+ (d) => d.severity === "error" && d.code === "C003",
94
+ ),
95
+ ).toBe(true);
96
+ });
97
+
98
+ it("throws structured error when no sources resolve", async () => {
99
+ await expect(
100
+ compileSchema({
101
+ sources: ["missing.ttl"],
102
+ prefixes: PREFIXES,
103
+ cwd: dir,
104
+ }),
105
+ ).rejects.toMatchObject({ code: "INVALID_INPUT" });
106
+ });
107
+
108
+ it("throws structured error on invalid TTL syntax", async () => {
109
+ writeFileSync(join(dir, "broken.ttl"), "this is not turtle .");
110
+
111
+ await expect(
112
+ compileSchema({
113
+ sources: ["broken.ttl"],
114
+ prefixes: PREFIXES,
115
+ cwd: dir,
116
+ }),
117
+ ).rejects.toMatchObject({ code: "STORE_ERROR" });
118
+ });
119
+ });