@canonical/pragma-cli 0.33.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 (250) hide show
  1. package/README.md +7 -7
  2. package/dist/pragma +0 -0
  3. package/package.json +18 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +13 -4
  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.verb.ts +31 -13
  9. package/src/capabilities/capabilities/catalog.ts +18 -10
  10. package/src/capabilities/capabilities/hints.ts +13 -19
  11. package/src/capabilities/colophon/collectColophon.ts +34 -19
  12. package/src/capabilities/colophon/colophon.verb.ts +8 -4
  13. package/src/capabilities/colophon/types.ts +16 -7
  14. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  15. package/src/capabilities/config/collectConfigShow.ts +41 -4
  16. package/src/capabilities/config/fields.ts +0 -27
  17. package/src/capabilities/config/set.verb.ts +7 -3
  18. package/src/capabilities/config/show.render.test.ts +77 -5
  19. package/src/capabilities/config/show.render.ts +7 -12
  20. package/src/capabilities/config/show.verb.ts +4 -3
  21. package/src/capabilities/config/types.ts +8 -0
  22. package/src/capabilities/create/byteEquality.test.ts +78 -100
  23. package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
  24. package/src/capabilities/create/constants.ts +75 -0
  25. package/src/capabilities/create/create.mcp.test.ts +2 -2
  26. package/src/capabilities/create/create.test.ts +162 -15
  27. package/src/capabilities/create/create.verb.ts +140 -69
  28. package/src/capabilities/create/index.ts +2 -10
  29. package/src/capabilities/create/pickGenerator.ts +34 -28
  30. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  31. package/src/capabilities/create/types.ts +10 -6
  32. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  33. package/src/capabilities/distribution.test.ts +120 -0
  34. package/src/capabilities/distribution.ts +59 -0
  35. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  36. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  37. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
  38. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  39. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  40. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  41. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  42. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  43. package/src/capabilities/doctor/doctor.render.test.ts +6 -6
  44. package/src/capabilities/doctor/doctor.render.ts +2 -1
  45. package/src/capabilities/doctor/doctor.test.ts +37 -8
  46. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  47. package/src/capabilities/doctor/runChecks.ts +5 -5
  48. package/src/capabilities/doctor/types.ts +1 -1
  49. package/src/capabilities/emptyRecovery.test.ts +32 -15
  50. package/src/capabilities/graph/inspect.verb.ts +3 -2
  51. package/src/capabilities/graph/query.verb.ts +3 -2
  52. package/src/capabilities/graph/runQuery.ts +8 -5
  53. package/src/capabilities/index.ts +22 -18
  54. package/src/capabilities/info/collectInfo.ts +10 -6
  55. package/src/capabilities/info/info.render.ts +3 -2
  56. package/src/capabilities/info/info.verb.ts +3 -2
  57. package/src/capabilities/lazy.test.ts +93 -2
  58. package/src/capabilities/ontology/verbs.ts +21 -21
  59. package/src/capabilities/prompt/prompt.test.ts +12 -6
  60. package/src/capabilities/prompt/verbs.ts +24 -11
  61. package/src/capabilities/resources/provider.ts +30 -11
  62. package/src/capabilities/resources/resources.test.ts +85 -29
  63. package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
  64. package/src/capabilities/setup/operations/setupMcp.ts +2 -2
  65. package/src/capabilities/setup/operations/setupSkills.ts +3 -3
  66. package/src/capabilities/setup/setup.verb.ts +12 -5
  67. package/src/capabilities/setup/shell.ts +29 -3
  68. package/src/capabilities/skill/discover.ts +11 -5
  69. package/src/capabilities/skill/render.ts +4 -5
  70. package/src/capabilities/skill/verbs.ts +7 -7
  71. package/src/capabilities/sources/collectStatus.ts +55 -50
  72. package/src/capabilities/sources/index.ts +1 -1
  73. package/src/capabilities/sources/installSkills.ts +2 -2
  74. package/src/capabilities/sources/runUpdate.ts +113 -122
  75. package/src/capabilities/sources/sources.test.ts +111 -117
  76. package/src/capabilities/sources/status.render.test.ts +53 -24
  77. package/src/capabilities/sources/status.render.ts +24 -29
  78. package/src/capabilities/sources/status.verb.ts +15 -8
  79. package/src/capabilities/sources/types.ts +18 -25
  80. package/src/capabilities/sources/update.render.ts +9 -2
  81. package/src/capabilities/sources/update.verb.ts +12 -23
  82. package/src/capabilities/surface.test.ts +33 -10
  83. package/src/capabilities/tier.parity.test.ts +144 -0
  84. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  85. package/src/constants.test.ts +8 -6
  86. package/src/constants.ts +47 -21
  87. package/src/identity.test.ts +333 -0
  88. package/src/index.ts +2 -2
  89. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
  90. package/src/kernel/completion/emitScripts.test.ts +1 -1
  91. package/src/kernel/completion/emitScripts.ts +4 -2
  92. package/src/kernel/completion/entitySource.test.ts +185 -41
  93. package/src/kernel/completion/entitySource.ts +117 -107
  94. package/src/kernel/completion/model.test.ts +1 -1
  95. package/src/kernel/completion/model.ts +11 -0
  96. package/src/kernel/completion/safety.test.ts +82 -36
  97. package/src/kernel/completion/shellDrive.test.ts +592 -0
  98. package/src/kernel/completion/templates/fish.ts +28 -6
  99. package/src/kernel/completion/types.ts +3 -3
  100. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  101. package/src/kernel/config/defaults.test.ts +85 -0
  102. package/src/kernel/config/defaults.ts +12 -23
  103. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  104. package/src/kernel/config/findProjectConfig.ts +4 -3
  105. package/src/kernel/config/firstRun.ts +12 -5
  106. package/src/kernel/config/globalConfig.ts +2 -2
  107. package/src/kernel/config/index.ts +4 -5
  108. package/src/kernel/config/paths.ts +17 -17
  109. package/src/kernel/config/readConfig.test.ts +242 -6
  110. package/src/kernel/config/readConfig.ts +8 -8
  111. package/src/kernel/config/schema.test.ts +50 -0
  112. package/src/kernel/config/schema.ts +97 -10
  113. package/src/kernel/config/types.ts +73 -19
  114. package/src/kernel/copy.test.ts +453 -0
  115. package/src/kernel/error/PragmaError.test.ts +1 -1
  116. package/src/kernel/error/constants.ts +3 -2
  117. package/src/kernel/error/index.ts +1 -1
  118. package/src/kernel/error/recovery.test.ts +22 -45
  119. package/src/kernel/error/recovery.ts +22 -27
  120. package/src/kernel/error/types.ts +5 -4
  121. package/src/kernel/packs/collect.test.ts +195 -11
  122. package/src/kernel/packs/collect.ts +213 -58
  123. package/src/kernel/packs/compile.test.ts +163 -7
  124. package/src/kernel/packs/compile.ts +15 -12
  125. package/src/kernel/packs/disclosure.test.ts +5 -2
  126. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  127. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  128. package/src/kernel/packs/iri.ts +5 -7
  129. package/src/kernel/packs/renderPack.ts +5 -3
  130. package/src/kernel/packs/resolveEntity.ts +21 -17
  131. package/src/kernel/packs/runBodies.ts +15 -14
  132. package/src/kernel/packs/schema.ts +96 -10
  133. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  134. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  135. package/src/kernel/packs/toolDescription.test.ts +10 -2
  136. package/src/kernel/packs/types.ts +39 -4
  137. package/src/kernel/packs/uniqueness.ts +4 -4
  138. package/src/kernel/project/cli/buildProgram.test.ts +9 -4
  139. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  140. package/src/kernel/project/cli/dispatch.ts +34 -12
  141. package/src/kernel/project/cli/helpFormat.ts +1 -1
  142. package/src/kernel/project/cli/rootHelp.ts +105 -105
  143. package/src/kernel/project/mcp/buildServer.ts +11 -6
  144. package/src/kernel/project/mcp/instructions.ts +11 -6
  145. package/src/kernel/project/mcp/parity.test.ts +39 -1
  146. package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
  147. package/src/kernel/project/mcp/prompts/provider.ts +22 -8
  148. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  149. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  150. package/src/kernel/project/mcp/serve.ts +11 -3
  151. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  152. package/src/kernel/render/disclosure.ts +4 -2
  153. package/src/kernel/render/prefixes.test.ts +36 -0
  154. package/src/kernel/render/prefixes.ts +40 -13
  155. package/src/kernel/render/renderers.test.ts +1 -1
  156. package/src/kernel/runtime/graphpack/build.ts +29 -3
  157. package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
  158. package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
  159. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
  160. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
  161. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  162. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  163. package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
  164. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  165. package/src/kernel/runtime/graphpack/index.ts +1 -1
  166. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  167. package/src/kernel/runtime/graphpack/read.ts +4 -9
  168. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  169. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  170. package/src/kernel/runtime/graphpack/types.ts +61 -48
  171. package/src/kernel/runtime/paths.ts +64 -15
  172. package/src/kernel/runtime/probe.test.ts +8 -4
  173. package/src/kernel/runtime/readEntity.ts +6 -5
  174. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  175. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  176. package/src/kernel/runtime/refs/resolve.ts +78 -62
  177. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  178. package/src/kernel/runtime/resolveSources.ts +36 -25
  179. package/src/kernel/runtime/storeReadiness.ts +9 -8
  180. package/src/kernel/runtime/storeless.test.ts +1 -1
  181. package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
  182. package/src/kernel/spec/emitReference.test.ts +3 -3
  183. package/src/kernel/spec/emitReference.ts +144 -11
  184. package/src/kernel/spec/emitSurface.ts +9 -5
  185. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  186. package/src/kernel/spec/surfaceConformance.ts +8 -0
  187. package/src/kernel/spec/types.ts +29 -9
  188. package/src/kernel/spec/validate.ts +2 -1
  189. package/src/kernel/vocabulary.test.ts +164 -0
  190. package/src/kernel/vocabulary.ts +146 -0
  191. package/src/testing/PARITY_GAPS.ts +8 -2
  192. package/src/testing/behavioral/README.md +3 -3
  193. package/src/testing/behavioral/completion.test.ts +19 -6
  194. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  195. package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
  196. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  197. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  198. package/src/testing/behavioral/journeys.cli.test.ts +116 -75
  199. package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
  200. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  201. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
  202. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  203. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  204. package/src/testing/behavioral/mcpSurface.test.ts +9 -4
  205. package/src/testing/behavioral/staleCommands.test.ts +10 -0
  206. package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
  207. package/src/testing/eval/cases/assembly.ts +13 -6
  208. package/src/testing/eval/cases/effectful.ts +25 -13
  209. package/src/testing/eval/cases/readNouns.ts +22 -20
  210. package/src/testing/eval/cases/stable.ts +5 -1
  211. package/src/testing/eval/eval.test.ts +7 -1
  212. package/src/testing/fixtures/fixtureCapability.ts +45 -1
  213. package/src/testing/fixtures/graph/canonical.ts +6 -3
  214. package/src/testing/fixtures/graph/defaultPack.ts +6 -5
  215. package/src/testing/fixtures/graph/owlExport.ts +7 -5
  216. package/src/testing/helpers/fixtureGraph.ts +33 -16
  217. package/src/testing/helpers/packRuntime.ts +2 -3
  218. package/src/testing/helpers/projectCli.ts +2 -1
  219. package/src/testing/helpers/projectMcp.ts +6 -0
  220. package/src/testing/perf/budgets.test.ts +4 -0
  221. package/src/testing/perf/budgets.ts +22 -7
  222. package/src/testing/perf/globalSetup.ts +121 -6
  223. package/src/types.ts +2 -2
  224. package/src/capabilities/block/blockList.test.ts +0 -79
  225. package/src/capabilities/block/blockList.verb.ts +0 -163
  226. package/src/capabilities/block/index.ts +0 -25
  227. package/src/capabilities/block/pack.ts +0 -166
  228. package/src/capabilities/block/parity.test.ts +0 -187
  229. package/src/capabilities/block/tierChain.ts +0 -78
  230. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  231. package/src/capabilities/modifier/index.ts +0 -15
  232. package/src/capabilities/modifier/pack.ts +0 -64
  233. package/src/capabilities/standard/index.ts +0 -18
  234. package/src/capabilities/standard/pack.ts +0 -128
  235. package/src/capabilities/tier/index.ts +0 -19
  236. package/src/capabilities/tier/lookup.render.test.ts +0 -72
  237. package/src/capabilities/tier/lookup.render.ts +0 -59
  238. package/src/capabilities/tier/lookup.test.ts +0 -83
  239. package/src/capabilities/tier/lookup.verb.ts +0 -44
  240. package/src/capabilities/tier/pack.ts +0 -28
  241. package/src/capabilities/tier/runLookup.ts +0 -87
  242. package/src/capabilities/token/addConfig.render.ts +0 -27
  243. package/src/capabilities/token/addConfig.test.ts +0 -62
  244. package/src/capabilities/token/addConfig.verb.ts +0 -40
  245. package/src/capabilities/token/index.ts +0 -19
  246. package/src/capabilities/token/pack.ts +0 -58
  247. package/src/capabilities/token/runAddConfig.ts +0 -95
  248. package/src/kernel/completion/bashDrive.test.ts +0 -73
  249. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  250. package/src/kernel/runtime/lock.ts +0 -90
