@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,751 @@
1
+ import type { Store } from "@canonical/ke";
2
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
+ import { PragmaError } from "#error";
4
+ import {
5
+ createTestStore,
6
+ RECIPE_PREFIXES,
7
+ RECIPE_STORY,
8
+ RECIPE_TTL,
9
+ } from "#testing";
10
+ import { DEFAULT_PREFIX_MAP } from "../../prefixes.js";
11
+ import type { PragmaRuntime } from "../../types/index.js";
12
+ import compilePackStories from "./compilePackStories.js";
13
+ import type { StoryPackDefinition } from "./types.js";
14
+
15
+ const PREFIXES = { ...DEFAULT_PREFIX_MAP, ...RECIPE_PREFIXES };
16
+
17
+ let store: Store;
18
+ let cleanup: () => void;
19
+ let rt: PragmaRuntime;
20
+
21
+ beforeAll(async () => {
22
+ const result = await createTestStore({
23
+ ttl: RECIPE_TTL,
24
+ prefixes: RECIPE_PREFIXES,
25
+ });
26
+ store = result.store;
27
+ cleanup = result.cleanup;
28
+ rt = { store } as PragmaRuntime;
29
+ });
30
+
31
+ afterAll(() => cleanup());
32
+
33
+ describe("compilePackStories — list", () => {
34
+ it("resolves the pack's preferred query into rows", async () => {
35
+ const { list } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
36
+ const rows = await list.resolve(rt, {});
37
+ expect(rows.map((row) => row.name)).toEqual(["Gazpacho", "Pancakes"]);
38
+ expect(list.toEnvelope(rows)).toEqual({
39
+ data: rows,
40
+ meta: { count: 2 },
41
+ });
42
+ });
43
+
44
+ it("renders the generic formatters with compacted prefixes", async () => {
45
+ const { list } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
46
+ const rows = await list.resolve(rt, {});
47
+ const plain = list.formatters.plain(rows);
48
+ expect(plain).toContain("Pancakes");
49
+ expect(plain).toContain("breakfast");
50
+ const llm = list.formatters.llm(rows);
51
+ expect(llm).toContain("## Recipe (2)");
52
+ expect(llm).toContain("`ex:gazpacho`");
53
+ expect(JSON.parse(list.formatters.json(rows))).toHaveLength(2);
54
+ });
55
+ });
56
+
57
+ describe("compilePackStories — list filters", () => {
58
+ it("projects declared filters as enum story parameters", () => {
59
+ const { list } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
60
+ const param = list.params.at(0);
61
+ expect(param?.name).toBe("category");
62
+ expect(param?.type).toBe("string");
63
+ expect(param?.enum).toEqual(["breakfast", "soup"]);
64
+ });
65
+
66
+ it("filters resolved rows by the provided value", async () => {
67
+ const { list } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
68
+ const rows = await list.resolve(rt, { category: "soup" });
69
+ expect(rows.map((row) => row.name)).toEqual(["Gazpacho"]);
70
+ expect(list.toEnvelope(rows).meta).toEqual({ count: 1 });
71
+ });
72
+
73
+ it("canonicalizes case-insensitive input through resolve", async () => {
74
+ const { list } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
75
+ const rows = await list.resolve(rt, { category: "SOUP" });
76
+ expect(rows.map((row) => row.name)).toEqual(["Gazpacho"]);
77
+ });
78
+
79
+ it("rejects a value outside the declared set with INVALID_INPUT", async () => {
80
+ const { list } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
81
+ await expect(
82
+ list.resolve(rt, { category: "dinner" }),
83
+ ).rejects.toMatchObject({ code: "INVALID_INPUT" });
84
+ });
85
+
86
+ it("mentions the first filter in the examples", () => {
87
+ const { list } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
88
+ expect(list.examples).toContain("pragma recipe list --category breakfast");
89
+ });
90
+ });
91
+
92
+ // Pack v1: value-free filters (data-driven value sets) and free-text search.
93
+ describe("compilePackStories — value-free filters and search", () => {
94
+ /** RECIPE_STORY with a value-free category filter and a name search. */
95
+ const SEARCHABLE_STORY: StoryPackDefinition = {
96
+ ...RECIPE_STORY,
97
+ list: {
98
+ ...RECIPE_STORY.list,
99
+ filters: [{ param: "category", variable: "category" }],
100
+ search: { variables: ["name"] },
101
+ },
102
+ };
103
+
104
+ it("projects a value-free filter as a plain string parameter", () => {
105
+ const { list } = compilePackStories(SEARCHABLE_STORY, "test", PREFIXES);
106
+ const param = list.params.find((p) => p.name === "category");
107
+ expect(param?.type).toBe("string");
108
+ expect(param?.enum).toBeUndefined();
109
+ });
110
+
111
+ it("filters rows by case-insensitive equality without a declared set", async () => {
112
+ const { list } = compilePackStories(SEARCHABLE_STORY, "test", PREFIXES);
113
+ const rows = await list.resolve(rt, { category: "SOUP" });
114
+ expect(rows.map((row) => row.name)).toEqual(["Gazpacho"]);
115
+ });
116
+
117
+ it("returns no rows (not an error) for an unknown filter value", async () => {
118
+ const { list } = compilePackStories(SEARCHABLE_STORY, "test", PREFIXES);
119
+ await expect(list.resolve(rt, { category: "dinner" })).resolves.toEqual([]);
120
+ });
121
+
122
+ it("projects a `search` string parameter", () => {
123
+ const { list } = compilePackStories(SEARCHABLE_STORY, "test", PREFIXES);
124
+ const param = list.params.find((p) => p.name === "search");
125
+ expect(param?.type).toBe("string");
126
+ expect(param?.description).toContain("name");
127
+ });
128
+
129
+ it("keeps rows whose searched variables contain the term", async () => {
130
+ const { list } = compilePackStories(SEARCHABLE_STORY, "test", PREFIXES);
131
+ const rows = await list.resolve(rt, { search: "gaz" });
132
+ expect(rows.map((row) => row.name)).toEqual(["Gazpacho"]);
133
+ });
134
+
135
+ it("applies search after filters, conjunctively", async () => {
136
+ const { list } = compilePackStories(SEARCHABLE_STORY, "test", PREFIXES);
137
+ const rows = await list.resolve(rt, {
138
+ category: "breakfast",
139
+ search: "gaz",
140
+ });
141
+ expect(rows).toEqual([]);
142
+ });
143
+
144
+ it("skips the generated filter example when no filter declares values", () => {
145
+ const { list } = compilePackStories(SEARCHABLE_STORY, "test", PREFIXES);
146
+ expect(
147
+ list.examples.some((example) => example.includes("--category")),
148
+ ).toBe(false);
149
+ });
150
+ });
151
+
152
+ // Pack v1: extra list-shaped verbs compiled through the list machinery.
153
+ describe("compilePackStories — extra verbs", () => {
154
+ const STORY_WITH_VERB: StoryPackDefinition = {
155
+ ...RECIPE_STORY,
156
+ verbs: [
157
+ {
158
+ verb: "categories",
159
+ description: "List recipe categories",
160
+ query:
161
+ "SELECT ?name (COUNT(?uri) AS ?count) WHERE { ?uri a ex:Recipe ; ex:category ?name } GROUP BY ?name ORDER BY ?name",
162
+ columns: [{ field: "name" }, { field: "count" }],
163
+ },
164
+ ],
165
+ };
166
+
167
+ it("compiles each declared verb into a read story on the noun", async () => {
168
+ const { verbs } = compilePackStories(STORY_WITH_VERB, "test", PREFIXES);
169
+ expect(verbs).toHaveLength(1);
170
+ const categories = verbs.at(0);
171
+ expect(categories?.noun).toBe("recipe");
172
+ expect(categories?.verb).toBe("categories");
173
+ expect(categories?.description).toBe("List recipe categories");
174
+ const rows = await categories?.resolve(rt, {});
175
+ expect(rows).toEqual([
176
+ { name: "breakfast", count: "1" },
177
+ { name: "soup", count: "1" },
178
+ ]);
179
+ expect(categories?.toEnvelope(rows ?? [])).toEqual({
180
+ data: rows,
181
+ meta: { count: 2 },
182
+ });
183
+ });
184
+
185
+ it("renders extra verbs through the generic list renderer", async () => {
186
+ const { verbs } = compilePackStories(STORY_WITH_VERB, "test", PREFIXES);
187
+ const categories = verbs.at(0);
188
+ const rows = (await categories?.resolve(rt, {})) ?? [];
189
+ const llm = categories?.formatters.llm(rows) ?? "";
190
+ expect(llm).toContain("## Recipe categories (2)");
191
+ expect(llm).toContain("breakfast");
192
+ });
193
+
194
+ it("compiles no extra verbs when none are declared", () => {
195
+ const { verbs } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
196
+ expect(verbs).toEqual([]);
197
+ });
198
+ });
199
+
200
+ // Pack v1: the generic sample verb.
201
+ describe("compilePackStories — sample", () => {
202
+ const STORY_WITH_SAMPLE: StoryPackDefinition = {
203
+ ...RECIPE_STORY,
204
+ lookup: {
205
+ // biome-ignore lint/style/noNonNullAssertion: fixture always declares lookup
206
+ ...RECIPE_STORY.lookup!,
207
+ sample: true,
208
+ },
209
+ };
210
+
211
+ it("draws N entities resolved through the lookup path", async () => {
212
+ const { sample } = compilePackStories(STORY_WITH_SAMPLE, "test", PREFIXES);
213
+ expect(sample?.noun).toBe("recipe");
214
+ expect(sample?.verb).toBe("sample");
215
+ const data = await sample?.resolve(rt, { count: "2" });
216
+ expect(data?.totalCount).toBe(2);
217
+ expect(data?.samples.map((entity) => entity.name).sort()).toEqual([
218
+ "Gazpacho",
219
+ "Pancakes",
220
+ ]);
221
+ // Full lookup shape, not just names.
222
+ expect(
223
+ data?.samples.every((entity) => typeof entity.instructions === "string"),
224
+ ).toBe(true);
225
+ });
226
+
227
+ it("clamps the requested count and rejects non-integers", async () => {
228
+ const { sample } = compilePackStories(STORY_WITH_SAMPLE, "test", PREFIXES);
229
+ const data = await sample?.resolve(rt, { count: "99" });
230
+ expect(data?.samples.length).toBeLessThanOrEqual(5);
231
+ await expect(sample?.resolve(rt, { count: "abc" })).rejects.toMatchObject({
232
+ code: "INVALID_INPUT",
233
+ });
234
+ });
235
+
236
+ it("envelopes samples with totalCount and nextSteps", async () => {
237
+ const { sample } = compilePackStories(STORY_WITH_SAMPLE, "test", PREFIXES);
238
+ const data = await sample?.resolve(rt, {});
239
+ if (!data || !sample) throw new Error("expected sample data");
240
+ const envelope = sample.toEnvelope(data);
241
+ expect(envelope.meta).toEqual({ count: data.samples.length });
242
+ const payload = envelope.data as {
243
+ totalCount: number;
244
+ nextSteps: string[];
245
+ };
246
+ expect(payload.totalCount).toBe(2);
247
+ expect(payload.nextSteps.join(" ")).toContain("recipe_lookup");
248
+ });
249
+
250
+ it("renders samples via the lookup formatters under a count header", async () => {
251
+ const { sample } = compilePackStories(STORY_WITH_SAMPLE, "test", PREFIXES);
252
+ const data = await sample?.resolve(rt, { count: "2" });
253
+ if (!data || !sample) throw new Error("expected sample data");
254
+ const llm = sample.formatters.llm(sample.toOutput(data, {}));
255
+ expect(llm).toContain("Showing 2 of 2 recipe entries");
256
+ expect(llm).toContain("## ");
257
+ });
258
+
259
+ it("honors a declared default count", async () => {
260
+ const configured: StoryPackDefinition = {
261
+ ...RECIPE_STORY,
262
+ lookup: {
263
+ // biome-ignore lint/style/noNonNullAssertion: fixture always declares lookup
264
+ ...RECIPE_STORY.lookup!,
265
+ sample: { count: 1 },
266
+ },
267
+ };
268
+ const { sample } = compilePackStories(configured, "test", PREFIXES);
269
+ const data = await sample?.resolve(rt, {});
270
+ expect(data?.samples).toHaveLength(1);
271
+ });
272
+
273
+ it("compiles no sample story when the capability is not declared", () => {
274
+ const { sample } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
275
+ expect(sample).toBeUndefined();
276
+ });
277
+ });
278
+
279
+ describe("compilePackStories — lookup", () => {
280
+ it("looks an entity up by name, case-insensitively", async () => {
281
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
282
+ const result = await lookup?.resolve(rt, ["pancakes"], {});
283
+ expect(result?.results).toHaveLength(1);
284
+ const entity = result?.results.at(0);
285
+ expect(entity?.name).toBe("Pancakes");
286
+ expect(entity?.category).toBe("breakfast");
287
+ expect(entity?.instructions).toBe("Mix, fry, flip.");
288
+ });
289
+
290
+ it("renders fields and sections through the generic lookup renderer", async () => {
291
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
292
+ const result = await lookup?.resolve(rt, ["Gazpacho"], {});
293
+ const entity = result?.results.at(0);
294
+ expect(entity).toBeDefined();
295
+ if (!entity || !lookup) throw new Error("expected entity");
296
+ const llm = lookup.formatters.llm(
297
+ lookup.toFmtInput(entity, {
298
+ surface: "cli",
299
+ detailed: false,
300
+ params: {},
301
+ }),
302
+ );
303
+ expect(llm).toContain("Gazpacho");
304
+ expect(llm).toContain("soup");
305
+ expect(llm).toContain("Blend everything cold.");
306
+ });
307
+
308
+ it("collects not-found errors with ranked suggestions", async () => {
309
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
310
+ const result = await lookup?.resolve(rt, ["Pancake"], {});
311
+ expect(result?.results).toHaveLength(0);
312
+ const error = result?.errors.at(0);
313
+ expect(error?.code).toBe("ENTITY_NOT_FOUND");
314
+ expect(error?.suggestions).toContain("Pancakes");
315
+ });
316
+
317
+ it("rejects empty names with a recovery hint", () => {
318
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
319
+ expect(() => {
320
+ const error = lookup?.emptyNamesError?.();
321
+ if (error) throw error;
322
+ }).toThrow(PragmaError);
323
+ });
324
+
325
+ it("completes names from the store", async () => {
326
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
327
+ const ctx = {
328
+ cwd: "/tmp",
329
+ globalFlags: { llm: false, format: "text" as const, verbose: false },
330
+ store,
331
+ };
332
+ await expect(lookup?.complete?.("pan", ctx)).resolves.toEqual(["Pancakes"]);
333
+ });
334
+ });
335
+
336
+ // Pack v1: IRI/prefixed-name addressing and glob expansion on lookups.
337
+ describe("compilePackStories — lookup by IRI and glob", () => {
338
+ it("resolves an absolute IRI to the exact entity", async () => {
339
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
340
+ const result = await lookup?.resolve(
341
+ rt,
342
+ ["http://example.org/recipes/pancakes"],
343
+ {},
344
+ );
345
+ expect(result?.errors).toEqual([]);
346
+ expect(result?.results.at(0)?.name).toBe("Pancakes");
347
+ });
348
+
349
+ it("resolves a prefixed name through the merged prefix map", async () => {
350
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
351
+ const result = await lookup?.resolve(rt, ["ex:gazpacho"], {});
352
+ expect(result?.errors).toEqual([]);
353
+ expect(result?.results.at(0)?.name).toBe("Gazpacho");
354
+ });
355
+
356
+ it("collects an INVALID_INPUT entry for an unknown prefix", async () => {
357
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
358
+ const result = await lookup?.resolve(rt, ["zz:nonexistent"], {});
359
+ expect(result?.results).toHaveLength(0);
360
+ expect(result?.errors.at(0)?.code).toBe("INVALID_INPUT");
361
+ });
362
+
363
+ it("collects ENTITY_NOT_FOUND for an IRI naming no entity", async () => {
364
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
365
+ const result = await lookup?.resolve(rt, ["ex:missing"], {});
366
+ expect(result?.results).toHaveLength(0);
367
+ expect(result?.errors.at(0)?.code).toBe("ENTITY_NOT_FOUND");
368
+ });
369
+
370
+ it("expands glob queries against the entity name list", async () => {
371
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
372
+ const result = await lookup?.resolve(rt, ["*a*"], {});
373
+ expect(result?.errors).toEqual([]);
374
+ expect(result?.results.map((entity) => entity.name).sort()).toEqual([
375
+ "Gazpacho",
376
+ "Pancakes",
377
+ ]);
378
+ });
379
+
380
+ it("reports EMPTY_RESULTS for a glob matching nothing", async () => {
381
+ const { lookup } = compilePackStories(RECIPE_STORY, "test", PREFIXES);
382
+ const result = await lookup?.resolve(rt, ["Zebra*", "Pancakes"], {});
383
+ expect(result?.results.at(0)?.name).toBe("Pancakes");
384
+ const error = result?.errors.at(0);
385
+ expect(error?.code).toBe("EMPTY_RESULTS");
386
+ expect(error?.query).toBe("Zebra*");
387
+ });
388
+ });
389
+
390
+ // ---------------------------------------------------------------------------
391
+ // Pack v1: multi-valued expand projections (nested SPARQL)
392
+ // ---------------------------------------------------------------------------
393
+
394
+ const RECIPE_TTL_WITH_INGREDIENTS = `${RECIPE_TTL}
395
+ ex:pancakes ex:ingredient [ ex:label "Flour" ; ex:amount "200g" ] .
396
+ ex:pancakes ex:ingredient [ ex:label "Milk" ; ex:amount "300ml" ] .
397
+ ex:gazpacho ex:ingredient [ ex:label "Tomato" ; ex:amount "1kg" ] .
398
+ `;
399
+
400
+ /** RECIPE_STORY with a multi-valued `ingredients` expand. */
401
+ const RECIPE_STORY_WITH_EXPAND: StoryPackDefinition = {
402
+ ...RECIPE_STORY,
403
+ lookup: {
404
+ // biome-ignore lint/style/noNonNullAssertion: fixture always declares lookup
405
+ ...RECIPE_STORY.lookup!,
406
+ expand: [
407
+ {
408
+ name: "ingredients",
409
+ heading: "Ingredients",
410
+ kind: "table",
411
+ relation: "ex:ingredient",
412
+ select: [
413
+ { name: "label", property: "ex:label" },
414
+ { name: "amount", property: "ex:amount" },
415
+ ],
416
+ },
417
+ ],
418
+ },
419
+ };
420
+
421
+ describe("compilePackStories — expand", () => {
422
+ let expandStore: Store;
423
+ let expandCleanup: () => void;
424
+ let expandRt: PragmaRuntime;
425
+
426
+ beforeAll(async () => {
427
+ const result = await createTestStore({
428
+ ttl: RECIPE_TTL_WITH_INGREDIENTS,
429
+ prefixes: RECIPE_PREFIXES,
430
+ });
431
+ expandStore = result.store;
432
+ expandCleanup = result.cleanup;
433
+ expandRt = { store: expandStore } as PragmaRuntime;
434
+ });
435
+
436
+ afterAll(() => expandCleanup());
437
+
438
+ it("attaches expanded child rows as an array on the entity", async () => {
439
+ const { lookup } = compilePackStories(
440
+ RECIPE_STORY_WITH_EXPAND,
441
+ "test",
442
+ PREFIXES,
443
+ );
444
+ const result = await lookup?.resolve(expandRt, ["Pancakes"], {});
445
+ const entity = result?.results.at(0) as
446
+ | { ingredients?: Array<Record<string, string>> }
447
+ | undefined;
448
+ expect(entity?.ingredients).toHaveLength(2);
449
+ const labels = entity?.ingredients?.map((i) => i.label).sort();
450
+ expect(labels).toEqual(["Flour", "Milk"]);
451
+ const flour = entity?.ingredients?.find((i) => i.label === "Flour");
452
+ expect(flour?.amount).toBe("200g");
453
+ });
454
+
455
+ it("renders the expand as a section through the generic renderer", async () => {
456
+ const { lookup } = compilePackStories(
457
+ RECIPE_STORY_WITH_EXPAND,
458
+ "test",
459
+ PREFIXES,
460
+ );
461
+ const result = await lookup?.resolve(expandRt, ["Pancakes"], {});
462
+ const entity = result?.results.at(0);
463
+ if (!entity || !lookup) throw new Error("expected entity");
464
+ const llm = lookup.formatters.llm(
465
+ lookup.toFmtInput(entity, { surface: "cli", detailed: true, params: {} }),
466
+ );
467
+ expect(llm).toContain("Ingredients");
468
+ expect(llm).toContain("Flour");
469
+ expect(llm).toContain("200g");
470
+ });
471
+
472
+ it("returns an empty array when the entity has no children", async () => {
473
+ const noExpand: StoryPackDefinition = {
474
+ ...RECIPE_STORY_WITH_EXPAND,
475
+ lookup: {
476
+ // biome-ignore lint/style/noNonNullAssertion: fixture always declares lookup
477
+ ...RECIPE_STORY_WITH_EXPAND.lookup!,
478
+ expand: [
479
+ {
480
+ name: "steps",
481
+ relation: "ex:step",
482
+ select: [{ name: "text", property: "ex:text" }],
483
+ },
484
+ ],
485
+ },
486
+ };
487
+ const { lookup } = compilePackStories(noExpand, "test", PREFIXES);
488
+ const result = await lookup?.resolve(expandRt, ["Gazpacho"], {});
489
+ const entity = result?.results.at(0) as { steps?: unknown[] } | undefined;
490
+ expect(entity?.steps).toEqual([]);
491
+ });
492
+
493
+ // -------------------------------------------------------------------------
494
+ // Disclosure capability: level-gated expands + derived --detail
495
+ // -------------------------------------------------------------------------
496
+
497
+ /** RECIPE_STORY_WITH_EXPAND with `ingredients` gated to a "detailed" level. */
498
+ const RECIPE_STORY_DISCLOSURE: StoryPackDefinition = {
499
+ ...RECIPE_STORY_WITH_EXPAND,
500
+ lookup: {
501
+ // biome-ignore lint/style/noNonNullAssertion: fixture always declares lookup
502
+ ...RECIPE_STORY_WITH_EXPAND.lookup!,
503
+ expand: [
504
+ {
505
+ name: "ingredients",
506
+ heading: "Ingredients",
507
+ kind: "table",
508
+ relation: "ex:ingredient",
509
+ select: [
510
+ { name: "label", property: "ex:label" },
511
+ { name: "amount", property: "ex:amount" },
512
+ ],
513
+ level: "detailed",
514
+ },
515
+ ],
516
+ disclosure: { levels: ["summary", "detailed"] },
517
+ },
518
+ };
519
+
520
+ it("derives a `detail` param enumerated from the declared levels", () => {
521
+ const { lookup } = compilePackStories(
522
+ RECIPE_STORY_DISCLOSURE,
523
+ "test",
524
+ PREFIXES,
525
+ );
526
+ const param = lookup?.params?.find((p) => p.name === "detail");
527
+ expect(param?.enum).toEqual(["summary", "detailed"]);
528
+ });
529
+
530
+ it("omits a level-gated expand at the base level (no fetch)", async () => {
531
+ const { lookup } = compilePackStories(
532
+ RECIPE_STORY_DISCLOSURE,
533
+ "test",
534
+ PREFIXES,
535
+ );
536
+ const result = await lookup?.resolve(expandRt, ["Pancakes"], {});
537
+ const entity = result?.results.at(0) as
538
+ | { ingredients?: unknown }
539
+ | undefined;
540
+ expect(entity?.ingredients).toBeUndefined();
541
+ });
542
+
543
+ it("includes a level-gated expand when --detail selects its level", async () => {
544
+ const { lookup } = compilePackStories(
545
+ RECIPE_STORY_DISCLOSURE,
546
+ "test",
547
+ PREFIXES,
548
+ );
549
+ const result = await lookup?.resolve(expandRt, ["Pancakes"], {
550
+ detail: "detailed",
551
+ });
552
+ const entity = result?.results.at(0) as
553
+ | { ingredients?: unknown[] }
554
+ | undefined;
555
+ expect(entity?.ingredients).toHaveLength(2);
556
+ });
557
+
558
+ it("honors the global config.detail default when no flag is passed", async () => {
559
+ const { lookup } = compilePackStories(
560
+ RECIPE_STORY_DISCLOSURE,
561
+ "test",
562
+ PREFIXES,
563
+ );
564
+ const configuredRt = {
565
+ store: expandStore,
566
+ config: { detail: "detailed" },
567
+ } as PragmaRuntime;
568
+ const result = await lookup?.resolve(configuredRt, ["Pancakes"], {});
569
+ const entity = result?.results.at(0) as
570
+ | { ingredients?: unknown[] }
571
+ | undefined;
572
+ expect(entity?.ingredients).toHaveLength(2);
573
+ });
574
+
575
+ it("lets an explicit --detail override the config default", async () => {
576
+ const { lookup } = compilePackStories(
577
+ RECIPE_STORY_DISCLOSURE,
578
+ "test",
579
+ PREFIXES,
580
+ );
581
+ const configuredRt = {
582
+ store: expandStore,
583
+ config: { detail: "detailed" },
584
+ } as PragmaRuntime;
585
+ const result = await lookup?.resolve(configuredRt, ["Pancakes"], {
586
+ detail: "summary",
587
+ });
588
+ const entity = result?.results.at(0) as
589
+ | { ingredients?: unknown }
590
+ | undefined;
591
+ expect(entity?.ingredients).toBeUndefined();
592
+ });
593
+
594
+ // ---------------------------------------------------------------------
595
+ // Legacy disclosure alias flags + the MCP full-data default
596
+ // ---------------------------------------------------------------------
597
+
598
+ /** Three levels so alias precedence (highest wins) is observable. */
599
+ const RECIPE_STORY_THREE_LEVELS: StoryPackDefinition = {
600
+ ...RECIPE_STORY_WITH_EXPAND,
601
+ lookup: {
602
+ // biome-ignore lint/style/noNonNullAssertion: fixture always declares lookup
603
+ ...RECIPE_STORY_WITH_EXPAND.lookup!,
604
+ expand: [
605
+ {
606
+ name: "ingredients",
607
+ relation: "ex:ingredient",
608
+ select: [{ name: "label", property: "ex:label" }],
609
+ level: "digest",
610
+ },
611
+ ],
612
+ disclosure: { levels: ["summary", "digest", "detailed"] },
613
+ },
614
+ };
615
+
616
+ it("derives a boolean alias flag per non-base level", () => {
617
+ const { lookup } = compilePackStories(
618
+ RECIPE_STORY_THREE_LEVELS,
619
+ "test",
620
+ PREFIXES,
621
+ );
622
+ const names = (lookup?.params ?? []).map((param) => param.name);
623
+ expect(names).toEqual(["detail", "digest", "detailed"]);
624
+ const digest = lookup?.params?.find((param) => param.name === "digest");
625
+ expect(digest?.type).toBe("boolean");
626
+ expect(digest?.default).toBe(false);
627
+ });
628
+
629
+ it("an alias flag implies its level", async () => {
630
+ const { lookup } = compilePackStories(
631
+ RECIPE_STORY_THREE_LEVELS,
632
+ "test",
633
+ PREFIXES,
634
+ );
635
+ const result = await lookup?.resolve(expandRt, ["Pancakes"], {
636
+ digest: true,
637
+ });
638
+ const entity = result?.results.at(0) as
639
+ | { ingredients?: unknown[] }
640
+ | undefined;
641
+ expect(entity?.ingredients).toHaveLength(2);
642
+ });
643
+
644
+ it("an explicit --detail beats an alias flag", async () => {
645
+ const { lookup } = compilePackStories(
646
+ RECIPE_STORY_THREE_LEVELS,
647
+ "test",
648
+ PREFIXES,
649
+ );
650
+ const result = await lookup?.resolve(expandRt, ["Pancakes"], {
651
+ detail: "summary",
652
+ detailed: true,
653
+ });
654
+ const entity = result?.results.at(0) as
655
+ | { ingredients?: unknown }
656
+ | undefined;
657
+ expect(entity?.ingredients).toBeUndefined();
658
+ });
659
+
660
+ it("the highest requested alias level wins when several are set", async () => {
661
+ /** Gate the expand at `detailed` so digest-vs-detailed is observable. */
662
+ const gatedAtDetailed: StoryPackDefinition = {
663
+ ...RECIPE_STORY_THREE_LEVELS,
664
+ lookup: {
665
+ // biome-ignore lint/style/noNonNullAssertion: fixture always declares lookup
666
+ ...RECIPE_STORY_THREE_LEVELS.lookup!,
667
+ expand: [
668
+ {
669
+ name: "ingredients",
670
+ relation: "ex:ingredient",
671
+ select: [{ name: "label", property: "ex:label" }],
672
+ level: "detailed",
673
+ },
674
+ ],
675
+ },
676
+ };
677
+ const { lookup } = compilePackStories(gatedAtDetailed, "test", PREFIXES);
678
+ const result = await lookup?.resolve(expandRt, ["Pancakes"], {
679
+ digest: true,
680
+ detailed: true,
681
+ });
682
+ const entity = result?.results.at(0) as
683
+ | { ingredients?: unknown[] }
684
+ | undefined;
685
+ expect(entity?.ingredients).toHaveLength(2);
686
+ });
687
+
688
+ it("an MCP call with no explicit choice gets the highest level", async () => {
689
+ const { lookup } = compilePackStories(
690
+ RECIPE_STORY_DISCLOSURE,
691
+ "test",
692
+ PREFIXES,
693
+ );
694
+ const result = await lookup?.resolve(
695
+ expandRt,
696
+ ["Pancakes"],
697
+ {},
698
+ {
699
+ surface: "mcp",
700
+ detailed: false,
701
+ params: {},
702
+ },
703
+ );
704
+ const entity = result?.results.at(0) as
705
+ | { ingredients?: unknown[] }
706
+ | undefined;
707
+ expect(entity?.ingredients).toHaveLength(2);
708
+ });
709
+
710
+ it("the global config.detail outranks the MCP full-data default", async () => {
711
+ // Counterpart of the CLI config.detail test above: a user-configured
712
+ // global `detail: summary` must win over the MCP surface fallback.
713
+ const { lookup } = compilePackStories(
714
+ RECIPE_STORY_DISCLOSURE,
715
+ "test",
716
+ PREFIXES,
717
+ );
718
+ const configuredRt = {
719
+ store: expandStore,
720
+ config: { detail: "summary" },
721
+ } as PragmaRuntime;
722
+ const result = await lookup?.resolve(
723
+ configuredRt,
724
+ ["Pancakes"],
725
+ {},
726
+ { surface: "mcp", detailed: false, params: {} },
727
+ );
728
+ const entity = result?.results.at(0) as
729
+ | { ingredients?: unknown }
730
+ | undefined;
731
+ expect(entity?.ingredients).toBeUndefined();
732
+ });
733
+
734
+ it("an MCP alias explicitly false opts down to the base level", async () => {
735
+ const { lookup } = compilePackStories(
736
+ RECIPE_STORY_DISCLOSURE,
737
+ "test",
738
+ PREFIXES,
739
+ );
740
+ const result = await lookup?.resolve(
741
+ expandRt,
742
+ ["Pancakes"],
743
+ { detailed: false },
744
+ { surface: "mcp", detailed: false, params: {} },
745
+ );
746
+ const entity = result?.results.at(0) as
747
+ | { ingredients?: unknown }
748
+ | undefined;
749
+ expect(entity?.ingredients).toBeUndefined();
750
+ });
751
+ });