@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
@@ -15,6 +15,8 @@
15
15
  * NOT imported — this is a kernel-layer emitter).
16
16
  */
17
17
 
18
+ import { BIN_NAME, PROJECT_CONFIG_FILENAME } from "../../constants.js";
19
+ import type { RawConfig } from "../config/types.js";
18
20
  import { ERROR_CODES } from "../error/constants.js";
19
21
  import {
20
22
  FIXED_SURFACE,
@@ -86,12 +88,12 @@ function compareDocVerbs(a: VerbSpec, b: VerbSpec): number {
86
88
  return labelA < labelB ? -1 : 1;
87
89
  }
88
90
 
89
- /** The full CLI invocation head for a verb (`pragma <noun>` or `pragma <noun> <verb>`). */
91
+ /** The full CLI invocation head for a verb (`<bin> <noun>` or `<bin> <noun> <verb>`). */
90
92
  function formatInvocation(verb: VerbSpec): string {
91
93
  const sub = verb.path[1];
92
94
  return sub === undefined
93
- ? `pragma ${verb.path[0]}`
94
- : `pragma ${verb.path[0]} ${sub}`;
95
+ ? `${BIN_NAME} ${verb.path[0]}`
96
+ : `${BIN_NAME} ${verb.path[0]} ${sub}`;
95
97
  }
96
98
 
97
99
  /** Format a positional param as its usage token (`<name>`/`[name]`, `...` when variadic). */
@@ -211,7 +213,7 @@ function formatExamples(examples: readonly Example[] | undefined): string {
211
213
  function formatVerbAttributes(verb: VerbSpec): string {
212
214
  const bullets = [
213
215
  verb.capability.needsStore
214
- ? "- Store: reads the local store (`pragma sources update` builds it)."
216
+ ? `- Store: reads the local store (\`${BIN_NAME} sources update\` builds it).`
215
217
  : "- Store: storeless.",
216
218
  ];
217
219
  if (verb.capability.mutates) {
@@ -251,7 +253,7 @@ function renderCommandSection(verb: VerbSpec): string {
251
253
  function renderCommandsPage(verbs: readonly VerbSpec[]): string {
252
254
  const blocks = [
253
255
  "# CLI command reference",
254
- "Every `pragma` command, grouped by noun. Generated from the live capability grammar — do not edit by hand.",
256
+ `Every \`${BIN_NAME}\` command, grouped by noun. Generated from the live capability grammar — do not edit by hand.`,
255
257
  "Global flags apply to every command: `--format <plain|llm|json>` (auto-detected — the llm/condensed-Markdown form turns on when output is piped), `--verbose`, and `--detail <summary|standard|detailed>`.",
256
258
  ];
257
259
  let currentNoun = "";
@@ -377,12 +379,12 @@ function renderNonToolSurface(modules: readonly CapabilityModule[]): string {
377
379
  const bullets: string[] = [];
378
380
  if (resources.length > 0) {
379
381
  bullets.push(
380
- `- **Resources**: ${resources.map((r) => `\`${r}\``).join(", ")} — entity reads addressed by URI (listing and autocomplete are storeless over the pack index).`,
382
+ `- **Resources**: ${resources.map((r) => `\`${r}\``).join(", ")} — entity reads addressed by URI (listing and autocomplete are storeless over the pack index). The template — its scheme and the \`_meta\` taxonomy keys its entries carry — is frozen protocol identity, served unchanged by every distribution: clients persist resource URIs, so the scheme never follows a fork's name.`,
381
383
  );
382
384
  }
383
385
  if (prompts) {
384
386
  bullets.push(
385
- "- **Prompts**: the design system's workflow templates are offered natively over `prompts/list` and `prompts/get`, and as the `prompt_list` / `prompt_lookup` content tools.",
387
+ "- **Prompts**: the workflow prompt templates the active graph declares are offered natively over `prompts/list` and `prompts/get`, and as the `prompt_list` / `prompt_lookup` content tools. A graph declaring none leaves both views empty.",
386
388
  );
387
389
  }
388
390
  bullets.push(
@@ -399,7 +401,7 @@ function renderToolsPage(
399
401
  const tools = verbs.filter((verb) => verb.capability.mcp.expose);
400
402
  const blocks = [
401
403
  "# MCP tool reference",
402
- "Every tool the pragma MCP server exposes, plus its non-tool surface. Generated from the live capability grammar — do not edit by hand.",
404
+ `Every tool the ${BIN_NAME} MCP server exposes, plus its non-tool surface. Generated from the live capability grammar — do not edit by hand. The server's \`serverInfo\` is a projection, not a constant: it introduces itself on the wire under the distribution's declared name at the package version, so a client should read \`serverInfo\` rather than assume a name.`,
403
405
  "Mutating tools are plan-first: called without `confirm: true` they return the plan they WOULD apply; called with `confirm: true` they execute. A mutating tool also accepts an optional absolute `cwd`.",
404
406
  ];
405
407
  for (const verb of tools) {
@@ -442,6 +444,135 @@ function renderErrorsPage(): string {
442
444
  ]);
443
445
  }
444
446
 
447
+ /** One documented config field: the type label a user sees, and the prose. */
448
+ interface ConfigFieldDoc {
449
+ /** The type as a reader needs it, e.g. `string (optional)`. */
450
+ readonly type: string;
451
+ /** What the field does, and anything true about it a reader would not guess. */
452
+ readonly notes: string;
453
+ }
454
+
455
+ /**
456
+ * Every field a config layer may declare. Typed as an exhaustive `Record` over
457
+ * `keyof RawConfig`, so a field added to the type without a row here — or a row
458
+ * left behind by a field that was removed — is a COMPILE error, exactly as
459
+ * {@link ERROR_CODE_DESCRIPTIONS} works for the error kernel. Colocated
460
+ * (single-use) rather than in a module of its own: only `renderConfigPage`
461
+ * reads it, and a third artifact for two consumers to keep in step is what this
462
+ * discipline exists to avoid.
463
+ *
464
+ * The `type` column is authored prose; the page says so. Its OPTIONALITY is
465
+ * checked against the zod validator by `kernel/config/schema.test.ts`.
466
+ */
467
+ const CONFIG_FIELD_DOCS: Record<keyof RawConfig, ConfigFieldDoc> = {
468
+ name: {
469
+ type: "string (optional)",
470
+ notes:
471
+ "Distribution-only — see below. The binary's own name, read from the distribution config at module load.",
472
+ },
473
+ help: {
474
+ type: "string (optional)",
475
+ notes:
476
+ "Distribution-only — see below. The one-line blurb on the front door and in the MCP handshake.",
477
+ },
478
+ colophon: {
479
+ type: "object (optional)",
480
+ notes:
481
+ "Distribution-only — see below. The toolchain's own story, rendered first by the `colophon` verb and titled with the distribution's name: `{ markdown, summary? }`, both Markdown bodies with no leading H1 (`summary` is the condensed `--format llm` form). Declared content, not code — a fork edits it to tell its own story.",
482
+ },
483
+ issuesUrl: {
484
+ type: "URL string (optional)",
485
+ notes:
486
+ "Distribution-only — see below. Where the first-run note asks users to report problems.",
487
+ },
488
+ tier: {
489
+ type: "string (optional)",
490
+ notes:
491
+ "Accepted by the validator and SCOPES NOTHING: since the block list became declared content, no read filters by tier — the value is only reported by `config show` and `info`. Set it with `config set tier <path>`; `none`, `default` or `-` clear it.",
492
+ },
493
+ channel: {
494
+ type: "`normal` | `experimental` | `prerelease` (optional)",
495
+ notes:
496
+ "Selects the npm dist-tag `upgrade` and `info` check the registry with. It no longer scopes graph reads — no read filters by channel. Defaults to `normal`. Set it with `config set channel <name>`.",
497
+ },
498
+ detail: {
499
+ type: "`summary` | `standard` | `detailed` (optional)",
500
+ notes:
501
+ "Default progressive-disclosure level. A closed enum, like `channel`: any other value fails at load with a `CONFIG_ERROR` naming the file and the three levels. Set it with `config set detail <level>`.",
502
+ },
503
+ packs: {
504
+ type: "array (optional)",
505
+ notes:
506
+ "Semantic pack sources built by `sources update`. Each entry is a bare npm name or `{ name, source, stories? }`; `stories` are read stories the pack supplies, in the pack grammar.",
507
+ },
508
+ stories: {
509
+ type: "array (optional)",
510
+ notes:
511
+ "Read stories not attached to any pack, in the pack grammar. Compiled at dispatch, and they win over the same noun declared under `packs[].stories`.",
512
+ },
513
+ prefixes: {
514
+ type: "record (optional)",
515
+ notes:
516
+ "Namespace prefixes the pack is built with — they win every harvest, so this decides which IRI a prefix binds in the store and the index. Every surface uses the compiled-in display/expansion map to compact and expand prefixed names; only the DISTRIBUTION layer seeds it, because it is also read on the storeless fast path, before any config layer exists.",
517
+ },
518
+ completion: {
519
+ type: "object (optional)",
520
+ notes:
521
+ "Completion policy read when `setup completions` emits a script: `minChars` and a per-noun `families` opt-out. It is the one field `config show` carries with NO origin at all.",
522
+ },
523
+ };
524
+
525
+ /**
526
+ * The fields `config show` prints with the layer that supplied them, in the
527
+ * order it prints them. NOT every field: `prefixes` and `completion` reach only
528
+ * the JSON payload, `stories` reaches only its `origins` key, and the four
529
+ * distribution-only fields reach neither. The page said the opposite — that
530
+ * every field but `completion` was reported — which is the claim this whole
531
+ * reference exists to make true rather than plausible.
532
+ *
533
+ * A literal, because the kernel emitter may not import a capability renderer.
534
+ * `capabilities/config/show.render.test.ts` derives the same list from the real
535
+ * formatter and fails if this page and that renderer disagree.
536
+ */
537
+ const REPORTED_FIELDS = ["tier", "channel", "detail", "packs"];
538
+
539
+ /** Render the layered-configuration reference: the layers, then the fields. */
540
+ function renderConfigPage(): string {
541
+ const rows = ["| Field | Type | Notes |", "| --- | --- | --- |"];
542
+ for (const [field, doc] of Object.entries(CONFIG_FIELD_DOCS)) {
543
+ rows.push(
544
+ `| \`${field}\` | ${escapeCell(doc.type)} | ${escapeCell(doc.notes)} |`,
545
+ );
546
+ }
547
+ return assemblePage([
548
+ "# Configuration reference",
549
+ `Every field a \`${BIN_NAME}\` config layer may declare. Generated from the config type — do not edit by hand. See [config-model.md](../config-model.md) for the authoring guide.`,
550
+ "## Layers",
551
+ "From lowest to highest precedence:",
552
+ [
553
+ `1. **Built-in defaults** — the distribution config compiled into the binary.`,
554
+ `2. **Global config** — \`$XDG_CONFIG_HOME/${BIN_NAME}/config.json\`, written by \`${BIN_NAME} config set\`.`,
555
+ `3. **Project config** — the nearest \`${PROJECT_CONFIG_FILENAME}\` (or \`${BIN_NAME}.config.js\`, the compiled-binary fallback), walking up from the working directory.`,
556
+ ].join("\n"),
557
+ "A higher layer REPLACES a lower one field by field. No field is deep-merged — not `packs`, not `prefixes`, not `completion`. A project declaring one prefix therefore replaces the distribution's whole prefix map, including the namespaces its own packs are built with; declare every prefix you need, not only the new one.",
558
+ "## Fields",
559
+ "The `Type` column is prose; the field set and each field's optionality are checked against the validator.",
560
+ rows.join("\n"),
561
+ "## Distribution-only fields",
562
+ `\`name\`, \`help\` and \`issuesUrl\` are read from the distribution config when the program loads, because the surfaces that need them — \`--help\`, shell completion, the MCP handshake, the first-run note — run before or without the config layer. \`colophon\` is read from the same file at render time: the \`colophon\` verb narrates whatever the distribution declares there. The validator ACCEPTS all four in a global or project layer, and they have **no effect there and are not reported** by \`config show\`. Changing them means forking: edit the distribution config and rebuild the binary. The distribution config's \`vocabulary\` export is not a config field at all — no layer may declare it, and a fork changes it in the same file it changes \`name\` in.`,
563
+ "## What `config show` reports",
564
+ `\`${BIN_NAME} config show\` prints ${REPORTED_FIELDS.map((field) => `\`${field}\``).join(", ")} — those and only those — each with the layer that supplied it. The rest resolve without being reported that way: \`prefixes\` and \`completion\` appear only in the \`--format json\` payload, \`prefixes\` with an origin and \`completion\` with none; \`stories\` carries an origin whose value the payload leaves out; and the four distribution-only fields above carry neither. The plain and llm forms print those rows and nothing else; \`--format json\` returns the resolved config and the origin map whole.`,
565
+ "## Renamed: `packages` → `packs`",
566
+ "The `packages` field was renamed to `packs`. A layer that still declares `packages:` fails loudly: the rename is detected before the schema's unknown-key stripping could hide it, and the error names it. Rename the key — the entry shape is unchanged.",
567
+ "## Removed: `generators`",
568
+ "The `generators` field was removed: it was accepted by the validator, layered, and read by nothing — the `create` verbs resolve their generators statically (a compiled binary can only run generators it was linked with), so declaring it changed only what `config show` printed. A layer that still declares it fails loudly at load with an error naming the removed field; delete it. Declared generators may return as a working feature in a later program.",
569
+ "## Removed: `completion.caseSensitive`",
570
+ "The `completion.caseSensitive` field was removed: it was accepted by the validator and read by nothing — completion matching is declared per parameter by the capability grammar, never configured. A layer that still sets it fails loudly at load with an error naming the removed field; delete the key. `completion.minChars` and `completion.families` are unchanged.",
571
+ "## Reading and writing",
572
+ `\`${BIN_NAME} config show\` prints the resolved config and each field's layer. \`${BIN_NAME} config set <key> <value>\` writes to the **global** layer only — project configs are authored by hand. Both are documented in the [command reference](./commands.md).`,
573
+ ]);
574
+ }
575
+
445
576
  /** Render the reference index: overview, derived counts, and links. */
446
577
  function renderIndexPage(
447
578
  verbs: readonly VerbSpec[],
@@ -450,8 +581,8 @@ function renderIndexPage(
450
581
  const nouns = new Set(verbs.map((verb) => verb.path[0]));
451
582
  const toolCount = verbs.filter((verb) => verb.capability.mcp.expose).length;
452
583
  return assemblePage([
453
- "# pragma reference",
454
- "Machine-generated reference for the `pragma` CLI and MCP server, projected from the live capability grammar. Every page here is regenerated by the build and pinned by a drift-guard test, so it can never fall out of step with the code.",
584
+ `# ${BIN_NAME} reference`,
585
+ `Machine-generated reference for the \`${BIN_NAME}\` CLI and MCP server, projected from the live capability grammar. Every page here is regenerated by the build and pinned by a drift-guard test, so it can never fall out of step with the code.`,
455
586
  "## At a glance",
456
587
  [
457
588
  `- **${nouns.size}** command nouns`,
@@ -461,9 +592,10 @@ function renderIndexPage(
461
592
  ].join("\n"),
462
593
  "## Pages",
463
594
  [
464
- "- [CLI command reference](./commands.md) — every `pragma <noun> <verb>`, its arguments, flags, and examples.",
595
+ `- [CLI command reference](./commands.md) — every \`${BIN_NAME} <noun> <verb>\`, its arguments, flags, and examples.`,
465
596
  "- [MCP tool reference](./tools.md) — every exposed tool, its input schema, and the non-tool surface.",
466
597
  "- [Errors & exit codes](./errors.md) — the exit-code table, response envelope, and error catalog.",
598
+ "- [Configuration reference](./config.md) — every config field, the three layers, and how they combine.",
467
599
  ].join("\n"),
468
600
  ]);
469
601
  }
@@ -487,5 +619,6 @@ export function emitReference(
487
619
  ["commands.md", renderCommandsPage(verbs)],
488
620
  ["tools.md", renderToolsPage(verbs, modules)],
489
621
  ["errors.md", renderErrorsPage()],
622
+ ["config.md", renderConfigPage()],
490
623
  ]);
491
624
  }
@@ -8,6 +8,7 @@
8
8
  * envelope, exit codes, budgets, ...). Hidden verbs are excluded.
9
9
  */
10
10
 
11
+ import { BIN_NAME, PROJECT_CONFIG_FILENAME } from "../../constants.js";
11
12
  import type { CapabilityModule, ParamSpec, VerbSpec } from "./types.js";
12
13
 
13
14
  /** kebab-case a camelCase param name for its flag form (`allTiers` -> `all-tiers`). */
@@ -74,7 +75,7 @@ export interface EmittedSurface {
74
75
  */
75
76
  export const FIXED_SURFACE = {
76
77
  bins: {
77
- pragma: "pragma v2 CLI and MCP server host (stdio)",
78
+ [BIN_NAME]: `${BIN_NAME} CLI and MCP server host (stdio)`,
78
79
  },
79
80
  globalFlags: [
80
81
  {
@@ -122,14 +123,17 @@ export const FIXED_SURFACE = {
122
123
  paramSource: "ParamSpec.complete",
123
124
  },
124
125
  configFiles: {
125
- project: "pragma.config.ts (evaluated, content-hash cached)",
126
- global: "$XDG_CONFIG_HOME/pragma/config.json",
127
- lock: "$XDG_STATE_HOME/pragma/config-cache/<sha256>.json",
126
+ project: `${PROJECT_CONFIG_FILENAME} (evaluated, content-hash cached)`,
127
+ global: `$XDG_CONFIG_HOME/${BIN_NAME}/config.json`,
128
+ // Named `configCache` because that is what it is: the evaluated project
129
+ // config's content-addressed cache. It was called `lock` for a project lock
130
+ // file that no longer exists.
131
+ configCache: `$XDG_STATE_HOME/${BIN_NAME}/config-cache/<sha256>.json`,
128
132
  defaults: "built-in defaults.ts",
129
133
  },
130
134
  budgets: {
131
135
  $comment:
132
- "help/complete are the designed <50ms aspiration; the ENFORCED ceilings (budgets.test.ts) are 130ms help / 100ms complete — ~2× the measured median on the build hardware, recorded in BUDGETS.md. Designed-aspiration vs enforced-measured.",
136
+ "help/complete/warmStoreVerb are designed aspirations; the ENFORCED ceilings (budgets.test.ts) are 130ms help / 100ms complete / 500ms warmStoreVerb, each derived from measurement on the build hardware and recorded in BUDGETS.md. Designed-aspiration vs enforced-measured. warmStoreVerb joined this list when the embedded pack became the distribution's real graph rather than a sample, which puts the reference box's projected p95 over the designed 300ms; BUDGETS.md carries the arithmetic.",
133
137
  help: "<50ms",
134
138
  complete: "<50ms",
135
139
  projectConfigLoad: "<10ms",
@@ -84,8 +84,8 @@ describe("surface conformance (PROTECTED)", () => {
84
84
  );
85
85
  });
86
86
 
87
- it("freezes exactly 38 designed MCP tools including info, config_show, config_set, colophon", () => {
88
- expect(golden.mcpSurface.tools).toHaveLength(38);
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
+ });