@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
@@ -1,45 +1,68 @@
1
1
  import { mkdirSync, mkdtempSync, 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, describe, expect, it, vi } from "vitest";
5
+ import { readConfig } from "../config/readConfig.js";
6
+ import { embeddedManifest } from "../runtime/graphpack/embedded.js";
7
+ import { activePackPath, packDir } from "../runtime/paths.js";
8
+ import { resolveSources } from "../runtime/resolveSources.js";
5
9
  import type { CapabilityModule, VerbSpec } from "../spec/types.js";
6
10
  import { runComplete } from "./complete.js";
7
- import { createIndexEntityReader, indexCompletionEnv } from "./entitySource.js";
11
+ import {
12
+ createIndexEntityReader,
13
+ indexCompletionEnv,
14
+ readPackIndex,
15
+ } from "./entitySource.js";
8
16
 
9
- /** A fresh cwd with no lock → the reader falls back to the embedded pack. */
17
+ /** A fresh cwd with no pointer → the reader falls back to the embedded pack. */
10
18
  const freshCwd = (): string => mkdtempSync(join(tmpdir(), "pragma-entity-"));
11
19
 
12
- /** A project whose lock points at a crafted index.json in a temp pack cache. */
20
+ /** The 64-hex content hash the crafted pack is filed under. */
21
+ const CRAFTED_HASH = "1".repeat(64);
22
+
23
+ /** A project pointed at a crafted index.json in a temp pack cache. */
13
24
  function projectWithIndex(index: unknown): string {
14
25
  const cwd = mkdtempSync(join(tmpdir(), "pragma-idx-cwd-"));
15
26
  const cache = mkdtempSync(join(tmpdir(), "pragma-idx-cache-"));
16
27
  vi.stubEnv("XDG_CACHE_HOME", cache);
17
- writeFileSync(
18
- join(cwd, "pragma.lock.json"),
19
- JSON.stringify({ contentHash: "testhash" }),
20
- );
21
- const packDir = join(cache, "pragma", "packs", "testhash");
28
+ const pointer = activePackPath(cwd);
29
+ mkdirSync(dirname(pointer), { recursive: true });
30
+ writeFileSync(pointer, CRAFTED_HASH);
31
+ const packDir = join(cache, "pragma", "packs", CRAFTED_HASH);
22
32
  mkdirSync(packDir, { recursive: true });
23
33
  writeFileSync(join(packDir, "index.json"), JSON.stringify(index));
24
34
  return cwd;
25
35
  }
26
36
 
27
- /** A crafted index exercising the index / prompts / tiers / prefixes sources. */
37
+ /**
38
+ * A crafted index exercising the index / prefixes sources. Deliberately in a
39
+ * NEUTRAL namespace: the reader knows no entity families any more, so a test
40
+ * proving it reads `label` or `altNames` must not need this distribution's
41
+ * vocabulary to say so. Since L-OPEN-9 no live verb declares the `altNames`
42
+ * field (the tier lookup completes entity names like every declared lookup),
43
+ * so this crafted index is where the field's read contract is held.
44
+ *
45
+ * Real-vocabulary coverage lives in two places, and neither is here: the
46
+ * PROTECTED contract describe below reads the shipped index for plain entity
47
+ * NAMES, and `safety.test.ts`'s "every declared name source resolves without
48
+ * constructing the store" drives the live grammar over the shipped index for
49
+ * entity names (`tier lookup ap` → `ds:apps`) and for `prefixes`.
50
+ */
28
51
  const CRAFTED_INDEX = {
29
52
  version: 2,
30
- contentHash: "testhash",
31
- prefixes: { ex: "https://example.com/", ds: "https://ds.canonical.com/" },
53
+ contentHash: CRAFTED_HASH,
54
+ prefixes: { ex: "https://example.com/" },
32
55
  instanceCountByType: {},
33
56
  entities: [
34
57
  { name: "ex:Button", type: "ex:Component" },
35
- { name: "ds:prompt.build", type: "ds:Prompt", label: "build-a-block" },
58
+ { name: "ex:prompt.build", type: "ex:Prompt", label: "build-a-block" },
36
59
  {
37
- name: "ds:tier.lxd",
38
- type: "ds:Tier",
60
+ name: "ex:tier.lxd",
61
+ type: "ex:Tier",
39
62
  label: "LXD",
40
63
  altNames: ["apps/lxd"],
41
64
  },
42
- { name: "ds:tier.core", type: "ds:Tier", label: "core" },
65
+ { name: "ex:tier.core", type: "ex:Tier", label: "core" },
43
66
  ],
44
67
  };
