@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
@@ -2,27 +2,26 @@
2
2
  * Resolve a {@link PackageRef} to its pinned revision and RDF source contents.
3
3
  *
4
4
  * `file` reads the local path in place; `npm` resolves the installed package
5
- * from the project's `node_modules`; `git` clones/fetches into the ref cache
6
- * (or, under `--frozen`, checks out exactly the lock's pinned commit and never
7
- * advances). Each package contributes its `definitions/**` and `data/**` TTL
8
- * files, labelled by a stable `pkg/relative-path` so the pack's content hash is
9
- * machine-independent. Reached only from the `sources update` Task body.
5
+ * from the project's `node_modules`; `git` clones/fetches into the ref cache.
6
+ * A ref that is already a commit SHA resolves to exactly that commit, which is
7
+ * how a project pins a revision. Each package contributes its `definitions/**`
8
+ * and `data/**` TTL files plus any `stories/*.json` it ships, labelled by a
9
+ * stable `pkg/relative-path` so the pack's content hash is machine-independent.
10
+ * Reached only from the `sources update` Task body.
10
11
  */
11
12
 
12
13
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
13
14
  import { createRequire } from "node:module";
14
15
  import { dirname, join, relative } from "node:path";
15
- import { RECOVERY_CLI_PREFIX } from "../../../constants.js";
16
16
  import { PragmaError } from "../../error/PragmaError.js";
17
- import { cliRecovery } from "../../error/recovery.js";
18
17
  import { refsCacheDir } from "../paths.js";
19
- import { checkoutCommit, cloneRef, fetchRef, headCommit } from "./gitOps.js";
18
+ import { cloneRef, fetchRef, headCommit } from "./gitOps.js";
20
19
  import { type PackageRef, redactUrl } from "./parseRef.js";
21
20
 
22
21
  /** A resolved package: its pinned revision and labelled RDF sources. */
