@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.
Files changed (344) hide show
  1. package/README.md +67 -15
  2. package/dist/pragma +0 -0
  3. package/package.json +16 -17
  4. package/src/config/defaultConfig.test.ts +33 -0
  5. package/src/config/defaultConfig.ts +59 -0
  6. package/src/config/defaults.json +17 -0
  7. package/src/config/findProjectConfigPath.test.ts +53 -0
  8. package/src/config/findProjectConfigPath.ts +36 -0
  9. package/src/config/index.ts +17 -2
  10. package/src/config/mergeConfigFileUpdate.ts +58 -0
  11. package/src/config/parseConfigValues.ts +215 -0
  12. package/src/config/readConfig.test.ts +104 -13
  13. package/src/config/readConfig.ts +12 -117
  14. package/src/config/readConfigLayers.test.ts +107 -0
  15. package/src/config/readConfigLayers.ts +115 -0
  16. package/src/config/resolveGlobalConfigPath.ts +13 -0
  17. package/src/config/resolveWriteConfigPath.test.ts +53 -0
  18. package/src/config/resolveWriteConfigPath.ts +31 -0
  19. package/src/config/types.ts +88 -2
  20. package/src/config/writeConfig.ts +25 -43
  21. package/src/constants.ts +12 -1
  22. package/src/domains/block/commands/index.ts +2 -1
  23. package/src/domains/block/commands/list.ts +5 -90
  24. package/src/domains/block/commands/lookup.ts +8 -167
  25. package/src/domains/block/commands/sample.ts +55 -0
  26. package/src/domains/block/formatters/index.ts +2 -1
  27. package/src/domains/block/formatters/sample.ts +21 -0
  28. package/src/domains/block/index.ts +3 -3
  29. package/src/domains/block/mcp/specs.ts +36 -168
  30. package/src/domains/block/operations/index.ts +2 -1
  31. package/src/domains/block/operations/lookup.ts +9 -0
  32. package/src/domains/block/operations/sample.ts +41 -0
  33. package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
  34. package/src/domains/block/orchestration/resolveBlockLookup.ts +11 -3
  35. package/src/domains/block/stories.test.ts +28 -0
  36. package/src/domains/block/stories.ts +254 -0
  37. package/src/domains/config/commands/channel.test.ts +14 -3
  38. package/src/domains/config/commands/channel.ts +11 -7
  39. package/src/domains/config/commands/configScope.ts +46 -0
  40. package/src/domains/config/commands/framework.test.ts +105 -0
  41. package/src/domains/config/commands/framework.ts +91 -0
  42. package/src/domains/config/commands/index.ts +2 -0
  43. package/src/domains/config/commands/show.ts +5 -45
  44. package/src/domains/config/commands/tier.test.ts +13 -2
  45. package/src/domains/config/commands/tier.ts +11 -7
  46. package/src/domains/config/commands/trace.test.ts +165 -0
  47. package/src/domains/config/commands/trace.ts +83 -0
  48. package/src/domains/config/formatters/channel.test.ts +20 -6
  49. package/src/domains/config/formatters/channel.ts +14 -11
  50. package/src/domains/config/formatters/framework.ts +37 -0
  51. package/src/domains/config/formatters/index.ts +2 -0
  52. package/src/domains/config/formatters/show.test.ts +27 -0
  53. package/src/domains/config/formatters/show.ts +27 -4
  54. package/src/domains/config/formatters/tier.test.ts +26 -10
  55. package/src/domains/config/formatters/tier.ts +19 -11
  56. package/src/domains/config/formatters/trace.ts +28 -0
  57. package/src/domains/config/index.ts +15 -2
  58. package/src/domains/config/mcp/specs.ts +33 -53
  59. package/src/domains/config/operations/index.ts +1 -0
  60. package/src/domains/config/operations/show.test.ts +18 -0
  61. package/src/domains/config/operations/show.ts +7 -1
  62. package/src/domains/config/operations/types.ts +7 -0
  63. package/src/domains/config/operations/validateFramework.ts +24 -0
  64. package/src/domains/config/stories.ts +42 -0
  65. package/src/domains/config/tasks/index.ts +3 -0
  66. package/src/domains/config/tasks/setChannel.ts +13 -22
  67. package/src/domains/config/tasks/setFramework.ts +21 -0
  68. package/src/domains/config/tasks/setTier.ts +10 -23
  69. package/src/domains/config/tasks/setTrace.ts +20 -0
  70. package/src/domains/config/tasks/writeConfigField.ts +41 -0
  71. package/src/domains/create/commands/component.ts +5 -5
  72. package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
  73. package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
  74. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
  75. package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
  76. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -48
  77. package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
  78. package/src/domains/doctor/operations/checks/index.ts +1 -0
  79. package/src/domains/doctor/operations/checks.test.ts +162 -8
  80. package/src/domains/doctor/operations/runChecks.test.ts +85 -4
  81. package/src/domains/doctor/operations/runChecks.ts +50 -12
  82. package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
  83. package/src/domains/graph/helpers/resolveUri.ts +28 -21
  84. package/src/domains/graphql/commands/build.test.ts +196 -0
  85. package/src/domains/graphql/commands/build.ts +117 -0
  86. package/src/domains/graphql/commands/check.test.ts +141 -0
  87. package/src/domains/graphql/commands/check.ts +81 -0
  88. package/src/domains/graphql/commands/index.ts +5 -0
  89. package/src/domains/graphql/commands/serve.test.ts +40 -0
  90. package/src/domains/graphql/commands/serve.ts +131 -0
  91. package/src/domains/graphql/constants.ts +10 -0
  92. package/src/domains/graphql/formatters/index.ts +3 -0
  93. package/src/domains/graphql/formatters/report.ts +78 -0
  94. package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
  95. package/src/domains/graphql/helpers/gatherSources.ts +58 -0
  96. package/src/domains/graphql/helpers/index.ts +6 -0
  97. package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
  98. package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
  99. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
  100. package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
  101. package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
  102. package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
  103. package/src/domains/graphql/index.ts +24 -0
  104. package/src/domains/graphql/operations/compileSchema.test.ts +95 -0
  105. package/src/domains/graphql/operations/compileSchema.ts +107 -0
  106. package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
  107. package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
  108. package/src/domains/graphql/operations/index.ts +12 -0
  109. package/src/domains/graphql/types.ts +29 -0
  110. package/src/domains/info/operations/collectInfo.test.ts +16 -5
  111. package/src/domains/info/operations/collectInfo.ts +16 -19
  112. package/src/domains/llm/capabilities.test.ts +91 -0
  113. package/src/domains/llm/commands/capabilities.ts +28 -0
  114. package/src/domains/llm/commands/index.ts +1 -0
  115. package/src/domains/llm/data/commandReference.ts +90 -18
  116. package/src/domains/llm/data/decisionTrees.ts +15 -13
  117. package/src/domains/llm/data/index.ts +2 -0
  118. package/src/domains/llm/data/toolCatalog.ts +190 -0
  119. package/src/domains/llm/formatters/capabilities.ts +50 -0
  120. package/src/domains/llm/formatters/index.ts +1 -0
  121. package/src/domains/llm/index.ts +3 -2
  122. package/src/domains/llm/mcp/index.ts +1 -1
  123. package/src/domains/llm/mcp/specs.ts +59 -52
  124. package/src/domains/llm/types.ts +40 -0
  125. package/src/domains/modifier/commands/index.ts +2 -1
  126. package/src/domains/modifier/commands/list.ts +5 -38
  127. package/src/domains/modifier/commands/lookup.ts +8 -79
  128. package/src/domains/modifier/commands/sample.ts +53 -0
  129. package/src/domains/modifier/formatters/index.ts +2 -1
  130. package/src/domains/modifier/formatters/sample.ts +15 -0
  131. package/src/domains/modifier/index.ts +7 -3
  132. package/src/domains/modifier/mcp/specs.ts +38 -70
  133. package/src/domains/modifier/operations/index.ts +2 -1
  134. package/src/domains/modifier/operations/lookup.ts +9 -0
  135. package/src/domains/modifier/operations/sample.ts +32 -0
  136. package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
  137. package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
  138. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +15 -2
  139. package/src/domains/modifier/stories.test.ts +20 -0
  140. package/src/domains/modifier/stories.ts +77 -0
  141. package/src/domains/ontology/commands/list.ts +5 -58
  142. package/src/domains/ontology/commands/show.ts +6 -58
  143. package/src/domains/ontology/formatters/show.ts +7 -1
  144. package/src/domains/ontology/mcp/specs.ts +7 -84
  145. package/src/domains/ontology/stories.test.ts +36 -0
  146. package/src/domains/ontology/stories.ts +108 -0
  147. package/src/domains/refs/operations/index.ts +1 -1
  148. package/src/domains/refs/operations/paths.ts +29 -1
  149. package/src/domains/refs/operations/updateRefs.test.ts +6 -3
  150. package/src/domains/refs/operations/updateRefs.ts +2 -36
  151. package/src/domains/setup/commands/skills.test.ts +6 -2
  152. package/src/domains/setup/commands/skills.ts +6 -2
  153. package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
  154. package/src/domains/setup/helpers/runSetupTask.ts +62 -70
  155. package/src/domains/setup/operations/setupMcp.test.ts +6 -1
  156. package/src/domains/shared/bootStore.ts +92 -50
  157. package/src/domains/shared/compactUri.test.ts +11 -0
  158. package/src/domains/shared/compactUri.ts +10 -5
  159. package/src/domains/shared/context.ts +2 -2
  160. package/src/domains/shared/contracts.ts +12 -0
  161. package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
  162. package/src/domains/shared/createInteractivePromptSession.ts +236 -0
  163. package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
  164. package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
  165. package/src/domains/shared/loaders/bundledLoader.ts +206 -0
  166. package/src/domains/shared/loaders/gitLoader.ts +31 -0
  167. package/src/domains/shared/loaders/index.ts +9 -0
  168. package/src/domains/shared/loaders/localLoader.ts +87 -0
  169. package/src/domains/shared/loaders/readPackageDir.ts +147 -0
  170. package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
  171. package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
  172. package/src/domains/shared/lookupMany.test.ts +120 -0
  173. package/src/domains/shared/lookupMany.ts +43 -3
  174. package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
  175. package/src/domains/shared/mergeAndParseRefs.ts +44 -0
  176. package/src/domains/shared/packages.ts +14 -120
  177. package/src/domains/shared/pickRandom.ts +22 -0
  178. package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
  179. package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
  180. package/src/domains/shared/runtime.ts +7 -63
  181. package/src/domains/shared/sampleFormatters.ts +47 -0
  182. package/src/domains/shared/semanticPackage.ts +112 -0
  183. package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
  184. package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
  185. package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
  186. package/src/domains/shared/stories/compileLookupTool.ts +96 -0
  187. package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
  188. package/src/domains/shared/stories/compileReadCommand.ts +48 -0
  189. package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
  190. package/src/domains/shared/stories/compileReadTool.ts +38 -0
  191. package/src/domains/shared/stories/condense.test.ts +18 -0
  192. package/src/domains/shared/stories/condense.ts +15 -0
  193. package/src/domains/shared/stories/index.ts +20 -0
  194. package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
  195. package/src/domains/shared/stories/normalizeNames.ts +22 -0
  196. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
  197. package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
  198. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
  199. package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
  200. package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
  201. package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
  202. package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
  203. package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
  204. package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
  205. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
  206. package/src/domains/shared/stories/pack/index.ts +28 -0
  207. package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
  208. package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
  209. package/src/domains/shared/stories/pack/types.ts +104 -0
  210. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
  211. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
  212. package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
  213. package/src/domains/shared/stories/projectCliParameters.ts +57 -0
  214. package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
  215. package/src/domains/shared/stories/projectToolParams.ts +32 -0
  216. package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
  217. package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
  218. package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
  219. package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
  220. package/src/domains/shared/stories/types.ts +171 -0
  221. package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
  222. package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
  223. package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
  224. package/src/domains/shared/suggestions/expandGlob.ts +52 -0
  225. package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
  226. package/src/domains/shared/suggestions/index.ts +7 -0
  227. package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
  228. package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
  229. package/src/domains/shared/suggestions/suggestNames.ts +130 -0
  230. package/src/domains/shared/types/index.ts +1 -0
  231. package/src/domains/shared/types/runtime.ts +4 -1
  232. package/src/domains/shared/types/sample.ts +17 -0
  233. package/src/domains/skill/commands/index.ts +3 -1
  234. package/src/domains/skill/commands/list.test.ts +6 -2
  235. package/src/domains/skill/commands/list.ts +5 -63
  236. package/src/domains/skill/commands/lookup.test.ts +118 -0
  237. package/src/domains/skill/commands/lookup.ts +14 -0
  238. package/src/domains/skill/formatters/index.ts +1 -0
  239. package/src/domains/skill/formatters/lookup.test.ts +46 -0
  240. package/src/domains/skill/formatters/lookup.ts +57 -0
  241. package/src/domains/skill/helpers/resolveSkillSources.ts +48 -16
  242. package/src/domains/skill/index.ts +7 -3
  243. package/src/domains/skill/mcp/specs.ts +10 -29
  244. package/src/domains/skill/operations/discover.test.ts +61 -30
  245. package/src/domains/skill/operations/discover.ts +28 -37
  246. package/src/domains/skill/operations/index.ts +2 -1
  247. package/src/domains/skill/operations/list.test.ts +15 -7
  248. package/src/domains/skill/operations/lookup.test.ts +91 -0
  249. package/src/domains/skill/operations/lookup.ts +70 -0
  250. package/src/domains/skill/stories.test.ts +43 -0
  251. package/src/domains/skill/stories.ts +103 -0
  252. package/src/domains/skill/types.ts +8 -0
  253. package/src/domains/standard/commands/index.ts +2 -1
  254. package/src/domains/standard/commands/list.ts +5 -84
  255. package/src/domains/standard/commands/lookup.ts +8 -93
  256. package/src/domains/standard/commands/sample.ts +57 -0
  257. package/src/domains/standard/formatters/index.ts +2 -1
  258. package/src/domains/standard/formatters/sample.ts +15 -0
  259. package/src/domains/standard/index.ts +8 -1
  260. package/src/domains/standard/mcp/specs.ts +44 -152
  261. package/src/domains/standard/operations/index.ts +2 -1
  262. package/src/domains/standard/operations/lookup.ts +9 -0
  263. package/src/domains/standard/operations/sample.ts +35 -0
  264. package/src/domains/standard/orchestration/resolveStandardLookup.ts +15 -2
  265. package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
  266. package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
  267. package/src/domains/standard/stories.test.ts +24 -0
  268. package/src/domains/standard/stories.ts +151 -0
  269. package/src/domains/tier/commands/list.ts +5 -55
  270. package/src/domains/tier/mcp/specs.ts +5 -31
  271. package/src/domains/tier/stories.test.ts +22 -0
  272. package/src/domains/tier/stories.ts +52 -0
  273. package/src/domains/token/commands/index.ts +2 -1
  274. package/src/domains/token/commands/sample.ts +53 -0
  275. package/src/domains/token/formatters/index.ts +2 -1
  276. package/src/domains/token/formatters/sample.ts +16 -0
  277. package/src/domains/token/index.ts +8 -1
  278. package/src/domains/token/mcp/specs.ts +52 -2
  279. package/src/domains/token/operations/index.ts +2 -1
  280. package/src/domains/token/operations/lookup.ts +9 -0
  281. package/src/domains/token/operations/sample.ts +32 -0
  282. package/src/domains/token/orchestration/resolveTokenLookup.ts +15 -2
  283. package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
  284. package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
  285. package/src/domains/trace/commands/clear.ts +71 -0
  286. package/src/domains/trace/commands/index.ts +5 -0
  287. package/src/domains/trace/commands/sessions.ts +32 -0
  288. package/src/domains/trace/commands/show.ts +145 -0
  289. package/src/domains/trace/formatters/index.ts +9 -0
  290. package/src/domains/trace/formatters/sessions.ts +80 -0
  291. package/src/domains/trace/formatters/show.ts +122 -0
  292. package/src/domains/trace/index.ts +17 -0
  293. package/src/domains/trace/operations/index.ts +8 -0
  294. package/src/domains/trace/operations/listSessions.test.ts +55 -0
  295. package/src/domains/trace/operations/listSessions.ts +52 -0
  296. package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
  297. package/src/domains/trace/operations/readTraceLog.ts +104 -0
  298. package/src/domains/trace/tracePlugin.test.ts +171 -0
  299. package/src/domains/trace/tracePlugin.ts +159 -0
  300. package/src/domains/trace/types.ts +65 -0
  301. package/src/error/PragmaError.ts +15 -2
  302. package/src/error/types.ts +15 -1
  303. package/src/mcp/error/serializeErrorPayload.ts +7 -0
  304. package/src/mcp/error/types.ts +8 -0
  305. package/src/mcp/registerResources.test.ts +405 -75
  306. package/src/mcp/registerResources.ts +5 -299
  307. package/src/mcp/resources/buildGraphIndex.ts +167 -0
  308. package/src/mcp/resources/buildResourceList.test.ts +97 -0
  309. package/src/mcp/resources/buildResourceList.ts +176 -0
  310. package/src/mcp/resources/classifyEntity.test.ts +40 -0
  311. package/src/mcp/resources/classifyEntity.ts +27 -0
  312. package/src/mcp/resources/constants.ts +105 -0
  313. package/src/mcp/resources/displayPredicates.ts +62 -0
  314. package/src/mcp/resources/pickFirstValue.test.ts +29 -0
  315. package/src/mcp/resources/pickFirstValue.ts +26 -0
  316. package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
  317. package/src/mcp/resources/pickPrimaryType.ts +26 -0
  318. package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
  319. package/src/mcp/resources/rankUriCompletions.ts +64 -0
  320. package/src/mcp/resources/readEntity.ts +169 -0
  321. package/src/mcp/resources/registerResources.ts +126 -0
  322. package/src/mcp/resources/types.ts +128 -0
  323. package/src/mcp/tools/index.ts +9 -0
  324. package/src/mcp/tools/registerTools.test.ts +84 -12
  325. package/src/mcp/types.ts +4 -0
  326. package/src/mcp/utils/wrapTool.test.ts +1 -0
  327. package/src/pipeline/collectCommands.ts +15 -2
  328. package/src/pipeline/renderError.ts +15 -0
  329. package/src/pipeline/resolveCommandKind.ts +8 -1
  330. package/src/pipeline/runCli.ts +51 -6
  331. package/src/testing/createTestMcpClient.ts +5 -1
  332. package/src/testing/graphqlFixtures.ts +72 -0
  333. package/src/testing/index.ts +17 -0
  334. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
  335. package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
  336. package/src/testing/integration/executor-execution.test.ts +101 -0
  337. package/src/testing/integration/executor-golden.test.ts +145 -0
  338. package/src/testing/integration/mcp-surface.test.ts +11 -5
  339. package/src/testing/integration/parity.test.ts +17 -12
  340. package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
  341. package/src/testing/setupXdgIsolation.ts +15 -0
  342. package/src/testing/standardParityFixtures.ts +92 -0
  343. package/src/testing/storyFixtures.ts +63 -0
  344. 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,8 @@
1
+ /** @module Re-exports trace operations. */
2
+
3
+ export { listSessions } from "./listSessions.js";
4
+ export {
5
+ followTraceLog,
6
+ type ReadTraceLogOptions,
7
+ readTraceLog,
8
+ } from "./readTraceLog.js";
@@ -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
+ });