@canonical/pragma-cli 0.29.0 → 0.30.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 (241) hide show
  1. package/README.md +67 -15
  2. package/dist/pragma +0 -0
  3. package/package.json +13 -13
  4. package/src/config/defaultConfig.test.ts +33 -0
  5. package/src/config/defaultConfig.ts +59 -0
  6. package/src/config/defaults.json +17 -0
  7. package/src/config/findProjectConfigPath.test.ts +53 -0
  8. package/src/config/findProjectConfigPath.ts +36 -0
  9. package/src/config/index.ts +17 -2
  10. package/src/config/mergeConfigFileUpdate.ts +58 -0
  11. package/src/config/parseConfigValues.ts +215 -0
  12. package/src/config/readConfig.test.ts +104 -13
  13. package/src/config/readConfig.ts +12 -153
  14. package/src/config/readConfigLayers.test.ts +107 -0
  15. package/src/config/readConfigLayers.ts +115 -0
  16. package/src/config/resolveGlobalConfigPath.ts +13 -0
  17. package/src/config/resolveWriteConfigPath.test.ts +53 -0
  18. package/src/config/resolveWriteConfigPath.ts +31 -0
  19. package/src/config/types.ts +76 -1
  20. package/src/config/writeConfig.ts +25 -55
  21. package/src/domains/block/commands/list.ts +5 -90
  22. package/src/domains/block/commands/lookup.ts +8 -167
  23. package/src/domains/block/mcp/specs.ts +16 -196
  24. package/src/domains/block/orchestration/resolveBlockLookup.ts +2 -1
  25. package/src/domains/block/stories.test.ts +28 -0
  26. package/src/domains/block/stories.ts +254 -0
  27. package/src/domains/config/commands/channel.test.ts +14 -3
  28. package/src/domains/config/commands/channel.ts +11 -7
  29. package/src/domains/config/commands/configScope.ts +46 -0
  30. package/src/domains/config/commands/framework.test.ts +18 -1
  31. package/src/domains/config/commands/framework.ts +13 -7
  32. package/src/domains/config/commands/show.ts +5 -45
  33. package/src/domains/config/commands/tier.test.ts +13 -2
  34. package/src/domains/config/commands/tier.ts +11 -7
  35. package/src/domains/config/commands/trace.test.ts +19 -2
  36. package/src/domains/config/commands/trace.ts +6 -3
  37. package/src/domains/config/formatters/channel.test.ts +20 -6
  38. package/src/domains/config/formatters/channel.ts +14 -11
  39. package/src/domains/config/formatters/framework.ts +14 -11
  40. package/src/domains/config/formatters/show.test.ts +27 -0
  41. package/src/domains/config/formatters/show.ts +27 -4
  42. package/src/domains/config/formatters/tier.test.ts +26 -10
  43. package/src/domains/config/formatters/tier.ts +19 -11
  44. package/src/domains/config/formatters/trace.ts +7 -7
  45. package/src/domains/config/mcp/specs.ts +33 -53
  46. package/src/domains/config/operations/show.test.ts +18 -0
  47. package/src/domains/config/operations/show.ts +7 -1
  48. package/src/domains/config/operations/types.ts +7 -0
  49. package/src/domains/config/stories.ts +42 -0
  50. package/src/domains/config/tasks/index.ts +1 -0
  51. package/src/domains/config/tasks/setChannel.ts +13 -28
  52. package/src/domains/config/tasks/setFramework.ts +11 -30
  53. package/src/domains/config/tasks/setTier.ts +10 -29
  54. package/src/domains/config/tasks/setTrace.ts +10 -28
  55. package/src/domains/config/tasks/writeConfigField.ts +41 -0
  56. package/src/domains/create/commands/component.ts +5 -5
  57. package/src/domains/doctor/operations/checks/checkConfigFile.ts +19 -5
  58. package/src/domains/doctor/operations/checks/checkMcpCommands.ts +130 -0
  59. package/src/domains/doctor/operations/checks/checkMcpConfigured.ts +1 -1
  60. package/src/domains/doctor/operations/checks/checkPackageRefs.ts +2 -14
  61. package/src/domains/doctor/operations/checks/checkSkillsSymlinked.ts +1 -1
  62. package/src/domains/doctor/operations/checks/index.ts +1 -0
  63. package/src/domains/doctor/operations/checks.test.ts +158 -7
  64. package/src/domains/doctor/operations/runChecks.test.ts +15 -6
  65. package/src/domains/doctor/operations/runChecks.ts +2 -0
  66. package/src/domains/graph/helpers/resolveUri.test.ts +50 -0
  67. package/src/domains/graph/helpers/resolveUri.ts +28 -21
  68. package/src/domains/graphql/commands/build.test.ts +5 -3
  69. package/src/domains/graphql/commands/build.ts +8 -16
  70. package/src/domains/graphql/commands/check.test.ts +5 -3
  71. package/src/domains/graphql/commands/check.ts +7 -13
  72. package/src/domains/graphql/commands/index.ts +1 -0
  73. package/src/domains/graphql/commands/serve.test.ts +40 -0
  74. package/src/domains/graphql/commands/serve.ts +131 -0
  75. package/src/domains/graphql/constants.ts +10 -0
  76. package/src/domains/graphql/helpers/gatherSources.test.ts +92 -0
  77. package/src/domains/graphql/helpers/gatherSources.ts +58 -0
  78. package/src/domains/graphql/helpers/index.ts +2 -0
  79. package/src/domains/graphql/helpers/routeGraphql.test.ts +43 -0
  80. package/src/domains/graphql/helpers/routeGraphql.ts +28 -0
  81. package/src/domains/graphql/index.ts +9 -6
  82. package/src/domains/graphql/operations/compileSchema.test.ts +21 -45
  83. package/src/domains/graphql/operations/compileSchema.ts +23 -25
  84. package/src/domains/graphql/operations/createSchemaServer.test.ts +50 -0
  85. package/src/domains/graphql/operations/createSchemaServer.ts +75 -0
  86. package/src/domains/graphql/operations/index.ts +5 -0
  87. package/src/domains/info/operations/collectInfo.test.ts +14 -3
  88. package/src/domains/info/operations/collectInfo.ts +12 -15
  89. package/src/domains/llm/capabilities.test.ts +4 -4
  90. package/src/domains/llm/data/toolCatalog.ts +6 -0
  91. package/src/domains/modifier/commands/list.ts +5 -38
  92. package/src/domains/modifier/commands/lookup.ts +8 -79
  93. package/src/domains/modifier/mcp/specs.ts +15 -94
  94. package/src/domains/modifier/orchestration/resolveModifierLookup.ts +2 -1
  95. package/src/domains/modifier/stories.test.ts +20 -0
  96. package/src/domains/modifier/stories.ts +77 -0
  97. package/src/domains/ontology/commands/list.ts +5 -58
  98. package/src/domains/ontology/commands/show.ts +6 -58
  99. package/src/domains/ontology/formatters/show.ts +7 -1
  100. package/src/domains/ontology/mcp/specs.ts +7 -84
  101. package/src/domains/ontology/stories.test.ts +36 -0
  102. package/src/domains/ontology/stories.ts +108 -0
  103. package/src/domains/refs/operations/updateRefs.ts +2 -37
  104. package/src/domains/setup/commands/skills.test.ts +6 -2
  105. package/src/domains/setup/commands/skills.ts +6 -2
  106. package/src/domains/setup/helpers/runSetupTask.test.ts +148 -150
  107. package/src/domains/setup/helpers/runSetupTask.ts +62 -70
  108. package/src/domains/setup/operations/setupMcp.test.ts +6 -1
  109. package/src/domains/shared/bootStore.ts +6 -3
  110. package/src/domains/shared/compactUri.test.ts +11 -0
  111. package/src/domains/shared/compactUri.ts +10 -5
  112. package/src/domains/shared/context.ts +2 -2
  113. package/src/domains/shared/contracts.ts +11 -0
  114. package/src/domains/shared/createInteractivePromptSession.test.ts +439 -0
  115. package/src/domains/shared/createInteractivePromptSession.ts +236 -0
  116. package/src/domains/shared/loaders/bundledLoader.test.ts +248 -0
  117. package/src/domains/shared/loaders/bundledLoader.ts +110 -12
  118. package/src/domains/shared/loaders/gitLoader.ts +2 -2
  119. package/src/domains/shared/loaders/index.ts +4 -1
  120. package/src/domains/shared/loaders/localLoader.ts +18 -4
  121. package/src/domains/shared/loaders/readPackageDir.ts +36 -1
  122. package/src/domains/shared/loaders/storyAssetsPlugin.test.ts +84 -0
  123. package/src/domains/shared/loaders/storyAssetsPlugin.ts +46 -0
  124. package/src/domains/shared/lookupMany.test.ts +120 -0
  125. package/src/domains/shared/lookupMany.ts +34 -2
  126. package/src/domains/shared/mergeAndParseRefs.test.ts +89 -0
  127. package/src/domains/shared/mergeAndParseRefs.ts +44 -0
  128. package/src/domains/shared/packages.ts +10 -17
  129. package/src/domains/shared/resolveConfiguredGraphs.test.ts +27 -0
  130. package/src/domains/shared/resolveConfiguredGraphs.ts +41 -0
  131. package/src/domains/shared/runtime.ts +4 -63
  132. package/src/domains/shared/semanticPackage.ts +17 -1
  133. package/src/domains/shared/stories/compileLookupCommand.test.ts +145 -0
  134. package/src/domains/shared/stories/compileLookupCommand.ts +100 -0
  135. package/src/domains/shared/stories/compileLookupTool.test.ts +105 -0
  136. package/src/domains/shared/stories/compileLookupTool.ts +96 -0
  137. package/src/domains/shared/stories/compileReadCommand.test.ts +121 -0
  138. package/src/domains/shared/stories/compileReadCommand.ts +48 -0
  139. package/src/domains/shared/stories/compileReadTool.test.ts +64 -0
  140. package/src/domains/shared/stories/compileReadTool.ts +38 -0
  141. package/src/domains/shared/stories/condense.test.ts +18 -0
  142. package/src/domains/shared/stories/condense.ts +15 -0
  143. package/src/domains/shared/stories/index.ts +20 -0
  144. package/src/domains/shared/stories/normalizeNames.test.ts +20 -0
  145. package/src/domains/shared/stories/normalizeNames.ts +22 -0
  146. package/src/domains/shared/stories/pack/applyPackFilters.test.ts +102 -0
  147. package/src/domains/shared/stories/pack/applyPackFilters.ts +76 -0
  148. package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +51 -0
  149. package/src/domains/shared/stories/pack/buildLookupQuery.ts +80 -0
  150. package/src/domains/shared/stories/pack/collectPackStories.test.ts +108 -0
  151. package/src/domains/shared/stories/pack/collectPackStories.ts +76 -0
  152. package/src/domains/shared/stories/pack/compilePackCommands.ts +40 -0
  153. package/src/domains/shared/stories/pack/compilePackStories.test.ts +146 -0
  154. package/src/domains/shared/stories/pack/compilePackStories.ts +253 -0
  155. package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +43 -0
  156. package/src/domains/shared/stories/pack/index.ts +28 -0
  157. package/src/domains/shared/stories/pack/runSelectQuery.ts +31 -0
  158. package/src/domains/shared/stories/pack/standardParity.test.ts +308 -0
  159. package/src/domains/shared/stories/pack/types.ts +104 -0
  160. package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +240 -0
  161. package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +408 -0
  162. package/src/domains/shared/stories/projectCliParameters.test.ts +59 -0
  163. package/src/domains/shared/stories/projectCliParameters.ts +57 -0
  164. package/src/domains/shared/stories/projectToolParams.test.ts +50 -0
  165. package/src/domains/shared/stories/projectToolParams.ts +32 -0
  166. package/src/domains/shared/stories/requirePragmaContext.test.ts +23 -0
  167. package/src/domains/shared/stories/requirePragmaContext.ts +23 -0
  168. package/src/domains/shared/stories/resolveLookupDetailed.test.ts +57 -0
  169. package/src/domains/shared/stories/resolveLookupDetailed.ts +27 -0
  170. package/src/domains/shared/stories/types.ts +171 -0
  171. package/src/domains/shared/suggestions/expandGlob.ts +1 -0
  172. package/src/domains/shared/suggestions/suggestNames.ts +43 -10
  173. package/src/domains/skill/commands/index.ts +3 -1
  174. package/src/domains/skill/commands/list.test.ts +6 -2
  175. package/src/domains/skill/commands/list.ts +5 -63
  176. package/src/domains/skill/commands/lookup.test.ts +118 -0
  177. package/src/domains/skill/commands/lookup.ts +14 -0
  178. package/src/domains/skill/formatters/index.ts +1 -0
  179. package/src/domains/skill/formatters/lookup.test.ts +46 -0
  180. package/src/domains/skill/formatters/lookup.ts +57 -0
  181. package/src/domains/skill/helpers/resolveSkillSources.ts +0 -1
  182. package/src/domains/skill/index.ts +7 -3
  183. package/src/domains/skill/mcp/specs.ts +10 -29
  184. package/src/domains/skill/operations/discover.test.ts +61 -30
  185. package/src/domains/skill/operations/discover.ts +28 -37
  186. package/src/domains/skill/operations/index.ts +2 -1
  187. package/src/domains/skill/operations/list.test.ts +15 -7
  188. package/src/domains/skill/operations/lookup.test.ts +91 -0
  189. package/src/domains/skill/operations/lookup.ts +70 -0
  190. package/src/domains/skill/stories.test.ts +43 -0
  191. package/src/domains/skill/stories.ts +103 -0
  192. package/src/domains/skill/types.ts +8 -0
  193. package/src/domains/standard/commands/list.ts +5 -84
  194. package/src/domains/standard/commands/lookup.ts +8 -93
  195. package/src/domains/standard/mcp/specs.ts +16 -171
  196. package/src/domains/standard/orchestration/resolveStandardLookup.ts +2 -1
  197. package/src/domains/standard/stories.test.ts +24 -0
  198. package/src/domains/standard/stories.ts +151 -0
  199. package/src/domains/tier/commands/list.ts +5 -55
  200. package/src/domains/tier/mcp/specs.ts +5 -31
  201. package/src/domains/tier/stories.test.ts +22 -0
  202. package/src/domains/tier/stories.ts +52 -0
  203. package/src/domains/token/mcp/specs.ts +5 -1
  204. package/src/domains/token/orchestration/resolveTokenLookup.ts +2 -1
  205. package/src/domains/trace/tracePlugin.test.ts +1 -1
  206. package/src/mcp/registerResources.test.ts +382 -74
  207. package/src/mcp/registerResources.ts +5 -348
  208. package/src/mcp/resources/buildGraphIndex.ts +167 -0
  209. package/src/mcp/resources/buildResourceList.test.ts +97 -0
  210. package/src/mcp/resources/buildResourceList.ts +176 -0
  211. package/src/mcp/resources/classifyEntity.test.ts +40 -0
  212. package/src/mcp/resources/classifyEntity.ts +27 -0
  213. package/src/mcp/resources/constants.ts +105 -0
  214. package/src/mcp/resources/displayPredicates.ts +62 -0
  215. package/src/mcp/resources/pickFirstValue.test.ts +29 -0
  216. package/src/mcp/resources/pickFirstValue.ts +26 -0
  217. package/src/mcp/resources/pickPrimaryType.test.ts +41 -0
  218. package/src/mcp/resources/pickPrimaryType.ts +26 -0
  219. package/src/mcp/resources/rankUriCompletions.test.ts +52 -0
  220. package/src/mcp/resources/rankUriCompletions.ts +64 -0
  221. package/src/mcp/resources/readEntity.ts +169 -0
  222. package/src/mcp/resources/registerResources.ts +126 -0
  223. package/src/mcp/resources/types.ts +128 -0
  224. package/src/mcp/tools/index.ts +9 -0
  225. package/src/mcp/tools/registerTools.test.ts +65 -8
  226. package/src/mcp/types.ts +4 -0
  227. package/src/pipeline/collectCommands.ts +8 -1
  228. package/src/pipeline/runCli.ts +40 -6
  229. package/src/testing/createTestMcpClient.ts +4 -1
  230. package/src/testing/index.ts +10 -0
  231. package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +1207 -0
  232. package/src/testing/integration/cross-binary-byte-equality.test.ts +129 -0
  233. package/src/testing/integration/executor-execution.test.ts +101 -0
  234. package/src/testing/integration/executor-golden.test.ts +145 -0
  235. package/src/testing/integration/mcp-surface.test.ts +1 -1
  236. package/src/testing/integration/parity.test.ts +17 -12
  237. package/src/testing/regression/0001-skill-discovery-granularity.test.ts +71 -0
  238. package/src/testing/setupXdgIsolation.ts +15 -0
  239. package/src/testing/standardParityFixtures.ts +92 -0
  240. package/src/testing/storyFixtures.ts +63 -0
  241. package/src/pipeline/runInteractiveCommand.ts +0 -198
