@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.
- package/dist/pragma +0 -0
- package/package.json +16 -17
- package/src/config/readConfig.ts +38 -2
- package/src/config/types.ts +12 -1
- package/src/config/writeConfig.ts +12 -0
- package/src/constants.ts +12 -1
- package/src/domains/block/commands/index.ts +2 -1
- package/src/domains/block/commands/sample.ts +55 -0
- package/src/domains/block/formatters/index.ts +2 -1
- package/src/domains/block/formatters/sample.ts +21 -0
- package/src/domains/block/index.ts +3 -3
- package/src/domains/block/mcp/specs.ts +48 -0
- package/src/domains/block/operations/index.ts +2 -1
- package/src/domains/block/operations/lookup.ts +9 -0
- package/src/domains/block/operations/sample.ts +41 -0
- package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
- package/src/domains/block/orchestration/resolveBlockLookup.ts +10 -3
- package/src/domains/config/commands/framework.test.ts +88 -0
- package/src/domains/config/commands/framework.ts +85 -0
- package/src/domains/config/commands/index.ts +2 -0
- package/src/domains/config/commands/trace.test.ts +148 -0
- package/src/domains/config/commands/trace.ts +80 -0
- package/src/domains/config/formatters/framework.ts +34 -0
- package/src/domains/config/formatters/index.ts +2 -0
- package/src/domains/config/formatters/trace.ts +28 -0
- package/src/domains/config/index.ts +15 -2
- package/src/domains/config/operations/index.ts +1 -0
- package/src/domains/config/operations/validateFramework.ts +24 -0
- package/src/domains/config/tasks/index.ts +2 -0
- package/src/domains/config/tasks/setChannel.ts +7 -1
- package/src/domains/config/tasks/setFramework.ts +40 -0
- package/src/domains/config/tasks/setTier.ts +7 -1
- package/src/domains/config/tasks/setTrace.ts +38 -0
- package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +35 -41
- package/src/domains/doctor/operations/checks.test.ts +4 -1
- package/src/domains/doctor/operations/runChecks.test.ts +72 -0
- package/src/domains/doctor/operations/runChecks.ts +48 -12
- package/src/domains/graphql/commands/build.test.ts +194 -0
- package/src/domains/graphql/commands/build.ts +125 -0
- package/src/domains/graphql/commands/check.test.ts +139 -0
- package/src/domains/graphql/commands/check.ts +87 -0
- package/src/domains/graphql/commands/index.ts +4 -0
- package/src/domains/graphql/formatters/index.ts +3 -0
- package/src/domains/graphql/formatters/report.ts +78 -0
- package/src/domains/graphql/helpers/index.ts +4 -0
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
- package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
- package/src/domains/graphql/index.ts +21 -0
- package/src/domains/graphql/operations/compileSchema.test.ts +119 -0
- package/src/domains/graphql/operations/compileSchema.ts +109 -0
- package/src/domains/graphql/operations/index.ts +7 -0
- package/src/domains/graphql/types.ts +29 -0
- package/src/domains/info/operations/collectInfo.test.ts +2 -2
- package/src/domains/info/operations/collectInfo.ts +4 -4
- package/src/domains/llm/capabilities.test.ts +91 -0
- package/src/domains/llm/commands/capabilities.ts +28 -0
- package/src/domains/llm/commands/index.ts +1 -0
- package/src/domains/llm/data/commandReference.ts +90 -18
- package/src/domains/llm/data/decisionTrees.ts +15 -13
- package/src/domains/llm/data/index.ts +2 -0
- package/src/domains/llm/data/toolCatalog.ts +184 -0
- package/src/domains/llm/formatters/capabilities.ts +50 -0
- package/src/domains/llm/formatters/index.ts +1 -0
- package/src/domains/llm/index.ts +3 -2
- package/src/domains/llm/mcp/index.ts +1 -1
- package/src/domains/llm/mcp/specs.ts +59 -52
- package/src/domains/llm/types.ts +40 -0
- package/src/domains/modifier/commands/index.ts +2 -1
- package/src/domains/modifier/commands/sample.ts +53 -0
- package/src/domains/modifier/formatters/index.ts +2 -1
- package/src/domains/modifier/formatters/sample.ts +15 -0
- package/src/domains/modifier/index.ts +7 -3
- package/src/domains/modifier/mcp/specs.ts +47 -0
- package/src/domains/modifier/operations/index.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +9 -0
- package/src/domains/modifier/operations/sample.ts +32 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +14 -2
- package/src/domains/refs/operations/index.ts +1 -1
- package/src/domains/refs/operations/paths.ts +29 -1
- package/src/domains/refs/operations/updateRefs.test.ts +6 -3
- package/src/domains/refs/operations/updateRefs.ts +3 -2
- package/src/domains/shared/bootStore.ts +88 -49
- package/src/domains/shared/contracts.ts +1 -0
- package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
- package/src/domains/shared/loaders/bundledLoader.ts +108 -0
- package/src/domains/shared/loaders/gitLoader.ts +31 -0
- package/src/domains/shared/loaders/index.ts +6 -0
- package/src/domains/shared/loaders/localLoader.ts +73 -0
- package/src/domains/shared/loaders/readPackageDir.ts +112 -0
- package/src/domains/shared/lookupMany.ts +9 -1
- package/src/domains/shared/packages.ts +21 -120
- package/src/domains/shared/pickRandom.ts +22 -0
- package/src/domains/shared/runtime.ts +6 -3
- package/src/domains/shared/sampleFormatters.ts +47 -0
- package/src/domains/shared/semanticPackage.ts +96 -0
- package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
- package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
- package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
- package/src/domains/shared/suggestions/expandGlob.ts +51 -0
- package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
- package/src/domains/shared/suggestions/index.ts +7 -0
- package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
- package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
- package/src/domains/shared/suggestions/suggestNames.ts +97 -0
- package/src/domains/shared/types/index.ts +1 -0
- package/src/domains/shared/types/runtime.ts +4 -1
- package/src/domains/shared/types/sample.ts +17 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +49 -16
- package/src/domains/standard/commands/index.ts +2 -1
- package/src/domains/standard/commands/sample.ts +57 -0
- package/src/domains/standard/formatters/index.ts +2 -1
- package/src/domains/standard/formatters/sample.ts +15 -0
- package/src/domains/standard/index.ts +8 -1
- package/src/domains/standard/mcp/specs.ts +48 -1
- package/src/domains/standard/operations/index.ts +2 -1
- package/src/domains/standard/operations/lookup.ts +9 -0
- package/src/domains/standard/operations/sample.ts +35 -0
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +14 -2
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
- package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
- package/src/domains/token/commands/index.ts +2 -1
- package/src/domains/token/commands/sample.ts +53 -0
- package/src/domains/token/formatters/index.ts +2 -1
- package/src/domains/token/formatters/sample.ts +16 -0
- package/src/domains/token/index.ts +8 -1
- package/src/domains/token/mcp/specs.ts +47 -1
- package/src/domains/token/operations/index.ts +2 -1
- package/src/domains/token/operations/lookup.ts +9 -0
- package/src/domains/token/operations/sample.ts +32 -0
- package/src/domains/token/orchestration/resolveTokenLookup.ts +14 -2
- package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
- package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
- package/src/domains/trace/commands/clear.ts +71 -0
- package/src/domains/trace/commands/index.ts +5 -0
- package/src/domains/trace/commands/sessions.ts +32 -0
- package/src/domains/trace/commands/show.ts +145 -0
- package/src/domains/trace/formatters/index.ts +9 -0
- package/src/domains/trace/formatters/sessions.ts +80 -0
- package/src/domains/trace/formatters/show.ts +122 -0
- package/src/domains/trace/index.ts +17 -0
- package/src/domains/trace/operations/index.ts +8 -0
- package/src/domains/trace/operations/listSessions.test.ts +55 -0
- package/src/domains/trace/operations/listSessions.ts +52 -0
- package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
- package/src/domains/trace/operations/readTraceLog.ts +104 -0
- package/src/domains/trace/tracePlugin.test.ts +171 -0
- package/src/domains/trace/tracePlugin.ts +159 -0
- package/src/domains/trace/types.ts +65 -0
- package/src/error/PragmaError.ts +15 -2
- package/src/error/types.ts +15 -1
- package/src/mcp/error/serializeErrorPayload.ts +7 -0
- package/src/mcp/error/types.ts +8 -0
- package/src/mcp/registerResources.test.ts +38 -16
- package/src/mcp/registerResources.ts +57 -8
- package/src/mcp/tools/registerTools.test.ts +22 -7
- package/src/mcp/utils/wrapTool.test.ts +1 -0
- package/src/pipeline/collectCommands.ts +7 -1
- package/src/pipeline/renderError.ts +15 -0
- package/src/pipeline/resolveCommandKind.ts +8 -1
- package/src/pipeline/runCli.ts +12 -1
- package/src/testing/createTestMcpClient.ts +1 -0
- package/src/testing/graphqlFixtures.ts +72 -0
- package/src/testing/index.ts +7 -0
- 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
|
+
}
|
package/src/error/PragmaError.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type {
|
|
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
|
|
package/src/error/types.ts
CHANGED
|
@@ -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
|
};
|
package/src/mcp/error/types.ts
CHANGED
|
@@ -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("
|
|
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(
|
|
45
|
-
expect(uris).toContain(
|
|
46
|
-
expect(uris).toContain(
|
|
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(
|
|
53
|
-
expect(uris).toContain(
|
|
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(
|
|
60
|
-
expect(uris).toContain(
|
|
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 ===
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
252
|
+
uri: `${P.ds}nonexistent`,
|
|
231
253
|
});
|
|
232
254
|
const first = result.contents[0] as ResourceContent;
|
|
233
255
|
expect(first.mimeType).toBe("text/plain");
|