@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,859 @@
1
+ import { PragmaError } from "#error";
2
+ import { MAX_SAMPLE_COUNT, MIN_SAMPLE_COUNT } from "../../parseSampleCount.js";
3
+ import type {
4
+ StoryPackColumn,
5
+ StoryPackDefinition,
6
+ StoryPackDisclosure,
7
+ StoryPackExpand,
8
+ StoryPackExpandField,
9
+ StoryPackField,
10
+ StoryPackFilter,
11
+ StoryPackList,
12
+ StoryPackLookup,
13
+ StoryPackSample,
14
+ StoryPackSearch,
15
+ StoryPackSection,
16
+ StoryPackVerb,
17
+ } from "./types.js";
18
+
19
+ const NOUN_PATTERN = /^[a-z][a-z0-9-]*$/;
20
+ const FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
21
+
22
+ /**
23
+ * Object-prototype key names no pack identifier may take.
24
+ *
25
+ * Pack identifiers become plain-object keys at run time — SELECT
26
+ * variables become row keys, expand names become entity keys, and filter
27
+ * params become parameter keys — so `__proto__`/`constructor`/`prototype`
28
+ * are rejected at boot, before any row or params object could be built
29
+ * with a prototype-polluting key.
30
+ */
31
+ const RESERVED_OBJECT_KEYS = new Set(["__proto__", "constructor", "prototype"]);
32
+
33
+ /**
34
+ * Reject an identifier that names a reserved object-prototype key.
35
+ *
36
+ * Shared guard for every FIELD_PATTERN/FILTER_PARAM_PATTERN-shaped
37
+ * identifier (column fields, field/section/expand/select names, filter
38
+ * params and variables, search variables, disclosure levels).
39
+ *
40
+ * @throws PragmaError with code `CONFIG_ERROR` when the value is reserved.
41
+ */
42
+ function rejectReservedObjectKey(
43
+ value: string,
44
+ where: string,
45
+ source: string,
46
+ ): void {
47
+ if (RESERVED_OBJECT_KEYS.has(value)) {
48
+ throw buildStoryConfigError(
49
+ source,
50
+ `"${where}" must not be the reserved object key "${value}".`,
51
+ );
52
+ }
53
+ }
54
+ /** A prefixed name (`ex:Recipe`) — prefix + local part, no path slashes. */
55
+ const PREFIXED_NAME_PATTERN = /^[A-Za-z][\w-]*:[^/<>"\s]+$/;
56
+
57
+ /** An absolute IRI that can be safely embedded as `<iri>` in SPARQL. */
58
+ const EMBEDDABLE_IRI_PATTERN = /^[A-Za-z][\w+.-]*:\/\/[^<>"\s]+$/;
59
+
60
+ /**
61
+ * Whether a resolved IRI matches the embeddable shape pack terms are held
62
+ * to (no whitespace or `<>"` characters, `scheme://` form).
63
+ *
64
+ * Shared with the lookup path so an IRI-addressed lookup applies the same
65
+ * strictness to resolved user queries as this validator applies to
66
+ * authored pack terms before either is embedded as `<iri>` in SPARQL.
67
+ */
68
+ export function isEmbeddableIri(value: string): boolean {
69
+ return EMBEDDABLE_IRI_PATTERN.test(value);
70
+ }
71
+
72
+ /**
73
+ * A filter parameter name: a single lowercase word.
74
+ *
75
+ * Deliberately stricter than a noun (no hyphens): the name is used
76
+ * verbatim as the CLI flag, the Commander result key, and the MCP
77
+ * parameter key. A hyphenated name would register as `--meal-type` but
78
+ * be read back by Commander under the camelCased `mealType`, so the
79
+ * filter would silently never apply on the CLI while working on MCP.
80
+ * Restricting to one lowercase word keeps all three identical.
81
+ */
82
+ const FILTER_PARAM_PATTERN = /^[a-z][a-z0-9]*$/;
83
+
84
+ /**
85
+ * Parameter names taken by global flags or parameters the kernel appends
86
+ * to every read story — `condensed` is added to every MCP list tool,
87
+ * `detailed` to every lookup, `search` is the compiled free-text search
88
+ * parameter, and `detail` the disclosure level selector — so a filter may
89
+ * not reuse them.
90
+ */
91
+ const RESERVED_FILTER_PARAMS = new Set([
92
+ "llm",
93
+ "format",
94
+ "verbose",
95
+ "detail",
96
+ "detailed",
97
+ "condensed",
98
+ "names",
99
+ "search",
100
+ "help",
101
+ ]);
102
+
103
+ /**
104
+ * Names a disclosure level may not take. Levels derive parameters — the
105
+ * `--detail <level>` selector plus one boolean alias flag per non-base
106
+ * level — so a level must not collide with `detail` itself, the lookup's
107
+ * `names` positional, or kernel/global parameter names. `detailed` is
108
+ * deliberately absent: it is the expected name for a pack's legacy
109
+ * detailed level and derives the compatible `--detailed`/`detailed`
110
+ * alias.
111
+ */
112
+ const RESERVED_LEVEL_NAMES = new Set([
113
+ "detail",
114
+ "names",
115
+ "condensed",
116
+ "search",
117
+ "llm",
118
+ "format",
119
+ "verbose",
120
+ "help",
121
+ ]);
122
+
123
+ /**
124
+ * Validate one raw story-pack definition.
125
+ *
126
+ * Fails fast with `CONFIG_ERROR` naming the source and the offending
127
+ * field, mirroring how the `packages` config field is validated.
128
+ *
129
+ * @param raw - Parsed JSON value to validate.
130
+ * @param source - Where the definition came from, for error messages.
131
+ * @returns The validated definition.
132
+ * @throws PragmaError with code `CONFIG_ERROR` when the shape is invalid.
133
+ */
134
+ export default function validateStoryPackDefinition(
135
+ raw: unknown,
136
+ source: string,
137
+ ): StoryPackDefinition {
138
+ const obj = requireObject(raw, "story", source);
139
+
140
+ const noun = requireString(obj.noun, "noun", source);
141
+ if (!NOUN_PATTERN.test(noun)) {
142
+ throw buildStoryConfigError(
143
+ source,
144
+ `"noun" must be kebab-case, got "${noun}".`,
145
+ );
146
+ }
147
+
148
+ const description =
149
+ obj.description === undefined
150
+ ? undefined
151
+ : requireString(obj.description, "description", source);
152
+ const toolDescription =
153
+ obj.toolDescription === undefined
154
+ ? undefined
155
+ : requireString(obj.toolDescription, "toolDescription", source);
156
+
157
+ const list = validateList(obj.list, source);
158
+ const verbs = validateVerbs(obj.verbs, source);
159
+ const lookup =
160
+ obj.lookup === undefined ? undefined : validateLookup(obj.lookup, source);
161
+
162
+ return {
163
+ noun,
164
+ ...(description !== undefined ? { description } : {}),
165
+ ...(toolDescription !== undefined ? { toolDescription } : {}),
166
+ list,
167
+ ...(verbs ? { verbs } : {}),
168
+ ...(lookup ? { lookup } : {}),
169
+ };
170
+ }
171
+
172
+ /**
173
+ * Verbs the compiler itself emits for a pack noun — an extra verb may not
174
+ * collide with them.
175
+ */
176
+ const COMPILED_VERBS = new Set(["list", "lookup", "sample"]);
177
+
178
+ function validateVerbs(
179
+ raw: unknown,
180
+ source: string,
181
+ ): readonly StoryPackVerb[] | undefined {
182
+ if (raw === undefined) return undefined;
183
+ if (!Array.isArray(raw) || raw.length === 0) {
184
+ throw buildStoryConfigError(source, '"verbs" must be a non-empty array.');
185
+ }
186
+ const seen = new Set<string>();
187
+ return raw.map((entry, index) => {
188
+ const where = `verbs[${index}]`;
189
+ const obj = requireObject(entry, where, source);
190
+ const verb = requireString(obj.verb, `${where}.verb`, source);
191
+ if (!NOUN_PATTERN.test(verb)) {
192
+ throw buildStoryConfigError(
193
+ source,
194
+ `"${where}.verb" must be kebab-case, got "${verb}".`,
195
+ );
196
+ }
197
+ if (COMPILED_VERBS.has(verb)) {
198
+ throw buildStoryConfigError(
199
+ source,
200
+ `"${where}.verb" "${verb}" collides with a compiled verb.`,
201
+ );
202
+ }
203
+ if (seen.has(verb)) {
204
+ throw buildStoryConfigError(source, `duplicate verb "${verb}".`);
205
+ }
206
+ seen.add(verb);
207
+ const description =
208
+ obj.description === undefined
209
+ ? undefined
210
+ : requireString(obj.description, `${where}.description`, source);
211
+ const toolDescription =
212
+ obj.toolDescription === undefined
213
+ ? undefined
214
+ : requireString(
215
+ obj.toolDescription,
216
+ `${where}.toolDescription`,
217
+ source,
218
+ );
219
+ return {
220
+ verb,
221
+ ...(description !== undefined ? { description } : {}),
222
+ ...(toolDescription !== undefined ? { toolDescription } : {}),
223
+ ...validateListShape(entry, where, source),
224
+ };
225
+ });
226
+ }
227
+
228
+ function validateList(
229
+ raw: unknown,
230
+ source: string,
231
+ ): StoryPackDefinition["list"] {
232
+ return validateListShape(raw, "list", source);
233
+ }
234
+
235
+ /**
236
+ * Validate a list-shaped story half: query, columns, filters, and search.
237
+ *
238
+ * Shared by `list` and (pack v1) extra list verbs, which compile through
239
+ * the same machinery. `where` names the field path for error messages.
240
+ */
241
+ function validateListShape(
242
+ raw: unknown,
243
+ where: string,
244
+ source: string,
245
+ ): StoryPackList {
246
+ const obj = requireObject(raw, where, source);
247
+ const query = requireString(obj.query, `${where}.query`, source);
248
+ // Strip leading PREFIX declarations so the real query verb is checked —
249
+ // "PREFIX ex: <…> CONSTRUCT {…}" must fail here, not at first use.
250
+ const afterPrefixes = query.replace(
251
+ /^(\s*PREFIX\s+[A-Za-z][\w-]*:\s*<[^>]*>)+/i,
252
+ "",
253
+ );
254
+ if (!/^\s*SELECT\s/i.test(afterPrefixes)) {
255
+ throw buildStoryConfigError(
256
+ source,
257
+ `"${where}.query" must be a SPARQL SELECT query.`,
258
+ );
259
+ }
260
+
261
+ if (!Array.isArray(obj.columns) || obj.columns.length === 0) {
262
+ throw buildStoryConfigError(
263
+ source,
264
+ `"${where}.columns" must be a non-empty array.`,
265
+ );
266
+ }
267
+ const columns = obj.columns.map((column, index) =>
268
+ validateColumn(column, `${where}.columns[${index}]`, source),
269
+ );
270
+ // Like filters and search below: a column naming a variable the query
271
+ // never binds is a typo that should fail at boot, not render an
272
+ // always-empty column at first use.
273
+ for (const column of columns) {
274
+ if (!referencesVariable(query, column.field)) {
275
+ throw buildStoryConfigError(
276
+ source,
277
+ `column field "?${column.field}" does not appear in "${where}.query".`,
278
+ );
279
+ }
280
+ }
281
+
282
+ const filters = validateFilterArray(obj.filters, where, source);
283
+ for (const filter of filters ?? []) {
284
+ if (!referencesVariable(query, filter.variable)) {
285
+ throw buildStoryConfigError(
286
+ source,
287
+ `filter variable "?${filter.variable}" does not appear in "${where}.query".`,
288
+ );
289
+ }
290
+ }
291
+
292
+ const search = validateSearch(obj.search, query, where, source);
293
+
294
+ return {
295
+ query,
296
+ columns,
297
+ ...(filters ? { filters } : {}),
298
+ ...(search ? { search } : {}),
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Validate a list's free-text `search` declaration.
304
+ *
305
+ * Every searched variable must appear in the query — like filters, this
306
+ * fails a typo at boot instead of a search that silently never matches.
307
+ */
308
+ function validateSearch(
309
+ raw: unknown,
310
+ query: string,
311
+ where: string,
312
+ source: string,
313
+ ): StoryPackSearch | undefined {
314
+ if (raw === undefined) return undefined;
315
+ const obj = requireObject(raw, `${where}.search`, source);
316
+ if (!Array.isArray(obj.variables) || obj.variables.length === 0) {
317
+ throw buildStoryConfigError(
318
+ source,
319
+ `"${where}.search.variables" must be a non-empty array.`,
320
+ );
321
+ }
322
+ const variables = obj.variables.map((value, index) => {
323
+ const variable = requireString(
324
+ value,
325
+ `${where}.search.variables[${index}]`,
326
+ source,
327
+ );
328
+ if (!FIELD_PATTERN.test(variable)) {
329
+ throw buildStoryConfigError(
330
+ source,
331
+ `"${where}.search.variables[${index}]" must name a SELECT variable.`,
332
+ );
333
+ }
334
+ rejectReservedObjectKey(
335
+ variable,
336
+ `${where}.search.variables[${index}]`,
337
+ source,
338
+ );
339
+ if (!referencesVariable(query, variable)) {
340
+ throw buildStoryConfigError(
341
+ source,
342
+ `search variable "?${variable}" does not appear in "${where}.query".`,
343
+ );
344
+ }
345
+ return variable;
346
+ });
347
+ const description =
348
+ obj.description === undefined
349
+ ? undefined
350
+ : requireString(obj.description, `${where}.search.description`, source);
351
+ return {
352
+ variables,
353
+ ...(description !== undefined ? { description } : {}),
354
+ };
355
+ }
356
+
357
+ /**
358
+ * Check that the query mentions the variable (`?name` or `$name`).
359
+ *
360
+ * A textual check, not a SPARQL parse — it exists to fail typos at boot
361
+ * instead of silently filtering every row out at first use.
362
+ */
363
+ function referencesVariable(query: string, variable: string): boolean {
364
+ const pattern = new RegExp(`[?$]${variable}\\b`);
365
+ return pattern.test(query);
366
+ }
367
+
368
+ function validateFilterArray(
369
+ raw: unknown,
370
+ where: string,
371
+ source: string,
372
+ ): readonly StoryPackFilter[] | undefined {
373
+ if (raw === undefined) return undefined;
374
+ if (!Array.isArray(raw) || raw.length === 0) {
375
+ throw buildStoryConfigError(
376
+ source,
377
+ `"${where}.filters" must be a non-empty array.`,
378
+ );
379
+ }
380
+ const filters = raw.map((entry, index) =>
381
+ validateFilter(entry, `${where}.filters[${index}]`, source),
382
+ );
383
+ const seen = new Set<string>();
384
+ for (const filter of filters) {
385
+ if (seen.has(filter.param)) {
386
+ throw buildStoryConfigError(
387
+ source,
388
+ `duplicate filter param "${filter.param}".`,
389
+ );
390
+ }
391
+ seen.add(filter.param);
392
+ }
393
+ return filters;
394
+ }
395
+
396
+ function validateFilter(
397
+ raw: unknown,
398
+ where: string,
399
+ source: string,
400
+ ): StoryPackFilter {
401
+ const obj = requireObject(raw, where, source);
402
+ const param = requireString(obj.param, `${where}.param`, source);
403
+ if (!FILTER_PARAM_PATTERN.test(param)) {
404
+ throw buildStoryConfigError(
405
+ source,
406
+ `"${where}.param" must be a single lowercase word (letters and digits, no hyphens), got "${param}".`,
407
+ );
408
+ }
409
+ if (RESERVED_FILTER_PARAMS.has(param)) {
410
+ throw buildStoryConfigError(
411
+ source,
412
+ `"${where}.param" "${param}" is a reserved name.`,
413
+ );
414
+ }
415
+ rejectReservedObjectKey(param, `${where}.param`, source);
416
+ const variable = requireString(obj.variable, `${where}.variable`, source);
417
+ if (!FIELD_PATTERN.test(variable)) {
418
+ throw buildStoryConfigError(
419
+ source,
420
+ `"${where}.variable" must name a SELECT variable.`,
421
+ );
422
+ }
423
+ rejectReservedObjectKey(variable, `${where}.variable`, source);
424
+ // `values` is optional (pack v1): without it the filter is a free-string
425
+ // parameter matched case-insensitively against the variable — used when
426
+ // the value set is data-driven and cannot be declared.
427
+ const values =
428
+ obj.values === undefined
429
+ ? undefined
430
+ : validateFilterValues(obj.values, where, source);
431
+ const description =
432
+ obj.description === undefined
433
+ ? undefined
434
+ : requireString(obj.description, `${where}.description`, source);
435
+ return {
436
+ param,
437
+ variable,
438
+ ...(values ? { values } : {}),
439
+ ...(description !== undefined ? { description } : {}),
440
+ };
441
+ }
442
+
443
+ function validateFilterValues(
444
+ raw: unknown,
445
+ where: string,
446
+ source: string,
447
+ ): readonly string[] {
448
+ if (!Array.isArray(raw) || raw.length === 0) {
449
+ throw buildStoryConfigError(
450
+ source,
451
+ `"${where}.values" must be a non-empty array.`,
452
+ );
453
+ }
454
+ const values = raw.map((value, index) =>
455
+ requireString(value, `${where}.values[${index}]`, source),
456
+ );
457
+ const lowered = new Set<string>();
458
+ for (const value of values) {
459
+ const key = value.toLowerCase();
460
+ if (lowered.has(key)) {
461
+ throw buildStoryConfigError(
462
+ source,
463
+ `"${where}.values" contains duplicate value "${value}" ` +
464
+ "(values must be unique, case-insensitively).",
465
+ );
466
+ }
467
+ lowered.add(key);
468
+ }
469
+ return values;
470
+ }
471
+
472
+ function validateColumn(
473
+ raw: unknown,
474
+ where: string,
475
+ source: string,
476
+ ): StoryPackColumn {
477
+ const obj = requireObject(raw, where, source);
478
+ const field = requireString(obj.field, `${where}.field`, source);
479
+ if (!FIELD_PATTERN.test(field)) {
480
+ throw buildStoryConfigError(
481
+ source,
482
+ `"${where}.field" must name a SELECT variable.`,
483
+ );
484
+ }
485
+ rejectReservedObjectKey(field, `${where}.field`, source);
486
+ const label =
487
+ obj.label === undefined
488
+ ? undefined
489
+ : requireString(obj.label, `${where}.label`, source);
490
+ return { field, ...(label !== undefined ? { label } : {}) };
491
+ }
492
+
493
+ function validateLookup(raw: unknown, source: string): StoryPackLookup {
494
+ const obj = requireObject(raw, "lookup", source);
495
+ const by = requirePredicateTerm(obj.by, "lookup.by", source);
496
+ const type =
497
+ obj.type === undefined
498
+ ? undefined
499
+ : requireTerm(obj.type, "lookup.type", source);
500
+ const description =
501
+ obj.description === undefined
502
+ ? undefined
503
+ : requireString(obj.description, "lookup.description", source);
504
+ const toolDescription =
505
+ obj.toolDescription === undefined
506
+ ? undefined
507
+ : requireString(obj.toolDescription, "lookup.toolDescription", source);
508
+
509
+ const fields = validateFieldArray(obj.fields, "lookup.fields", source);
510
+ const sections = validateSectionArray(
511
+ obj.sections,
512
+ "lookup.sections",
513
+ source,
514
+ );
515
+ const expand = validateExpandArray(obj.expand, "lookup.expand", source);
516
+ const disclosure = validateDisclosure(obj.disclosure, source);
517
+ const sample = validateSample(obj.sample, source);
518
+
519
+ // Cross-check: every expand `level` must name a declared disclosure level.
520
+ const levels = new Set(disclosure?.levels ?? []);
521
+ for (const entry of expand ?? []) {
522
+ if (entry.level !== undefined && !levels.has(entry.level)) {
523
+ throw buildStoryConfigError(
524
+ source,
525
+ `expand "${entry.name}" level "${entry.level}" is not a declared disclosure level.`,
526
+ );
527
+ }
528
+ }
529
+
530
+ return {
531
+ by,
532
+ ...(type !== undefined ? { type } : {}),
533
+ ...(description !== undefined ? { description } : {}),
534
+ ...(toolDescription !== undefined ? { toolDescription } : {}),
535
+ ...(fields ? { fields } : {}),
536
+ ...(sections ? { sections } : {}),
537
+ ...(expand ? { expand } : {}),
538
+ ...(disclosure ? { disclosure } : {}),
539
+ ...(sample !== undefined ? { sample } : {}),
540
+ };
541
+ }
542
+
543
+ function validateSample(
544
+ raw: unknown,
545
+ source: string,
546
+ ): true | StoryPackSample | undefined {
547
+ if (raw === undefined) return undefined;
548
+ if (raw === true) return true;
549
+ const obj = requireObject(raw, "lookup.sample", source);
550
+ if (obj.count !== undefined) {
551
+ // Authors fail fast on an out-of-range default; user-provided counts
552
+ // are clamped at run time by parseSampleCount instead.
553
+ if (
554
+ typeof obj.count !== "number" ||
555
+ !Number.isInteger(obj.count) ||
556
+ obj.count < MIN_SAMPLE_COUNT ||
557
+ obj.count > MAX_SAMPLE_COUNT
558
+ ) {
559
+ throw buildStoryConfigError(
560
+ source,
561
+ `"lookup.sample.count" must be an integer between ${MIN_SAMPLE_COUNT} and ${MAX_SAMPLE_COUNT}.`,
562
+ );
563
+ }
564
+ }
565
+ const description =
566
+ obj.description === undefined
567
+ ? undefined
568
+ : requireString(obj.description, "lookup.sample.description", source);
569
+ const toolDescription =
570
+ obj.toolDescription === undefined
571
+ ? undefined
572
+ : requireString(
573
+ obj.toolDescription,
574
+ "lookup.sample.toolDescription",
575
+ source,
576
+ );
577
+ return {
578
+ ...(obj.count !== undefined ? { count: obj.count as number } : {}),
579
+ ...(description !== undefined ? { description } : {}),
580
+ ...(toolDescription !== undefined ? { toolDescription } : {}),
581
+ };
582
+ }
583
+
584
+ function validateDisclosure(
585
+ raw: unknown,
586
+ source: string,
587
+ ): StoryPackDisclosure | undefined {
588
+ if (raw === undefined) return undefined;
589
+ const obj = requireObject(raw, "lookup.disclosure", source);
590
+ if (!Array.isArray(obj.levels) || obj.levels.length === 0) {
591
+ throw buildStoryConfigError(
592
+ source,
593
+ '"lookup.disclosure.levels" must be a non-empty array.',
594
+ );
595
+ }
596
+ const seen = new Set<string>();
597
+ const levels = obj.levels.map((value, index) => {
598
+ const level = requireString(
599
+ value,
600
+ `lookup.disclosure.levels[${index}]`,
601
+ source,
602
+ );
603
+ // Levels derive parameter names (`--detail` enum values and the alias
604
+ // flags), so they are held to the same single-word rule as filter
605
+ // params and may not shadow reserved parameter names.
606
+ if (!FILTER_PARAM_PATTERN.test(level)) {
607
+ throw buildStoryConfigError(
608
+ source,
609
+ `disclosure level "${level}" must be a single lowercase word (letters and digits, no hyphens).`,
610
+ );
611
+ }
612
+ if (RESERVED_LEVEL_NAMES.has(level)) {
613
+ throw buildStoryConfigError(
614
+ source,
615
+ `disclosure level "${level}" is a reserved name.`,
616
+ );
617
+ }
618
+ rejectReservedObjectKey(
619
+ level,
620
+ `lookup.disclosure.levels[${index}]`,
621
+ source,
622
+ );
623
+ if (seen.has(level)) {
624
+ throw buildStoryConfigError(
625
+ source,
626
+ `duplicate disclosure level "${level}".`,
627
+ );
628
+ }
629
+ seen.add(level);
630
+ return level;
631
+ });
632
+ const defaultLevel =
633
+ obj.default === undefined
634
+ ? undefined
635
+ : requireString(obj.default, "lookup.disclosure.default", source);
636
+ if (defaultLevel !== undefined && !seen.has(defaultLevel)) {
637
+ throw buildStoryConfigError(
638
+ source,
639
+ `disclosure default "${defaultLevel}" is not one of the declared levels.`,
640
+ );
641
+ }
642
+ return {
643
+ levels,
644
+ ...(defaultLevel !== undefined ? { default: defaultLevel } : {}),
645
+ };
646
+ }
647
+
648
+ function validateExpandArray(
649
+ raw: unknown,
650
+ where: string,
651
+ source: string,
652
+ ): readonly StoryPackExpand[] | undefined {
653
+ if (raw === undefined) return undefined;
654
+ if (!Array.isArray(raw)) {
655
+ throw buildStoryConfigError(source, `"${where}" must be an array.`);
656
+ }
657
+ const names = new Set<string>();
658
+ return raw.map((entry, index) => {
659
+ const expand = validateExpand(entry, `${where}[${index}]`, source);
660
+ if (names.has(expand.name)) {
661
+ throw buildStoryConfigError(
662
+ source,
663
+ `duplicate expand name "${expand.name}" in "${where}".`,
664
+ );
665
+ }
666
+ names.add(expand.name);
667
+ return expand;
668
+ });
669
+ }
670
+
671
+ function validateExpand(
672
+ raw: unknown,
673
+ where: string,
674
+ source: string,
675
+ ): StoryPackExpand {
676
+ const obj = requireObject(raw, where, source);
677
+ const name = requireString(obj.name, `${where}.name`, source);
678
+ if (!FIELD_PATTERN.test(name)) {
679
+ throw buildStoryConfigError(
680
+ source,
681
+ `"${where}.name" must be a simple identifier.`,
682
+ );
683
+ }
684
+ rejectReservedObjectKey(name, `${where}.name`, source);
685
+ const relation = requirePredicateTerm(
686
+ obj.relation,
687
+ `${where}.relation`,
688
+ source,
689
+ );
690
+ if (obj.kind !== undefined && obj.kind !== "list" && obj.kind !== "table") {
691
+ throw buildStoryConfigError(
692
+ source,
693
+ `"${where}.kind" must be "list" or "table".`,
694
+ );
695
+ }
696
+ if (!Array.isArray(obj.select) || obj.select.length === 0) {
697
+ throw buildStoryConfigError(
698
+ source,
699
+ `"${where}.select" must be a non-empty array.`,
700
+ );
701
+ }
702
+ const select: StoryPackExpandField[] = obj.select.map((entry, index) =>
703
+ validateField(entry, `${where}.select[${index}]`, source),
704
+ );
705
+ const heading =
706
+ obj.heading === undefined
707
+ ? undefined
708
+ : requireString(obj.heading, `${where}.heading`, source);
709
+ // Membership in the declared levels is cross-checked in validateLookup.
710
+ const level =
711
+ obj.level === undefined
712
+ ? undefined
713
+ : requireString(obj.level, `${where}.level`, source);
714
+ return {
715
+ name,
716
+ relation,
717
+ select,
718
+ ...(heading !== undefined ? { heading } : {}),
719
+ ...(obj.kind !== undefined ? { kind: obj.kind as "list" | "table" } : {}),
720
+ ...(obj.showWhenEmpty === true ? { showWhenEmpty: true } : {}),
721
+ ...(level !== undefined ? { level } : {}),
722
+ };
723
+ }
724
+
725
+ function validateFieldArray(
726
+ raw: unknown,
727
+ where: string,
728
+ source: string,
729
+ ): readonly StoryPackField[] | undefined {
730
+ if (raw === undefined) return undefined;
731
+ if (!Array.isArray(raw)) {
732
+ throw buildStoryConfigError(source, `"${where}" must be an array.`);
733
+ }
734
+ return raw.map((entry, index) =>
735
+ validateField(entry, `${where}[${index}]`, source),
736
+ );
737
+ }
738
+
739
+ function validateSectionArray(
740
+ raw: unknown,
741
+ where: string,
742
+ source: string,
743
+ ): readonly StoryPackSection[] | undefined {
744
+ if (raw === undefined) return undefined;
745
+ if (!Array.isArray(raw)) {
746
+ throw buildStoryConfigError(source, `"${where}" must be an array.`);
747
+ }
748
+ return raw.map((entry, index) => {
749
+ const base = validateField(entry, `${where}[${index}]`, source);
750
+ const obj = entry as Record<string, unknown>;
751
+ if (obj.kind !== undefined && obj.kind !== "field" && obj.kind !== "code") {
752
+ throw buildStoryConfigError(
753
+ source,
754
+ `"${where}[${index}].kind" must be "field" or "code".`,
755
+ );
756
+ }
757
+ return {
758
+ ...base,
759
+ ...(obj.kind !== undefined ? { kind: obj.kind as "field" | "code" } : {}),
760
+ };
761
+ });
762
+ }
763
+
764
+ function validateField(
765
+ raw: unknown,
766
+ where: string,
767
+ source: string,
768
+ ): StoryPackField {
769
+ const obj = requireObject(raw, where, source);
770
+ const name = requireString(obj.name, `${where}.name`, source);
771
+ if (!FIELD_PATTERN.test(name)) {
772
+ throw buildStoryConfigError(
773
+ source,
774
+ `"${where}.name" must be a simple identifier.`,
775
+ );
776
+ }
777
+ rejectReservedObjectKey(name, `${where}.name`, source);
778
+ const property = requirePredicateTerm(
779
+ obj.property,
780
+ `${where}.property`,
781
+ source,
782
+ );
783
+ const label =
784
+ obj.label === undefined
785
+ ? undefined
786
+ : requireString(obj.label, `${where}.label`, source);
787
+ return { name, property, ...(label !== undefined ? { label } : {}) };
788
+ }
789
+
790
+ function requireObject(
791
+ raw: unknown,
792
+ where: string,
793
+ source: string,
794
+ ): Record<string, unknown> {
795
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
796
+ throw buildStoryConfigError(source, `"${where}" must be an object.`);
797
+ }
798
+ return raw as Record<string, unknown>;
799
+ }
800
+
801
+ function requireString(raw: unknown, where: string, source: string): string {
802
+ if (typeof raw !== "string" || raw.trim() === "") {
803
+ throw buildStoryConfigError(
804
+ source,
805
+ `"${where}" must be a non-empty string.`,
806
+ );
807
+ }
808
+ return raw;
809
+ }
810
+
811
+ /** Require a prefixed name (`ex:Recipe`) or an embeddable absolute IRI. */
812
+ function requireTerm(raw: unknown, where: string, source: string): string {
813
+ const value = requireString(raw, where, source);
814
+ if (value.includes("://")) {
815
+ if (EMBEDDABLE_IRI_PATTERN.test(value)) return value;
816
+ throw buildStoryConfigError(
817
+ source,
818
+ `"${where}" is not a valid IRI (no whitespace or <>" characters).`,
819
+ );
820
+ }
821
+ if (PREFIXED_NAME_PATTERN.test(value)) return value;
822
+ throw buildStoryConfigError(
823
+ source,
824
+ `"${where}" must be a prefixed name (ex:Recipe) or an absolute IRI.`,
825
+ );
826
+ }
827
+
828
+ /**
829
+ * Require a predicate term: a single term or a slash-separated SPARQL
830
+ * property path of prefixed names (`cs:hasCategory/cs:slug`).
831
+ *
832
+ * Paths are only meaningful in predicate position (`lookup.by` and
833
+ * `fields[].property`/`sections[].property`); class terms go through
834
+ * `requireTerm` and reject paths.
835
+ */
836
+ function requirePredicateTerm(
837
+ raw: unknown,
838
+ where: string,
839
+ source: string,
840
+ ): string {
841
+ const value = requireString(raw, where, source);
842
+ if (value.includes("://") || !value.includes("/")) {
843
+ return requireTerm(value, where, source);
844
+ }
845
+ const segments = value.split("/");
846
+ for (const segment of segments) {
847
+ if (!PREFIXED_NAME_PATTERN.test(segment)) {
848
+ throw buildStoryConfigError(
849
+ source,
850
+ `"${where}" property path segment "${segment}" must be a prefixed name.`,
851
+ );
852
+ }
853
+ }
854
+ return value;
855
+ }
856
+
857
+ function buildStoryConfigError(source: string, message: string): PragmaError {
858
+ return PragmaError.configError(`Invalid story in ${source}: ${message}`);
859
+ }