@canonical/pragma-cli 0.32.0 → 0.34.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 (295) hide show
  1. package/README.md +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +19 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +15 -6
  6. package/src/capabilities/block.parity.test.ts +250 -0
  7. package/src/capabilities/capabilities/capabilities.render.ts +3 -2
  8. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  9. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  10. package/src/capabilities/capabilities/catalog.ts +20 -8
  11. package/src/capabilities/capabilities/hints.ts +21 -36
  12. package/src/capabilities/capabilities/types.ts +2 -0
  13. package/src/capabilities/colophon/collectColophon.ts +34 -19
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +11 -4
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/types.ts +16 -7
  20. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  21. package/src/capabilities/config/collectConfigShow.ts +41 -4
  22. package/src/capabilities/config/fields.ts +10 -108
  23. package/src/capabilities/config/set.test.ts +6 -6
  24. package/src/capabilities/config/set.verb.ts +12 -8
  25. package/src/capabilities/config/show.render.test.ts +137 -0
  26. package/src/capabilities/config/show.render.ts +57 -16
  27. package/src/capabilities/config/show.verb.ts +9 -8
  28. package/src/capabilities/config/types.ts +8 -0
  29. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/byteEquality.test.ts +82 -101
  31. package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
  32. package/src/capabilities/create/constants.ts +75 -0
  33. package/src/capabilities/create/create.mcp.test.ts +2 -2
  34. package/src/capabilities/create/create.test.ts +192 -16
  35. package/src/capabilities/create/create.verb.ts +208 -79
  36. package/src/capabilities/create/index.ts +2 -10
  37. package/src/capabilities/create/pickGenerator.ts +34 -28
  38. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  39. package/src/capabilities/create/types.ts +10 -6
  40. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  41. package/src/capabilities/distribution.test.ts +120 -0
  42. package/src/capabilities/distribution.ts +59 -0
  43. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  44. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  45. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  46. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
  47. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  48. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  49. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  50. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  51. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  52. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  53. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  54. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  55. package/src/capabilities/doctor/doctor.render.ts +117 -34
  56. package/src/capabilities/doctor/doctor.test.ts +68 -9
  57. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  58. package/src/capabilities/doctor/runChecks.ts +33 -9
  59. package/src/capabilities/doctor/types.ts +16 -1
  60. package/src/capabilities/emptyRecovery.test.ts +32 -15
  61. package/src/capabilities/graph/inspect.verb.ts +3 -2
  62. package/src/capabilities/graph/query.verb.ts +3 -2
  63. package/src/capabilities/graph/runQuery.ts +8 -5
  64. package/src/capabilities/index.ts +22 -18
  65. package/src/capabilities/info/collectInfo.ts +10 -6
  66. package/src/capabilities/info/info.render.ts +3 -2
  67. package/src/capabilities/info/info.verb.ts +3 -2
  68. package/src/capabilities/lazy.test.ts +93 -2
  69. package/src/capabilities/ontology/index.ts +10 -3
  70. package/src/capabilities/ontology/ontology.test.ts +157 -5
  71. package/src/capabilities/ontology/verbs.ts +203 -104
  72. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  73. package/src/capabilities/prompt/prompt.render.ts +23 -6
  74. package/src/capabilities/prompt/prompt.test.ts +68 -1
  75. package/src/capabilities/prompt/verbs.ts +24 -11
  76. package/src/capabilities/reference.test.ts +32 -0
  77. package/src/capabilities/resources/provider.ts +58 -25
  78. package/src/capabilities/resources/resources.test.ts +176 -21
  79. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  80. package/src/capabilities/setup/index.ts +7 -1
  81. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  82. package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
  83. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  84. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  85. package/src/capabilities/setup/operations/setupSkills.ts +10 -4
  86. package/src/capabilities/setup/setup.render.test.ts +109 -0
  87. package/src/capabilities/setup/setup.render.ts +61 -14
  88. package/src/capabilities/setup/setup.test.ts +352 -1
  89. package/src/capabilities/setup/setup.verb.ts +68 -9
  90. package/src/capabilities/setup/shell.ts +29 -3
  91. package/src/capabilities/setup/types.ts +59 -2
  92. package/src/capabilities/shared/bands.ts +16 -0
  93. package/src/capabilities/skill/discover.ts +11 -5
  94. package/src/capabilities/skill/render.ts +4 -5
  95. package/src/capabilities/skill/verbs.ts +7 -7
  96. package/src/capabilities/sources/collectStatus.ts +69 -49
  97. package/src/capabilities/sources/index.ts +1 -1
  98. package/src/capabilities/sources/installSkills.ts +2 -2
  99. package/src/capabilities/sources/runUpdate.ts +146 -98
  100. package/src/capabilities/sources/sources.test.ts +264 -84
  101. package/src/capabilities/sources/status.render.test.ts +94 -0
  102. package/src/capabilities/sources/status.render.ts +55 -33
  103. package/src/capabilities/sources/status.verb.ts +15 -8
  104. package/src/capabilities/sources/types.ts +18 -25
  105. package/src/capabilities/sources/update.render.ts +9 -2
  106. package/src/capabilities/sources/update.verb.ts +12 -23
  107. package/src/capabilities/surface.test.ts +65 -17
  108. package/src/capabilities/tier.parity.test.ts +144 -0
  109. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  110. package/src/constants.test.ts +9 -7
  111. package/src/constants.ts +49 -23
  112. package/src/identity.test.ts +333 -0
  113. package/src/index.ts +2 -2
  114. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
  115. package/src/kernel/completion/complete.test.ts +3 -5
  116. package/src/kernel/completion/emitScripts.test.ts +1 -1
  117. package/src/kernel/completion/emitScripts.ts +4 -2
  118. package/src/kernel/completion/entitySource.test.ts +185 -41
  119. package/src/kernel/completion/entitySource.ts +131 -113
  120. package/src/kernel/completion/model.test.ts +3 -3
  121. package/src/kernel/completion/model.ts +11 -1
  122. package/src/kernel/completion/parse.test.ts +2 -4
  123. package/src/kernel/completion/safety.test.ts +82 -36
  124. package/src/kernel/completion/shellDrive.test.ts +592 -0
  125. package/src/kernel/completion/templates/fish.ts +28 -6
  126. package/src/kernel/completion/types.ts +3 -3
  127. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  128. package/src/kernel/config/defaults.test.ts +85 -0
  129. package/src/kernel/config/defaults.ts +12 -23
  130. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  131. package/src/kernel/config/findProjectConfig.ts +4 -3
  132. package/src/kernel/config/firstRun.ts +12 -5
  133. package/src/kernel/config/globalConfig.ts +2 -2
  134. package/src/kernel/config/index.ts +4 -5
  135. package/src/kernel/config/paths.ts +17 -17
  136. package/src/kernel/config/readConfig.test.ts +242 -6
  137. package/src/kernel/config/readConfig.ts +8 -8
  138. package/src/kernel/config/schema.test.ts +50 -0
  139. package/src/kernel/config/schema.ts +97 -10
  140. package/src/kernel/config/types.ts +73 -19
  141. package/src/kernel/copy.test.ts +453 -0
  142. package/src/kernel/error/PragmaError.test.ts +5 -5
  143. package/src/kernel/error/constants.ts +3 -2
  144. package/src/kernel/error/index.ts +1 -1
  145. package/src/kernel/error/recovery.test.ts +22 -45
  146. package/src/kernel/error/recovery.ts +22 -27
  147. package/src/kernel/error/renderError.ts +1 -1
  148. package/src/kernel/error/types.ts +5 -4
  149. package/src/kernel/packs/collect.test.ts +195 -11
  150. package/src/kernel/packs/collect.ts +213 -58
  151. package/src/kernel/packs/compile.test.ts +163 -7
  152. package/src/kernel/packs/compile.ts +15 -12
  153. package/src/kernel/packs/disclosure.test.ts +5 -2
  154. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  155. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  156. package/src/kernel/packs/iri.ts +5 -7
  157. package/src/kernel/packs/renderPack.ts +5 -3
  158. package/src/kernel/packs/resolveEntity.ts +21 -17
  159. package/src/kernel/packs/runBodies.ts +15 -14
  160. package/src/kernel/packs/schema.ts +96 -10
  161. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  162. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  163. package/src/kernel/packs/toolDescription.test.ts +10 -2
  164. package/src/kernel/packs/types.ts +39 -4
  165. package/src/kernel/packs/uniqueness.ts +4 -4
  166. package/src/kernel/project/cli/buildProgram.test.ts +60 -11
  167. package/src/kernel/project/cli/buildProgram.ts +11 -0
  168. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  169. package/src/kernel/project/cli/dispatch.ts +34 -12
  170. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  171. package/src/kernel/project/cli/globalFlags.ts +14 -14
  172. package/src/kernel/project/cli/helpFormat.ts +1 -1
  173. package/src/kernel/project/cli/rootHelp.ts +109 -107
  174. package/src/kernel/project/mcp/buildServer.ts +11 -6
  175. package/src/kernel/project/mcp/instructions.ts +12 -7
  176. package/src/kernel/project/mcp/mcpError.ts +52 -0
  177. package/src/kernel/project/mcp/parity.test.ts +39 -1
  178. package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
  179. package/src/kernel/project/mcp/prompts/provider.ts +51 -7
  180. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  181. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  182. package/src/kernel/project/mcp/serve.ts +11 -3
  183. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  184. package/src/kernel/render/disclosure.ts +4 -2
  185. package/src/kernel/render/formatters.ts +5 -4
  186. package/src/kernel/render/prefixes.test.ts +36 -0
  187. package/src/kernel/render/prefixes.ts +40 -13
  188. package/src/kernel/render/renderers.test.ts +35 -1
  189. package/src/kernel/render/renderers.ts +25 -7
  190. package/src/kernel/render/style.test.ts +31 -0
  191. package/src/kernel/render/style.ts +74 -0
  192. package/src/kernel/runtime/graphpack/build.ts +57 -3
  193. package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
  194. package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
  195. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
  196. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
  197. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  198. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  199. package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
  200. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  201. package/src/kernel/runtime/graphpack/index.ts +1 -1
  202. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  203. package/src/kernel/runtime/graphpack/read.ts +32 -22
  204. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  205. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  206. package/src/kernel/runtime/graphpack/types.ts +72 -45
  207. package/src/kernel/runtime/loadSession.ts +4 -12
  208. package/src/kernel/runtime/paths.ts +64 -15
  209. package/src/kernel/runtime/probe.test.ts +8 -4
  210. package/src/kernel/runtime/readEntity.ts +6 -5
  211. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  212. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  213. package/src/kernel/runtime/refs/resolve.ts +141 -65
  214. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  215. package/src/kernel/runtime/resolveSources.ts +36 -25
  216. package/src/kernel/runtime/storeReadiness.ts +62 -0
  217. package/src/kernel/runtime/storeless.test.ts +1 -1
  218. package/src/kernel/runtime/types.ts +1 -1
  219. package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
  220. package/src/kernel/spec/emitReference.test.ts +196 -0
  221. package/src/kernel/spec/emitReference.ts +624 -0
  222. package/src/kernel/spec/emitSurface.ts +13 -7
  223. package/src/kernel/spec/index.ts +2 -0
  224. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  225. package/src/kernel/spec/surfaceConformance.ts +8 -0
  226. package/src/kernel/spec/types.ts +29 -9
  227. package/src/kernel/spec/validate.ts +2 -1
  228. package/src/kernel/vocabulary.test.ts +164 -0
  229. package/src/kernel/vocabulary.ts +146 -0
  230. package/src/testing/PARITY_GAPS.ts +9 -3
  231. package/src/testing/behavioral/README.md +3 -3
  232. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  233. package/src/testing/behavioral/completion.test.ts +21 -8
  234. package/src/testing/behavioral/disclosure.test.ts +12 -0
  235. package/src/testing/behavioral/docExamples.test.ts +283 -0
  236. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  237. package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
  238. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  239. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  240. package/src/testing/behavioral/journeys.cli.test.ts +116 -73
  241. package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
  242. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  243. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  244. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  245. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
  246. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  247. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  248. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  249. package/src/testing/behavioral/staleCommands.test.ts +86 -0
  250. package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
  251. package/src/testing/eval/cases/assembly.ts +16 -9
  252. package/src/testing/eval/cases/effectful.ts +31 -16
  253. package/src/testing/eval/cases/readNouns.ts +23 -18
  254. package/src/testing/eval/cases/stable.ts +5 -1
  255. package/src/testing/eval/eval.test.ts +7 -1
  256. package/src/testing/fixtures/fixtureCapability.ts +201 -1
  257. package/src/testing/fixtures/graph/canonical.ts +6 -3
  258. package/src/testing/fixtures/graph/defaultPack.ts +179 -0
  259. package/src/testing/fixtures/graph/owlExport.ts +97 -0
  260. package/src/testing/helpers/extractSnippets.ts +29 -0
  261. package/src/testing/helpers/fixtureGraph.ts +33 -16
  262. package/src/testing/helpers/golden.ts +1 -1
  263. package/src/testing/helpers/packRuntime.ts +2 -3
  264. package/src/testing/helpers/projectCli.ts +2 -1
  265. package/src/testing/helpers/projectMcp.ts +6 -0
  266. package/src/testing/perf/budgets.test.ts +4 -0
  267. package/src/testing/perf/budgets.ts +22 -7
  268. package/src/testing/perf/globalSetup.ts +121 -6
  269. package/src/types.ts +2 -2
  270. package/src/capabilities/block/blockList.verb.ts +0 -150
  271. package/src/capabilities/block/index.ts +0 -25
  272. package/src/capabilities/block/pack.ts +0 -155
  273. package/src/capabilities/block/parity.test.ts +0 -180
  274. package/src/capabilities/block/tierChain.ts +0 -78
  275. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  276. package/src/capabilities/config/field.test.ts +0 -188
  277. package/src/capabilities/modifier/index.ts +0 -15
  278. package/src/capabilities/modifier/pack.ts +0 -64
  279. package/src/capabilities/standard/index.ts +0 -18
  280. package/src/capabilities/standard/pack.ts +0 -128
  281. package/src/capabilities/tier/index.ts +0 -19
  282. package/src/capabilities/tier/lookup.render.ts +0 -34
  283. package/src/capabilities/tier/lookup.test.ts +0 -68
  284. package/src/capabilities/tier/lookup.verb.ts +0 -44
  285. package/src/capabilities/tier/pack.ts +0 -28
  286. package/src/capabilities/tier/runLookup.ts +0 -59
  287. package/src/capabilities/token/addConfig.render.ts +0 -27
  288. package/src/capabilities/token/addConfig.test.ts +0 -62
  289. package/src/capabilities/token/addConfig.verb.ts +0 -40
  290. package/src/capabilities/token/index.ts +0 -19
  291. package/src/capabilities/token/pack.ts +0 -58
  292. package/src/capabilities/token/runAddConfig.ts +0 -95
  293. package/src/kernel/completion/bashDrive.test.ts +0 -73
  294. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  295. package/src/kernel/runtime/lock.ts +0 -90
