@canonical/pragma-cli 0.27.1-experimental.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (344) hide show
  1. package/README.md +67 -15
  2. package/dist/pragma +0 -0
  3. package/package.json +16 -17
  4. package/src/config/defaultConfig.test.ts +33 -0
  5. package/src/config/defaultConfig.ts +59 -0
  6. package/src/config/defaults.json +17 -0
  7. package/src/config/findProjectConfigPath.test.ts +53 -0
  8. package/src/config/findProjectConfigPath.ts +36 -0
  9. package/src/config/index.ts +17 -2
  10. package/src/config/mergeConfigFileUpdate.ts +58 -0
  11. package/src/config/parseConfigValues.ts +215 -0
  12. package/src/config/readConfig.test.ts +104 -13
  13. package/src/config/readConfig.ts +12 -117
  14. package/src/config/readConfigLayers.test.ts +107 -0
  15. package/src/config/readConfigLayers.ts +115 -0
  16. package/src/config/resolveGlobalConfigPath.ts +13 -0
  17. package/src/config/resolveWriteConfigPath.test.ts +53 -0
  18. package/src/config/resolveWriteConfigPath.ts +31 -0
  19. package/src/config/types.ts +88 -2
  20. package/src/config/writeConfig.ts +25 -43
  21. package/src/constants.ts +12 -1
  22. package/src/domains/block/commands/index.ts +2 -1
  23. package/src/domains/block/commands/list.ts +5 -90
  24. package/src/domains/block/commands/lookup.ts +8 -167
  25. package/src/domains/block/commands/sample.ts +55 -0
  26. package/src/domains/block/formatters/index.ts +2 -1
  27. package/src/domains/block/formatters/sample.ts +21 -0
  28. package/src/domains/block/index.ts +3 -3
  29. package/src/domains/block/mcp/specs.ts +36 -168
  30. package/src/domains/block/operations/index.ts +2 -1
  31. package/src/domains/block/operations/lookup.ts +9 -0
  32. package/src/domains/block/operations/sample.ts +41 -0
  33. package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
  34. package/src/domains/block/orchestration/resolveBlockLookup.ts +11 -3
  35. package/src/domains/block/stories.test.ts +28 -0
  36. package/src/domains/block/stories.ts +254 -0
  37. package/src/domains/config/commands/channel.test.ts +14 -3
  38. package/src/domains/config/commands/channel.ts +11 -7
  39. package/src/domains/config/commands/configScope.ts +46 -0
  40. package/src/domains/config/commands/framework.test.ts +105 -0
  41. package/src/domains/config/commands/framework.ts +91 -0
  42. package/src/domains/config/commands/index.ts +2 -0
  43. package/src/domains/config/commands/show.ts +5 -45
  44. package/src/domains/config/commands/tier.test.ts +13 -2
  45. package/src/domains/config/commands/tier.ts +11 -7
  46. package/src/domains/config/commands/trace.test.ts +165 -0
  47. package/src/domains/config/commands/trace.ts +83 -0
  48. package/src/domains/config/formatters/channel.test.ts +20 -6
  49. package/src/domains/config/formatters/channel.ts +14 -11
  50. package/src/domains/config/formatters/framework.ts +37 -0
  51. package/src/domains/config/formatters/index.ts +2 -0
  52. package/src/domains/config/formatters/show.test.ts +27 -0
  53. package/src/domains/config/formatters/show.ts +27 -4
  54. package/src/domains/config/formatters/tier.test.ts +26 -10
  55. package/src/domains/config/formatters/tier.ts +19 -11
  56. package/src/domains/config/formatters/trace.ts +28 -0
  57. package/src/domains/config/index.ts +15 -2
  58. package/src/domains/config/mcp/specs.ts +33 -53
  59. package/src/domains/config/operations/index.ts +1 -0
  60. package/src/domains/config/operations/show.test.ts +18 -0
  61. package/src/domains/config/operations/show.ts +7 -1
  62. package/src/domains/config/operations/types.ts +7 -0
  63. package/src/domains/config/operations/validateFramework.ts +24 -0
  64. package/src/domains/config/stories.ts +42 -0
  65. package/src/domains/config/tasks/index.ts +3 -0
  66. package/src/domains/config/tasks/setChannel.ts +13 -22
  67. package/src/domains/config/tasks/setFramework.ts +21 -0
  68. package/src/domains/config/tasks/setTier.ts +10 -23
  69. package/src/domains/config/tasks/setTrace.ts +20 -0
  70. package/src/domains/config/tasks/writeConfigField.ts +41 -0
  71. package/src/domains/create/commands/component.ts +5 -5
  72. package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
  73. package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
  74. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
  75. package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
  76. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -48
  77. package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
  78. package/src/domains/doctor/operations/checks/index.ts +1 -0
  79. package/src/domains/doctor/operations/checks.test.ts +162 -8
  80. package/src/domains/doctor/operations/runChecks.test.ts +85 -4
  81. package/src/domains/doctor/operations/runChecks.ts +50 -12
  82. package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
  83. package/src/domains/graph/helpers/resolveUri.ts +28 -21
  84. package/src/domains/graphql/commands/build.test.ts +196 -0
  85. package/src/domains/graphql/commands/build.ts +117 -0
  86. package/src/domains/graphql/commands/check.test.ts +141 -0
  87. package/src/domains/graphql/commands/check.ts +81 -0
  88. package/src/domains/graphql/commands/index.ts +5 -0
  89. package/src/domains/graphql/commands/serve.test.ts +40 -0
  90. package/src/domains/graphql/commands/serve.ts +131 -0
  91. package/src/domains/graphql/constants.ts +10 -0
  92. package/src/domains/graphql/formatters/index.ts +3 -0
  93. package/src/domains/graphql/formatters/report.ts +78 -0
  94. package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
  95. package/src/domains/graphql/helpers/gatherSources.ts +58 -0
  96. package/src/domains/graphql/helpers/index.ts +6 -0
  97. package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
  98. package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
  99. package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
  100. package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
  101. package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
  102. package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
  103. package/src/domains/graphql/index.ts +24 -0
  104. package/src/domains/graphql/operations/compileSchema.test.ts +95 -0
  105. package/src/domains/graphql/operations/compileSchema.ts +107 -0
  106. package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
  107. package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
  108. package/src/domains/graphql/operations/index.ts +12 -0
  109. package/src/domains/graphql/types.ts +29 -0
  110. package/src/domains/info/operations/collectInfo.test.ts +16 -5
  111. package/src/domains/info/operations/collectInfo.ts +16 -19
  112. package/src/domains/llm/capabilities.test.ts +91 -0
  113. package/src/domains/llm/commands/capabilities.ts +28 -0
  114. package/src/domains/llm/commands/index.ts +1 -0
  115. package/src/domains/llm/data/commandReference.ts +90 -18
  116. package/src/domains/llm/data/decisionTrees.ts +15 -13
  117. package/src/domains/llm/data/index.ts +2 -0
  118. package/src/domains/llm/data/toolCatalog.ts +190 -0
  119. package/src/domains/llm/formatters/capabilities.ts +50 -0
  120. package/src/domains/llm/formatters/index.ts +1 -0
  121. package/src/domains/llm/index.ts +3 -2
  122. package/src/domains/llm/mcp/index.ts +1 -1
  123. package/src/domains/llm/mcp/specs.ts +59 -52
  124. package/src/domains/llm/types.ts +40 -0
  125. package/src/domains/modifier/commands/index.ts +2 -1
  126. package/src/domains/modifier/commands/list.ts +5 -38
  127. package/src/domains/modifier/commands/lookup.ts +8 -79
  128. package/src/domains/modifier/commands/sample.ts +53 -0
  129. package/src/domains/modifier/formatters/index.ts +2 -1
  130. package/src/domains/modifier/formatters/sample.ts +15 -0
  131. package/src/domains/modifier/index.ts +7 -3
  132. package/src/domains/modifier/mcp/specs.ts +38 -70
  133. package/src/domains/modifier/operations/index.ts +2 -1
  134. package/src/domains/modifier/operations/lookup.ts +9 -0
  135. package/src/domains/modifier/operations/sample.ts +32 -0
  136. package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
  137. package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
  138. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +15 -2
  139. package/src/domains/modifier/stories.test.ts +20 -0
  140. package/src/domains/modifier/stories.ts +77 -0
  141. package/src/domains/ontology/commands/list.ts +5 -58
  142. package/src/domains/ontology/commands/show.ts +6 -58
  143. package/src/domains/ontology/formatters/show.ts +7 -1
  144. package/src/domains/ontology/mcp/specs.ts +7 -84
  145. package/src/domains/ontology/stories.test.ts +36 -0
  146. package/src/domains/ontology/stories.ts +108 -0
  147. package/src/domains/refs/operations/index.ts +1 -1
  148. package/src/domains/refs/operations/paths.ts +29 -1
  149. package/src/domains/refs/operations/updateRefs.test.ts +6 -3
  150. package/src/domains/refs/operations/updateRefs.ts +2 -36
  151. package/src/domains/setup/commands/skills.test.ts +6 -2
  152. package/src/domains/setup/commands/skills.ts +6 -2
  153. package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
  154. package/src/domains/setup/helpers/runSetupTask.ts +62 -70
  155. package/src/domains/setup/operations/setupMcp.test.ts +6 -1
  156. package/src/domains/shared/bootStore.ts +92 -50
  157. package/src/domains/shared/compactUri.test.ts +11 -0
  158. package/src/domains/shared/compactUri.ts +10 -5
  159. package/src/domains/shared/context.ts +2 -2
  160. package/src/domains/shared/contracts.ts +12 -0
  161. package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
  162. package/src/domains/shared/createInteractivePromptSession.ts +236 -0
  163. package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
  164. package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
  165. package/src/domains/shared/loaders/bundledLoader.ts +206 -0
  166. package/src/domains/shared/loaders/gitLoader.ts +31 -0
  167. package/src/domains/shared/loaders/index.ts +9 -0
  168. package/src/domains/shared/loaders/localLoader.ts +87 -0
  169. package/src/domains/shared/loaders/readPackageDir.ts +147 -0
  170. package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
  171. package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
  172. package/src/domains/shared/lookupMany.test.ts +120 -0
  173. package/src/domains/shared/lookupMany.ts +43 -3
  174. package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
  175. package/src/domains/shared/mergeAndParseRefs.ts +44 -0
  176. package/src/domains/shared/packages.ts +14 -120
  177. package/src/domains/shared/pickRandom.ts +22 -0
  178. package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
  179. package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
  180. package/src/domains/shared/runtime.ts +7 -63
  181. package/src/domains/shared/sampleFormatters.ts +47 -0
  182. package/src/domains/shared/semanticPackage.ts +112 -0
  183. package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
  184. package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
  185. package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
  186. package/src/domains/shared/stories/compileLookupTool.ts +96 -0
  187. package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
  188. package/src/domains/shared/stories/compileReadCommand.ts +48 -0
  189. package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
  190. package/src/domains/shared/stories/compileReadTool.ts +38 -0
  191. package/src/domains/shared/stories/condense.test.ts +18 -0
  192. package/src/domains/shared/stories/condense.ts +15 -0
  193. package/src/domains/shared/stories/index.ts +20 -0
  194. package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
  195. package/src/domains/shared/stories/normalizeNames.ts +22 -0
  196. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
  197. package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
  198. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
  199. package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
  200. package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
  201. package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
  202. package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
  203. package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
  204. package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
  205. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
  206. package/src/domains/shared/stories/pack/index.ts +28 -0
  207. package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
  208. package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
  209. package/src/domains/shared/stories/pack/types.ts +104 -0
  210. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
  211. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
  212. package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
  213. package/src/domains/shared/stories/projectCliParameters.ts +57 -0
  214. package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
  215. package/src/domains/shared/stories/projectToolParams.ts +32 -0
  216. package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
  217. package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
  218. package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
  219. package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
  220. package/src/domains/shared/stories/types.ts +171 -0
  221. package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
  222. package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
  223. package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
  224. package/src/domains/shared/suggestions/expandGlob.ts +52 -0
  225. package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
  226. package/src/domains/shared/suggestions/index.ts +7 -0
  227. package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
  228. package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
  229. package/src/domains/shared/suggestions/suggestNames.ts +130 -0
  230. package/src/domains/shared/types/index.ts +1 -0
  231. package/src/domains/shared/types/runtime.ts +4 -1
  232. package/src/domains/shared/types/sample.ts +17 -0
  233. package/src/domains/skill/commands/index.ts +3 -1
  234. package/src/domains/skill/commands/list.test.ts +6 -2
  235. package/src/domains/skill/commands/list.ts +5 -63
  236. package/src/domains/skill/commands/lookup.test.ts +118 -0
  237. package/src/domains/skill/commands/lookup.ts +14 -0
  238. package/src/domains/skill/formatters/index.ts +1 -0
  239. package/src/domains/skill/formatters/lookup.test.ts +46 -0
  240. package/src/domains/skill/formatters/lookup.ts +57 -0
  241. package/src/domains/skill/helpers/resolveSkillSources.ts +48 -16
  242. package/src/domains/skill/index.ts +7 -3
  243. package/src/domains/skill/mcp/specs.ts +10 -29
  244. package/src/domains/skill/operations/discover.test.ts +61 -30
  245. package/src/domains/skill/operations/discover.ts +28 -37
  246. package/src/domains/skill/operations/index.ts +2 -1
  247. package/src/domains/skill/operations/list.test.ts +15 -7
  248. package/src/domains/skill/operations/lookup.test.ts +91 -0
  249. package/src/domains/skill/operations/lookup.ts +70 -0
  250. package/src/domains/skill/stories.test.ts +43 -0
  251. package/src/domains/skill/stories.ts +103 -0
  252. package/src/domains/skill/types.ts +8 -0
  253. package/src/domains/standard/commands/index.ts +2 -1
  254. package/src/domains/standard/commands/list.ts +5 -84
  255. package/src/domains/standard/commands/lookup.ts +8 -93
  256. package/src/domains/standard/commands/sample.ts +57 -0
  257. package/src/domains/standard/formatters/index.ts +2 -1
  258. package/src/domains/standard/formatters/sample.ts +15 -0
  259. package/src/domains/standard/index.ts +8 -1
  260. package/src/domains/standard/mcp/specs.ts +44 -152
  261. package/src/domains/standard/operations/index.ts +2 -1
  262. package/src/domains/standard/operations/lookup.ts +9 -0
  263. package/src/domains/standard/operations/sample.ts +35 -0
  264. package/src/domains/standard/orchestration/resolveStandardLookup.ts +15 -2
  265. package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
  266. package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
  267. package/src/domains/standard/stories.test.ts +24 -0
  268. package/src/domains/standard/stories.ts +151 -0
  269. package/src/domains/tier/commands/list.ts +5 -55
  270. package/src/domains/tier/mcp/specs.ts +5 -31
  271. package/src/domains/tier/stories.test.ts +22 -0
  272. package/src/domains/tier/stories.ts +52 -0
  273. package/src/domains/token/commands/index.ts +2 -1
  274. package/src/domains/token/commands/sample.ts +53 -0
  275. package/src/domains/token/formatters/index.ts +2 -1
  276. package/src/domains/token/formatters/sample.ts +16 -0
  277. package/src/domains/token/index.ts +8 -1
  278. package/src/domains/token/mcp/specs.ts +52 -2
  279. package/src/domains/token/operations/index.ts +2 -1
  280. package/src/domains/token/operations/lookup.ts +9 -0
  281. package/src/domains/token/operations/sample.ts +32 -0
  282. package/src/domains/token/orchestration/resolveTokenLookup.ts +15 -2
  283. package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
  284. package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
  285. package/src/domains/trace/commands/clear.ts +71 -0
  286. package/src/domains/trace/commands/index.ts +5 -0
  287. package/src/domains/trace/commands/sessions.ts +32 -0
  288. package/src/domains/trace/commands/show.ts +145 -0
  289. package/src/domains/trace/formatters/index.ts +9 -0
  290. package/src/domains/trace/formatters/sessions.ts +80 -0
  291. package/src/domains/trace/formatters/show.ts +122 -0
  292. package/src/domains/trace/index.ts +17 -0
  293. package/src/domains/trace/operations/index.ts +8 -0
  294. package/src/domains/trace/operations/listSessions.test.ts +55 -0
  295. package/src/domains/trace/operations/listSessions.ts +52 -0
  296. package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
  297. package/src/domains/trace/operations/readTraceLog.ts +104 -0
  298. package/src/domains/trace/tracePlugin.test.ts +171 -0
  299. package/src/domains/trace/tracePlugin.ts +159 -0
  300. package/src/domains/trace/types.ts +65 -0
  301. package/src/error/PragmaError.ts +15 -2
  302. package/src/error/types.ts +15 -1
  303. package/src/mcp/error/serializeErrorPayload.ts +7 -0
  304. package/src/mcp/error/types.ts +8 -0
  305. package/src/mcp/registerResources.test.ts +405 -75
  306. package/src/mcp/registerResources.ts +5 -299
  307. package/src/mcp/resources/buildGraphIndex.ts +167 -0
  308. package/src/mcp/resources/buildResourceList.test.ts +97 -0
  309. package/src/mcp/resources/buildResourceList.ts +176 -0
  310. package/src/mcp/resources/classifyEntity.test.ts +40 -0
  311. package/src/mcp/resources/classifyEntity.ts +27 -0
  312. package/src/mcp/resources/constants.ts +105 -0
  313. package/src/mcp/resources/displayPredicates.ts +62 -0
  314. package/src/mcp/resources/pickFirstValue.test.ts +29 -0
  315. package/src/mcp/resources/pickFirstValue.ts +26 -0
  316. package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
  317. package/src/mcp/resources/pickPrimaryType.ts +26 -0
  318. package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
  319. package/src/mcp/resources/rankUriCompletions.ts +64 -0
  320. package/src/mcp/resources/readEntity.ts +169 -0
  321. package/src/mcp/resources/registerResources.ts +126 -0
  322. package/src/mcp/resources/types.ts +128 -0
  323. package/src/mcp/tools/index.ts +9 -0
  324. package/src/mcp/tools/registerTools.test.ts +84 -12
  325. package/src/mcp/types.ts +4 -0
  326. package/src/mcp/utils/wrapTool.test.ts +1 -0
  327. package/src/pipeline/collectCommands.ts +15 -2
  328. package/src/pipeline/renderError.ts +15 -0
  329. package/src/pipeline/resolveCommandKind.ts +8 -1
  330. package/src/pipeline/runCli.ts +51 -6
  331. package/src/testing/createTestMcpClient.ts +5 -1
  332. package/src/testing/graphqlFixtures.ts +72 -0
  333. package/src/testing/index.ts +17 -0
  334. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
  335. package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
  336. package/src/testing/integration/executor-execution.test.ts +101 -0
  337. package/src/testing/integration/executor-golden.test.ts +145 -0
  338. package/src/testing/integration/mcp-surface.test.ts +11 -5
  339. package/src/testing/integration/parity.test.ts +17 -12
  340. package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
  341. package/src/testing/setupXdgIsolation.ts +15 -0
  342. package/src/testing/standardParityFixtures.ts +92 -0
  343. package/src/testing/storyFixtures.ts +63 -0
  344. package/src/pipeline/runInteractiveCommand.ts +0 -198
