@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,33 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { VALID_CHANNELS } from "../constants.js";
3
+ import { parsePackageEntry } from "../domains/refs/operations/parseRef.js";
4
+ import { DEFAULT_CONFIG } from "./defaultConfig.js";
5
+
6
+ describe("DEFAULT_CONFIG", () => {
7
+ it("declares a valid channel", () => {
8
+ expect(VALID_CHANNELS).toContain(DEFAULT_CONFIG.channel);
9
+ });
10
+
11
+ it("declares at least one package, all parseable", () => {
12
+ expect(DEFAULT_CONFIG.packages.length).toBeGreaterThan(0);
13
+ for (const entry of DEFAULT_CONFIG.packages) {
14
+ const ref = parsePackageEntry(entry);
15
+ expect(ref.pkg.length).toBeGreaterThan(0);
16
+ }
17
+ });
18
+
19
+ it("carries the canonical design-system package by default", () => {
20
+ const names = DEFAULT_CONFIG.packages.map((entry) =>
21
+ typeof entry === "string" ? entry : entry.name,
22
+ );
23
+ expect(names).toContain("@canonical/design-system");
24
+ });
25
+
26
+ it("does not declare fields the document must not layer", () => {
27
+ // tier/trace/framework defaults are semantic absences, and packages
28
+ // must reach the merge via DEFAULT_PACKAGES (see defaultConfig.ts).
29
+ expect(DEFAULT_CONFIG.tier).toBeUndefined();
30
+ expect(DEFAULT_CONFIG.trace).toBeUndefined();
31
+ expect(DEFAULT_CONFIG.framework).toBeUndefined();
32
+ });
33
+ });
@@ -0,0 +1,59 @@
1
+ import type { Channel } from "../constants.js";
2
+ import type { RawPackageEntry } from "../domains/refs/operations/parseRef.js";
3
+ import { PragmaError } from "../error/PragmaError.js";
4
+ import defaultsDocument from "./defaults.json" with { type: "json" };
5
+ import parseConfigValues from "./parseConfigValues.js";
6
+ import type { ConfigFileValues } from "./types.js";
7
+
8
+ /** The built-in defaults with the fields every install relies on present. */
9
+ interface DefaultConfig extends ConfigFileValues {
10
+ /** Release channel active when no config file sets one. */
11
+ readonly channel: Channel;
12
+ /** Semantic packages loaded when no config or global refs declare any. */
13
+ readonly packages: ReadonlyArray<RawPackageEntry>;
14
+ }
15
+
16
+ /**
17
+ * Validate the embedded defaults document into the built-in config layer.
18
+ *
19
+ * The document goes through the same parser as user config files, so an
20
+ * invalid edit to `defaults.json` fails loudly at first import (and in
21
+ * tests) instead of silently changing behavior.
22
+ *
23
+ * @throws PragmaError with code `CONFIG_ERROR` when the document is
24
+ * invalid or misses a required default.
25
+ */
26
+ function parseDefaultsDocument(): DefaultConfig {
27
+ const values = parseConfigValues(
28
+ JSON.stringify(defaultsDocument),
29
+ "built-in defaults (src/config/defaults.json)",
30
+ );
31
+ if (values.channel === undefined) {
32
+ throw PragmaError.configError("Built-in defaults must declare a channel.", {
33
+ recovery: { message: "Restore `channel` in src/config/defaults.json." },
34
+ });
35
+ }
36
+ if (values.packages === undefined || values.packages.length === 0) {
37
+ throw PragmaError.configError(
38
+ "Built-in defaults must declare at least one semantic package.",
39
+ {
40
+ recovery: {
41
+ message: "Restore `packages` in src/config/defaults.json.",
42
+ },
43
+ },
44
+ );
45
+ }
46
+ return { ...values, channel: values.channel, packages: values.packages };
47
+ }
48
+
49
+ /**
50
+ * The built-in configuration layer, sourced from the embedded declarative
51
+ * `defaults.json` document rather than hardcoded values.
52
+ *
53
+ * `packages` is deliberately NOT surfaced through the layered `pick()` in
54
+ * `readConfigLayers` — it feeds `DEFAULT_PACKAGES` (and from there the
55
+ * ref merge), where global `refs.json` entries still merge over it by
56
+ * package name. Sourcing it as an ordinary config layer would make it
57
+ * look user-declared and shut global refs out.
58
+ */
59
+ export const DEFAULT_CONFIG: DefaultConfig = parseDefaultsDocument();
@@ -0,0 +1,17 @@
1
+ {
2
+ "channel": "normal",
3
+ "packages": [
4
+ {
5
+ "name": "@canonical/design-system",
6
+ "source": "git+https://github.com/canonical/design-system.git#main"
7
+ },
8
+ {
9
+ "name": "@canonical/anatomy-dsl",
10
+ "source": "git+https://github.com/canonical/anatomy-dsl.git#main"
11
+ },
12
+ {
13
+ "name": "@canonical/code-standards",
14
+ "source": "git+https://github.com/canonical/web-code-standards.git#main"
15
+ }
16
+ ]
17
+ }
@@ -0,0 +1,53 @@
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 findProjectConfigPath from "./findProjectConfigPath.js";
6
+
7
+ describe("findProjectConfigPath", () => {
8
+ let root: string;
9
+
10
+ beforeEach(() => {
11
+ root = mkdtempSync(join(tmpdir(), "pragma-find-config-"));
12
+ mkdirSync(join(root, ".git"));
13
+ mkdirSync(join(root, "packages/app/src"), { recursive: true });
14
+ });
15
+
16
+ afterEach(() => {
17
+ rmSync(root, { recursive: true, force: true });
18
+ });
19
+
20
+ it("finds the config in the starting directory", () => {
21
+ writeFileSync(join(root, "packages/app/pragma.config.json"), "{}");
22
+ expect(findProjectConfigPath(join(root, "packages/app"))).toBe(
23
+ join(root, "packages/app/pragma.config.json"),
24
+ );
25
+ });
26
+
27
+ it("walks up to the nearest config", () => {
28
+ writeFileSync(join(root, "pragma.config.json"), "{}");
29
+ expect(findProjectConfigPath(join(root, "packages/app/src"))).toBe(
30
+ join(root, "pragma.config.json"),
31
+ );
32
+ });
33
+
34
+ it("prefers the nearest config over a higher one", () => {
35
+ writeFileSync(join(root, "pragma.config.json"), "{}");
36
+ writeFileSync(join(root, "packages/app/pragma.config.json"), "{}");
37
+ expect(findProjectConfigPath(join(root, "packages/app/src"))).toBe(
38
+ join(root, "packages/app/pragma.config.json"),
39
+ );
40
+ });
41
+
42
+ it("stops at the repository root (.git) without escaping", () => {
43
+ // The fixture root has .git and no config; nothing above it may leak in.
44
+ expect(findProjectConfigPath(join(root, "packages/app/src"))).toBe(
45
+ undefined,
46
+ );
47
+ });
48
+
49
+ it("checks the .git directory's own level before stopping", () => {
50
+ writeFileSync(join(root, "pragma.config.json"), "{}");
51
+ expect(findProjectConfigPath(root)).toBe(join(root, "pragma.config.json"));
52
+ });
53
+ });
@@ -0,0 +1,36 @@
1
+ import { existsSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+
5
+ /**
6
+ * Find the nearest `pragma.config.json` at or above `cwd`.
7
+ *
8
+ * Walks up the directory tree so pragma works from anywhere inside a
9
+ * project. The walk is bounded: it stops after checking the first
10
+ * directory that contains `.git` (the repository root), after checking
11
+ * the home directory, and at the filesystem root — a config outside the
12
+ * repository or home never leaks in.
13
+ *
14
+ * @param cwd - Directory to start from.
15
+ * @returns Absolute path of the nearest config file, or `undefined`.
16
+ * @note Impure — probes the filesystem while walking up.
17
+ */
18
+ export default function findProjectConfigPath(cwd: string): string | undefined {
19
+ const home = homedir();
20
+ let dir = resolve(cwd);
21
+
22
+ while (true) {
23
+ const candidate = join(dir, "pragma.config.json");
24
+ if (existsSync(candidate)) {
25
+ return candidate;
26
+ }
27
+ if (existsSync(join(dir, ".git")) || dir === home) {
28
+ return undefined;
29
+ }
30
+ const parent = dirname(dir);
31
+ if (parent === dir) {
32
+ return undefined;
33
+ }
34
+ dir = parent;
35
+ }
36
+ }
@@ -1,6 +1,21 @@
1
- /** @module Configuration file reading, writing, and path resolution. */
1
+ /** @module Configuration reading, writing, layered resolution, and paths. */
2
2
  export { default as configExists } from "./configExists.js";
3
+ export { default as findProjectConfigPath } from "./findProjectConfigPath.js";
4
+ export { default as mergeConfigFileUpdate } from "./mergeConfigFileUpdate.js";
5
+ export { default as parseConfigValues } from "./parseConfigValues.js";
3
6
  export { default as readConfig } from "./readConfig.js";
7
+ export { default as readConfigLayers } from "./readConfigLayers.js";
4
8
  export { default as resolveConfigPath } from "./resolveConfigPath.js";
5
- export type { ConfigUpdate, PragmaConfig } from "./types.js";
9
+ export { default as resolveGlobalConfigPath } from "./resolveGlobalConfigPath.js";
10
+ export { default as resolveWriteConfigPath } from "./resolveWriteConfigPath.js";
11
+ export type {
12
+ ConfigFileValues,
13
+ ConfigLayer,
14
+ ConfigLayers,
15
+ ConfigOrigin,
16
+ ConfigOrigins,
17
+ ConfigScope,
18
+ ConfigUpdate,
19
+ PragmaConfig,
20
+ } from "./types.js";
6
21
  export { default as writeConfig } from "./writeConfig.js";
@@ -0,0 +1,58 @@
1
+ import { readFileSync } from "node:fs";
2
+ import type { ConfigUpdate } from "./types.js";
3
+
4
+ /**
5
+ * Compute a config file's new contents after applying an update.
6
+ *
7
+ * Reads the target file itself (not the merged layers — a write must
8
+ * never copy values from another layer into this file), applies the
9
+ * update per field (`undefined` with the key present removes the field),
10
+ * and returns the serialized JSON. Unknown keys already in the file are
11
+ * preserved verbatim.
12
+ *
13
+ * @param configPath - Absolute path of the config file to update.
14
+ * @param update - Fields to set or remove.
15
+ * @returns The file's new contents, newline-terminated.
16
+ * @throws The raw filesystem/parse error when the existing file is
17
+ * present but unreadable or not valid JSON — never silently clobbers.
18
+ * @note Impure — reads the current file contents.
19
+ */
20
+ export default function mergeConfigFileUpdate(
21
+ configPath: string,
22
+ update: ConfigUpdate,
23
+ ): string {
24
+ let existing: Record<string, unknown> = {};
25
+ try {
26
+ const raw = readFileSync(configPath, "utf-8");
27
+ existing = JSON.parse(raw) as Record<string, unknown>;
28
+ } catch (err: unknown) {
29
+ // Only swallow "file not found" — surface parse or permission errors.
30
+ const isNotFound =
31
+ err instanceof Error &&
32
+ "code" in err &&
33
+ (err as NodeJS.ErrnoException).code === "ENOENT";
34
+ if (!isNotFound) {
35
+ throw err;
36
+ }
37
+ }
38
+
39
+ applyField(existing, update, "tier");
40
+ applyField(existing, update, "channel");
41
+ applyField(existing, update, "packages");
42
+ applyField(existing, update, "trace");
43
+ applyField(existing, update, "framework");
44
+
45
+ return `${JSON.stringify(existing, null, 2)}\n`;
46
+ }
47
+
48
+ function applyField(
49
+ existing: Record<string, unknown>,
50
+ update: ConfigUpdate,
51
+ field: keyof ConfigUpdate,
52
+ ): void {
53
+ if (update[field] !== undefined) {
54
+ existing[field] = update[field];
55
+ } else if (field in update) {
56
+ delete existing[field];
57
+ }
58
+ }
@@ -0,0 +1,217 @@
1
+ import {
2
+ type Channel,
3
+ type Framework,
4
+ VALID_CHANNELS,
5
+ VALID_FRAMEWORKS,
6
+ } from "../constants.js";
7
+ import {
8
+ parsePackageEntry,
9
+ type RawPackageEntry,
10
+ } from "../domains/refs/operations/parseRef.js";
11
+ import type { StoryPackDefinition } from "../domains/shared/stories/pack/types.js";
12
+ import validateStoryPackDefinition from "../domains/shared/stories/pack/validateStoryPackDefinition.js";
13
+ import { PragmaError } from "../error/PragmaError.js";
14
+ import type { ConfigFileValues } from "./types.js";
15
+
16
+ /**
17
+ * Type guard: check whether a value is a recognized channel string.
18
+ *
19
+ * @param value - Candidate value.
20
+ * @returns `true` if value is a valid Channel.
21
+ */
22
+ function isValidChannel(value: unknown): value is Channel {
23
+ return typeof value === "string" && VALID_CHANNELS.includes(value as Channel);
24
+ }
25
+
26
+ /**
27
+ * Type guard: check whether a value is a recognized framework string.
28
+ *
29
+ * @param value - Candidate value.
30
+ * @returns `true` if value is a valid Framework.
31
+ */
32
+ function isValidFramework(value: unknown): value is Framework {
33
+ return (
34
+ typeof value === "string" && VALID_FRAMEWORKS.includes(value as Framework)
35
+ );
36
+ }
37
+
38
+ /**
39
+ * Parse and validate one config file's raw contents.
40
+ *
41
+ * Returns only the fields the file actually sets — presence drives layer
42
+ * merging in `readConfigLayers`. An empty or whitespace-only file sets
43
+ * nothing.
44
+ *
45
+ * @param raw - The file's raw text.
46
+ * @param sourcePath - Absolute path, used in error messages.
47
+ * @returns The values this file declares.
48
+ * @throws PragmaError with code `CONFIG_ERROR` on invalid JSON or values.
49
+ */
50
+ export default function parseConfigValues(
51
+ raw: string,
52
+ sourcePath: string,
53
+ ): ConfigFileValues {
54
+ const trimmed = raw.trim();
55
+ if (trimmed === "") {
56
+ return {};
57
+ }
58
+
59
+ let parsed: Record<string, unknown>;
60
+ try {
61
+ parsed = JSON.parse(trimmed) as Record<string, unknown>;
62
+ } catch {
63
+ throw PragmaError.configError(`Invalid JSON in ${sourcePath}.`);
64
+ }
65
+
66
+ const tier = typeof parsed.tier === "string" ? parsed.tier : undefined;
67
+
68
+ let channel: Channel | undefined;
69
+ if (parsed.channel !== undefined) {
70
+ if (!isValidChannel(parsed.channel)) {
71
+ throw PragmaError.configError(
72
+ `Invalid channel "${String(parsed.channel)}".`,
73
+ { validOptions: [...VALID_CHANNELS] },
74
+ );
75
+ }
76
+ channel = parsed.channel;
77
+ }
78
+
79
+ const trace = typeof parsed.trace === "boolean" ? parsed.trace : undefined;
80
+
81
+ let framework: Framework | undefined;
82
+ if (parsed.framework !== undefined) {
83
+ if (!isValidFramework(parsed.framework)) {
84
+ throw PragmaError.configError(
85
+ `Invalid framework "${String(parsed.framework)}".`,
86
+ { validOptions: [...VALID_FRAMEWORKS] },
87
+ );
88
+ }
89
+ framework = parsed.framework;
90
+ }
91
+
92
+ const packages = parsePackagesField(parsed.packages);
93
+ const stories = parseStoriesField(parsed.stories, sourcePath);
94
+ const prefixes = parsePrefixesField(parsed.prefixes);
95
+ const detail = typeof parsed.detail === "string" ? parsed.detail : undefined;
96
+
97
+ return {
98
+ ...(tier !== undefined ? { tier } : {}),
99
+ ...(channel !== undefined ? { channel } : {}),
100
+ ...(packages ? { packages } : {}),
101
+ ...(trace !== undefined ? { trace } : {}),
102
+ ...(framework !== undefined ? { framework } : {}),
103
+ ...(stories ? { stories } : {}),
104
+ ...(prefixes ? { prefixes } : {}),
105
+ ...(detail !== undefined ? { detail } : {}),
106
+ };
107
+ }
108
+
109
+ /**
110
+ * Validate and parse the experimental `stories` config field.
111
+ *
112
+ * @returns The validated definitions, or `undefined` when absent.
113
+ * @throws PragmaError if the field is present but malformed.
114
+ */
115
+ function parseStoriesField(
116
+ raw: unknown,
117
+ sourcePath: string,
118
+ ): ReadonlyArray<StoryPackDefinition> | undefined {
119
+ if (raw === undefined || raw === null) return undefined;
120
+ if (!Array.isArray(raw)) {
121
+ throw PragmaError.configError(
122
+ '"stories" must be an array of story-pack definitions.',
123
+ );
124
+ }
125
+ return raw.map((entry, index) =>
126
+ validateStoryPackDefinition(entry, `${sourcePath} (stories[${index}])`),
127
+ );
128
+ }
129
+
130
+ /**
131
+ * Validate and parse the `prefixes` config field.
132
+ *
133
+ * @returns The prefix map, or `undefined` when absent.
134
+ * @throws PragmaError if the field is present but malformed.
135
+ */
136
+ function parsePrefixesField(
137
+ raw: unknown,
138
+ ): Readonly<Record<string, string>> | undefined {
139
+ if (raw === undefined || raw === null) return undefined;
140
+ if (typeof raw !== "object" || Array.isArray(raw)) {
141
+ throw PragmaError.configError(
142
+ '"prefixes" must be an object mapping prefix to namespace IRI.',
143
+ );
144
+ }
145
+ const entries = Object.entries(raw as Record<string, unknown>);
146
+ for (const [prefix, namespace] of entries) {
147
+ if (typeof namespace !== "string" || !namespace.includes("://")) {
148
+ throw PragmaError.configError(
149
+ `Invalid namespace for prefix "${prefix}": expected an absolute IRI.`,
150
+ );
151
+ }
152
+ }
153
+ return Object.fromEntries(entries) as Record<string, string>;
154
+ }
155
+
156
+ // ---------------------------------------------------------------------------
157
+ // Packages field parsing
158
+ // ---------------------------------------------------------------------------
159
+
160
+ /**
161
+ * Validate and parse the `packages` config field.
162
+ *
163
+ * @returns The validated array, or `undefined` when the field is absent.
164
+ * @throws PragmaError if the field is present but malformed.
165
+ */
166
+ function parsePackagesField(
167
+ raw: unknown,
168
+ ): ReadonlyArray<RawPackageEntry> | undefined {
169
+ if (raw === undefined || raw === null) return undefined;
170
+
171
+ if (!Array.isArray(raw)) {
172
+ throw PragmaError.configError(
173
+ '"packages" must be an array of strings or { name, source? } objects.',
174
+ );
175
+ }
176
+
177
+ // An explicitly empty list means "not configured": ref resolution falls
178
+ // back to global refs/defaults for an empty list (see mergeAndParseRefs),
179
+ // so dropping the field here keeps provenance in config show/doctor/info
180
+ // consistent with the behavior instead of reporting a project/global
181
+ // origin that resolution then ignores.
182
+ if (raw.length === 0) return undefined;
183
+
184
+ const entries: RawPackageEntry[] = [];
185
+ for (const item of raw) {
186
+ if (typeof item === "string") {
187
+ entries.push(item);
188
+ continue;
189
+ }
190
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
191
+ const obj = item as Record<string, unknown>;
192
+ if (typeof obj.name !== "string" || obj.name.length === 0) {
193
+ throw PragmaError.configError(
194
+ 'Each object in "packages" must have a non-empty "name" string.',
195
+ );
196
+ }
197
+ const entry: { name: string; source?: string } = { name: obj.name };
198
+ if (obj.source !== undefined) {
199
+ if (typeof obj.source !== "string") {
200
+ throw PragmaError.configError(
201
+ `Invalid source for "${obj.name}": expected a string.`,
202
+ );
203
+ }
204
+ entry.source = obj.source;
205
+ }
206
+ // Validate format eagerly — fail fast on bad config
207
+ parsePackageEntry(entry);
208
+ entries.push(entry);
209
+ continue;
210
+ }
211
+ throw PragmaError.configError(
212
+ 'Each entry in "packages" must be a string or { name, source? } object.',
213
+ );
214
+ }
215
+
216
+ return entries;
217
+ }