@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,724 @@
1
+ import type { Store } from "@canonical/ke";
2
+ import { PragmaError } from "#error";
3
+ import resolveUri from "../../../graph/helpers/resolveUri.js";
4
+ import type {
5
+ ColumnDef,
6
+ LookupField,
7
+ RenderListOptions,
8
+ RenderLookupOptions,
9
+ SectionDef,
10
+ } from "../../contracts.js";
11
+ import type { Formatters } from "../../formatters.js";
12
+ import lookupMany from "../../lookupMany.js";
13
+ import parseSampleCount, {
14
+ DEFAULT_SAMPLE_COUNT,
15
+ MAX_SAMPLE_COUNT,
16
+ MIN_SAMPLE_COUNT,
17
+ } from "../../parseSampleCount.js";
18
+ import pickRandom from "../../pickRandom.js";
19
+ import {
20
+ renderListLlm,
21
+ renderListPlain,
22
+ renderLookupLlm,
23
+ renderLookupPlain,
24
+ } from "../../renderers.js";
25
+ import createSampleFormatters from "../../sampleFormatters.js";
26
+ import {
27
+ expandGlob,
28
+ isGlobPattern,
29
+ suggestNames,
30
+ } from "../../suggestions/index.js";
31
+ import type { SampleOutput } from "../../types/index.js";
32
+ import requirePragmaContext from "../requirePragmaContext.js";
33
+ import type { LookupStory, ReadStory, StoryParam } from "../types.js";
34
+ import applyPackFilters from "./applyPackFilters.js";
35
+ import applyPackSearch from "./applyPackSearch.js";
36
+ import buildLookupQuery, {
37
+ buildExpandQuery,
38
+ buildLookupByIriQuery,
39
+ buildLookupNamesQuery,
40
+ } from "./buildLookupQuery.js";
41
+ import runSelectQuery from "./runSelectQuery.js";
42
+ import type {
43
+ StoryPackDefinition,
44
+ StoryPackDisclosure,
45
+ StoryPackFilter,
46
+ StoryPackList,
47
+ StoryPackLookup,
48
+ StoryPackSearch,
49
+ } from "./types.js";
50
+ import { isEmbeddableIri } from "./validateStoryPackDefinition.js";
51
+
52
+ /** A pack entity/row: SELECT variable name → string value. */
53
+ type PackRow = Record<string, string>;
54
+
55
+ /**
56
+ * A looked-up pack entity: flat SELECT fields plus any expanded child arrays
57
+ * (pack v1 nested projections). Scalar fields are strings; an expand's value
58
+ * is the array of its child rows.
59
+ */
60
+ type PackEntity = Record<string, string | readonly PackRow[]>;
61
+
62
+ /** Resolved sample data: the drawn entities plus the population size. */
63
+ interface PackSampleData {
64
+ readonly samples: PackEntity[];
65
+ readonly totalCount: number;
66
+ }
67
+
68
+ /** The read stories a pack definition compiles to. */
69
+ export interface CompiledPackStories {
70
+ readonly list: ReadStory<PackRow[], PackRow[]>;
71
+ /** Extra list-shaped verbs (e.g. `categories`), in declaration order. */
72
+ readonly verbs: readonly ReadStory<PackRow[], PackRow[]>[];
73
+ readonly lookup?: LookupStory<PackEntity, PackEntity>;
74
+ /** The `sample` story, when the lookup declares the capability. */
75
+ readonly sample?: ReadStory<PackSampleData, SampleOutput<PackEntity>>;
76
+ }
77
+
78
+ /**
79
+ * Compile one validated story-pack definition into kernel read stories.
80
+ *
81
+ * The list story (and any extra list-shaped verbs) run the pack's
82
+ * preferred SELECT verbatim; the lookup and sample stories run generated,
83
+ * injection-safe queries. All render through the shared generic renderers
84
+ * with the runtime's merged prefix map, so foreign namespaces display
85
+ * compacted.
86
+ *
87
+ * @param definition - A validated pack definition.
88
+ * @param source - Where the definition came from, for diagnostics.
89
+ * @param prefixes - The merged prefix map used for display.
90
+ * @returns The compiled list story, extra verbs, and — when declared —
91
+ * the lookup and sample stories.
92
+ */
93
+ export default function compilePackStories(
94
+ definition: StoryPackDefinition,
95
+ source: string,
96
+ prefixes: Readonly<Record<string, string>>,
97
+ ): CompiledPackStories {
98
+ const { noun } = definition;
99
+ const description = definition.description ?? `List ${noun} entries`;
100
+
101
+ const list = compileListVerb(definition.list, {
102
+ noun,
103
+ verb: "list",
104
+ heading: capitalize(noun),
105
+ description,
106
+ toolDescription:
107
+ definition.toolDescription ?? `${description} (story pack: ${source}).`,
108
+ source,
109
+ prefixes,
110
+ });
111
+
112
+ const verbs = (definition.verbs ?? []).map((verb) => {
113
+ const verbDescription = verb.description ?? `List ${noun} ${verb.verb}`;
114
+ return compileListVerb(verb, {
115
+ noun,
116
+ verb: verb.verb,
117
+ heading: `${capitalize(noun)} ${verb.verb}`,
118
+ description: verbDescription,
119
+ toolDescription:
120
+ verb.toolDescription ?? `${verbDescription} (story pack: ${source}).`,
121
+ source,
122
+ prefixes,
123
+ });
124
+ });
125
+
126
+ const compiled = definition.lookup
127
+ ? compileLookup(definition.lookup, noun, source, prefixes)
128
+ : undefined;
129
+
130
+ return {
131
+ list,
132
+ verbs,
133
+ ...(compiled ? { lookup: compiled.lookup } : {}),
134
+ ...(compiled?.sample ? { sample: compiled.sample } : {}),
135
+ };
136
+ }
137
+
138
+ /** Presentation facts for one compiled list-shaped verb. */
139
+ interface ListVerbMeta {
140
+ readonly noun: string;
141
+ readonly verb: string;
142
+ readonly heading: string;
143
+ readonly description: string;
144
+ readonly toolDescription: string;
145
+ readonly source: string;
146
+ readonly prefixes: Readonly<Record<string, string>>;
147
+ }
148
+
149
+ /**
150
+ * Compile one list-shaped story half (the `list` verb or an extra verb)
151
+ * into a kernel read story.
152
+ */
153
+ function compileListVerb(
154
+ shape: StoryPackList,
155
+ meta: ListVerbMeta,
156
+ ): ReadStory<PackRow[], PackRow[]> {
157
+ const { noun, verb, source, prefixes } = meta;
158
+ const listColumns: ColumnDef<PackRow>[] = shape.columns.map((column) => ({
159
+ key: column.field,
160
+ label: column.label ?? column.field,
161
+ }));
162
+ const listOptions: RenderListOptions<PackRow> = {
163
+ heading: meta.heading,
164
+ columns: listColumns,
165
+ prefixes,
166
+ };
167
+ const listFormatters: Formatters<PackRow[]> = {
168
+ plain: (rows) => renderListPlain(rows, listOptions),
169
+ llm: (rows) => renderListLlm(rows, listOptions),
170
+ json: (rows) => JSON.stringify(rows, null, 2),
171
+ };
172
+
173
+ const filters = shape.filters;
174
+ const search = shape.search;
175
+ // The generated example uses the first filter with a declared value set —
176
+ // a value-free filter has no representative value to show.
177
+ const filterExample = filters?.find((filter) => filter.values !== undefined);
178
+ return {
179
+ noun,
180
+ verb,
181
+ description: meta.description,
182
+ toolDescription: meta.toolDescription,
183
+ params: [...projectFilterParams(filters), ...projectSearchParam(search)],
184
+ examples: [
185
+ `pragma ${noun} ${verb}`,
186
+ ...(filterExample
187
+ ? [
188
+ `pragma ${noun} ${verb} --${filterExample.param} ${quoteExampleValue(filterExample.values?.at(0) ?? "")}`,
189
+ ]
190
+ : []),
191
+ `pragma ${noun} ${verb} --llm`,
192
+ ],
193
+ resolve: async (rt, params) =>
194
+ applyPackSearch(
195
+ applyPackFilters(
196
+ await runSelectQuery(rt.store, shape.query, source),
197
+ filters,
198
+ params,
199
+ ),
200
+ search,
201
+ params,
202
+ ),
203
+ toOutput: (rows) => rows,
204
+ formatters: listFormatters,
205
+ toEnvelope: (rows) => ({ data: rows, meta: { count: rows.length } }),
206
+ };
207
+ }
208
+
209
+ function compileLookup(
210
+ lookup: StoryPackLookup,
211
+ noun: string,
212
+ source: string,
213
+ prefixes: Readonly<Record<string, string>>,
214
+ ): {
215
+ lookup: LookupStory<PackEntity, PackEntity>;
216
+ sample?: ReadStory<PackSampleData, SampleOutput<PackEntity>>;
217
+ } {
218
+ const fields: LookupField<PackEntity>[] = (lookup.fields ?? []).map(
219
+ (field) => ({
220
+ label: field.label ?? field.name,
221
+ value: (entity) => entity[field.name],
222
+ }),
223
+ );
224
+ const flatSections: SectionDef<PackEntity>[] = (lookup.sections ?? []).map(
225
+ (section) => ({
226
+ key: section.name,
227
+ heading: section.label ?? section.name,
228
+ kind: section.kind ?? "field",
229
+ }),
230
+ );
231
+ // Expands render after the flat sections, as list/table collections.
232
+ const expandSections: SectionDef<PackEntity>[] = (lookup.expand ?? []).map(
233
+ (expand) => ({
234
+ key: expand.name,
235
+ heading: expand.heading ?? expand.name,
236
+ kind: expand.kind ?? "list",
237
+ ...(expand.showWhenEmpty ? { showWhenEmpty: true } : {}),
238
+ }),
239
+ );
240
+ const sections = [...flatSections, ...expandSections];
241
+ const lookupOptions: RenderLookupOptions<PackEntity> = {
242
+ title: (entity) => scalar(entity.name) ?? scalar(entity.uri) ?? "(unnamed)",
243
+ fields,
244
+ sections,
245
+ prefixes,
246
+ };
247
+ const lookupFormatters: Formatters<PackEntity> = {
248
+ plain: (entity) => renderLookupPlain(entity, lookupOptions),
249
+ llm: (entity) => renderLookupLlm(entity, lookupOptions),
250
+ json: (entity) => JSON.stringify(entity, null, 2),
251
+ };
252
+
253
+ // Disclosure capability: a declared level set derives a `--detail <level>`
254
+ // parameter (enum of the levels) on both surfaces, plus one legacy alias
255
+ // boolean per non-base level (`--digest`, `--detailed`) that implies its
256
+ // level — preserving the pre-pack flag ergonomics on both surfaces.
257
+ const disclosure = lookup.disclosure;
258
+ const detailParams: StoryParam[] = disclosure
259
+ ? [
260
+ {
261
+ name: "detail",
262
+ type: "string",
263
+ description: `Disclosure level (${disclosure.levels.join(", ")})`,
264
+ enum: [...disclosure.levels],
265
+ },
266
+ ...disclosure.levels.slice(1).map(
267
+ (level): StoryParam => ({
268
+ name: level,
269
+ type: "boolean",
270
+ description: `Shorthand for --detail ${level}`,
271
+ toolDescription: `Shorthand for detail="${level}"`,
272
+ default: false,
273
+ }),
274
+ ),
275
+ ]
276
+ : [];
277
+
278
+ const lookupStory: LookupStory<PackEntity, PackEntity> = {
279
+ noun,
280
+ description:
281
+ lookup.description ?? `Look up ${noun} details by name, IRI, or glob`,
282
+ toolDescription:
283
+ lookup.toolDescription ??
284
+ `Get details for one or more ${noun} entries by name, prefixed name (ex:thing), absolute IRI, or glob pattern (story pack: ${source}).`,
285
+ namesDescription: `${capitalize(noun)} names, IRIs/prefixed names, or glob patterns`,
286
+ params: detailParams,
287
+ complete: async (partial, cmdCtx) => {
288
+ const ctx = requirePragmaContext(cmdCtx);
289
+ const names = await listEntityNames(ctx.store, lookup, source);
290
+ return names.filter((name) =>
291
+ name.toLowerCase().startsWith(partial.toLowerCase()),
292
+ );
293
+ },
294
+ examples: [`pragma ${noun} lookup <name>`],
295
+ resolve: async (rt, names, params, view) => {
296
+ const level = disclosure
297
+ ? resolveDetailLevel(
298
+ params,
299
+ rt.config ?? {},
300
+ disclosure,
301
+ view?.surface ?? "cli",
302
+ )
303
+ : undefined;
304
+ // Glob queries (`react/*`) expand against the entity name list before
305
+ // resolution; a glob matching nothing becomes its own error entry.
306
+ const expanded = await expandPackLookupQueries(
307
+ rt.store,
308
+ lookup,
309
+ noun,
310
+ source,
311
+ names,
312
+ );
313
+ const result = await lookupMany(expanded.names, (query) =>
314
+ lookupPackEntity(
315
+ rt.store,
316
+ lookup,
317
+ noun,
318
+ query,
319
+ source,
320
+ level,
321
+ prefixes,
322
+ ),
323
+ );
324
+ return {
325
+ ...result,
326
+ errors: [...result.errors, ...expanded.globErrors],
327
+ };
328
+ },
329
+ toFmtInput: (entity) => entity,
330
+ formatters: lookupFormatters,
331
+ emptyNamesError: () =>
332
+ PragmaError.invalidInput("names", "(empty)", {
333
+ recovery: {
334
+ message: `List available ${noun} entries.`,
335
+ cli: `pragma ${noun} list`,
336
+ mcp: { tool: `${noun}_list` },
337
+ },
338
+ }),
339
+ };
340
+
341
+ const sample = lookup.sample
342
+ ? compileSample(lookup, noun, source, prefixes, lookupFormatters)
343
+ : undefined;
344
+
345
+ return { lookup: lookupStory, ...(sample ? { sample } : {}) };
346
+ }
347
+
348
+ /**
349
+ * Compile the `sample` story for a lookup that declares the capability.
350
+ *
351
+ * Draws N random entity names (clamped 1–5) and resolves each through the
352
+ * SAME lookup path at the HIGHEST disclosure level, so exemplars carry the
353
+ * full shape — fields, expands, and all. Rendering reuses the lookup
354
+ * formatters per entity under the shared sample header.
355
+ */
356
+ function compileSample(
357
+ lookup: StoryPackLookup,
358
+ noun: string,
359
+ source: string,
360
+ prefixes: Readonly<Record<string, string>>,
361
+ lookupFormatters: Formatters<PackEntity>,
362
+ ): ReadStory<PackSampleData, SampleOutput<PackEntity>> {
363
+ const config =
364
+ lookup.sample === true || lookup.sample === undefined ? {} : lookup.sample;
365
+ const description =
366
+ config.description ??
367
+ `Return randomly selected complete ${noun} entries as exemplars for shape discovery`;
368
+ const toolDescription =
369
+ config.toolDescription ??
370
+ `Return ${MIN_SAMPLE_COUNT}–${MAX_SAMPLE_COUNT} randomly selected complete ${noun} entries as exemplars. Use BEFORE writing queries to see actual data shapes, property names, and value formats. Each call returns different instances.`;
371
+ const sampleFormatters = createSampleFormatters<PackEntity>(
372
+ `${noun} entries`,
373
+ (entity, mode) => lookupFormatters[mode](entity),
374
+ );
375
+ const toOutput = (data: PackSampleData): SampleOutput<PackEntity> => ({
376
+ ...data,
377
+ nextSteps: sampleNextSteps(noun, data),
378
+ });
379
+
380
+ return {
381
+ noun,
382
+ verb: "sample",
383
+ description,
384
+ toolDescription,
385
+ params: [
386
+ {
387
+ name: "count",
388
+ type: "string",
389
+ description: `Number of samples to return (${MIN_SAMPLE_COUNT}–${MAX_SAMPLE_COUNT}, default ${config.count ?? DEFAULT_SAMPLE_COUNT})`,
390
+ positional: true,
391
+ },
392
+ ],
393
+ examples: [`pragma ${noun} sample`, `pragma ${noun} sample 3`],
394
+ resolve: async (rt, params) => {
395
+ const count = parseSampleCount(params.count ?? config.count);
396
+ const names = await listEntityNames(rt.store, lookup, source);
397
+ // Exemplars resolve at the HIGHEST disclosure level so every expand
398
+ // is fetched — samples exist to show the full data shape.
399
+ const level = lookup.disclosure?.levels.at(-1);
400
+ const selected = pickRandom(names, count);
401
+ const samples = await Promise.all(
402
+ selected.map((name) =>
403
+ lookupPackEntity(
404
+ rt.store,
405
+ lookup,
406
+ noun,
407
+ name,
408
+ source,
409
+ level,
410
+ prefixes,
411
+ ),
412
+ ),
413
+ );
414
+ return { samples, totalCount: names.length };
415
+ },
416
+ toOutput,
417
+ formatters: sampleFormatters,
418
+ toEnvelope: (data) => ({
419
+ data: toOutput(data),
420
+ meta: { count: data.samples.length },
421
+ }),
422
+ };
423
+ }
424
+
425
+ /** Agent-facing follow-ups appended to sample output. */
426
+ function sampleNextSteps(noun: string, data: PackSampleData): string[] {
427
+ return [
428
+ `These are ${data.samples.length} of ${data.totalCount} total ${noun} entries.`,
429
+ `Use ${noun}_lookup to inspect specific entries by name.`,
430
+ `Use ${noun}_list to browse all entries.`,
431
+ ];
432
+ }
433
+
434
+ /**
435
+ * Partition lookup queries into concrete names and expanded globs.
436
+ *
437
+ * Literal names pass through unchanged. Glob patterns expand against the
438
+ * pack's entity name list; a glob matching nothing produces an
439
+ * `EMPTY_RESULTS` error entry (mirroring the shared lookup-glob contract)
440
+ * instead of failing the whole batch.
441
+ *
442
+ * @note Impure — fetches the entity name list when a glob is present.
443
+ */
444
+ async function expandPackLookupQueries(
445
+ store: Store,
446
+ lookup: StoryPackLookup,
447
+ noun: string,
448
+ source: string,
449
+ queries: readonly string[],
450
+ ): Promise<{
451
+ names: string[];
452
+ globErrors: { query: string; code: string; message: string }[];
453
+ }> {
454
+ if (!queries.some(isGlobPattern)) {
455
+ return { names: [...queries], globErrors: [] };
456
+ }
457
+
458
+ const allNames = await listEntityNames(store, lookup, source);
459
+ const names: string[] = [];
460
+ const globErrors: { query: string; code: string; message: string }[] = [];
461
+ for (const query of queries) {
462
+ if (!isGlobPattern(query)) {
463
+ names.push(query);
464
+ continue;
465
+ }
466
+ const expanded = expandGlob(query, allNames);
467
+ if (expanded.length === 0) {
468
+ globErrors.push({
469
+ query,
470
+ code: "EMPTY_RESULTS",
471
+ message: `No ${noun} entries matching "${query}".`,
472
+ });
473
+ } else {
474
+ names.push(...expanded);
475
+ }
476
+ }
477
+ return { names, globErrors };
478
+ }
479
+
480
+ /**
481
+ * Look up one pack entity by name, prefixed name, or absolute IRI.
482
+ *
483
+ * A query shaped like an IRI (`https://…` or `prefix:local`) is resolved
484
+ * through the runtime prefix map and addressed exactly via a generated
485
+ * `BIND(<iri> AS ?uri)` query; anything else matches the `by` property's
486
+ * value case-insensitively. Both query forms are generated — the resolved
487
+ * IRI is validated against the embeddable-IRI shape and names are escaped
488
+ * as SPARQL string literals, so user input never reaches query text raw.
489
+ *
490
+ * @throws PragmaError with code `ENTITY_NOT_FOUND` and ranked suggestions
491
+ * when no entity matches, or `INVALID_INPUT` when an IRI-shaped query
492
+ * uses an unknown prefix or malformed IRI.
493
+ * @note Impure — queries the ke store.
494
+ */
495
+ async function lookupPackEntity(
496
+ store: Store,
497
+ lookup: StoryPackLookup,
498
+ noun: string,
499
+ query: string,
500
+ source: string,
501
+ level: string | undefined,
502
+ prefixes: Readonly<Record<string, string>>,
503
+ ): Promise<PackEntity> {
504
+ const rows = await runSelectQuery(
505
+ store,
506
+ buildEntityQuery(lookup, query, prefixes),
507
+ source,
508
+ );
509
+ const base = rows.at(0);
510
+ if (!base) {
511
+ const candidates = await listEntityNames(store, lookup, source);
512
+ throw PragmaError.notFound(noun, query, {
513
+ suggestions: suggestNames(query, candidates),
514
+ recovery: {
515
+ message: `List available ${noun} entries.`,
516
+ cli: `pragma ${noun} list`,
517
+ mcp: { tool: `${noun}_list` },
518
+ },
519
+ });
520
+ }
521
+
522
+ // Enrich with any declared multi-valued expands (pack v1 nested projections).
523
+ // Each expand runs a sub-SELECT bound to the resolved entity IRI (`base.uri`),
524
+ // which is store-derived, never user input. An expand tagged with a
525
+ // disclosure level is fetched only at/above that level — so a lower level
526
+ // never runs the sub-SELECT and the renderer omits the (absent) section.
527
+ const entity: PackEntity = { ...base };
528
+ const entityUri = base.uri;
529
+ if (entityUri) {
530
+ const levels = lookup.disclosure?.levels ?? [];
531
+ const activeIdx = level ? levels.indexOf(level) : Number.POSITIVE_INFINITY;
532
+ for (const expand of lookup.expand ?? []) {
533
+ const requiredIdx = expand.level ? levels.indexOf(expand.level) : 0;
534
+ if (activeIdx < requiredIdx) continue;
535
+ entity[expand.name] = await runSelectQuery(
536
+ store,
537
+ buildExpandQuery(expand, entityUri),
538
+ source,
539
+ );
540
+ }
541
+ }
542
+ return entity;
543
+ }
544
+
545
+ /**
546
+ * Build the base entity SELECT for a lookup query.
547
+ *
548
+ * Detection mirrors the retired built-in standard lookup: a query starting
549
+ * with `http(s)://` or containing `:` is treated as an IRI or prefixed
550
+ * name and resolved through the merged prefix map. The resolved IRI must
551
+ * still match the embeddable shape the pack validator holds authored
552
+ * terms to; anything else is rejected before it can reach query text.
553
+ *
554
+ * @throws PragmaError with code `INVALID_INPUT` when the query is
555
+ * IRI-shaped but uses an unknown prefix or resolves to a non-embeddable
556
+ * IRI.
557
+ */
558
+ function buildEntityQuery(
559
+ lookup: StoryPackLookup,
560
+ query: string,
561
+ prefixes: Readonly<Record<string, string>>,
562
+ ): string {
563
+ if (!looksLikeIri(query)) {
564
+ return buildLookupQuery(lookup, query);
565
+ }
566
+ // resolveUri validates prefix membership and rejects IRI-breaking
567
+ // characters; the embeddable-shape check adds the validator's stricter
568
+ // `scheme://` contract before the IRI is embedded as `<iri>`.
569
+ const resolved = resolveUri(query, prefixes);
570
+ if (!isEmbeddableIri(resolved)) {
571
+ throw PragmaError.invalidInput("name", query, {
572
+ recovery: {
573
+ message:
574
+ "Use an absolute IRI (https://…), a prefixed name (ex:thing), or a plain entity name.",
575
+ },
576
+ });
577
+ }
578
+ return buildLookupByIriQuery(lookup, resolved);
579
+ }
580
+
581
+ /** Whether a lookup query addresses an entity by IRI or prefixed name. */
582
+ function looksLikeIri(query: string): boolean {
583
+ return (
584
+ query.startsWith("http://") ||
585
+ query.startsWith("https://") ||
586
+ query.includes(":")
587
+ );
588
+ }
589
+
590
+ /**
591
+ * Resolve the effective disclosure level for a lookup call.
592
+ *
593
+ * Precedence (highest → lowest): an explicit `--detail`/`detail` value, a
594
+ * legacy alias flag set to true (the HIGHEST requested level wins when
595
+ * several are set), the global `config.detail` default, the MCP full-data
596
+ * default, the pack's declared `disclosure.default`, then the base level
597
+ * (the first declared). A config/pack value that is not one of the pack's
598
+ * levels is ignored (a global default may target another pack).
599
+ *
600
+ * `config.detail` deliberately outranks the MCP default: the config value
601
+ * is something the user explicitly chose, while the MCP default is only a
602
+ * surface fallback — a user who configured `detail: summary` gets summary
603
+ * on both surfaces.
604
+ *
605
+ * The MCP default follows the ratified surface contract (see
606
+ * `resolveLookupDetailed`): the CLI opts in while MCP opts out, so an MCP
607
+ * call with no explicit or configured choice resolves to the highest
608
+ * level — agents want full data by default — and an alias explicitly set
609
+ * to false (e.g. `detailed: false`) opts down to the pack default or base
610
+ * level.
611
+ */
612
+ function resolveDetailLevel(
613
+ params: Record<string, unknown>,
614
+ config: { readonly detail?: string },
615
+ disclosure: StoryPackDisclosure,
616
+ surface: "cli" | "mcp",
617
+ ): string {
618
+ const { levels } = disclosure;
619
+ const explicit =
620
+ typeof params.detail === "string" ? params.detail : undefined;
621
+ if (explicit !== undefined && levels.includes(explicit)) {
622
+ return explicit;
623
+ }
624
+
625
+ const aliased = levels
626
+ .filter((level, index) => index > 0 && params[level] === true)
627
+ .at(-1);
628
+ if (aliased !== undefined) {
629
+ return aliased;
630
+ }
631
+
632
+ if (config.detail !== undefined && levels.includes(config.detail)) {
633
+ return config.detail;
634
+ }
635
+
636
+ if (surface === "mcp") {
637
+ const optedDown = levels.some(
638
+ (level, index) => index > 0 && params[level] === false,
639
+ );
640
+ const highest = levels.at(-1);
641
+ if (!optedDown && highest !== undefined) {
642
+ return highest;
643
+ }
644
+ }
645
+
646
+ const packDefault = disclosure.default;
647
+ if (packDefault !== undefined && levels.includes(packDefault)) {
648
+ return packDefault;
649
+ }
650
+ return levels[0] ?? "";
651
+ }
652
+
653
+ async function listEntityNames(
654
+ store: Store,
655
+ lookup: StoryPackLookup,
656
+ source: string,
657
+ ): Promise<string[]> {
658
+ const rows = await runSelectQuery(
659
+ store,
660
+ buildLookupNamesQuery(lookup),
661
+ source,
662
+ );
663
+ return rows.map((row) => row.name ?? "").filter((name) => name !== "");
664
+ }
665
+
666
+ /** Return a value only when it is a scalar string (expands hold arrays). */
667
+ function scalar(
668
+ value: string | readonly PackRow[] | undefined,
669
+ ): string | undefined {
670
+ return typeof value === "string" ? value : undefined;
671
+ }
672
+
673
+ /**
674
+ * Quote a filter value for a shell help example when it is not a bare word.
675
+ *
676
+ * Values with whitespace or shell-sensitive characters are double-quoted
677
+ * so the generated `pragma … list --flag value` example stays valid to
678
+ * copy-paste; simple tokens render unquoted.
679
+ */
680
+ function quoteExampleValue(value: string): string {
681
+ return /^[\w.-]+$/.test(value) ? value : JSON.stringify(value);
682
+ }
683
+
684
+ /**
685
+ * Project declared pack filters onto kernel story parameters.
686
+ *
687
+ * A declared value set becomes the parameter's enum, which the kernel
688
+ * projects to CLI select choices and an MCP enum schema. A value-free
689
+ * filter projects as a plain string parameter — its valid values are
690
+ * data-driven and enforced only as a post-query row predicate.
691
+ */
692
+ function projectFilterParams(
693
+ filters: readonly StoryPackFilter[] | undefined,
694
+ ): StoryParam[] {
695
+ return (filters ?? []).map((filter) => ({
696
+ name: filter.param,
697
+ type: "string",
698
+ description:
699
+ filter.description ??
700
+ (filter.values
701
+ ? `Filter by ${filter.variable} (${filter.values.join(", ")})`
702
+ : `Filter by ${filter.variable}`),
703
+ ...(filter.values ? { enum: filter.values } : {}),
704
+ }));
705
+ }
706
+
707
+ /**
708
+ * Project a declared free-text search onto the `search` story parameter.
709
+ */
710
+ function projectSearchParam(search: StoryPackSearch | undefined): StoryParam[] {
711
+ if (!search) return [];
712
+ return [
713
+ {
714
+ name: "search",
715
+ type: "string",
716
+ description:
717
+ search.description ?? `Search in ${search.variables.join(", ")}`,
718
+ },
719
+ ];
720
+ }
721
+
722
+ function capitalize(value: string): string {
723
+ return value.charAt(0).toUpperCase() + value.slice(1);
724
+ }