@@ -0,0 +1,109 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import type { PragmaError } from "#error";
3
+ import blockSpecs from "../block/mcp/specs.js";
4
+ import blockEmptyError from "../block/orchestration/blockEmptyError.js";
5
+ import modifierSpecs from "../modifier/mcp/specs.js";
6
+ import modifierEmptyError from "../modifier/orchestration/modifierEmptyError.js";
7
+ import standardSpecs from "../standard/mcp/specs.js";
8
+ import standardEmptyError from "../standard/orchestration/standardEmptyError.js";
9
+ import tokenSpecs from "../token/mcp/specs.js";
10
+ import tokenEmptyError from "../token/orchestration/tokenEmptyError.js";
11
+ import type { FilterConfig } from "./types/index.js";
12
+
13
+ const filters: FilterConfig = { tier: "Apps/WPE", channel: "normal" };
14
+ const allTiers: FilterConfig = { tier: undefined, channel: "normal" };
15
+
16
+ /**
17
+ * Every distinct empty-result scenario each domain can produce, paired with the
18
+ * set of MCP tool names that domain actually registers. The invariant below
19
+ * holds the four hints to the S-grade contract so authored consistency cannot
20
+ * silently re-drift.
21
+ */
22
+ const cases: ReadonlyArray<{
23
+ domain: string;
24
+ error: PragmaError;
25
+ toolNames: ReadonlySet<string>;
26
+ // store-empty (packages absent) → the install rung, which must NOT carry an
27
+ // mcp retry (re-listing an absent store loops). filter-narrowing → may carry
28
+ // a list-retry mcp pointer.
29
+ storeEmpty: boolean;
30
+ }> = [
31
+ {
32
+ domain: "block (filter active)",
33
+ error: blockEmptyError(filters, false),
34
+ toolNames: new Set(blockSpecs.map((s) => s.name)),
35
+ storeEmpty: false,
36
+ },
37
+ {
38
+ domain: "block (all tiers — terminal)",
39
+ error: blockEmptyError(allTiers, true),
40
+ toolNames: new Set(blockSpecs.map((s) => s.name)),
41
+ storeEmpty: false,
42
+ },
43
+ {
44
+ domain: "token (category active)",
45
+ error: tokenEmptyError("color"),
46
+ toolNames: new Set(tokenSpecs.map((s) => s.name)),
47
+ storeEmpty: false,
48
+ },
49
+ {
50
+ domain: "token (store-empty)",
51
+ error: tokenEmptyError(),
52
+ toolNames: new Set(tokenSpecs.map((s) => s.name)),
53
+ storeEmpty: true,
54
+ },
55
+ {
56
+ domain: "modifier (store-empty)",
57
+ error: modifierEmptyError(),
58
+ toolNames: new Set(modifierSpecs.map((s) => s.name)),
59
+ storeEmpty: true,
60
+ },
61
+ {
62
+ domain: "standard (filter active)",
63
+ error: standardEmptyError({ category: "react" }),
64
+ toolNames: new Set(standardSpecs.map((s) => s.name)),
65
+ storeEmpty: false,
66
+ },
67
+ {
68
+ domain: "standard (store-empty)",
69
+ error: standardEmptyError({}),
70
+ toolNames: new Set(standardSpecs.map((s) => s.name)),
71
+ storeEmpty: true,
72
+ },
73
+ ];
74
+
75
+ describe("empty-error recovery contract (cross-domain)", () => {
76
+ it.each(cases)("$domain honours the S-grade recovery shape", ({
77
+ error,
78
+ toolNames,
79
+ storeEmpty,
80
+ }) => {
81
+ const recovery = error.recovery;
82
+
83
+ // Terminal state (e.g. block already at all-tiers) is allowed.
84
+ if (recovery === undefined) return;
85
+
86
+ // Every hint carries a human-readable message.
87
+ expect(recovery.message).toBeTruthy();
88
+
89
+ // No hint is message-only: a runnable cli is always present (either a
90
+ // pragma re-list for filter-narrowing, or a bun add for store-empty).
91
+ expect(recovery.cli).toBeTruthy();
92
+
93
+ if (storeEmpty) {
94
+ // Store-empty must NOT carry an mcp retry: re-listing an absent store
95
+ // just loops. This is the anti-drift guard for the install rung.
96
+ expect(recovery.mcp).toBeUndefined();
97
+ } else if (recovery.mcp !== undefined) {
98
+ // A filter-narrowing mcp pointer must name a tool the domain actually
99
+ // registers — catches renamed/dangling tool references.
100
+ expect(toolNames.has(recovery.mcp.tool)).toBe(true);
101
+ }
102
+ });
103
+
104
+ it("never points an install hint at @canonical/ds-global (not a real default)", () => {
105
+ for (const { error } of cases) {
106
+ expect(error.recovery?.cli ?? "").not.toContain("ds-global");
107
+ }
108
+ });
109
+ });
@@ -0,0 +1,248 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { PackageRef } from "../../refs/operations/parseRef.js";
3
+ import createBundledLoader, {
4
+ resetBundledLoaderCache,
5
+ } from "./bundledLoader.js";
6
+
7
+ const ref: PackageRef = { kind: "npm", pkg: "@canonical/design-system" };
8
+
9
+ /** Minimal stand-in for a Bun embedded-file blob. */
10
+ interface FakeBlob {
11
+ readonly name: string;
12
+ text(): Promise<string>;
13
+ }
14
+
15
+ const blob = (name: string, content: string): FakeBlob => ({
16
+ name,
17
+ text: async () => content,
18
+ });
19
+
20
+ const corruptBlob = (name: string): FakeBlob => ({
21
+ name,
22
+ text: async () => {
23
+ throw new Error("embedded blob unreadable");
24
+ },
25
+ });
26
+
27
+ const TTL = "@prefix ds: <https://example.test/ds#> .";
28
+
29
+ function stubEmbeddedFiles(blobs: readonly FakeBlob[]): void {
30
+ (globalThis as { Bun?: unknown }).Bun = { embeddedFiles: blobs };
31
+ }
32
+
33
+ const hadBun = typeof (globalThis as { Bun?: unknown }).Bun !== "undefined";
34
+ const originalBun = (globalThis as { Bun?: unknown }).Bun;
35
+
36
+ let stderrSpy: ReturnType<typeof vi.spyOn>;
37
+
38
+ beforeEach(() => {
39
+ resetBundledLoaderCache();
40
+ stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
41
+ });
42
+
43
+ afterEach(() => {
44
+ resetBundledLoaderCache();
45
+ if (hadBun) {
46
+ (globalThis as { Bun?: unknown }).Bun = originalBun;
47
+ } else {
48
+ delete (globalThis as { Bun?: unknown }).Bun;
49
+ }
50
+ stderrSpy.mockRestore();
51
+ });
52
+
53
+ const stderrText = (): string =>
54
+ stderrSpy.mock.calls.map((call) => String(call[0])).join("");
55
+
56
+ describe("createBundledLoader", () => {
57
+ it("returns undefined outside a compiled Bun binary", async () => {
58
+ if (!hadBun) {
59
+ delete (globalThis as { Bun?: unknown }).Bun;
60
+ const resolved = await createBundledLoader().resolve(ref);
61
+ expect(resolved).toBeUndefined();
62
+ }
63
+ });
64
+
65
+ it("bundles all embedded TTL and reads the manifest version", async () => {
66
+ stubEmbeddedFiles([
67
+ blob("button-a1b2c3d4.ttl", TTL),
68
+ blob("package-a1b2c3d4.json", JSON.stringify({ version: "1.2.3" })),
69
+ ]);
70
+
71
+ const resolved = await createBundledLoader().resolve(ref);
72
+
73
+ expect(resolved).toBeDefined();
74
+ expect(resolved?.source).toBe("bundled");
75
+ expect(resolved?.version).toBe("1.2.3");
76
+ expect(resolved?.graphs).toEqual([
77
+ {
78
+ path: "(bundled)/button-a1b2c3d4.ttl",
79
+ content: TTL,
80
+ format: "turtle",
81
+ },
82
+ ]);
83
+ });
84
+
85
+ it("accepts a plain package.json basename", async () => {
86
+ stubEmbeddedFiles([
87
+ blob("button.ttl", TTL),
88
+ blob("package.json", JSON.stringify({ version: "2.0.0" })),
89
+ ]);
90
+
91
+ const resolved = await createBundledLoader().resolve(ref);
92
+ expect(resolved?.version).toBe("2.0.0");
93
+ });
94
+
95
+ it("warns and skips a corrupt TTL blob instead of silently dropping it", async () => {
96
+ stubEmbeddedFiles([
97
+ blob("good-a1b2c3d4.ttl", TTL),
98
+ corruptBlob("bad-a1b2c3d4.ttl"),
99
+ ]);
100
+
101
+ const resolved = await createBundledLoader().resolve(ref);
102
+
103
+ expect(resolved?.graphs).toHaveLength(1);
104
+ expect(stderrText()).toContain("bad-a1b2c3d4.ttl");
105
+ expect(stderrText()).toContain("embedded blob unreadable");
106
+ });
107
+
108
+ it("serves embedded story JSON as story-file entries", async () => {
109
+ stubEmbeddedFiles([
110
+ blob("button-a1b2c3d4.ttl", TTL),
111
+ blob("package-a1b2c3d4.json", JSON.stringify({ version: "1.2.3" })),
112
+ blob("colors-fyskc7pf.json", JSON.stringify({ noun: "color" })),
113
+ ]);
114
+
115
+ const resolved = await createBundledLoader().resolve(ref);
116
+
117
+ expect(resolved?.stories).toEqual([
118
+ {
119
+ path: "(bundled)/colors-fyskc7pf.json",
120
+ definition: { noun: "color" },
121
+ },
122
+ ]);
123
+ expect(resolved?.version).toBe("1.2.3");
124
+ });
125
+
126
+ it("does not serve package manifests as stories", async () => {
127
+ stubEmbeddedFiles([
128
+ blob("button.ttl", TTL),
129
+ blob("package.json", JSON.stringify({ version: "2.0.0" })),
130
+ blob("package-deadbeef.json", JSON.stringify({ version: "3.0.0" })),
131
+ ]);
132
+
133
+ const resolved = await createBundledLoader().resolve(ref);
134
+
135
+ expect(resolved?.stories).toEqual([]);
136
+ });
137
+
138
+ it("warns and skips a story blob with invalid JSON instead of crashing", async () => {
139
+ stubEmbeddedFiles([
140
+ blob("button.ttl", TTL),
141
+ blob("good-a1b2c3d4.json", JSON.stringify({ noun: "color" })),
142
+ blob("broken-a1b2c3d4.json", "{ not json"),
143
+ ]);
144
+
145
+ const resolved = await createBundledLoader().resolve(ref);
146
+
147
+ expect(resolved?.stories).toHaveLength(1);
148
+ expect(resolved?.stories.at(0)?.definition).toEqual({ noun: "color" });
149
+ expect(stderrText()).toContain("broken-a1b2c3d4.json");
150
+ });
151
+
152
+ it("warns and skips an unreadable story blob", async () => {
153
+ stubEmbeddedFiles([
154
+ blob("button.ttl", TTL),
155
+ corruptBlob("story-a1b2c3d4.json"),
156
+ ]);
157
+
158
+ const resolved = await createBundledLoader().resolve(ref);
159
+
160
+ expect(resolved?.stories).toEqual([]);
161
+ expect(stderrText()).toContain("story-a1b2c3d4.json");
162
+ expect(stderrText()).toContain("embedded blob unreadable");
163
+ });
164
+
165
+ it("resolves a stories-only bundle with no TTL graphs", async () => {
166
+ stubEmbeddedFiles([
167
+ blob("colors-fyskc7pf.json", JSON.stringify({ noun: "color" })),
168
+ ]);
169
+
170
+ const resolved = await createBundledLoader().resolve(ref);
171
+
172
+ expect(resolved).toBeDefined();
173
+ expect(resolved?.graphs).toEqual([]);
174
+ expect(resolved?.stories).toHaveLength(1);
175
+ });
176
+
177
+ it("returns undefined when no TTL or story content is embedded", async () => {
178
+ stubEmbeddedFiles([
179
+ blob("template-a1b2c3d4.ejs", "<%= name %>"),
180
+ blob("skill-a1b2c3d4.md", "# Skill"),
181
+ ]);
182
+
183
+ const resolved = await createBundledLoader().resolve(ref);
184
+
185
+ expect(resolved).toBeUndefined();
186
+ });
187
+
188
+ it("warns on a corrupt package manifest and falls back to 0.0.0", async () => {
189
+ stubEmbeddedFiles([
190
+ blob("button.ttl", TTL),
191
+ blob("package-deadbeef.json", "{ not json"),
192
+ ]);
193
+
194
+ const resolved = await createBundledLoader().resolve(ref);
195
+
196
+ expect(resolved?.version).toBe("0.0.0");
197
+ expect(stderrText()).toContain("package-deadbeef.json");
198
+ });
199
+
200
+ it("rejects JSON blobs whose basename is not a package manifest", async () => {
201
+ stubEmbeddedFiles([
202
+ blob("button.ttl", TTL),
203
+ blob("mypackage.json", JSON.stringify({ version: "9.9.9" })),
204
+ blob("package-lock.json", JSON.stringify({ version: "8.8.8" })),
205
+ blob("packages.json", JSON.stringify({ version: "7.7.7" })),
206
+ ]);
207
+
208
+ const resolved = await createBundledLoader().resolve(ref);
209
+ expect(resolved?.version).toBe("0.0.0");
210
+ });
211
+
212
+ it("warns on an invalid semver version and keeps looking", async () => {
213
+ stubEmbeddedFiles([
214
+ blob("button.ttl", TTL),
215
+ blob("package-aaaaaaaa.json", JSON.stringify({ version: "not-semver" })),
216
+ blob("package-bbbbbbbb.json", JSON.stringify({ version: "3.4.5" })),
217
+ ]);
218
+
219
+ const resolved = await createBundledLoader().resolve(ref);
220
+
221
+ expect(resolved?.version).toBe("3.4.5");
222
+ expect(stderrText()).toContain('invalid semver "not-semver"');
223
+ });
224
+
225
+ it("caches the resolved package until the cache is reset", async () => {
226
+ stubEmbeddedFiles([
227
+ blob("button.ttl", TTL),
228
+ blob("package.json", JSON.stringify({ version: "1.0.0" })),
229
+ ]);
230
+
231
+ const first = await createBundledLoader().resolve(ref);
232
+ expect(first?.version).toBe("1.0.0");
233
+
234
+ // New fixture without reset — cached result still returned.
235
+ stubEmbeddedFiles([
236
+ blob("other.ttl", TTL),
237
+ blob("package.json", JSON.stringify({ version: "5.0.0" })),
238
+ ]);
239
+ const second = await createBundledLoader().resolve(ref);
240
+ expect(second).toBe(first);
241
+
242
+ // After reset the loader re-reads the embedded files.
243
+ resetBundledLoaderCache();
244
+ const third = await createBundledLoader().resolve(ref);
245
+ expect(third?.version).toBe("5.0.0");
246
+ expect(third).not.toBe(first);
247
+ });
248
+ });
@@ -0,0 +1,206 @@
1
+ /**
2
+ * Bundled package loader for compiled binaries.
3
+ *
4
+ * Resolves semantic content from `Bun.embeddedFiles` — the TTL graphs
5
+ * and story-pack JSON embedded into the compiled binary via the build
6
+ * command. Skills are not embedded; this loader reports `skills: []`.
7
+ *
8
+ * Blob names are hashed by bun (e.g. `button-a1b2c3d4.ttl`), losing
9
+ * directory and package association. The loader therefore returns a
10
+ * single SemanticPackage containing ALL embedded TTL and story content,
11
+ * and the same cached package is returned for every ref this loader
12
+ * resolves.
13
+ * Note that repeated loads are NOT fully deduplicated by the store:
14
+ * Oxigraph applies set semantics to ground triples, but blank nodes are
15
+ * re-minted on every parse, so content loaded both here and by a
16
+ * higher-precedence loader can produce duplicated blank-node subgraphs.
17
+ * The orchestrator's first-loader-wins precedence (local > git >
18
+ * bundled) is what keeps individual packages from double-loading.
19
+ *
20
+ * @note Impure — reads embedded blobs.
21
+ */
22
+
23
+ import { basename } from "node:path";
24
+ import type { PackageRef } from "../../refs/operations/parseRef.js";
25
+ import type {
26
+ GraphContent,
27
+ PackageLoader,
28
+ SemanticPackage,
29
+ StoryFileEntry,
30
+ } from "../semanticPackage.js";
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // State
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /** Cached result — embedded content is immutable, no need to re-read. */
37
+ let cached: SemanticPackage | undefined;
38
+
39
+ /**
40
+ * Reset the module-level cache. Test-only: lets suites exercise the loader
41
+ * against different `Bun.embeddedFiles` fixtures without module isolation.
42
+ */
43
+ export function resetBundledLoaderCache(): void {
44
+ cached = undefined;
45
+ }
46
+
47
+ // ---------------------------------------------------------------------------
48
+ // Validation
49
+ // ---------------------------------------------------------------------------
50
+
51
+ /**
52
+ * Embedded package manifest basename: exactly `package.json`, or
53
+ * `package-<hash>.json` where `<hash>` is bun's content hash suffix.
54
+ */
55
+ const PACKAGE_MANIFEST_PATTERN = /^package(?:-[0-9a-f]{6,64})?\.json$/i;
56
+
57
+ /**
58
+ * Semver 2.0.0 validation (semver.org's suggested pattern).
59
+ */
60
+ const SEMVER_PATTERN =
61
+ /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$/;
62
+
63
+ /**
64
+ * Whether a blob name is an embedded package manifest — strict basename
65
+ * match, so unrelated JSON blobs (e.g. `mypackage.json`) are not misread.
66
+ */
67
+ function isPackageManifest(name: string): boolean {
68
+ return PACKAGE_MANIFEST_PATTERN.test(basename(name));
69
+ }
70
+
71
+ /**
72
+ * Emit a loader warning on stderr (safe for both CLI and MCP stdio use).
73
+ */
74
+ function warn(message: string): void {
75
+ process.stderr.write(`Warning: bundled loader: ${message}\n`);
76
+ }
77
+
78
+ // ---------------------------------------------------------------------------
79
+ // Public API
80
+ // ---------------------------------------------------------------------------
81
+
82
+ export default function createBundledLoader(): PackageLoader {
83
+ return {
84
+ name: "bundled",
85
+ async resolve(_ref: PackageRef): Promise<SemanticPackage | undefined> {
86
+ if (cached) return cached;
87
+
88
+ if (typeof globalThis.Bun === "undefined") return undefined;
89
+ // Bun.embeddedFiles blobs have a .name property at runtime
90
+ const blobs = (globalThis.Bun as { embeddedFiles?: readonly Blob[] })
91
+ .embeddedFiles as ReadonlyArray<Blob & { name: string }> | undefined;
92
+ if (!blobs?.length) return undefined;
93
+
94
+ const graphs = await readEmbeddedGraphs(blobs);
95
+ const stories = await readEmbeddedStories(blobs);
96
+ if (graphs.length === 0 && stories.length === 0) return undefined;
97
+
98
+ const version = await readEmbeddedVersion(blobs);
99
+
100
+ cached = {
101
+ name: "(bundled)",
102
+ version,
103
+ source: "bundled",
104
+ graphs,
105
+ skills: [],
106
+ stories,
107
+ };
108
+
109
+ return cached;
110
+ },
111
+ };
112
+ }
113
+
114
+ // ---------------------------------------------------------------------------
115
+ // Graph reading
116
+ // ---------------------------------------------------------------------------
117
+
118
+ async function readEmbeddedGraphs(
119
+ blobs: ReadonlyArray<Blob & { name: string }>,
120
+ ): Promise<GraphContent[]> {
121
+ const graphs: GraphContent[] = [];
122
+
123
+ for (const blob of blobs) {
124
+ if (!blob.name.endsWith(".ttl")) continue;
125
+
126
+ try {
127
+ const content = await blob.text();
128
+ graphs.push({
129
+ path: `(bundled)/${blob.name}`,
130
+ content,
131
+ format: "turtle",
132
+ });
133
+ } catch (error) {
134
+ const reason = error instanceof Error ? error.message : String(error);
135
+ warn(`skipping unreadable embedded graph "${blob.name}": ${reason}`);
136
+ }
137
+ }
138
+
139
+ return graphs;
140
+ }
141
+
142
+ // ---------------------------------------------------------------------------
143
+ // Story reading
144
+ // ---------------------------------------------------------------------------
145
+
146
+ /**
147
+ * Read story-pack definitions from embedded `.json` blobs.
148
+ *
149
+ * The build's story-assets plugin makes `stories/*.json` the only JSON
150
+ * embedded as file assets (see storyAssetsPlugin.ts), so every `.json`
151
+ * blob not named like a package manifest is a story file. Mirrors the
152
+ * local loader: a blob that cannot be read or parsed warns on stderr
153
+ * and is skipped — one bad file cannot break boot. Definitions are
154
+ * shape-validated later by the story-pack compiler.
155
+ */
156
+ async function readEmbeddedStories(
157
+ blobs: ReadonlyArray<Blob & { name: string }>,
158
+ ): Promise<StoryFileEntry[]> {
159
+ const stories: StoryFileEntry[] = [];
160
+
161
+ for (const blob of blobs) {
162
+ if (!blob.name.endsWith(".json") || isPackageManifest(blob.name)) continue;
163
+
164
+ try {
165
+ stories.push({
166
+ path: `(bundled)/${blob.name}`,
167
+ definition: JSON.parse(await blob.text()),
168
+ });
169
+ } catch (error) {
170
+ const reason = error instanceof Error ? error.message : String(error);
171
+ warn(`skipping invalid embedded story "${blob.name}": ${reason}`);
172
+ }
173
+ }
174
+
175
+ return stories;
176
+ }
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Version reading
180
+ // ---------------------------------------------------------------------------
181
+
182
+ async function readEmbeddedVersion(
183
+ blobs: ReadonlyArray<Blob & { name: string }>,
184
+ ): Promise<string> {
185
+ for (const blob of blobs) {
186
+ if (!isPackageManifest(blob.name)) continue;
187
+
188
+ try {
189
+ const text = await blob.text();
190
+ const parsed = JSON.parse(text) as { version?: unknown };
191
+ if (typeof parsed.version !== "string") continue;
192
+ if (!SEMVER_PATTERN.test(parsed.version)) {
193
+ warn(
194
+ `ignoring invalid semver "${parsed.version}" in embedded manifest "${blob.name}"`,
195
+ );
196
+ continue;
197
+ }
198
+ return parsed.version;
199
+ } catch (error) {
200
+ const reason = error instanceof Error ? error.message : String(error);
201
+ warn(`skipping corrupt embedded manifest "${blob.name}": ${reason}`);
202
+ }
203
+ }
204
+
205
+ return "0.0.0";
206
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Git cache package loader.
3
+ *
4
+ * Resolves semantic packages from the git ref cache at
5
+ * `~/.cache/pragma/refs/<pkg>/<ref>/`, populated by `pragma update-refs`.
6
+ *
7
+ * Only handles `git` kind refs. File and npm refs are skipped.
8
+ *
9
+ * @note Impure — reads filesystem.
10
+ */
11
+
12
+ import { existsSync } from "node:fs";
13
+ import type { PackageRef } from "../../refs/operations/parseRef.js";
14
+ import { gitCacheDir } from "../../refs/operations/paths.js";
15
+ import type { PackageLoader, SemanticPackage } from "../semanticPackage.js";
16
+ import readPackageDir from "./readPackageDir.js";
17
+
18
+ export default function createGitLoader(): PackageLoader {
19
+ return {
20
+ name: "git",
21
+ resolve(ref: PackageRef): SemanticPackage | undefined {
22
+ if (ref.kind !== "git") return undefined;
23
+
24
+ const dir = gitCacheDir(ref.pkg, ref.ref);
25
+ if (!existsSync(dir)) return undefined;
26
+
27
+ const { version, graphs, skills, stories } = readPackageDir(dir);
28
+ return { name: ref.pkg, version, source: "git", graphs, skills, stories };
29
+ },
30
+ };
31
+ }
@@ -0,0 +1,9 @@
1
+ /** @module Semantic package loaders — local, git, and bundled resolution. */
2
+
3
+ export {
4
+ default as createBundledLoader,
5
+ resetBundledLoaderCache,
6
+ } from "./bundledLoader.js";
7
+ export { default as createGitLoader } from "./gitLoader.js";
8
+ export { default as createLocalLoader } from "./localLoader.js";
9
+ export { default as readPackageDir } from "./readPackageDir.js";
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Local package loader.
3
+ *
4
+ * Resolves semantic packages from the local filesystem:
5
+ * - `file://` refs → direct path check
6
+ * - All ref kinds → upward `node_modules` walk from this module's location
7
+ *
8
+ * The node_modules walk avoids `require.resolve('pkg/package.json')` which
9
+ * breaks for packages with strict `exports` fields (e.g. @canonical/anatomy-dsl).
10
+ *
11
+ * @note Impure — reads filesystem.
12
+ */
13
+
14
+ import { existsSync, realpathSync } from "node:fs";
15
+ import { dirname, join } from "node:path";
16
+ import type { PackageRef } from "../../refs/operations/parseRef.js";
17
+ import type { PackageLoader, SemanticPackage } from "../semanticPackage.js";
18
+ import readPackageDir from "./readPackageDir.js";
19
+
20
+ // ---------------------------------------------------------------------------
21
+ // Public API
22
+ // ---------------------------------------------------------------------------
23
+
24
+ export default function createLocalLoader(): PackageLoader {
25
+ return {
26
+ name: "local",
27
+ resolve(ref: PackageRef): SemanticPackage | undefined {
28
+ // file:// refs → direct path check first
29
+ if (ref.kind === "file") {
30
+ if (!existsSync(ref.path)) return undefined;
31
+ const { version, graphs, skills, stories } = readPackageDir(ref.path);
32
+ return {
33
+ name: ref.pkg,
34
+ version,
35
+ source: "local",
36
+ graphs,
37
+ skills,
38
+ stories,
39
+ };
40
+ }
41
+
42
+ // All ref kinds → try node_modules walk
43
+ const dir = findPackageDir(ref.pkg);
44
+ if (!dir) return undefined;
45
+
46
+ const { version, graphs, skills, stories } = readPackageDir(dir);
47
+ return {
48
+ name: ref.pkg,
49
+ version,
50
+ source: "local",
51
+ graphs,
52
+ skills,
53
+ stories,
54
+ };
55
+ },
56
+ };
57
+ }
58
+
59
+ // ---------------------------------------------------------------------------
60
+ // node_modules walk
61
+ // ---------------------------------------------------------------------------
62
+
63
+ /**
64
+ * Find a package directory by walking up from this module's location.
65
+ *
66
+ * Checks each ancestor's `node_modules/<pkg>` directory. Handles both
67
+ * scoped (`@scope/name`) and unscoped packages. Follows symlinks via
68
+ * `realpathSync` so hoisted workspace layouts resolve correctly.
69
+ */
70
+ function findPackageDir(pkg: string): string | undefined {
71
+ const segments = pkg.split("/");
72
+ let current = import.meta.dirname;
73
+
74
+ while (current !== dirname(current)) {
75
+ const candidate = join(current, "node_modules", ...segments);
76
+ if (existsSync(candidate)) {
77
+ try {
78
+ return realpathSync(candidate);
79
+ } catch {
80
+ return candidate;
81
+ }
82
+ }
83
+ current = dirname(current);
84
+ }
85
+
86
+ return undefined;
87
+ }