@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
@@ -24,29 +24,20 @@ import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
24
24
  import { tmpdir } from "node:os";
25
25
  import { dirname, join, resolve } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
- import { beforeAll, describe, expect, it } from "vitest";
27
+ import { describe, expect, it } from "vitest";
28
28
 
29
29
  const here = dirname(fileURLToPath(import.meta.url));
30
30
  const repoRoot = resolve(here, "../../../../../..");
31
- const cliNextDir = join(repoRoot, "packages/cli/next");
31
+ const cliNextDir = join(repoRoot, "packages/cli/pragma");
32
32
  const pragmaBin = join(cliNextDir, "src/bin.ts");
33
33
  const compiledBin = join(cliNextDir, "dist/pragma");
34
34
  const freshCwd = (): string => mkdtempSync(join(tmpdir(), "pragma-compiled-"));
35
35
 
36
- // Build the standalone binary ONCE for every describe in this file (create +
37
- // the READ smoke share it), so the tests always exercise the current bundle +
38
- // embedded manifest rather than a stale `dist/pragma`.
39
- beforeAll(() => {
40
- const result = spawnSync("bun", ["run", "scripts/build.ts"], {
41
- cwd: cliNextDir,
42
- stdio: "pipe",
43
- });
44
- if (result.status !== 0) {
45
- throw new Error(
46
- `failed to build dist/pragma:\n${result.stderr?.toString() ?? ""}`,
47
- );
48
- }
49
- }, 180_000);
36
+ // The binary is provisioned by `testing/perf/globalSetup.ts`, which rebuilds it
37
+ // whenever it is missing or older than `src/**`, `scripts/**`, `pragma.conf.ts`
38
+ // or `package.json` — so these tests exercise the current bundle + embedded
39
+ // manifest without a second `beforeAll` writing `dist/pragma` in place while
40
+ // another worker's test is spawning it.
50
41
 
51
42
  /** Read a directory tree into a sorted map of relative path → contents. */
52
43
  function snapshot(dir: string): Map<string, string> {
@@ -108,6 +99,52 @@ describe("compiled pragma create component (PROTECTED)", () => {
108
99
  }
109
100
  });
110
101
 
