@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
@@ -14,13 +14,20 @@ import { join } from "node:path";
14
14
  import { runTask } from "@canonical/task/node";
15
15
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
16
16
  import { VERSION } from "../../constants.js";
17
- import type { ConfigLayers, PackageEntry } from "../../kernel/config/types.js";
17
+ import type {
18
+ ConfigLayers,
19
+ PackDeclaration,
20
+ } from "../../kernel/config/types.js";
18
21
  import { PragmaError } from "../../kernel/error/PragmaError.js";
19
22
  import { executeVerb } from "../../kernel/project/cli/dispatch.js";
20
23
  import { bootRuntime } from "../../kernel/runtime/boot.js";
21
24
  import { createQueryFacade } from "../../kernel/runtime/facade.js";
22
- import { readLock } from "../../kernel/runtime/lock.js";
23
- import { lockPath, packDir } from "../../kernel/runtime/paths.js";
25
+ import { readManifest } from "../../kernel/runtime/graphpack/manifest.js";
26
+ import {
27
+ activePackPath,
28
+ packDir,
29
+ readActivePack,
30
+ } from "../../kernel/runtime/paths.js";
24
31
  import {
25
32
  detectPrefixClashes,
26
33
  resolvePackageJson,
@@ -58,18 +65,17 @@ const tmp = (prefix: string): string => {
58
65
  return dir;
59
66
  };
60
67
 
61
- /** A runtime whose config is the given package list (no config files needed). */
62
- function runtimeFor(cwd: string, packages: PackageEntry[]): PragmaRuntime {
68
+ /** A runtime whose config is the given pack list (no config files needed). */
69
+ function runtimeFor(cwd: string, packs: PackDeclaration[]): PragmaRuntime {
63
70
  const layers: ConfigLayers = {
64
- config: { channel: "normal", packages },
71
+ config: { channel: "normal", packs },
65
72
  origins: {
66
73
  tier: "default",
67
74
  channel: "default",
68
75
  detail: "default",
69
- packages: "project",
76
+ packs: "project",
70
77
  stories: "default",
71
78
  prefixes: "default",
72
- prompts: "default",
73
79
  },
74
80
  global: { path: "/nonexistent", exists: false },
75
81
  project: { exists: false },
@@ -101,30 +107,24 @@ function filePackage(): string {
101
107
  return pkg;
102
108
  }
103
109
 
104
- describe("sources lock round-trip (PROTECTED)", () => {
105
- it("file source: builds, locks, and --frozen rewrites a byte-identical lock", async () => {
110
+ describe("sources update round-trip (PROTECTED)", () => {
111
+ it("file source: builds and points the project at the built pack", async () => {
106
112
  const pkg = filePackage();
107
113
  const cwd = tmp("pragma-proj-");
108
114
  const runtime = runtimeFor(cwd, [
109
115
  { name: "pkg-a", source: `file://${pkg}` },
110
116
  ]);
111
117
 
112
- const result = await runTask(await buildUpdateTask(runtime, false));
118
+ const result = await runTask(await buildUpdateTask(runtime));
113
119
  expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
114
-
115
- const lock = readLock(cwd);
116
- expect(lock?.packs).toHaveLength(1);
117
- expect(lock?.packs[0]?.name).toBe("pkg-a");
118
- expect(lock?.packs[0]?.resolved).toBe(pkg);
119
- expect(lock?.contentHash).toBe(result.contentHash);
120
-
121
- const firstBytes = readFileSync(lockPath(cwd), "utf-8");
122
- // A --frozen re-run reproduces the locked state byte-for-byte.
123
- await runTask(await buildUpdateTask(runtime, true));
124
- expect(readFileSync(lockPath(cwd), "utf-8")).toBe(firstBytes);
120
+ expect(result.packs).toEqual([
121
+ { name: "pkg-a", resolved: pkg, sourceCount: 1, storyCount: 0 },
122
+ ]);
123
+ // The pointer the next boot reads names exactly the pack just built.
124
+ expect(readActivePack(cwd)).toBe(result.contentHash);
125
125
  });
126
126
 
127
- it("git source: resolves and locks a commit SHA, --frozen pins it", async () => {
127
+ it("git source: resolves the ref to a commit SHA", async () => {
128
128
  const repo = tmp("pragma-repo-");
129
129
  const git = (args: string[]) =>
130
130
  execFileSync("git", args, {
@@ -149,34 +149,39 @@ describe("sources lock round-trip (PROTECTED)", () => {
149
149
  { name: "pkg-git", source: `git+file://${repo}#main` },
150
150
  ]);
151
151
 
152
- await runTask(await buildUpdateTask(runtime, false));
153
- const lock = readLock(cwd);
154
- expect(lock?.packs[0]?.resolved).toMatch(/^[0-9a-f]{40}$/);
155
-
156
- const firstBytes = readFileSync(lockPath(cwd), "utf-8");
157
- await runTask(await buildUpdateTask(runtime, true));
158
- expect(readFileSync(lockPath(cwd), "utf-8")).toBe(firstBytes);
152
+ const result = await runTask(await buildUpdateTask(runtime));
153
+ const sha = result.packs.at(0)?.resolved;
154
+ expect(sha).toMatch(/^[0-9a-f]{40}$/);
155
+ expect(readActivePack(cwd)).toBe(result.contentHash);
156
+ // The revision reaches the manifest's provenance label, which is the only
157
+ // place `sources status` and `doctor` can read it from now that no lock
158
+ // records it. Same `<name>@<kind>:<resolved>` shape the bundler writes.
159
+ expect(readManifest(packDir(result.contentHash))?.sourceRef).toBe(
160
+ `pkg-git@git:${sha}`,
161
+ );
159
162
  });
160
163
 
161
- it("a non-frozen re-run over an unchanged source rewrites a byte-identical lock (L1)", async () => {
162
- // On base, every non-frozen update stamped a fresh `resolvedAt`, so a no-op
163
- // re-run dirtied the tree. When the revision is unchanged the timestamp is
164
- // preserved, so the lock is byte-identical.
164
+ it("a re-run over an unchanged source leaves the pointer byte-identical (L1)", async () => {
165
+ // On base the lock file carried a `resolvedAt` timestamp, so a no-op re-run
166
+ // rewrote it and dirtied the tree. The pointer holds only the content hash,
167
+ // which is a pure function of the sources — so this is now true by
168
+ // construction. The guard stays because it is the property users cared
169
+ // about: repeating an update changes nothing when nothing changed.
165
170
  const pkg = filePackage();
166
171
  const cwd = tmp("pragma-proj-");
167
172
  const runtime = runtimeFor(cwd, [
168
173
  { name: "pkg-a", source: `file://${pkg}` },
169
174
  ]);
170
- await runTask(await buildUpdateTask(runtime, false));
171
- const firstBytes = readFileSync(lockPath(cwd), "utf-8");
172
- await runTask(await buildUpdateTask(runtime, false));
173
- expect(readFileSync(lockPath(cwd), "utf-8")).toBe(firstBytes);
175
+ await runTask(await buildUpdateTask(runtime));
176
+ const firstBytes = readFileSync(activePackPath(cwd), "utf-8");
177
+ await runTask(await buildUpdateTask(runtime));
178
+ expect(readFileSync(activePackPath(cwd), "utf-8")).toBe(firstBytes);
174
179
  });
175
180
 
176
181
  it("follows a symlinked .ttl into the build (L6)", async () => {
177
182
  // pnpm / workspace trees symlink their sources; Dirent.isFile is false for a
178
183
  // symlink, so on base the only `.ttl` was skipped → 0 sources → the empty
179
- // build. Following the link ingests it and the build+lock succeed.
184
+ // build. Following the link ingests it and the build succeeds.
180
185
  const pkg = tmp("pragma-symlink-pkg-");
181
186
  mkdirSync(join(pkg, "definitions"), { recursive: true });
182
187
  const realTtl = join(pkg, "real.ttl");
@@ -187,9 +192,9 @@ describe("sources lock round-trip (PROTECTED)", () => {
187
192
  { name: "pkg-a", source: `file://${pkg}` },
188
193
  ]);
189
194
 
190
- const result = await runTask(await buildUpdateTask(runtime, false));
195
+ const result = await runTask(await buildUpdateTask(runtime));
191
196
  expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
192
- expect(readLock(cwd)?.packs).toHaveLength(1);
197
+ expect(readActivePack(cwd)).toBe(result.contentHash);
193
198
  });
194
199
 
195
200
  it("does not recurse into a symlinked directory cycle (L6 safety)", async () => {
@@ -207,21 +212,20 @@ describe("sources lock round-trip (PROTECTED)", () => {
207
212
  { name: "pkg-a", source: `file://${pkg}` },
208
213
  ]);
209
214
 
210
- const result = await runTask(await buildUpdateTask(runtime, false));
215
+ const result = await runTask(await buildUpdateTask(runtime));
211
216
  expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
212
- expect(readLock(cwd)?.packs).toHaveLength(1);
217
+ expect(readActivePack(cwd)).toBe(result.contentHash);
213
218
  });
214
219
 
215
- it("undo restores the prior lock (no prior → removed)", async () => {
220
+ it("undo removes the pointer when the project had none before", async () => {
216
221
  const pkg = filePackage();
217
222
  const cwd = tmp("pragma-proj-");
218
223
  const runtime = runtimeFor(cwd, [
219
224
  { name: "pkg-a", source: `file://${pkg}` },
220
225
  ]);
221
226
  const { runUndo } = await import("@canonical/task/node");
222
- await runUndo(await buildUpdateTask(runtime, false));
223
- // Prior lock was absent, so undo removes the file.
224
- expect(readLock(cwd)).toBeUndefined();
227
+ await runUndo(await buildUpdateTask(runtime));
228
+ expect(readActivePack(cwd)).toBeUndefined();
225
229
  });
226
230
  });
227
231
 
@@ -237,7 +241,7 @@ describe("sources update — package-declared prefixes (M1)", () => {
237
241
  { name: "pkg-a", source: `file://${pkg}` },
238
242
  ]);
239
243
 
240
- const result = await runTask(await buildUpdateTask(runtime, false));
244
+ const result = await runTask(await buildUpdateTask(runtime));
241
245
  const index = JSON.parse(
242
246
  readFileSync(join(packDir(result.contentHash), "index.json"), "utf-8"),
243
247
  ) as {
@@ -267,30 +271,30 @@ describe("sources update — refuses an empty store (A4)", () => {
267
271
  return pkg;
268
272
  }
269
273
 
270
- it("a package with no .ttl is refused, and no lock is written", async () => {
274
+ it("a package with no .ttl is refused, and no pointer is written", async () => {
271
275
  // On base this builds a 0-triple pack whose empty data.nq fails the
272
276
  // completeness gate — so the "successful" update boots to a PERMANENT
273
- // STORE_UNAVAILABLE loop. The fix refuses before writing the lock.
277
+ // STORE_UNAVAILABLE loop. The fix refuses before writing the pointer.
274
278
  const pkg = emptyPackage();
275
279
  const cwd = tmp("pragma-proj-");
276
280
  const runtime = runtimeFor(cwd, [
277
281
  { name: "pkg-a", source: `file://${pkg}` },
278
282
  ]);
279
283
 
280
- await expect(buildUpdateTask(runtime, false)).rejects.toMatchObject({
284
+ await expect(buildUpdateTask(runtime)).rejects.toMatchObject({
281
285
  code: "CONFIG_ERROR",
282
286
  });
283
- // No lock → the embedded fallback / prior state survives, no boot loop.
284
- expect(readLock(cwd)).toBeUndefined();
287
+ // No pointer → the embedded fallback / prior state survives, no boot loop.
288
+ expect(readActivePack(cwd)).toBeUndefined();
285
289
  });
286
290
 
287
- it("no configured packages is refused rather than locking an empty pack", async () => {
291
+ it("no configured packs is refused rather than pointing at an empty pack", async () => {
288
292
  const cwd = tmp("pragma-proj-");
289
293
  const runtime = runtimeFor(cwd, []);
290
- await expect(buildUpdateTask(runtime, false)).rejects.toMatchObject({
294
+ await expect(buildUpdateTask(runtime)).rejects.toMatchObject({
291
295
  code: "CONFIG_ERROR",
292
296
  });
293
- expect(readLock(cwd)).toBeUndefined();
297
+ expect(readActivePack(cwd)).toBeUndefined();
294
298
  });
295
299
 
296
300
  it("a comment-only .ttl (0 triples) is refused too", async () => {
@@ -305,10 +309,10 @@ describe("sources update — refuses an empty store (A4)", () => {
305
309
  const runtime = runtimeFor(cwd, [
306
310
  { name: "pkg-a", source: `file://${pkg}` },
307
311
  ]);
308
- await expect(buildUpdateTask(runtime, false)).rejects.toMatchObject({
312
+ await expect(buildUpdateTask(runtime)).rejects.toMatchObject({
309
313
  code: "CONFIG_ERROR",
310
314
  });
311
- expect(readLock(cwd)).toBeUndefined();
315
+ expect(readActivePack(cwd)).toBeUndefined();
312
316
  });
313
317
  });
314
318
 
@@ -358,7 +362,7 @@ describe("sources update — conflicting @prefix detection (A5)", () => {
358
362
  report: (message: string) => reports.push(message),
359
363
  };
360
364
 
361
- await runTask(await buildUpdateTask(runtime, false));
365
+ await runTask(await buildUpdateTask(runtime));
362
366
  expect(
363
367
  reports.some(
364
368
  (r) => r.includes('Prefix "ex:"') && r.includes("conflicting"),
@@ -385,11 +389,12 @@ describe("sources update — network-free preview (M2)", () => {
385
389
 
386
390
  // A successful plan — the unreachable clone was never attempted.
387
391
  expect(outcome.exitCode).toBe(0);
388
- expect(outcome.stdout).toContain("Resolve and build 1 package(s)");
392
+ expect(outcome.stdout).toContain("Resolve and build 1 pack(s)");
389
393
  expect(outcome.stdout).toContain(UNREACHABLE);
390
- // The one project mutation is previewed, not performed.
391
- expect(outcome.stdout).toContain("pragma.lock.json");
392
- expect(readLock(cwd)).toBeUndefined();
394
+ // The project mutation is previewed, not performed: the plan names the
395
+ // exact pointer file a real run would write, and no pointer landed.
396
+ expect(outcome.stdout).toContain(activePackPath(cwd));
397
+ expect(readActivePack(cwd)).toBeUndefined();
393
398
  // The store was never even asked for.
394
399
  expect(runtime.store.booted).toBe(false);
395
400
  });
@@ -398,7 +403,7 @@ describe("sources update — network-free preview (M2)", () => {
398
403
  const cwd = tmp("pragma-proj-");
399
404
  writeFileSync(
400
405
  join(cwd, "pragma.config.ts"),
401
- `export default { packages: [{ name: "pkg-remote", source: "${UNREACHABLE}" }] };\n`,
406
+ `export default { packs: [{ name: "pkg-remote", source: "${UNREACHABLE}" }] };\n`,
402
407
  );
403
408
 
404
409
  const mcp = await projectMcp([sourcesModule], cwd);
@@ -412,25 +417,8 @@ describe("sources update — network-free preview (M2)", () => {
412
417
  });
413
418
  const plan = (envelope.data as { plan: string[] }).plan;
414
419
  expect(plan.some((line) => line.includes(UNREACHABLE))).toBe(true);
415
- // Plan-first withheld the write — no lock landed.
416
- expect(readLock(cwd)).toBeUndefined();
417
- });
418
- });
419
-
420
- describe("sources update — reproducible-or-fail (m5)", () => {
421
- it("--frozen refuses a package that has no lock entry", async () => {
422
- const pkg = filePackage();
423
- const cwd = tmp("pragma-proj-");
424
- const runtime = runtimeFor(cwd, [
425
- { name: "pkg-a", source: `file://${pkg}` },
426
- ]);
427
-
428
- // No prior lock → nothing to reproduce → refuse rather than silently
429
- // advance. The throw is raised during setup, before any Task is returned.
430
- await expect(buildUpdateTask(runtime, true)).rejects.toMatchObject({
431
- code: "CONFIG_ERROR",
432
- });
433
- expect(readLock(cwd)).toBeUndefined();
420
+ // Plan-first withheld the write — no pointer landed.
421
+ expect(readActivePack(cwd)).toBeUndefined();
434
422
  });
435
423
  });
436
424
 
@@ -498,7 +486,7 @@ describe("sources update — data-failure classification (U6)", () => {
498
486
 
499
487
  let caught: unknown;
500
488
  try {
501
- await buildUpdateTask(runtime, false);
489
+ await buildUpdateTask(runtime);
502
490
  } catch (error) {
503
491
  caught = error;
504
492
  }
@@ -516,8 +504,8 @@ describe("sources update — data-failure classification (U6)", () => {
516
504
  expect(err.recovery?.message ?? "").not.toContain("report this issue");
517
505
  // The recovery points the user at a runnable, useful next step.
518
506
  expect(err.recovery?.cli).toBe("pragma sources update --verbose");
519
- // Nothing was locked on failure.
520
- expect(readLock(cwd)).toBeUndefined();
507
+ // Nothing was pointed at on failure.
508
+ expect(readActivePack(cwd)).toBeUndefined();
521
509
  });
522
510
 
523
511
  it("classifies a git clone failure, naming the package (not INTERNAL)", async () => {
@@ -534,7 +522,7 @@ describe("sources update — data-failure classification (U6)", () => {
534
522
 
535
523
  let caught: unknown;
536
524
  try {
537
- await buildUpdateTask(runtime, false);
525
+ await buildUpdateTask(runtime);
538
526
  } catch (error) {
539
527
  caught = error;
540
528
  }
@@ -543,7 +531,7 @@ describe("sources update — data-failure classification (U6)", () => {
543
531
  expect(err.code).toBe("CONFIG_ERROR");
544
532
  expect(err.message).toContain("pkg-remote");
545
533
  expect(err.message).not.toContain("Internal error");
546
- expect(readLock(cwd)).toBeUndefined();
534
+ expect(readActivePack(cwd)).toBeUndefined();
547
535
  });
548
536
 
549
537
  it("names the SPECIFIC bad file among several good ones", async () => {
@@ -557,12 +545,12 @@ describe("sources update — data-failure classification (U6)", () => {
557
545
  const cwd = tmp("pragma-proj-");
558
546
  const runtime = runtimeFor(cwd, [{ name: "mix", source: `file://${pkg}` }]);
559
547
 
560
- await expect(buildUpdateTask(runtime, false)).rejects.toMatchObject({
548
+ await expect(buildUpdateTask(runtime)).rejects.toMatchObject({
561
549
  code: "CONFIG_ERROR",
562
550
  });
563
551
  let caught: unknown;
564
552
  try {
565
- await buildUpdateTask(runtime, false);
553
+ await buildUpdateTask(runtime);
566
554
  } catch (error) {
567
555
  caught = error;
568
556
  }
@@ -594,9 +582,9 @@ describe("sources update — hidden files and --skip-invalid", () => {
594
582
  { name: "pkg-a", source: `file://${pkg}` },
595
583
  ]);
596
584
 
597
- const result = await runTask(await buildUpdateTask(runtime, false));
585
+ const result = await runTask(await buildUpdateTask(runtime));
598
586
  expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
599
- expect(readLock(cwd)?.packs).toHaveLength(1);
587
+ expect(readActivePack(cwd)).toMatch(/^[0-9a-f]{64}$/);
600
588
  });
601
589
 
602
590
  it("without --skip-invalid, one malformed source fails the whole update", async () => {
@@ -605,10 +593,10 @@ describe("sources update — hidden files and --skip-invalid", () => {
605
593
  const runtime = runtimeFor(cwd, [
606
594
  { name: "pkg-a", source: `file://${pkg}` },
607
595
  ]);
608
- await expect(buildUpdateTask(runtime, false, false)).rejects.toMatchObject({
596
+ await expect(buildUpdateTask(runtime, false)).rejects.toMatchObject({
609
597
  code: "CONFIG_ERROR",
610
598
  });
611
- expect(readLock(cwd)).toBeUndefined();
599
+ expect(readActivePack(cwd)).toBeUndefined();
612
600
  });
613
601
 
614
602
  it("with --skip-invalid, drops the bad source, warns loudly, and builds from the rest", async () => {
@@ -620,10 +608,10 @@ describe("sources update — hidden files and --skip-invalid", () => {
620
608
  report: (message: string) => reports.push(message),
621
609
  };
622
610
 
623
- const result = await runTask(await buildUpdateTask(runtime, false, true));
611
+ const result = await runTask(await buildUpdateTask(runtime, true));
624
612
  // Built from the good widget.ttl, not aborted.
625
613
  expect(result.contentHash).toMatch(/^[0-9a-f]{64}$/);
626
- expect(readLock(cwd)?.packs).toHaveLength(1);
614
+ expect(readActivePack(cwd)).toMatch(/^[0-9a-f]{64}$/);
627
615
  // Loud per-source warning names the dropped file …
628
616
  expect(
629
617
  reports.some(
@@ -643,10 +631,10 @@ describe("sources update — hidden files and --skip-invalid", () => {
643
631
  ...runtimeFor(cwd, [{ name: "pkg-a", source: `file://${pkg}` }]),
644
632
  report: () => {},
645
633
  };
646
- await expect(buildUpdateTask(runtime, false, true)).rejects.toMatchObject({
634
+ await expect(buildUpdateTask(runtime, true)).rejects.toMatchObject({
647
635
  code: "CONFIG_ERROR",
648
636
  });
649
- expect(readLock(cwd)).toBeUndefined();
637
+ expect(readActivePack(cwd)).toBeUndefined();
650
638
  });
651
639
  });
652
640
 
@@ -740,7 +728,7 @@ describe("sources update — installs package skills (U10)", () => {
740
728
  { name: "pkg-a", source: `file://${pkg}` },
741
729
  ]);
742
730
 
743
- await runTask(await buildUpdateTask(runtime, false));
731
+ await runTask(await buildUpdateTask(runtime));
744
732
 
745
733
  // Installed as a symlink into $XDG_DATA_HOME/pragma/skills …
746
734
  const linked = join(dataHome, "pragma", "skills", "foo");
@@ -762,7 +750,7 @@ describe("sources update — installs package skills (U10)", () => {
762
750
  const runtime = runtimeFor(cwd, [
763
751
  { name: "pkg-a", source: `file://${pkg}` },
764
752
  ]);
765
- await runTask(await buildUpdateTask(runtime, false));
753
+ await runTask(await buildUpdateTask(runtime));
766
754
 
767
755
  const shared = discoverSkills(cwd).filter((s) => s.name === "shared");
768
756
  expect(shared).toHaveLength(1);
@@ -776,35 +764,42 @@ describe("sources update — installs package skills (U10)", () => {
776
764
  { name: "pkg-a", source: `file://${pkg}` },
777
765
  ]);
778
766
  const { runUndo } = await import("@canonical/task/node");
779
- await runUndo(await buildUpdateTask(runtime, false));
767
+ await runUndo(await buildUpdateTask(runtime));
780
768
 
781
769
  expect(existsSync(join(dataHome, "pragma", "skills", "bar"))).toBe(false);
782
770
  });
783
771
  });
784
772
 
785
773
  describe("sources status — entityCount from the manifest (A10)", () => {
786
- it("reports the count without re-parsing index.json", async () => {
787
- const pkg = filePackage(); // ex:Widget class + ex:one individual
774
+ it("reports the count from the manifest even when index.json is unreadable", async () => {
775
+ // A pack of its OWN (unique TTL ⇒ unique content hash), so corrupting its
776
+ // cached index cannot reach any other test's pack in the shared cache.
777
+ const pkg = tmp("pragma-a10-pkg-");
778
+ mkdirSync(join(pkg, "definitions"), { recursive: true });
779
+ writeFileSync(
780
+ join(pkg, "definitions", "widget.ttl"),
781
+ `${TTL}ex:a10only a ex:Widget ; rdfs:label "A10" .\n`,
782
+ );
788
783
  const cwd = tmp("pragma-proj-");
789
784
  const runtime = runtimeFor(cwd, [
790
- { name: "pkg-a", source: `file://${pkg}` },
785
+ { name: "pkg-a10", source: `file://${pkg}` },
791
786
  ]);
792
- const result = await runTask(await buildUpdateTask(runtime, false));
787
+ const result = await runTask(await buildUpdateTask(runtime));
793
788
 
794
- // Delete index.json: any path that PARSED it to count would now fail —
795
- // proving the figure is read from the manifest's persisted entityCount.
796
- rmSync(join(packDir(result.contentHash), "index.json"), { force: true });
789
+ // Corrupt index.json but keep it non-empty, so the pack still reads as
790
+ // complete: any path that PARSED it to count would now yield null.
791
+ writeFileSync(join(packDir(result.contentHash), "index.json"), "not json");
797
792
 
798
793
  const status = await collectStatus(bootRuntime(flagsJson, cwd));
799
- // One abox individual (ex:one); the ex:Widget class is tbox, not counted.
800
- expect(status.entityCount).toBe(1);
794
+ // Two abox individuals (ex:one, ex:a10only); ex:Widget is tbox, not counted.
795
+ expect(status.entityCount).toBe(2);
801
796
  });
802
797
  });
803
798
 
804
799
  describe("sources status CLI-json == MCP tool (PROTECTED)", () => {
805
- it("a cold store's status is byte-equal on both surfaces", async () => {
806
- // Both surfaces boot from the SAME cwd + isolated config (default packages,
807
- // no lock), so the storeless status must be identical.
800
+ it("a fresh install's status is byte-equal on both surfaces", async () => {
801
+ // Both surfaces boot from the SAME cwd + isolated config (default packs,
802
+ // nothing built), so the storeless status must be identical.
808
803
  const cwd = tmp("pragma-proj-");
809
804
  const cli = await executeVerb(
810
805
  statusVerb,
@@ -820,9 +815,8 @@ describe("sources status CLI-json == MCP tool (PROTECTED)", () => {
820
815
 
821
816
  expect(cliEnvelope).toEqual(mcpEnvelope);
822
817
  expect(cliEnvelope.ok).toBe(true);
823
- expect((cliEnvelope.data as { lockPresent: boolean }).lockPresent).toBe(
824
- false,
825
- );
826
- expect((cliEnvelope.data as { cached: boolean }).cached).toBe(false);
818
+ // Nothing built here, and the packs are the distribution's own — so the
819
+ // embedded snapshot is what answers reads, and status says exactly that.
820
+ expect((cliEnvelope.data as { store: string }).store).toBe("embedded");
827
821
  });
828
822
  });
@@ -3,26 +3,17 @@ import { type RenderStyle, styleFor } from "../../kernel/render/style.js";
3
3
  import { renderSourcesStatusPlain } from "./status.render.js";
4
4
  import type { SourcesStatusData } from "./types.js";
5
5
 
6
- const DATA: SourcesStatusData = {
6
+ /** A project reading from its own built pack, with two configured sources. */
7
+ const BUILT: SourcesStatusData = {
7
8
  cwd: "/repo",
8
- lockPresent: true,
9
+ store: "built",
9
10
  contentHash: "abcdef012345aaaa",
10
- cached: true,
11
+ sourceRef: "ds,extra",
11
12
  builtAt: "2026-01-01",
12
13
  entityCount: 42,
13
14
  sources: [
14
- {
15
- name: "ds",
16
- ref: "ds@1",
17
- resolved: "1111111111112222",
18
- staleness: "up-to-date",
19
- },
20
- {
21
- name: "extra",
22
- ref: "extra@1",
23
- resolved: null,
24
- staleness: "config-drift",
25
- },
15
+ { name: "ds", ref: "git+https://example.test/ds.git#main" },
16
+ { name: "extra", ref: "extra@1" },
26
17
  ],
27
18
  };
28
19
 
@@ -38,28 +29,66 @@ const TAGGED: RenderStyle = {
38
29
 
39
30
  describe("renderSourcesStatusPlain", () => {
40
31
  it("is byte-identical to the plain form when the styler is disabled", () => {
41
- expect(renderSourcesStatusPlain(DATA, styleFor(false))).toBe(
32
+ expect(renderSourcesStatusPlain(BUILT, styleFor(false))).toBe(
42
33
  [
43
34
  "Store: ready",
44
35
  " pack: abcdef012345 — 42 entities, built 2026-01-01",
36
+ " from: ds,extra",
45
37
  "",
46
38
  "Sources:",
47
- " ds [ok] @ 111111111111",
48
- " extra [config drift]",
39
+ " ds git+https://example.test/ds.git#main",
40
+ " extra extra@1",
49
41
  ].join("\n"),
50
42
  );
51
43
  });
52
44
 
53
- it("bolds the heading, aligns names, and tints markers on a TTY", () => {
54
- const out = renderSourcesStatusPlain(DATA, TAGGED);
45
+ it("bolds the heading, aligns names, and dims refs on a TTY", () => {
46
+ const out = renderSourcesStatusPlain(BUILT, TAGGED);
55
47
  const nameWidth = "extra".length;
56
48
  expect(out).toContain("<b>Sources:</b>");
57
- // up-to-date tints green with a dim resolved suffix; drift tints yellow.
58
49
  expect(out).toContain(
59
- ` ${"ds".padEnd(nameWidth)} <g>[ok]</g><d> @ 111111111111</d>`,
50
+ ` ${"ds".padEnd(nameWidth)} <d>git+https://example.test/ds.git#main</d>`,
60
51
  );
61
- expect(out).toContain(
62
- ` ${"extra".padEnd(nameWidth)} <y>[config drift]</y>`,
52
+ expect(out).toContain(` ${"extra".padEnd(nameWidth)} <d>extra@1</d>`);
53
+ });
54
+
55
+ it("names the embedded snapshot AND the update that would replace it", () => {
56
+ // The embedded pack answers reads on a fresh install, but it IS a snapshot —
57
+ // rendering it as "ready" would read as "up to date", which it is not.
58
+ const out = renderSourcesStatusPlain(
59
+ {
60
+ ...BUILT,
61
+ store: "embedded",
62
+ sourceRef: "@canonical/design-system@git:abc",
63
+ },
64
+ styleFor(false),
65
+ );
66
+ expect(out).toContain("Store: embedded snapshot");
67
+ expect(out).toContain("pragma sources update");
68
+ expect(out).toContain(" from: @canonical/design-system@git:abc");
69
+ });
70
+
71
+ it("reports an unbuilt store with its recovery and no pack line", () => {
72
+ const out = renderSourcesStatusPlain(
73
+ {
74
+ ...BUILT,
75
+ store: "unavailable",
76
+ contentHash: null,
77
+ sourceRef: null,
78
+ builtAt: null,
79
+ entityCount: null,
80
+ },
81
+ styleFor(false),
82
+ );
83
+ expect(out).toContain("Store: not built (run `pragma sources update`)");
84
+ expect(out).not.toContain(" pack: ");
85
+ });
86
+
87
+ it("says so when no packs are configured", () => {
88
+ const out = renderSourcesStatusPlain(
89
+ { ...BUILT, sources: [] },
90
+ styleFor(false),
63
91
  );
92
+ expect(out).toContain(" (none configured)");
64
93
  });
65
94
  });