@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
@@ -1,14 +1,14 @@
1
1
  /**
2
- * Parse a config `packages` entry into a typed {@link PackageRef}.
2
+ * Parse a config `packs` declaration into a typed {@link PackageRef}.
3
3
  *
4
4
  * Ported from the v1 refs domain. Three resolution strategies, discriminated by
5
5
  * `kind`: `npm` (a bare package name, resolved via `require.resolve`), `file`
6
6
  * (a `file://` local path override), and `git` (a `git+<proto>://…#ref` shallow
7
- * clone). The `source` string is kept verbatim so the lock records exactly what
8
- * the config declared.
7
+ * clone). The `source` string is kept verbatim so `sources status` reports
8
+ * exactly what the config declared.
9
9
  */
10
10
 
11
- import type { PackageEntry } from "../../config/types.js";
11
+ import type { PackDeclaration } from "../../config/types.js";
12
12
  import { PragmaError } from "../../error/PragmaError.js";
13
13
 
14
14
  /** A parsed, validated package reference — discriminated by `kind`. */
@@ -29,13 +29,13 @@ export type PackageRef =
29
29
  };
30
30
 
31
31
  /**
32
- * Parse a config `packages` entry into a {@link PackageRef}.
32
+ * Parse a config `packs` declaration into a {@link PackageRef}.
33
33
  *
34
34
  * @param entry - A bare npm name, or a `{ name, source }` declaration.
35
35
  * @returns The parsed reference (with the verbatim `source`).
36
36
  * @throws PragmaError CONFIG_ERROR on an invalid `source`.
37
37
  */