102
+ /**
103
+ * PROTECTED — the compiled-binary `create` gate.
104
+ *
105
+ * `create component` runs from the binary (the describe above proves it byte for
106
+ * byte) because summon-component routes every template read through
107
+ * `loadTemplateSync` and passes `content:` into `template()`. `create package` /
108
+ * `create application` do NOT: they call `template({ source })`, summon-core
109
+ * falls through to `readFile(options.source)`, and the run dies with `ENOENT …
110
+ * /$bunfs/templates/package.json.ejs` AFTER `mkdir` has already run — a
111
+ * half-made package left on the user's disk. Measured against a real
112
+ * `dist/pragma` with the gate lifted, for both nouns.
113
+ *
114
+ * A `--dry-run` does NOT test this: it exits 0 without reading a template and
115
+ * merely PRINTS `Read file: /$bunfs/templates/package.json.ejs` as a planned
116
+ * effect. That false positive is why the gate has to be pinned by a real run.
117
+ *
118
+ * The `readdirSync(dir)` assertion is the load-bearing one: it fails with the
119
+ * real symptom if the gate is ever lifted without fixing the generators.
120
+ */
121
+ describe("compiled pragma create gate (PROTECTED)", () => {
122
+ // A LITERAL noun list, deliberately not derived from `create`'s own
123
+ // declaration: changing the gate's input must turn this red rather than
124
+ // silently drop the case.
125
+ for (const kind of ["package", "application"] as const) {
126
+ it(`refuses \`create ${kind}\` and leaves the cwd untouched`, () => {
127
+ const dir = freshCwd();
128
+ const result = spawnSync(compiledBin, ["create", kind, "--yes"], {
129
+ cwd: dir,
130
+ stdio: "pipe",
131
+ encoding: "utf-8",
132
+ });
133
+ const output = `${result.stdout ?? ""}${result.stderr ?? ""}`;
134
+ // 1, not merely non-zero: UNSUPPORTED is a runtime refusal, which
135
+ // `kernel/error/constants.ts` maps to the generic runtime exit.
136
+ expect(result.status).toBe(1);
137
+ expect(output).toContain("not available in the compiled pragma binary");
138
+ // The clean refusal, not the crash it exists to prevent.
139
+ expect(output).not.toMatch(/ENOENT/);
140
+ expect(output).not.toMatch(/Internal error/);
141
+ // And nothing half-made on disk: the generator's `mkdir` effects run
142
+ // BEFORE its first template read, so a lifted gate leaves a stub tree.
143
+ expect(readdirSync(dir)).toEqual([]);
144
+ }, 30_000);
145
+ }
146
+ });
147
+
111
148
  /**
112
149
  * PROTECTED — the compiled-binary READ guard (U1-orig).
113
150
  *
@@ -0,0 +1,75 @@
1
+ /**
2
+ * The generator bindings the `create` surface exposes — the one list the
3
+ * surface's mechanical copies derive from: the `CreateKind` union (`types.ts`),
4
+ * the `--framework` enum and the compiled-binary gate (`create.verb.ts`), the
5
+ * generator lookup (`pickGenerator.ts`), and the template roots the bundler
6
+ * embeds (`scripts/build.ts`). Prose is NOT derived: the verb summaries below
7
+ * `createVerbs` and `capabilities/hints.ts` still name the frameworks by hand.
8
+ *
9
+ * This table is the SINGLE authoring point for which generator packages the
10
+ * distribution ships: it binds each `create` noun to the generator-map key it
11
+ * runs, and — for the one generator whose templates the binary carries — to
12
+ * the package `scripts/build.ts` harvests them from. (A parallel `generators`
13
+ * CONFIG field once declared the same packages; it was validated, layered and
14
+ * read by nothing, and was removed under the L-OPEN-1 ruling — the config
15
+ * validator now rejects it loudly.) The binding is hand-written because
16
+ * neither half can be discovered:
17
+ * - surfacing a noun also needs a hand-written prompt mirror, path param and
18
+ * examples in `create.verb.ts`, so the surface is a deliberate SUBSET —
19
+ * `@canonical/summon-application` ships `application/react`, `domain`, `route`
20
+ * and `wrapper`, and `create` exposes one of them;
21
+ * - `bun build --compile` bundles only statically analysable import specifiers,
22
+ * so a shipped binary can never `import(name)` a declared package (measured:
23
+ * `Cannot find module '@canonical/summon-component' from '/$bunfs/root/…'`).
24
+ * `pickGenerator` must import all three statically.
25
+ *
26
+ * `create.test.ts` pins what is checkable: every binding resolves to the
27
+ * generator it names, and the embedded manifest carries only the templates of
28
+ * the binding that reads through it.
29
+ *
30
+ * Deliberately free of any import: `create.verb.ts` reads this on the `--help` /
31
+ * `__complete` fast path (`capabilities/lazy.test.ts`).
32
+ */
33
+ export const CREATE_GENERATORS = {
34
+ component: {
35
+ /**
36
+ * The declaring package: `scripts/build.ts` harvests its `.ejs` for the
37
+ * binary.
38
+ */
39
+ name: "@canonical/summon-component",
40
+ /** `--framework <f>` runs `component/<f>`; the FIRST is the enum default. */
41
+ frameworks: ["react", "svelte", "lit"],
42
+ /**
43
+ * Runs from the compiled binary: this generator routes every template read
44
+ * through `loadTemplateSync` and passes `content:` into `template()`, so the
45
+ * embedded manifest serves it. That — not the templates' file extensions —
46
+ * is the property that decides it, and it is a fact about the GENERATOR's
47
+ * source, not about what this build chooses to embed.
48
+ */
49
+ readsEmbeddedTemplates: true,
50
+ },
51
+ package: {
52
+ /** The generator-map key `create package` runs. */
53
+ key: "package",
54
+ /**
55
+ * NOT runnable from the compiled binary: this generator calls
56
+ * `template({ source })` with no `content:`, so summon-core falls through to
57
+ * `readFile(options.source)`. Measured against a real `dist/pragma` with the
58
+ * gate lifted: `ENOENT: no such file or directory, open
59
+ * '/$bunfs/templates/package.json.ejs'`, after `mkdir` had already created
60
+ * `my-lib/` and `my-lib/src/` — a half-made package left on disk. A
61
+ * `--dry-run` exits 0 without reading a template, so it does NOT test this.
62
+ */
63
+ readsEmbeddedTemplates: false,
64
+ },
65
+ application: {
66
+ /** The generator-map key `create application` runs. */
67
+ key: "application/react",
68
+ /**
69
+ * NOT runnable from the compiled binary — same cause as `package` (measured:
70
+ * `ENOENT … '/$bunfs/root/templates/package.json.ejs'`, after the app
71
+ * directory was created). It also `copy()`s non-`.ejs` assets.
72
+ */
73
+ readsEmbeddedTemplates: false,
74
+ },
75
+ } as const;
@@ -14,7 +14,7 @@ import { bootRuntime } from "../../kernel/runtime/boot.js";
14
14
  import type { GlobalFlags } from "../../kernel/runtime/types.js";
