@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,147 @@
1
+ /**
2
+ * Read semantic content from a package directory on disk.
3
+ *
4
+ * Shared by LocalLoader and GitLoader. Given a directory path, reads
5
+ * package.json for version, globs TTL files from definitions/ and data/,
6
+ * and scans skills/ for subdirectories.
7
+ *
8
+ * @note Impure — reads filesystem.
9
+ */
10
+
11
+ import {
12
+ existsSync,
13
+ globSync,
14
+ readdirSync,
15
+ readFileSync,
16
+ statSync,
17
+ } from "node:fs";
18
+ import { join } from "node:path";
19
+ import type {
20
+ GraphContent,
21
+ SkillEntry,
22
+ StoryFileEntry,
23
+ } from "../semanticPackage.js";
24
+
25
+ // ---------------------------------------------------------------------------
26
+ // TTL discovery convention
27
+ // ---------------------------------------------------------------------------
28
+
29
+ const TTL_DIRS: readonly { dir: string; glob: string }[] = [
30
+ { dir: "definitions", glob: "definitions/**/*.ttl" },
31
+ { dir: "data", glob: "data/**/*.ttl" },
32
+ ];
33
+
34
+ // ---------------------------------------------------------------------------
35
+ // Public API
36
+ // ---------------------------------------------------------------------------
37
+
38
+ export interface PackageDirContents {
39
+ readonly version: string;
40
+ readonly graphs: GraphContent[];
41
+ readonly skills: SkillEntry[];
42
+ readonly stories: StoryFileEntry[];
43
+ }
44
+
45
+ /**
46
+ * Read semantic package content from a directory.
47
+ *
48
+ * @param dir - Absolute path to the package root directory.
49
+ * @returns Package version, TTL graph content, and skill entries.
50
+ */
51
+ export default function readPackageDir(dir: string): PackageDirContents {
52
+ return {
53
+ version: readVersion(dir),
54
+ graphs: readGraphs(dir),
55
+ skills: readSkills(dir),
56
+ stories: readStories(dir),
57
+ };
58
+ }
59
+
60
+ // ---------------------------------------------------------------------------
61
+ // Helpers
62
+ // ---------------------------------------------------------------------------
63
+
64
+ function readVersion(dir: string): string {
65
+ const pjsonPath = join(dir, "package.json");
66
+ try {
67
+ const raw = readFileSync(pjsonPath, "utf-8");
68
+ const parsed = JSON.parse(raw) as { version?: string };
69
+ return parsed.version ?? "0.0.0";
70
+ } catch {
71
+ return "0.0.0";
72
+ }
73
+ }
74
+
75
+ function readGraphs(dir: string): GraphContent[] {
76
+ const graphs: GraphContent[] = [];
77
+
78
+ for (const entry of TTL_DIRS) {
79
+ if (!existsSync(join(dir, entry.dir))) continue;
80
+
81
+ const files = globSync(entry.glob, { cwd: dir });
82
+ for (const file of files) {
83
+ const absPath = join(dir, file);
84
+ try {
85
+ const content = readFileSync(absPath, "utf-8");
86
+ graphs.push({ path: absPath, content, format: "turtle" });
87
+ } catch {
88
+ // Unreadable file — skip silently
89
+ }
90
+ }
91
+ }
92
+
93
+ return graphs;
94
+ }
95
+
96
+ function readStories(dir: string): StoryFileEntry[] {
97
+ const storiesDir = join(dir, "stories");
98
+ if (!existsSync(storiesDir)) return [];
99
+
100
+ const stories: StoryFileEntry[] = [];
101
+ try {
102
+ // Sorted: readdir order is filesystem-dependent, and story collision
103
+ // resolution must be deterministic across platforms.
104
+ for (const entry of [...readdirSync(storiesDir)].sort()) {
105
+ if (!entry.endsWith(".json")) continue;
106
+ const entryPath = join(storiesDir, entry);
107
+ try {
108
+ stories.push({
109
+ path: entryPath,
110
+ definition: JSON.parse(readFileSync(entryPath, "utf-8")),
111
+ });
112
+ } catch {
113
+ process.stderr.write(
114
+ `Warning: invalid JSON in story file ${entryPath}\n`,
115
+ );
116
+ }
117
+ }
118
+ } catch {
119
+ // readdir failed — no stories
120
+ }
121
+
122
+ return stories;
123
+ }
124
+
125
+ function readSkills(dir: string): SkillEntry[] {
126
+ const skillsDir = join(dir, "skills");
127
+ if (!existsSync(skillsDir)) return [];
128
+
129
+ const skills: SkillEntry[] = [];
130
+ try {
131
+ const entries = readdirSync(skillsDir);
132
+ for (const entry of entries) {
133
+ const entryPath = join(skillsDir, entry);
134
+ try {
135
+ if (statSync(entryPath).isDirectory()) {
136
+ skills.push({ dir: entryPath, folderName: entry });
137
+ }
138
+ } catch {
139
+ // stat failed — skip
140
+ }
141
+ }
142
+ } catch {
143
+ // readdir failed — no skills
144
+ }
145
+
146
+ return skills;
147
+ }
@@ -0,0 +1,84 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import type { BunPlugin } from "bun";
5
+ import { afterEach, describe, expect, it } from "vitest";
6
+ import createStoryAssetsPlugin, {
7
+ STORY_ASSET_PATH_PATTERN,
8
+ } from "./storyAssetsPlugin.js";
9
+
10
+ interface OnLoadRegistration {
11
+ readonly filter: RegExp;
12
+ readonly callback: (args: { path: string }) => unknown;
13
+ }
14
+
15
+ /** Run the plugin's setup against a stub builder and capture its onLoad. */
16
+ function captureOnLoadRegistration(): OnLoadRegistration {
17
+ let captured: OnLoadRegistration | undefined;
18
+ const builder = {
19
+ onLoad(
20
+ constraints: { filter: RegExp },
21
+ callback: OnLoadRegistration["callback"],
22
+ ): void {
23
+ captured = { filter: constraints.filter, callback };
24
+ },
25
+ };
26
+
27
+ createStoryAssetsPlugin().setup(
28
+ builder as unknown as Parameters<BunPlugin["setup"]>[0],
29
+ );
30
+
31
+ if (!captured) throw new Error("plugin did not register an onLoad handler");
32
+ return captured;
33
+ }
34
+
35
+ describe("STORY_ASSET_PATH_PATTERN", () => {
36
+ it("matches JSON directly inside a stories directory", () => {
37
+ expect(
38
+ STORY_ASSET_PATH_PATTERN.test(
39
+ "/repo/node_modules/@canonical/design-system/stories/color.json",
40
+ ),
41
+ ).toBe(true);
42
+ });
43
+
44
+ it("rejects manifests, nested files, near-miss directories, and non-JSON", () => {
45
+ const nonStories = [
46
+ "/repo/node_modules/@canonical/design-system/package.json",
47
+ "/repo/node_modules/@canonical/design-system/stories/nested/color.json",
48
+ "/repo/node_modules/@canonical/design-system/histories/color.json",
49
+ "/repo/node_modules/@canonical/design-system/stories/color.jsonc",
50
+ "/repo/node_modules/@canonical/design-system/stories/color.ttl",
51
+ ];
52
+ for (const path of nonStories) {
53
+ expect(STORY_ASSET_PATH_PATTERN.test(path), path).toBe(false);
54
+ }
55
+ });
56
+ });
57
+
58
+ describe("createStoryAssetsPlugin", () => {
59
+ let tmpDir: string | undefined;
60
+
61
+ afterEach(() => {
62
+ if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
63
+ tmpDir = undefined;
64
+ });
65
+
66
+ it("registers its load hook under the story path filter", () => {
67
+ const { filter } = captureOnLoadRegistration();
68
+ expect(filter).toBe(STORY_ASSET_PATH_PATTERN);
69
+ });
70
+
71
+ it("loads story JSON contents under the file loader", () => {
72
+ tmpDir = mkdtempSync(join(tmpdir(), "pragma-story-assets-"));
73
+ const storyPath = join(tmpDir, "color.json");
74
+ const content = JSON.stringify({ noun: "color" });
75
+ writeFileSync(storyPath, content);
76
+
77
+ const { callback } = captureOnLoadRegistration();
78
+
79
+ expect(callback({ path: storyPath })).toEqual({
80
+ contents: content,
81
+ loader: "file",
82
+ });
83
+ });
84
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Build-time bundler plugin: embed story-pack JSON as file assets.
3
+ *
4
+ * Bun compiles `.json` build inputs into JSON modules, which never
5
+ * surface in `Bun.embeddedFiles`. Story-pack files must instead be
6
+ * embedded as file assets — like TTL graphs — so the bundled loader can
7
+ * serve them inside the compiled binary. This plugin scopes the `file`
8
+ * loader to story paths only, leaving every other JSON input (e.g.
9
+ * `package.json` imported for the CLI version) compiled as a module.
10
+ *
11
+ * Contract with the bundled loader: story files are the only `.json`
12
+ * content embedded as file assets, so every `.json` blob in
13
+ * `Bun.embeddedFiles` that is not named like a package manifest is a
14
+ * story definition. Consequence: no module in the bundle may import a
15
+ * `stories/*.json` file as a JSON module — the plugin would rewrite
16
+ * such an import into a file-path string.
17
+ *
18
+ * Consumed by `scripts/build.ts` only; never bundled into the binary.
19
+ */
20
+
21
+ import { readFileSync } from "node:fs";
22
+ import type { BunPlugin } from "bun";
23
+
24
+ /**
25
+ * Matches story-pack files: a `.json` directly inside a `stories/`
26
+ * directory — the same non-recursive scope the local loader scans.
27
+ */
28
+ export const STORY_ASSET_PATH_PATTERN = /[\\/]stories[\\/][^\\/]+\.json$/;
29
+
30
+ /**
31
+ * Create the bundler plugin that embeds `stories/*.json` as file assets.
32
+ *
33
+ * @returns A `Bun.build` plugin for the compiled-binary build.
34
+ * @note Impure — the registered load callback reads the filesystem.
35
+ */
36
+ export default function createStoryAssetsPlugin(): BunPlugin {
37
+ return {
38
+ name: "story-assets",
39
+ setup(build) {
40
+ build.onLoad({ filter: STORY_ASSET_PATH_PATTERN }, (args) => ({
41
+ contents: readFileSync(args.path, "utf-8"),
42
+ loader: "file",
43
+ }));
44
+ },
45
+ };
46
+ }
@@ -0,0 +1,120 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { PragmaError } from "#error";
3
+ import lookupMany, { lookupToolMeta } from "./lookupMany.js";
4
+
5
+ describe("lookupMany", () => {
6
+ it("returns all results when every query resolves", async () => {
7
+ const result = await lookupMany(["a", "b"], async (q) => q.toUpperCase());
8
+
9
+ expect(result.results).toEqual(["A", "B"]);
10
+ expect(result.errors).toEqual([]);
11
+ expect(result.meta).toEqual({ internalErrorCount: 0 });
12
+ });
13
+
14
+ it("collects PragmaError failures as structured error entries", async () => {
15
+ const result = await lookupMany(["Button", "Buton"], async (q) => {
16
+ if (q === "Buton") {
17
+ throw PragmaError.notFound("block", q, { suggestions: ["Button"] });
18
+ }
19
+ return q;
20
+ });
21
+
22
+ expect(result.results).toEqual(["Button"]);
23
+ expect(result.errors).toEqual([
24
+ {
25
+ query: "Buton",
26
+ code: "ENTITY_NOT_FOUND",
27
+ message: 'block "Buton" not found.',
28
+ suggestions: ["Button"],
29
+ },
30
+ ]);
31
+ expect(result.meta).toEqual({ internalErrorCount: 0 });
32
+ });
33
+
34
+ it("keeps N-1 results when one query throws a non-PragmaError", async () => {
35
+ const result = await lookupMany(
36
+ ["a", "poisoned", "c"],
37
+ async (q): Promise<string> => {
38
+ if (q === "poisoned") {
39
+ throw new TypeError("cannot read properties of undefined");
40
+ }
41
+ return q.toUpperCase();
42
+ },
43
+ );
44
+
45
+ expect(result.results).toEqual(["A", "C"]);
46
+ expect(result.errors).toEqual([
47
+ {
48
+ query: "poisoned",
49
+ code: "INTERNAL_ERROR",
50
+ message: "Internal error: cannot read properties of undefined",
51
+ },
52
+ ]);
53
+ expect(result.meta).toEqual({ internalErrorCount: 1 });
54
+ });
55
+
56
+ it("stringifies non-Error rejection values", async () => {
57
+ const result = await lookupMany(["a"], async () => {
58
+ throw "string failure";
59
+ });
60
+
61
+ expect(result.results).toEqual([]);
62
+ expect(result.errors).toEqual([
63
+ {
64
+ query: "a",
65
+ code: "INTERNAL_ERROR",
66
+ message: "Internal error: string failure",
67
+ },
68
+ ]);
69
+ expect(result.meta).toEqual({ internalErrorCount: 1 });
70
+ });
71
+
72
+ it("counts each internal error and mixes with PragmaError entries", async () => {
73
+ const result = await lookupMany(
74
+ ["ok", "missing", "boom-1", "boom-2"],
75
+ async (q): Promise<string> => {
76
+ if (q === "missing") throw PragmaError.notFound("token", q);
77
+ if (q.startsWith("boom")) throw new Error(q);
78
+ return q;
79
+ },
80
+ );
81
+
82
+ expect(result.results).toEqual(["ok"]);
83
+ expect(result.errors).toHaveLength(3);
84
+ expect(result.errors.map((e) => e.code)).toEqual([
85
+ "ENTITY_NOT_FOUND",
86
+ "INTERNAL_ERROR",
87
+ "INTERNAL_ERROR",
88
+ ]);
89
+ expect(result.meta).toEqual({ internalErrorCount: 2 });
90
+ });
91
+ });
92
+
93
+ describe("lookupToolMeta", () => {
94
+ it("reports count and omits internalErrorCount when zero", async () => {
95
+ const result = await lookupMany(["a", "b"], async (q) => q);
96
+
97
+ expect(lookupToolMeta(result)).toEqual({ count: 2 });
98
+ });
99
+
100
+ it("includes internalErrorCount when at least one query failed unexpectedly", async () => {
101
+ const result = await lookupMany(
102
+ ["a", "boom"],
103
+ async (q): Promise<string> => {
104
+ if (q === "boom") throw new Error("boom");
105
+ return q;
106
+ },
107
+ );
108
+
109
+ expect(lookupToolMeta(result)).toEqual({
110
+ count: 1,
111
+ internalErrorCount: 1,
112
+ });
113
+ });
114
+
115
+ it("tolerates hand-built results without meta", () => {
116
+ expect(lookupToolMeta({ results: ["x"], errors: [] })).toEqual({
117
+ count: 1,
118
+ });
119
+ });
120
+ });
@@ -6,16 +6,27 @@ import type { LookupResult } from "./contracts.js";
6
6
  *
7
7
  * Used by MCP lookup tools so a single call can return multiple results
8
8
  * without failing the entire request when one query is missing.
9
+ *
10
+ * The collection loop never rejects: a `PragmaError` becomes an error entry
11
+ * with its own code, and any other thrown value becomes an `INTERNAL_ERROR`
12
+ * entry — so one poisoned query cannot discard the other queries' results.
13
+ * `meta.internalErrorCount` reports how many entries are internal errors.
9
14
  */
10
15
  export default async function lookupMany<TResult>(
11
16
  queries: readonly string[],
12
17
  lookup: (query: string) => Promise<TResult>,
13
18
  ): Promise<LookupResult<TResult>> {
14
- const errors: Array<{ query: string; code: string; message: string }> = [];
19
+ const errors: Array<{
20
+ query: string;
21
+ code: string;
22
+ message: string;
23
+ suggestions?: readonly string[];
24
+ }> = [];
15
25
  const settled = await Promise.allSettled(
16
26
  queries.map((query) => lookup(query)),
17
27
  );
18
28
  const results: TResult[] = [];
29
+ let internalErrorCount = 0;
19
30
 
20
31
  for (const [index, outcome] of settled.entries()) {
21
32
  const query = queries[index];
@@ -35,11 +46,40 @@ export default async function lookupMany<TResult>(
35
46
  query,
36
47
  code: error.code,
37
48
  message: error.message,
49
+ ...(error.suggestions.length > 0 && {
50
+ suggestions: error.suggestions,
51
+ }),
38
52
  });
39
53
  } else {
40
- throw error;
54
+ internalErrorCount += 1;
55
+ const reason = error instanceof Error ? error.message : String(error);
56
+ errors.push({
57
+ query,
58
+ code: "INTERNAL_ERROR",
59
+ message: `Internal error: ${reason}`,
60
+ });
41
61
  }
42
62
  }
43
63
 
44
- return { results, errors };
64
+ return { results, errors, meta: { internalErrorCount } };
65
+ }
66
+
67
+ /**
68
+ * Build MCP envelope metadata for a lookup result.
69
+ *
70
+ * Always reports `count`; includes `internalErrorCount` only when at least
71
+ * one query failed unexpectedly, so healthy responses stay unchanged.
72
+ *
73
+ * @param result - A lookup result, typically produced via {@link lookupMany}.
74
+ * @returns Meta object for the `{ ok, data, meta }` tool envelope.
75
+ */
76
+ export function lookupToolMeta<TResult>(result: LookupResult<TResult>): {
77
+ count: number;
78
+ internalErrorCount?: number;
79
+ } {
80
+ const internalErrorCount = result.meta?.internalErrorCount ?? 0;
81
+ return {
82
+ count: result.results.length,
83
+ ...(internalErrorCount > 0 && { internalErrorCount }),
84
+ };
45
85
  }