38
- export function parsePackageEntry(entry: PackageEntry): PackageRef {
38
+ export function parsePackDeclaration(entry: PackDeclaration): PackageRef {
39
39
  if (typeof entry === "string") {
40
40
  return { kind: "npm", pkg: entry, source: entry };
41
41
  }
@@ -43,7 +43,7 @@ export function parsePackageEntry(entry: PackageEntry): PackageRef {
43
43
  const { name, source } = entry;
44
44
  if (typeof name !== "string" || name.length === 0) {
45
45
  throw PragmaError.configError(
46
- 'Package entry must have a non-empty "name".',
46
+ 'Pack declaration must have a non-empty "name".',
47
47
  );
48
48
  }
49
49
  if (source === undefined) {
@@ -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
- import { existsSync, readdirSync, readFileSync } from "node:fs";
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
 
@@ -84,9 +90,28 @@ function walkTtl(
84
90
  // build (a `.`-prefixed Turtle local name isn't even valid).
85
91
  if (entry.name.startsWith(".")) continue;
86
92
  const full = join(dir, entry.name);
87
- if (entry.isDirectory()) {
93
+ // Resolve symlinks: pnpm / workspace trees link their sources, but
94
+ // `Dirent.isFile`/`isDirectory` are BOTH false for a symlink — so a
95
+ // symlinked `.ttl` was silently skipped (L6). Follow the link to recover a
96
+ // symlinked FILE, but NEVER recurse into a symlinked DIRECTORY: a symlink
97
+ // cycle (a link to an ancestor dir) would recurse without bound — a
98
+ // stack-overflow RangeError surfacing as an INTERNAL "please report" — and a
99
+ // linked directory could pull `.ttl` from OUTSIDE the package root. Linking
100
+ // individual source files (all L6 needs) stays supported; a dangling link is
101
+ // skipped, not fatal.
102
+ let isDir = entry.isDirectory();
103
+ let isFile = entry.isFile();
104
+ if (entry.isSymbolicLink()) {
105
+ try {
106
+ isFile = statSync(full).isFile();
107
+ isDir = false;
108
+ } catch {
109
+ continue;
110
+ }
111
+ }
112
+ if (isDir) {
88
113
  walkTtl(full, base, label, out);
89
- } else if (entry.isFile() && entry.name.endsWith(".ttl")) {
114
+ } else if (isFile && entry.name.endsWith(".ttl")) {
90
115
  out.push({
91
116
  path: `${label}/${relative(base, full)}`,
92
117
  content: readFileSync(full, "utf-8"),
@@ -108,6 +133,47 @@ function readTtlSources(
108
133
  return sources;
109
134
  }
110
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
+
111
177
  /**
112
178
  * A Turtle/SPARQL prefix prologue declaration: `@prefix ex: <iri> .` or the
113
179
  * keyword-form `PREFIX ex: <iri>`. Both are legal in Turtle 1.1; the keyword is
@@ -143,6 +209,47 @@ export function harvestPrefixes(
143
209
  return prefixes;
144
210
  }
145
211
 
212
+ /** One `@prefix` clash: a label declared with two or more distinct IRIs. */
213
+ export interface PrefixClash {
214
+ readonly label: string;
215
+ /** The distinct IRIs bound to the label, in first-seen order. */
216
+ readonly iris: readonly string[];
217
+ }
218
+
219
+ /**
220
+ * Detect `@prefix` label clashes across a set of sources.
221
+ *
222
+ * {@link harvestPrefixes} resolves clashes last-wins (Turtle's own rule),
223
+ * silently compacting the losing package's URIs to the wrong prefix (or to full
224
+ * IRIs). This surfaces the clashes so `sources update` can warn (A5): a label
225
+ * bound to two or more distinct IRIs. Harmless same-label/same-IRI
226
+ * redeclarations (the common case) are NOT clashes. Result is sorted by label
227
+ * so the warnings are deterministic.
228
+ *
229
+ * @param sources - The packages' labelled RDF sources.
230
+ * @returns One entry per clashing label, each with its distinct IRIs.
231
+ */
232
+ export function detectPrefixClashes(
233
+ sources: readonly { readonly content: string }[],
234
+ ): PrefixClash[] {
235
+ const byLabel = new Map<string, string[]>();
236
+ for (const { content } of sources) {
237
+ for (const match of content.matchAll(PREFIX_DECL)) {
238
+ const [, label, iri] = match;
239
+ if (label === undefined || iri === undefined) continue;
240
+ const iris = byLabel.get(label) ?? [];
241
+ if (!iris.includes(iri)) iris.push(iri);
242
+ byLabel.set(label, iris);
243
+ }
244
+ }
245
+ const clashes: PrefixClash[] = [];
246
+ for (const [label, iris] of byLabel) {
247
+ if (iris.length > 1) clashes.push({ label, iris });
248
+ }
249
+ clashes.sort((a, b) => (a.label < b.label ? -1 : a.label > b.label ? 1 : 0));
250
+ return clashes;
251
+ }
252
+
146
253
  /**
147
254
  * Locate a package's `package.json` from the project.
148
255
  *
@@ -194,31 +301,15 @@ export function resolvePackageJson(
194
301
  * Resolve a package reference to its revision and RDF sources.
195
302
  *
196
303
  * @param ref - The parsed package reference.
197
- * @param options - The cwd, frozen flag, and any pinned revision.
304
+ * @param options - The project directory an `npm` ref resolves from.
198
305
  * @returns The resolved package.
199
306
  * @throws PragmaError on a missing file path or unresolvable npm package.
200
- * @throws PragmaError under `--frozen` when the package has no lock entry.
201
307
  * @note Impure — may clone/fetch git, reads TTL from disk.
202
308
  */
203
309
  export async function resolvePackage(
204
310
  ref: PackageRef,
205
311
  options: ResolveOptions,
206
312
  ): Promise<ResolvedPackage> {
207
- // `--frozen` means "reproduce the locked state exactly". A configured package
208
- // with no lock entry has nothing to reproduce, so refuse rather than silently
209
- // resolving it fresh (which would advance the very state the lock pins).
210
- if (options.frozen && options.pinned === undefined) {
211
- throw PragmaError.configError(
212
- `Cannot resolve "${ref.pkg}" under --frozen: it has no entry in the lock.`,
213
- {
214
- recovery: cliRecovery(
215
- `${RECOVERY_CLI_PREFIX}sources update`,
216
- "Update the lock without --frozen, then commit it.",
217
- ),
218
- },
219
- );
220
- }
221
-
222
313
  switch (ref.kind) {
223
314
  case "file": {
224
315
  if (!existsSync(ref.path)) {
@@ -232,6 +323,7 @@ export async function resolvePackage(
232
323
  resolved: ref.path,
233
324
  root: ref.path,
234
325
  sources: readTtlSources(ref.path, ref.pkg),
326
+ stories: readStorySources(ref.path, ref.pkg),
235
327
  };
236
328
  }
237
329
 
@@ -262,22 +354,15 @@ export async function resolvePackage(
262
354
  resolved: version,
263
355
  root: dir,
264
356
  sources: readTtlSources(dir, ref.pkg),
357
+ stories: readStorySources(dir, ref.pkg),
265
358
  };
266
359
  }
267
360
 
268
361
  case "git": {
269
- const useCommit = options.frozen && options.pinned;
270
- const dir = join(
271
- refsCacheDir(),
272
- sanitize(ref.pkg),
273
- sanitize(useCommit ? (options.pinned as string) : ref.ref),
274
- );
362
+ const dir = join(refsCacheDir(), sanitize(ref.pkg), sanitize(ref.ref));
275
363
  let resolved: string;
276
364
  try {
277
- if (useCommit) {
278
- checkoutCommit(ref.url, options.pinned as string, dir);
279
- resolved = options.pinned as string;
280
- } else if (existsSync(dir)) {
365
+ if (existsSync(dir)) {
281
366
  resolved = fetchRef(ref.url, ref.ref, dir);
282
367
  } else {
283
368
  cloneRef(ref.url, ref.ref, dir);
@@ -285,28 +370,18 @@ export async function resolvePackage(
285
370
  }
286
371
  } catch (error) {
287
372
  // A git clone/fetch/checkout failed — an unreachable remote, a moved or
288
- // deleted ref, an auth/credential problem, or (under --frozen) a pinned
289
- // commit that is gone. Name the package + ref with git's own reason so
290
- // it reads as a fixable data/reproducibility error, not INTERNAL_ERROR.
291
- throw useCommit
292
- ? PragmaError.configError(
293
- `Cannot reproduce "${ref.pkg}" under --frozen: commit ${options.pinned} could not be checked out from ${redactUrl(ref.url)}. ${errorDetail(error)}`,
294
- {
295
- recovery: {
296
- message:
297
- "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.",
298
- },
299
- },
300
- )
301
- : PragmaError.configError(
302
- `Cannot resolve "${ref.pkg}" from ${redactUrl(ref.url)}#${ref.ref}: ${errorDetail(error)}`,
303
- {
304
- recovery: {
305
- message:
306
- "Check the git URL and ref, your network, and your git credentials, then re-run the update.",
307
- },
308
- },
309
- );
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
+ );
310
385
  }
311
386
  return {
312
387
  name: ref.pkg,
@@ -314,6 +389,7 @@ export async function resolvePackage(
314
389
  resolved,
315
390
  root: dir,
316
391
  sources: readTtlSources(dir, ref.pkg),
392
+ stories: readStorySources(dir, ref.pkg),
317
393
  };
318
394
  }
319
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
  }