@@ -0,0 +1,188 @@
1
+ /**
2
+ * The package tier, end to end: a noun the binary never shipped.
3
+ *
4
+ * A package that ships `stories/*.json` and is declared as a `file:` pack
5
+ * contributes a working command to a project that declares it — resolved by
6
+ * `sources update`, hashed into the pack, written into the pack directory, read
7
+ * back storelessly at dispatch, and answered from the fixture graph. Proven both
8
+ * in-process and through the COMPILED binary in a real subprocess.
9
+ *
10
+ * The same package also ships a malformed story and a schema-invalid one. Those
11
+ * must be DROPPED, not fatal: package stories reach dispatch before the command
12
+ * tree exists, so a throw there fails every command — including `sources update`
13
+ * and `doctor`, the only two that can recover from it. Both of those are run
14
+ * here after the bad stories are in the pack.
15
+ */
16
+
17
+ import { readFileSync } from "node:fs";
18
+ import { join } from "node:path";
19
+ import { runTask } from "@canonical/task/node";
20
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
21
+ import { runChecks } from "../../capabilities/doctor/runChecks.js";
22
+ import { capabilities } from "../../capabilities/index.js";
23
+ import { buildUpdateTask } from "../../capabilities/sources/runUpdate.js";
24
+ import { loadEffectiveModules } from "../../kernel/packs/collect.js";
25
+ import { packDir, readActivePack } from "../../kernel/runtime/paths.js";
26
+ import type { VerbSpec } from "../../kernel/spec/types.js";
27
+ import {
28
+ bootFixtureRuntime,
29
+ type FixtureGraph,
30
+ } from "../helpers/fixtureGraph.js";
31
+ import { runCli } from "../helpers/runCli.js";
32
+
33
+ const TTL = `
34
+ @prefix ex: <https://example.org/kitchen#> .
35
+ @prefix owl: <http://www.w3.org/2002/07/owl#> .
36
+ @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
37
+ @prefix xsd: <http://www.w3.org/2001/XMLSchema#> .
38
+
39
+ ex:Recipe a owl:Class .
40
+ ex:name a owl:DatatypeProperty ; rdfs:domain ex:Recipe ; rdfs:range xsd:string .
41
+
42
+ ex:soup a ex:Recipe ; ex:name "Soup" .
43
+ ex:stew a ex:Recipe ; ex:name "Stew" .
44
+ `;
45
+
46
+ /** A valid read story — the noun the binary never shipped. */
47
+ const RECIPE_STORY = JSON.stringify({
48
+ noun: "recipe",
49
+ description: "List recipes.",
50
+ colophon: "Recipes are modelled as ex:Recipe individuals.",
51
+ list: {
52
+ query:
53
+ "SELECT ?uri ?name WHERE { ?uri a ex:Recipe ; ex:name ?name } ORDER BY ?name",
54
+ columns: [{ field: "uri" }, { field: "name" }],
55
+ },
56
+ });
57
+
58
+ /** Schema-invalid: a well-formed JSON document the pack grammar rejects. */
59
+ const INVALID_STORY = JSON.stringify({ noun: "Bad Noun", list: {} });
60
+
61
+ let graph: FixtureGraph;
62
+ beforeAll(async () => {
63
+ graph = await bootFixtureRuntime({
64
+ ttl: TTL,
65
+ stories: {
66
+ "recipe.json": RECIPE_STORY,
67
+ // Not JSON at all.
68
+ "broken.json": "{ this is not json",
69
+ "invalid.json": INVALID_STORY,
70
+ },
71
+ });
72
+ }, 60_000);
73
+ afterAll(async () => {
74
+ await graph.dispose();
75
+ });
76
+
77
+ describe("a package's stories reach the pack", () => {
78
+ it("sources update carries all three story files into the pack, verbatim", () => {
79
+ const hash = readActivePack(graph.cwd);
80
+ expect(hash).toBeDefined();
81
+ const carried = JSON.parse(
82
+ readFileSync(join(packDir(hash as string), "stories.json"), "utf-8"),
83
+ ) as { source: string; content: string }[];
84
+ expect(carried.map((record) => record.source)).toEqual([
85
+ "fixture/stories/broken.json",
86
+ "fixture/stories/invalid.json",
87
+ "fixture/stories/recipe.json",
88
+ ]);
89
+ expect(
90
+ carried.find((r) => r.source === "fixture/stories/recipe.json")?.content,
91
+ ).toBe(RECIPE_STORY);
92
+ });
93
+ });
94
+
95
+ describe("a package-declared noun answers reads (in-process)", () => {
96
+ it("recipe list returns rows from the package's own graph", async () => {
97
+ const { modules } = await loadEffectiveModules(capabilities, graph.cwd);
98
+ const recipe = modules.find((module) => module.name === "recipe");
99
+ expect(recipe?.story).toBe(true);
100
+ expect(recipe?.colophon).toBe(
101
+ "Recipes are modelled as ex:Recipe individuals.",
102
+ );
103
+ const list = recipe?.verbs.find(
104
+ (verb) => verb.path.join(" ") === "recipe list",
105
+ ) as VerbSpec;
106
+ const rows = (await list.run({}, graph.runtime)) as { name: string }[];
107
+ expect(rows.map((row) => row.name)).toEqual(["Soup", "Stew"]);
108
+ });
109
+
110
+ it("names the unusable stories without failing the load", async () => {
111
+ const { problems } = await loadEffectiveModules(capabilities, graph.cwd);
112
+ expect(problems.map((problem) => problem.source).sort()).toEqual([
113
+ "fixture/stories/broken.json",
114
+ "fixture/stories/invalid.json",
115
+ ]);
116
+ // Each says WHY, not just that something was wrong.
117
+ const invalid = problems.find((p) => p.source.endsWith("invalid.json"));
118
+ expect(invalid?.message).toMatch(/Invalid story/);
119
+ });
120
+ });
121
+
122
+ describe("the two recoveries still run with bad stories in the pack", () => {
123
+ it("sources update re-resolves and reports the carried story count", async () => {
124
+ const result = await runTask(await buildUpdateTask(graph.runtime));
125
+ expect(result.packs).toEqual([
126
+ {
127
+ name: "fixture",
128
+ resolved: expect.any(String),
129
+ sourceCount: 1,
130
+ storyCount: 3,
131
+ },
132
+ ]);
133
+ });
134
+
135
+ it("doctor passes `pack refs` and lists each ignored story as a failing item", async () => {
136
+ const data = await runChecks(graph.runtime);
137
+ const check = data.checks.find((entry) => entry.name === "pack refs");
138
+ // The pack DOES answer reads, so the check passes…
139
+ expect(check?.status).toBe("pass");
140
+ expect(check?.detail).toContain("2 stories ignored");
141
+ // …with the unusable stories named underneath it.
142
+ expect(check?.items?.map((item) => item.label).sort()).toEqual([
143
+ "fixture/stories/broken.json",
144
+ "fixture/stories/invalid.json",
145
+ ]);
146
+ for (const item of check?.items ?? []) expect(item.status).toBe("fail");
147
+ });
148
+ });
149
+
150
+ describe("the compiled binary answers the package's noun (subprocess)", () => {
151
+ it("pragma recipe list --format json returns the package's rows", () => {
152
+ const result = runCli(["recipe", "list", "--format", "json"], {
153
+ cwd: graph.cwd,
154
+ });
155
+ expect(result.exitCode).toBe(0);
156
+ const rows = (JSON.parse(result.stdout) as { data: { name: string }[] })
157
+ .data;
158
+ expect(rows.map((row) => row.name)).toEqual(["Soup", "Stew"]);
159
+ // The bad stories are named on stderr, never on stdout — `--format json`
160
+ // stays machine-readable.
161
+ expect(result.stderr).toContain("fixture/stories/broken.json");
162
+ expect(result.stderr).toContain("fixture/stories/invalid.json");
163
+ });
164
+
165
+ it("pragma capabilities lists the package's tools — the surface agents read", () => {
166
+ // The MCP server registers its tools from the effective modules, so a
167
+ // catalog built from the STATIC registry would omit exactly the nouns
168
+ // `tools/list` advertises. `config show` points agents here for the verbs a
169
+ // story produces; that pointer has to be true.
170
+ const result = runCli(["capabilities", "--format", "json"], {
171
+ cwd: graph.cwd,
172
+ });
173
+ expect(result.exitCode).toBe(0);
174
+ const { data } = JSON.parse(result.stdout) as {
175
+ data: { tools: { name: string }[] };
176
+ };
177
+ expect(data.tools.map((tool) => tool.name)).toContain("recipe_list");
178
+ });
179
+
180
+ it("pragma --help does NOT list recipe — the documented fast-path limit", () => {
181
+ // `--help` reads the STATIC capability set (no config, no pack read), which
182
+ // is what keeps its budget. A package-declared noun is dispatch-only.
183
+ const help = runCli(["--help"], { cwd: graph.cwd });
184
+ expect(help.exitCode).toBe(0);
185
+ expect(help.stdout).not.toContain("recipe");
186
+ // …and the command it does not advertise still runs (asserted above).
187
+ });
188
+ });
@@ -61,9 +61,10 @@ export const liveVerbs: readonly LiveVerb[] = buildLiveVerbs();
61
61
  /**
62
62
  * Read nouns' `lookup` verbs, MCP-exposed, store-backed — restricted to the
63
63
  * PACK-lookup contract (a variadic `<name...>` positional), which the B-tier
64
- * list→lookup→batch suites drive uniformly. Bespoke single-name lookups
65
- * (`tier_lookup`, `prompt_lookup`) have their own tests, so they are excluded
66
- * here structurally (by param shape, not by naming the noun — R2).
64
+ * list→lookup→batch suites drive uniformly. The one bespoke single-name lookup
65
+ * left (`prompt_lookup`) has its own tests, so it is excluded here structurally
66
+ * (by param shape, not by naming the noun — R2). `tier_lookup` joined the pack
67
+ * contract in L-OPEN-9, so it is swept here like every other declared lookup.
67
68
  */
