@canonical/pragma-cli 0.29.0 → 0.31.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 (410) hide show
  1. package/README.md +128 -47
  2. package/dist/pragma +0 -0
  3. package/package.json +15 -13
  4. package/src/completions/constants.ts +7 -0
  5. package/src/completions/handleQuery.test.ts +32 -0
  6. package/src/completions/handleQuery.ts +20 -1
  7. package/src/completions/queryCompletions.test.ts +5 -0
  8. package/src/completions/queryCompletions.ts +17 -3
  9. package/src/completions/querySocket.test.ts +31 -0
  10. package/src/completions/querySocket.ts +36 -9
  11. package/src/config/defaultConfig.test.ts +33 -0
  12. package/src/config/defaultConfig.ts +59 -0
  13. package/src/config/defaults.json +17 -0
  14. package/src/config/findProjectConfigPath.test.ts +53 -0
  15. package/src/config/findProjectConfigPath.ts +36 -0
  16. package/src/config/index.ts +17 -2
  17. package/src/config/mergeConfigFileUpdate.ts +58 -0
  18. package/src/config/parseConfigValues.ts +217 -0
  19. package/src/config/readConfig.test.ts +104 -13
  20. package/src/config/readConfig.ts +12 -153
  21. package/src/config/readConfigLayers.test.ts +109 -0
  22. package/src/config/readConfigLayers.ts +118 -0
  23. package/src/config/resolveGlobalConfigPath.ts +13 -0
  24. package/src/config/resolveWriteConfigPath.test.ts +53 -0
  25. package/src/config/resolveWriteConfigPath.ts +31 -0
  26. package/src/config/types.ts +83 -1
  27. package/src/config/writeConfig.ts +25 -55
  28. package/src/constants.ts +3 -1
  29. package/src/domains/block/commands/list.ts +5 -90
  30. package/src/domains/block/commands/lookup.ts +8 -167
  31. package/src/domains/block/commands/sample.ts +2 -1
  32. package/src/domains/block/formatters/list.test.ts +3 -3
  33. package/src/domains/block/formatters/lookup.test.ts +5 -5
  34. package/src/domains/block/mcp/specs.ts +16 -196
  35. package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
  36. package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
  37. package/src/domains/block/stories.test.ts +28 -0
  38. package/src/domains/block/stories.ts +260 -0
  39. package/src/domains/config/commands/channel.test.ts +30 -3
  40. package/src/domains/config/commands/channel.ts +25 -8
  41. package/src/domains/config/commands/configScope.ts +46 -0
  42. package/src/domains/config/commands/framework.test.ts +32 -1
  43. package/src/domains/config/commands/framework.ts +26 -8
  44. package/src/domains/config/commands/show.ts +5 -45
  45. package/src/domains/config/commands/tier.test.ts +20 -2
  46. package/src/domains/config/commands/tier.ts +25 -8
  47. package/src/domains/config/commands/trace.test.ts +19 -2
  48. package/src/domains/config/commands/trace.ts +6 -3
  49. package/src/domains/config/formatters/channel.test.ts +20 -6
  50. package/src/domains/config/formatters/channel.ts +14 -11
  51. package/src/domains/config/formatters/framework.ts +14 -11
  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 +7 -7
  57. package/src/domains/config/mcp/specs.ts +33 -53
  58. package/src/domains/config/operations/show.test.ts +18 -0
  59. package/src/domains/config/operations/show.ts +7 -1
  60. package/src/domains/config/operations/types.ts +7 -0
  61. package/src/domains/config/operations/validateTier.test.ts +9 -0
  62. package/src/domains/config/operations/validateTier.ts +10 -2
  63. package/src/domains/config/stories.ts +42 -0
  64. package/src/domains/config/tasks/index.ts +1 -0
  65. package/src/domains/config/tasks/setChannel.ts +13 -28
  66. package/src/domains/config/tasks/setFramework.ts +11 -30
  67. package/src/domains/config/tasks/setTier.ts +10 -29
  68. package/src/domains/config/tasks/setTrace.ts +10 -28
  69. package/src/domains/config/tasks/writeConfigField.ts +41 -0
  70. package/src/domains/create/generatorPack/bundled.ts +49 -0
  71. package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
  72. package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
  73. package/src/domains/create/index.ts +13 -7
  74. package/src/domains/create/mcp/specs.ts +6 -161
  75. package/src/domains/create/renderGeneratorUi.test.ts +103 -0
  76. package/src/domains/create/renderGeneratorUi.ts +65 -0
  77. package/src/domains/doctor/formatters/doctor.test.ts +50 -2
  78. package/src/domains/doctor/formatters/doctor.ts +90 -38
  79. package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
  80. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +133 -0
  81. package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
  82. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +32 -26
  83. package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
  84. package/src/domains/doctor/operations/checks/index.ts +1 -0
  85. package/src/domains/doctor/operations/checks.test.ts +160 -7
  86. package/src/domains/doctor/operations/runChecks.test.ts +15 -6
  87. package/src/domains/doctor/operations/runChecks.ts +2 -0
  88. package/src/domains/doctor/operations/types.ts +23 -1
  89. package/src/domains/graph/commands/inspect.test.ts +2 -2
  90. package/src/domains/graph/commands/query.test.ts +5 -3
  91. package/src/domains/graph/formatters/inspect.ts +7 -7
  92. package/src/domains/graph/formatters/query.test.ts +87 -0
  93. package/src/domains/graph/formatters/query.ts +42 -5
  94. package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
  95. package/src/domains/graph/helpers/resolveUri.ts +28 -21
  96. package/src/domains/graph/operations/inspectUri.test.ts +5 -5
  97. package/src/domains/graphql/commands/build.test.ts +5 -3
  98. package/src/domains/graphql/commands/build.ts +8 -16
  99. package/src/domains/graphql/commands/check.test.ts +5 -3
  100. package/src/domains/graphql/commands/check.ts +7 -13
  101. package/src/domains/graphql/commands/index.ts +1 -0
  102. package/src/domains/graphql/commands/serve.test.ts +46 -0
  103. package/src/domains/graphql/commands/serve.ts +141 -0
  104. package/src/domains/graphql/constants.ts +10 -0
  105. package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
  106. package/src/domains/graphql/helpers/gatherSources.ts +58 -0
  107. package/src/domains/graphql/helpers/index.ts +2 -0
  108. package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
  109. package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
  110. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
  111. package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
  112. package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
  113. package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
  114. package/src/domains/graphql/index.ts +9 -6
  115. package/src/domains/graphql/operations/compileSchema.test.ts +23 -47
  116. package/src/domains/graphql/operations/compileSchema.ts +23 -25
  117. package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
  118. package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
  119. package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
  120. package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
  121. package/src/domains/graphql/operations/index.ts +10 -0
  122. package/src/domains/info/formatters/upgrade.test.ts +7 -4
  123. package/src/domains/info/formatters/upgrade.ts +3 -1
  124. package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
  125. package/src/domains/info/operations/collectEntityCounts.ts +123 -0
  126. package/src/domains/info/operations/collectInfo.test.ts +14 -3
  127. package/src/domains/info/operations/collectInfo.ts +12 -15
  128. package/src/domains/info/operations/index.ts +1 -0
  129. package/src/domains/info/types.ts +13 -0
  130. package/src/domains/llm/capabilities.test.ts +24 -4
  131. package/src/domains/llm/commands/capabilities.ts +12 -3
  132. package/src/domains/llm/commands/llm.ts +5 -1
  133. package/src/domains/llm/data/commandReference.ts +20 -11
  134. package/src/domains/llm/data/decisionTrees.ts +10 -5
  135. package/src/domains/llm/data/toolCatalog.ts +55 -18
  136. package/src/domains/llm/formatters/orientation.ts +5 -1
  137. package/src/domains/llm/llmCommand.test.ts +11 -3
  138. package/src/domains/llm/mcp/specs.ts +3 -4
  139. package/src/domains/llm/operations/collectContext.test.ts +64 -0
  140. package/src/domains/llm/operations/collectContext.ts +7 -17
  141. package/src/domains/modifier/commands/list.ts +5 -38
  142. package/src/domains/modifier/commands/lookup.ts +8 -79
  143. package/src/domains/modifier/commands/sample.ts +2 -1
  144. package/src/domains/modifier/mcp/specs.ts +15 -94
  145. package/src/domains/modifier/operations/lookup.ts +5 -3
  146. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
  147. package/src/domains/modifier/stories.test.ts +20 -0
  148. package/src/domains/modifier/stories.ts +86 -0
  149. package/src/domains/ontology/commands/list.test.ts +5 -3
  150. package/src/domains/ontology/commands/list.ts +5 -58
  151. package/src/domains/ontology/commands/show.test.ts +85 -9
  152. package/src/domains/ontology/commands/show.ts +6 -58
  153. package/src/domains/ontology/formatters/index.ts +1 -0
  154. package/src/domains/ontology/formatters/list.ts +37 -12
  155. package/src/domains/ontology/formatters/show.ts +361 -64
  156. package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
  157. package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
  158. package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
  159. package/src/domains/ontology/helpers/index.ts +14 -0
  160. package/src/domains/ontology/helpers/queryClasses.ts +32 -16
  161. package/src/domains/ontology/helpers/queryProperties.ts +39 -6
  162. package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
  163. package/src/domains/ontology/mcp/specs.ts +7 -84
  164. package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
  165. package/src/domains/ontology/operations/listOntologies.ts +126 -30
  166. package/src/domains/ontology/operations/showOntology.test.ts +131 -12
  167. package/src/domains/ontology/operations/showOntology.ts +289 -7
  168. package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
  169. package/src/domains/ontology/stories.test.ts +36 -0
  170. package/src/domains/ontology/stories.ts +153 -0
  171. package/src/domains/refs/operations/updateRefs.ts +2 -38
  172. package/src/domains/setup/commands/index.ts +1 -0
  173. package/src/domains/setup/commands/setup.ts +70 -0
  174. package/src/domains/setup/commands/skills.test.ts +6 -2
  175. package/src/domains/setup/commands/skills.ts +6 -2
  176. package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
  177. package/src/domains/setup/helpers/runSetupTask.ts +62 -70
  178. package/src/domains/setup/index.ts +7 -3
  179. package/src/domains/setup/operations/setupLsp.test.ts +3 -2
  180. package/src/domains/setup/operations/setupLsp.ts +7 -2
  181. package/src/domains/setup/operations/setupMcp.test.ts +6 -1
  182. package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
  183. package/src/domains/shared/bootStore.test.ts +82 -0
  184. package/src/domains/shared/bootStore.ts +57 -18
  185. package/src/domains/shared/compactUri.test.ts +11 -0
  186. package/src/domains/shared/compactUri.ts +10 -5
  187. package/src/domains/shared/context.ts +2 -2
  188. package/src/domains/shared/contracts.ts +11 -0
  189. package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
  190. package/src/domains/shared/createInteractivePromptSession.ts +236 -0
  191. package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
  192. package/src/domains/shared/extractLocalName.test.ts +3 -3
  193. package/src/domains/shared/formatters.ts +23 -2
  194. package/src/domains/shared/listTiers.ts +40 -0
  195. package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
  196. package/src/domains/shared/loaders/bundledLoader.ts +158 -12
  197. package/src/domains/shared/loaders/gitLoader.ts +1 -2
  198. package/src/domains/shared/loaders/index.ts +4 -1
  199. package/src/domains/shared/loaders/localLoader.ts +2 -4
  200. package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
  201. package/src/domains/shared/loaders/readPackageDir.ts +82 -9
  202. package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
  203. package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
  204. package/src/domains/shared/lookupMany.test.ts +135 -0
  205. package/src/domains/shared/lookupMany.ts +50 -2
  206. package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
  207. package/src/domains/shared/mergeAndParseRefs.ts +44 -0
  208. package/src/domains/shared/namespaces.ts +13 -5
  209. package/src/domains/shared/packages.ts +10 -17
  210. package/src/domains/shared/parseSampleCount.test.ts +47 -0
  211. package/src/domains/shared/parseSampleCount.ts +45 -0
  212. package/src/domains/shared/prefixes.test.ts +211 -0
  213. package/src/domains/shared/prefixes.ts +182 -15
  214. package/src/domains/shared/renderers.ts +5 -5
  215. package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
  216. package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
  217. package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
  218. package/src/domains/shared/resolveLookupExitCode.ts +42 -0
  219. package/src/domains/shared/runtime.ts +4 -63
  220. package/src/domains/shared/semanticPackage.test.ts +89 -0
  221. package/src/domains/shared/semanticPackage.ts +34 -3
  222. package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
  223. package/src/domains/shared/stories/compileLookupCommand.ts +102 -0
  224. package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
  225. package/src/domains/shared/stories/compileLookupTool.ts +96 -0
  226. package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
  227. package/src/domains/shared/stories/compileReadCommand.ts +48 -0
  228. package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
  229. package/src/domains/shared/stories/compileReadTool.ts +38 -0
  230. package/src/domains/shared/stories/condense.test.ts +18 -0
  231. package/src/domains/shared/stories/condense.ts +15 -0
  232. package/src/domains/shared/stories/index.ts +20 -0
  233. package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
  234. package/src/domains/shared/stories/normalizeNames.ts +22 -0
  235. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +151 -0
  236. package/src/domains/shared/stories/pack/applyPackFilters.ts +111 -0
  237. package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
  238. package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
  239. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +81 -0
  240. package/src/domains/shared/stories/pack/buildLookupQuery.ts +154 -0
  241. package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
  242. package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
  243. package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
  244. package/src/domains/shared/stories/pack/collectPackStories.test.ts +406 -0
  245. package/src/domains/shared/stories/pack/collectPackStories.ts +158 -0
  246. package/src/domains/shared/stories/pack/compilePackCommands.ts +50 -0
  247. package/src/domains/shared/stories/pack/compilePackStories.test.ts +751 -0
  248. package/src/domains/shared/stories/pack/compilePackStories.ts +724 -0
  249. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +55 -0
  250. package/src/domains/shared/stories/pack/index.ts +42 -0
  251. package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
  252. package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
  253. package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
  254. package/src/domains/shared/stories/pack/standardParity.test.ts +381 -0
  255. package/src/domains/shared/stories/pack/types.ts +224 -0
  256. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +742 -0
  257. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +859 -0
  258. package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
  259. package/src/domains/shared/stories/projectCliParameters.ts +57 -0
  260. package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
  261. package/src/domains/shared/stories/projectToolParams.ts +32 -0
  262. package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
  263. package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
  264. package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
  265. package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
  266. package/src/domains/shared/stories/types.ts +179 -0
  267. package/src/domains/shared/suggestions/expandGlob.ts +1 -0
  268. package/src/domains/shared/suggestions/suggestNames.ts +43 -10
  269. package/src/domains/shared/types/index.ts +5 -9
  270. package/src/domains/shared/types/ontology.ts +119 -24
  271. package/src/domains/shared/types/standard.ts +9 -52
  272. package/src/domains/skill/commands/index.ts +3 -1
  273. package/src/domains/skill/commands/list.test.ts +6 -2
  274. package/src/domains/skill/commands/list.ts +5 -63
  275. package/src/domains/skill/commands/lookup.test.ts +118 -0
  276. package/src/domains/skill/commands/lookup.ts +14 -0
  277. package/src/domains/skill/formatters/index.ts +1 -0
  278. package/src/domains/skill/formatters/lookup.test.ts +46 -0
  279. package/src/domains/skill/formatters/lookup.ts +57 -0
  280. package/src/domains/skill/helpers/resolveSkillSources.ts +0 -1
  281. package/src/domains/skill/index.ts +7 -3
  282. package/src/domains/skill/mcp/specs.ts +10 -29
  283. package/src/domains/skill/operations/discover.test.ts +61 -30
  284. package/src/domains/skill/operations/discover.ts +28 -37
  285. package/src/domains/skill/operations/index.ts +2 -1
  286. package/src/domains/skill/operations/list.test.ts +15 -7
  287. package/src/domains/skill/operations/lookup.test.ts +91 -0
  288. package/src/domains/skill/operations/lookup.ts +70 -0
  289. package/src/domains/skill/stories.test.ts +43 -0
  290. package/src/domains/skill/stories.ts +103 -0
  291. package/src/domains/skill/types.ts +8 -0
  292. package/src/domains/token/commands/lookup.test.ts +8 -0
  293. package/src/domains/token/commands/lookup.ts +12 -0
  294. package/src/domains/token/commands/sample.ts +2 -1
  295. package/src/domains/token/featureFlag.ts +17 -0
  296. package/src/domains/token/index.ts +5 -0
  297. package/src/domains/token/mcp/specs.ts +18 -2
  298. package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
  299. package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
  300. package/src/domains/trace/commands/show.ts +30 -4
  301. package/src/domains/trace/tracePlugin.test.ts +1 -1
  302. package/src/index.ts +1 -13
  303. package/src/mcp/registerResources.test.ts +382 -74
  304. package/src/mcp/registerResources.ts +5 -348
  305. package/src/mcp/resources/buildGraphIndex.ts +167 -0
  306. package/src/mcp/resources/buildResourceList.test.ts +97 -0
  307. package/src/mcp/resources/buildResourceList.ts +176 -0
  308. package/src/mcp/resources/classifyEntity.test.ts +40 -0
  309. package/src/mcp/resources/classifyEntity.ts +27 -0
  310. package/src/mcp/resources/constants.ts +105 -0
  311. package/src/mcp/resources/displayPredicates.ts +62 -0
  312. package/src/mcp/resources/pickFirstValue.test.ts +29 -0
  313. package/src/mcp/resources/pickFirstValue.ts +26 -0
  314. package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
  315. package/src/mcp/resources/pickPrimaryType.ts +26 -0
  316. package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
  317. package/src/mcp/resources/rankUriCompletions.ts +64 -0
  318. package/src/mcp/resources/readEntity.ts +169 -0
  319. package/src/mcp/resources/registerResources.ts +126 -0
  320. package/src/mcp/resources/types.ts +128 -0
  321. package/src/mcp/tools/index.ts +22 -5
  322. package/src/mcp/tools/registerTools.test.ts +167 -34
  323. package/src/mcp/types.ts +4 -0
  324. package/src/pipeline/collectCommands.test.ts +151 -1
  325. package/src/pipeline/collectCommands.ts +35 -13
  326. package/src/pipeline/createProgram.test.ts +5 -1
  327. package/src/pipeline/createProgram.ts +2 -2
  328. package/src/pipeline/firstRun.test.ts +80 -0
  329. package/src/pipeline/firstRun.ts +99 -0
  330. package/src/pipeline/parseGlobalFlags.test.ts +102 -1
  331. package/src/pipeline/parseGlobalFlags.ts +98 -9
  332. package/src/pipeline/resolveCommandKind.test.ts +12 -0
  333. package/src/pipeline/resolveCommandKind.ts +4 -0
  334. package/src/pipeline/rootHelp.test.ts +87 -0
  335. package/src/pipeline/rootHelp.ts +265 -0
  336. package/src/pipeline/runCli.orchestration.test.ts +28 -12
  337. package/src/pipeline/runCli.test.ts +17 -4
  338. package/src/pipeline/runCli.ts +202 -25
  339. package/src/testing/cli.ts +30 -2
  340. package/src/testing/createTestMcpClient.ts +4 -1
  341. package/src/testing/dsFixtures.ts +21 -4
  342. package/src/testing/index.ts +7 -0
  343. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
  344. package/src/testing/integration/cli-user-stories.test.ts +63 -0
  345. package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
  346. package/src/testing/integration/edge-cases.test.ts +62 -50
  347. package/src/testing/integration/executor-execution.test.ts +103 -0
  348. package/src/testing/integration/executor-golden.test.ts +145 -0
  349. package/src/testing/integration/mcp-surface.test.ts +40 -15
  350. package/src/testing/integration/parity.test.ts +136 -82
  351. package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
  352. package/src/testing/setupXdgIsolation.ts +26 -0
  353. package/src/testing/standardParityFixtures.ts +35 -0
  354. package/src/testing/store.ts +4 -4
  355. package/src/testing/storyFixtures.ts +63 -0
  356. package/src/domains/create/commands/component.test.ts +0 -166
  357. package/src/domains/create/commands/component.ts +0 -123
  358. package/src/domains/create/commands/index.ts +0 -3
  359. package/src/domains/create/commands/package.test.ts +0 -35
  360. package/src/domains/create/commands/package.ts +0 -21
  361. package/src/domains/create/generators.ts +0 -8
  362. package/src/domains/standard/commands/categories.test.ts +0 -70
  363. package/src/domains/standard/commands/categories.ts +0 -48
  364. package/src/domains/standard/commands/index.ts +0 -6
  365. package/src/domains/standard/commands/list.test.ts +0 -105
  366. package/src/domains/standard/commands/list.ts +0 -90
  367. package/src/domains/standard/commands/lookup.test.ts +0 -135
  368. package/src/domains/standard/commands/lookup.ts +0 -99
  369. package/src/domains/standard/commands/sample.ts +0 -57
  370. package/src/domains/standard/formatters/categories.test.ts +0 -44
  371. package/src/domains/standard/formatters/categories.ts +0 -38
  372. package/src/domains/standard/formatters/index.ts +0 -10
  373. package/src/domains/standard/formatters/list.test.ts +0 -169
  374. package/src/domains/standard/formatters/list.ts +0 -184
  375. package/src/domains/standard/formatters/lookup.test.ts +0 -95
  376. package/src/domains/standard/formatters/lookup.ts +0 -101
  377. package/src/domains/standard/formatters/sample.ts +0 -15
  378. package/src/domains/standard/formatters/types.ts +0 -31
  379. package/src/domains/standard/index.ts +0 -40
  380. package/src/domains/standard/mcp/index.ts +0 -2
  381. package/src/domains/standard/mcp/specs.ts +0 -244
  382. package/src/domains/standard/operations/categories.test.ts +0 -61
  383. package/src/domains/standard/operations/categories.ts +0 -38
  384. package/src/domains/standard/operations/index.ts +0 -6
  385. package/src/domains/standard/operations/list.test.ts +0 -75
  386. package/src/domains/standard/operations/list.ts +0 -64
  387. package/src/domains/standard/operations/lookup.test.ts +0 -58
  388. package/src/domains/standard/operations/lookup.ts +0 -151
  389. package/src/domains/standard/operations/sample.ts +0 -35
  390. package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
  391. package/src/domains/standard/orchestration/index.ts +0 -4
  392. package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
  393. package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -32
  394. package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
  395. package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
  396. package/src/domains/standard/standardConfig.ts +0 -42
  397. package/src/domains/standard/types.ts +0 -15
  398. package/src/domains/tier/commands/index.ts +0 -3
  399. package/src/domains/tier/commands/list.test.ts +0 -67
  400. package/src/domains/tier/commands/list.ts +0 -59
  401. package/src/domains/tier/formatters/index.ts +0 -3
  402. package/src/domains/tier/formatters/list.test.ts +0 -33
  403. package/src/domains/tier/formatters/list.ts +0 -45
  404. package/src/domains/tier/index.ts +0 -17
  405. package/src/domains/tier/mcp/index.ts +0 -2
  406. package/src/domains/tier/mcp/specs.ts +0 -38
  407. package/src/domains/tier/operations/index.ts +0 -3
  408. package/src/domains/tier/operations/list.test.ts +0 -32
  409. package/src/domains/tier/operations/list.ts +0 -33
  410. package/src/pipeline/runInteractiveCommand.ts +0 -198
