@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,15 +2,14 @@
2
2
  * Formatters for `skill list` and `skill lookup`.
3
3
  */
4
4
 
5
+ import { BIN_NAME } from "../../constants.js";
5
6
  import type { Formatters } from "../../kernel/spec/types.js";
6
7
  import type { DiscoveredSkill } from "./discover.js";
7
8
  import type { SkillLookup } from "./verbs.js";
8
9
 
9
- /** Empty-state guidance (U5) — where skills come from now that packages ship them. */
10
- const EMPTY_PLAIN =
11
- "No skills found.\nSkills come from installed design-system packages — add one to your config, run `pragma sources update`, then `pragma setup skills`.";
12
- const EMPTY_LLM =
13
- "## Skills (0)\n\nNo skills found. Skills come from installed design-system packages — add one to the project config, run `pragma sources update`, then `pragma setup skills`.";
10
+ /** Empty-state guidance (U5) — where skills come from now that packs ship them. */
11
+ const EMPTY_PLAIN = `No skills found.\nSkills come from design-system packs — add one to your config's packs, run \`${BIN_NAME} sources update\`, then \`${BIN_NAME} setup skills\`.`;
12
+ const EMPTY_LLM = `## Skills (0)\n\nNo skills found. Skills come from design-system packs — add one to the project config's packs, run \`${BIN_NAME} sources update\`, then \`${BIN_NAME} setup skills\`.`;
14
13
 
15
14
  export const skillListFormatters: Formatters<DiscoveredSkill[]> = {
16
15
  plain: (skills) =>
@@ -8,7 +8,9 @@
8
8
 
9
9
  import { readFileSync } from "node:fs";
10
10
  import { join } from "node:path";
11
+ import { BIN_NAME } from "../../constants.js";
11
12
  import { PragmaError } from "../../kernel/error/PragmaError.js";
13
+ import { cliRecovery } from "../../kernel/error/recovery.js";
12
14
  import { suggestNames } from "../../kernel/project/cli/suggestNames.js";
13
15
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
14
16
  import { asVerb } from "../../kernel/spec/asVerb.js";
@@ -26,7 +28,7 @@ const listVerb: VerbSpec<Record<string, unknown>, DiscoveredSkill[]> = {
26
28
  summary: "List discovered skills (SKILL.md files under the skill roots).",
27
29
  params: [],
28
30
  output: { formatters: skillListFormatters },
29
- examples: [{ cmd: "pragma skill list" }],
31
+ examples: [{ cmd: `${BIN_NAME} skill list` }],
30
32
  capability: {
31
33
  needsStore: false,
32
34
  mutates: false,
@@ -53,7 +55,7 @@ const lookupVerb: VerbSpec<Record<string, unknown>, SkillLookup> = {
53
55
  },
54
56
  ],
55
57
  output: { formatters: skillLookupFormatters },
56
- examples: [{ cmd: "pragma skill lookup docx" }],
58
+ examples: [{ cmd: `${BIN_NAME} skill lookup docx` }],
57
59
  capability: {
58
60
  needsStore: false,
59
61
  mutates: false,
@@ -74,11 +76,9 @@ const lookupVerb: VerbSpec<Record<string, unknown>, SkillLookup> = {
74
76
  name,
75
77
  skills.map((skill) => skill.name),
76
78
  ),
77
- recovery: {
78
- message: "List discovered skills.",
79
- cli: "pragma skill list",
80
- mcp: { tool: "skill_list" },
81
- },
79
+ recovery: cliRecovery("skill list", "List discovered skills.", {
80
+ tool: "skill_list",
81
+ }),
82
82
  });
83
83
  }
84
84
  return { ...match, instructions: readInstructions(match.sourcePath) };
