@canonical/pragma-cli 0.27.1-experimental.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 +16 -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,171 @@
1
+ import { mkdtempSync, readFileSync, rmSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { createStore, sparql } from "@canonical/ke";
5
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
6
+ import { createTracePlugin } from "./tracePlugin.js";
7
+ import type { TraceRecord } from "./types.js";
8
+
9
+ describe("createTracePlugin", () => {
10
+ let traceDir: string;
11
+
12
+ beforeEach(() => {
13
+ traceDir = mkdtempSync(join(tmpdir(), "pragma-trace-test-"));
14
+ });
15
+
16
+ afterEach(() => {
17
+ rmSync(traceDir, { recursive: true, force: true });
18
+ });
19
+
20
+ it("records SELECT queries as NDJSON", async () => {
21
+ const plugin = createTracePlugin({
22
+ traceDir,
23
+ sessionId: "test-session",
24
+ });
25
+
26
+ const store = await createStore({
27
+ sources: [],
28
+ plugins: [plugin],
29
+ });
30
+
31
+ await store.query(sparql`SELECT ?s ?p ?o WHERE { ?s ?p ?o } LIMIT 5`);
32
+
33
+ store.dispose();
34
+
35
+ const logPath = join(traceDir, "test-session.ndjson");
36
+ const content = readFileSync(logPath, "utf-8");
37
+ const lines = content.trim().split("\n");
38
+
39
+ expect(lines.length).toBe(1);
40
+
41
+ const record = JSON.parse(lines[0]) as TraceRecord;
42
+ expect(record.sid).toBe("test-session");
43
+ expect(record.seq).toBe(0);
44
+ expect(record.type).toBe("select");
45
+ expect(record.qh).toMatch(/^[0-9a-f]{8}$/);
46
+ expect(record.ms).toBeGreaterThanOrEqual(0);
47
+ expect(record.vars).toBeDefined();
48
+ expect(record.count).toBeGreaterThanOrEqual(0);
49
+ expect(record.bindings).toBeDefined();
50
+ });
51
+
52
+ it("records ASK queries", async () => {
53
+ const plugin = createTracePlugin({
54
+ traceDir,
55
+ sessionId: "test-ask",
56
+ });
57
+
58
+ const store = await createStore({
59
+ sources: [],
60
+ plugins: [plugin],
61
+ });
62
+
63
+ await store.query(
64
+ sparql`ASK { <http://example.org/x> <http://example.org/p> <http://example.org/y> }`,
65
+ );
66
+
67
+ store.dispose();
68
+
69
+ const logPath = join(traceDir, "test-ask.ndjson");
70
+ const content = readFileSync(logPath, "utf-8");
71
+ const record = JSON.parse(content.trim()) as TraceRecord;
72
+
73
+ expect(record.type).toBe("ask");
74
+ expect(record.count).toBe(1);
75
+ expect(record.ask).toBe(false);
76
+ });
77
+
78
+ it("increments seq across queries", async () => {
79
+ const plugin = createTracePlugin({
80
+ traceDir,
81
+ sessionId: "test-seq",
82
+ });
83
+
84
+ const store = await createStore({
85
+ sources: [],
86
+ plugins: [plugin],
87
+ });
88
+
89
+ await store.query(sparql`SELECT ?s WHERE { ?s ?p ?o } LIMIT 1`);
90
+ await store.query(sparql`SELECT ?p WHERE { ?s ?p ?o } LIMIT 1`);
91
+ await store.query(sparql`SELECT ?o WHERE { ?s ?p ?o } LIMIT 1`);
92
+
93
+ store.dispose();
94
+
95
+ const logPath = join(traceDir, "test-seq.ndjson");
96
+ const content = readFileSync(logPath, "utf-8");
97
+ const records = content
98
+ .trim()
99
+ .split("\n")
100
+ .map((l) => JSON.parse(l) as TraceRecord);
101
+
102
+ expect(records).toHaveLength(3);
103
+ expect(records[0].seq).toBe(0);
104
+ expect(records[1].seq).toBe(1);
105
+ expect(records[2].seq).toBe(2);
106
+ });
107
+
108
+ it("exposes TraceApi via store.api()", async () => {
109
+ const plugin = createTracePlugin({
110
+ traceDir,
111
+ sessionId: "test-api",
112
+ });
113
+
114
+ const store = await createStore({
115
+ sources: [],
116
+ plugins: [plugin],
117
+ });
118
+
119
+ const api = store.api<import("./types.js").TraceApi>("trace");
120
+ expect(api).toBeDefined();
121
+ expect(api?.sessionId).toBe("test-api");
122
+ expect(api?.logPath).toBe(join(traceDir, "test-api.ndjson"));
123
+ expect(api?.queryCount).toBe(0);
124
+
125
+ await store.query(sparql`SELECT ?s WHERE { ?s ?p ?o } LIMIT 1`);
126
+ expect(api?.queryCount).toBe(1);
127
+
128
+ store.dispose();
129
+ });
130
+
131
+ it("caps bindings at maxBindings", async () => {
132
+ const plugin = createTracePlugin({
133
+ traceDir,
134
+ sessionId: "test-cap",
135
+ maxBindings: 2,
136
+ });
137
+
138
+ const store = await createStore({
139
+ sources: [],
140
+ plugins: [plugin],
141
+ });
142
+
143
+ // Load some data so we get results
144
+ // Even with empty store, the record should respect the cap
145
+ await store.query(sparql`SELECT ?s ?p ?o WHERE { ?s ?p ?o }`);
146
+
147
+ store.dispose();
148
+
149
+ const logPath = join(traceDir, "test-cap.ndjson");
150
+ const content = readFileSync(logPath, "utf-8");
151
+ const record = JSON.parse(content.trim()) as TraceRecord;
152
+
153
+ // bindings should be at most 2 (may be 0 with empty store)
154
+ expect(record.bindings).toBeDefined();
155
+ expect(record.bindings!.length).toBeLessThanOrEqual(2);
156
+ });
157
+
158
+ it("generates a session ID when not provided", async () => {
159
+ const plugin = createTracePlugin({ traceDir });
160
+
161
+ const store = await createStore({
162
+ sources: [],
163
+ plugins: [plugin],
164
+ });
165
+
166
+ const api = store.api<import("./types.js").TraceApi>("trace");
167
+ expect(api?.sessionId).toMatch(/^\d{8}-\d{6}-[0-9a-f]{4}$/);
168
+
169
+ store.dispose();
170
+ });
171
+ });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * KE plugin for query access tracing.
3
+ *
4
+ * Intercepts `onQuery` and `onResult` hooks to record every
5
+ * `store.query()` call as an NDJSON line in a session-scoped log file.
6
+ *
7
+ * Activation: conditional — only instantiated when `PRAGMA_TRACE=1` or
8
+ * `trace: true` in pragma.config.json.
9
+ * Correlation: closure variable `pending` — safe because KE calls
10
+ * `onQuery` → Oxigraph WASM (sync) → `onResult` with no interleaving.
11
+ */
12
+
13
+ import { appendFileSync, mkdirSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { definePlugin } from "@canonical/ke";
16
+ import type { TraceApi, TracePluginOptions, TraceRecord } from "./types.js";
17
+
18
+ // ---------------------------------------------------------------------------
19
+ // FNV-1a 32-bit hash
20
+ // ---------------------------------------------------------------------------
21
+
22
+ const FNV_OFFSET = 0x811c9dc5;
23
+ const FNV_PRIME = 0x01000193;
24
+
25
+ function fnv1a(input: string): string {
26
+ let hash = FNV_OFFSET;
27
+ for (let i = 0; i < input.length; i++) {
28
+ hash ^= input.charCodeAt(i);
29
+ hash = Math.imul(hash, FNV_PRIME);
30
+ }
31
+ return (hash >>> 0).toString(16).padStart(8, "0");
32
+ }
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Session ID generation
36
+ // ---------------------------------------------------------------------------
37
+
38
+ function generateSessionId(): string {
39
+ const now = new Date();
40
+ const date = [
41
+ now.getFullYear(),
42
+ String(now.getMonth() + 1).padStart(2, "0"),
43
+ String(now.getDate()).padStart(2, "0"),
44
+ ].join("");
45
+ const time = [
46
+ String(now.getHours()).padStart(2, "0"),
47
+ String(now.getMinutes()).padStart(2, "0"),
48
+ String(now.getSeconds()).padStart(2, "0"),
49
+ ].join("");
50
+ const rand = Math.random().toString(16).slice(2, 6);
51
+ return `${date}-${time}-${rand}`;
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Record builder
56
+ // ---------------------------------------------------------------------------
57
+
58
+ function buildRecord(
59
+ sessionId: string,
60
+ seq: number,
61
+ sparql: string,
62
+ startMs: number,
63
+ result: import("@canonical/ke").QueryResult,
64
+ maxBindings: number,
65
+ ): TraceRecord {
66
+ const ms = Math.round((performance.now() - startMs) * 100) / 100;
67
+ const qh = fnv1a(sparql);
68
+
69
+ const base = {
70
+ ts: new Date().toISOString(),
71
+ sid: sessionId,
72
+ seq,
73
+ qh,
74
+ q: sparql,
75
+ ms,
76
+ };
77
+
78
+ switch (result.type) {
79
+ case "select":
80
+ return {
81
+ ...base,
82
+ type: "select",
83
+ count: result.bindings.length,
84
+ vars: result.variables,
85
+ bindings: result.bindings.slice(0, maxBindings),
86
+ };
87
+ case "construct":
88
+ return {
89
+ ...base,
90
+ type: "construct",
91
+ count: result.triples.length,
92
+ triples: result.triples.slice(0, maxBindings).map((t) => ({
93
+ s: t.subject,
94
+ p: t.predicate,
95
+ o: t.object,
96
+ })),
97
+ };
98
+ case "ask":
99
+ return {
100
+ ...base,
101
+ type: "ask",
102
+ count: 1,
103
+ ask: result.result,
104
+ };
105
+ }
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // Plugin factory
110
+ // ---------------------------------------------------------------------------
111
+
112
+ export function createTracePlugin(options: TracePluginOptions) {
113
+ const maxBindings = options.maxBindings ?? 200;
114
+ const sessionId = options.sessionId ?? generateSessionId();
115
+ const logPath = join(options.traceDir, `${sessionId}.ndjson`);
116
+
117
+ let seq = 0;
118
+ let pendingQuery: string | undefined;
119
+ let pendingStart: number | undefined;
120
+
121
+ return definePlugin<TraceApi>({
122
+ name: "trace",
123
+
124
+ onReady() {
125
+ mkdirSync(options.traceDir, { recursive: true });
126
+ return {
127
+ sessionId,
128
+ logPath,
129
+ get queryCount() {
130
+ return seq;
131
+ },
132
+ };
133
+ },
134
+
135
+ onQuery(sparql) {
136
+ pendingQuery = sparql;
137
+ pendingStart = performance.now();
138
+ return undefined;
139
+ },
140
+
141
+ onResult(result) {
142
+ if (pendingQuery !== undefined && pendingStart !== undefined) {
143
+ const record = buildRecord(
144
+ sessionId,
145
+ seq,
146
+ pendingQuery,
147
+ pendingStart,
148
+ result,
149
+ maxBindings,
150
+ );
151
+ appendFileSync(logPath, `${JSON.stringify(record)}\n`);
152
+ seq++;
153
+ pendingQuery = undefined;
154
+ pendingStart = undefined;
155
+ }
156
+ return undefined;
157
+ },
158
+ });
159
+ }
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Types for query access tracing.
3
+ *
4
+ * One NDJSON record per `store.query()` call. The trace plugin writes
5
+ * these to session-scoped log files; the CLI reads them back.
6
+ */
7
+
8
+ /** A single trace record — one per store.query() call. */
9
+ export interface TraceRecord {
10
+ /** ISO-8601 timestamp. */
11
+ ts: string;
12
+ /** Session identifier. */
13
+ sid: string;
14
+ /** Monotonic query index within the session. */
15
+ seq: number;
16
+ /** FNV-1a hash of the query string (8 hex chars). */
17
+ qh: string;
18
+ /** SPARQL query string (after prefix expansion). */
19
+ q: string;
20
+ /** Execution duration in milliseconds. */
21
+ ms: number;
22
+ /** Result type discriminant. */
23
+ type: "select" | "construct" | "ask";
24
+ /** True result count (bindings / triples / 1). */
25
+ count: number;
26
+ /** SELECT variable names. */
27
+ vars?: string[];
28
+ /** SELECT result rows (capped at maxBindings). */
29
+ bindings?: Record<string, string>[];
30
+ /** CONSTRUCT triples (capped at maxBindings). */
31
+ triples?: { s: string; p: string; o: string }[];
32
+ /** ASK result. */
33
+ ask?: boolean;
34
+ }
35
+
36
+ /** API exposed via `store.api<TraceApi>("trace")`. */
37
+ export interface TraceApi {
38
+ readonly sessionId: string;
39
+ readonly logPath: string;
40
+ readonly queryCount: number;
41
+ }
42
+
43
+ /** Options for creating the trace plugin. */
44
+ export interface TracePluginOptions {
45
+ /** Directory for trace log files. */
46
+ traceDir: string;
47
+ /** Session identifier (generated if omitted). */
48
+ sessionId?: string;
49
+ /** Max bindings/triples to record per query. Default: 200. */
50
+ maxBindings?: number;
51
+ }
52
+
53
+ /** A trace session on disk. */
54
+ export interface TraceSession {
55
+ /** Session identifier (derived from filename). */
56
+ sessionId: string;
57
+ /** Absolute path to the NDJSON file. */
58
+ path: string;
59
+ /** Number of query records in the file. */
60
+ queryCount: number;
61
+ /** File size in bytes. */
62
+ sizeBytes: number;
63
+ /** File creation time. */
64
+ createdAt: Date;
65
+ }
@@ -1,4 +1,9 @@
1
- import type { ErrorCode, PragmaErrorData, Recovery } from "./types.js";
1
+ import type {
2
+ CrossDomainRecovery,
3
+ ErrorCode,
4
+ PragmaErrorData,
5
+ Recovery,
6
+ } from "./types.js";
2
7
 
3
8
  /**
4
9
  * Structured error for all pragma domain operations.
@@ -16,6 +21,8 @@ class PragmaError extends Error {
16
21
  readonly suggestions: string[];
17
22
  /** Structured recovery hint with optional CLI command or MCP tool call. */
18
23
  readonly recovery: Recovery | undefined;
24
+ /** Cross-domain redirect when name exists in another domain. */
25
+ readonly crossDomain: CrossDomainRecovery | undefined;
19
26
  /** Active filters at the time of the error, for diagnostic context. */
20
27
  readonly filters: Record<string, string> | undefined;
21
28
  /** Enumerated valid options when input was rejected. */
@@ -31,6 +38,7 @@ class PragmaError extends Error {
31
38
  this.entity = data.entity;
32
39
  this.suggestions = data.suggestions ?? [];
33
40
  this.recovery = data.recovery;
41
+ this.crossDomain = data.crossDomain;
34
42
  this.filters = data.filters;
35
43
  this.validOptions = data.validOptions;
36
44
  }
@@ -49,6 +57,7 @@ class PragmaError extends Error {
49
57
  opts: {
50
58
  suggestions?: string[];
51
59
  recovery?: Recovery;
60
+ crossDomain?: CrossDomainRecovery;
52
61
  } = {},
53
62
  ): PragmaError {
54
63
  return new PragmaError({
@@ -57,6 +66,7 @@ class PragmaError extends Error {
57
66
  entity: { type: entityType, name: entityName },
58
67
  suggestions: opts.suggestions,
59
68
  recovery: opts.recovery,
69
+ crossDomain: opts.crossDomain,
60
70
  });
61
71
  }
62
72
 
@@ -70,15 +80,18 @@ class PragmaError extends Error {
70
80
  static emptyResults(
71
81
  entityType: string,
72
82
  opts: {
83
+ message?: string;
73
84
  filters?: Record<string, string>;
74
85
  recovery?: Recovery;
86
+ validOptions?: string[];
75
87
  } = {},
76
88
  ): PragmaError {
77
89
  return new PragmaError({
78
90
  code: "EMPTY_RESULTS",
79
- message: `No ${entityType}s found.`,
91
+ message: opts.message ?? `No ${entityType}s found.`,
80
92
  filters: opts.filters,
81
93
  recovery: opts.recovery,
94
+ validOptions: opts.validOptions,
82
95
  });
83
96
  }
84
97
 
@@ -20,6 +20,18 @@ interface Recovery {
20
20
  mcp?: { tool: string; params?: Record<string, unknown> };
21
21
  }
22
22
 
23
+ /** Hint pointing the user/agent to a different domain when a name exists there. */
24
+ interface CrossDomainRecovery {
25
+ /** Domain where the name was found (e.g., "modifier"). */
26
+ domain: string;
27
+ /** Entity type label for messages. */
28
+ entityType: string;
29
+ /** CLI command to run. */
30
+ cli: string;
31
+ /** MCP tool invocation. */
32
+ mcp: { tool: string; params: Record<string, unknown> };
33
+ }
34
+
23
35
  /** Raw data payload used to construct a PragmaError. */
24
36
  interface PragmaErrorData {
25
37
  /** Machine-readable error classification. */
@@ -32,10 +44,12 @@ interface PragmaErrorData {
32
44
  suggestions?: string[];
33
45
  /** Recovery hint for CLI and MCP adapters. */
34
46
  recovery?: Recovery;
47
+ /** Cross-domain redirect when name exists in another domain. */
48
+ crossDomain?: CrossDomainRecovery;
35
49
  /** Active filters at the time of the error. */
36
50
  filters?: Record<string, string>;
37
51
  /** Enumerated valid options when input was rejected. */
38
52
  validOptions?: string[];
39
53
  }
40
54
 
41
- export type { ErrorCode, PragmaErrorData, Recovery };
55
+ export type { CrossDomainRecovery, ErrorCode, PragmaErrorData, Recovery };
@@ -21,6 +21,13 @@ export default function serializeErrorPayload(
21
21
  message: error.message,
22
22
  ...(error.suggestions.length > 0 && { suggestions: error.suggestions }),
23
23
  ...(recovery && { recovery }),
24
+ ...(error.crossDomain && {
25
+ crossDomain: {
26
+ domain: error.crossDomain.domain,
27
+ tool: error.crossDomain.mcp.tool,
28
+ params: error.crossDomain.mcp.params,
29
+ },
30
+ }),
24
31
  ...(error.filters && { filters: error.filters }),
25
32
  ...(error.validOptions && { validOptions: error.validOptions }),
26
33
  };
@@ -28,11 +28,19 @@ export interface McpRecovery {
28
28
  * Structured MCP error payload embedded in tool response content.
29
29
  * Returned as JSON text content with `isError: true`.
30
30
  */
31
+ /** Cross-domain redirect hint for agents. */
32
+ export interface McpCrossDomainHint {
33
+ readonly domain: string;
34
+ readonly tool: string;
35
+ readonly params: Record<string, unknown>;
36
+ }
37
+
31
38
  export interface McpErrorPayload {
32
39
  readonly code: ErrorCode;
33
40
  readonly message: string;
34
41
  readonly suggestions?: readonly string[];
35
42
  readonly recovery?: McpRecovery;
43
+ readonly crossDomain?: McpCrossDomainHint;
36
44
  readonly filters?: Record<string, string>;
37
45
  readonly validOptions?: readonly string[];
38
46
  }
@@ -35,38 +35,60 @@ describe("resource listing", () => {
35
35
  it("registers 1 resource template", async () => {
36
36
  const { resourceTemplates } = await client.listResourceTemplates();
37
37
  expect(resourceTemplates).toHaveLength(1);
38
- expect(resourceTemplates[0]?.uriTemplate).toBe("pragma:{+uri}");
38
+ expect(resourceTemplates[0]?.uriTemplate).toBe("{+uri}");
39
39
  });
40
40
 
41
41
  it("lists resources including known entities", async () => {
42
42
  const { resources } = await client.listResources();
43
43
  const uris = resources.map((r) => r.uri);
44
- expect(uris).toContain(`pragma:${P.ds}global.component.button`);
45
- expect(uris).toContain(`pragma:${P.ds}global.component.card`);
46
- expect(uris).toContain(`pragma:${P.ds}global`);
44
+ expect(uris).toContain(`${P.ds}global.component.button`);
45
+ expect(uris).toContain(`${P.ds}global.component.card`);
46
+ expect(uris).toContain(`${P.ds}global`);
47
47
  });
48
48
 
49
49
  it("lists OWL classes as resources", async () => {
50
50
  const { resources } = await client.listResources();
51
51
  const uris = resources.map((r) => r.uri);
52
- expect(uris).toContain(`pragma:${P.ds}UIBlock`);
53
- expect(uris).toContain(`pragma:${P.ds}Component`);
52
+ expect(uris).toContain(`${P.ds}UIBlock`);
53
+ expect(uris).toContain(`${P.ds}Component`);
54
54
  });
55
55
 
56
56
  it("lists code standards as resources", async () => {
57
57
  const { resources } = await client.listResources();
58
58
  const uris = resources.map((r) => r.uri);
59
- expect(uris).toContain(`pragma:${P.cs}react_folder`);
60
- expect(uris).toContain(`pragma:${P.cs}code_purity`);
59
+ expect(uris).toContain(`${P.cs}react_folder`);
60
+ expect(uris).toContain(`${P.cs}code_purity`);
61
61
  });
62
62
 
63
63
  it("uses label as resource name when available", async () => {
64
64
  const { resources } = await client.listResources();
65
65
  const button = resources.find(
66
- (r) => r.uri === `pragma:${P.ds}global.component.button`,
66
+ (r) => r.uri === `${P.ds}global.component.button`,
67
67
  );
68
68
  expect(button?.name).toBe("Button");
69
69
  });
70
+
71
+ it("includes type-aware description for components", async () => {
72
+ const { resources } = await client.listResources();
73
+ const button = resources.find(
74
+ (r) => r.uri === `${P.ds}global.component.button`,
75
+ );
76
+ expect(button?.description).toMatch(/^Component/);
77
+ });
78
+
79
+ it("includes type-aware description for code standards", async () => {
80
+ const { resources } = await client.listResources();
81
+ const standard = resources.find((r) => r.uri === `${P.cs}code_purity`);
82
+ expect(standard?.description).toMatch(/^Code Standard/);
83
+ });
84
+
85
+ it("marks resources with assistant audience", async () => {
86
+ const { resources } = await client.listResources();
87
+ const button = resources.find(
88
+ (r) => r.uri === `${P.ds}global.component.button`,
89
+ );
90
+ expect(button?.annotations?.audience).toEqual(["assistant"]);
91
+ });
70
92
  });
71
93
 
72
94
  // =============================================================================
@@ -76,7 +98,7 @@ describe("resource listing", () => {
76
98
  describe("read component instance", () => {
77
99
  it("returns entity with types and label", async () => {
78
100
  const result = await client.readResource({
79
- uri: `pragma:${P.ds}global.component.button`,
101
+ uri: `${P.ds}global.component.button`,
80
102
  });
81
103
  const entity = parseContents(result) as {
82
104
  uri: string;
@@ -92,7 +114,7 @@ describe("read component instance", () => {
92
114
 
93
115
  it("resolves level-1 URI objects to summaries", async () => {
94
116
  const result = await client.readResource({
95
- uri: `pragma:${P.ds}global.component.button`,
117
+ uri: `${P.ds}global.component.button`,
96
118
  });
97
119
  const entity = parseContents(result) as {
98
120
  properties: {
@@ -117,7 +139,7 @@ describe("read component instance", () => {
117
139
 
118
140
  it("includes literal values", async () => {
119
141
  const result = await client.readResource({
120
- uri: `pragma:${P.ds}global.component.button`,
142
+ uri: `${P.ds}global.component.button`,
121
143
  });
122
144
  const entity = parseContents(result) as {
123
145
  properties: {
@@ -142,7 +164,7 @@ describe("read component instance", () => {
142
164
  describe("read OWL class", () => {
143
165
  it("returns class with rdfs:label", async () => {
144
166
  const result = await client.readResource({
145
- uri: `pragma:${P.ds}UIBlock`,
167
+ uri: `${P.ds}UIBlock`,
146
168
  });
147
169
  const entity = parseContents(result) as {
148
170
  types: string[];
@@ -167,7 +189,7 @@ describe("read OWL class", () => {
167
189
  describe("read code standard", () => {
168
190
  it("returns standard with properties", async () => {
169
191
  const result = await client.readResource({
170
- uri: `pragma:${P.cs}code_purity`,
192
+ uri: `${P.cs}code_purity`,
171
193
  });
172
194
  const entity = parseContents(result) as {
173
195
  types: string[];
@@ -202,7 +224,7 @@ describe("read code standard", () => {
202
224
  describe("read tier", () => {
203
225
  it("returns tier with name", async () => {
204
226
  const result = await client.readResource({
205
- uri: `pragma:${P.ds}apps_lxd`,
227
+ uri: `${P.ds}apps_lxd`,
206
228
  });
207
229
  const entity = parseContents(result) as {
208
230
  types: string[];
@@ -227,7 +249,7 @@ describe("read tier", () => {
227
249
  describe("error handling", () => {
228
250
  it("returns text/plain error for unknown entity", async () => {
229
251
  const result = await client.readResource({
230
- uri: `pragma:${P.ds}nonexistent`,
252
+ uri: `${P.ds}nonexistent`,
231
253
  });
232
254
  const first = result.contents[0] as ResourceContent;
233
255
  expect(first.mimeType).toBe("text/plain");