23
22
  export interface ResolvedPackage {
24
23
  readonly name: string;
25
- /** The config `packages` source ref, verbatim. */
24
+ /** The config `packs` source ref, verbatim. */
26
25
  readonly source: string;
27
26
  /** The resolved commit / version / absolute path. */
28
27
  readonly resolved: string;
@@ -34,19 +33,26 @@ export interface ResolvedPackage {
34
33
  readonly root: string;
35
34
  /** The labelled RDF sources (path label + content). */
36
35
  readonly sources: { readonly path: string; readonly content: string }[];
36
+ /**
37
+ * The package's `stories/*.json` files, as RAW TEXT — never parsed here. The
38
+ * pack carries the package's bytes verbatim so every interpretation failure
39
+ * (malformed JSON and schema-invalid JSON alike) is caught behind one guard
40
+ * at dispatch instead of two half-guards at two layers.
41
+ */
42
+ readonly stories: { readonly path: string; readonly content: string }[];
37
43
  }
38
44
 
39
- /** Options for a resolution: whether to honour a pinned commit (`--frozen`). */
45
+ /** Options for a resolution — the project directory an `npm` ref resolves from. */
40
46
  export interface ResolveOptions {
41
47
  readonly cwd: string;
42
- readonly frozen: boolean;
43
- /** The lock's pinned revision for this package (used under `--frozen`). */
44
- readonly pinned?: string;
45
48
  }
46
49
 
47
50
  /** The package subdirectories scanned for `.ttl` sources. */
48
51
  const TTL_DIRS = ["definitions", "data"];
49
52
 
53
+ /** The package subdirectory whose immediate `*.json` children are read stories. */
54
+ const STORIES_DIR = "stories";
55
+
50
56
  /** Sanitize a ref for use as a cache path segment. */
51
57
  const sanitize = (value: string): string => value.replace(/[/\\:*?"<>|]/g, "_");
52
58
 
@@ -127,6 +133,47 @@ function readTtlSources(
127
133
  return sources;
128
134
  }
129
135
 
136
+ /**
137
+ * Read a package's `stories/*.json` as raw text, with stable labels.
138
+ *
139
+ * IMMEDIATE children only — the same discipline `skills/` uses, and recursion
140
+ * buys nothing. Hidden entries are skipped and a symlinked FILE is followed but
141
+ * a symlinked DIRECTORY is never recursed into, exactly as {@link walkTtl}
142
+ * argues. Sorted so the pack's content hash does not depend on readdir order.
143
+ *
144
+ * @param rootDir - The package root.
145
+ * @param label - The package name, used as the label prefix.
146
+ * @returns The labelled story files (`pkg/stories/name.json` + raw content).
147
+ * @note Impure — reads from disk.
148
+ */
149
+ function readStorySources(
150
+ rootDir: string,
151
+ label: string,
152
+ ): { path: string; content: string }[] {
153
+ const dir = join(rootDir, STORIES_DIR);
154
+ if (!existsSync(dir)) return [];
155
+ const stories: { path: string; content: string }[] = [];
156
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
157
+ if (entry.name.startsWith(".") || !entry.name.endsWith(".json")) continue;
158
+ const full = join(dir, entry.name);
159
+ let isFile = entry.isFile();
160
+ if (entry.isSymbolicLink()) {
161
+ try {
162
+ isFile = statSync(full).isFile();
163
+ } catch {
164
+ continue;
165
+ }
166
+ }
167
+ if (!isFile) continue;
168
+ stories.push({
169
+ path: `${label}/${STORIES_DIR}/${entry.name}`,
170
+ content: readFileSync(full, "utf-8"),
171
+ });
172
+ }
173
+ stories.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
174
+ return stories;
175
+ }
176
+
130
177
  /**
131
178
  * A Turtle/SPARQL prefix prologue declaration: `@prefix ex: <iri> .` or the
132
179
  * keyword-form `PREFIX ex: <iri>`. Both are legal in Turtle 1.1; the keyword is
@@ -254,31 +301,15 @@ export function resolvePackageJson(
254
301
  * Resolve a package reference to its revision and RDF sources.
255
302
  *
256
303
  * @param ref - The parsed package reference.
257
- * @param options - The cwd, frozen flag, and any pinned revision.
304
+ * @param options - The project directory an `npm` ref resolves from.
258
305
  * @returns The resolved package.
259
306
  * @throws PragmaError on a missing file path or unresolvable npm package.
260
- * @throws PragmaError under `--frozen` when the package has no lock entry.
261
307
  * @note Impure — may clone/fetch git, reads TTL from disk.
262
308
  */
263
309
  export async function resolvePackage(
264
310
  ref: PackageRef,
265
311
  options: ResolveOptions,
266
312
  ): Promise<ResolvedPackage> {
267
- // `--frozen` means "reproduce the locked state exactly". A configured package
268
- // with no lock entry has nothing to reproduce, so refuse rather than silently
269
- // resolving it fresh (which would advance the very state the lock pins).
270
- if (options.frozen && options.pinned === undefined) {
271
- throw PragmaError.configError(
272
- `Cannot resolve "${ref.pkg}" under --frozen: it has no entry in the lock.`,
273
- {
274
- recovery: cliRecovery(
275
- `${RECOVERY_CLI_PREFIX}sources update`,
276
- "Update the lock without --frozen, then commit it.",
277
- ),
278
- },
279
- );
280
- }
281
-
282
313
  switch (ref.kind) {
283
314
  case "file": {
284
315
  if (!existsSync(ref.path)) {
@@ -292,6 +323,7 @@ export async function resolvePackage(
292
323
  resolved: ref.path,
293
324
  root: ref.path,
294
325
  sources: readTtlSources(ref.path, ref.pkg),
326
+ stories: readStorySources(ref.path, ref.pkg),
295
327
  };
296
328
  }
297
329
 
@@ -322,22 +354,15 @@ export async function resolvePackage(
322
354
  resolved: version,
323
355
  root: dir,
324
356
  sources: readTtlSources(dir, ref.pkg),
357
+ stories: readStorySources(dir, ref.pkg),
325
358
  };
326
359
  }
327
360
 
328
361
  case "git": {
329
- const useCommit = options.frozen && options.pinned;
330
- const dir = join(
331
- refsCacheDir(),
332
- sanitize(ref.pkg),
333
- sanitize(useCommit ? (options.pinned as string) : ref.ref),
334
- );
362
+ const dir = join(refsCacheDir(), sanitize(ref.pkg), sanitize(ref.ref));
335
363
  let resolved: string;
336
364
  try {
337
- if (useCommit) {
338
- checkoutCommit(ref.url, options.pinned as string, dir);
339
- resolved = options.pinned as string;
340
- } else if (existsSync(dir)) {
365
+ if (existsSync(dir)) {
341
366
  resolved = fetchRef(ref.url, ref.ref, dir);
342
367
  } else {
343
368
  cloneRef(ref.url, ref.ref, dir);
@@ -345,28 +370,18 @@ export async function resolvePackage(
345
370
  }
346
371
  } catch (error) {
347
372
  // A git clone/fetch/checkout failed — an unreachable remote, a moved or
348
- // deleted ref, an auth/credential problem, or (under --frozen) a pinned
349
- // commit that is gone. Name the package + ref with git's own reason so
350
- // it reads as a fixable data/reproducibility error, not INTERNAL_ERROR.
351
- throw useCommit
352
- ? PragmaError.configError(
353
- `Cannot reproduce "${ref.pkg}" under --frozen: commit ${options.pinned} could not be checked out from ${redactUrl(ref.url)}. ${errorDetail(error)}`,
354
- {
355
- recovery: {
356
- message:
357
- "The pinned commit may have been force-pushed away or the remote is unreachable. Restore access to it, or re-run `pragma sources update` without --frozen to re-pin.",
358
- },
359
- },
360
- )
361
- : PragmaError.configError(
362
- `Cannot resolve "${ref.pkg}" from ${redactUrl(ref.url)}#${ref.ref}: ${errorDetail(error)}`,
363
- {
364
- recovery: {
365
- message:
366
- "Check the git URL and ref, your network, and your git credentials, then re-run the update.",
367
- },
368
- },
369
- );
373
+ // deleted ref, an auth/credential problem, or a pinned SHA that has been
374
+ // force-pushed away. Name the package + ref with git's own reason so it
375
+ // reads as a fixable data error, not INTERNAL_ERROR.
376
+ throw PragmaError.configError(
377
+ `Cannot resolve "${ref.pkg}" from ${redactUrl(ref.url)}#${ref.ref}: ${errorDetail(error)}`,
378
+ {
379
+ recovery: {
380
+ message:
381
+ "Check the git URL and ref, your network, and your git credentials, then re-run the update.",
382
+ },
383
+ },
384
+ );
370
385
  }
371
386
  return {
372
387
  name: ref.pkg,
@@ -374,6 +389,7 @@ export async function resolvePackage(
374
389
  resolved,
375
390
  root: dir,
376
391
  sources: readTtlSources(dir, ref.pkg),
392
+ stories: readStorySources(dir, ref.pkg),
377
393
  };
378
394
  }
379
395
  }
@@ -1,11 +1,10 @@
1
1
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { tmpdir } from "node:os";
3
- import { join } from "node:path";
3
+ import { dirname, join } from "node:path";
4
4
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
5
  import type { ConfigLayers } from "../config/types.js";
6
6
  import { loadStoreSession } from "./loadSession.js";
7
- import { writeLock } from "./lock.js";
8
- import { packDir } from "./paths.js";
7
+ import { activePackPath, packDir } from "./paths.js";
9
8
  import { resolveSources } from "./resolveSources.js";
10
9
 
11
10
  /**
@@ -21,24 +20,21 @@ const tmp = (): string => {
21
20
  return dir;
22
21
  };
23
22
 
24
- /** Config layers whose only relevant knob is the `packages` origin. */
25
- function layersWith(packagesOrigin: "default" | "project"): ConfigLayers {
23
+ /** Config layers whose only relevant knob is the `packs` origin. */
24
+ function layersWith(packsOrigin: "default" | "project"): ConfigLayers {
26
25
  return {
27
26
  config: {
28
27
  channel: "normal",
29
- packages:
30
- packagesOrigin === "project"
31
- ? [{ name: "x", source: "file:///x" }]
32
- : [],
28
+ packs:
29
+ packsOrigin === "project" ? [{ name: "x", source: "file:///x" }] : [],
33
30
  },
34
31
  origins: {
35
32
  tier: "default",
36
33
  channel: "default",
37
34
  detail: "default",
38
- packages: packagesOrigin,
35
+ packs: packsOrigin,
39
36
  stories: "default",
40
37
  prefixes: "default",
41
- prompts: "default",
42
38
  },
43
39
  global: { path: "/nonexistent", exists: false },
44
40
  project: { exists: false },
@@ -67,10 +63,18 @@ function writeCompletePack(hash: string): string {
67
63
  writeFileSync(join(dir, "data.nq"), "<urn:s> <urn:p> <urn:o> .\n");
68
64
  writeFileSync(join(dir, "schema.json"), "{}");
69
65
  writeFileSync(join(dir, "index.json"), "{}");
66
+ writeFileSync(join(dir, "stories.json"), "[]");
70
67
  writeManifest(dir, hash);
71
68
  return dir;
72
69
  }
73
70
 
71
+ /** Plant the active-pack pointer a `sources update` would have written. */
72
+ function writePointer(cwd: string, content: string): void {
73
+ const path = activePackPath(cwd);
74
+ mkdirSync(dirname(path), { recursive: true });
75
+ writeFileSync(path, content);
76
+ }
77
+
74
78
  beforeEach(() => {
75
79
  roots = [];
76
80
  });
@@ -79,11 +83,11 @@ afterEach(() => {
79
83
  });
80
84
 
81
85
  describe("resolveSources decision table", () => {
82
- it("lock present + pack cached → load the locked pack", () => {
86
+ it("pointer present + pack cached → load the built pack", () => {
83
87
  const cwd = tmp();
84
88
  const hash = "a".repeat(64);
85
89
  const dir = writeCompletePack(hash);
86
- writeLock(cwd, { version: 1, contentHash: hash, packs: [] });
90
+ writePointer(cwd, hash);
87
91
 
88
92
  expect(resolveSources(layersWith("default"), cwd)).toEqual({
89
93
  kind: "pack",
@@ -92,46 +96,81 @@ describe("resolveSources decision table", () => {
92
96
  });
93
97
  });
94
98
 
95
- it("lock present + pack evicted → STORE_UNAVAILABLE", () => {
99
+ it("pointer present + pack evicted → STORE_UNAVAILABLE", () => {
96
100
  const cwd = tmp();
97
- // A lock whose content-addressed pack is absent from the cache.
98
- writeLock(cwd, { version: 1, contentHash: "b".repeat(64), packs: [] });
101
+ // A pointer whose content-addressed pack is absent from the cache.
102
+ writePointer(cwd, "b".repeat(64));
99
103
 
100
104
  expect(resolveSources(layersWith("default"), cwd)).toEqual({
101
105
  kind: "unavailable",
102
- reason: "the locked pack is missing from the cache",
106
+ reason: "the built pack is missing from the cache",
103
107
  });
104
108
  });
105
109
 
106
- it("lock present + pack with a torn schema/index → STORE_UNAVAILABLE", () => {
110
+ it("pointer present + pack with a torn schema/index → STORE_UNAVAILABLE", () => {
107
111
  const cwd = tmp();
108
112
  const hash = "c".repeat(64);
109
113
  // manifest + non-empty dump present, but the extracted schema/index are
110
114
  // missing (a torn or partially-evicted pack). This used to slip through
111
115
  // `packIsComplete` and then crash at read time as an INTERNAL error; it must
112
- // now be treated as not-built so the boot surfaces STORE_UNAVAILABLE.
116
+ // now be treated as not-built so the boot surfaces STORE_UNAVAILABLE — and
117
+ // say INCOMPLETE, not "missing", because the directory is right there. This
118
+ // is also the shape every pack built before `stories.json` now takes.
113
119
  const dir = packDir(hash);
114
120
  mkdirSync(dir, { recursive: true });
115
121
  writeFileSync(join(dir, "data.nq"), "<urn:s> <urn:p> <urn:o> .\n");
116
122
  writeManifest(dir, hash);
117
- writeLock(cwd, { version: 1, contentHash: hash, packs: [] });
123
+ writePointer(cwd, hash);
124
+
125
+ expect(resolveSources(layersWith("default"), cwd)).toEqual({
126
+ kind: "unavailable",
127
+ reason: "the built pack is incomplete — an older or torn build",
128
+ });
129
+ });
130
+
131
+ it("pointer present + a pack whose stories.json is not an array → STORE_UNAVAILABLE", () => {
132
+ const cwd = tmp();
133
+ const hash = "d".repeat(64);
134
+ const dir = writeCompletePack(hash);
135
+ // Every file present and non-empty, but `stories.json` holds an object.
136
+ // Gated on size alone this passed, so `buildPack` REUSED the directory and
137
+ // every package-declared noun disappeared while `sources update` reported
138
+ // success. It is a torn build like any other, and says so.
139
+ writeFileSync(join(dir, "stories.json"), '{"noun":"recipe"}');
140
+ writePointer(cwd, hash);
118
141
 
119
142
  expect(resolveSources(layersWith("default"), cwd)).toEqual({
120
143
  kind: "unavailable",
121
- reason: "the locked pack is missing from the cache",
144
+ reason: "the built pack is incomplete — an older or torn build",
145
+ });
146
+ });
147
+
148
+ it("a malformed pointer is treated as absent, not as a pack name", () => {
149
+ // A truncated/garbage pointer must never name a cache directory. With
150
+ // default packs it falls through to the embedded row, exactly as no
151
+ // pointer at all would.
152
+ const cwd = tmp();
153
+ writePointer(cwd, "not-a-content-hash\n");
154
+
155
+ expect(resolveSources(layersWith("default"), cwd)).toEqual({
156
+ kind: "embedded",
157
+ });
158
+ expect(resolveSources(layersWith("project"), cwd)).toEqual({
159
+ kind: "unavailable",
160
+ reason: "packs are configured but the store has not been built",
122
161
  });
123
162
  });
124
163
 
125
- it("no lock + default packages → embedded fallback", () => {
164
+ it("no pointer + default packs → embedded fallback", () => {
126
165
  expect(resolveSources(layersWith("default"), tmp())).toEqual({
127
166
  kind: "embedded",
128
167
  });
129
168
  });
130
169
 
131
- it("no lock + packages configured → STORE_UNAVAILABLE", () => {
170
+ it("no pointer + packs configured → STORE_UNAVAILABLE", () => {
132
171
  expect(resolveSources(layersWith("project"), tmp())).toEqual({
133
172
  kind: "unavailable",
134
- reason: "packages are configured but the store has not been built",
173
+ reason: "packs are configured but the store has not been built",
135
174
  });
136
175
  });
137
176
  });
@@ -1,26 +1,33 @@
1
1
  /**
2
2
  * The store-boot decision table — storeless, and the single place the boot
3
- * strategy is decided.
3
+ * strategy is decided. No other module re-derives it: `sources status`, `info`,
4
+ * `doctor`, and the native MCP prompt/resource surfaces all switch on what this
5
+ * returns (`readPackIndex` takes the decision rather than resolving a pack of
6
+ * its own). The one module that cannot is the shell-completion fast path, which
7
+ * is denied the config evaluator and so implements the pointer half only — it
8
+ * says so, and it answers with completion candidates, never content.
4
9
  *
5
- * Boot never touches the network. From the lock and the resolved config:
10
+ * Boot never touches the network. From the project's active-pack pointer (the
11
+ * content hash `sources update` last built here) and the resolved config:
6
12
  *
7
- * | lock present | pack cached | packages origin | → decision |
8
- * |--------------|-------------|-----------------|-----------------------|
9
- * | yes | yes | — | load the locked pack |
10
- * | yes | no | — | STORE_UNAVAILABLE |
11
- * | no | — | default | embedded fallback |
12
- * | no | — | configured | STORE_UNAVAILABLE |
13
+ * | pointer | pack complete | packs origin | → decision |
14
+ * |---------|---------------|--------------|-----------------------|
15
+ * | yes | yes | — | load the built pack |
16
+ * | yes | no | — | STORE_UNAVAILABLE |
17
+ * | no | — | default | embedded fallback |
18
+ * | no | — | configured | STORE_UNAVAILABLE |
13
19
  *
14
- * A configured-but-unbuilt store (or a lock whose pack the cache lost)
20
+ * A configured-but-unbuilt store (or a pointer whose pack the cache lost)
15
21
  * surfaces STORE_UNAVAILABLE with a single recovery: `pragma sources update`.
16
- * "packages origin default" means the user has not pinned their own packages —
17
- * a fresh install — so the embedded fallback answers reads offline.
22
+ * "packs origin default" means the user has not pinned their own packs —
23
+ * a fresh install — so the embedded pack answers reads offline. A project that
24
+ * DID configure its own packs is never quietly served the distribution's graph.
18
25
  */
19
26
 
27
+ import { existsSync } from "node:fs";
20
28
  import type { ConfigLayers } from "../config/types.js";
21
29
  import { packIsComplete } from "./graphpack/manifest.js";
22
- import { readLock } from "./lock.js";
23
- import { packDir } from "./paths.js";
30
+ import { packDir, readActivePack } from "./paths.js";
24
31
 
25
32
  /** The resolved boot strategy. */
26
33
  export type SourcesDecision =
@@ -35,32 +42,36 @@ export type SourcesDecision =
35
42
  /**
36
43
  * Decide how (or whether) to boot the store — without any network or store I/O.
37
44
  *
38
- * @param layers - The resolved config layers (for the `packages` origin).
39
- * @param cwd - The project directory (for the lock).
45
+ * @param layers - The resolved config layers (for the `packs` origin).
46
+ * @param cwd - The project directory (for the active-pack pointer).
40
47
  * @returns The boot decision.
41
- * @note Impure — reads the lock and probes the pack cache.
48
+ * @note Impure — reads the pointer and probes the pack cache.
42
49
  */
43
50
  export function resolveSources(
44
51
  layers: ConfigLayers,
45
52
  cwd: string,
46
53
  ): SourcesDecision {
47
- const lock = readLock(cwd);
48
- if (lock) {
49
- const dir = packDir(lock.contentHash);
50
- if (packIsComplete(dir)) {
51
- return { kind: "pack", dir, contentHash: lock.contentHash };
52
- }
54
+ const contentHash = readActivePack(cwd);
55
+ if (contentHash !== undefined) {
56
+ const dir = packDir(contentHash);
57
+ if (packIsComplete(dir)) return { kind: "pack", dir, contentHash };
58
+ // Two different things, and the recovery text is the only thing a user has
59
+ // to go on: the cache evicted the pack, or the directory is there but does
60
+ // not hold a whole pack — the state EVERY install upgraded past the
61
+ // four-file pack lands in, where "missing" is plainly contradicted by `ls`.
53
62
  return {
54
63
  kind: "unavailable",
55
- reason: "the locked pack is missing from the cache",
64
+ reason: existsSync(dir)
65
+ ? "the built pack is incomplete — an older or torn build"
66
+ : "the built pack is missing from the cache",
56
67
  };
57
68
  }
58
69
 
59
- if (layers.origins.packages === "default") {
70
+ if (layers.origins.packs === "default") {
60
71
  return { kind: "embedded" };
61
72
  }
62
73
  return {
63
74
  kind: "unavailable",
64
- reason: "packages are configured but the store has not been built",
75
+ reason: "packs are configured but the store has not been built",
65
76
  };
66
77
  }
@@ -12,12 +12,11 @@
12
12
  * and one silently-empty list. Centralizing the construction here is what keeps
13
13
  * those surfaces from drifting apart.
14
14
  *
15
- * It reaches only {@link resolveSources} (a lock + config probe — no store, no
16
- * ke), so a caller on the storeless fast path can pre-check availability without
17
- * ever booting the store.
15
+ * It reaches only {@link resolveSources} (a pointer + config probe — no store,
16
+ * no ke), so a caller on the storeless fast path can pre-check availability
17
+ * without ever booting the store.
18
18
  */
19
19
 
20
- import { RECOVERY_CLI_PREFIX } from "../../constants.js";
21
20
  import type { ConfigLayers } from "../config/types.js";
22
21
  import { PragmaError } from "../error/PragmaError.js";
23
22
  import { cliRecovery } from "../error/recovery.js";
@@ -28,14 +27,15 @@ import { resolveSources } from "./resolveSources.js";
28
27
  * single `pragma sources update` recovery that also names the `sources_update`
29
28
  * tool an agent calls (then retries — PR9's C1 cold-store retry succeeds).
30
29
  *
31
- * @param reason - The boot decision's reason (e.g. "the locked pack is missing").
30
+ * @param reason - The boot decision's reason (e.g. "the built pack is missing
31
+ * from the cache").
32
32
  * @returns The structured STORE_UNAVAILABLE error.
33
33
  */
34
34
  export function storeUnavailable(reason: string): PragmaError {
35
35
  return PragmaError.storeUnavailable(`${reason}.`, {
36
36
  recovery: cliRecovery(
37
- `${RECOVERY_CLI_PREFIX}sources update`,
38
- "Build the local store from the configured packages.",
37
+ "sources update",
38
+ "Build the local store from the configured packs.",
39
39
  { tool: "sources_update" },
40
40
  ),
41
41
  });
@@ -43,7 +43,8 @@ export function storeUnavailable(reason: string): PragmaError {
43
43
 
44
44
  /**
45
45
  * The STORE_UNAVAILABLE error to surface when the store cannot boot from the
46
- * current config/lock, or `undefined` when a store read may proceed. STORELESS —
46
+ * current config and active-pack pointer, or `undefined` when a store read may
47
+ * proceed. STORELESS —
47
48
  * probes the decision table only, never constructs the store.
48
49
  *
49
50
  * @param layers - The resolved config layers.
@@ -58,7 +58,7 @@ describe("storeless guarantee — the store never boots for storeless verbs (PRO
58
58
  expect(runtime.store.booted).toBe(false);
59
59
  });
60
60
 
61
- it("sources status does not construct the store (reports a cold store)", async () => {
61
+ it("sources status does not construct the store", async () => {
62
62
  const runtime = bootRuntime(FLAGS, freshCwd());
63
63
  const outcome = await executeVerb(sourcesStatusVerb, {}, NO_MUT, runtime);
64
64
  expect(outcome.exitCode).toBe(0);
@@ -1,15 +1,10 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import {
3
- existsSync,
4
- mkdirSync,
5
- mkdtempSync,
6
- rmSync,
7
- writeFileSync,
8
- } from "node:fs";
2
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
9
3
  import { tmpdir } from "node:os";
10
4
  import { join } from "node:path";
11
5
  import { fileURLToPath } from "node:url";
12
6
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
7
+ import { embeddedManifest } from "./graphpack/embedded.js";
13
8
 
14
9
  /**
15
10
  * The store-boot path only fully lives in the standalone binary: this is the
@@ -74,8 +69,11 @@ describe("oxigraph WASM + embedded pack embed in the compiled binary (PROTECTED)
74
69
  triples: string;
75
70
  };
76
71
  expect(out.ok).toBe(true);
77
- expect(out.entities).toBe(6);
78
- expect(Number(out.triples)).toBeGreaterThan(0);
72
+ // The compiled binary is the only place this is worth asserting: the store
73
+ // it booted from the inlined `data.nq` must hold exactly what the committed
74
+ // `manifest.json` claims was built into it.
75
+ expect(Number(out.triples)).toBe(embeddedManifest().tripleCount);
76
+ expect(out.entities).toBeGreaterThan(0);
79
77
  });
80
78
 
81
79
  it("dist binary runs storeless sources status", () => {
@@ -83,18 +81,17 @@ describe("oxigraph WASM + embedded pack embed in the compiled binary (PROTECTED)
83
81
  expect(run.status, run.stderr).toBe(0);
84
82
  const envelope = JSON.parse(run.stdout.trim()) as {
85
83
  ok: boolean;
86
- data: { lockPresent: boolean; cached: boolean };
84
+ data: { store: string };
87
85
  };
88
86
  expect(envelope.ok).toBe(true);
89
- // A fresh install: no lock, store not yet built.
90
- expect(envelope.data.lockPresent).toBe(false);
91
- expect(envelope.data.cached).toBe(false);
87
+ // A fresh install: nothing built here, so the embedded pack answers reads.
88
+ expect(envelope.data.store).toBe("embedded");
92
89
  });
93
90
 
94
91
  // Regression guard: `sources update` walks a package's TTL directories. The
95
92
  // compiled binary's node:fs globSync mishandles `**` (returns bogus paths),
96
93
  // which the in-process suite could not catch — so exercise the real binary.
97
- it("dist binary runs sources update end-to-end (build + lock + status)", () => {
94
+ it("dist binary runs sources update end-to-end (build + point + status)", () => {
98
95
  const pkg = join(workdir, "pkg");
99
96
  const proj = join(workdir, "proj");
100
97
  mkdirSync(join(pkg, "definitions"), { recursive: true });
@@ -110,7 +107,7 @@ ex:one a ex:Widget ; rdfs:label "One" .
110
107
  );
111
108
  writeFileSync(
112
109
  join(proj, "pragma.config.ts"),
113
- `export default { packages: [{ name: "pkg-a", source: "file://${pkg}" }] };\n`,
110
+ `export default { packs: [{ name: "pkg-a", source: "file://${pkg}" }] };\n`,
114
111
  );
115
112
 
116
113
  const env = {
@@ -129,14 +126,20 @@ ex:one a ex:Widget ; rdfs:label "One" .
129
126
 
130
127
  const update = inProj(["sources", "update", "--yes", "--format", "json"]);
131
128
  expect(update.status, update.stderr).toBe(0);
132
- expect(existsSync(join(proj, "pragma.lock.json"))).toBe(true);
129
+ const built = (
130
+ JSON.parse(update.stdout.trim()) as {
131
+ data: { contentHash: string };
132
+ }
133
+ ).data.contentHash;
134
+ expect(built).toMatch(/^[0-9a-f]{64}$/);
133
135
 
134
136
  const status = inProj(["sources", "status", "--format", "json"]);
135
137
  const envelope = JSON.parse(status.stdout.trim()) as {
136
- data: { lockPresent: boolean; cached: boolean; entityCount: number };
138
+ data: { store: string; contentHash: string; entityCount: number };
137
139
  };
138
- expect(envelope.data.lockPresent).toBe(true);
139
- expect(envelope.data.cached).toBe(true);
140
+ // The project now reads its OWN pack — the one the update just built.
141
+ expect(envelope.data.store).toBe("built");
142
+ expect(envelope.data.contentHash).toBe(built);
140
143
  // Distinct abox subjects: the one individual (ex:one). The ex:Widget class
141
144
  // is tbox schema, not an entity — the raw entity count would double this (A1).
142
145
  expect(envelope.data.entityCount).toBe(1);
@@ -6,8 +6,8 @@ import {
6
6
  import { ERROR_CODES } from "../error/constants.js";
7
7
  import { emitReference } from "./emitReference.js";
8
8
 
9
- /** The four pages every reference doc set carries. */
10
- const PAGES = ["index.md", "commands.md", "tools.md", "errors.md"];
9
+ /** The five pages every reference doc set carries. */
10
+ const PAGES = ["index.md", "commands.md", "tools.md", "errors.md", "config.md"];
11
11
 
12
12
  describe("emitReference", () => {
13
13
  const docs = emitReference([fixtureReferenceModule]);
@@ -16,7 +16,7 @@ describe("emitReference", () => {
16
16
  const tools = docs.get("tools.md") ?? "";
17
17
  const errors = docs.get("errors.md") ?? "";
18
18
 
19
- it("emits exactly the four reference pages", () => {
19
+ it("emits exactly the five reference pages", () => {
20
20
  expect([...docs.keys()].sort()).toEqual([...PAGES].sort());
21
21
  });
22
22