@@ -5,26 +5,38 @@
5
5
  * refinements that encode the source rule (§3): `type` XOR `types`, a
6
6
  * graphql-sourced lookup names its fragment target, SPARQL expands stay
7
7
  * single-hop, GraphQL fields reject property paths, and every disclosure `level`
8
- * names a declared canonical level.
8
+ * names a declared canonical level. Two more refinements make the definition
9
+ * COMPILABLE, not merely well-shaped: distinct compiled verb names and distinct
10
+ * filter params (see {@link refineVerbNames} / {@link refineFilterParams}).
9
11
  *
10
12
  * zod lives here and ONLY here in the pack layer, imported lazily (never on the
11
- * `--help`/`__complete` fast path). Bundled packs are authored in-repo and typed
12
- * by TypeScript, so `collect` skips validation for them (per-source skip); this
13
- * runs for config/package packs and in tests.
13
+ * `--help`/`__complete` fast path). The distribution's own stories are compiled
14
+ * statically by `capabilities/distribution.ts` and never revalidated at dispatch
15
+ * (`collect.projectStoryTiers`'s default-origin carve-out); this runs for config-
16
+ * and package-declared stories, and in `distribution.test.ts`'s round-trip.
14
17
  */
15
18
 
16
19
  import { z } from "zod";
17
- import { DETAIL_LEVELS } from "../../constants.js";
20
+ import { DETAIL_LEVELS, RECOVERY_CLI_PREFIX } from "../../constants.js";
18
21
  import { PragmaError } from "../error/PragmaError.js";
19
22
  import type { PackDefinition } from "./types.js";
20
23
 
21
24
  const NOUN_PATTERN = /^[a-z][a-z0-9-]*$/;
25
+ /** Message for {@link NOUN_PATTERN} — third-party authors never see the regex. */
26
+ const NOUN_MESSAGE = 'must be lowercase kebab-case, e.g. "design-token"';
22
27
  const FILTER_PARAM_PATTERN = /^[a-z][a-z0-9]*$/;
23
28
  const FIELD_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
24
29
  const GRAPHQL_NAME_PATTERN = /^[_A-Za-z][_0-9A-Za-z]*$/;
25
- /** A prefixed name (`ds:Thing`), a property path of them, or an absolute IRI. */
30
+ /**
31
+ * A prefixed name (`ds:Thing`), a property path of them, or an absolute IRI.
32
+ * Each path segment may carry the SPARQL inverse marker (`^ds:tier`) — the
33
+ * contract (`types.ts`) admits "prefixed name, IRI, or path", and an inverse
34
+ * step IS a property path, so rejecting it here was a schema bug, not a
35
+ * grammar decision (L-OPEN-9: the tier story's `blocks` expand reads
36
+ * `^ds:tier`).
37
+ */
26
38
  const TERM_PATTERN =
27
- /^(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/[^<>"\s]+|[A-Za-z][\w-]*:[^/<>"\s]+(?:\/[A-Za-z][\w-]*:[^/<>"\s]+)*)$/;
39
+ /^(?:[A-Za-z][A-Za-z0-9+.-]*:\/\/[^<>"\s]+|\^?[A-Za-z][\w-]*:[^/<>"\s]+(?:\/\^?[A-Za-z][\w-]*:[^/<>"\s]+)*)$/;
28
40
 
29
41
  /** Params a filter/verb may not claim (they are the shared read vocabulary). */
30
42
  const RESERVED_PARAMS = new Set(["search", "detail", "name", "count"]);
@@ -59,9 +71,18 @@ const searchSchema = z
59
71
  const emptyRecoverySchema = z
60
72
  .object({
61
73
  message: z.string().min(1),
74
+ // The command WITHOUT a binary name — the renderer prepends the CONSUMING
75
+ // distribution's. A story is portable only if it does not name a binary, so
76
+ // the old prefixed form is rejected rather than accepted and doubled. Named
77
+ // loudly, in the `packages` → `packs` tradition, because a third-party pack
78
+ // written against the old grammar is data, not a typo.
62
79
  cli: z
63
80
  .string()
64
- .startsWith("pragma ", 'emptyRecovery.cli must be a "pragma " command')
81
+ .min(1)
82
+ .refine(
83
+ (value) => !value.startsWith(RECOVERY_CLI_PREFIX),
84
+ `emptyRecovery.cli is now the command WITHOUT the binary name — write "sources update", not "${RECOVERY_CLI_PREFIX}sources update"`,
85
+ )
65
86
  .optional(),
66
87
  })
67
88
  .strict();
@@ -78,7 +99,7 @@ const listSchema = z.object(listShape).strict();
78
99
  const verbSchema = z
79
100
  .object({
80
101
  ...listShape,
81
- verb: z.string().regex(NOUN_PATTERN),
102
+ verb: z.string().regex(NOUN_PATTERN, NOUN_MESSAGE),
82
103
  description: z.string().optional(),
83
104
  toolDescription: z.string().optional(),
84
105
  })
@@ -168,6 +189,7 @@ const sampleSchema = z.union([
168
189
  z
169
190
  .object({
170
191
  count: z.number().int().min(1).max(5).optional(),
192
+ fixedCount: z.boolean().optional(),
171
193
  description: z.string().optional(),
172
194
  toolDescription: z.string().optional(),
173
195
  })
@@ -202,7 +224,7 @@ const lookupSchema = z
202
224
 
203
225
  const definitionSchema = z
204
226
  .object({
205
- noun: z.string().regex(NOUN_PATTERN),
227
+ noun: z.string().regex(NOUN_PATTERN, NOUN_MESSAGE),
206
228
  description: z.string().optional(),
207
229
  toolDescription: z.string().optional(),
208
230
  list: listSchema.optional(),
@@ -228,9 +250,73 @@ const definitionSchema = z
228
250
  path: ["list", "query"],
229
251
  });
230
252
  }
253
+ refineVerbNames(def, ctx);
254
+ if (def.list) refineFilterParams(def.list.filters, ["list"], ctx);
255
+ for (const [index, verb] of (def.verbs ?? []).entries()) {
256
+ refineFilterParams(verb.filters, ["verbs", index], ctx);
257
+ }
231
258
  if (def.lookup) refineLookup(def.lookup, ctx);
232
259
  });
233
260
 
261
+ /**
262
+ * The compiled `(noun, verb)` keys must be distinct.
263
+ *
264
+ * `compilePack` emits `list`, then each `verbs[].verb`, then `lookup` and
265
+ * `sample`. A repeat there survives the grammar into `assembleEffectiveModules`,
266
+ * where `assertUniqueVerbs` throws — and package stories reach dispatch before
267
+ * the command tree exists, so that throw would fail EVERY command, `doctor` and
268
+ * `sources update` included. The collision is a property of the definition, so
269
+ * the grammar is where it is caught.
270
+ */
271
+ function refineVerbNames(
272
+ def: {
273
+ list?: unknown;
274
+ verbs?: readonly { verb: string }[];
275
+ lookup?: unknown;
276
+ },
277
+ ctx: z.RefinementCtx,
278
+ ): void {
279
+ const emitted = new Set<string>(def.list ? ["list"] : []);
280
+ if (def.lookup) {
281
+ emitted.add("lookup");
282
+ if ((def.lookup as { sample?: unknown }).sample) emitted.add("sample");
283
+ }
284
+ const seen = new Set<string>();
285
+ for (const [index, verb] of (def.verbs ?? []).entries()) {
286
+ if (emitted.has(verb.verb) || seen.has(verb.verb)) {
287
+ ctx.addIssue({
288
+ code: z.ZodIssueCode.custom,
289
+ message: `verb "${verb.verb}" is already compiled by this pack — every verb must be distinct.`,
290
+ path: ["verbs", index, "verb"],
291
+ });
292
+ }
293
+ seen.add(verb.verb);
294
+ }
295
+ }
296
+
297
+ /**
298
+ * Filter params project to CLI options, so a repeat inside one list shape makes
299
+ * Commander refuse to register the second `--<param>` — outside every error
300
+ * envelope the CLI owns.
301
+ */
302
+ function refineFilterParams(
303
+ filters: readonly { param: string }[] | undefined,
304
+ path: readonly (string | number)[],
305
+ ctx: z.RefinementCtx,
306
+ ): void {
307
+ const seen = new Set<string>();
308
+ for (const [index, filter] of (filters ?? []).entries()) {
309
+ if (seen.has(filter.param)) {
310
+ ctx.addIssue({
311
+ code: z.ZodIssueCode.custom,
312
+ message: `filter param "${filter.param}" is declared twice.`,
313
+ path: [...path, "filters", index, "param"],
314
+ });
315
+ }
316
+ seen.add(filter.param);
317
+ }
318
+ }
319
+
234
320
  /** Cross-field lookup rules — the source rule (§3, F8). */
235
321
  function refineLookup(
236
322
  lookup: z.infer<typeof lookupSchema>,
@@ -1,18 +1,32 @@
1
1
  /**
2
2
  * `runSelect` — the single choke point for every TRUSTED, generated SPARQL read.
3
3
  *
4
- * Pins the unseeded-store remap: a generated pack query only references the
5
- * pack's own `ds:`/`cs:` prefixes, so a "Prefix not found" from the facade means
6
- * the store was never built — an actionable STORE_UNAVAILABLE, NOT a raw error
7
- * collapsing to INTERNAL_ERROR ("please report this issue"). The recovery carries
8
- * BOTH the CLI command (`pragma sources update`) and the MCP tool an agent calls
9
- * (`sources_update`), so either surface can recover.
4
+ * Pins the remap: a generated query is composed from declared and pack-authored
5
+ * terms, never from user input, so a "Prefix not found" from the facade means
6
+ * the store cannot answer this read — an actionable error, NOT a raw one
7
+ * collapsing to INTERNAL_ERROR ("please report this issue").
8
+ *
9
+ * WHICH actionable error is the story's provenance to decide, which is the
10
+ * second thing pinned here. A DISTRIBUTION story keeps STORE_UNAVAILABLE and its
11
+ * recovery, carrying BOTH the CLI command (`pragma sources update`) and the MCP
12
+ * tool an agent calls (`sources_update`), so either surface can recover. A
13
+ * CONFIG or PACKAGE story gets CONFIG_ERROR naming the story instead: its author
14
+ * named a prefix nothing binds, and `sources update` cannot conjure a term.
10
15
  */
11
16
 
12
17
  import { describe, expect, it } from "vitest";
13
18
  import type { PragmaRuntime } from "../../runtime/types.js";
19
+ import { distributionSource, type StorySource } from "../types.js";
14
20
  import { runSelect } from "./runSelect.js";
15
21
 
22
+ /** A story declared by a package (a third party's own file). */
23
+ const PACKAGE: StorySource = {
24
+ label: "@acme/recipes/stories/recipe.json",
25
+ origin: "package",
26
+ };
27
+ /** A story declared in the user's config. */
28
+ const CONFIG: StorySource = { label: "config", origin: "config" };
29
+
16
30
  /** A runtime whose query facade throws the given error for every SPARQL call. */
17
31
  function throwingRuntime(error: unknown): Pick<PragmaRuntime, "query"> {
18
32
  return {
@@ -35,14 +49,14 @@ function selectRuntime(
35
49
  };
36
50
  }
37
51
 
38
- describe("runSelect — unseeded-store remap (ROOT A)", () => {
52
+ describe("runSelect — unanswerable-store remap (ROOT A)", () => {
39
53
  it("remaps a 'Prefix not found' to STORE_UNAVAILABLE with CLI + MCP recovery", async () => {
40
54
  let caught: unknown;
41
55
  try {
42
56
  await runSelect(
43
57
  throwingRuntime(new Error("Prefix not found: ds")),
44
58
  "SELECT ?x WHERE { ?x a ds:Thing }",
45
- "block",
59
+ distributionSource("block"),
46
60
  );
47
61
  } catch (error) {
48
62
  caught = error;
@@ -50,6 +64,12 @@ describe("runSelect — unseeded-store remap (ROOT A)", () => {
50
64
 
51
65
  // The whole point: it is NOT an unclassified INTERNAL_ERROR.
52
66
  expect(caught).toMatchObject({ code: "STORE_UNAVAILABLE" });
67
+ // …and it claims only what it knows. The same failure reaches a store that
68
+ // IS built, from a pack whose vocabulary is simply different, so the
69
+ // message must not tell that user their store is unbuilt.
70
+ expect((caught as { message: string }).message).toContain(
71
+ "not built from a pack that defines every term",
72
+ );
53
73
  const recovery = (caught as { recovery?: Record<string, unknown> })
54
74
  .recovery;
55
75
  expect(recovery?.cli).toBe("pragma sources update");
@@ -61,7 +81,11 @@ describe("runSelect — unseeded-store remap (ROOT A)", () => {
61
81
  const boom = new Error("some other engine failure");
62
82
  let caught: unknown;
63
83
  try {
64
- await runSelect(throwingRuntime(boom), "SELECT ?x WHERE {}", "block");
84
+ await runSelect(
85
+ throwingRuntime(boom),
86
+ "SELECT ?x WHERE {}",
87
+ distributionSource("block"),
88
+ );
65
89
  } catch (error) {
66
90
  caught = error;
67
91
  }
@@ -73,8 +97,78 @@ describe("runSelect — unseeded-store remap (ROOT A)", () => {
73
97
  const rows = await runSelect(
74
98
  selectRuntime([{ name: "Button" }, { name: "Card" }]),
75
99
  "SELECT ?name WHERE { ?x ds:name ?name }",
76
- "block",
100
+ distributionSource("block"),
77
101
  );
78
102
  expect(rows).toEqual([{ name: "Button" }, { name: "Card" }]);
79
103
  });
80
104
  });
105
+
106
+ describe("runSelect — an unbound prefix is diagnosed by PROVENANCE", () => {
107
+ const unbound = (): Error => new Error("Prefix not found: acme");
108
+
109
+ for (const source of [PACKAGE, CONFIG]) {
110
+ it(`a ${source.origin} story gets CONFIG_ERROR naming the story`, async () => {
111
+ // `sources update` cannot help an author who named a prefix nothing
112
+ // binds — no amount of building conjures a term. Telling them to run it
113
+ // (as this path used to) sends them round a loop that cannot terminate.
114
+ // `buildIndex` already treats the same condition as the ordinary
115
+ // third-party case; the query path used to escalate it.
116
+ let caught: unknown;
117
+ try {
118
+ await runSelect(
119
+ throwingRuntime(unbound()),
120
+ "SELECT ?x WHERE { ?x a acme:Thing }",
121
+ source,
122
+ );
123
+ } catch (error) {
124
+ caught = error;
125
+ }
126
+ expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
127
+ const { message } = caught as { message: string };
128
+ // It names the story, so the author knows which file to fix.
129
+ expect(message).toContain(source.label);
130
+ expect(message).toContain("does not bind");
131
+ // And it does NOT send them to the lever that cannot help.
132
+ expect(message).not.toContain("sources update");
133
+ expect(
134
+ (caught as { recovery?: { cli?: string } }).recovery?.cli,
135
+ ).toBeUndefined();
136
+ });
137
+ }
138
+
139
+ it("the distribution's own story keeps STORE_UNAVAILABLE", async () => {
140
+ // Unchanged, and the reason is real: the distribution's stories ship with
141
+ // the packs that bind their terms, so an unbound prefix there means nothing
142
+ // is built yet — which `sources update` does fix.
143
+ let caught: unknown;
144
+ try {
145
+ await runSelect(
146
+ throwingRuntime(unbound()),
147
+ "SELECT ?x WHERE { ?x a ds:Thing }",
148
+ distributionSource("pragma.conf.ts"),
149
+ );
150
+ } catch (error) {
151
+ caught = error;
152
+ }
153
+ expect(caught).toMatchObject({ code: "STORE_UNAVAILABLE" });
154
+ expect((caught as { recovery?: { cli?: string } }).recovery?.cli).toBe(
155
+ "pragma sources update",
156
+ );
157
+ });
158
+
159
+ it("a non-SELECT still names the story on every origin", async () => {
160
+ const askRuntime = {
161
+ query: {
162
+ sparql: async () => ({ type: "ask", boolean: true }),
163
+ } as unknown as PragmaRuntime["query"],
164
+ };
165
+ for (const source of [PACKAGE, CONFIG, distributionSource("block")]) {
166
+ await expect(
167
+ runSelect(askRuntime, "ASK { ?x ?y ?z }", source),
168
+ ).rejects.toMatchObject({
169
+ code: "CONFIG_ERROR",
170
+ message: expect.stringContaining(source.label),
171
+ });
172
+ }
173
+ });
174
+ });
@@ -10,18 +10,28 @@
10
10
  * (`graph query`) goes through the facade directly and stays INVALID_INPUT.
11
11
  */
12
12
 
13
- import { RECOVERY_CLI_PREFIX } from "../../../constants.js";
14
13
  import { PragmaError } from "../../error/PragmaError.js";
15
14
  import { cliRecovery } from "../../error/recovery.js";
16
15
  import type { PragmaRuntime } from "../../runtime/types.js";
17
- import type { PackRow } from "../types.js";
16
+ import type { PackRow, StorySource } from "../types.js";
18
17
 
19
18
  /**
20
19
  * True when a trusted, generated query failed because the store does not know a
21
- * prefix it used. A generated pack query only references the pack's own
22
- * ontology prefixes (`ds:`/`cs:`), so a "prefix not found" means the store is
23
- * unseeded — the fresh-install embedded fallback carries no design-system
24
- * ontology — rather than a query bug.
20
+ * prefix it used. Generated queries are composed from terms the distribution
21
+ * declares and from a pack's own story text, never from user input — so an
22
+ * unknown prefix is never a query bug. It means this store was not built from a
23
+ * pack that binds that term: most often nothing was built at all, and otherwise
24
+ * a store built from a pack whose vocabulary is simply different.
25
+ *
26
+ * For a DISTRIBUTION story both are answered by the same lever
27
+ * (`sources update`), so both are remapped to it; the message says which claim
28
+ * it is actually making, and does NOT assert the store is unbuilt. Widening the
29
+ * detection any further would start hiding query bugs, which is the failure this
30
+ * whole choke point exists to prevent.
31
+ *
32
+ * A story from a package or a user's config is a different diagnosis, which is
33
+ * why {@link runSelect} now takes the query's provenance: see
34
+ * {@link unboundPrefixError}.
25
35
  */
26
36
  function isUnseededStoreError(error: unknown): boolean {
27
37
  const message = error instanceof Error ? error.message : String(error);
@@ -29,33 +39,60 @@ function isUnseededStoreError(error: unknown): boolean {
29
39
  }
30
40
 
31
41
  /**
32
- * Run the facade query, remapping an unseeded-store failure. A generated pack
33
- * query hitting an unknown prefix means the store was never built for this
34
- * design system (the first thing a fresh install hits) — surface the actionable
35
- * STORE_UNAVAILABLE with the canonical `pragma sources update` recovery instead
36
- * of a raw SPARQL "Prefix not found" wrapped as INTERNAL_ERROR ("please report
37
- * this issue"). Returns the inferred facade result type, so this module stays
38
- * clear of a static `@canonical/ke` import (the lazy-dispatch guard).
42
+ * The error for a generated query naming a prefix the graph does not bind,
43
+ * diagnosed by WHO declared the story.
44
+ *
45
+ * A distribution story ships with the packs that bind its terms, so the honest
46
+ * reading is "nothing is built yet" (or "what is built came from a pack with a
47
+ * different vocabulary") — both fixed by `sources update`, so it keeps today's
48
+ * STORE_UNAVAILABLE and that recovery.
49
+ *
50
+ * A config or package story is the author's own: they named a prefix the graph
51
+ * does not bind, and `sources update` cannot help them — `buildIndex` already
52
+ * treats the same condition as the ordinary third-party case and degrades
53
+ * gracefully, while this path escalated it. It is a CONFIG_ERROR naming the
54
+ * story, which is the module's own precedent for a declaration that cannot be
55
+ * served (ruling R3).
56
+ */
57
+ function unboundPrefixError(source: StorySource): PragmaError {
58
+ if (source.origin !== "distribution") {
59
+ return PragmaError.configError(
60
+ `Story query in ${source.label} uses a prefix the graph does not bind. ` +
61
+ `Declare it under \`prefixes\`, or use a term the configured packs define.`,
62
+ );
63
+ }
64
+ return PragmaError.storeUnavailable(
65
+ "The local store was not built from a pack that defines every term this read uses.",
66
+ {
67
+ recovery: cliRecovery(
68
+ "sources update",
69
+ "Build the local store from the configured packs.",
70
+ // An agent recovers by calling the tool, then retrying (PR9 C1 cold-
71
+ // store retry makes the post-update retry succeed).
72
+ { tool: "sources_update" },
73
+ ),
74
+ },
75
+ );
76
+ }
77
+
78
+ /**
79
+ * Run the facade query, remapping a store that cannot answer it. A generated
80
+ * query hitting an unknown prefix surfaces an actionable error — chosen by the
81
+ * story's provenance, see {@link unboundPrefixError} — instead of a raw SPARQL
82
+ * "Prefix not found" wrapped as INTERNAL_ERROR ("please report this issue").
83
+ * Returns the inferred facade result type, so this module stays clear of a
84
+ * static `@canonical/ke` import (the lazy-dispatch guard).
39
85
  */
40
- async function queryOrRemap(rt: Pick<PragmaRuntime, "query">, query: string) {
86
+ async function queryOrRemap(
87
+ rt: Pick<PragmaRuntime, "query">,
88
+ query: string,
89
+ source: StorySource,
90
+ ) {
41
91
  try {
42
92
  return await rt.query.sparql(query);
43
93
  } catch (error) {
44
94
  if (error instanceof PragmaError) throw error;
45
- if (isUnseededStoreError(error)) {
46
- throw PragmaError.storeUnavailable(
47
- "The store has not been built for this project yet.",
48
- {
49
- recovery: cliRecovery(
50
- `${RECOVERY_CLI_PREFIX}sources update`,
51
- "Build the local store from the configured design-system packages.",
52
- // An agent recovers by calling the tool, then retrying (PR9 C1 cold-
53
- // store retry makes the post-update retry succeed).
54
- { tool: "sources_update" },
55
- ),
56
- },
57
- );
58
- }
95
+ if (isUnseededStoreError(error)) throw unboundPrefixError(source);
59
96
  throw error;
60
97
  }
61
98
  }
@@ -63,21 +100,23 @@ async function queryOrRemap(rt: Pick<PragmaRuntime, "query">, query: string) {
63
100
  /**
64
101
  * @param rt - The runtime (its query facade over the lazy store).
65
102
  * @param query - SPARQL SELECT text (prefixes auto-applied by the store).
66
- * @param source - The pack source, for error attribution.
103
+ * @param source - The story's provenance: its label for attribution, and the
104
+ * layer that declared it, which decides how an unbound prefix is diagnosed.
67
105
  * @returns One record per row, keyed by SELECT variable name.
68
- * @throws PragmaError CONFIG_ERROR when the query is not a SELECT;
69
- * STORE_UNAVAILABLE (exit 3) when the store is unseeded.
106
+ * @throws PragmaError CONFIG_ERROR when the query is not a SELECT, or when a
107
+ * config/package story names a prefix the graph does not bind;
108
+ * STORE_UNAVAILABLE (exit 3) when a distribution story finds one.
70
109
  * @note Impure — queries the store through the facade.
71
110
  */
72
111
  export async function runSelect(
73
112
  rt: Pick<PragmaRuntime, "query">,
74
113
  query: string,
75
- source: string,
114
+ source: StorySource,
76
115
  ): Promise<PackRow[]> {
77
- const result = await queryOrRemap(rt, query);
116
+ const result = await queryOrRemap(rt, query, source);
78
117
  if (result.type !== "select") {
79
118
  throw PragmaError.configError(
80
- `Story query in ${source} must be a SELECT (got ${result.type}).`,
119
+ `Story query in ${source.label} must be a SELECT (got ${result.type}).`,
81
120
  );
82
121
  }
83
122
  return result.bindings as PackRow[];
@@ -13,12 +13,20 @@
13
13
  */
14
14
 
15
15
  import { describe, expect, it } from "vitest";
16
- import { standardModule } from "../../capabilities/standard/index.js";
17
- import { tokenModule } from "../../capabilities/token/index.js";
16
+ import { storyModules } from "../../capabilities/distribution.js";
18
17
  import { projectMcp } from "../../testing/helpers/projectMcp.js";
19
18
  import { formatVerbHelp } from "../project/cli/verbHelp.js";
20
19
  import type { VerbSpec } from "../spec/types.js";
21
20
 
21
+ const standardModule = storyModules.get("standard");
22
+ if (!standardModule) {
23
+ throw new Error('pragma.conf.ts declares no story for "standard"');
24
+ }
25
+ const tokenModule = storyModules.get("token");
26
+ if (!tokenModule) {
27
+ throw new Error('pragma.conf.ts declares no story for "token"');
28
+ }
29
+
22
30
  const verb = (
23
31
  module: { verbs: readonly VerbSpec[] },
24
32
  label: string,
@@ -170,7 +170,11 @@ export interface PackSearch {
170
170
  export interface PackEmptyRecovery {
171
171
  /** Human-readable cause + fix (e.g. which packages provide the data). */
172
172
  readonly message: string;
173
- /** Runnable pragma command fixing the emptiness (rendered on the CLI). */
173
+ /**
174
+ * The command that fixes the emptiness, WITHOUT the binary name
175
+ * (`sources update`). The consuming distribution's renderer prepends its own
176
+ * name, so a story stays portable across distributions.
177
+ */
174
178
  readonly cli?: string;
175
179
  }
176
180
 
@@ -312,8 +316,8 @@ export interface PackDefinition {
312
316
  /** MCP tool description (defaults to the CLI description). */
313
317
  readonly toolDescription?: string;
314
318
  /**
315
- * The list story. Optional so a pack can serve only the lookup verb of a noun
316
- * whose list stays hand-written (e.g. `block`). At least one of `list`/`lookup`
319
+ * The list story. Optional so a pack can serve a noun that is addressable but
320
+ * not enumerable — a lookup with no list. At least one of `list`/`lookup`
317
321
  * must be declared.
318
322
  */
319
323
  readonly list?: PackList;
@@ -331,7 +335,38 @@ export interface PackDefinition {
331
335
 
332
336
  /** A validated pack definition paired with where it was declared. */
333
337
  export interface PackEntry {
334
- /** Where the definition was declared (config path, package file, or bundled). */
338
+ /** The package story file it came from, e.g. `@acme/recipes/stories/recipe.json`. */
335
339
  readonly source: string;
336
340
  readonly definition: PackDefinition;
337
341
  }
342
+
343
+ /**
344
+ * Which layer declared a story. The label alone cannot answer this — a label is
345
+ * free text for a human — and the difference changes what a failure MEANS.
346
+ *
347
+ * A generated query that names a prefix the graph does not bind is, from a
348
+ * `distribution` story, almost always an unbuilt store: the distribution's own
349
+ * stories and its packs ship together, so the terms exist as soon as anything is
350
+ * built. From a `config` or `package` story it is the opposite: the author named
351
+ * a prefix nothing binds, and no amount of building will conjure it. One is
352
+ * STORE_UNAVAILABLE with a `sources update` recovery, the other a CONFIG_ERROR
353
+ * naming the story — and telling them apart needs this at the failure site.
354
+ */
355
+ export type StoryOrigin = "distribution" | "config" | "package";
356
+
357
+ /**
358
+ * A story's provenance, threaded from where it was declared down to the query
359
+ * that runs on its behalf.
360
+ */
361
+ export interface StorySource {
362
+ /** Human-readable location, used verbatim in diagnostics. */
363
+ readonly label: string;
364
+ /** The layer that declared it. */
365
+ readonly origin: StoryOrigin;
366
+ }
367
+
368
+ /** A story the distribution itself declares (`pragma.conf.ts` and its kin). */
369
+ export const distributionSource = (label: string): StorySource => ({
370
+ label,
371
+ origin: "distribution",
372
+ });
@@ -2,10 +2,10 @@
2
2
  * Plain `(noun, verb)` uniqueness across compiled verbs.
3
3
  *
4
4
  * Replaces the old reserved-verb / derive-reserved machinery: every verb the
5
- * projectors see — authored, bundled-pack, or dynamic-pack — must own a distinct
6
- * `(noun, verb)` key. Precedence (config > package > bundled) is resolved by
7
- * NOUN before this runs, so a surviving collision here is a real conflict (a
8
- * dynamic pack claiming an authored noun's verb), reported with both sources.
5
+ * projectors see — authored, distribution-declared, or dynamic — must own a
6
+ * distinct `(noun, verb)` key. Precedence (config > package > distribution) is
7
+ * resolved by NOUN before this runs, so a surviving collision here is a real
8
+ * conflict, reported with both sources.
9
9
  */
10
10
 
11
11
  import type { VerbSpec } from "../spec/types.js";