@@ -1,35 +1,32 @@
1
1
  /**
2
2
  * Collect the `sources status` payload — storeless.
3
3
  *
4
- * Reports the store's readiness without booting it: it reads the lock, the
5
- * resolved config, and — if the locked pack is cached — the manifest and the
6
- * index.json count (a plain `JSON.parse`, no store, no oxigraph). This is the
7
- * capability that must stay off the store factory (the storeless-guarantee spy
8
- * covers it), so it reaches for the pack files directly rather than through a
9
- * store session. It absorbs the v1 `info store` summary.
4
+ * Reports the store's readiness without booting it. The `store` field IS the
5
+ * boot decision ({@link resolveSources}) — status never re-derives which pack
6
+ * answers reads, so it can never disagree with what the next read actually
7
+ * loads. Everything else is provenance read straight off that pack's manifest
8
+ * (a plain `JSON.parse`, no store, no oxigraph). This is the capability that
9
+ * must stay off the store factory (the storeless-guarantee spy covers it). It
10
+ * absorbs the v1 `info store` summary.
10
11
  */
11
12
 
12
13
  import { existsSync, readFileSync } from "node:fs";
13
14
  import { join } from "node:path";
14
- import type { PackageEntry } from "../../kernel/config/types.js";
15
+ import type { PackDeclaration } from "../../kernel/config/types.js";
16
+ import { embeddedManifest } from "../../kernel/runtime/graphpack/embedded.js";
15
17
  import { readManifest } from "../../kernel/runtime/graphpack/manifest.js";
16
18
  import { INDEX_FILE } from "../../kernel/runtime/graphpack/types.js";
17
- import { readLock } from "../../kernel/runtime/lock.js";
18
- import { packDir } from "../../kernel/runtime/paths.js";
19
+ import { resolveSources } from "../../kernel/runtime/resolveSources.js";
19
20
  import type { PragmaRuntime } from "../../kernel/runtime/types.js";
20
- import type {
21
- SourceStaleness,
22
- SourceStatusEntry,
23
- SourcesStatusData,
24
- } from "./types.js";
21
+ import type { SourcesStatusData } from "./types.js";
25
22
 
26
- const entryName = (entry: PackageEntry): string =>
23
+ const entryName = (entry: PackDeclaration): string =>
27
24
  typeof entry === "string" ? entry : entry.name;
28
- const entrySource = (entry: PackageEntry): string =>
25
+ const entrySource = (entry: PackDeclaration): string =>
29
26
  typeof entry === "string" ? entry : (entry.source ?? entry.name);
30
27
 
31
28
  /**
32
- * The distinct-abox entity count from a cached pack's index.json (storeless).
29
+ * The distinct-abox entity count from a built pack's index.json (storeless).
33
30
  *
34
31
  * Only reached for LEGACY packs whose manifest predates the persisted
35
32
  * `entityCount` (A10) — a current pack's count is read straight from the
@@ -59,44 +56,52 @@ function indexEntityCount(dir: string): number | null {
59
56
  *
60
57
  * @param runtime - The per-invocation runtime.
61
58
  * @returns The storeless status payload.
62
- * @note Impure — reads config, the lock, and the pack cache from disk.
59
+ * @note Impure — reads config, the active-pack pointer, and the pack cache.
63
60
  */