@@ -0,0 +1,89 @@
1
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import type { RawPackageEntry } from "../refs/operations/parseRef.js";
6
+ import { mergeAndParseRefs } from "./mergeAndParseRefs.js";
7
+ import { DEFAULT_PACKAGES } from "./packages.js";
8
+
9
+ const DEFAULT_NAMES = DEFAULT_PACKAGES.map((entry) =>
10
+ typeof entry === "string" ? entry : entry.name,
11
+ );
12
+
13
+ function writeGlobalRefs(
14
+ xdgDir: string,
15
+ packages: ReadonlyArray<RawPackageEntry>,
16
+ ): void {
17
+ const dir = join(xdgDir, "pragma");
18
+ mkdirSync(dir, { recursive: true });
19
+ writeFileSync(join(dir, "refs.json"), JSON.stringify({ packages }));
20
+ }
21
+
22
+ describe("mergeAndParseRefs", () => {
23
+ let tmpDir: string;
24
+ const origEnv = { ...process.env };
25
+
26
+ beforeEach(() => {
27
+ tmpDir = mkdtempSync(join(tmpdir(), "pragma-merge-refs-"));
28
+ // Point XDG_CONFIG_HOME to our temp dir so readGlobalRefs() resolves there
29
+ process.env.XDG_CONFIG_HOME = tmpDir;
30
+ });
31
+
32
+ afterEach(() => {
33
+ process.env = { ...origEnv };
34
+ rmSync(tmpDir, { recursive: true, force: true });
35
+ });
36
+
37
+ it("returns parsed defaults when config and global refs are absent", () => {
38
+ const refs = mergeAndParseRefs(undefined);
39
+ expect(refs.map((ref) => ref.pkg)).toEqual(DEFAULT_NAMES);
40
+ expect(refs.every((ref) => ref.kind === "git")).toBe(true);
41
+ });
42
+
43
+ it("treats an explicitly empty config list as not configured", () => {
44
+ expect(mergeAndParseRefs([])).toEqual(mergeAndParseRefs(undefined));
45
+ });
46
+
47
+ it("falls back to global refs when the config list is empty", () => {
48
+ writeGlobalRefs(tmpDir, [
49
+ { name: "@canonical/design-system", source: "file:///tmp/ds" },
50
+ ]);
51
+
52
+ const refs = mergeAndParseRefs([]);
53
+
54
+ expect(refs.map((ref) => ref.pkg)).toEqual(DEFAULT_NAMES);
55
+ expect(refs.at(0)).toEqual({
56
+ kind: "file",
57
+ pkg: "@canonical/design-system",
58
+ path: "/tmp/ds",
59
+ });
60
+ });
61
+
62
+ it("merges global refs over defaults by package name", () => {
63
+ writeGlobalRefs(tmpDir, [
64
+ { name: "@canonical/design-system", source: "file:///tmp/ds" },
65
+ "@acme/extra",
66
+ ]);
67
+
68
+ const refs = mergeAndParseRefs(undefined);
69
+
70
+ expect(refs.map((ref) => ref.pkg)).toEqual([
71
+ ...DEFAULT_NAMES,
72
+ "@acme/extra",
73
+ ]);
74
+ expect(refs.at(0)?.kind).toBe("file");
75
+ expect(refs.at(-1)).toEqual({ kind: "npm", pkg: "@acme/extra" });
76
+ });
77
+
78
+ it("replaces defaults and global refs with a non-empty config list", () => {
79
+ writeGlobalRefs(tmpDir, ["@acme/global-only"]);
80
+
81
+ const refs = mergeAndParseRefs([
82
+ { name: "@acme/only", source: "file:///tmp/only" },
83
+ ]);
84
+
85
+ expect(refs).toEqual([
86
+ { kind: "file", pkg: "@acme/only", path: "/tmp/only" },
87
+ ]);
88
+ });
89
+ });
@@ -0,0 +1,44 @@
1
+ import {
2
+ type PackageRef,
3
+ parsePackageEntry,
4
+ type RawPackageEntry,
5
+ } from "../refs/operations/parseRef.js";
6
+ import readGlobalRefs from "../refs/operations/readGlobalRefs.js";
7
+ import { DEFAULT_PACKAGES } from "./packages.js";
8
+
9
+ /**
10
+ * Merge config packages, global refs, and the built-in defaults into a
11
+ * final parsed `PackageRef` array.
12
+ *
13
+ * Priority: config `packages` > global `refs.json` > hardcoded defaults.
14
+ * A non-empty config list is a full replacement — only the listed packages
15
+ * resolve. An empty or absent list means "not configured": global refs are
16
+ * merged over the defaults by package name, so a global entry for
17
+ * "@canonical/foo" overrides the default entry for the same package.
18
+ *
19
+ * Kept free of store imports so config-only consumers (e.g.
20
+ * `resolveConfiguredGraphs`) can merge refs without loading the ke/WASM
21
+ * runtime.
22
+ *
23
+ * @note Impure — reads global refs from `~/.config/pragma/refs.json`.
24
+ *
25
+ * @param configPackages - The `packages` field from `pragma.config.json`.
26
+ * @returns Parsed refs ready for the loader chain.
27
+ * @throws PragmaError with code CONFIG_ERROR when an entry is malformed.
28
+ */
29
+ export function mergeAndParseRefs(
30
+ configPackages?: ReadonlyArray<RawPackageEntry>,
31
+ ): PackageRef[] {
32
+ // A non-empty config list replaces the package set entirely.
33
+ if (configPackages && configPackages.length > 0) {
34
+ return configPackages.map((entry) => parsePackageEntry(entry));
35
+ }
36
+
37
+ // Empty or absent → merge global refs over the defaults, keyed by name.
38
+ const merged = new Map<string, RawPackageEntry>();
39
+ for (const entry of [...DEFAULT_PACKAGES, ...readGlobalRefs()]) {
40
+ const name = typeof entry === "string" ? entry : entry.name;
41
+ merged.set(name, entry);
42
+ }
43
+ return [...merged.values()].map((entry) => parsePackageEntry(entry));
44
+ }