@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
@@ -84,8 +84,8 @@ describe("surface conformance (PROTECTED)", () => {
84
84
  );
85
85
  });
86
86
 
87
- it("freezes exactly 40 designed MCP tools including info, config_show, config_set, colophon", () => {
88
- expect(golden.mcpSurface.tools).toHaveLength(40);
87
+ it("freezes exactly 37 designed MCP tools including info, config_show, config_set, colophon", () => {
88
+ expect(golden.mcpSurface.tools).toHaveLength(37);
89
89
  expect(golden.mcpSurface.tools).toContain("info");
90
90
  expect(golden.mcpSurface.tools).toContain("config_show");
91
91
  expect(golden.mcpSurface.tools).toContain("config_set");
@@ -36,6 +36,14 @@ export interface Covenant {
36
36
  readonly resources?: readonly string[];
37
37
  readonly prompts?: boolean;
38
38
  readonly instructions?: boolean;
39
+ /**
40
+ * The wire-identity RULE (L-PR6 covenant extension): `serverInfo` projects
41
+ * from the distribution, stated as placeholders rather than live values so
42
+ * the covenant does not churn per release. Documentation for conformance —
43
+ * `assertConforms` does not deep-check it; `surface.test.ts` pins the rule
44
+ * text and the live handshake, `identity.test.ts` pins it from a fork.
45
+ */
46
+ readonly serverInfo?: { readonly name: string; readonly version: string };
39
47
  };
40
48
  readonly [section: string]: unknown;
41
49
  }
@@ -42,19 +42,28 @@ export type Capability = {
42
42
  * `__complete` never boots the graph, so every source is disk-readable: the
43
43
  * precomputed pack index, the filesystem, or the prefix table.
44
44
  */
45
- export type CompletionFrom =
46
- | "index"
47
- | "skills"
48
- | "tiers"
49
- | "prompts"
50
- | "prefixes";
51
-
52
- /** A reference to one name source (with an optional prefixed type filter). */
45
+ export type CompletionFrom = "index" | "skills" | "prefixes";
46
+
47
+ /**
48
+ * Which of an index entity's name-bearing fields a param completes against.
49
+ * The kernel knows no entity families — a family is a `type` filter plus the
50
+ * field that family is addressed by, both declared at the verb.
51
+ */
52
+ export type CompletionField = "name" | "label" | "altNames";
53
+
54
+ /** A reference to one name source: where from, and which field of it. */
53
55
  export interface CompletionSourceRef {
54
56
  /** Which storeless source to read candidate names from. */
55
57
  readonly from: CompletionFrom;
56
58
  /** Prefixed type filter — meaningful only for `from: "index"` (empty = any). */
57
59
  readonly type?: string;
60
+ /**
61
+ * Which field the candidates come from — `from: "index"` only, default
62
+ * `"name"`. Declare the field the verb's lookup MATCHES on: `label` and
63
+ * `altNames` are optional index enrichment, and an entity carrying neither
64
+ * contributes no candidate rather than a token the lookup would refuse.
65
+ */
66
+ readonly field?: CompletionField;
58
67
  }
59
68
 
60
69
  /** How a partial word is matched against candidate names. */
@@ -153,7 +162,8 @@ export interface ErrorSpec {
153
162
  * `run` is the effect seam — a read returns `Promise<R>`; a mutation returns a
154
163
  * `Task<R>` the dispatcher interprets under the node / dry-run interpreters. A
155
164
  * mutation that needs async setup before its effects are known (e.g.
156
- * `sources update` resolves and builds before locking) actually returns a
165
+ * `sources update` resolves and builds before pointing the project at the pack)
166
+ * actually returns a
157
167
  * `Promise<Task<R>>`, which the dispatcher and MCP handler both `await` into a
158
168
  * `Task<R>` before interpreting. That third shape is presented through the
159
169
  * `Task<R>` arm by a cast at the one call site (`update.verb.ts`): adding a
@@ -225,6 +235,16 @@ export interface CapabilityModule {
225
235
  * it has zero covenant impact.
226
236
  */
227
237
  readonly colophon?: string;
238
+ /**
239
+ * This module was compiled — wholly or in part — from a declared read story,
240
+ * so a story in the user's own CONFIG for the same noun REPLACES it. Authored
241
+ * modules (config, ontology, doctor, …) carry no story and can never be
242
+ * replaced; neither can any shipped noun be replaced by a PACKAGE story (see
243
+ * `kernel/packs/collect.validateStories`). Set where a story is compiled
244
+ * (`capabilities/distribution.ts` and the three composite nouns), read once,
245
+ * by `kernel/packs/collect.assembleEffectiveModules`.
246
+ */
247
+ readonly story?: true;
228
248
  readonly boot?: (rt: PragmaRuntime) => void;
229
249
  /** An optional MCP resource surface (NOT a VerbSpec field — a module hook). */
230
250
  readonly mcpResources?: McpResourceProvider;
@@ -17,8 +17,9 @@ const fn = z.custom<(...args: unknown[]) => unknown>(
17
17
  );
18
18
 
19
19
  const completionSourceRefSchema = z.object({
20
- from: z.enum(["index", "skills", "tiers", "prompts", "prefixes"]),
20
+ from: z.enum(["index", "skills", "prefixes"]),
21
21
  type: z.string().optional(),
22
+ field: z.enum(["name", "label", "altNames"]).optional(),
22
23
  });
23
24
 
24
25
  const paramCompleteSchema = z.union([
@@ -0,0 +1,164 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { dataNq } from "./runtime/graphpack/embedded/pack.generated.js";
3
+ import { embeddedManifest } from "./runtime/graphpack/embedded.js";
4
+ import type { DeclaredVocabulary } from "./vocabulary.js";
5
+ import { parseVocabulary, VOCABULARY } from "./vocabulary.js";
6
+
7
+ /** A valid declaration, cloned per case so one field can be made hostile. */
8
+ const createValidDeclaration = (): DeclaredVocabulary => ({
9
+ altName: "ex:name",
10
+ prompt: {
11
+ type: "ex:Prompt",
12
+ body: "ex:body",
13
+ argument: "ex:argument",
14
+ argName: "ex:argName",
15
+ argRequired: "ex:argRequired",
16
+ },
17
+ });
18
+
19
+ /** The bindings `createValidDeclaration`'s terms resolve against. */
20
+ const BOUND = { ex: "https://ex.test/" };
21
+
22
+ describe("parseVocabulary", () => {
23
+ it("returns the same declaration when every term is readable", () => {
24
+ const declaration = createValidDeclaration();
25
+ expect(parseVocabulary(declaration, BOUND, "x.conf.ts")).toBe(declaration);
26
+ });
27
+
28
+ it("rejects an absolute IRI, naming the field and the file", () => {
29
+ // The defect this validator exists for. An absolute IRI written bare is a
30
+ // SPARQL parse error; caught at the read site it becomes "this graph has no
31
+ // prompts", indistinguishable from an empty graph. Here it is a startup
32
+ // CONFIG_ERROR that says which term is wrong and where.
33
+ let caught: unknown;
34
+ try {
35
+ parseVocabulary(
36
+ { ...createValidDeclaration(), altName: "https://ex.test/name" },
37
+ BOUND,
38
+ "x.conf.ts",
39
+ );
40
+ } catch (error) {
41
+ caught = error;
42
+ }
43
+ expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
44
+ const { message } = caught as { message: string };
45
+ expect(message).toContain("altName");
46
+ expect(message).toContain("x.conf.ts");
47
+ expect(message).toContain("https://ex.test/name");
48
+ });
49
+
50
+ it("rejects a term whose prefix nothing binds, naming that prefix", () => {
51
+ // The likelier malformation, and the one the shape check alone lets past.
52
+ // A one-character typo passes `prefix:local` and then makes every read of
53
+ // that term fail — the index silently loses `altNames`, and the prompt
54
+ // SELECT fails in a way a caller cannot tell from an empty graph. So the
55
+ // binding is checked here, where the file that declares both is one place.
56
+ let caught: unknown;
57
+ try {
58
+ parseVocabulary(
59
+ { ...createValidDeclaration(), altName: "exx:name" },
60
+ BOUND,
61
+ "x.conf.ts",
62
+ );
63
+ } catch (error) {
64
+ caught = error;
65
+ }
66
+ expect(caught).toMatchObject({ code: "CONFIG_ERROR" });
67
+ const { message, recovery } = caught as {
68
+ message: string;
69
+ recovery?: { message: string };
70
+ };
71
+ expect(message).toContain("altName");
72
+ expect(message).toContain("exx:");
73
+ expect(recovery?.message).toContain("exx");
74
+ });
75
+
76
+ it("rejects a nested prompt term whose prefix nothing binds", () => {
77
+ const declaration = createValidDeclaration();
78
+ expect(() =>
79
+ parseVocabulary(
80
+ {
81
+ ...declaration,
82
+ prompt: { ...declaration.prompt, type: "other:Prompt" },
83
+ },
84
+ BOUND,
85
+ "x.conf.ts",
86
+ ),
87
+ ).toThrow(/prompt\.type/);
88
+ });
89
+
90
+ it("rejects a term naming an inherited Object property as its prefix", () => {
91
+ // `prefixes` is a plain record, so an unbound prefix must be UNBOUND, not
92
+ // whatever the prototype chain answers. Without an own-key check
93
+ // `constructor:name` passes and expands to a stringified function.
94
+ expect(() =>
95
+ parseVocabulary(
96
+ { ...createValidDeclaration(), altName: "constructor:name" },
97
+ BOUND,
98
+ "x.conf.ts",
99
+ ),
100
+ ).toThrow(/constructor:/);
101
+ });
102
+
103
+ it("rejects a nested prompt term, naming its path", () => {
104
+ const declaration = createValidDeclaration();
105
+ expect(() =>
106
+ parseVocabulary(
107
+ {
108
+ ...declaration,
109
+ prompt: { ...declaration.prompt, body: "<ex:body>" },
110
+ },
111
+ BOUND,
112
+ "x.conf.ts",
113
+ ),
114
+ ).toThrow(/prompt\.body/);
115
+ });
116
+
117
+ it("rejects an empty, unprefixed, whitespace-bearing or malformed term", () => {
118
+ for (const term of ["", "name", "ex:", ":name", "ex: name", "ex:na me"]) {
119
+ expect(() =>
120
+ parseVocabulary(
121
+ { ...createValidDeclaration(), altName: term },
122
+ BOUND,
123
+ "x.conf.ts",
124
+ ),
125
+ ).toThrow(/altName/);
126
+ }
127
+ });
128
+ });
129
+
130
+ describe("the shipped declaration", () => {
131
+ it("declares the name property entities are addressed by", () => {
132
+ expect(VOCABULARY.altName).toBe("ds:name");
133
+ });
134
+
135
+ it("names a property the shipped pack actually carries", () => {
136
+ // The declaration and the committed pack are produced independently, and
137
+ // NOTHING makes the pack follow the declaration: `contentHash` covers pack
138
+ // sources only, and `buildPack` reuses any complete cache directory, so
139
+ // editing `altName` here re-mints no index and `sources update` cannot
140
+ // repair the mismatch. Left silent, the CLI would offer the old property's
141
+ // tokens from the index and match the new one in every lookup.
142
+ //
143
+ // So expand the declared term through the pack's OWN manifest and look for
144
+ // it as a predicate in the pack's data. The prompt terms get no such check
145
+ // — they are a read contract, and this graph carries no prompt entities.
146
+ const [prefix, local] = VOCABULARY.altName.split(":");
147
+ const namespace = embeddedManifest().prefixes[prefix ?? ""];
148
+ expect(namespace, `pack binding for \`${prefix}:\``).toBeDefined();
149
+ expect(
150
+ dataNq.includes(` <${namespace}${local}> `),
151
+ `\`${VOCABULARY.altName}\` used as a predicate in the shipped pack`,
152
+ ).toBe(true);
153
+ });
154
+
155
+ it("declares the prompt entity shape, value for value", () => {
156
+ expect(VOCABULARY.prompt).toEqual({
157
+ type: "ds:Prompt",
158
+ body: "ds:promptBody",
159
+ argument: "ds:promptArgument",
160
+ argName: "ds:argName",
161
+ argRequired: "ds:argRequired",
162
+ });
163
+ });
164
+ });
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The domain vocabulary this distribution declares, validated once at load.
3
+ *
4
+ * The kernel reads a graph whose vocabulary it must not name. Two of its reads
5
+ * need domain terms: the pack index projects an alternative-name property into
6
+ * `altNames` (what a family is addressed by, hence what completes and what a
7
+ * lookup resolves), and the MCP prompt surface materializes prompt entities
8
+ * with a generated SELECT. Both take their terms from here, and the bundled
9
+ * capability packs that query the same terms read the same declaration — so
10
+ * complete and resolve cannot drift apart.
11
+ *
12
+ * A DISTRIBUTION DECLARATION, not a config layer field, and deliberately so.
13
+ * Both readers sit where the config layer is unreachable: the storeless
14
+ * `__complete` fast path builds a completion ref at module load, and the index
15
+ * is built inside the pack builder. Made layerable, a user setting it in their
16
+ * project config would change nothing anywhere — silence, which is the exact
17
+ * failure a dead config field is. A fork ships its own binary: it edits the
18
+ * declaration and rebuilds.
19
+ *
20
+ * Terms must be PREFIXED NAMES whose prefix the distribution BINDS, and
21
+ * {@link parseVocabulary} rejects anything else by name. They are interpolated
22
+ * into SPARQL, where an absolute IRI written bare is a parse error and an
23
+ * unbound prefix is either a parse error or a silently empty result — and a
24
+ * failed read is indistinguishable from an empty graph, which is how a
25
+ * malformed declaration would otherwise surface as "this distribution has no
26
+ * prompts". Both halves matter: rejecting the shape alone still lets a
27
+ * one-character typo (`ds:name` → `dss:name`) pass startup and then drop every
28
+ * `altNames` from the index while reporting a built store as unbuilt. Failing
29
+ * at module load is correct for a compiled-in build defect: the distribution
30
+ * config's own layer validation (`config/defaults.ts`) already throws the same
31
+ * way.
32
+ *
33
+ * Standard vocabulary (`rdfs:label`, `rdfs:comment`) is deliberately absent —
34
+ * it is the same in every graph, and the index already treats it as universal.
35
+ */
36
+
37
+ import conf, { vocabulary } from "../../pragma.conf.js";
38
+ import { PragmaError } from "./error/PragmaError.js";
39
+
40
+ /** The domain terms the kernel reads a distribution's graph with. */
41
+ export interface DeclaredVocabulary {
42
+ /**
43
+ * The property an entity family is addressed by, beside `rdfs:label` — the
44
+ * tokens the index offers as candidates and a bespoke lookup matches on.
45
+ */
46
+ readonly altName: string;
47
+ /** The prompt entity shape the MCP prompt surface reads. */
48
+ readonly prompt: {
49
+ /** The class every prompt entity carries. */
50
+ readonly type: string;
51
+ /** The template body, with `{{arg}}` placeholders. */
52
+ readonly body: string;
53
+ /** Links a prompt to one of its declared arguments. */
54
+ readonly argument: string;
55
+ /** An argument's name. */
56
+ readonly argName: string;
57
+ /** Whether an argument is required. */
58
+ readonly argRequired: string;
59
+ };
60
+ }
61
+
62
+ /** A prefixed name — `prefix:local`, the only form a generated query can read. */
63
+ const PREFIXED_NAME = /^[A-Za-z][A-Za-z0-9_-]*:[A-Za-z_][A-Za-z0-9_.-]*$/;
64
+
65
+ /**
66
+ * The distribution config file, quoted in the diagnostics.
67
+ *
68
+ * The literal, not `${BIN_NAME}.conf.ts`: the import above names this file, and
69
+ * so do four other modules, so a fork CANNOT rename it — deriving the name from
70
+ * the fork's would send its maintainer to a path that is not on disk.
71
+ * `config/defaults.ts` names the same file for the same reason.
72
+ */
73
+ const CONF_FILE = "pragma.conf.ts";
74
+
75
+ /**
76
+ * Hold one declared term to a form a generated query can actually read: the
77
+ * `prefix:local` shape, AND a prefix the distribution binds.
78
+ *
79
+ * @param field - The term's path in the declaration, for the message.
80
+ * @param term - The declared value.
81
+ * @param prefixes - The distribution's declared namespace bindings.
82
+ * @param source - The file the declaration was read from.
83
+ * @throws PragmaError CONFIG_ERROR when the term is not `prefix:local`, or
84
+ * names a prefix `prefixes` does not bind.
85
+ */
86
+ function assertReadableTerm(
87
+ field: string,
88
+ term: string,
89
+ prefixes: Readonly<Record<string, string>>,
90
+ source: string,
91
+ ): void {
92
+ const reject = (problem: string, fix: string): never => {
93
+ throw PragmaError.configError(
94
+ `Invalid vocabulary in ${source}: \`${field}\` ${problem}, not ${JSON.stringify(term)}.`,
95
+ { recovery: { message: `In ${source}, ${fix}` } },
96
+ );
97
+ };
98
+ if (!PREFIXED_NAME.test(term)) {
99
+ reject(
100
+ "must be a prefixed name like `ex:thing`",
101
+ `write \`${field}\` as \`<prefix>:<local>\`. These terms are interpolated into queries, which cannot read an absolute IRI here.`,
102
+ );
103
+ }
104
+ const prefix = term.slice(0, term.indexOf(":"));
105
+ if (!Object.hasOwn(prefixes, prefix)) {
106
+ reject(
107
+ `must use a prefix \`prefixes\` binds, and nothing binds \`${prefix}:\``,
108
+ `either bind \`${prefix}\` under \`prefixes\` or write \`${field}\` with a prefix that is bound. An unbound prefix makes the generated query fail, which a read cannot tell apart from an empty graph.`,
109
+ );
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Validate a declared vocabulary.
115
+ *
116
+ * @param raw - A distribution's `vocabulary` export.
117
+ * @param prefixes - That distribution's `prefixes`, which must bind every
118
+ * prefix the terms use.
119
+ * @param source - The file it was declared in, quoted in errors.
120
+ * @returns The same declaration, once every term is readable.
121
+ * @throws PragmaError CONFIG_ERROR naming the offending field and its value.
122
+ */
123
+ export function parseVocabulary(
124
+ raw: DeclaredVocabulary,
125
+ prefixes: Readonly<Record<string, string>>,
126
+ source: string,
127
+ ): DeclaredVocabulary {
128
+ assertReadableTerm("altName", raw.altName, prefixes, source);
129
+ for (const [term, value] of Object.entries(raw.prompt)) {
130
+ assertReadableTerm(`prompt.${term}`, value, prefixes, source);
131
+ }
132
+ return raw;
133
+ }
134
+
135
+ /**
136
+ * This distribution's validated vocabulary.
137
+ *
138
+ * The declaration is type-checked at this call — a fork that drops or mistypes
139
+ * a term fails `tsc`, and one that writes an unreadable term fails here at
140
+ * startup rather than at the read that would have swallowed it.
141
+ */
142
+ export const VOCABULARY = parseVocabulary(
143
+ vocabulary,
144
+ conf.prefixes ?? {},
145
+ CONF_FILE,
146
+ );
@@ -78,13 +78,19 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
78
78
  id: "generic-renderers-not-bespoke-templates",
79
79
  area: "render",
80
80
  description:
81
- "Every noun's plain/llm output — including the one hand-written verb, `block list` — renders through the SAME generic list/lookup renderer (`kernel/render/renderers.ts`), driven by declarative column/field/section metadata. There are no per-noun hand-authored render templates left to keep in parity.",
81
+ "Every noun's plain/llm output renders through the SAME generic list/lookup renderer (`kernel/render/renderers.ts`), driven by declarative column/field/section metadata. There are no per-noun hand-authored render templates left to keep in parity (the last hand-written verb, `block list`, became declared content in L-OPEN-9).",
82
82
  },
83
83
  {
84
84
  id: "block-lookup-not-tier-scoped",
85
85
  area: "block",
86
86
  description:
87
- "`block lookup` resolves by `ds:name` GLOBALLY, with no tier or channel filtering — only the hand-written `block list` is tier/channel-aware (PR3 Risk5, `capabilities/block/tierChain.ts`). A same-named block declared at two tiers cannot be disambiguated by `block lookup` alone (the old shell's tier-scoped lookup disambiguation has no v2 equivalent).",
87
+ "`block lookup` resolves by `ds:name` GLOBALLY, with no tier or channel filtering. A same-named block declared at two tiers cannot be disambiguated by plain name (the old shell's tier-scoped lookup disambiguation has no v2 equivalent) — address it by prefixed IRI instead. Since L-OPEN-9 NOTHING is tier/channel-aware: the hand-written, tier-chain-filtered `block list` was removed with its `tierChain.ts`.",
88
+ },
89
+ {
90
+ id: "block-list-unfiltered",
91
+ area: "block",
92
+ description:
93
+ "L-OPEN-9 (owner-signed): `block list` is the block story's compiled, UNFILTERED list — it shows every block, including experimental/alpha ones, for everyone, until filtering returns in declared form. The old tier-chain inheritance, channel visibility, and `--all-tiers` escape were hand-written filtering the pack grammar has no term for, and the ruling removed them rather than growing the grammar to keep them. `config.tier` consequently scopes nothing (reported by `config show`/`info` only); `config.channel` keeps only its npm dist-tag role for `upgrade`/`info`.",
88
94
  },
89
95
  {
90
96
  id: "graph-query-deferred",
@@ -126,7 +132,7 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
126
132
  id: "info-entitycount-storeless-not-triplecount",
127
133
  area: "info",
128
134
  description:
129
- "`info`'s entity total comes from the storeless pack index (`readPackIndex` → sum of `instanceCountByType`), NEVER a store boot — so the storeless invariant (`store.booted === false`) holds. The old `collectStoreSummary`'s SPARQL triple-count is DROPPED (`sources status` owns the per-source breakdown; info shows one total). `capabilities/info/collectInfo.ts`.",
135
+ "`info`'s entity total comes from the storeless pack index (`readPackIndex` → `entityTotal` = count of DISTINCT abox subjects), NEVER a store boot — so the storeless invariant (`store.booted === false`) holds. It is deliberately NOT the sum of `instanceCountByType`: that raw multiset double-counts real OWL exports (individual typed as its class AND `owl:NamedIndividual`, plus `owl:Class`/property meta-buckets), so the total ran ~2× (A1). The old `collectStoreSummary`'s SPARQL triple-count is DROPPED (`sources status` owns the per-source breakdown; info shows one total). `capabilities/info/collectInfo.ts`.",
130
136
  },
131
137
  {
132
138
  id: "setup-skills-undo-recompute",
@@ -40,7 +40,7 @@ the emitted set legitimately grows PR-by-PR.
40
40
  | `agentSession.mcp.test.ts` | A multi-step agent MCP session (list → pick → lookup) + the SPARQL escape hatch. |
41
41
  | `mcpSurface.test.ts` | Every MCP tool the live catalog exposes is callable and well-formed — the "surface ⊆ covenant, exercised" proof. |
42
42
  | `disclosure.test.ts` | The cross-noun disclosure sweep (mechanism itself is PR3's/kernel's protected unit test — this only sweeps the noun SET). |
43
- | `journeys.cli.test.ts` | The `block list` tier-chain/channel journey (the one hand-written verb). |
43
+ | `journeys.cli.test.ts` | The `block list` journey — since L-OPEN-9 it pins that the declared list is UNFILTERED: four tier/channel configs, one answer. |
44
44
  | `completion.test.ts` | The THIN main-line `__complete` candidate contract (§6 — see "Completion" below). |
45
45
 
46
46
  Plus `testing/eval/` (the eval harness + seed cases) and `testing/PARITY_GAPS.ts`
@@ -71,7 +71,7 @@ import { CANONICAL_TTL, CANONICAL_CONFIG } from "../fixtures/graph/canonical.js"
71
71
 
72
72
  const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL, config: CANONICAL_CONFIG });
73
73
  try {
74
- // fixture.cwd carries a REAL pragma.lock.json — bootRuntime(flags, fixture.cwd),
74
+ // fixture.cwd is pointed at a REAL built pack — bootRuntime(flags, fixture.cwd),
75
75
  // executeVerb(...), projectMcp(modules, fixture.cwd), and (if you must)
76
76
  // runCli(args, { cwd: fixture.cwd }) all resolve the SAME cached pack.
77
77
  } finally {
@@ -80,7 +80,7 @@ try {
80
80
  ```
81
81
 
82
82
  Use `packRuntime.ts#buildFixtureRuntime` (PR3's helper) instead when you only
83
- need ONE surface (a direct `verb.run()` call) — it's cheaper (no lock file).
83
+ need ONE surface (a direct `verb.run()` call) — it's cheaper (no real build).
84
84
  Reach for `bootFixtureRuntime` specifically when you need CLI and MCP (or a
85
85
  spawn) to agree on the same store.
86
86
 
@@ -36,9 +36,19 @@ describe("auto-LLM on a piped (non-TTY) stdout (A10, e2e)", () => {
36
36
  expect(result.stdout).toContain("pragma v");
37
37
  });
38
38
 
39
- it("an explicit --llm matches the auto-selected output", () => {
39
+ it("an explicit --format llm matches the auto-selected output", () => {
40
40
  const auto = runCli(["info"]);
41
- const explicit = runCli(["info", "--llm"]);
41
+ const explicit = runCli(["info", "--format", "llm"]);
42
42
  expect(explicit.stdout).toBe(auto.stdout);
43
43
  });
44
+
45
+ it("rejects the removed --llm flag as an unknown option (exit 2)", () => {
46
+ // The covenant's whole premise: the dedicated `--llm` flag is GONE, folded
47
+ // into `--format llm` (and the piped auto-default). A real command must now
48
+ // FAIL on it as a usage error — not silently accept or ignore it.
49
+ const result = runCli(["block", "list", "--llm"]);
50
+ expect(result.exitCode).toBe(2);
51
+ expect(result.stderr).toContain("unknown option");
52
+ expect(result.stderr).toContain("--llm");
53
+ });
44
54
  });
@@ -4,7 +4,7 @@
4
4
  * The full completion engine (`kernel/completion`: parse → resolve → rank, the
5
5
  * spec-derived model, the entity seam) owns the unit-level guarantees; this
6
6
  * pins the behaviour end-to-end against the LIVE capabilities and — for the
7
- * entity tier — against a REAL built pack index on disk (the locked-pack branch
7
+ * entity tier — against a REAL built pack index on disk (the built-pack branch
8
8
  * of the storeless reader, which the engine's own tests exercise only via the
9
9
  * embedded fallback). Parameterized over the live read surface so noun/verb
10
10
  * renames never churn it.
@@ -89,21 +89,34 @@ describe("flags are kebab-cased at emission (the live, testable guarantee)", ()
89
89
  });
90
90
  });
91
91
 
92
- describe("entity params resolve from a real built pack index (B10)", () => {
93
- it("a lookup's entity positional completes from the built pack index", async () => {
92
+ describe("entity params resolve from the project's built pack, not the embedded snapshot (B10)", () => {
93
+ it("a lookup's entity positional offers the built pack's subjects and none of the snapshot's", async () => {
94
94
  const fixture = await bootFixtureRuntime({
95
95
  ttl: CANONICAL_TTL,
96
96
  config: ALL_VISIBLE_CONFIG,
97
97
  });
98
98
  try {
99
- // The locked-pack branch of the storeless reader: a real index.json on
99
+ // The built-pack branch of the storeless reader: a real index.json on
100
100
  // disk under the fixture cwd, read without booting the store.
101
101
  const all = await runComplete(
102
102
  ["block", "lookup", ""],
103
103
  capabilities,
104
104
  indexCompletionEnv(fixture.cwd),
105
105
  );
106
- expect(all.length).toBeGreaterThan(0);
106
+ // The four `ds:Component` subjects the canonical fixture declares
107
+ // (`blockGraph.ts`'s Button/Modal plus `canonical.ts`'s LXD Panel and
108
+ // Beta Widget) — the fixture's own data, not a recomputation. Stating
109
+ // the SET is what makes the distribution's own snapshot names provably
110
+ // absent: a pointer read that regressed to the embedded fallback would
111
+ // complete another graph's names while every read answered correctly,
112
+ // and the old `narrowed.length < all.length` heuristic only noticed by
113
+ // the accident that no snapshot name contains `ds:b`.
114
+ expect([...all].sort()).toEqual([
115
+ "ds:betaWidget",
116
+ "ds:button",
117
+ "ds:lxdPanel",
118
+ "ds:modal",
119
+ ]);
107
120
 
108
121
  const narrowed = await runComplete(
109
122
  ["block", "lookup", "ds:b"],
@@ -111,7 +124,7 @@ describe("entity params resolve from a real built pack index (B10)", () => {
111
124
  indexCompletionEnv(fixture.cwd),
112
125
  );
113
126
  // Every ranked candidate carries the partial (case-insensitive), and the
114
- // filter is strictly narrowing — the index tier really ran.
127
+ // filter is strictly narrowing — the ranker really ran over this index.
115
128
  expect(
116
129
  narrowed.every((name) => name.toLowerCase().includes("ds:b")),
117
130
  ).toBe(true);
@@ -125,8 +138,8 @@ describe("entity params resolve from a real built pack index (B10)", () => {
125
138
 
126
139
  describe("global flags complete for a dash prefix, at any position", () => {
127
140
  it("completes global flags regardless of position", async () => {
128
- await expect(runComplete(["--l"], capabilities)).resolves.toEqual([
129
- "--llm",
141
+ await expect(runComplete(["--d"], capabilities)).resolves.toEqual([
142
+ "--detail",
130
143
  ]);
131
144
  });
132
145
  });
@@ -64,6 +64,18 @@ describe("declares its levels within the canonical index (B6)", () => {
64
64
  });
65
65
  });
66
66
 
67
+ describe("disclosure lookups declare the aligned canonical level set (B4)", () => {
68
+ // B4: block used to declare only `[summary, detailed]` while standard declared
69
+ // the full `[summary, standard, detailed]`, so a config `detail=standard` named
70
+ // a level block never advertised. Every disclosure lookup now declares the same
71
+ // canonical ladder; per-noun DEFAULTS may still differ (domain-tuned).
72
+ it.each(
73
+ disclosureLookupVerbs,
74
+ )("$noun: levels === the canonical ladder", (v) => {
75
+ expect(v.spec.disclosure?.levels).toEqual([...DETAIL_LEVELS]);
76
+ });
77
+ });
78
+
67
79
  describe("detail:summary vs detail:detailed — key-set growth (B6)", () => {
68
80
  it.each(
69
81
  disclosureLookupVerbs,