64
61
  export async function collectStatus(
65
62
  runtime: PragmaRuntime,
66
63
  ): Promise<SourcesStatusData> {
67
64
  const layers = await runtime.loadConfig();
68
- const entries = layers.config.packages ?? [];
69
- const lock = readLock(runtime.cwd);
65
+ const sources = (layers.config.packs ?? []).map((entry) => ({
66
+ name: entryName(entry),
67
+ ref: entrySource(entry),
68
+ }));
69
+ const decision = resolveSources(layers, runtime.cwd);
70
+ const base = { cwd: runtime.cwd, sources };
70
71
 
71
- const dir = lock ? packDir(lock.contentHash) : undefined;
72
- const manifest = dir ? readManifest(dir) : undefined;
73
- const cached = manifest !== undefined;
74
-
75
- const sources: SourceStatusEntry[] = entries.map((entry) => {
76
- const name = entryName(entry);
77
- const ref = entrySource(entry);
78
- const lockEntry = lock?.packs.find((pack) => pack.name === name);
79
- let staleness: SourceStaleness;
80
- if (!lock || !cached) {
81
- staleness = "uncached";
82
- } else if (!lockEntry || lockEntry.source !== ref) {
83
- staleness = "config-drift";
84
- } else {
85
- staleness = "up-to-date";
72
+ switch (decision.kind) {
73
+ case "embedded": {
74
+ const manifest = embeddedManifest();
75
+ return {
76
+ ...base,
77
+ store: "embedded",
78
+ contentHash: manifest.contentHash,
79
+ sourceRef: manifest.sourceRef,
80
+ builtAt: manifest.createdAt,
81
+ entityCount: manifest.entityCount ?? null,
82
+ };
86
83
  }
87
- return { name, ref, resolved: lockEntry?.resolved ?? null, staleness };
88
- });
89
-
90
- return {
91
- cwd: runtime.cwd,
92
- lockPresent: lock !== undefined,
93
- contentHash: lock?.contentHash ?? null,
94
- cached,
95
- builtAt: manifest?.createdAt ?? null,
96
- // Prefer the manifest's persisted count (no index parse); a legacy pack
97
- // without it falls back to counting the index's abox subjects (A10).
98
- entityCount:
99
- dir && cached ? (manifest?.entityCount ?? indexEntityCount(dir)) : null,
100
- sources,
101
- };
84
+ case "pack": {
85
+ const manifest = readManifest(decision.dir);
86
+ return {
87
+ ...base,
88
+ store: "built",
89
+ contentHash: decision.contentHash,
90
+ sourceRef: manifest?.sourceRef ?? null,
91
+ builtAt: manifest?.createdAt ?? null,
92
+ // Prefer the manifest's persisted count (no index parse); a legacy pack
93
+ // without it falls back to counting the index's abox subjects (A10).
94
+ entityCount: manifest?.entityCount ?? indexEntityCount(decision.dir),
95
+ };
96
+ }
97
+ case "unavailable":
98
+ return {
99
+ ...base,
100
+ store: "unavailable",
101
+ contentHash: null,
102
+ sourceRef: null,
103
+ builtAt: null,
104
+ entityCount: null,
105
+ };
106
+ }
102
107
  }
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * The `sources` capability barrel — `status` (storeless read) and `update`
3
- * (the resolve/build/lock Task).
3
+ * (the resolve/build/point Task).
4
4
  */
5
5
 
6
6
  import { asVerb } from "../../kernel/spec/asVerb.js";
@@ -4,9 +4,9 @@
4
4
  * Design-system packages ship a top-level `skills/<name>/SKILL.md`, but skill
5
5
  * discovery only scans the project root and the installed-skills root — never a
6
6
  * resolved package's clone/dir. So `sources update` INSTALLS each package's
7
- * skills into the installed-skills root (`$XDG_DATA_HOME/pragma/skills`) as
7
+ * skills into the installed-skills root (`$XDG_DATA_HOME/<bin>/skills`) as
8
8
  * symlinks, exactly the root `discoverSkills` already reads. Precedence is
9
- * preserved (a project `.pragma/skills` entry still wins — it is discovered
9
+ * preserved (a project `.<bin>/skills` entry still wins — it is discovered
10
10
  * first), and the update Task stays reversible (each created link carries an
11
11
  * unlink undo).
12
12
  *
@@ -5,18 +5,19 @@
5
5
  *
6
6
  * - Preview (CLI `--dry-run`, or MCP without `confirm`): NETWORK-FREE. It reads
7
7
  * only the config, then hands back a plan-only Task listing the refs it would
8
- * resolve+build and the lock it would write — no git fetch, no compile, no
8
+ * resolve+build and the pointer it would write — no git fetch, no compile, no
9
9
  * cache write. This is what a dry-run / agent "preview" must be: side-effect
10
10
  * free and offline-safe.
11
- * - Real execution: the heavy work — resolve every configured package (git
11
+ * - Real execution: the heavy work — resolve every configured pack (git
12
12
  * clone/fetch, file verify, npm resolve), then build the ONE combined
13
13
  * content-addressed pack — runs eagerly (it is not expressible as a task
14
14
  * effect: the effect set is fs + exec, and the in-process compile is not an
15
- * effect at all). The returned Task models the one project mutation, the
16
- * `pragma.lock.json` write, with an undo that restores (or removes) the prior
17
- * lock. Under `--frozen` each package re-resolves to the lock's pinned
18
- * revision and keeps its `resolvedAt`, so an unchanged update rewrites a
19
- * byte-identical lock.
15
+ * effect at all). The returned Task models the mutations: the active-pack
16
+ * pointer write (undo restores or removes the prior pointer) and the package
17
+ * skill symlinks.
18
+ *
19
+ * Pinning a revision is the ref's job, not a flag's: put a SHA in the source
20
+ * (`git+https://…#<sha>`) and every update resolves to exactly that commit.
20
21
  */
21
22
 
22
23
  import {
@@ -29,20 +30,14 @@ import {
29
30
  type Task,
30
31
  writeFile,
31
32
  } from "@canonical/task";
32
- import { RECOVERY_CLI_PREFIX, VERSION } from "../../constants.js";
33
- import type { PackageEntry } from "../../kernel/config/types.js";
33
+ import { VERSION } from "../../constants.js";
34
34
  import { PragmaError } from "../../kernel/error/PragmaError.js";
35
35
  import { cliRecovery } from "../../kernel/error/recovery.js";
36
36
  import { buildPack } from "../../kernel/runtime/graphpack/build.js";
37
- import {
38
- type PragmaLock,
39
- readLock,
40
- serializeLock,
41
- } from "../../kernel/runtime/lock.js";
42
- import { LOCK_BASENAME, lockPath } from "../../kernel/runtime/paths.js";
37
+ import { activePackPath, readActivePack } from "../../kernel/runtime/paths.js";
43
38
  import type { PackageRef } from "../../kernel/runtime/refs/parseRef.js";
44
39
  import {
45
- parsePackageEntry,
40
+ parsePackDeclaration,
46
41
  redactUrl,
47
42
  } from "../../kernel/runtime/refs/parseRef.js";
48
43
  import {
@@ -67,17 +62,43 @@ const CORE_PREFIXES: Readonly<Record<string, string>> = {
67
62
  dcterms: "http://purl.org/dc/terms/",
68
63
  };
69
64
 
70
- const entryName = (entry: PackageEntry): string =>
71
- typeof entry === "string" ? entry : entry.name;
65
+ /**
66
+ * The prefix map a pack is built with: core < the packs' own `@prefix` < config.
67
+ *
68
+ * A resolved package's own declarations are harvested from its TTL (ke does not
69
+ * fold parsed-Turtle prefixes into `store.prefixes`) and merged beneath config
70
+ * so the index compacts pack URIs to `pfx:Local` — the FROZEN `{name,type}`
71
+ * token contract — while config still wins any clash. The merged map is
72
+ * persisted in the manifest, so boot reads the same names.
73
+ *
74
+ * Exported because the bundler (`scripts/bundle.ts`) compiles the embedded pack
75
+ * through this exact precedence. If the two pipelines computed prefixes
76
+ * separately, the embed could compact entities differently from what the SAME
77
+ * config produces on a user's machine — a silent divergence in entity names.
78
+ *
79
+ * @param inputs - The resolved packages' labelled RDF sources.
80
+ * @param configPrefixes - The config layer's `prefixes`, if any.
81
+ * @returns The merged prefix map to build with.
82
+ */
83
+ export function buildPackPrefixes(
84
+ inputs: readonly { readonly content: string }[],
85
+ configPrefixes: Readonly<Record<string, string>> | undefined,
86
+ ): Record<string, string> {
87
+ return {
88
+ ...CORE_PREFIXES,
89
+ ...harvestPrefixes(inputs),
90
+ ...(configPrefixes ?? {}),
91
+ };
92
+ }
72
93
 
73
94
  /**
74
95
  * Build the NETWORK-FREE preview plan for `sources update`.
75
96
  *
76
97
  * A plan-only Task: it reads the config to list the refs a real update would
77
- * resolve and build, then models the single project mutation (the lock write)
78
- * as effects the dry-run interpreter can describe WITHOUT running them. No git,
79
- * no compile, no cache write — so `--dry-run` and an MCP plan-first call are
80
- * offline-safe. Config refs are parsed (a cheap, offline validity check) but
98
+ * resolve and build, then models the project mutation (the active-pack pointer
99
+ * write) as effects the dry-run interpreter can describe WITHOUT running them.
100
+ * No git, no compile, no cache write — so `--dry-run` and an MCP plan-first call
101
+ * are offline-safe. Config refs are parsed (a cheap, offline validity check) but
81
102
  * never resolved.
82
103
  *
83
104
  * @param runtime - The per-invocation runtime.
@@ -87,57 +108,51 @@ async function buildUpdatePlan(
87
108
  runtime: PragmaRuntime,
88
109
  ): Promise<Task<SourcesUpdateData>> {
89
110
  const layers = await runtime.loadConfig();
90
- const entries = layers.config.packages ?? [];
91
- const refs = entries.map(parsePackageEntry);
92
- const path = lockPath(runtime.cwd);
111
+ const entries = layers.config.packs ?? [];
112
+ const refs = entries.map(parsePackDeclaration);
93
113
 
94
114
  const data: SourcesUpdateData = {
95
115
  contentHash: "",
96
116
  reused: false,
97
- lockPath: path,
98
- packs: refs.map((ref) => ({ name: ref.pkg, resolved: "", sourceCount: 0 })),
99
- };
100
- // A representative (never-written) lock, so the previewed write shows a
101
- // plausible shape/size. Placeholders make clear nothing was resolved.
102
- const previewLock: PragmaLock = {
103
- version: 1,
104
- contentHash: "(resolved on execute)",
105
117
  packs: refs.map((ref) => ({
106
118
  name: ref.pkg,
107
- source: ref.source,
108
- resolved: "(resolved on execute)",
109
- resolvedAt: "(resolved on execute)",
119
+ resolved: "",
120
+ sourceCount: 0,
121
+ storyCount: 0,
110
122
  })),
111
123
  };
112
- const previewContent = serializeLock(previewLock);
113
124
 
114
125
  return gen(function* () {
115
126
  yield* $(
116
127
  info(
117
128
  refs.length > 0
118
- ? `Resolve and build ${refs.length} package(s): ${refs.map((ref) => ref.source).join(", ")}`
119
- : "No packages configured — the embedded pack answers store reads",
129
+ ? `Resolve and build ${refs.length} pack(s): ${refs.map((ref) => ref.source).join(", ")}`
130
+ : "No packs configured — the embedded pack answers store reads",
120
131
  ),
121
132
  );
122
- // The one project mutation, previewed (dry-run never executes it).
123
- yield* $(writeFile(path, previewContent));
133
+ // The project mutation, previewed (dry-run never executes it). The content
134
+ // is a placeholder: the real hash only exists once the build has run.
135
+ yield* $(
136
+ writeFile(activePackPath(runtime.cwd), "(the pack hash, on execute)"),
137
+ );
124
138
  return data;
125
139
  });
126
140
  }
127
141
 
128
142
  /**
129
- * Resolve, build, and produce the lock-writing Task — or, for a preview, the
143
+ * Resolve, build, and produce the pointer-writing Task — or, for a preview, the
130
144
  * network-free plan.
131
145
  *
132
146
  * @param runtime - The per-invocation runtime.
133
- * @param frozen - When true, re-resolve to the lock's pinned revisions only.
134
- * @returns A Task that writes `pragma.lock.json` and returns the update result.
147
+ * @param skipInvalid - Drop sources that fail to parse (warning about each)
148
+ * rather than failing the whole update.
149
+ * @returns A Task that points the project at the built pack and returns the
150
+ * update result.
135
151
  * @note Impure — resolves packages (may hit git) and builds the pack eagerly,
136
152
  * UNLESS `runtime.mutation.preview` is set, in which case it stays offline.
137
153
  */
138
154
  export async function buildUpdateTask(
139
155
  runtime: PragmaRuntime,
140
- frozen: boolean,
141
156
  skipInvalid = false,
142
157
  ): Promise<Task<SourcesUpdateData>> {
143
158
  if (runtime.mutation?.preview) return buildUpdatePlan(runtime);
@@ -149,55 +164,45 @@ export async function buildUpdateTask(
149
164
  const verbose = runtime.globalFlags.verbose;
150
165
 
151
166
  const layers = await runtime.loadConfig();
152
- const entries = layers.config.packages ?? [];
153
- const existing = readLock(runtime.cwd);
154
- const priorContent = existing ? serializeLock(existing) : undefined;
167
+ const entries = layers.config.packs ?? [];
168
+ const priorHash = readActivePack(runtime.cwd);
155
169
 
156
- const resolved: ResolvedPackage[] = [];
170
+ const resolved: (ResolvedPackage & { readonly kind: PackageRef["kind"] })[] =
171
+ [];
157
172
  for (const entry of entries) {
158
- const ref = parsePackageEntry(entry);
173
+ const ref = parsePackDeclaration(entry);
159
174
  report?.(resolveProgress(ref));
160
- const pinned = existing?.packs.find((pack) => pack.name === ref.pkg);
161
- resolved.push(
162
- await resolvePackage(ref, {
163
- cwd: runtime.cwd,
164
- frozen,
165
- pinned: pinned?.resolved,
166
- }),
167
- );
175
+ resolved.push({
176
+ kind: ref.kind,
177
+ ...(await resolvePackage(ref, { cwd: runtime.cwd })),
178
+ });
168
179
  }
169
180
 
170
- // Prefix precedence: core < pack < config. A resolved package's own
171
- // `@prefix` declarations are harvested from its TTL and merged beneath config
172
- // so the index compacts pack URIs to `pfx:Local` (the FROZEN {name,type}
173
- // token contract); config still wins any clash. The merged map is persisted
174
- // in the manifest, so boot reads the same names.
175
181
  const inputs = resolved.flatMap((pkg) => pkg.sources);
182
+ // The packages' own `stories/*.json`, carried into the pack verbatim (see
183
+ // graphpack/types.STORIES_FILE). Raw text: nothing interprets them here.
184
+ const stories = resolved.flatMap((pkg) => pkg.stories);
176
185
 
177
- // Refuse to lock an empty store (A4). A package that ships no `.ttl` (or no
178
- // configured packages at all) would build a 0-triple pack whose empty
186
+ // Refuse to build an empty store (A4). A package that ships no `.ttl` (or no
187
+ // configured packs at all) would build a 0-triple pack whose empty
179
188
  // `data.nq` fails the completeness gate — so the "successful" update boots to
180
189
  // a PERMANENT `STORE_UNAVAILABLE` loop. Fail loudly here, leaving the embedded
181
- // fallback (or the prior lock) intact, instead of writing that broken lock.
190
+ // pack (or the prior build) answering reads, instead of pointing at that one.
182
191
  if (inputs.length === 0) {
183
192
  throw PragmaError.configError(
184
193
  entries.length === 0
185
- ? "No packages are configured, so there are no sources to build a store from. The embedded sample pack answers reads until you add a package."
186
- : `The ${entries.length} configured package(s) resolved 0 RDF sources (no definitions/**.ttl or data/**.ttl). Refusing to write a lock for an empty store.`,
194
+ ? "No packs are configured, so there are no sources to build a store from. The embedded pack answers reads until you add a pack."
195
+ : `The ${entries.length} configured pack(s) resolved 0 RDF sources (no definitions/**.ttl or data/**.ttl). Refusing to build an empty store.`,
187
196
  {
188
197
  recovery: cliRecovery(
189
- `${RECOVERY_CLI_PREFIX}sources update --verbose`,
190
- "Add a package that ships `.ttl` under definitions/ or data/, then re-run.",
198
+ "sources update --verbose",
199
+ "Add a pack that ships `.ttl` under definitions/ or data/, then re-run.",
191
200
  ),
192
201
  },
193
202
  );
194
203
  }
195
204
 
196
- const prefixes = {
197
- ...CORE_PREFIXES,
198
- ...harvestPrefixes(inputs),
199
- ...(layers.config.prefixes ?? {}),
200
- };
205
+ const prefixes = buildPackPrefixes(inputs, layers.config.prefixes);
201
206
 
202
207
  // Warn on conflicting `@prefix` declarations across packages (A5): last-wins
203
208
  // is silent otherwise, compacting the losing package's URIs to the wrong
@@ -210,6 +215,8 @@ export async function buildUpdateTask(
210
215
 
211
216
  report?.(`Building store from ${inputs.length} source(s)`);
212
217
  if (verbose) for (const input of inputs) report?.(` parse ${input.path}`);
218
+ if (stories.length > 0)
219
+ report?.(`Carrying ${stories.length} package read story file(s)`);
213
220
 
214
221
  // Build the pack. On a parse/build failure, classify it as a NAMED data error
215
222
  // (U6) — not INTERNAL_ERROR's "please report this issue" — identifying the
@@ -217,7 +224,20 @@ export async function buildUpdateTask(
217
224
  // With `--skip-invalid`, drop the unparseable sources (warning LOUDLY about
218
225
  // each — never a silent partial graph) and build from the rest instead of
219
226
  // failing the whole update.
220
- const sourceRef = entries.map(entryName).join(",") || "embedded";
227
+ // `<name>@<kind>:<resolved>` — the SAME provenance label the bundler writes
228
+ // for the embed, so the manifest field means one thing whoever built the pack,
229
+ // and `sources status` / `doctor` can still answer "which revision is my store
230
+ // built from?" now that no lock records it. A `file:` pack resolves to a local
231
+ // PATH rather than a revision, so it contributes its name alone: a machine
232
+ // path is not provenance, and `sources status` already lists the ref.
233
+ const sourceRef =
234
+ resolved
235
+ .map((pkg) =>
236
+ pkg.kind === "file"
237
+ ? pkg.name
238
+ : `${pkg.name}@${pkg.kind}:${pkg.resolved}`,
239
+ )
240
+ .join(", ") || "embedded";
221
241
  let built: Awaited<ReturnType<typeof buildPack>>;
222
242
  try {
223
243
  built = await buildPack(inputs, {
@@ -225,6 +245,7 @@ export async function buildUpdateTask(
225
245
  version: VERSION,
226
246
  sourceRef,
227
247
  prefixes,
248
+ stories,
228
249
  });
229
250
  } catch (error) {
230
251
  if (!skipInvalid) throw await classifySourceBuildError(error, inputs);
@@ -244,7 +265,7 @@ export async function buildUpdateTask(
244
265
  `All ${inputs.length} configured source(s) failed to parse — nothing to build.`,
245
266
  {
246
267
  recovery: cliRecovery(
247
- `${RECOVERY_CLI_PREFIX}sources update --verbose`,
268
+ "sources update --verbose",
248
269
  "Fix the reported sources, then re-run.",
249
270
  ),
250
271
  },
@@ -256,11 +277,8 @@ export async function buildUpdateTask(
256
277
  sourceRef,
257
278
  // Re-harvest prefixes from only the sources that survived, so a dropped
258
279
  // file's declarations don't skew compaction.
259
- prefixes: {
260
- ...CORE_PREFIXES,
261
- ...harvestPrefixes(usableInputs),
262
- ...(layers.config.prefixes ?? {}),
263
- },
280
+ prefixes: buildPackPrefixes(usableInputs, layers.config.prefixes),
281
+ stories,
264
282
  });
265
283
  } catch (rebuildError) {
266
284
  throw await classifySourceBuildError(rebuildError, usableInputs);
@@ -270,64 +288,37 @@ export async function buildUpdateTask(
270
288
  `${built.reused ? "Reused" : "Built"} store ${built.contentHash.slice(0, 12)}`,
271
289
  );
272
290
 
273
- // Refuse to lock a 0-triple build (A4): non-empty sources can still parse to
274
- // no triples (e.g. comment-only TTL, or a file of only `@prefix` lines). A
275
- // locked empty pack boots to the same `STORE_UNAVAILABLE` loop, so treat it
276
- // as a data error rather than a silent "success". (A manifest predating the
277
- // triple count is left alone — `undefined` never trips this.)
291
+ // Refuse a 0-triple build (A4): non-empty sources can still parse to no
292
+ // triples (e.g. comment-only TTL, or a file of only `@prefix` lines). Pointing
293
+ // the project at an empty pack boots to the same `STORE_UNAVAILABLE` loop, so
294
+ // treat it as a data error rather than a silent "success". (A manifest
295
+ // predating the triple count is left alone — `undefined` never trips this.)
278
296
  if (built.manifest.tripleCount === 0) {
279
297
  throw PragmaError.configError(
280
- `The configured sources parsed to 0 RDF triples, so the store would be empty. Refusing to write a lock for an empty store.`,
298
+ `The configured sources parsed to 0 RDF triples, so the store would be empty. Refusing to build an empty store.`,
281
299
  {
282
300
  recovery: cliRecovery(
283
- `${RECOVERY_CLI_PREFIX}sources update --verbose`,
301
+ "sources update --verbose",
284
302
  "Check that the package sources actually contain RDF triples, then re-run.",
285
303
  ),
286
304
  },
287
305
  );
288
306
  }
289
307
 
290
- const now = new Date().toISOString();
291
- const lock: PragmaLock = {
292
- version: 1,
293
- contentHash: built.contentHash,
294
- packs: resolved.map((pkg) => {
295
- const prev = existing?.packs.find((entry) => entry.name === pkg.name);
296
- // Preserve the prior `resolvedAt` when nothing actually changed — under
297
- // --frozen (re-pinned to the same revision) AND on a plain re-run that
298
- // resolves the IDENTICAL revision from the same source. Otherwise every
299
- // `sources update` stamped a fresh timestamp and rewrote the lock, dirtying
300
- // the git tree on a no-op run (L1). A moved revision still restamps.
301
- const reproducible =
302
- prev !== undefined &&
303
- (frozen ||
304
- (prev.resolved === pkg.resolved && prev.source === pkg.source));
305
- return {
306
- name: pkg.name,
307
- source: pkg.source,
308
- resolved: pkg.resolved,
309
- resolvedAt: reproducible && prev ? prev.resolvedAt : now,
310
- };
311
- }),
312
- };
313
- const newContent = serializeLock(lock);
314
- const path = lockPath(runtime.cwd);
315
-
308
+ const path = activePackPath(runtime.cwd);
316
309
  const data: SourcesUpdateData = {
317
310
  contentHash: built.contentHash,
318
311
  reused: built.reused,
319
- lockPath: path,
320
312
  packs: resolved.map((pkg) => ({
321
313
  name: pkg.name,
322
314
  resolved: pkg.resolved,
323
315
  sourceCount: pkg.sources.length,
316
+ storyCount: pkg.stories.length,
324
317
  })),
325
318
  };
326
319
 
327
320
  const undo =
328
- priorContent !== undefined
329
- ? writeFile(path, priorContent)
330
- : deleteFile(path);
321
+ priorHash !== undefined ? writeFile(path, priorHash) : deleteFile(path);
331
322
 
332
323
  // Install package-provided skills (U10): symlink each resolved package's
333
324
  // `skills/*` into the installed-skills root, so `skill list` / `setup skills`
@@ -339,9 +330,9 @@ export async function buildUpdateTask(
339
330
  if (skillLinks.length > 0)
340
331
  report?.(`Installing ${skillLinks.length} skill(s)`);
341
332
 
342
- report?.(`Writing ${LOCK_BASENAME}`);
333
+ report?.(`Pointing ${runtime.cwd} at pack ${built.contentHash.slice(0, 12)}`);
343
334
  return gen(function* () {
344
- yield* $(writeFile(path, newContent, { undo }));
335
+ yield* $(writeFile(path, built.contentHash, { undo }));
345
336
  if (skillLinks.length > 0) {
346
337
  yield* $(mkdir(installedSkillsDir(), true));
347
338
  for (const link of skillLinks) {
@@ -398,7 +389,7 @@ export async function classifySourceBuildError(
398
389
  : "The configured package sources could not be built into a store";
399
390
  return PragmaError.configError(`${where}: ${detail}`, {
400
391
  recovery: cliRecovery(
401
- `${RECOVERY_CLI_PREFIX}sources update --verbose`,
392
+ "sources update --verbose",
402
393
  "Re-run with --verbose to see each file as it parses. If a package ships malformed RDF, report it to that package's maintainer.",
403
394
  ),
404
395
  });