15
15
  import type { VerbSpec } from "../../kernel/spec/types.js";
16
16
  import { projectMcp } from "../../testing/helpers/projectMcp.js";
17
- import { createComponentVerb } from "./create.verb.js";
17
+ import { createVerbs } from "./create.verb.js";
18
18
  import { createModule } from "./index.js";
19
19
 
20
20
  const FLAGS: GlobalFlags = {
@@ -117,7 +117,7 @@ describe("create over MCP (PROTECTED)", () => {
117
117
  const mcpPlan = (mcpResult.data as { plan: string[] }).plan;
118
118
 
119
119
  const cliOutcome = await executeVerb(
120
- createComponentVerb as VerbSpec,
120
+ createVerbs.component as VerbSpec,
121
121
  {
122
122
  framework: "react",
123
123
  componentPath: "src/components/Button",
@@ -25,13 +25,14 @@ import {
25
25
  import { bootRuntime } from "../../kernel/runtime/boot.js";
26
26
  import type { GlobalFlags } from "../../kernel/runtime/types.js";
27
27
  import type { ParamSpec } from "../../kernel/spec/types.js";
28
+ import { CREATE_GENERATORS } from "./constants.js";
28
29
  import {
29
- createApplicationVerb,
30
- createComponentVerb,
31
- createPackageVerb,
30
+ createVerbs,
31
+ INCLUDE_FLAG_ALIASES,
32
32
  isModuleNotFound,
33
33
  } from "./create.verb.js";
34
34
  import { generatorToParams } from "./generatorToVerbSpec.js";
35
+ import { createModule } from "./index.js";
35
36
 
36
37
  const FLAGS: GlobalFlags = {
37
38
  llm: false,
@@ -52,7 +53,7 @@ async function runIn(
52
53
  process.chdir(dir);
53
54
  try {
54
55
  return await executeVerb(
55
- createComponentVerb,
56
+ createVerbs.component,
56
57
  params,
57
58
  mutation,
58
59
  bootRuntime(FLAGS, dir),
@@ -85,7 +86,11 @@ describe("create — real generation + stamp", () => {
85
86
  expect(readFileSync(main, "utf-8").split("\n")[0]).toBe(
86
87
  "// Generated by @canonical/summon-component:react v0.1.0",
87
88
  );
88
- });
89
+ // Explicit timeout, like the package's other real-run gates: this drives a
90
+ // full generator run (~2.4s idle), and under a contended CI matrix the 5s
91
+ // vitest default fails it on load, not on a defect — both build-matrix
92
+ // legs timed out at 5000ms on a run whose import phase alone took 88s.
93
+ }, 60_000);
89
94
 
90
95
  it("--dry-run writes nothing and previews effects (no Prompt lines)", async () => {
91
96
  const dir = freshCwd();
@@ -100,6 +105,45 @@ describe("create — real generation + stamp", () => {
100
105
  expect(walk(dir)).toEqual([]); // nothing written
101
106
  });
102
107
 
108
+ it("--dry-run reports the bytes the real run writes (post-stamp)", async () => {
109
+ // The mock previewer never called `onEffectStart`, so summon's stamping
110
+ // transform did not run and every planned byte count under-reported the
111
+ // written file by the stamp's length. The honest previewer runs it.
112
+ const params = {
113
+ framework: "react",
114
+ componentPath: "src/components/Widget",
115
+ withStyles: true,
116
+ withStories: false,
117
+ withSsrTests: false,
118
+ };
119
+ const previewDir = freshCwd();
120
+ const preview = await runIn(previewDir, params, {
121
+ dryRun: true,
122
+ undo: false,
123
+ yes: false,
124
+ });
125
+ expect(preview.exitCode).toBe(0);
126
+ expect(walk(previewDir)).toEqual([]);
127
+
128
+ const planned = new Map<string, number>();
129
+ for (const line of (preview.stdout ?? "").split("\n")) {
130
+ const match = /^ {2}- Write file: (.+) \((\d+) bytes\)$/.exec(line);
131
+ if (match) planned.set(match[1] as string, Number(match[2]));
132
+ }
133
+ expect(planned.size).toBeGreaterThan(0);
134
+
135
+ const realDir = freshCwd();
136
+ expect((await runIn(realDir, params)).exitCode).toBe(0);
137
+ for (const [relative, bytes] of planned) {
138
+ expect({
139
+ file: relative,
140
+ bytes: readFileSync(join(realDir, relative), "utf-8").length,
141
+ }).toEqual({ file: relative, bytes });
142
+ }
143
+ // Same set of files, not just the same counts for the ones checked.
144
+ expect(walk(realDir).sort()).toEqual([...planned.keys()].sort());
145
+ });
146
+
103
147
  it("--undo reverses a run", async () => {
104
148
  const dir = freshCwd();
105
149
  await runIn(dir, {
@@ -122,7 +166,9 @@ describe("create — real generation + stamp", () => {
122
166
  );
123
167
  expect(outcome.exitCode).toBe(0);
124
168
  expect(outcome.stdout).toMatch(/Undid \d+ step/);
125
- });
169
+ // TWO real runs (generate, then undo) — the same load-sensitivity as the
170
+ // stamp test above, doubled; same explicit ceiling.
171
+ }, 60_000);
126
172
 
127
173
  it("rejects an unknown framework with INVALID_INPUT", async () => {
128
174
  const dir = freshCwd();
@@ -130,7 +176,7 @@ describe("create — real generation + stamp", () => {
130
176
  // framework param to exercise pickGenerator's own guard too.
131
177
  const runtime = bootRuntime(FLAGS, dir);
132
178
  await expect(
133
- createComponentVerb.run({ framework: "vue" }, {
179
+ createVerbs.component.run({ framework: "vue" }, {
134
180
  ...runtime,
135
181
  interaction: { isTTY: false, transport: "cli", yes: true },
136
182
  } as never),
@@ -216,9 +262,19 @@ describe("lazy-React discipline (PROTECTED)", () => {
216
262
  "@canonical/summon-component",
217
263
  "@canonical/summon-package",
218
264
  "@canonical/summon-application",
265
+ // The generators pull summon-core, and summon-core is what costs the
266
+ // fast paths (+43 MB RSS once it loads). Dropping the `type` from one of
267
+ // the four `import type … from "@canonical/summon-core"` lines in this
268
+ // directory is a one-word regression nothing else catches.
269
+ "@canonical/summon-core",
219
270
  ];
220
271
  for (const file of graph) {
221
- const src = readFileSync(file, "utf-8");
272
+ // Type-only imports are erased before runtime, so they load nothing —
273
+ // strip them before asking what the file actually pulls in.
274
+ const src = readFileSync(file, "utf-8").replace(
275
+ /\bimport\s+type\b[^;]*;/g,
276
+ "",
277
+ );
222
278
  for (const pkg of forbidden) {
223
279
  const esc = pkg.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
224
280
  expect(
@@ -284,7 +340,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
284
340
  (generators as Record<string, { prompts: unknown[] }>)["component/react"]
285
341
  .prompts as never,
286
342
  );
287
- const verb = createComponentVerb.params;
343
+ const verb = createVerbs.component.params;
288
344
 
289
345
  // The verb SYNTHESIZES a `framework` enum (the 3 framework generators
290
346
  // collapse to one verb): react|svelte|lit, default react — no generator
@@ -327,7 +383,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
327
383
  .prompts as never,
328
384
  );
329
385
  // The package verb's grammar IS generatorToParams(PACKAGE_MIRROR).
330
- expect(behavioural(createPackageVerb.params)).toEqual(
386
+ expect(behavioural(createVerbs.package.params)).toEqual(
331
387
  behavioural(applyRunInstallOverride(real)),
332
388
  );
333
389
 
@@ -335,7 +391,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
335
391
  const realRunInstall = real.find((p) => p.name === "runInstall") as
336
392
  | Record<string, unknown>
337
393
  | undefined;
338
- const mirrorRunInstall = createPackageVerb.params.find(
394
+ const mirrorRunInstall = createVerbs.package.params.find(
339
395
  (p) => p.name === "runInstall",
340
396
  ) as Record<string, unknown> | undefined;
341
397
  expect(realRunInstall?.default).toBe(true);
@@ -350,6 +406,18 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
350
406
  });
351
407
  });
352
408
 
409
+ /** Map a mirror's CLI `--with-X` include-flag names back to the generator
410
+ * prompt names (AV-228 B8) so the parity comparison still checks kinds and
411
+ * defaults against the real generator despite the deliberate flag rename. */
412
+ const toGeneratorNames = (
413
+ kind: "application",
414
+ params: readonly ParamSpec[],
415
+ ): ParamSpec[] =>
416
+ params.map((p) => {
417
+ const generatorName = INCLUDE_FLAG_ALIASES[kind][p.name];
418
+ return generatorName ? ({ ...p, name: generatorName } as ParamSpec) : p;
419
+ });
420
+
353
421
  it("application: mirror matches the application/react generator (defaults)", async () => {
354
422
  const { generators } = await import("@canonical/summon-application");
355
423
  const real = generatorToParams(
@@ -357,14 +425,30 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
357
425
  "application/react"
358
426
  ].prompts as never,
359
427
  );
360
- expect(behavioural(createApplicationVerb.params)).toEqual(
361
- behavioural(applyRunInstallOverride(real)),
362
- );
428
+ // Bridge the B8 include-flag rename before comparing: the CLI grammar
429
+ // exposes `withSsr`/`withRouter`/`withForms`, which map back to the
430
+ // generator's `ssr`/`router`/`forms`.
431
+ expect(
432
+ behavioural(
433
+ toGeneratorNames("application", createVerbs.application.params),
434
+ ),
435
+ ).toEqual(behavioural(applyRunInstallOverride(real)));
436
+
437
+ // Guard the B8 rename itself: ALL application include-flags ARE on the
438
+ // `--with-X` convention (params `withSsr`/`withRouter`/`withForms`/
439
+ // `withRelay`), not the bare `ssr`/`router`/`forms`/`relay` booleans.
440
+ const names = createVerbs.application.params.map((p) => p.name);
441
+ expect(names).toContain("withSsr");
442
+ expect(names).toContain("withRouter");
443
+ expect(names).toContain("withForms");
444
+ expect(names).toContain("withRelay");
445
+ expect(names).not.toContain("ssr");
446
+ expect(names).not.toContain("relay");
363
447
 
364
448
  const realRunInstall = real.find((p) => p.name === "runInstall") as
365
449
  | Record<string, unknown>
366
450
  | undefined;
367
- const mirrorRunInstall = createApplicationVerb.params.find(
451
+ const mirrorRunInstall = createVerbs.application.params.find(
368
452
  (p) => p.name === "runInstall",
369
453
  ) as Record<string, unknown> | undefined;
370
454
  expect(realRunInstall?.default).toBe(true);
@@ -372,7 +456,7 @@ describe("generator→grammar adapter parity (PROTECTED)", () => {
372
456
 
373
457
  // appPath is the positional entry point (its default carries across).
374
458
  expect(
375
- createApplicationVerb.params.find((p) => p.name === "appPath"),
459
+ createVerbs.application.params.find((p) => p.name === "appPath"),
376
460
  ).toMatchObject({ kind: "string", positional: true, default: "my-app" });
377
461
  });
378
462
  });
@@ -401,3 +485,95 @@ describe("compiled-binary create gate (M4)", () => {
401
485
  expect(isModuleNotFound("nope")).toBe(false);
402
486
  });
403
487
  });
488
+
489
+ describe("declared generator bindings (PROTECTED)", () => {
490
+ // CREATE_GENERATORS is the one place the create surface's generator facts are
491
+ // written down. What is checkable against something outside itself — the live
492
+ // generator maps and the embedded manifest — is checked here.
493
+
494
+ it("every binding resolves to the generator it names", async () => {
495
+ const { pickGenerator } = await import("./pickGenerator.js");
496
+ // `meta.name` is the generator's OWN identity, so a swapped `key` or a
497
+ // framework the map does not carry turns this red — which `toBeDefined()`
498
+ // on a non-nullable return could not.
499
+ for (const framework of CREATE_GENERATORS.component.frameworks) {
500
+ expect(pickGenerator("component", { framework }).meta.name).toBe(
501
+ `component/${framework}`,
502
+ );
503
+ }
504
+ expect(pickGenerator("package", {}).meta.name).toBe("package");
505
+ expect(pickGenerator("application", {}).meta.name).toBe(
506
+ "application/react",
507
+ );
508
+ });
509
+
510
+ it("exactly the two source-run-only verbs publish the availability caveat", () => {
511
+ // A LITERAL list, for the same reason `compiledCreate.subprocess.test.ts`
512
+ // uses one: the caveat is DERIVED from `readsEmbeddedTemplates`, so an
513
+ // assertion derived from the same bit would agree with itself no matter
514
+ // which way the bit was flipped. These two are the nouns that test proves
515
+ // refuse on a real compiled binary, so this is what the reference is held
516
+ // to — and flipping the bit turns this red, along with the reference
517
+ // drift-guard, in the same run.
518
+ const caveated = createModule.verbs
519
+ .filter((verb) => verb.summary.includes("Source-run only."))
520
+ .map((verb) => verb.path.at(1));
521
+ expect(caveated).toEqual(["package", "application"]);
522
+ for (const verb of createModule.verbs) {
523
+ const kind = verb.path.at(1);
524
+ const gated = caveated.includes(kind);
525
+ // The `doc` is what `tools.md` and the MCP tool description render, so
526
+ // the explanation has to travel with the marker, and it quotes the
527
+ // command it is about — a copy-paste between bindings is then visible.
528
+ expect(verb.doc?.includes("UNSUPPORTED") ?? false, kind).toBe(gated);
529
+ if (gated) {
530
+ expect(verb.doc).toContain(`create ${kind}`);
531
+ // And it still says what the verb MAKES. `doc ?? summary` means a doc
532
+ // REPLACES the summary on the MCP surface, so a caveat alone left an
533
+ // agent no statement anywhere of what `create_package` scaffolds.
534
+ expect(verb.doc).toContain(
535
+ verb.summary.replace(" Source-run only.", ""),
536
+ );
537
+ }
538
+ }
539
+ });
540
+
541
+ it("the --framework enum offers the declared frameworks and defaults to one of them", () => {
542
+ const framework = createVerbs.component.params.find(
543
+ (p) => p.name === "framework",
544
+ );
545
+ // A `values: []` enum with a `default` would leave every `--framework`
546
+ // rejected while `--help` still advertises a default — the hazard this names.
547
+ // (That every value resolves is the test above.)
548
+ expect(framework?.kind).toBe("enum");
549
+ const values = (framework as { values: readonly string[] }).values;
550
+ expect(values.length).toBeGreaterThan(0);
551
+ expect(values).toContain(framework?.default);
552
+ });
553
+
554
+ it("embeds only the templates the component loader can key", async () => {
555
+ const { TEMPLATES } = await import("./templates.embedded.generated.js");
556
+ const keys = Object.keys(TEMPLATES);
557
+ // summon-component's `qualifiedKey()` prefixes EVERY lookup with
558
+ // `component/`, so any other entry ships dead weight the binary can never
559
+ // read — and a binding embedded without a manifest-reading generator would
560
+ // put one here. Non-empty first: `.every()` over an empty list is vacuous.
561
+ expect(keys.length).toBeGreaterThan(0);
562
+ expect(keys.filter((k) => !k.startsWith("component/"))).toEqual([]);
563
+ });
564
+
565
+ it("create surfaces exactly the three declared nouns", () => {
566
+ // A LITERAL surface pin, deliberately not derived from CREATE_GENERATORS:
567
+ // surfacing a noun also needs a hand-written prompt mirror, path param and
568
+ // examples in create.verb.ts, so the surface is a deliberate SUBSET of what
569
+ // the declared packages ship (`@canonical/summon-application` also ships
570
+ // `domain`, `route` and `wrapper`). Pinning OUR three nouns rather than any
571
+ // third-party package's generator list keeps an upstream release from
572
+ // turning this red.
573
+ expect(createModule.verbs.map((v) => v.path[1])).toEqual([
574
+ "component",
575
+ "package",
576
+ "application",
577
+ ]);
578
+ });
579
+ });