68
69
  export const lookupVerbs: readonly LiveVerb[] = liveVerbs.filter(
69
70
  (v) =>
@@ -21,6 +21,8 @@
21
21
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
22
22
  import { capabilities } from "../../capabilities/index.js";
23
23
  import { readPackIndex } from "../../kernel/completion/entitySource.js";
24
+ import type { PackIndex } from "../../kernel/runtime/graphpack/types.js";
25
+ import { resolveSources } from "../../kernel/runtime/resolveSources.js";
24
26
  import {
25
27
  ALL_VISIBLE_CONFIG,
26
28
  CANONICAL_TTL,
@@ -41,6 +43,8 @@ let mcp: Awaited<ReturnType<typeof projectMcp>>;
41
43
  let firstNameByNoun: Map<string, string>;
42
44
  /** The registered tool list, fetched once (annotations are call-invariant). */
43
45
  let registeredTools: Awaited<ReturnType<typeof mcp.listTools>>;
46
+ /** The index of the pack the fixture's boot decision names. */
47
+ let activeIndex: PackIndex | undefined;
44
48
 
45
49
  beforeAll(async () => {
46
50
  fixture = await bootFixtureRuntime({
@@ -49,6 +53,9 @@ beforeAll(async () => {
49
53
  });
50
54
  mcp = await projectMcp(capabilities, fixture.cwd);
51
55
  registeredTools = await mcp.listTools();
56
+ activeIndex = readPackIndex(
57
+ resolveSources(await fixture.runtime.loadConfig(), fixture.cwd),
58
+ );
52
59
 
53
60
  firstNameByNoun = new Map();
54
61
  for (const v of listVerbs) {
@@ -81,15 +88,13 @@ function representativeValue(
81
88
  param: LiveVerb["spec"]["params"][number],
82
89
  ): unknown {
83
90
  if (param.name === "prefix") {
84
- const prefix = Object.keys(readPackIndex(fixture.cwd)?.prefixes ?? {}).find(
91
+ const prefix = Object.keys(activeIndex?.prefixes ?? {}).find(
85
92
  (p) => p !== "owl" && p !== "rdfs" && p !== "rdf" && p !== "xsd",
86
93
  );
87
94
  return prefix ?? "ds";
88
95
  }
89
96
  if (param.name === "uri") {
90
- const entity = readPackIndex(fixture.cwd)?.entities.find(
91
- (e) => e.box === "abox",
92
- );
97
+ const entity = activeIndex?.entities.find((e) => e.box === "abox");
93
98
  return entity?.prefixed ?? entity?.name ?? "";
94
99
  }
95
100
  // An enum positional (e.g. `config set <key>`) must
@@ -51,6 +51,16 @@ const BANNED: readonly BannedPattern[] = [
51
51
  reason:
52
52
  "retired `--llm` flag (now the `--format llm` form, auto-detected when piped)",
53
53
  },
54
+ {
55
+ pattern: /--all-tiers\b/,
56
+ reason:
57
+ "retired `--all-tiers` flag (L-OPEN-9: `block list` is the declared, unfiltered story — there is no tier filter to escape)",
58
+ },
59
+ {
60
+ pattern: /\btoken[ _]add-config\b/,
61
+ reason:
62
+ "removed `token add-config` command / `token_add-config` tool (L-OPEN-9: a mutation the declared read grammar has no verb for)",
63
+ },
54
64
  ];
55
65
 
56
66
  // Every shipped doc must be free of retired vocabulary: the README plus every
@@ -2,6 +2,11 @@
2
2
 
3
3
  exports[`eval harness — seed gate > runs the full seed (stable + read-noun) cases clean 1`] = `
4
4
  [
5
+ {
6
+ "id": "content-block-list-is-channel-independent",
7
+ "kind": "content",
8
+ "passed": true,
9
+ },
5
10
  {
6
11
  "id": "content-canonical-graph-has-4-components",
7
12
  "kind": "content",
@@ -57,11 +62,6 @@ exports[`eval harness — seed gate > runs the full seed (stable + read-noun) ca
57
62
  "kind": "content",
58
63
  "passed": true,
59
64
  },
60
- {
61
- "id": "content-normal-channel-drops-beta-only-block",
62
- "kind": "content",
63
- "passed": true,
64
- },
65
65
  {
66
66
  "id": "content-standard-list-length-equals-category-count-sum",
67
67
  "kind": "content",
@@ -113,7 +113,7 @@ exports[`eval harness — seed gate > runs the full seed (stable + read-noun) ca
113
113
  "passed": true,
114
114
  },
115
115
  {
116
- "id": "prompt-tokens-config-maps-to-token-add-config",
116
+ "id": "prompt-tokens-config-maps-to-nothing",
117
117
  "kind": "prompt",
118
118
  "passed": true,
119
119
  },
@@ -183,7 +183,7 @@ exports[`eval harness — seed gate > runs the full seed (stable + read-noun) ca
183
183
  "passed": true,
184
184
  },
185
185
  {
186
- "id": "tool-token-add-config-is-plan-first",
186
+ "id": "tool-token-noun-is-read-only",
187
187
  "kind": "tool",
188
188
  "passed": true,
189
189
  },
@@ -171,15 +171,22 @@ export const assemblyEvalCases: readonly EvalCase[] = [
171
171
  },
172
172
  },
173
173
  {
174
- id: "prompt-tokens-config-maps-to-token-add-config",
174
+ id: "prompt-tokens-config-maps-to-nothing",
175
175
  kind: "prompt",
176
176
  input:
177
- '"Generate a terrazzo tokens config" should map to `token_add-config`.',
177
+ '"Generate a terrazzo tokens config" maps to NO tool — `token_add-config` was removed in L-OPEN-9, and the token noun is read-only.',
178
178
  async expect({ mcp }) {
179
- assertIntentTool(
180
- await mcp.listTools(),
181
- "token_add-config",
182
- /token|terrazzo/i,
179
+ // The inverse of an intent-mapping case, and the point of the ruling: an
180
+ // agent asked to write a tokens config finds nothing in the catalog that
181
+ // claims to do it, rather than a verb the declared grammar cannot express.
182
+ const tools = await mcp.listTools();
183
+ assert.ok(
184
+ !tools.some(
185
+ (tool) =>
186
+ tool.name === "token_add-config" ||
187
+ /terrazzo|tokens\.config/i.test(String(tool.description ?? "")),
188
+ ),
189
+ "no tool may still offer to write a tokens config",
183
190
  );
184
191
  },
185
192
  },
@@ -170,18 +170,23 @@ export const effectfulEvalCases: readonly EvalCase[] = [
170
170
  },
171
171
  },
172
172
  {
173
- id: "tool-token-add-config-is-plan-first",
173
+ id: "tool-token-noun-is-read-only",
174
174
  kind: "tool",
175
175
  input:
176
- "token_add-config (needsStore mutation) without `confirm` returns a plan naming tokens.config.mjs and writes nothing.",
176
+ "the `token` noun exposes only reads — `token_add-config` (the tokens.config.mjs writer) was REMOVED in L-OPEN-9, not ported.",
177
177
  async expect() {
178
178
  await withCanonicalFixture(CANONICAL_CONFIG, async (mcp) => {
179
- const result = await mcp.callTool("token_add-config");
180
- assert.equal(result.ok, true);
181
- const meta = result.meta as { planOnly?: boolean };
182
- assert.equal(meta.planOnly, true);
183
- const data = result.data as { plan: string[] };
184
- assert.match(data.plan.join("\n"), /tokens\.config\.mjs/);
179
+ // This case used to pin `token_add-config`'s plan-first contract. The
180
+ // verb is gone, so it pins the ruling instead: the read surface is
181
+ // declared content and carries no mutation. (The plan-first MECHANIC is
182
+ // still covered by `tool-config-set-is-plan-first` and
183
+ // `tool-upgrade-is-plan-first`.)
184
+ const tools = (await mcp.listTools()).map((tool) => tool.name);
185
+ assert.ok(!tools.includes("token_add-config"));
186
+ assert.deepEqual(
187
+ tools.filter((name) => name.startsWith("token")).sort(),
188
+ ["token_list", "token_lookup", "token_sample"],
189
+ );
185
190
  });
186
191
  },
187
192
  },
@@ -189,14 +194,21 @@ export const effectfulEvalCases: readonly EvalCase[] = [
189
194
  id: "tool-tier-lookup-lists-scoped-blocks",
190
195
  kind: "tool",
191
196
  input:
192
- "tier_lookup {name:apps/lxd} resolves the tier and lists the blocks scoped to it (LXD Panel).",
197
+ "tier_lookup {name:[apps/lxd]} resolves the tier and expands the blocks scoped to it (LXD Panel).",
193
198
  async expect() {
194
199
  await withCanonicalFixture(ALL_VISIBLE_CONFIG, async (mcp) => {
195
- const result = await mcp.callTool("tier_lookup", { name: "apps/lxd" });
200
+ // The declared pack lookup (L-OPEN-9): variadic names in, the uniform
201
+ // `{ results, errors }` lookup envelope out, blocks as an expand.
202
+ const result = await mcp.callTool("tier_lookup", {
203
+ name: ["apps/lxd"],
204
+ });
196
205
  assert.equal(result.ok, true);
197
- const data = result.data as { name: string; blocks: string[] };
198
- assert.equal(data.name, "apps/lxd");
199
- assert.ok(data.blocks.includes("LXD Panel"));
206
+ const data = result.data as {
207
+ results: { name: string; blocks: { name: string }[] }[];
208
+ };
209
+ const tier = data.results[0];
210
+ assert.equal(tier?.name, "apps/lxd");
211
+ assert.ok(tier?.blocks.some((block) => block.name === "LXD Panel"));
200
212
  });
201
213
  },
202
214
  },
@@ -90,15 +90,15 @@ export const readNounEvalCases: readonly EvalCase[] = [
90
90
  id: "content-canonical-graph-has-4-components",
91
91
  kind: "content",
92
92
  input:
93
- "the canonical fixture graph carries 4 ds:Component individuals (Button, Modal, LXD Panel, Beta Widget); block list --all-tiers also surfaces the untiered Button Icon subcomponent (A2).",
93
+ "the canonical fixture graph carries 4 ds:Component individuals (Button, Modal, LXD Panel, Beta Widget); block_list also surfaces the untiered Button Icon subcomponent (A2).",
94
94
  async expect() {
95
95
  await withCanonicalFixture(ALL_VISIBLE_CONFIG, async (mcp) => {
96
- const result = await mcp.callTool("block_list", { allTiers: true });
96
+ const result = await mcp.callTool("block_list");
97
97
  const names = (result.data as { name: string }[])
98
98
  .map((r) => r.name)
99
99
  .sort();
100
- // --all-tiers now reveals the untiered Button Icon subcomponent
101
- // alongside the 4 tiered components (A2).
100
+ // The declared list takes no arguments and filters nothing: the 4 tiered
101
+ // components plus the untiered Button Icon subcomponent (A2).
102
102
  assert.deepEqual(names, [
103
103
  "Beta Widget",
104
104
  "Button",
@@ -168,25 +168,27 @@ export const readNounEvalCases: readonly EvalCase[] = [
168
168
  },
169
169
  },
170
170
  {
171
- id: "content-normal-channel-drops-beta-only-block",
171
+ id: "content-block-list-is-channel-independent",
172
172
  kind: "content",
173
173
  input:
174
- "the normal channel (CANONICAL_CONFIG) excludes the beta-only block; the prerelease channel (ALL_VISIBLE_CONFIG) includes it.",
174
+ "block_list returns the SAME rows on the normal channel (CANONICAL_CONFIG) and the prerelease one (ALL_VISIBLE_CONFIG) — including the beta-only block, which the normal channel used to hide (L-OPEN-9).",
175
175
  async expect() {
176
- await withCanonicalFixture(CANONICAL_CONFIG, async (mcp) => {
177
- const normal = await mcp.callTool("block_list", { allTiers: true });
178
- const normalNames = (normal.data as { name: string }[]).map(
179
- (r) => r.name,
180
- );
181
- assert.ok(!normalNames.includes("Beta Widget"));
182
- });
183
- await withCanonicalFixture(ALL_VISIBLE_CONFIG, async (mcp) => {
184
- const prerelease = await mcp.callTool("block_list", { allTiers: true });
185
- const prereleaseNames = (prerelease.data as { name: string }[]).map(
186
- (r) => r.name,
187
- );
188
- assert.ok(prereleaseNames.includes("Beta Widget"));
189
- });
176
+ const namesUnder = async (
177
+ config: typeof CANONICAL_CONFIG | typeof ALL_VISIBLE_CONFIG,
178
+ ): Promise<string[]> => {
179
+ let names: string[] = [];
180
+ await withCanonicalFixture(config, async (mcp) => {
181
+ const result = await mcp.callTool("block_list");
182
+ names = (result.data as { name: string }[]).map((r) => r.name).sort();
183
+ });
184
+ return names;
185
+ };
186
+ const normal = await namesUnder(CANONICAL_CONFIG);
187
+ const prerelease = await namesUnder(ALL_VISIBLE_CONFIG);
188
+ // The signed-off consequence: an experimental block is visible to
189
+ // everyone, on every channel, until filtering returns in declared form.
190
+ assert.ok(normal.includes("Beta Widget"));
191
+ assert.deepEqual(normal, prerelease);
190
192
  },
191
193
  },
192
194
  {
@@ -110,7 +110,11 @@ export const stableEvalCases: readonly EvalCase[] = [
110
110
  try {
111
111
  const widgetModule: CapabilityModule = {
112
112
  name: "widget",
113
- verbs: compilePack(WIDGET_PACK, "test:widget", WIDGET_PREFIXES),
113
+ verbs: compilePack(
114
+ WIDGET_PACK,
115
+ { label: "test:widget", origin: "package" },
116
+ WIDGET_PREFIXES,
117
+ ),
114
118
  };
115
119
  const mcp = await projectMcp([widgetModule], fixture.cwd);
116
120
  try {
@@ -70,5 +70,11 @@ describe("eval harness — seed gate", () => {
70
70
  expect(report.cases.length).toBeGreaterThanOrEqual(35);
71
71
  expect(report.cases.length).toBeLessThanOrEqual(60);
72
72
  expect(normalize(report.cases)).toMatchSnapshot();
73
- });
73
+ // Explicit timeout, like the other suite-level gates: this drives every
74
+ // seed case through a real runtime AND a real MCP server, and since a
75
+ // preview now performs its reads for real rather than answering from a
76
+ // mock, the effectful cases pay genuine IO. Measured 3.7s before that
77
+ // change and 5.4s after, so the vitest 5s default fails it on cost the
78
+ // PR intends to pay — not on a defect. 60s leaves room for CI contention.
79
+ }, 60_000);
74
80
  });
@@ -7,7 +7,7 @@
7
7
 
8
8
  import { tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
- import { $, gen, succeed, writeFile } from "@canonical/task";
10
+ import { $, gen, readFile, succeed, writeFile } from "@canonical/task";
11
11
  import type { CapabilityModule } from "../../kernel/spec/types.js";
12
12
 
13
13
  /** A synthetic module: noun `widget`, three verbs (one hidden). */
@@ -174,6 +174,50 @@ export const fixtureEffectsModule: CapabilityModule = {
174
174
  ],
175
175
  };
176
176
 
177
+ /**
178
+ * A synthetic module for the HONEST-preview guards (PR7). `probe graft` READS a
179
+ * file before writing beside it, so a run whose source is missing dies on its
180
+ * first effect. The mock previewer answered that read with a placeholder string
181
+ * and reported a confident plan with exit 0 — the structural false positive
182
+ * PRA-104 indicts. Under `runPreview` the read is real, so `--dry-run` (and the
183
+ * MCP plan-first twin) fail exactly where and how the run would. Test-only (D8)
184
+ * — never listed in `capabilities/index.ts`.
185
+ */
186
+ export const fixturePreviewModule: CapabilityModule = {
187
+ name: "fixture-preview",
188
+ verbs: [
189
+ {
190
+ path: ["probe", "graft"],
191
+ summary: "Copy a file's content into a sibling.",
192
+ params: [
193
+ {
194
+ kind: "string",
195
+ name: "source",
196
+ doc: "The file to read.",
197
+ positional: true,
198
+ required: true,
199
+ },
200
+ ],
201
+ output: { formatters: passthroughFormatters },
202
+ capability: {
203
+ needsStore: false,
204
+ mutates: true,
205
+ mcp: {
206
+ expose: true,
207
+ annotations: { readOnlyHint: false, openWorldHint: false },
208
+ },
209
+ },
210
+ run: (p) =>
211
+ gen(function* () {
212
+ const { source } = p as { source: string };
213
+ const content = yield* $(readFile(source));
214
+ yield* $(writeFile(`${source}.grafted`, content));
215
+ return { grafted: source };
216
+ }),
217
+ },
218
+ ],
219
+ };
220
+
177
221
  /**
178
222
  * A synthetic module that exercises every RENDER path of `emitReference` (D8 —
179
223
  * never listed in `src/capabilities/index.ts`). Distinct from `fixtureModule`,
@@ -6,9 +6,12 @@
6
6
  * `block` content-parity + `ontology`/`graph inspect` tests are protected
7
7
  * against) byte-for-byte and EXTENDS it with the entities the other bundled
8
8
  * read nouns (`standard`, `modifier`, `token`, `tier`) need, plus two more
9
- * blocks that exercise tier-chain inheritance and channel visibility (PR3's
10
- * hand-written `block list`, Risk5). Because `BLOCK_TTL` is reused verbatim,
11
- * Button/Modal content can never drift between PR3's fixture and this one.
9
+ * blocks that used to exercise tier-chain inheritance and channel visibility
10
+ * (PR3's hand-written `block list`, Risk5). Those two now serve the opposite
11
+ * purpose: L-OPEN-9 removed the filtering, so a tier-scoped and a
12
+ * channel-gated block are what prove the declared list hides NEITHER. Because
13
+ * `BLOCK_TTL` is reused verbatim, Button/Modal content can never drift between
14
+ * PR3's fixture and this one.
12
15
  *
13
16
  * Composed as one Turtle document, so ONE store answers every read noun:
14
17
  * `block`/`ontology`/`graph inspect` were already provable on `BLOCK_TTL`
@@ -19,17 +19,18 @@
19
19
  * 2. Two+ domain classes (`ds:Component` and `ds:Pattern`) so a read that keys
20
20
  * off the primary domain type is exercised across more than one class.
21
21
  * 3. An UNTIERED block (`ds:orphanWidget` — a `ds:Component` with a `ds:name`
22
- * but NO `ds:tier`). `block list`'s SELECT inner-joins `?c ds:tier ?t`, so an
23
- * untiered block is silently dropped from every listing (A2), even though
24
- * `graph query` finds it — the fixture pins both halves of that divergence.
22
+ * but NO `ds:tier`). `block list`'s SELECT once inner-joined `?c ds:tier ?t`,
23
+ * silently dropping an untiered block from every listing (A2) even though
24
+ * `graph query` found it; the declared story's OPTIONAL tier join is what
25
+ * the fixture now holds honest.
25
26
  * 4. A multilingual `rdfs:label "Button"@en, "Bouton"@fr` — two same-predicate
26
27
  * labels whose winner in the single-valued index is store-order-arbitrary
27
28
  * (A7), a shape a bare-string fixture can never produce.
28
29
  *
29
30
  * The vocabulary is the live `ds:` design-system namespace (same as
30
31
  * `blockGraph.ts`/`canonical.ts`), so `block list`, `ontology list/show`, and
31
- * `graph query` all run against real terms — the hand-written `block list` verb
32
- * hard-codes `ds:Component`/`ds:name`/`ds:tier`/`ds:release`.
32
+ * `graph query` all run against real terms — the `block` story's declared SELECT
33
+ * names `ds:Component`/`ds:Pattern`/`ds:name`/`ds:tier` directly.
33
34
  *
34
35
  * Stable anchors this fixture guarantees (pinned by the journey suite):
35
36
  * - Exactly 4 blocks in the graph: Button (`ds:Component`, global), Card
@@ -1,17 +1,19 @@
1
1
  /**
2
2
  * A realistic multi-typed OWL / Protégé-export fixture graph.
3
3
  *
4
- * The embedded `sample.ttl` (Button/Card/Dialog, each typed by exactly ONE
5
- * domain class, every block tiered, every label untagged) is a TOY: it never
6
- * exercises the counters/classifiers the way a real vocabulary does. This graph
7
- * deliberately carries the shapes that only bite real packs (audit backlog A):
4
+ * The other fixture graphs (`canonical.ts`, `defaultPack.ts` — Button/Card/
5
+ * Dialog, each typed by exactly ONE domain class, every block tiered, every
6
+ * label untagged) are TOYS: they never exercise the counters/classifiers the way
7
+ * a real vocabulary does. This graph deliberately carries the shapes that only
8
+ * bite real packs (audit backlog A):
8
9
  *
9
10
  * - Protégé DUAL-typing: an individual asserted as its domain class AND
10
11
  * `owl:NamedIndividual` — the multiset that made `entityTotal` run ~2× (A1).
11
12
  * - A MULTI-DOMAIN individual (`ex:toggle`: two domain classes) — so the
12
13
  * primary-type tie-break must be deterministic, not store-order (A3).
13
14
  * - An UNTIERED block (`ds:datePicker`: a `ds:Component` with no `ds:tier`) —
14
- * which `block list --all-tiers` must surface, not hide (A2).
15
+ * which `block list` must surface, not hide (A2; the tier join is OPTIONAL,
16
+ * and since L-OPEN-9 there is no tier filter at all).
15
17
  * - A blank-node `rdf:type` (`ex:field`'s anonymous SHACL-style class) — which
16
18
  * must NOT become a `type:"_:b0"` primary key (A6).
17
19
  * - An OWL-PUNNED subject (`ex:Slider`: a class that is ALSO a metaclass