45
68
 
@@ -55,15 +78,27 @@ describe("indexCompletionEnv — multi-source names(ref)", () => {
55
78
  ]);
56
79
  });
57
80
 
58
- it("prompts: ds:Prompt entities emit label || name", async () => {
81
+ it("index + field label: emits the label, and nothing for an entity without one", async () => {
59
82
  const env = indexCompletionEnv(projectWithIndex(CRAFTED_INDEX));
60
- expect(await env.names({ from: "prompts" })).toEqual(["build-a-block"]);
83
+ expect(
84
+ await env.names({ from: "index", type: "ex:Prompt", field: "label" }),
85
+ ).toEqual(["build-a-block"]);
86
+ // `ex:Button` carries no label. The lookup that declared this field matches
87
+ // on the label, so standing the name in would offer a token that lookup
88
+ // cannot resolve.
89
+ expect(
90
+ await env.names({ from: "index", type: "ex:Component", field: "label" }),
91
+ ).toEqual([]);
61
92
  });
62
93
 
63
- it("tiers: emit ds:name (altNames) when present, else label ?? name", async () => {
94
+ it("index + field altNames: emits only the alt names, never a label in their place", async () => {
64
95
  const env = indexCompletionEnv(projectWithIndex(CRAFTED_INDEX));
65
- // apps/lxd from altNames (the ds:name); core from the label fallback.
66
- expect(await env.names({ from: "tiers" })).toEqual(["apps/lxd", "core"]);
96
+ // `ex:tier.lxd` carries `apps/lxd`; `ex:tier.core` carries none, so its
97
+ // label `core` is NOT offered — a bespoke tier lookup filters on the
98
+ // declared alt-name property and would refuse it.
99
+ expect(
100
+ await env.names({ from: "index", type: "ex:Tier", field: "altNames" }),
101
+ ).toEqual(["apps/lxd"]);
67
102
  });
68
103
 
69
104
  it("prefixes: the index's prefixes ∪ the default display map", async () => {
@@ -92,18 +127,23 @@ describe("indexCompletionEnv — multi-source names(ref)", () => {
92
127
  expect(await env.names({ from: "skills" })).toEqual(["docx"]);
93
128
  });
94
129
 
95
- it("missing sources degrade to [] (prefixes still lists the default map)", async () => {
130
+ it("a type nothing matches yields [] (prefixes still list the default map)", async () => {
96
131
  const env = indexCompletionEnv(
97
132
  projectWithIndex({
98
133
  version: 2,
99
- contentHash: "testhash",
134
+ contentHash: CRAFTED_HASH,
100
135
  prefixes: {},
101
136
  instanceCountByType: {},
102
137
  entities: [{ name: "ex:Button", type: "ex:Component" }],
103
138
  }),
104
139
  );
105
- expect(await env.names({ from: "prompts" })).toEqual([]);
106
- expect(await env.names({ from: "tiers" })).toEqual([]);
140
+ expect(
141
+ await env.names({ from: "index", type: "ex:Prompt", field: "label" }),
142
+ ).toEqual([]);
143
+ expect(
144
+ await env.names({ from: "index", type: "ex:Tier", field: "altNames" }),
145
+ ).toEqual([]);
146
+ // The display map is compiled in, so it answers with no index at all.
107
147
  expect(await env.names({ from: "prefixes" })).toContain("ds");
108
148
  });
109
149
  });
@@ -112,16 +152,18 @@ describe("entity source contract (PROTECTED)", () => {
112
152
  it("reads the embedded index storelessly, filtering by type + partial", () => {
113
153
  const read = createIndexEntityReader(freshCwd());
114
154
 
115
- // Abox individuals of ex:Component, sorted.
116
- expect(read("ex:Component", "")).toEqual([
117
- "ex:Button",
118
- "ex:Card",
119
- "ex:Dialog",
120
- ]);
155
+ // An abox individual of ds:Component. Membership, not the full 100+ list:
156
+ // the roster moves whenever the design system does, but a design system
157
+ // without a button is a change a human should look at.
158
+ expect(read("ds:Component", "")).toContain("ds:global.component.button");
121
159
  // Tbox class.
122
- expect(read("owl:Class", "")).toEqual(["ex:Component"]);
123
- // Partial-prefix filter.
124
- expect(read("ex:Component", "ex:B")).toEqual(["ex:Button"]);
160
+ expect(read("owl:Class", "")).toContain("ds:Component");
161
+ // Partial-prefix filter: every candidate starts with what was typed.
162
+ const partial = read("ds:Component", "ds:global.component.but");
163
+ expect(partial).toContain("ds:global.component.button");
164
+ for (const name of partial) {
165
+ expect(name.startsWith("ds:global.component.but")).toBe(true);
166
+ }
125
167
  // Unknown type → no matches (never throws).
126
168
  expect(read("ds:Nope", "")).toEqual([]);
127
169
  });
@@ -129,16 +171,16 @@ describe("entity source contract (PROTECTED)", () => {
129
171
  it("relies only on the frozen { name, type } minimum", () => {
130
172
  const read = createIndexEntityReader(freshCwd());
131
173
  // Every result is a bare name token (string), usable with no other field.
132
- for (const name of read("ex:Component", "")) {
174
+ for (const name of read("ds:Component", "")) {
133
175
  expect(typeof name).toBe("string");
134
- expect(name.startsWith("ex:")).toBe(true);
176
+ expect(name.startsWith("ds:")).toBe(true);
135
177
  }
136
178
  });
137
179
 
138
180
  it("is fast (well under the 50ms storeless budget)", () => {
139
181
  const read = createIndexEntityReader(freshCwd());
140
182
  const start = performance.now();
141
- read("ex:Component", "");
183
+ read("ds:Component", "");
142
184
  expect(performance.now() - start).toBeLessThan(50);
143
185
  });
144
186
  });
@@ -159,7 +201,7 @@ describe("__complete entity tier wiring", () => {
159
201
  required: true,
160
202
  complete: {
161
203
  kind: "names",
162
- source: { from: "index", type: "ex:Component" },
204
+ source: { from: "index", type: "ds:Component" },
163
205
  },
164
206
  },
165
207
  ],
@@ -185,14 +227,116 @@ describe("__complete entity tier wiring", () => {
185
227
  // index-backed reader over cwd, adapted to the resolver's EntityNameReader.
186
228
  await expect(
187
229
  runComplete(
188
- ["block", "lookup", "ex:B"],
230
+ ["block", "lookup", "ds:global.component.but"],
189
231
  [lookupModule],
190
232
  indexCompletionEnv(freshCwd()),
191
233
  ),
192
- ).resolves.toEqual(["ex:Button"]);
234
+ ).resolves.toContain("ds:global.component.button");
193
235
  // Without the env, the entity tier yields nothing (grammar-only).
194
236
  await expect(
195
- runComplete(["block", "lookup", "ex:B"], [lookupModule]),
237
+ runComplete(
238
+ ["block", "lookup", "ds:global.component.but"],
239
+ [lookupModule],
240
+ ),
196
241
  ).resolves.toEqual([]);
197
242
  });
198
243
  });
244
+
245
+ describe("readPackIndex answers the boot decision, never around it", () => {
246
+ afterEach(() => {
247
+ vi.unstubAllEnvs();
248
+ });
249
+
250
+ // `info`, `doctor`, the MCP resource browser and native `prompts/list` all
251
+ // read through this. It takes the decision rather than re-deriving one, so it
252
+ // covers all three arms — including the one a re-derivation gets wrong.
253
+ it("reads the pack the decision names", () => {
254
+ projectWithIndex(CRAFTED_INDEX); // plants the pack under a stubbed cache
255
+ const index = readPackIndex({
256
+ kind: "pack",
257
+ dir: packDir(CRAFTED_HASH),
258
+ contentHash: CRAFTED_HASH,
259
+ });
260
+ expect(index?.contentHash).toBe(CRAFTED_HASH);
261
+ expect(index?.entities.map((entity) => entity.name)).toContain("ex:Button");
262
+ });
263
+
264
+ it("reads the embedded snapshot on the embedded arm", () => {
265
+ const index = readPackIndex({ kind: "embedded" });
266
+ expect(index?.contentHash).toBe(embeddedManifest().contentHash);
267
+ });
268
+
269
+ it("reads NOTHING when the store is unavailable", () => {
270
+ // The row that matters: a project that declared its own packs and never
271
+ // built them must not be listed the distribution's graph while every read
272
+ // of it fails STORE_UNAVAILABLE.
273
+ expect(readPackIndex({ kind: "unavailable", reason: "not built" })).toBe(
274
+ undefined,
275
+ );
276
+ });
277
+ });
278
+
279
+ describe("the storeless fast path implements the pointer half", () => {
280
+ afterEach(() => {
281
+ vi.unstubAllEnvs();
282
+ });
283
+
284
+ // `__complete` cannot see `origins.packs` (the config evaluator is banned on
285
+ // this graph), so it cannot distinguish a fresh install from a configured-but-
286
+ // unbuilt project. It CAN see the pointer, and must not prefer the snapshot
287
+ // over a pointer whose pack the cache lost — that is the decision table's
288
+ // second row, where the two readers used to disagree.
289
+ it("offers no candidates when the pointed-at pack is gone", async () => {
290
+ const cwd = mkdtempSync(join(tmpdir(), "pragma-idx-cwd-"));
291
+ const cache = mkdtempSync(join(tmpdir(), "pragma-idx-cache-"));
292
+ vi.stubEnv("XDG_CACHE_HOME", cache);
293
+ const pointer = activePackPath(cwd);
294
+ mkdirSync(dirname(pointer), { recursive: true });
295
+ writeFileSync(pointer, CRAFTED_HASH); // …but no pack directory for it.
296
+
297
+ expect(createIndexEntityReader(cwd)("ds:Component", "")).toEqual([]);
298
+ expect(
299
+ await indexCompletionEnv(cwd).names({ from: "index", type: "" }),
300
+ ).toEqual([]);
301
+ });
302
+
303
+ it("still offers the snapshot's names in a configured-but-unbuilt project (the documented price)", async () => {
304
+ // The bounded exception `entitySource.ts` documents, pinned as it IS and
305
+ // not as it should be. The fast path is deliberately denied the config
306
+ // layer, so a `pragma.config.ts` declaring packs is invisible to it: with
307
+ // no pointer it cannot tell this project from a fresh install, and it
308
+ // answers from the embedded snapshot.
309
+ //
310
+ // Both halves are asserted against the SAME cwd, because the ASYMMETRY is
311
+ // the whole claim — asserting only that the snapshot answers would restate
312
+ // the fresh-install case the PROTECTED contract describe already covers,
313
+ // and would pass with the config file deleted. The read half refusing is
314
+ // what makes this directory a configured-but-unbuilt project rather than a
315
+ // fresh install. (`doctor` says so too, in the same words: verified against
316
+ // the compiled binary, where `block list` raises STORE_UNAVAILABLE while
317
+ // this same completion still answers.)
318
+ //
319
+ // If a later change gives the fast path a config-free way to see
320
+ // `origins.packs`, THIS is the test that must move, deliberately.
321
+ const cwd = mkdtempSync(join(tmpdir(), "pragma-unbuilt-cwd-"));
322
+ vi.stubEnv(
323
+ "XDG_CACHE_HOME",
324
+ mkdtempSync(join(tmpdir(), "pragma-unbuilt-")),
325
+ );
326
+ writeFileSync(
327
+ join(cwd, "pragma.config.ts"),
328
+ 'export default { packs: [{ name: "unbuilt", source: "file:///pragma-never-built" }] };\n',
329
+ );
330
+
331
+ const decision = resolveSources(await readConfig(cwd), cwd);
332
+ expect(decision).toEqual({
333
+ kind: "unavailable",
334
+ reason: "packs are configured but the store has not been built",
335
+ });
336
+ expect(readPackIndex(decision)).toBeUndefined();
337
+
338
+ expect(createIndexEntityReader(cwd)("ds:Component", "")).toContain(
339
+ "ds:global.component.button",
340
+ );
341
+ });
342
+ });
@@ -4,46 +4,59 @@
4
4
  * browser (`info`, `doctor`, MCP prompts, the resource provider) shares.
5
5
  *
6
6
  * {@link indexCompletionEnv} is the one source-dispatched reader wired at the
7
- * `__complete` fast path (bin) and the `__complete` verb. It resolves five
7
+ * `__complete` fast path (bin) and the `__complete` verb. It resolves three
8
8
  * storeless sources — all disk-readable, never a store/SPARQL:
9
- * - `index` → the active pack's `index.json` entity names of a prefixed type.
9
+ * - `index` → the active pack's `index.json`, filtered by a prefixed `type`
10
+ * and read from the declared `field` (`name` by default).
10
11
  * - `skills` → SKILL.md names via {@link discoverSkills} (filesystem walk).
11
- * - `prompts` → the pack index's `ds:Prompt` entities (`label || name`).
12
- * - `tiers` → the pack index's `ds:Tier` entities (their `ds:name`, else
13
- * `label ?? name` when the index carries no `ds:name`).
14
12
  * - `prefixes` → the pack index's prefixes ∪ the default display prefixes.
15
13
  *
14
+ * There is no per-family source. An entity family IS a `type` filter plus the
15
+ * field that family is addressed by, and both are declared at the verb, so this
16
+ * module names no entity type at all.
17
+ *
16
18
  * The index is loaded once (lazy `readFileSync` + plain `JSON.parse` — never a
17
19
  * store, facade, config evaluator, or zod schema; any I/O or parse error
18
- * degrades to `[]`) and shared across the index/prompts/tiers/prefixes sources;
20
+ * degrades to `[]`) and shared across the index and prefixes sources;
19
21
  * the skills walk is memoized to one walk per `__complete` process.
20
22
  * {@link createIndexEntityReader} exposes the index read for the storeless
21
23
  * behavioural/safety tests; {@link emptyNameSource} is the default (no names).
22
24
  *
23
25
  * {@link readPackIndex} and {@link entityTotal} expose the same storeless read
24
- * to `info`/`doctor` and the resource surfaces. Everything here relies only on
25
- * the FROZEN `{ name, type }` index minimum (plus optional enrichment like
26
- * `label`/`altNames`), so the index can grow without breaking completion.
26
+ * to `info`/`doctor` and the resource surfaces — but those callers are NOT on
27
+ * the fast path, so `readPackIndex` takes the boot decision rather than
28
+ * re-deriving one. Everything here relies only on the FROZEN `{ name, type }`
29
+ * index minimum (plus optional enrichment like `label`/`altNames`), so the
30
+ * index can grow without breaking completion.
27
31
  *
28
32
  * Storeless-graph notes:
29
33
  * - The embedded fallback is read from `pack.index.generated` — its OWN
30
34
  * generated module (only the index string) — so the storeless `__complete`
31
- * path never pulls the n-quads/schema/manifest strings that live in
32
- * `pack.generated`.
33
- * - The pack-cache path is INLINED here rather than imported from
34
- * `kernel/runtime/paths` — that module reaches `kernel/config`, which the
35
- * storeless-guarantee test (`safety.test.ts`) forbids on the completion
36
- * import graph. The two lines of XDG resolution are duplicated on purpose to
37
- * keep the fast path free of the config layer; `PackIndex`/`PackIndexEntity`
38
- * are imported type-only, so no zod schema is loaded at runtime.
39
- * - `discoverSkills` (`node:fs/os/path` only) and `DEFAULT_PREFIX_MAP` (a pure
40
- * const) are the only new edges — both leaf-clean, so the fast path stays free
41
- * of boot/config/store/zod. The `ds:Prompt`/`ds:Tier` filters are INLINED
42
- * (importing `prompts/source.ts` would cycle back into this module).
35
+ * path never EVALUATES the n-quads/schema/manifest strings that live in
36
+ * `pack.generated`. It does not avoid parsing them: `bun build --compile`
37
+ * emits one script, so the whole embed is parsed at process start on every
38
+ * invocation (~+25 ms here, measured on `--version` against a toy-embed
39
+ * binary; BUDGETS.md records it). What the split buys is that a completion
40
+ * never allocates the 1.87 MB.
41
+ * - The active pack is resolved through `kernel/runtime/paths` — a LEAF module
42
+ * (node builtins only) that shares the pointer read with `resolveSources`.
43
+ * `resolveSources` itself is unreachable from here: it pulls the graphpack
44
+ * manifest schema (zod), which `safety.test.ts` forbids on this graph, and
45
+ * `origins.packs` needs the config evaluator. So {@link loadActiveIndex}
46
+ * implements only the POINTER half of the decision table — pointer → that
47
+ * pack (else nothing), no pointer → the embedded snapshot. It cannot tell a
48
+ * fresh install from a project that declared its own packs and never built
49
+ * them, so a `__complete` in the latter still offers the snapshot's names.
50
+ * That is the price of a config-free fast path, and it is bounded to
51
+ * completion candidates: every read, and every surface that takes the
52
+ * decision, refuses. `PackIndex`/`PackIndexEntity` are imported type-only, so
53
+ * no zod schema is loaded at runtime.
54
+ * - `discoverSkills` (`node:fs/os/path` only) and `DEFAULT_PREFIX_MAP` (whose
55
+ * domain half the distribution declares) are the only new edges — both
56
+ * leaf-clean, so the fast path stays free of boot/config/store/zod.
43
57
  */
44
58
 
45
- import { existsSync, readFileSync } from "node:fs";
46
- import { homedir } from "node:os";
59
+ import { readFileSync } from "node:fs";
47
60
  import { join } from "node:path";
48
61
  import { discoverSkills } from "../../capabilities/skill/discover.js";
49
62
  // Inlined embedded index — its OWN generated module (only the index string), so
@@ -52,49 +65,41 @@ import { discoverSkills } from "../../capabilities/skill/discover.js";
52
65
  import { DEFAULT_PREFIX_MAP } from "../render/prefixes.js";
53
66
  import { indexJson as EMBEDDED_INDEX_JSON } from "../runtime/graphpack/embedded/pack.index.generated.js";
54
67
  import type { PackIndex, PackIndexEntity } from "../runtime/graphpack/types.js";
55
- import type { CompletionSourceRef } from "../spec/types.js";
68
+ import { packDir, readActivePack } from "../runtime/paths.js";
69
+ import type { SourcesDecision } from "../runtime/resolveSources.js";
70
+ import type { CompletionField, CompletionSourceRef } from "../spec/types.js";
56
71
  import type { CompletionEnv } from "./types.js";
57
72
 
58
- /** The prefixed KG type every prompt entity carries (inlined; no cycle). */
59
- const PROMPT_TYPE = "ds:Prompt";
60
- /** The prefixed KG type every tier entity carries (inlined; no cycle). */
61
- const TIER_TYPE = "ds:Tier";
62
-
63
73
  /** The default name source: no index tier, so no candidates. */
64
74
  export const emptyNameSource: CompletionEnv["names"] = () => [];
65
75
 
66
- /** The committed project lock basename (mirrors `kernel/runtime/paths`). */
67
- const LOCK_BASENAME = "pragma.lock.json";
68
76
  /** The pack index filename (kept local so this path never imports the zod schema). */
69
77
  const INDEX_FILE = "index.json";
70
78
 
71
79
  /**
72
- * `$XDG_CACHE_HOME/pragma/packs/<hash>` — a pack's cache directory.
80
+ * Read the storeless index of the pack the BOOT DECISION names — for `info`,
81
+ * `doctor`, the MCP resource browser, and native `prompts/list`. Never boots the
82
+ * store, never validates with zod — a plain `JSON.parse` off disk.
73
83
  *
74
- * Inlined from `kernel/config/paths` + `kernel/runtime/paths` so the completion
75
- * fast path stays off the config layer (see the module docblock).
76
- */
77
- function packDir(contentHash: string): string {
78
- const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
79
- return join(base, "pragma", "packs", contentHash);
80
- }
81
-
82
- /** The committed project lock file for a working directory. */
83
- function lockPath(cwd: string): string {
84
- return join(cwd, LOCK_BASENAME);
85
- }
86
-
87
- /**
88
- * Read the active pack's storeless index (the locked pack, else the embedded
89
- * fallback), for the resource browser's list/autocomplete. Never boots the
90
- * store, never validates with zod — a plain `JSON.parse` off disk. Returns
91
- * `undefined` when no index is reachable, so callers degrade to a recovery hint.
84
+ * It takes the decision rather than re-deriving one so these surfaces cannot
85
+ * list entities the same project's reads refuse: `unavailable` yields
86
+ * `undefined`, and callers degrade to a recovery hint (`buildResourceList`
87
+ * emits the `pragma:sources` entry, `info` omits the total).
92
88
  *
93
- * @param cwd - The project directory (to resolve the active pack).
94
- * @returns The active pack index, or `undefined` when none is reachable.
89
+ * @param decision - The boot decision from `resolveSources`.
90
+ * @returns The answering pack's index, or `undefined` when none answers.
95
91
  */
96
- export function readPackIndex(cwd: string): PackIndex | undefined {
97
- return loadActiveIndex(cwd);
92
+ export function readPackIndex(
93
+ decision: SourcesDecision,
94
+ ): PackIndex | undefined {
95
+ switch (decision.kind) {
96
+ case "pack":
97
+ return readIndexFile(decision.dir);
98
+ case "embedded":
99
+ return parseIndex(EMBEDDED_INDEX_JSON);
100
+ case "unavailable":
101
+ return undefined;
102
+ }
98
103
  }
99
104
 
100
105
  /**
@@ -120,31 +125,37 @@ export function entityTotal(index: PackIndex): number {
120
125
  return subjects.size;
121
126
  }
122
127
 
123
- /** Load the active pack's index: the locked pack, else the embedded fallback. */
124
- function loadActiveIndex(cwd: string): PackIndex | undefined {
128
+ /** Parse an index JSON string; any malformed input degrades to `undefined`. */
129
+ function parseIndex(json: string): PackIndex | undefined {
125
130
  try {
126
- const path = lockPath(cwd);
127
- if (existsSync(path)) {
128
- const lock = JSON.parse(readFileSync(path, "utf-8")) as {
129
- contentHash?: unknown;
130
- };
131
- if (typeof lock.contentHash === "string") {
132
- const indexPath = join(packDir(lock.contentHash), INDEX_FILE);
133
- if (existsSync(indexPath)) {
134
- return JSON.parse(readFileSync(indexPath, "utf-8")) as PackIndex;
135
- }
136
- }
137
- }
131
+ return JSON.parse(json) as PackIndex;
138
132
  } catch {
139
- // Fall through to the embedded fallback on any read/parse failure.
133
+ return undefined;
140
134
  }
135
+ }
136
+
137
+ /** Read a pack directory's `index.json`; any I/O or parse error is `undefined`. */
138
+ function readIndexFile(dir: string): PackIndex | undefined {
141
139
  try {
142
- return JSON.parse(EMBEDDED_INDEX_JSON) as PackIndex;
140
+ return parseIndex(readFileSync(join(dir, INDEX_FILE), "utf-8"));
143
141
  } catch {
144
142
  return undefined;
145
143
  }
146
144
  }
147
145
 
146
+ /**
147
+ * The fast path's index — the POINTER half of the decision table (see the module
148
+ * docblock). A project with a pointer reads that pack and nothing else: a
149
+ * pointer whose pack the cache lost yields no candidates rather than the
150
+ * snapshot's, which is what the boot decision does with the same state.
151
+ */
152
+ function loadActiveIndex(cwd: string): PackIndex | undefined {
153
+ const contentHash = readActivePack(cwd);
154
+ return contentHash === undefined
155
+ ? parseIndex(EMBEDDED_INDEX_JSON)
156
+ : readIndexFile(packDir(contentHash));
157
+ }
158
+
148
159
  /** Whether an entity matches a prefixed type filter (primary type or any type). */
149
160
  function matchesType(entity: PackIndexEntity, type: string): boolean {
150
161
  if (!type) return true;
@@ -180,42 +191,45 @@ export function createIndexEntityReader(
180
191
  };
181
192
  }
182
193
 
183
- /** All names of a prefixed type in the index (empty type = any), sorted. */
184
- function indexNames(index: PackIndex | undefined, type: string): string[] {
185
- if (!index) return [];
186
- const names = new Set<string>();
187
- for (const entity of index.entities) {
188
- if (matchesType(entity, type)) names.add(entity.name);
189
- }
190
- return [...names].sort();
191
- }
192
-
193
- /** The `ds:Prompt` entities' completable names (`label || name`), sorted. */
194
- function promptNames(index: PackIndex | undefined): string[] {
195
- if (!index) return [];
196
- const names = new Set<string>();
197
- for (const entity of index.entities) {
198
- if (matchesType(entity, PROMPT_TYPE))
199
- names.add(entity.label || entity.name);
194
+ /**
195
+ * One entity's completable tokens for a declared field, with NO fallback.
196
+ *
197
+ * A verb declares the field because that is the one its lookup matches on, so
198
+ * an entity the pack did not enrich contributes nothing. Standing a label in
199
+ * for an absent alt name would offer a token the lookup provably cannot resolve
200
+ * — a completion that hands the user a value and then refuses it — which is the
201
+ * complete-but-cannot-resolve split this whole declaration exists to close.
202
+ * Offering nothing for an under-enriched entity is the honest answer.
203
+ *
204
+ * @param entity - One index entity.
205
+ * @param field - The declared field to read.
206
+ * @returns The tokens that entity contributes; empty when it carries none.
207
+ */
208
+ function readEntityNames(
209
+ entity: PackIndexEntity,
210
+ field: CompletionField,
211
+ ): readonly string[] {
212
+ switch (field) {
213
+ case "name":
214
+ return [entity.name];
215
+ case "label":
216
+ return entity.label ? [entity.label] : [];
217
+ case "altNames":
218
+ return entity.altNames ?? [];
200
219
  }
201
- return [...names].sort();
202
220
  }
203
221
 
204
- /**
205
- * The `ds:Tier` entities' completable names, sorted. `tier lookup` matches
206
- * `ds:name`, so when the index carries it (projected by `buildIndex` into
207
- * `altNames`) those are emitted exactly; a pack whose tiers were indexed without
208
- * `ds:name` falls back to `label ?? name` (empty rather than wrong).
209
- */
210
- function tierNames(index: PackIndex | undefined): string[] {
222
+ /** A ref's candidates: entities of its type, read from its field, sorted. */
223
+ function indexNames(
224
+ index: PackIndex | undefined,
225
+ ref: CompletionSourceRef,
226
+ ): string[] {
211
227
  if (!index) return [];
212
228
  const names = new Set<string>();
213
229
  for (const entity of index.entities) {
214
- if (!matchesType(entity, TIER_TYPE)) continue;
215
- if (entity.altNames && entity.altNames.length > 0) {
216
- for (const alt of entity.altNames) names.add(alt);
217
- } else {
218
- names.add(entity.label || entity.name);
230
+ if (!matchesType(entity, ref.type ?? "")) continue;
231
+ for (const name of readEntityNames(entity, ref.field ?? "name")) {
232
+ names.add(name);
219
233
  }
220
234
  }
221
235
  return [...names].sort();
@@ -235,8 +249,8 @@ function prefixNames(index: PackIndex | undefined): string[] {
235
249
  * reader every name family shares. Wired at the `__complete` fast path (bin) and
236
250
  * the `__complete` verb; storeless throughout (see the module docblock).
237
251
  *
238
- * The pack index is loaded once, lazily, and reused across the index/prompts/
239
- * tiers/prefixes sources; the skills filesystem walk is memoized to one walk per
252
+ * The pack index is loaded once, lazily, and reused across the index and
253
+ * prefixes sources; the skills filesystem walk is memoized to one walk per
240
254
  * process (the design's perf caveat). Every source returns the FULL candidate
241
255
  * list in canonical casing — the resolver ranks/filters against the partial.
242
256
  *
@@ -262,13 +276,9 @@ export function indexCompletionEnv(cwd: string): CompletionEnv {
262
276
  names(ref: CompletionSourceRef): readonly string[] {
263
277
  switch (ref.from) {
264
278
  case "index":
265
- return indexNames(getIndex(), ref.type ?? "");
279
+ return indexNames(getIndex(), ref);
266
280
  case "skills":
267
281
  return getSkills();
268
- case "prompts":
269
- return promptNames(getIndex());
270
- case "tiers":
271
- return tierNames(getIndex());
272
282
  case "prefixes":
273
283
  return prefixNames(getIndex());
274
284
  }
@@ -236,7 +236,7 @@ describe("buildCompletionModel — flags and sources", () => {
236
236
  positional: true,
237
237
  complete: {
238
238
  kind: "names",
239
- source: { from: "tiers" },
239
+ source: { from: "index", type: "ex:Tier", field: "altNames" },
240
240
  enabled: false,
241
241
  },
242
242
  },