@@ -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
+ }
@@ -6,12 +6,20 @@
6
6
  */
7
7
 
8
8
  import { createNamespace } from "@canonical/ke";
9
- import { PREFIX_MAP } from "./prefixes.js";
9
+ import { PREFIX_MAP, TRANSITIONAL_DS_PREFIX_MAP } from "./prefixes.js";
10
10
 
11
- /** Design system namespace — classes, properties, and instances (e.g., `ds:Component`). */
12
- export const ds = createNamespace(PREFIX_MAP.ds);
13
- /** Component specification namespace — component-level metadata (e.g., `cs:anatomy`). */
14
- export const cs = createNamespace(PREFIX_MAP.cs);
11
+ /**
12
+ * Design system namespace — classes, properties, and instances (e.g., `ds:Component`).
13
+ * @remarks transitional — sourced from the bundled DS fallback until the
14
+ * design-system package declares its own prefixes (remove in P4).
15
+ */
16
+ export const ds = createNamespace(TRANSITIONAL_DS_PREFIX_MAP.ds);
17
+ /**
18
+ * Component specification namespace — component-level metadata (e.g., `cs:anatomy`).
19
+ * @remarks transitional — sourced from the bundled DS fallback until the
20
+ * code-standards package declares its own prefixes (remove in P4).
21
+ */
22
+ export const cs = createNamespace(TRANSITIONAL_DS_PREFIX_MAP.cs);
15
23
  /** RDF Schema namespace — core vocabulary (e.g., `rdfs:label`, `rdfs:subClassOf`). */
