@canonical/pragma-cli 0.27.1-experimental.0 → 0.30.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -15
- package/dist/pragma +0 -0
- package/package.json +16 -17
- package/src/config/defaultConfig.test.ts +33 -0
- package/src/config/defaultConfig.ts +59 -0
- package/src/config/defaults.json +17 -0
- package/src/config/findProjectConfigPath.test.ts +53 -0
- package/src/config/findProjectConfigPath.ts +36 -0
- package/src/config/index.ts +17 -2
- package/src/config/mergeConfigFileUpdate.ts +58 -0
- package/src/config/parseConfigValues.ts +215 -0
- package/src/config/readConfig.test.ts +104 -13
- package/src/config/readConfig.ts +12 -117
- package/src/config/readConfigLayers.test.ts +107 -0
- package/src/config/readConfigLayers.ts +115 -0
- package/src/config/resolveGlobalConfigPath.ts +13 -0
- package/src/config/resolveWriteConfigPath.test.ts +53 -0
- package/src/config/resolveWriteConfigPath.ts +31 -0
- package/src/config/types.ts +88 -2
- package/src/config/writeConfig.ts +25 -43
- package/src/constants.ts +12 -1
- package/src/domains/block/commands/index.ts +2 -1
- package/src/domains/block/commands/list.ts +5 -90
- package/src/domains/block/commands/lookup.ts +8 -167
- 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 +36 -168
- 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 +11 -3
- package/src/domains/block/stories.test.ts +28 -0
- package/src/domains/block/stories.ts +254 -0
- package/src/domains/config/commands/channel.test.ts +14 -3
- package/src/domains/config/commands/channel.ts +11 -7
- package/src/domains/config/commands/configScope.ts +46 -0
- package/src/domains/config/commands/framework.test.ts +105 -0
- package/src/domains/config/commands/framework.ts +91 -0
- package/src/domains/config/commands/index.ts +2 -0
- package/src/domains/config/commands/show.ts +5 -45
- package/src/domains/config/commands/tier.test.ts +13 -2
- package/src/domains/config/commands/tier.ts +11 -7
- package/src/domains/config/commands/trace.test.ts +165 -0
- package/src/domains/config/commands/trace.ts +83 -0
- package/src/domains/config/formatters/channel.test.ts +20 -6
- package/src/domains/config/formatters/channel.ts +14 -11
- package/src/domains/config/formatters/framework.ts +37 -0
- package/src/domains/config/formatters/index.ts +2 -0
- package/src/domains/config/formatters/show.test.ts +27 -0
- package/src/domains/config/formatters/show.ts +27 -4
- package/src/domains/config/formatters/tier.test.ts +26 -10
- package/src/domains/config/formatters/tier.ts +19 -11
- package/src/domains/config/formatters/trace.ts +28 -0
- package/src/domains/config/index.ts +15 -2
- package/src/domains/config/mcp/specs.ts +33 -53
- package/src/domains/config/operations/index.ts +1 -0
- package/src/domains/config/operations/show.test.ts +18 -0
- package/src/domains/config/operations/show.ts +7 -1
- package/src/domains/config/operations/types.ts +7 -0
- package/src/domains/config/operations/validateFramework.ts +24 -0
- package/src/domains/config/stories.ts +42 -0
- package/src/domains/config/tasks/index.ts +3 -0
- package/src/domains/config/tasks/setChannel.ts +13 -22
- package/src/domains/config/tasks/setFramework.ts +21 -0
- package/src/domains/config/tasks/setTier.ts +10 -23
- package/src/domains/config/tasks/setTrace.ts +20 -0
- package/src/domains/config/tasks/writeConfigField.ts +41 -0
- package/src/domains/create/commands/component.ts +5 -5
- package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
- package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
- package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -48
- package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
- package/src/domains/doctor/operations/checks/index.ts +1 -0
- package/src/domains/doctor/operations/checks.test.ts +162 -8
- package/src/domains/doctor/operations/runChecks.test.ts +85 -4
- package/src/domains/doctor/operations/runChecks.ts +50 -12
- package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
- package/src/domains/graph/helpers/resolveUri.ts +28 -21
- package/src/domains/graphql/commands/build.test.ts +196 -0
- package/src/domains/graphql/commands/build.ts +117 -0
- package/src/domains/graphql/commands/check.test.ts +141 -0
- package/src/domains/graphql/commands/check.ts +81 -0
- package/src/domains/graphql/commands/index.ts +5 -0
- package/src/domains/graphql/commands/serve.test.ts +40 -0
- package/src/domains/graphql/commands/serve.ts +131 -0
- package/src/domains/graphql/constants.ts +10 -0
- package/src/domains/graphql/formatters/index.ts +3 -0
- package/src/domains/graphql/formatters/report.ts +78 -0
- package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
- package/src/domains/graphql/helpers/gatherSources.ts +58 -0
- package/src/domains/graphql/helpers/index.ts +6 -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/helpers/routeGraphql.test.ts +43 -0
- package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
- package/src/domains/graphql/index.ts +24 -0
- package/src/domains/graphql/operations/compileSchema.test.ts +95 -0
- package/src/domains/graphql/operations/compileSchema.ts +107 -0
- package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
- package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
- package/src/domains/graphql/operations/index.ts +12 -0
- package/src/domains/graphql/types.ts +29 -0
- package/src/domains/info/operations/collectInfo.test.ts +16 -5
- package/src/domains/info/operations/collectInfo.ts +16 -19
- 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 +190 -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/list.ts +5 -38
- package/src/domains/modifier/commands/lookup.ts +8 -79
- 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 +38 -70
- 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 +15 -2
- package/src/domains/modifier/stories.test.ts +20 -0
- package/src/domains/modifier/stories.ts +77 -0
- package/src/domains/ontology/commands/list.ts +5 -58
- package/src/domains/ontology/commands/show.ts +6 -58
- package/src/domains/ontology/formatters/show.ts +7 -1
- package/src/domains/ontology/mcp/specs.ts +7 -84
- package/src/domains/ontology/stories.test.ts +36 -0
- package/src/domains/ontology/stories.ts +108 -0
- 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 +2 -36
- package/src/domains/setup/commands/skills.test.ts +6 -2
- package/src/domains/setup/commands/skills.ts +6 -2
- package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
- package/src/domains/setup/helpers/runSetupTask.ts +62 -70
- package/src/domains/setup/operations/setupMcp.test.ts +6 -1
- package/src/domains/shared/bootStore.ts +92 -50
- package/src/domains/shared/compactUri.test.ts +11 -0
- package/src/domains/shared/compactUri.ts +10 -5
- package/src/domains/shared/context.ts +2 -2
- package/src/domains/shared/contracts.ts +12 -0
- package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
- package/src/domains/shared/createInteractivePromptSession.ts +236 -0
- package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
- package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
- package/src/domains/shared/loaders/bundledLoader.ts +206 -0
- package/src/domains/shared/loaders/gitLoader.ts +31 -0
- package/src/domains/shared/loaders/index.ts +9 -0
- package/src/domains/shared/loaders/localLoader.ts +87 -0
- package/src/domains/shared/loaders/readPackageDir.ts +147 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
- package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
- package/src/domains/shared/lookupMany.test.ts +120 -0
- package/src/domains/shared/lookupMany.ts +43 -3
- package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
- package/src/domains/shared/mergeAndParseRefs.ts +44 -0
- package/src/domains/shared/packages.ts +14 -120
- package/src/domains/shared/pickRandom.ts +22 -0
- package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
- package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
- package/src/domains/shared/runtime.ts +7 -63
- package/src/domains/shared/sampleFormatters.ts +47 -0
- package/src/domains/shared/semanticPackage.ts +112 -0
- package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
- package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
- package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
- package/src/domains/shared/stories/compileLookupTool.ts +96 -0
- package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
- package/src/domains/shared/stories/compileReadCommand.ts +48 -0
- package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
- package/src/domains/shared/stories/compileReadTool.ts +38 -0
- package/src/domains/shared/stories/condense.test.ts +18 -0
- package/src/domains/shared/stories/condense.ts +15 -0
- package/src/domains/shared/stories/index.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
- package/src/domains/shared/stories/normalizeNames.ts +22 -0
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
- package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
- package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
- package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
- package/src/domains/shared/stories/pack/index.ts +28 -0
- package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
- package/src/domains/shared/stories/pack/types.ts +104 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
- package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
- package/src/domains/shared/stories/projectCliParameters.ts +57 -0
- package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
- package/src/domains/shared/stories/projectToolParams.ts +32 -0
- package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
- package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
- package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
- package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
- package/src/domains/shared/stories/types.ts +171 -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 +52 -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 +130 -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/commands/index.ts +3 -1
- package/src/domains/skill/commands/list.test.ts +6 -2
- package/src/domains/skill/commands/list.ts +5 -63
- package/src/domains/skill/commands/lookup.test.ts +118 -0
- package/src/domains/skill/commands/lookup.ts +14 -0
- package/src/domains/skill/formatters/index.ts +1 -0
- package/src/domains/skill/formatters/lookup.test.ts +46 -0
- package/src/domains/skill/formatters/lookup.ts +57 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +48 -16
- package/src/domains/skill/index.ts +7 -3
- package/src/domains/skill/mcp/specs.ts +10 -29
- package/src/domains/skill/operations/discover.test.ts +61 -30
- package/src/domains/skill/operations/discover.ts +28 -37
- package/src/domains/skill/operations/index.ts +2 -1
- package/src/domains/skill/operations/list.test.ts +15 -7
- package/src/domains/skill/operations/lookup.test.ts +91 -0
- package/src/domains/skill/operations/lookup.ts +70 -0
- package/src/domains/skill/stories.test.ts +43 -0
- package/src/domains/skill/stories.ts +103 -0
- package/src/domains/skill/types.ts +8 -0
- package/src/domains/standard/commands/index.ts +2 -1
- package/src/domains/standard/commands/list.ts +5 -84
- package/src/domains/standard/commands/lookup.ts +8 -93
- 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 +44 -152
- 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 +15 -2
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
- package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
- package/src/domains/standard/stories.test.ts +24 -0
- package/src/domains/standard/stories.ts +151 -0
- package/src/domains/tier/commands/list.ts +5 -55
- package/src/domains/tier/mcp/specs.ts +5 -31
- package/src/domains/tier/stories.test.ts +22 -0
- package/src/domains/tier/stories.ts +52 -0
- 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 +52 -2
- 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 +15 -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 +405 -75
- package/src/mcp/registerResources.ts +5 -299
- package/src/mcp/resources/buildGraphIndex.ts +167 -0
- package/src/mcp/resources/buildResourceList.test.ts +97 -0
- package/src/mcp/resources/buildResourceList.ts +176 -0
- package/src/mcp/resources/classifyEntity.test.ts +40 -0
- package/src/mcp/resources/classifyEntity.ts +27 -0
- package/src/mcp/resources/constants.ts +105 -0
- package/src/mcp/resources/displayPredicates.ts +62 -0
- package/src/mcp/resources/pickFirstValue.test.ts +29 -0
- package/src/mcp/resources/pickFirstValue.ts +26 -0
- package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
- package/src/mcp/resources/pickPrimaryType.ts +26 -0
- package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
- package/src/mcp/resources/rankUriCompletions.ts +64 -0
- package/src/mcp/resources/readEntity.ts +169 -0
- package/src/mcp/resources/registerResources.ts +126 -0
- package/src/mcp/resources/types.ts +128 -0
- package/src/mcp/tools/index.ts +9 -0
- package/src/mcp/tools/registerTools.test.ts +84 -12
- package/src/mcp/types.ts +4 -0
- package/src/mcp/utils/wrapTool.test.ts +1 -0
- package/src/pipeline/collectCommands.ts +15 -2
- package/src/pipeline/renderError.ts +15 -0
- package/src/pipeline/resolveCommandKind.ts +8 -1
- package/src/pipeline/runCli.ts +51 -6
- package/src/testing/createTestMcpClient.ts +5 -1
- package/src/testing/graphqlFixtures.ts +72 -0
- package/src/testing/index.ts +17 -0
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
- package/src/testing/integration/executor-execution.test.ts +101 -0
- package/src/testing/integration/executor-golden.test.ts +145 -0
- package/src/testing/integration/mcp-surface.test.ts +11 -5
- package/src/testing/integration/parity.test.ts +17 -12
- package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
- package/src/testing/setupXdgIsolation.ts +15 -0
- package/src/testing/standardParityFixtures.ts +92 -0
- package/src/testing/storyFixtures.ts +63 -0
- package/src/pipeline/runInteractiveCommand.ts +0 -198
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Three-mode formatter for `pragma trace` output.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Formatters } from "../../shared/formatters.js";
|
|
6
|
+
import type { TraceRecord } from "../types.js";
|
|
7
|
+
|
|
8
|
+
export interface ShowData {
|
|
9
|
+
sessionId: string;
|
|
10
|
+
records: TraceRecord[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatTime(isoString: string): string {
|
|
14
|
+
const d = new Date(isoString);
|
|
15
|
+
return [
|
|
16
|
+
String(d.getHours()).padStart(2, "0"),
|
|
17
|
+
String(d.getMinutes()).padStart(2, "0"),
|
|
18
|
+
String(d.getSeconds()).padStart(2, "0"),
|
|
19
|
+
].join(":");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function formatResultSummary(record: TraceRecord): string {
|
|
23
|
+
switch (record.type) {
|
|
24
|
+
case "select":
|
|
25
|
+
return `${record.count} row${record.count !== 1 ? "s" : ""}`;
|
|
26
|
+
case "construct":
|
|
27
|
+
return `${record.count} triple${record.count !== 1 ? "s" : ""}`;
|
|
28
|
+
case "ask":
|
|
29
|
+
return record.ask ? "true" : "false";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function totalDuration(records: TraceRecord[]): string {
|
|
34
|
+
const total = records.reduce((sum, r) => sum + r.ms, 0);
|
|
35
|
+
if (total < 1000) return `${Math.round(total)}ms`;
|
|
36
|
+
return `${(total / 1000).toFixed(1)}s`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function truncateQuery(query: string, maxLen = 120): string {
|
|
40
|
+
// Collapse whitespace for display
|
|
41
|
+
const collapsed = query.replace(/\s+/g, " ").trim();
|
|
42
|
+
if (collapsed.length <= maxLen) return collapsed;
|
|
43
|
+
return `${collapsed.slice(0, maxLen - 3)}...`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const formatters: Formatters<ShowData> = {
|
|
47
|
+
plain: ({ sessionId, records }) => {
|
|
48
|
+
if (records.length === 0) {
|
|
49
|
+
return `Session ${sessionId} (no queries)`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const lines: string[] = [];
|
|
53
|
+
lines.push(
|
|
54
|
+
`Session ${sessionId} (${records.length} quer${records.length === 1 ? "y" : "ies"}, ${totalDuration(records)})`,
|
|
55
|
+
);
|
|
56
|
+
lines.push("");
|
|
57
|
+
|
|
58
|
+
for (const r of records) {
|
|
59
|
+
const seqStr = `#${r.seq}`.padEnd(5);
|
|
60
|
+
const time = formatTime(r.ts);
|
|
61
|
+
const type = r.type.toUpperCase().padEnd(10);
|
|
62
|
+
const summary = formatResultSummary(r).padEnd(12);
|
|
63
|
+
const ms = `${r.ms}ms`.padEnd(9);
|
|
64
|
+
|
|
65
|
+
lines.push(`${seqStr} ${time} ${type}${summary}${ms} ${r.qh}`);
|
|
66
|
+
lines.push(` ${truncateQuery(r.q)}`);
|
|
67
|
+
lines.push("");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
},
|
|
72
|
+
|
|
73
|
+
llm: ({ sessionId, records }) => {
|
|
74
|
+
if (records.length === 0) {
|
|
75
|
+
return `## Trace: ${sessionId}\n\nNo queries recorded.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lines: string[] = [];
|
|
79
|
+
lines.push(
|
|
80
|
+
`## Trace: ${sessionId} (${records.length} queries, ${totalDuration(records)})`,
|
|
81
|
+
);
|
|
82
|
+
lines.push("");
|
|
83
|
+
lines.push("| # | Type | Count | ms | Hash | Query |");
|
|
84
|
+
lines.push("|---|------|-------|----|------|-------|");
|
|
85
|
+
|
|
86
|
+
for (const r of records) {
|
|
87
|
+
lines.push(
|
|
88
|
+
`| ${r.seq} | ${r.type} | ${formatResultSummary(r)} | ${r.ms} | ${r.qh} | \`${truncateQuery(r.q, 80)}\` |`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
json: ({ sessionId, records }) =>
|
|
96
|
+
JSON.stringify({ sessionId, records }, null, 2),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
/** Formatters for the "no trace sessions exist yet" state (all three modes). */
|
|
100
|
+
export const noSessionsFormatters: Formatters<void> = {
|
|
101
|
+
plain: () =>
|
|
102
|
+
"No trace sessions found.\n\nEnable tracing: PRAGMA_TRACE=1 pragma <command>\n or: pragma config trace on",
|
|
103
|
+
llm: () =>
|
|
104
|
+
"No trace sessions found. Enable tracing with `PRAGMA_TRACE=1 pragma <command>` or `pragma config trace on`.",
|
|
105
|
+
json: () =>
|
|
106
|
+
JSON.stringify({ sessions: [], hint: "enable tracing to record queries" }),
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Formatters for the "requested session does not exist" state (all three modes). */
|
|
110
|
+
export const sessionNotFoundFormatters: Formatters<{
|
|
111
|
+
requested: string;
|
|
112
|
+
available: string[];
|
|
113
|
+
}> = {
|
|
114
|
+
plain: ({ requested, available }) =>
|
|
115
|
+
`Session not found: ${requested}\n\nAvailable sessions: ${available.join(", ")}`,
|
|
116
|
+
llm: ({ requested, available }) =>
|
|
117
|
+
`Session not found: \`${requested}\`. Available sessions: ${available.map((s) => `\`${s}\``).join(", ")}.`,
|
|
118
|
+
json: ({ requested, available }) =>
|
|
119
|
+
JSON.stringify({ error: "session_not_found", requested, available }),
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export default formatters;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @module Trace domain — CLI commands for query access tracing.
|
|
3
|
+
*
|
|
4
|
+
* Commands: `pragma trace`, `pragma trace sessions`, `pragma trace clear`.
|
|
5
|
+
* Plugin: {@link createTracePlugin} (wired in bootStore when PRAGMA_TRACE=1).
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { CommandDefinition } from "@canonical/cli-core";
|
|
9
|
+
import {
|
|
10
|
+
clearCommand,
|
|
11
|
+
sessionsCommand,
|
|
12
|
+
showCommand,
|
|
13
|
+
} from "./commands/index.js";
|
|
14
|
+
|
|
15
|
+
export function commands(): CommandDefinition[] {
|
|
16
|
+
return [showCommand, sessionsCommand, clearCommand];
|
|
17
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { 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 { listSessions } from "./listSessions.js";
|
|
6
|
+
|
|
7
|
+
describe("listSessions", () => {
|
|
8
|
+
let dir: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
dir = mkdtempSync(join(tmpdir(), "pragma-trace-sessions-"));
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
rmSync(dir, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns empty array for nonexistent directory", () => {
|
|
19
|
+
const result = listSessions("/nonexistent/path");
|
|
20
|
+
expect(result).toEqual([]);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("returns empty array for empty directory", () => {
|
|
24
|
+
const result = listSessions(dir);
|
|
25
|
+
expect(result).toEqual([]);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("lists .ndjson files as sessions", () => {
|
|
29
|
+
writeFileSync(join(dir, "20260512-143022-a7f3.ndjson"), '{"seq":0}\n');
|
|
30
|
+
writeFileSync(
|
|
31
|
+
join(dir, "20260512-140100-b8e2.ndjson"),
|
|
32
|
+
'{"seq":0}\n{"seq":1}\n',
|
|
33
|
+
);
|
|
34
|
+
// Non-ndjson file should be ignored
|
|
35
|
+
writeFileSync(join(dir, "notes.txt"), "ignore me");
|
|
36
|
+
|
|
37
|
+
const result = listSessions(dir);
|
|
38
|
+
|
|
39
|
+
expect(result).toHaveLength(2);
|
|
40
|
+
// Sorted newest first
|
|
41
|
+
expect(result[0].sessionId).toBe("20260512-143022-a7f3");
|
|
42
|
+
expect(result[1].sessionId).toBe("20260512-140100-b8e2");
|
|
43
|
+
expect(result[0].queryCount).toBe(1);
|
|
44
|
+
expect(result[1].queryCount).toBe(2);
|
|
45
|
+
expect(result[0].sizeBytes).toBeGreaterThan(0);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("ignores non-ndjson files", () => {
|
|
49
|
+
writeFileSync(join(dir, "readme.md"), "# traces");
|
|
50
|
+
writeFileSync(join(dir, ".hidden"), "secret");
|
|
51
|
+
|
|
52
|
+
const result = listSessions(dir);
|
|
53
|
+
expect(result).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List available trace sessions from the trace directory.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import type { TraceSession } from "../types.js";
|
|
8
|
+
|
|
9
|
+
export function listSessions(traceDir: string): TraceSession[] {
|
|
10
|
+
let entries: string[];
|
|
11
|
+
try {
|
|
12
|
+
entries = readdirSync(traceDir);
|
|
13
|
+
} catch {
|
|
14
|
+
return [];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const sessions: TraceSession[] = [];
|
|
18
|
+
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (!entry.endsWith(".ndjson")) continue;
|
|
21
|
+
|
|
22
|
+
const sessionId = entry.replace(/\.ndjson$/, "");
|
|
23
|
+
const filePath = join(traceDir, entry);
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const stat = statSync(filePath);
|
|
27
|
+
// Count records by counting newline bytes on the raw Buffer. Each NDJSON
|
|
28
|
+
// record is one newline-terminated line, so this avoids decoding the
|
|
29
|
+
// whole file to a string and allocating a large split() array.
|
|
30
|
+
const buf = readFileSync(filePath);
|
|
31
|
+
let lineCount = 0;
|
|
32
|
+
for (let i = 0; i < buf.length; i++) {
|
|
33
|
+
if (buf[i] === 0x0a) lineCount++;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
sessions.push({
|
|
37
|
+
sessionId,
|
|
38
|
+
path: filePath,
|
|
39
|
+
queryCount: lineCount,
|
|
40
|
+
sizeBytes: stat.size,
|
|
41
|
+
createdAt: stat.birthtime,
|
|
42
|
+
});
|
|
43
|
+
} catch {
|
|
44
|
+
// Skip files we can't stat/read
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Sort by session ID descending (newest first — IDs are timestamp-based)
|
|
49
|
+
sessions.sort((a, b) => b.sessionId.localeCompare(a.sessionId));
|
|
50
|
+
|
|
51
|
+
return sessions;
|
|
52
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { 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 type { TraceRecord } from "../types.js";
|
|
6
|
+
import { readTraceLog } from "./readTraceLog.js";
|
|
7
|
+
|
|
8
|
+
function makeRecord(seq: number): TraceRecord {
|
|
9
|
+
return {
|
|
10
|
+
ts: new Date().toISOString(),
|
|
11
|
+
sid: "test-session",
|
|
12
|
+
seq,
|
|
13
|
+
qh: "abcd1234",
|
|
14
|
+
q: `SELECT ?x WHERE { ?x ?p ?o } LIMIT ${seq + 1}`,
|
|
15
|
+
ms: seq * 1.5,
|
|
16
|
+
type: "select",
|
|
17
|
+
count: seq + 1,
|
|
18
|
+
vars: ["x"],
|
|
19
|
+
bindings: [{ x: `http://example.org/${seq}` }],
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
describe("readTraceLog", () => {
|
|
24
|
+
let dir: string;
|
|
25
|
+
|
|
26
|
+
beforeEach(() => {
|
|
27
|
+
dir = mkdtempSync(join(tmpdir(), "pragma-trace-read-"));
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
afterEach(() => {
|
|
31
|
+
rmSync(dir, { recursive: true, force: true });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("parses NDJSON records", () => {
|
|
35
|
+
const path = join(dir, "test.ndjson");
|
|
36
|
+
const records = [makeRecord(0), makeRecord(1), makeRecord(2)];
|
|
37
|
+
writeFileSync(path, records.map((r) => JSON.stringify(r)).join("\n"));
|
|
38
|
+
|
|
39
|
+
const result = readTraceLog({ path });
|
|
40
|
+
|
|
41
|
+
expect(result).toHaveLength(3);
|
|
42
|
+
expect(result[0].seq).toBe(0);
|
|
43
|
+
expect(result[1].seq).toBe(1);
|
|
44
|
+
expect(result[2].seq).toBe(2);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns empty array for missing file", () => {
|
|
48
|
+
const result = readTraceLog({ path: join(dir, "nonexistent.ndjson") });
|
|
49
|
+
expect(result).toEqual([]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("skips malformed lines", () => {
|
|
53
|
+
const path = join(dir, "malformed.ndjson");
|
|
54
|
+
writeFileSync(
|
|
55
|
+
path,
|
|
56
|
+
[
|
|
57
|
+
JSON.stringify(makeRecord(0)),
|
|
58
|
+
"not json",
|
|
59
|
+
JSON.stringify(makeRecord(1)),
|
|
60
|
+
].join("\n"),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const result = readTraceLog({ path });
|
|
64
|
+
|
|
65
|
+
expect(result).toHaveLength(2);
|
|
66
|
+
expect(result[0].seq).toBe(0);
|
|
67
|
+
expect(result[1].seq).toBe(1);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("respects limit (returns last N records)", () => {
|
|
71
|
+
const path = join(dir, "limited.ndjson");
|
|
72
|
+
const records = Array.from({ length: 10 }, (_, i) => makeRecord(i));
|
|
73
|
+
writeFileSync(path, records.map((r) => JSON.stringify(r)).join("\n"));
|
|
74
|
+
|
|
75
|
+
const result = readTraceLog({ path, limit: 3 });
|
|
76
|
+
|
|
77
|
+
expect(result).toHaveLength(3);
|
|
78
|
+
expect(result[0].seq).toBe(7);
|
|
79
|
+
expect(result[1].seq).toBe(8);
|
|
80
|
+
expect(result[2].seq).toBe(9);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("returns all records when limit exceeds count", () => {
|
|
84
|
+
const path = join(dir, "small.ndjson");
|
|
85
|
+
writeFileSync(path, JSON.stringify(makeRecord(0)));
|
|
86
|
+
|
|
87
|
+
const result = readTraceLog({ path, limit: 100 });
|
|
88
|
+
|
|
89
|
+
expect(result).toHaveLength(1);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("returns empty array when limit is 0", () => {
|
|
93
|
+
const path = join(dir, "zero.ndjson");
|
|
94
|
+
writeFileSync(path, JSON.stringify(makeRecord(0)));
|
|
95
|
+
|
|
96
|
+
const result = readTraceLog({ path, limit: 0 });
|
|
97
|
+
|
|
98
|
+
expect(result).toHaveLength(0);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read and parse an NDJSON trace log file.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { closeSync, openSync, readFileSync, readSync, statSync } from "node:fs";
|
|
6
|
+
import type { TraceRecord } from "../types.js";
|
|
7
|
+
|
|
8
|
+
export interface ReadTraceLogOptions {
|
|
9
|
+
/** Absolute path to the NDJSON file. */
|
|
10
|
+
path: string;
|
|
11
|
+
/** Maximum number of records to return (from the tail). */
|
|
12
|
+
limit?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Parse NDJSON lines into TraceRecord array, skipping malformed lines. */
|
|
16
|
+
function parseNdjson(content: string): TraceRecord[] {
|
|
17
|
+
const lines = content.split("\n").filter((line) => line.length > 0);
|
|
18
|
+
const records: TraceRecord[] = [];
|
|
19
|
+
for (const line of lines) {
|
|
20
|
+
try {
|
|
21
|
+
records.push(JSON.parse(line) as TraceRecord);
|
|
22
|
+
} catch {
|
|
23
|
+
// Skip malformed lines
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return records;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function readTraceLog(options: ReadTraceLogOptions): TraceRecord[] {
|
|
30
|
+
const { path, limit } = options;
|
|
31
|
+
|
|
32
|
+
let content: string;
|
|
33
|
+
try {
|
|
34
|
+
content = readFileSync(path, "utf-8");
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const records = parseNdjson(content);
|
|
40
|
+
|
|
41
|
+
if (limit !== undefined) {
|
|
42
|
+
// A non-positive or non-finite limit returns no records. Guard explicitly:
|
|
43
|
+
// `slice(-0)` is `slice(0)` (the whole array) and `slice(NaN)` likewise
|
|
44
|
+
// returns everything, so the naive `slice(-limit)` mishandles both.
|
|
45
|
+
if (!Number.isFinite(limit) || limit <= 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return records.slice(-limit);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return records;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Tail a trace log file, calling `onRecord` for each new record.
|
|
56
|
+
* Returns a cleanup function that stops the polling loop.
|
|
57
|
+
*/
|
|
58
|
+
export function followTraceLog(
|
|
59
|
+
path: string,
|
|
60
|
+
onRecord: (record: TraceRecord) => void,
|
|
61
|
+
): () => void {
|
|
62
|
+
let offset = 0;
|
|
63
|
+
let stopped = false;
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
const stat = statSync(path);
|
|
67
|
+
offset = stat.size;
|
|
68
|
+
} catch {
|
|
69
|
+
// File doesn't exist yet — start from 0
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const poll = () => {
|
|
73
|
+
if (stopped) return;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const stat = statSync(path);
|
|
77
|
+
if (stat.size > offset) {
|
|
78
|
+
const buf = Buffer.alloc(stat.size - offset);
|
|
79
|
+
const fd = openSync(path, "r");
|
|
80
|
+
readSync(fd, buf, 0, buf.length, offset);
|
|
81
|
+
closeSync(fd);
|
|
82
|
+
|
|
83
|
+
const chunk = buf.toString("utf-8");
|
|
84
|
+
for (const record of parseNdjson(chunk)) {
|
|
85
|
+
onRecord(record);
|
|
86
|
+
}
|
|
87
|
+
offset = stat.size;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// File may not exist yet or be temporarily unavailable
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!stopped) {
|
|
94
|
+
setTimeout(poll, 250);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// Start polling on next tick
|
|
99
|
+
setTimeout(poll, 0);
|
|
100
|
+
|
|
101
|
+
return () => {
|
|
102
|
+
stopped = true;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
@@ -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
|
+
});
|