@@ -0,0 +1,129 @@
1
+ /**
2
+ * The P.05 byte-equality gate: `pragma create X` and `summon X` must write
3
+ * identical bytes for the same generator invocation.
4
+ *
5
+ * Both binaries reduce to compositions over the shared execution core:
6
+ * pragma's batch path is `executeGenerator` (which stamps via
7
+ * `createStampOnEffectStart`), and summon's Ink executor runs
8
+ * `runGeneratorTask` with the same stamp transform and the defaults prompt
9
+ * handler. This test runs both compositions over the real generators into
10
+ * separate directories and asserts the trees are byte-identical — stamps
11
+ * included.
12
+ */
13
+
14
+ import {
15
+ mkdirSync,
16
+ mkdtempSync,
17
+ readdirSync,
18
+ readFileSync,
19
+ rmSync,
20
+ } from "node:fs";
21
+ import { tmpdir } from "node:os";
22
+ import { join, relative } from "node:path";
23
+ import {
24
+ answerPromptWithDefaults,
25
+ createGeneratorStamp,
26
+ createStampOnEffectStart,
27
+ executeGenerator,
28
+ runGeneratorTask,
29
+ } from "@canonical/cli-core";
30
+ import type { GeneratorDefinition } from "@canonical/summon-core";
31
+ import { generators as packageGenerators } from "@canonical/summon-package";
32
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
33
+ import { COMPONENT_GENERATORS } from "../../domains/create/generators.js";
34
+
35
+ interface Case {
36
+ readonly name: string;
37
+ readonly gen: GeneratorDefinition;
38
+ readonly answers: Record<string, unknown>;
39
+ }
40
+
41
+ const cases: Case[] = [
42
+ {
43
+ name: "component/react",
44
+ gen: COMPONENT_GENERATORS.react as GeneratorDefinition,
45
+ answers: { componentPath: "src/components/Button" },
46
+ },
47
+ {
48
+ name: "package/library",
49
+ gen: packageGenerators.package as GeneratorDefinition,
50
+ answers: { name: "@canonical/example", type: "library" },
51
+ },
52
+ ];
53
+
54
+ /** Collect every file under root as relative path → raw content bytes. */
55
+ const readTree = (root: string, dir = root): Map<string, Buffer> => {
56
+ const files = new Map<string, Buffer>();
57
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
58
+ const full = join(dir, entry.name);
59
+ if (entry.isDirectory()) {
60
+ for (const [k, v] of readTree(root, full)) files.set(k, v);
61
+ } else {
62
+ files.set(relative(root, full), readFileSync(full));
63
+ }
64
+ }
65
+ return files;
66
+ };
67
+
68
+ describe("cross-binary byte equality — pragma create ≡ summon", () => {
69
+ let dir: string;
70
+ let originalCwd: string;
71
+
72
+ beforeEach(() => {
73
+ originalCwd = process.cwd();
74
+ dir = mkdtempSync(join(tmpdir(), "byte-equality-"));
75
+ });
76
+
77
+ afterEach(() => {
78
+ process.chdir(originalCwd);
79
+ rmSync(dir, { recursive: true, force: true });
80
+ });
81
+
82
+ for (const testCase of cases) {
83
+ it(`${testCase.name} — both binaries write identical bytes, stamps included`, async () => {
84
+ const pragmaDir = join(dir, "pragma");
85
+ const summonDir = join(dir, "summon");
86
+ mkdirSync(pragmaDir);
87
+ mkdirSync(summonDir);
88
+
89
+ // pragma path: the create command's batch execution (stamps by default).
90
+ const result = await executeGenerator(
91
+ testCase.gen,
92
+ { ...testCase.answers, yes: true },
93
+ {
94
+ cwd: pragmaDir,
95
+ globalFlags: { llm: false, format: "text", verbose: false },
96
+ },
97
+ );
98
+ expect(result.tag).toBe("output");
99
+
100
+ // summon path: the Ink executor's exact composition over the shared
101
+ // core. Both binaries fold prompt defaults into the answers before
102
+ // generating; do the same here.
103
+ const answers = { ...testCase.answers };
104
+ for (const prompt of testCase.gen.prompts) {
105
+ if (!(prompt.name in answers) && prompt.default !== undefined) {
106
+ answers[prompt.name] = prompt.default;
107
+ }
108
+ }
109
+ await runGeneratorTask(testCase.gen.generate(answers), {
110
+ cwd: summonDir,
111
+ promptHandler: answerPromptWithDefaults,
112
+ onEffectStart: createStampOnEffectStart(
113
+ createGeneratorStamp(testCase.gen),
114
+ ),
115
+ });
116
+
117
+ const pragmaTree = readTree(pragmaDir);
118
+ const summonTree = readTree(summonDir);
119
+
120
+ expect([...pragmaTree.keys()].sort()).toEqual(
121
+ [...summonTree.keys()].sort(),
122
+ );
123
+ for (const [path, content] of pragmaTree) {
124
+ expect(summonTree.get(path)?.equals(content), path).toBe(true);
125
+ }
126
+ expect(pragmaTree.size).toBeGreaterThan(0);
127
+ });
128
+ }
129
+ });
@@ -0,0 +1,101 @@
1
+ /**
2
+ * End-to-end characterization of the real-execution path the executor merge
3
+ * (4a) rewired: every generator the CLI actually dispatches, and a real setup
4
+ * task, run through the shared UI-free core ({@link runGeneratorTask}) and
5
+ * produce their files on disk.
6
+ *
7
+ * The golden snapshots (executor-golden.test.ts) lock the *preview* modes
8
+ * (dry-run/json/llm), which never execute. This locks the *real* run: the
9
+ * merged core executes the genuine task trees — not test doubles — with the
10
+ * correct working-directory semantics.
11
+ */
12
+
13
+ import {
14
+ existsSync,
15
+ mkdirSync,
16
+ mkdtempSync,
17
+ readdirSync,
18
+ rmSync,
19
+ } from "node:fs";
20
+ import { tmpdir } from "node:os";
21
+ import { join } from "node:path";
22
+ import { runGeneratorTask } from "@canonical/cli-core";
23
+ import { generators as packageGenerators } from "@canonical/summon-package";
24
+ import type { Task } from "@canonical/task";
25
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
26
+ import { COMPONENT_GENERATORS } from "../../domains/create/generators.js";
27
+ import setupMcp from "../../domains/setup/operations/setupMcp.js";
28
+
29
+ interface Case {
30
+ readonly name: string;
31
+ readonly build: () => Task<unknown>;
32
+ }
33
+
34
+ const componentParams = { componentPath: "src/components/Button" };
35
+ const packageParams = { name: "@canonical/example", type: "library" };
36
+
37
+ const cases: Case[] = [
38
+ {
39
+ name: "component/react",
40
+ build: () => COMPONENT_GENERATORS.react.generate(componentParams),
41
+ },
42
+ {
43
+ name: "component/svelte",
44
+ build: () => COMPONENT_GENERATORS.svelte.generate(componentParams),
45
+ },
46
+ {
47
+ name: "component/lit",
48
+ build: () => COMPONENT_GENERATORS.lit.generate(componentParams),
49
+ },
50
+ {
51
+ name: "package/library",
52
+ build: () => packageGenerators.package.generate(packageParams),
53
+ },
54
+ ];
55
+
56
+ const countFiles = (root: string): number => {
57
+ let total = 0;
58
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
59
+ total += entry.isDirectory() ? countFiles(join(root, entry.name)) : 1;
60
+ }
61
+ return total;
62
+ };
63
+
64
+ describe("shared executor — real execution over the dispatched tasks", () => {
65
+ let dir: string;
66
+ let originalCwd: string;
67
+
68
+ beforeEach(() => {
69
+ originalCwd = process.cwd();
70
+ dir = mkdtempSync(join(tmpdir(), "executor-execution-"));
71
+ });
72
+
73
+ afterEach(() => {
74
+ process.chdir(originalCwd);
75
+ rmSync(dir, { recursive: true, force: true });
76
+ });
77
+
78
+ for (const testCase of cases) {
79
+ it(`${testCase.name} — executes for real in the target cwd`, async () => {
80
+ const targetDir = join(dir, "target");
81
+ mkdirSync(targetDir);
82
+
83
+ await runGeneratorTask(testCase.build(), { cwd: targetDir });
84
+
85
+ expect(countFiles(targetDir)).toBeGreaterThan(0);
86
+ expect(process.cwd()).toBe(originalCwd);
87
+ });
88
+ }
89
+
90
+ // A real setup task on the rewired production surface. setupMcp in force
91
+ // mode writes root-relative config (no prompts, no home-dir writes), so it
92
+ // is safe to execute in a sandbox.
93
+ it("setup/mcp (force) — writes the harness config for real", async () => {
94
+ const root = join(dir, "project");
95
+ mkdirSync(root);
96
+
97
+ await runGeneratorTask(setupMcp(root, "cursor"));
98
+
99
+ expect(existsSync(join(root, ".cursor", "mcp.json"))).toBe(true);
100
+ });
101
+ });
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Characterization (golden) snapshots of the two current executors —
3
+ * `executeGenerator` (cli-core, drives `pragma create`) and `runSetupTask`
4
+ * (drives `pragma setup`) — across the real generators and setup tasks.
5
+ *
6
+ * These lock the *observable output* of each executor for representative,
7
+ * fully-answered inputs in every non-interactive mode. Both executors now run
8
+ * real execution through the shared UI-free core (`runGeneratorTask`), and the
9
+ * merged path must keep reproducing these byte-for-byte; the execution
10
+ * characterization test alongside asserts the real-run path. Interactive
11
+ * behavior is characterized separately, since it runs through the injected
12
+ * `promptHandler` seam.
13
+ */
14
+
15
+ import { dirname, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ import type { CommandContext, CommandResult } from "@canonical/cli-core";
18
+ import { executeGenerator } from "@canonical/cli-core";
19
+ import { generators as packageGenerators } from "@canonical/summon-package";
20
+ import { describe, expect, it } from "vitest";
21
+ import { COMPONENT_GENERATORS } from "../../domains/create/generators.js";
22
+ import runSetupTask from "../../domains/setup/helpers/runSetupTask.js";
23
+ import setupCompletions from "../../domains/setup/operations/setupCompletions.js";
24
+ import setupLsp from "../../domains/setup/operations/setupLsp.js";
25
+
26
+ const ROOT = "/tmp/characterization-project";
27
+
28
+ const ctx = (format: "text" | "json", llm = false): CommandContext => ({
29
+ cwd: ROOT,
30
+ globalFlags: { llm, format, verbose: false },
31
+ });
32
+
33
+ /**
34
+ * Render an executor result to the plain string a user would see, with
35
+ * environment-specific bytes normalised so a committed baseline compares
36
+ * equal on every machine and in CI: ANSI color codes are stripped (chalk
37
+ * enables them under nx's FORCE_COLOR in CI but not in a plain local run),
38
+ * then the repo checkout root (package-generator plans embed absolute
39
+ * template source paths) and the home directory (shell-completion install
40
+ * targets embed `$HOME`) become stable tokens. The repo root is replaced
41
+ * first — on a dev machine it usually lives under `$HOME`.
42
+ */
43
+ const plain = (result: CommandResult): string => {
44
+ let text =
45
+ result.tag === "output"
46
+ ? result.render.plain(result.value)
47
+ : `[${result.tag}]`;
48
+ // biome-ignore lint/suspicious/noControlCharactersInRegex: matching the ANSI escape byte is the point
49
+ text = text.replace(/\u001b\[[0-9;]*m/g, "");
50
+ const repoRoot = resolve(
51
+ dirname(fileURLToPath(import.meta.url)),
52
+ "../../../../../..",
53
+ );
54
+ text = text.split(repoRoot).join("<repo>");
55
+ const home = process.env.HOME ?? "";
56
+ return home === "" ? text : text.split(home).join("<home>");
57
+ };
58
+
59
+ // =============================================================================
60
+ // create — executeGenerator across the real component + package generators
61
+ // =============================================================================
62
+
63
+ const componentParams = { componentPath: "src/components/Button" };
64
+ const packageParams = { name: "@canonical/example", type: "library" };
65
+
66
+ describe("golden: executeGenerator — components", () => {
67
+ for (const framework of ["react", "svelte", "lit"] as const) {
68
+ const gen = COMPONENT_GENERATORS[framework];
69
+
70
+ it(`${framework} — dry-run plan`, async () => {
71
+ const result = await executeGenerator(
72
+ gen,
73
+ { ...componentParams, dryRun: true },
74
+ ctx("text"),
75
+ );
76
+ expect(plain(result)).toMatchSnapshot();
77
+ });
78
+
79
+ it(`${framework} — json plan`, async () => {
80
+ const result = await executeGenerator(
81
+ gen,
82
+ { ...componentParams },
83
+ ctx("json"),
84
+ );
85
+ expect(plain(result)).toMatchSnapshot();
86
+ });
87
+
88
+ it(`${framework} — llm plan`, async () => {
89
+ const result = await executeGenerator(
90
+ gen,
91
+ { ...componentParams },
92
+ ctx("text", true),
93
+ );
94
+ expect(plain(result)).toMatchSnapshot();
95
+ });
96
+ }
97
+ });
98
+
99
+ describe("golden: executeGenerator — package", () => {
100
+ const gen = packageGenerators.package;
101
+
102
+ it("library — dry-run plan", async () => {
103
+ const result = await executeGenerator(
104
+ gen,
105
+ { ...packageParams, dryRun: true },
106
+ ctx("text"),
107
+ );
108
+ expect(plain(result)).toMatchSnapshot();
109
+ });
110
+
111
+ it("library — json plan", async () => {
112
+ const result = await executeGenerator(
113
+ gen,
114
+ { ...packageParams },
115
+ ctx("json"),
116
+ );
117
+ expect(plain(result)).toMatchSnapshot();
118
+ });
119
+ });
120
+
121
+ // =============================================================================
122
+ // setup — runSetupTask across representative deterministic setup tasks
123
+ // =============================================================================
124
+
125
+ describe("golden: runSetupTask — setups", () => {
126
+ it("lsp — dry-run", async () => {
127
+ const result = await runSetupTask(setupLsp(ROOT), { dryRun: true });
128
+ expect(plain(result)).toMatchSnapshot();
129
+ });
130
+
131
+ it("completions (bash) — dry-run", async () => {
132
+ const result = await runSetupTask(setupCompletions("bash"), {
133
+ dryRun: true,
134
+ });
135
+ expect(plain(result)).toMatchSnapshot();
136
+ });
137
+
138
+ it("lsp — dry-run json", async () => {
139
+ const result = await runSetupTask(setupLsp(ROOT), {
140
+ dryRun: true,
141
+ format: "json",
142
+ });
143
+ expect(plain(result)).toMatchSnapshot();
144
+ });
145
+ });
@@ -55,7 +55,7 @@ describe("capabilities", () => {
55
55
  expect(toolNames).toContain("block_list");
56
56
  expect(toolNames).toContain("capabilities");
57
57
  expect(data.tools.every((t) => t.use_when.length > 0)).toBe(true);
58
- expect(data.counts.total).toBe(29);
58
+ expect(data.counts.total).toBe(30);
59
59
  expect(data.conventions).toBeDefined();
60
60
  expect(data.version).toBeDefined();
61
61
  });
@@ -10,6 +10,7 @@
10
10
  import type { Client } from "@modelcontextprotocol/sdk/client/index.js";
11
11
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
12
12
  // ---- Shared utilities ----
13
+ import { readConfigLayers } from "#config";
13
14
  import { VERSION } from "#constants";
14
15
  import { detectInstallSource } from "#package-manager";
15
16
  // ---- Formatters ----
@@ -440,14 +441,25 @@ describe("ontology parity", () => {
440
441
  // ---------------------------------------------------------------------------
441
442
 
442
443
  describe("config parity", () => {
443
- it("config_show: data matches resolveConfigShow", async () => {
444
+ // Both surfaces resolve from the filesystem via the shared config show
445
+ // story, so parity is asserted against the real config path/existence
446
+ // (the MCP tool previously hardcoded "pragma.config.json" / true).
447
+ function resolveExpectedConfigShow() {
448
+ const layers = readConfigLayers(rt.cwd);
444
449
  const install = detectInstallSource();
445
- const opResult = resolveConfigShow(rt.config, {
450
+ return resolveConfigShow(layers.config, {
446
451
  packageManager: install.packageManager,
447
452
  installSource: install.label,
448
- configFilePath: "pragma.config.json",
449
- configFileExists: true,
453
+ configFilePath: layers.project.path,
454
+ configFileExists: layers.project.exists,
455
+ globalConfigPath: layers.global.path,
456
+ globalConfigExists: layers.global.exists,
457
+ origins: layers.origins,
450
458
  });
459
+ }
460
+
461
+ it("config_show: data matches resolveConfigShow", async () => {
462
+ const opResult = resolveExpectedConfigShow();
451
463
 
452
464
  const mcpRes = await client.callTool({
453
465
  name: "config_show",
@@ -458,14 +470,7 @@ describe("config parity", () => {
458
470
  });
459
471
 
460
472
  it("config_show condensed: matches llm formatter", async () => {
461
- const install = detectInstallSource();
462
- const opResult = resolveConfigShow(rt.config, {
463
- packageManager: install.packageManager,
464
- installSource: install.label,
465
- configFilePath: "pragma.config.json",
466
- configFileExists: true,
467
- });
468
- const expectedText = configShowFmt.llm(opResult);
473
+ const expectedText = configShowFmt.llm(resolveExpectedConfigShow());
469
474
 
470
475
  const mcpRes = await client.callTool({
471
476
  name: "config_show",
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Regression: skill discovery returned zero skills on real installs.
3
+ *
4
+ * `readPackageDir` emits one `SkillEntry.dir` per skill folder
5
+ * (`<package>/skills/<name>`), but `discoverSkills` treated each source
6
+ * directory as a *parent* of skill folders and scanned it for
7
+ * subdirectories containing SKILL.md. Production sources therefore never
8
+ * matched — `pragma skill list` errored with "No SKILL.md files found"
9
+ * and MCP `skill_list` returned an empty list, despite `doctor` counting
10
+ * skills in the very same packages. This test pins the granularity
11
+ * contract between the loader and discovery using both real
12
+ * implementations end-to-end.
13
+ */
14
+
15
+ import { mkdirSync, rmSync, writeFileSync } from "node:fs";
16
+ import { tmpdir } from "node:os";
17
+ import { join } from "node:path";
18
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
19
+ import readPackageDir from "../../domains/shared/loaders/readPackageDir.js";
20
+ import type { SemanticPackage } from "../../domains/shared/semanticPackage.js";
21
+ import { resolveSkillSources } from "../../domains/skill/helpers/index.js";
22
+ import discoverSkills from "../../domains/skill/operations/discover.js";
23
+
24
+ const TMP_ROOT = join(tmpdir(), `pragma-regression-0001-${Date.now()}`);
25
+ const PKG_DIR = join(TMP_ROOT, "node_modules/@canonical/design-system");
26
+
27
+ beforeAll(() => {
28
+ const skillDir = join(PKG_DIR, "skills/design-auditor");
29
+ mkdirSync(skillDir, { recursive: true });
30
+ writeFileSync(
31
+ join(PKG_DIR, "package.json"),
32
+ JSON.stringify({ name: "@canonical/design-system", version: "0.1.2" }),
33
+ );
34
+ writeFileSync(
35
+ join(skillDir, "SKILL.md"),
36
+ `---
37
+ name: design-auditor
38
+ description: Audit design system coverage, consistency, and quality
39
+ ---
40
+
41
+ # Design Auditor
42
+ `,
43
+ );
44
+ });
45
+
46
+ afterAll(() => {
47
+ rmSync(TMP_ROOT, { recursive: true, force: true });
48
+ });
49
+
50
+ describe("skill discovery granularity (loader -> discovery contract)", () => {
51
+ it("discovers skills from sources produced by readPackageDir", async () => {
52
+ const contents = readPackageDir(PKG_DIR);
53
+ expect(contents.skills).toHaveLength(1);
54
+
55
+ const pkg: SemanticPackage = {
56
+ name: "@canonical/design-system",
57
+ version: contents.version,
58
+ source: "local",
59
+ graphs: contents.graphs,
60
+ skills: contents.skills,
61
+ };
62
+
63
+ const sources = resolveSkillSources([pkg]);
64
+ const skills = await discoverSkills(TMP_ROOT, sources);
65
+
66
+ expect(skills).toHaveLength(1);
67
+ expect(skills.at(0)?.name).toBe("design-auditor");
68
+ expect(skills.at(0)?.folderName).toBe("design-auditor");
69
+ expect(skills.at(0)?.sourcePackage).toBe("@canonical/design-system");
70
+ });
71
+ });
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Global test setup: isolate the XDG config layer.
3
+ *
4
+ * The layered config reader consults `$XDG_CONFIG_HOME/pragma/config.json`
5
+ * on every read, and global-first writes create it. Point XDG at a
6
+ * per-run temp directory so tests neither observe nor pollute the
7
+ * developer's real global configuration. Individual tests that exercise
8
+ * XDG behavior override this value themselves.
9
+ */
10
+
11
+ import { mkdtempSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { join } from "node:path";
14
+
15
+ process.env.XDG_CONFIG_HOME = mkdtempSync(join(tmpdir(), "pragma-test-xdg-"));
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Standard-noun parity pilot fixtures.
3
+ *
4
+ * The built-in `standard` list/lookup read stories re-expressed as a v0
5
+ * story-pack definition, plus the honest record of what v0 cannot yet
6
+ * reproduce. Together they form the pilot's contract: the definition is
7
+ * asserted byte-compatible with the built-in output where the format
8
+ * reaches (`standardParity.test.ts`), and every remaining divergence
9
+ * must appear in {@link PARITY_GAPS} rather than being papered over.
10
+ */
11
+
12
+ import type { StoryPackDefinition } from "../domains/shared/stories/pack/types.js";
13
+
14
+ /**
15
+ * The built-in `standard` stories as a declarative pack definition.
16
+ *
17
+ * The list SELECT mirrors the query `listStandards` sends the store,
18
+ * with variables renamed to the built-in's output fields (`?standard` →
19
+ * `?uri`, `?categoryName` → `?category`) so pack rows carry the same
20
+ * keys as the built-in summary items. The lookup mirrors the built-in
21
+ * title and inline fields as far as v0 reaches — {@link PARITY_GAPS}
22
+ * lists what it cannot express.
23
+ */
24
+ export const STANDARD_PACK_STORY: StoryPackDefinition = {
25
+ noun: "standard",
26
+ description: "List all code standards",
27
+ list: {
28
+ query: [
29
+ "SELECT ?uri ?name ?category ?description",
30
+ "WHERE {",
31
+ " ?uri a cs:CodeStandard ;",
32
+ " cs:name ?name ;",
33
+ " cs:description ?description .",
34
+ " OPTIONAL {",
35
+ " ?uri cs:hasCategory ?cat .",
36
+ " ?cat cs:slug ?category .",
37
+ " }",
38
+ "}",
39
+ "ORDER BY ?name",
40
+ ].join("\n"),
41
+ columns: [
42
+ { field: "uri", label: "IRI" },
43
+ { field: "name", label: "Name" },
44
+ { field: "category", label: "Category" },
45
+ { field: "description", label: "Description" },
46
+ ],
47
+ },
48
+ lookup: {
49
+ type: "cs:CodeStandard",
50
+ by: "cs:name",
51
+ fields: [
52
+ {
53
+ name: "category",
54
+ property: "cs:hasCategory/cs:slug",
55
+ label: "Category",
56
+ },
57
+ {
58
+ name: "description",
59
+ property: "cs:description",
60
+ label: "Description",
61
+ },
62
+ { name: "extends", property: "cs:extends", label: "Extends" },
63
+ ],
64
+ },
65
+ };
66
+
67
+ /**
68
+ * Every place the v0 pack format cannot reproduce the built-in standard
69
+ * stories — the pilot's primary output. Each entry names the concrete
70
+ * missing pack-format capability; remove an entry only when the format
71
+ * gains the capability and the parity test asserts it.
72
+ */
73
+ export const PARITY_GAPS: readonly string[] = [
74
+ "list params: packs declare no story parameters, so the built-in --category and --search filters (and their MCP tool arguments) cannot be expressed",
75
+ "list disclosure: no digest/detailed progressive-disclosure levels — the built-in --digest/--detailed flags enrich rows with extends/example/dos/donts and add disclosure meta to the MCP envelope; pack lists always render summary rows",
76
+ "list empty guard: the built-in raises EMPTY_RESULTS with an install-hint recovery when no standards load; the pack format has no emptyError hook, so a pack list renders nothing",
77
+ "list plain template: packs render the shared aligned-column table; the built-in renders stacked `name [category]` blocks with an indented description — v0 columns carry no layout or template control",
78
+ "list llm template: pack headings are `## <Noun> (<count>)` with `` `iri` — **name** value | value `` items; the built-in emits `## Standards` with `- **name** [category]` plus an indented description line — heading text and item template are not authorable",
79
+ "lookup uri field: the entity IRI cannot be declared as an inline field — v0 fields are property-derived, and a field named `uri` generates a duplicate ?uri SELECT variable the store rejects; the built-in renders `URI: cs:...` as its first lookup field",
80
+ "lookup property paths: `category` needs the two-hop path cs:hasCategory/cs:slug; the documented v0 `property` contract is a single predicate, and the path only passes validation because the prefixed-name pattern rejects `/` in the first character alone — multi-hop fields need first-class support",
81
+ "lookup data compaction: the built-in compacts `extends` in resolved data (JSON shows `cs:ComponentFolderStructure`); pack rows keep the raw IRI, so lookup JSON diverges for standards with cs:extends (plain/llm still match because renderers compact at display time)",
82
+ "lookup sections: dos/donts (cs:do/cs:dont → Example nodes with description/language/code) cannot be declared — v0 sections are single-valued field/code kinds, and the generated single-row lookup query cannot collect arrays of structured code blocks",
83
+ "lookup detailed toggle: no detailedParam — the built-in CLI adds Do/Don't sections under --detailed and MCP defaults to detailed with a summary projection (project); a pack lookup has one fixed output shape",
84
+ "lookup empty placeholders: the built-in renders `Category: —` for a standard without a category; pack renderers skip empty fields — no fallback/placeholder declaration (every current standard has a category, so outputs match today)",
85
+ "lookup glob expansion: the built-in expands glob queries (e.g. `react/*`) via expandLookupQueries before resolving; pack lookups pass names through verbatim",
86
+ "lookup by IRI: the built-in resolves an IRI or prefixed name (e.g. `cs:ComponentProps`) as the query; a pack lookup only matches the `by` property's string value, and only case-insensitively — exact-match semantics are not selectable",
87
+ "lookup recovery copy: built-in not-found errors carry cross-domain hints and the recovery message `List available standards.`; pack errors generate `List available standard entries.` — recovery copy is not authorable",
88
+ "lookup descriptions and examples: the pack lookup CLI description, tool description, names description, and examples are generated from the noun; the built-in curates all of them (e.g. `Look up detailed information for a standard by name or IRI`)",
89
+ "ink renderers: the built-in stories declare renderInk TUI views; the pack format has none",
90
+ "extra verbs: the standard noun also ships `categories` and `sample` commands; v0 packs compile only list and lookup",
91
+ "noun cutover: a pack story cannot register the noun `standard` while the built-in exists (reserved-noun guard in collectPackStories), so cutover must remove the built-in stories in the same release",
92
+ ];