16
24
  export const rdfs = createNamespace(PREFIX_MAP.rdfs);
17
25
  /** OWL namespace — ontology constructs (e.g., `owl:Class`, `owl:ObjectProperty`). */
@@ -1,27 +1,20 @@
1
1
  /**
2
2
  * Default semantic graph package registry.
3
3
  *
4
- * Defines the default graph packages loaded when no config overrides
5
- * are present. Each entry is a git ref pointing to the canonical
6
- * repository so `pragma update-refs` fetches data out of the box.
4
+ * Sourced from the declarative built-in defaults document
5
+ * (`src/config/defaults.json`) — pragma core carries no hardcoded
6
+ * knowledge of specific design-system packages. These form the base
7
+ * package set whenever a config layer does not declare a non-empty
8
+ * `packages` list; global `refs.json` entries then merge over them by
9
+ * package name, and a non-empty config list replaces them entirely
10
+ * (see `mergeAndParseRefs`).
7
11
  *
8
12
  * Resolution is handled by the loader chain in `loaders/`.
9
13
  */
10
14
 
15
+ import { DEFAULT_CONFIG } from "../../config/defaultConfig.js";
11
16
  import type { RawPackageEntry } from "../refs/operations/parseRef.js";
12
17
 
13
18
  /** Default semantic graph packages loaded when no config overrides are present. */
14
- export const DEFAULT_PACKAGES: readonly RawPackageEntry[] = [
15
- {
16
- name: "@canonical/design-system",
17
- source: "git+https://github.com/canonical/design-system.git#main",
18
- },
19
- {
20
- name: "@canonical/anatomy-dsl",
21
- source: "git+https://github.com/canonical/anatomy-dsl.git#main",
22
- },
23
- {
24
- name: "@canonical/code-standards",
25
- source: "git+https://github.com/canonical/web-code-standards.git#main",
26
- },
27
- ];
19
+ export const DEFAULT_PACKAGES: readonly RawPackageEntry[] =
20
+ DEFAULT_CONFIG.packages;
@@ -0,0 +1,47 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { PragmaError } from "../../error/index.js";
3
+ import parseSampleCount, {
4
+ DEFAULT_SAMPLE_COUNT,
5
+ MAX_SAMPLE_COUNT,
6
+ MIN_SAMPLE_COUNT,
7
+ } from "./parseSampleCount.js";
8
+
9
+ describe("parseSampleCount", () => {
10
+ it("returns the default for undefined/null/empty input", () => {
11
+ expect(parseSampleCount(undefined)).toBe(DEFAULT_SAMPLE_COUNT);
12
+ expect(parseSampleCount(null)).toBe(DEFAULT_SAMPLE_COUNT);
13
+ expect(parseSampleCount("")).toBe(DEFAULT_SAMPLE_COUNT);
14
+ });
15
+
16
+ it("accepts an in-range integer verbatim", () => {
17
+ expect(parseSampleCount("3")).toBe(3);
18
+ expect(parseSampleCount(4)).toBe(4);
19
+ });
20
+
21
+ it("clamps below the minimum and above the maximum", () => {
22
+ expect(parseSampleCount("0")).toBe(MIN_SAMPLE_COUNT);
23
+ expect(parseSampleCount("-3")).toBe(MIN_SAMPLE_COUNT);
24
+ expect(parseSampleCount("99")).toBe(MAX_SAMPLE_COUNT);
25
+ });
26
+
27
+ it("tolerates surrounding whitespace", () => {
28
+ expect(parseSampleCount(" 3 ")).toBe(3);
29
+ });
30
+
31
+ it.each([
32
+ "abc",
33
+ "3.5",
34
+ "0x5",
35
+ "1e1",
36
+ "3,4",
37
+ "NaN",
38
+ "Infinity",
39
+ ])("throws INVALID_INPUT for the non-integer value %j", (value) => {
40
+ expect(() => parseSampleCount(value)).toThrowError(PragmaError);
41
+ try {
42
+ parseSampleCount(value);
43
+ } catch (error) {
44
+ expect((error as PragmaError).code).toBe("INVALID_INPUT");
45
+ }
46
+ });
47
+ });
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Parse and validate the `--count` argument shared by every `sample` command.
3
+ *
4
+ * A missing count falls back to the default. A non-numeric or non-integer
5
+ * value is rejected as invalid input (so the command exits non-zero) rather
6
+ * than silently coercing to `NaN` and returning zero samples. A valid integer
7
+ * is clamped to the supported 1–5 range.
8
+ */
9
+
10
+ import { PragmaError } from "#error";
11
+
12
+ /** Smallest number of samples a `sample` command will return. */
13
+ export const MIN_SAMPLE_COUNT = 1;
14
+ /** Largest number of samples a `sample` command will return. */
15
+ export const MAX_SAMPLE_COUNT = 5;
16
+ /** Number of samples returned when `--count` is omitted. */
17
+ export const DEFAULT_SAMPLE_COUNT = 2;
18
+
19
+ /**
20
+ * Resolve the raw `--count` param into a clamped sample count.
21
+ *
22
+ * @param raw - The raw param value (`params.count`), possibly undefined.
23
+ * @returns An integer in [MIN_SAMPLE_COUNT, MAX_SAMPLE_COUNT].
24
+ * @throws PragmaError INVALID_INPUT when `raw` is present but not an integer.
25
+ */
26
+ export default function parseSampleCount(raw: unknown): number {
27
+ if (raw === undefined || raw === null || raw === "") {
28
+ return DEFAULT_SAMPLE_COUNT;
29
+ }
30
+
31
+ // Accept only a plain decimal integer. Number() would otherwise coerce hex
32
+ // ("0x5"), scientific ("1e1"), and whitespace-padded forms, none of which a
33
+ // user should be able to pass to --count.
34
+ const text = String(raw).trim();
35
+ if (!/^-?\d+$/.test(text)) {
36
+ throw PragmaError.invalidInput("count", String(raw), {
37
+ recovery: {
38
+ message: `Provide an integer between ${MIN_SAMPLE_COUNT} and ${MAX_SAMPLE_COUNT}.`,
39
+ },
40
+ });
41
+ }
42
+
43
+ const parsed = Number(text);
44
+ return Math.max(MIN_SAMPLE_COUNT, Math.min(MAX_SAMPLE_COUNT, parsed));
45
+ }
@@ -0,0 +1,211 @@
1
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
+ import {
3
+ DEFAULT_PREFIX_MAP,
4
+ P,
5
+ PREFIX_MAP,
6
+ resolvePrefixes,
7
+ TRANSITIONAL_DS_PREFIX_MAP,
8
+ TTL_PREFIXES,
9
+ } from "./prefixes.js";
10
+
11
+ describe("core PREFIX_MAP", () => {
12
+ it("ships only generic RDF vocabularies — no design-system prefixes", () => {
13
+ expect(Object.keys(PREFIX_MAP).sort()).toEqual([
14
+ "owl",
15
+ "rdf",
16
+ "rdfs",
17
+ "skos",
18
+ "xsd",
19
+ ]);
20
+ expect(PREFIX_MAP).not.toHaveProperty("ds");
21
+ expect(PREFIX_MAP).not.toHaveProperty("cs");
22
+ });
23
+ });
24
+
25
+ describe("TRANSITIONAL_DS_PREFIX_MAP", () => {
26
+ it("bundles the design-system prefixes separately from the core", () => {
27
+ expect(TRANSITIONAL_DS_PREFIX_MAP).toEqual({
28
+ ds: "https://ds.canonical.com/",
29
+ cs: "http://pragma.canonical.com/codestandards#",
30
+ });
31
+ });
32
+ });
33
+
34
+ describe("DEFAULT_PREFIX_MAP (display set)", () => {
35
+ it("reproduces the pre-P0 compaction set — ds/cs plus rdfs/owl/skos", () => {
36
+ expect(Object.keys(DEFAULT_PREFIX_MAP).sort()).toEqual([
37
+ "cs",
38
+ "ds",
39
+ "owl",
40
+ "rdfs",
41
+ "skos",
42
+ ]);
43
+ });
44
+
45
+ it("omits rdf/xsd so URI compaction in output is unchanged", () => {
46
+ expect(DEFAULT_PREFIX_MAP).not.toHaveProperty("rdf");
47
+ expect(DEFAULT_PREFIX_MAP).not.toHaveProperty("xsd");
48
+ });
49
+ });
50
+
51
+ describe("P accessor", () => {
52
+ it("exposes prefixed-name accessors including the transitional ds/cs", () => {
53
+ expect(P.ds).toBe("ds:");
54
+ expect(P.cs).toBe("cs:");
55
+ expect(P.rdfs).toBe("rdfs:");
56
+ expect(P.owl).toBe("owl:");
57
+ expect(P.skos).toBe("skos:");
58
+ });
59
+ });
60
+
61
+ describe("TTL_PREFIXES", () => {
62
+ it("declares every display prefix plus xsd", () => {
63
+ expect(TTL_PREFIXES).toContain("@prefix ds: <https://ds.canonical.com/> .");
64
+ expect(TTL_PREFIXES).toContain(
65
+ "@prefix cs: <http://pragma.canonical.com/codestandards#> .",
66
+ );
67
+ expect(TTL_PREFIXES).toContain(
68
+ "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .",
69
+ );
70
+ });
71
+ });
72
+
73
+ describe("resolvePrefixes", () => {
74
+ it("returns core plus the transitional DS fallback for a bare core", () => {
75
+ const resolved = resolvePrefixes([]);
76
+ expect(resolved).toEqual({ ...PREFIX_MAP, ...TRANSITIONAL_DS_PREFIX_MAP });
77
+ });
78
+
79
+ it("layers package-declared prefixes over the fallback", () => {
80
+ const resolved = resolvePrefixes([
81
+ { prefixes: { ex: "https://example.org/" } },
82
+ ]);
83
+ expect(resolved.ex).toBe("https://example.org/");
84
+ // core + fallback still present
85
+ expect(resolved.ds).toBe(TRANSITIONAL_DS_PREFIX_MAP.ds);
86
+ expect(resolved.rdf).toBe(PREFIX_MAP.rdf);
87
+ });
88
+
89
+ it("lets a package override the transitional DS fallback", () => {
90
+ const resolved = resolvePrefixes([
91
+ { prefixes: { ds: "https://packaged-ds.example/" } },
92
+ ]);
93
+ expect(resolved.ds).toBe("https://packaged-ds.example/");
94
+ });
95
+
96
+ it("lets config prefixes win over both packages and the fallback", () => {
97
+ const resolved = resolvePrefixes(
98
+ [{ prefixes: { ds: "https://packaged-ds.example/" } }],
99
+ { ds: "https://config-ds.example/" },
100
+ );
101
+ expect(resolved.ds).toBe("https://config-ds.example/");
102
+ });
103
+
104
+ it("merges multiple packages in array order", () => {
105
+ const resolved = resolvePrefixes([
106
+ { prefixes: { a: "https://one.example/" } },
107
+ { prefixes: { a: "https://two.example/", b: "https://b.example/" } },
108
+ ]);
109
+ expect(resolved.a).toBe("https://two.example/");
110
+ expect(resolved.b).toBe("https://b.example/");
111
+ });
112
+
113
+ it("tolerates packages that declare no prefixes", () => {
114
+ const resolved = resolvePrefixes([{}, { prefixes: undefined }]);
115
+ expect(resolved).toEqual({ ...PREFIX_MAP, ...TRANSITIONAL_DS_PREFIX_MAP });
116
+ });
117
+ });
118
+
119
+ describe("resolvePrefixes — injection & shadowing hardening", () => {
120
+ let warnings: string[];
121
+ let write: typeof process.stderr.write;
122
+
123
+ beforeEach(() => {
124
+ warnings = [];
125
+ write = process.stderr.write;
126
+ process.stderr.write = ((chunk: string | Uint8Array) => {
127
+ warnings.push(String(chunk));
128
+ return true;
129
+ }) as typeof process.stderr.write;
130
+ });
131
+
132
+ afterEach(() => {
133
+ process.stderr.write = write;
134
+ });
135
+
136
+ it("drops a namespace that could break out of the SPARQL IRIREF", () => {
137
+ // A `>` would close `<...>` early and inject arbitrary SPARQL into every
138
+ // query the store runs.
139
+ const resolved = resolvePrefixes([
140
+ { prefixes: { evil: "http://e> SELECT * WHERE {?s ?p ?o} #" } },
141
+ ]);
142
+ expect(resolved).not.toHaveProperty("evil");
143
+ expect(warnings.join("")).toContain("not valid in an IRI");
144
+ });
145
+
146
+ it("rejects namespaces containing whitespace or angle/quote/brace chars", () => {
147
+ const resolved = resolvePrefixes([
148
+ {
149
+ prefixes: {
150
+ space: "http://e /x",
151
+ quote: 'http://e"x',
152
+ brace: "http://e{x}",
153
+ },
154
+ },
155
+ ]);
156
+ expect(resolved).not.toHaveProperty("space");
157
+ expect(resolved).not.toHaveProperty("quote");
158
+ expect(resolved).not.toHaveProperty("brace");
159
+ });
160
+
161
+ it("keeps a clean namespace untouched", () => {
162
+ const resolved = resolvePrefixes([
163
+ { prefixes: { ex: "https://example.org/ns#" } },
164
+ ]);
165
+ expect(resolved.ex).toBe("https://example.org/ns#");
166
+ expect(warnings).toHaveLength(0);
167
+ });
168
+
169
+ it("refuses to let a package redefine a reserved core prefix", () => {
170
+ const resolved = resolvePrefixes([
171
+ { prefixes: { rdf: "https://evil.example/rdf#" } },
172
+ ]);
173
+ expect(resolved.rdf).toBe(PREFIX_MAP.rdf);
174
+ expect(warnings.join("")).toContain("reserved");
175
+ });
176
+
177
+ it("refuses reserved prefixes from config too", () => {
178
+ const resolved = resolvePrefixes([], { owl: "https://evil.example/owl#" });
179
+ expect(resolved.owl).toBe(PREFIX_MAP.owl);
180
+ });
181
+
182
+ it("skips a malformed prefix name", () => {
183
+ const resolved = resolvePrefixes([
184
+ { prefixes: { "bad name!": "https://example.org/" } },
185
+ ]);
186
+ expect(resolved).not.toHaveProperty("bad name!");
187
+ expect(warnings.join("")).toContain("not a valid prefix name");
188
+ });
189
+
190
+ it("warns on a last-wins collision between two packages", () => {
191
+ const resolved = resolvePrefixes([
192
+ { prefixes: { a: "https://one.example/" } },
193
+ { prefixes: { a: "https://two.example/" } },
194
+ ]);
195
+ expect(resolved.a).toBe("https://two.example/");
196
+ expect(warnings.join("")).toContain("overrides an earlier declaration");
197
+ });
198
+
199
+ it("names the declaring package in collision warnings when available", () => {
200
+ resolvePrefixes([
201
+ { name: "@canonical/one", prefixes: { a: "https://one.example/" } },
202
+ { name: "@canonical/two", prefixes: { a: "https://two.example/" } },
203
+ ]);
204
+ expect(warnings.join("")).toContain('package "@canonical/two"');
205
+ });
206
+
207
+ it("does not warn when a package overrides the trusted DS fallback", () => {
208
+ resolvePrefixes([{ prefixes: { ds: "https://packaged-ds.example/" } }]);
209
+ expect(warnings).toHaveLength(0);
210
+ });
211
+ });
@@ -1,37 +1,204 @@
1
1
  /**
2
- * Prefix map for ke store creation.
2
+ * Prefix maps for ke store creation and display compaction.
3
3
  *
4
- * Registered as PREFIX declarations in every SPARQL query.
5
- * Single source of truth — rename a key here and P updates automatically.
4
+ * The core ships only generic RDF vocabularies. Domain prefixes (e.g. the
5
+ * design system's `ds:`/`cs:`) are supplied by the loaded semantic
6
+ * packages and merged over the core at boot via {@link resolvePrefixes}.
7
+ * Renaming a key here updates every derived accessor automatically.
6
8
  */
7
9
 
8
10
  import type { PrefixMap } from "@canonical/ke";
9
11
 
12
+ /**
13
+ * Core prefix map — the generic RDF vocabularies pragma itself ships.
14
+ *
15
+ * Registered as PREFIX declarations in every SPARQL query and used as the
16
+ * base of the effective prefix map. Contains no domain-specific prefixes:
17
+ * `ds:`/`cs:` and any other package namespaces arrive from the resolved
18
+ * semantic packages (see {@link resolvePrefixes}).
19
+ */
10
20
  export const PREFIX_MAP = {
11
- ds: "https://ds.canonical.com/",
12
- cs: "http://pragma.canonical.com/codestandards#",
21
+ rdf: "http://www.w3.org/1999/02/22-rdf-syntax-ns#",
13
22
  rdfs: "http://www.w3.org/2000/01/rdf-schema#",
14
23
  owl: "http://www.w3.org/2002/07/owl#",
15
24
  skos: "http://www.w3.org/2004/02/skos/core#",
25
+ xsd: "http://www.w3.org/2001/XMLSchema#",
16
26
  } as const satisfies PrefixMap;
17
27
 
18
28
  /**
19
- * SPARQL prefix accessors derived from {@link PREFIX_MAP}.
29
+ * Transitional design-system prefixes bundled with the core.
30
+ *
31
+ * @remarks transitional — remove in P4. These belong to the
32
+ * `@canonical/design-system` and `@canonical/code-standards` packages and
33
+ * must move into their own prefix declarations. Until those packages
34
+ * declare `pragma.prefixes`, this bundled fallback keeps existing DS
35
+ * queries resolving and DS IRIs compacting in output. Package-declared
36
+ * prefixes (and config overrides) win over this fallback.
37
+ */
38
+ export const TRANSITIONAL_DS_PREFIX_MAP = {
39
+ ds: "https://ds.canonical.com/",
40
+ cs: "http://pragma.canonical.com/codestandards#",
41
+ } as const satisfies PrefixMap;
42
+
43
+ /**
44
+ * The default display/compaction prefix set and the base of the derived
45
+ * accessors — the exact set compacted before P0 split the core from the
46
+ * design system, kept stable during the transition.
47
+ *
48
+ * Deliberately omits `rdf:`/`xsd:` (present in {@link PREFIX_MAP}) so URI
49
+ * compaction in output is unchanged from pre-P0 behavior; the store's
50
+ * query prefix map (see {@link resolvePrefixes}) still carries them.
51
+ *
52
+ * @remarks transitional — the `ds:`/`cs:` members move to their packages
53
+ * in P4, at which point this collapses toward {@link PREFIX_MAP}. Runtime
54
+ * code should prefer the map from {@link resolvePrefixes}, which layers in
55
+ * package-declared and config prefixes over the store base.
56
+ */
57
+ export const DEFAULT_PREFIX_MAP = {
58
+ ds: TRANSITIONAL_DS_PREFIX_MAP.ds,
59
+ cs: TRANSITIONAL_DS_PREFIX_MAP.cs,
60
+ rdfs: PREFIX_MAP.rdfs,
61
+ owl: PREFIX_MAP.owl,
62
+ skos: PREFIX_MAP.skos,
63
+ } as const satisfies PrefixMap;
64
+
65
+ /**
66
+ * Core prefixes reserved for pragma's own vocabularies. Packages and config
67
+ * may not redefine these — every query and the RDF/OWL/SKOS toolchain depend
68
+ * on them resolving to the canonical W3C namespaces, so a package that shadows
69
+ * `rdf:` (accidentally or maliciously) would silently corrupt every query.
70
+ */
71
+ const RESERVED_PREFIXES: ReadonlySet<string> = new Set(Object.keys(PREFIX_MAP));
72
+
73
+ /** A syntactically safe SPARQL prefix name (PN_PREFIX subset we accept). */
74
+ const SAFE_PREFIX_NAME = /^[A-Za-z][A-Za-z0-9_-]*$/;
75
+
76
+ /**
77
+ * Reject a namespace that cannot appear inside a SPARQL `<IRIREF>` unescaped.
78
+ * ke injects prefixes verbatim as `PREFIX name: <iri>`; a namespace containing
79
+ * `>` (or whitespace/control/`<"{}|^`\``) would break out of the IRI and inject
80
+ * arbitrary SPARQL into every query. SPARQL's IRIREF grammar forbids exactly
81
+ * these, so anything matching is both unsafe and invalid Turtle/SPARQL.
82
+ */
83
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: IRIREF forbids U+0000–U+0020 by grammar
84
+ const UNSAFE_NAMESPACE = /[\x00-\x20<>"{}|^`\\]/;
85
+
86
+ /**
87
+ * Validate and merge one source of declared prefixes onto an accumulator.
88
+ *
89
+ * Skips (with a stderr warning) any entry that is unsafe to inject: a
90
+ * malformed prefix name, a namespace that could break out of the SPARQL
91
+ * IRIREF, or an attempt to redefine a reserved core prefix. Also warns when a
92
+ * later source silently overrides an already-declared prefix (last-wins
93
+ * collision), so a name clash between two packages is visible rather than
94
+ * mysterious.
95
+ *
96
+ * @note Impure — writes warnings to stderr.
97
+ */
98
+ function mergePrefixSource(
99
+ acc: Record<string, string>,
100
+ claimed: Set<string>,
101
+ source: Readonly<Record<string, string>> | undefined,
102
+ origin: string,
103
+ ): void {
104
+ if (!source) return;
105
+ for (const [name, namespace] of Object.entries(source)) {
106
+ if (!SAFE_PREFIX_NAME.test(name)) {
107
+ process.stderr.write(
108
+ `Warning: ignoring prefix "${name}" from ${origin} — not a valid prefix name.\n`,
109
+ );
110
+ continue;
111
+ }
112
+ if (RESERVED_PREFIXES.has(name)) {
113
+ process.stderr.write(
114
+ `Warning: ignoring prefix "${name}" from ${origin} — "${name}:" is reserved for a core RDF vocabulary and cannot be redefined.\n`,
115
+ );
116
+ continue;
117
+ }
118
+ if (UNSAFE_NAMESPACE.test(namespace)) {
119
+ process.stderr.write(
120
+ `Warning: ignoring prefix "${name}" from ${origin} — namespace contains characters that are not valid in an IRI.\n`,
121
+ );
122
+ continue;
123
+ }
124
+ // Warn only on a collision between two *validated* sources (package vs.
125
+ // package/config). Silently overriding the trusted DS fallback is the
126
+ // expected transitional handoff, not a clash.
127
+ if (claimed.has(name) && acc[name] !== namespace) {
128
+ process.stderr.write(
129
+ `Warning: prefix "${name}" from ${origin} overrides an earlier declaration (was <${acc[name]}>, now <${namespace}>).\n`,
130
+ );
131
+ }
132
+ acc[name] = namespace;
133
+ claimed.add(name);
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Resolve the effective prefix map for a boot.
139
+ *
140
+ * Precedence (lowest → highest): the generic {@link PREFIX_MAP}, the
141
+ * transitional DS fallback, prefixes declared by resolved packages (in
142
+ * array order), then config-level overrides. A package that declares its
143
+ * own `ds:`/`cs:` therefore overrides the bundled fallback, and a user's
144
+ * `pragma.config.json` `prefixes` wins over everything.
145
+ *
146
+ * Package- and config-declared prefixes are validated before they reach the
147
+ * store: unsafe names/namespaces are skipped (they would otherwise be injected
148
+ * verbatim into every SPARQL query), reserved core prefixes cannot be
149
+ * redefined, and last-wins collisions are surfaced as warnings. The bundled
150
+ * core and DS fallback are trusted and merged as-is.
151
+ *
152
+ * @param packages - Resolved packages, each optionally declaring prefixes.
153
+ * @param configPrefixes - Config-level prefix overrides.
154
+ * @returns The merged prefix map for the store and every query.
155
+ */
156
+ export function resolvePrefixes(
157
+ packages: readonly {
158
+ readonly name?: string;
159
+ readonly prefixes?: Readonly<Record<string, string>>;
160
+ }[] = [],
161
+ configPrefixes?: Readonly<Record<string, string>>,
162
+ ): Record<string, string> {
163
+ const merged: Record<string, string> = {
164
+ ...PREFIX_MAP,
165
+ ...TRANSITIONAL_DS_PREFIX_MAP,
166
+ };
167
+ // Keys claimed by a validated source, so collisions between two such
168
+ // sources warn while overriding the trusted seed stays silent.
169
+ const claimed = new Set<string>();
170
+ for (const pkg of packages) {
171
+ // Name the declaring package in the origin so collision/injection
172
+ // warnings point at the actual source, not a generic placeholder.
173
+ const origin = pkg.name ? `package "${pkg.name}"` : "a semantic package";
174
+ mergePrefixSource(merged, claimed, pkg.prefixes, origin);
175
+ }
176
+ mergePrefixSource(merged, claimed, configPrefixes, "pragma.config.json");
177
+ return merged;
178
+ }
179
+
180
+ /**
181
+ * SPARQL prefix accessors derived from {@link DEFAULT_PREFIX_MAP}.
20
182
  *
21
183
  * Use in query templates: `${P.ds}Component` → `"ds:Component"`.
22
- * Renaming a key in PREFIX_MAP automatically updates every query.
184
+ * Renaming a key in the underlying maps automatically updates every query.
185
+ *
186
+ * @remarks The `ds:`/`cs:` accessors are transitional (see
187
+ * {@link TRANSITIONAL_DS_PREFIX_MAP}); they resolve at query time against
188
+ * the store's merged prefix map, which the packages now supply.
23
189
  */
24
190
  export const P = Object.fromEntries(
25
- Object.keys(PREFIX_MAP).map((k) => [k, `${k}:`]),
26
- ) as { readonly [K in keyof typeof PREFIX_MAP]: `${K}:` };
191
+ Object.keys(DEFAULT_PREFIX_MAP).map((k) => [k, `${k}:`]),
192
+ ) as { readonly [K in keyof typeof DEFAULT_PREFIX_MAP]: `${K}:` };
27
193
 
28
194
  /**
29
- * Turtle `@prefix` declarations derived from {@link PREFIX_MAP}.
30
- *
31
- * Includes `xsd:` which TTL data files commonly use but SPARQL queries
32
- * don't need (ke auto-injects PREFIX_MAP for queries).
195
+ * Turtle `@prefix` declarations derived from {@link DEFAULT_PREFIX_MAP},
196
+ * plus `xsd:` — commonly used by TTL data files but omitted from the
197
+ * display set (SPARQL queries auto-inject the store prefix map).
33
198
  */
34
199
  export const TTL_PREFIXES = [
35
- ...Object.entries(PREFIX_MAP).map(([k, uri]) => `@prefix ${k}: <${uri}> .`),
36
- "@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .",
200
+ ...Object.entries(DEFAULT_PREFIX_MAP).map(
201
+ ([k, uri]) => `@prefix ${k}: <${uri}> .`,
202
+ ),
203
+ `@prefix xsd: <${PREFIX_MAP.xsd}> .`,
37
204
  ].join("\n");
@@ -13,7 +13,7 @@ import type {
13
13
  RenderLookupOptions,
14
14
  SectionDef,
15
15
  } from "./contracts.js";
16
- import { PREFIX_MAP } from "./prefixes.js";
16
+ import { DEFAULT_PREFIX_MAP } from "./prefixes.js";
17
17
 
18
18
  type RenderMode = "plain" | "llm";
19
19
 
@@ -21,7 +21,7 @@ export function renderListPlain<T>(
21
21
  items: readonly T[],
22
22
  options: RenderListOptions<T>,
23
23
  ): string {
24
- const prefixes = options.prefixes ?? PREFIX_MAP;
24
+ const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
25
25
  const rows = items.map((item) =>
26
26
  options.columns
27
27
  .map((column) => ({
@@ -52,7 +52,7 @@ export function renderListLlm<T>(
52
52
  items: readonly T[],
53
53
  options: RenderListOptions<T>,
54
54
  ): string {
55
- const prefixes = options.prefixes ?? PREFIX_MAP;
55
+ const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
56
56
  const lines = [`## ${options.heading} (${items.length})`, ""];
57
57
 
58
58
  for (const item of items) {
@@ -121,7 +121,7 @@ function renderLookupFields<T>(
121
121
  options: RenderLookupOptions<T>,
122
122
  mode: RenderMode,
123
123
  ): string[] {
124
- const prefixes = options.prefixes ?? PREFIX_MAP;
124
+ const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
125
125
  return options.fields.flatMap((field) => {
126
126
  const value = field.value(entity);
127
127
  if (isEmptyValue(value)) {
@@ -173,7 +173,7 @@ function renderSectionValue<T>(
173
173
  options: RenderLookupOptions<T>,
174
174
  mode: RenderMode,
175
175
  ): string | null {
176
- const prefixes = options.prefixes ?? PREFIX_MAP;
176
+ const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
177
177
 
178
178
  switch (section.kind) {
179
179
  case "field":