@canonical/pragma-cli 0.32.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (295) hide show
  1. package/README.md +72 -0
  2. package/dist/pragma +0 -0
  3. package/package.json +19 -15
  4. package/pragma.conf.ts +664 -0
  5. package/src/bin.ts +15 -6
  6. package/src/capabilities/block.parity.test.ts +250 -0
  7. package/src/capabilities/capabilities/capabilities.render.ts +3 -2
  8. package/src/capabilities/capabilities/capabilities.test.ts +10 -1
  9. package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
  10. package/src/capabilities/capabilities/catalog.ts +20 -8
  11. package/src/capabilities/capabilities/hints.ts +21 -36
  12. package/src/capabilities/capabilities/types.ts +2 -0
  13. package/src/capabilities/colophon/collectColophon.ts +34 -19
  14. package/src/capabilities/colophon/colophon.render.test.ts +31 -4
  15. package/src/capabilities/colophon/colophon.render.ts +13 -4
  16. package/src/capabilities/colophon/colophon.test.ts +3 -3
  17. package/src/capabilities/colophon/colophon.verb.ts +11 -4
  18. package/src/capabilities/colophon/markdownTerminal.ts +33 -14
  19. package/src/capabilities/colophon/types.ts +16 -7
  20. package/src/capabilities/config/collectConfigShow.test.ts +57 -0
  21. package/src/capabilities/config/collectConfigShow.ts +41 -4
  22. package/src/capabilities/config/fields.ts +10 -108
  23. package/src/capabilities/config/set.test.ts +6 -6
  24. package/src/capabilities/config/set.verb.ts +12 -8
  25. package/src/capabilities/config/show.render.test.ts +137 -0
  26. package/src/capabilities/config/show.render.ts +57 -16
  27. package/src/capabilities/config/show.verb.ts +9 -8
  28. package/src/capabilities/config/types.ts +8 -0
  29. package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
  30. package/src/capabilities/create/byteEquality.test.ts +82 -101
  31. package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
  32. package/src/capabilities/create/constants.ts +75 -0
  33. package/src/capabilities/create/create.mcp.test.ts +2 -2
  34. package/src/capabilities/create/create.test.ts +192 -16
  35. package/src/capabilities/create/create.verb.ts +208 -79
  36. package/src/capabilities/create/index.ts +2 -10
  37. package/src/capabilities/create/pickGenerator.ts +34 -28
  38. package/src/capabilities/create/templates.embedded.generated.ts +4 -29
  39. package/src/capabilities/create/types.ts +10 -6
  40. package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
  41. package/src/capabilities/distribution.test.ts +120 -0
  42. package/src/capabilities/distribution.ts +59 -0
  43. package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
  44. package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
  45. package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
  46. package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
  47. package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
  48. package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
  49. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
  50. package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
  51. package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
  52. package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
  53. package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
  54. package/src/capabilities/doctor/doctor.render.test.ts +170 -0
  55. package/src/capabilities/doctor/doctor.render.ts +117 -34
  56. package/src/capabilities/doctor/doctor.test.ts +68 -9
  57. package/src/capabilities/doctor/doctor.verb.ts +6 -2
  58. package/src/capabilities/doctor/runChecks.ts +33 -9
  59. package/src/capabilities/doctor/types.ts +16 -1
  60. package/src/capabilities/emptyRecovery.test.ts +32 -15
  61. package/src/capabilities/graph/inspect.verb.ts +3 -2
  62. package/src/capabilities/graph/query.verb.ts +3 -2
  63. package/src/capabilities/graph/runQuery.ts +8 -5
  64. package/src/capabilities/index.ts +22 -18
  65. package/src/capabilities/info/collectInfo.ts +10 -6
  66. package/src/capabilities/info/info.render.ts +3 -2
  67. package/src/capabilities/info/info.verb.ts +3 -2
  68. package/src/capabilities/lazy.test.ts +93 -2
  69. package/src/capabilities/ontology/index.ts +10 -3
  70. package/src/capabilities/ontology/ontology.test.ts +157 -5
  71. package/src/capabilities/ontology/verbs.ts +203 -104
  72. package/src/capabilities/prompt/prompt.render.test.ts +94 -0
  73. package/src/capabilities/prompt/prompt.render.ts +23 -6
  74. package/src/capabilities/prompt/prompt.test.ts +68 -1
  75. package/src/capabilities/prompt/verbs.ts +24 -11
  76. package/src/capabilities/reference.test.ts +32 -0
  77. package/src/capabilities/resources/provider.ts +58 -25
  78. package/src/capabilities/resources/resources.test.ts +176 -21
  79. package/src/capabilities/scopeTypeSync.test.ts +37 -0
  80. package/src/capabilities/setup/index.ts +7 -1
  81. package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
  82. package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
  83. package/src/capabilities/setup/operations/setupLsp.ts +56 -12
  84. package/src/capabilities/setup/operations/setupMcp.ts +216 -39
  85. package/src/capabilities/setup/operations/setupSkills.ts +10 -4
  86. package/src/capabilities/setup/setup.render.test.ts +109 -0
  87. package/src/capabilities/setup/setup.render.ts +61 -14
  88. package/src/capabilities/setup/setup.test.ts +352 -1
  89. package/src/capabilities/setup/setup.verb.ts +68 -9
  90. package/src/capabilities/setup/shell.ts +29 -3
  91. package/src/capabilities/setup/types.ts +59 -2
  92. package/src/capabilities/shared/bands.ts +16 -0
  93. package/src/capabilities/skill/discover.ts +11 -5
  94. package/src/capabilities/skill/render.ts +4 -5
  95. package/src/capabilities/skill/verbs.ts +7 -7
  96. package/src/capabilities/sources/collectStatus.ts +69 -49
  97. package/src/capabilities/sources/index.ts +1 -1
  98. package/src/capabilities/sources/installSkills.ts +2 -2
  99. package/src/capabilities/sources/runUpdate.ts +146 -98
  100. package/src/capabilities/sources/sources.test.ts +264 -84
  101. package/src/capabilities/sources/status.render.test.ts +94 -0
  102. package/src/capabilities/sources/status.render.ts +55 -33
  103. package/src/capabilities/sources/status.verb.ts +15 -8
  104. package/src/capabilities/sources/types.ts +18 -25
  105. package/src/capabilities/sources/update.render.ts +9 -2
  106. package/src/capabilities/sources/update.verb.ts +12 -23
  107. package/src/capabilities/surface.test.ts +65 -17
  108. package/src/capabilities/tier.parity.test.ts +144 -0
  109. package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
  110. package/src/constants.test.ts +9 -7
  111. package/src/constants.ts +49 -23
  112. package/src/identity.test.ts +333 -0
  113. package/src/index.ts +2 -2
  114. package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
  115. package/src/kernel/completion/complete.test.ts +3 -5
  116. package/src/kernel/completion/emitScripts.test.ts +1 -1
  117. package/src/kernel/completion/emitScripts.ts +4 -2
  118. package/src/kernel/completion/entitySource.test.ts +185 -41
  119. package/src/kernel/completion/entitySource.ts +131 -113
  120. package/src/kernel/completion/model.test.ts +3 -3
  121. package/src/kernel/completion/model.ts +11 -1
  122. package/src/kernel/completion/parse.test.ts +2 -4
  123. package/src/kernel/completion/safety.test.ts +82 -36
  124. package/src/kernel/completion/shellDrive.test.ts +592 -0
  125. package/src/kernel/completion/templates/fish.ts +28 -6
  126. package/src/kernel/completion/types.ts +3 -3
  127. package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
  128. package/src/kernel/config/defaults.test.ts +85 -0
  129. package/src/kernel/config/defaults.ts +12 -23
  130. package/src/kernel/config/evaluateProjectConfig.ts +2 -3
  131. package/src/kernel/config/findProjectConfig.ts +4 -3
  132. package/src/kernel/config/firstRun.ts +12 -5
  133. package/src/kernel/config/globalConfig.ts +2 -2
  134. package/src/kernel/config/index.ts +4 -5
  135. package/src/kernel/config/paths.ts +17 -17
  136. package/src/kernel/config/readConfig.test.ts +242 -6
  137. package/src/kernel/config/readConfig.ts +8 -8
  138. package/src/kernel/config/schema.test.ts +50 -0
  139. package/src/kernel/config/schema.ts +97 -10
  140. package/src/kernel/config/types.ts +73 -19
  141. package/src/kernel/copy.test.ts +453 -0
  142. package/src/kernel/error/PragmaError.test.ts +5 -5
  143. package/src/kernel/error/constants.ts +3 -2
  144. package/src/kernel/error/index.ts +1 -1
  145. package/src/kernel/error/recovery.test.ts +22 -45
  146. package/src/kernel/error/recovery.ts +22 -27
  147. package/src/kernel/error/renderError.ts +1 -1
  148. package/src/kernel/error/types.ts +5 -4
  149. package/src/kernel/packs/collect.test.ts +195 -11
  150. package/src/kernel/packs/collect.ts +213 -58
  151. package/src/kernel/packs/compile.test.ts +163 -7
  152. package/src/kernel/packs/compile.ts +15 -12
  153. package/src/kernel/packs/disclosure.test.ts +5 -2
  154. package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
  155. package/src/kernel/packs/graphqlSource.test.ts +6 -3
  156. package/src/kernel/packs/iri.ts +5 -7
  157. package/src/kernel/packs/renderPack.ts +5 -3
  158. package/src/kernel/packs/resolveEntity.ts +21 -17
  159. package/src/kernel/packs/runBodies.ts +15 -14
  160. package/src/kernel/packs/schema.ts +96 -10
  161. package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
  162. package/src/kernel/packs/sparql/runSelect.ts +73 -34
  163. package/src/kernel/packs/toolDescription.test.ts +10 -2
  164. package/src/kernel/packs/types.ts +39 -4
  165. package/src/kernel/packs/uniqueness.ts +4 -4
  166. package/src/kernel/project/cli/buildProgram.test.ts +60 -11
  167. package/src/kernel/project/cli/buildProgram.ts +11 -0
  168. package/src/kernel/project/cli/dispatch.test.ts +67 -0
  169. package/src/kernel/project/cli/dispatch.ts +34 -12
  170. package/src/kernel/project/cli/globalFlags.test.ts +12 -4
  171. package/src/kernel/project/cli/globalFlags.ts +14 -14
  172. package/src/kernel/project/cli/helpFormat.ts +1 -1
  173. package/src/kernel/project/cli/rootHelp.ts +109 -107
  174. package/src/kernel/project/mcp/buildServer.ts +11 -6
  175. package/src/kernel/project/mcp/instructions.ts +12 -7
  176. package/src/kernel/project/mcp/mcpError.ts +52 -0
  177. package/src/kernel/project/mcp/parity.test.ts +39 -1
  178. package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
  179. package/src/kernel/project/mcp/prompts/provider.ts +51 -7
  180. package/src/kernel/project/mcp/prompts/source.ts +68 -67
  181. package/src/kernel/project/mcp/registerVerb.ts +25 -6
  182. package/src/kernel/project/mcp/serve.ts +11 -3
  183. package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
  184. package/src/kernel/render/disclosure.ts +4 -2
  185. package/src/kernel/render/formatters.ts +5 -4
  186. package/src/kernel/render/prefixes.test.ts +36 -0
  187. package/src/kernel/render/prefixes.ts +40 -13
  188. package/src/kernel/render/renderers.test.ts +35 -1
  189. package/src/kernel/render/renderers.ts +25 -7
  190. package/src/kernel/render/style.test.ts +31 -0
  191. package/src/kernel/render/style.ts +74 -0
  192. package/src/kernel/runtime/graphpack/build.ts +57 -3
  193. package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
  194. package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
  195. package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
  196. package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
  197. package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
  198. package/src/kernel/runtime/graphpack/embedded.ts +37 -11
  199. package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
  200. package/src/kernel/runtime/graphpack/hash.ts +3 -2
  201. package/src/kernel/runtime/graphpack/index.ts +1 -1
  202. package/src/kernel/runtime/graphpack/manifest.ts +114 -12
  203. package/src/kernel/runtime/graphpack/read.ts +32 -22
  204. package/src/kernel/runtime/graphpack/schemas.ts +74 -0
  205. package/src/kernel/runtime/graphpack/stories.ts +84 -0
  206. package/src/kernel/runtime/graphpack/types.ts +72 -45
  207. package/src/kernel/runtime/loadSession.ts +4 -12
  208. package/src/kernel/runtime/paths.ts +64 -15
  209. package/src/kernel/runtime/probe.test.ts +8 -4
  210. package/src/kernel/runtime/readEntity.ts +6 -5
  211. package/src/kernel/runtime/refs/gitOps.ts +1 -19
  212. package/src/kernel/runtime/refs/parseRef.ts +7 -7
  213. package/src/kernel/runtime/refs/resolve.ts +141 -65
  214. package/src/kernel/runtime/resolveSources.test.ts +63 -24
  215. package/src/kernel/runtime/resolveSources.ts +36 -25
  216. package/src/kernel/runtime/storeReadiness.ts +62 -0
  217. package/src/kernel/runtime/storeless.test.ts +1 -1
  218. package/src/kernel/runtime/types.ts +1 -1
  219. package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
  220. package/src/kernel/spec/emitReference.test.ts +196 -0
  221. package/src/kernel/spec/emitReference.ts +624 -0
  222. package/src/kernel/spec/emitSurface.ts +13 -7
  223. package/src/kernel/spec/index.ts +2 -0
  224. package/src/kernel/spec/surfaceConformance.test.ts +2 -2
  225. package/src/kernel/spec/surfaceConformance.ts +8 -0
  226. package/src/kernel/spec/types.ts +29 -9
  227. package/src/kernel/spec/validate.ts +2 -1
  228. package/src/kernel/vocabulary.test.ts +164 -0
  229. package/src/kernel/vocabulary.ts +146 -0
  230. package/src/testing/PARITY_GAPS.ts +9 -3
  231. package/src/testing/behavioral/README.md +3 -3
  232. package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
  233. package/src/testing/behavioral/completion.test.ts +21 -8
  234. package/src/testing/behavioral/disclosure.test.ts +12 -0
  235. package/src/testing/behavioral/docExamples.test.ts +283 -0
  236. package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
  237. package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
  238. package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
  239. package/src/testing/behavioral/harness.foundation.test.ts +14 -5
  240. package/src/testing/behavioral/journeys.cli.test.ts +116 -73
  241. package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
  242. package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
  243. package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
  244. package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
  245. package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
  246. package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
  247. package/src/testing/behavioral/liveReadSurface.ts +4 -3
  248. package/src/testing/behavioral/mcpSurface.test.ts +10 -5
  249. package/src/testing/behavioral/staleCommands.test.ts +86 -0
  250. package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
  251. package/src/testing/eval/cases/assembly.ts +16 -9
  252. package/src/testing/eval/cases/effectful.ts +31 -16
  253. package/src/testing/eval/cases/readNouns.ts +23 -18
  254. package/src/testing/eval/cases/stable.ts +5 -1
  255. package/src/testing/eval/eval.test.ts +7 -1
  256. package/src/testing/fixtures/fixtureCapability.ts +201 -1
  257. package/src/testing/fixtures/graph/canonical.ts +6 -3
  258. package/src/testing/fixtures/graph/defaultPack.ts +179 -0
  259. package/src/testing/fixtures/graph/owlExport.ts +97 -0
  260. package/src/testing/helpers/extractSnippets.ts +29 -0
  261. package/src/testing/helpers/fixtureGraph.ts +33 -16
  262. package/src/testing/helpers/golden.ts +1 -1
  263. package/src/testing/helpers/packRuntime.ts +2 -3
  264. package/src/testing/helpers/projectCli.ts +2 -1
  265. package/src/testing/helpers/projectMcp.ts +6 -0
  266. package/src/testing/perf/budgets.test.ts +4 -0
  267. package/src/testing/perf/budgets.ts +22 -7
  268. package/src/testing/perf/globalSetup.ts +121 -6
  269. package/src/types.ts +2 -2
  270. package/src/capabilities/block/blockList.verb.ts +0 -150
  271. package/src/capabilities/block/index.ts +0 -25
  272. package/src/capabilities/block/pack.ts +0 -155
  273. package/src/capabilities/block/parity.test.ts +0 -180
  274. package/src/capabilities/block/tierChain.ts +0 -78
  275. package/src/capabilities/colophon/pragmaColophon.ts +0 -66
  276. package/src/capabilities/config/field.test.ts +0 -188
  277. package/src/capabilities/modifier/index.ts +0 -15
  278. package/src/capabilities/modifier/pack.ts +0 -64
  279. package/src/capabilities/standard/index.ts +0 -18
  280. package/src/capabilities/standard/pack.ts +0 -128
  281. package/src/capabilities/tier/index.ts +0 -19
  282. package/src/capabilities/tier/lookup.render.ts +0 -34
  283. package/src/capabilities/tier/lookup.test.ts +0 -68
  284. package/src/capabilities/tier/lookup.verb.ts +0 -44
  285. package/src/capabilities/tier/pack.ts +0 -28
  286. package/src/capabilities/tier/runLookup.ts +0 -59
  287. package/src/capabilities/token/addConfig.render.ts +0 -27
  288. package/src/capabilities/token/addConfig.test.ts +0 -62
  289. package/src/capabilities/token/addConfig.verb.ts +0 -40
  290. package/src/capabilities/token/index.ts +0 -19
  291. package/src/capabilities/token/pack.ts +0 -58
  292. package/src/capabilities/token/runAddConfig.ts +0 -95
  293. package/src/kernel/completion/bashDrive.test.ts +0 -73
  294. package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
  295. package/src/kernel/runtime/lock.ts +0 -90
@@ -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,91 +65,97 @@ 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
  /**
101
- * Sum a pack index's per-type instance counts — the "total entities" figure
106
+ * Count a pack index's DISTINCT abox subjects — the "total entities" figure
102
107
  * `info` and `doctor` report. Works over any {@link PackIndex}, whether read
103
108
  * storelessly via {@link readPackIndex} or taken from a booted store session.
104
109
  *
110
+ * NOT a sum of `instanceCountByType`: on a real OWL/Protégé export that raw
111
+ * multiset double-counts — every individual is typed as both its domain class
112
+ * AND `owl:NamedIndividual`, and the `owl:Class`/property meta-buckets pile on
113
+ * top — so the total ran ~2× the real entity count (A1). Counting distinct
114
+ * abox subjects (the individuals, each once) is the figure users expect; the
115
+ * tbox schema classes/properties are not "entities" in this count.
116
+ *
105
117
  * @param index - A pack index.
106
- * @returns The total number of indexed entity instances across every type.
118
+ * @returns The number of distinct abox subjects indexed.
107
119
  */
108
120
  export function entityTotal(index: PackIndex): number {
109
- return Object.values(index.instanceCountByType).reduce(
110
- (sum, n) => sum + n,
111
- 0,
112
- );
121
+ const subjects = new Set<string>();
122
+ for (const entity of index.entities) {
123
+ if (entity.box === "abox") subjects.add(entity.uri ?? entity.name);
124
+ }
125
+ return subjects.size;
113
126
  }
114
127
 
115
- /** Load the active pack's index: the locked pack, else the embedded fallback. */
116
- 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 {
117
130
  try {
118
- const path = lockPath(cwd);
119
- if (existsSync(path)) {
120
- const lock = JSON.parse(readFileSync(path, "utf-8")) as {
121
- contentHash?: unknown;
122
- };
123
- if (typeof lock.contentHash === "string") {
124
- const indexPath = join(packDir(lock.contentHash), INDEX_FILE);
125
- if (existsSync(indexPath)) {
126
- return JSON.parse(readFileSync(indexPath, "utf-8")) as PackIndex;
127
- }
128
- }
129
- }
131
+ return JSON.parse(json) as PackIndex;
130
132
  } catch {
131
- // Fall through to the embedded fallback on any read/parse failure.
133
+ return undefined;
132
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 {
133
139
  try {
134
- return JSON.parse(EMBEDDED_INDEX_JSON) as PackIndex;
140
+ return parseIndex(readFileSync(join(dir, INDEX_FILE), "utf-8"));
135
141
  } catch {
136
142
  return undefined;
137
143
  }
138
144
  }
139
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
+
140
159
  /** Whether an entity matches a prefixed type filter (primary type or any type). */
141
160
  function matchesType(entity: PackIndexEntity, type: string): boolean {
142
161
  if (!type) return true;
@@ -172,42 +191,45 @@ export function createIndexEntityReader(
172
191
  };
173
192
  }
174
193
 
175
- /** All names of a prefixed type in the index (empty type = any), sorted. */
176
- function indexNames(index: PackIndex | undefined, type: string): string[] {
177
- if (!index) return [];
178
- const names = new Set<string>();
179
- for (const entity of index.entities) {
180
- if (matchesType(entity, type)) names.add(entity.name);
181
- }
182
- return [...names].sort();
183
- }
184
-
185
- /** The `ds:Prompt` entities' completable names (`label || name`), sorted. */
186
- function promptNames(index: PackIndex | undefined): string[] {
187
- if (!index) return [];
188
- const names = new Set<string>();
189
- for (const entity of index.entities) {
190
- if (matchesType(entity, PROMPT_TYPE))
191
- 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 ?? [];
192
219
  }
193
- return [...names].sort();
194
220
  }
195
221
 
196
- /**
197
- * The `ds:Tier` entities' completable names, sorted. `tier lookup` matches
198
- * `ds:name`, so when the index carries it (projected by `buildIndex` into
199
- * `altNames`) those are emitted exactly; a pack whose tiers were indexed without
200
- * `ds:name` falls back to `label ?? name` (empty rather than wrong).
201
- */
202
- 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[] {
203
227
  if (!index) return [];
204
228
  const names = new Set<string>();
205
229
  for (const entity of index.entities) {
206
- if (!matchesType(entity, TIER_TYPE)) continue;
207
- if (entity.altNames && entity.altNames.length > 0) {
208
- for (const alt of entity.altNames) names.add(alt);
209
- } else {
210
- 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);
211
233
  }
212
234
  }
213
235
  return [...names].sort();
@@ -227,8 +249,8 @@ function prefixNames(index: PackIndex | undefined): string[] {
227
249
  * reader every name family shares. Wired at the `__complete` fast path (bin) and
228
250
  * the `__complete` verb; storeless throughout (see the module docblock).
229
251
  *
230
- * The pack index is loaded once, lazily, and reused across the index/prompts/
231
- * 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
232
254
  * process (the design's perf caveat). Every source returns the FULL candidate
233
255
  * list in canonical casing — the resolver ranks/filters against the partial.
234
256
  *
@@ -254,13 +276,9 @@ export function indexCompletionEnv(cwd: string): CompletionEnv {
254
276
  names(ref: CompletionSourceRef): readonly string[] {
255
277
  switch (ref.from) {
256
278
  case "index":
257
- return indexNames(getIndex(), ref.type ?? "");
279
+ return indexNames(getIndex(), ref);
258
280
  case "skills":
259
281
  return getSkills();
260
- case "prompts":
261
- return promptNames(getIndex());
262
- case "tiers":
263
- return tierNames(getIndex());
264
282
  case "prefixes":
265
283
  return prefixNames(getIndex());
266
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
  },
@@ -264,13 +264,13 @@ describe("buildCompletionModel — flags and sources", () => {
264
264
  const flags = Object.fromEntries(model.globalFlags.map((f) => [f.flag, f]));
265
265
  expect(flags["--format"]?.source).toEqual({
266
266
  kind: "values",
267
- values: ["plain", "json"],
267
+ values: ["plain", "llm", "json"],
268
268
  });
269
269
  expect(flags["--detail"]?.source).toEqual({
270
270
  kind: "values",
271
271
  values: ["summary", "standard", "detailed"],
272
272
  });
273
- expect(flags["--llm"]?.takesValue).toBe(false);
273
+ expect(flags["--llm"]).toBeUndefined();
274
274
  expect(flags["--version"]?.rootOnly).toBe(true);
275
275
  expect(flags["--help"]?.rootOnly).toBeUndefined();
276
276
  });
@@ -40,6 +40,17 @@ import type {
40
40
  * The allowlist every inlined completion token must match: an alphanumeric
41
41
  * head, then word/URI-ish characters. No whitespace, quotes, `$`, backticks,
42
42
  * semicolons, or globs — nothing a shell could expand or split.
43
+ *
44
+ * KNOWN GAP, deliberately not closed: it admits shell RESERVED WORDS — `esac`,
45
+ * `in`, `do`, `done`, `fi`. A noun so named emits a `case` arm bash and zsh both
46
+ * refuse to parse, so `_pragma` is never defined and the user gets NO completion
47
+ * at all rather than a wrong candidate (`fish -n` does not catch it either). The
48
+ * gap is owned by the syntax gates, not by this regex: `shellDrive.test.ts`
49
+ * runs `bash -n`/`zsh -n` over the emitted scripts, so the day a live noun is
50
+ * named `done` the build fails loudly and names the file. A denylist here would
51
+ * be new production code guarding a case nothing in the tree exhibits — and a
52
+ * partial one (reserved words differ across families and versions) reads as
53
+ * more protection than it gives.
43
54
  */
44
55
  export const SAFE_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9@/:._+-]*$/;
45
56
 
@@ -61,7 +72,6 @@ export function assertSafeToken(token: string, where: string): void {
61
72
 
62
73
  /** The global flags completion offers, mirroring the surface's globalFlags. */
63
74
  const GLOBAL_FLAGS: readonly FlagEntry[] = [
64
- { flag: "--llm", takesValue: false, repeatable: false, source: none() },
65
75
  {
66
76
  flag: "--format",
67
77
  takesValue: true,
@@ -73,7 +73,7 @@ describe("parseWords — nouns and verbs (PROTECTED)", () => {
73
73
  context: { kind: "noun" },
74
74
  partial: "blo",
75
75
  });
76
- expect(parse(["--llm", "block", ""]).context).toEqual({
76
+ expect(parse(["--verbose", "block", ""]).context).toEqual({
77
77
  kind: "verb",
78
78
  noun: "block",
79
79
  });
@@ -83,7 +83,6 @@ describe("parseWords — nouns and verbs (PROTECTED)", () => {
83
83
  describe("parseWords — flag names (PROTECTED)", () => {
84
84
  it("offers only globals (with --version) at the root", () => {
85
85
  expect(flagsOf(parse(["--"]).context)).toEqual([
86
- "--llm",
87
86
  "--format",
88
87
  "--verbose",
89
88
  "--detail",
@@ -105,7 +104,6 @@ describe("parseWords — flag names (PROTECTED)", () => {
105
104
  "--out",
106
105
  "--with-meta",
107
106
  "--note",
108
- "--llm",
109
107
  "--format",
110
108
  "--verbose",
111
109
  "--detail",
@@ -192,7 +190,7 @@ describe("parseWords — flag values (PROTECTED)", () => {
192
190
  context: {
193
191
  kind: "flag-value",
194
192
  flag: "--format",
195
- source: { kind: "values", values: ["plain", "json"] },
193
+ source: { kind: "values", values: ["plain", "llm", "json"] },
196
194
  },
197
195
  });
198
196
  expect(parse(["block", "get", "--detail", "s"])).toMatchObject({
@@ -1,7 +1,7 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { existsSync, mkdtempSync, readdirSync, readFileSync } from "node:fs";
3
3
  import { tmpdir } from "node:os";
4
- import { dirname, join, resolve } from "node:path";
4
+ import { dirname, join, relative, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  import { createStore } from "@canonical/ke";
7
7
  import { describe, expect, it, vi } from "vitest";
@@ -207,6 +207,8 @@ function staticImportGraph(
207
207
  }
208
208
 
209
209
  const here = dirname(fileURLToPath(import.meta.url));
210
+ /** The package root, so a failure names `src/…` instead of an absolute path. */
211
+ const pkgRoot = resolve(here, "../../..");
210
212
  const has = (graph: Set<string>, fragment: string): boolean =>
211
213
  [...graph].some((file) => file.includes(fragment));
212
214
 
@@ -224,16 +226,25 @@ describe("storeless guarantee (PROTECTED)", () => {
224
226
  // Runtime boot (bootRuntime) — never on the completion path.
225
227
  expect(has(graph, "kernel/runtime/boot.ts")).toBe(false);
226
228
  // The config LAYER (reader, evaluator, paths, schema) — storeless means
227
- // no read. PR2's pure-leaf `config/types.ts` (the ConfigLayers type,
228
- // imported type-only via VerbSpec.run's PragmaRuntime and erased at
229
- // runtime) is the lone tolerated seam — as spec/types.ts and
230
- // runtime/types.ts already sit in this graph as type-only.
231
- const configModules = [...graph].filter(
232
- (file) =>
233
- file.includes("kernel/config/") &&
234
- !file.endsWith("kernel/config/types.ts"),
235
- );
236
- expect(configModules).toEqual([]);
229
+ // no read. Its tolerated seams are POSITIVE-LISTED, not filtered out by a
230
+ // path fragment: the distribution config sits at the package root, so a
231
+ // `kernel/config/`-shaped filter would not see it and this guard would
232
+ // keep passing after it stopped being true.
233
+ // - `kernel/config/types.ts` — the ConfigLayers type, imported type-only
234
+ // via VerbSpec.run's PragmaRuntime and erased at runtime, as
235
+ // spec/types.ts and runtime/types.ts already are in this graph.
236
+ // - `pragma.conf.ts` — the distribution config, statically imported by
237
+ // `constants.ts` (identity), `render/prefixes.ts` (the domain
238
+ // namespaces) and `kernel/vocabulary.ts` (the domain terms). Safe only
239
+ // while it stays inert data; `capabilities/lazy.test.ts` pins that.
240
+ const configModules = [...graph]
241
+ .filter((file) => /config|\.conf\./.test(file))
242
+ .map((file) => relative(pkgRoot, file))
243
+ .sort();
244
+ expect(configModules).toEqual([
245
+ "pragma.conf.ts",
246
+ "src/kernel/config/types.ts",
247
+ ]);
237
248
  // zod stays off the fast path (validate.ts is the registration seam).
238
249
  expect(has(graph, "kernel/spec/validate.ts")).toBe(false);
239
250
  // No store/graph modules, present or future.
@@ -266,7 +277,7 @@ describe("storeless guarantee (PROTECTED)", () => {
266
277
  required: true,
267
278
  complete: {
268
279
  kind: "names",
269
- source: { from: "index", type: "ex:Component" },
280
+ source: { from: "index", type: "ds:Component" },
270
281
  },
271
282
  },
272
283
  ],
@@ -286,23 +297,23 @@ describe("storeless guarantee (PROTECTED)", () => {
286
297
  } as VerbSpec,
287
298
  ],
288
299
  };
289
- // Fresh cwd → no lock → the reader falls back to the embedded pack index.
300
+ // Fresh cwd → no pointer → the reader falls back to the embedded index.
290
301
  const cwd = mkdtempSync(join(tmpdir(), "pragma-storespy-"));
291
302
  const matches = await runComplete(
292
- ["block", "lookup", "ex:B"],
303
+ ["block", "lookup", "ds:global.component.but"],
293
304
  [lookupModule],
294
305
  indexCompletionEnv(cwd),
295
306
  );
296
307
  // It DID read the index (a real name came back)…
297
- expect(matches).toEqual(["ex:Button"]);
308
+ expect(matches).toContain("ds:global.component.button");
298
309
  // …and it did so without ever constructing the store.
299
310
  expect(vi.mocked(createStore)).not.toHaveBeenCalled();
300
311
  });
301
312
 
302
- it("the non-index name sources resolve without constructing the store", async () => {
303
- // skills (filesystem), prompts/tiers (index filter), prefixes (index ∪
304
- // default map) — every family completes storelessly against the live
305
- // capabilities. A fresh cwd → no lock → the embedded fallback index.
313
+ it("every declared name source resolves without constructing the store", async () => {
314
+ // skills (filesystem), prefixes (index ∪ default map) and the index's own
315
+ // `label`/`altNames` fields — every family completes storelessly against
316
+ // the live capabilities. A fresh cwd → no pointer → the embedded index.
306
317
  const cwd = mkdtempSync(join(tmpdir(), "pragma-storeless-src-"));
307
318
  const env = indexCompletionEnv(cwd);
308
319
 
@@ -311,16 +322,23 @@ describe("storeless guarantee (PROTECTED)", () => {
311
322
  runComplete(["ontology", "show", "d"], capabilities, env),
312
323
  ).resolves.toContain("ds");
313
324
 
314
- // skills / prompts / tiers: no data here, but the point is they never boot
315
- // the store — an empty list is the correct storeless answer.
325
+ // index (derived, no field): the tier lookup is a declared pack lookup
326
+ // (L-OPEN-9), so its candidates are the index's entity NAMES for the
327
+ // declared type — prefixed IRIs, like every other declared lookup's
328
+ // (D9-A). `ds:apps` is a real `ds:Tier` subject in the embedded index,
329
+ // and the lookup resolves a prefixed-IRI candidate directly.
330
+ await expect(
331
+ runComplete(["tier", "lookup", "ap"], capabilities, env),
332
+ ).resolves.toContain("ds:apps");
333
+
334
+ // skills (no skills root here) and prompt labels (this graph carries no
335
+ // prompt entities) have nothing to offer, and the honest storeless answer
336
+ // is an empty list rather than a store boot.
316
337
  for (const words of [
317
338
  ["skill", "lookup", "do"],
318
339
  ["prompt", "lookup", "bu"],
319
- ["tier", "lookup", "ap"],
320
340
  ]) {
321
- await expect(
322
- runComplete(words, capabilities, env),
323
- ).resolves.toBeInstanceOf(Array);
341
+ await expect(runComplete(words, capabilities, env)).resolves.toEqual([]);
324
342
  }
325
343
 
326
344
  expect(vi.mocked(createStore)).not.toHaveBeenCalled();
@@ -328,31 +346,59 @@ describe("storeless guarantee (PROTECTED)", () => {
328
346
 
329
347
  it("the spawned __complete fast path answers without touching any state", () => {
330
348
  // The perf globalSetup guarantees dist/pragma exists.
349
+ // A FRESH INSTALL, reproduced: nothing is inherited from this process's
350
+ // environment, the cwd holds no config, and $HOME plus all three XDG roots
351
+ // are empty temps. Anything the binary answers here it answered from
352
+ // itself. (The suite before this inherited `...process.env`, so it could
353
+ // not tell a compiled-in answer from an ambient one.)
331
354
  const binary = fileURLToPath(
332
355
  new URL("../../../dist/pragma", import.meta.url),
333
356
  );
357
+ const home = mkdtempSync(join(tmpdir(), "pragma-storeless-home-"));
334
358
  const xdgConfig = mkdtempSync(join(tmpdir(), "pragma-storeless-cfg-"));
335
359
  const xdgState = mkdtempSync(join(tmpdir(), "pragma-storeless-state-"));
336
360
  const xdgCache = mkdtempSync(join(tmpdir(), "pragma-storeless-cache-"));
361
+ const emptyCwd = mkdtempSync(join(tmpdir(), "pragma-storeless-cwd-"));
362
+ const env = {
363
+ HOME: home,
364
+ XDG_CONFIG_HOME: xdgConfig,
365
+ XDG_STATE_HOME: xdgState,
366
+ XDG_CACHE_HOME: xdgCache,
367
+ };
337
368
 
338
- const result = spawnSync(binary, ["__complete", "--", "co"], {
369
+ const nouns = spawnSync(binary, ["__complete", "--", "co"], {
339
370
  encoding: "utf-8",
340
- env: {
341
- ...process.env,
342
- XDG_CONFIG_HOME: xdgConfig,
343
- XDG_STATE_HOME: xdgState,
344
- XDG_CACHE_HOME: xdgCache,
345
- },
371
+ cwd: emptyCwd,
372
+ env,
346
373
  });
374
+ expect(nouns.status).toBe(0);
375
+ expect(nouns.stdout).toBe("colophon\nconfig\n");
376
+ expect(nouns.stderr).toBe("");
377
+
378
+ // The headline guarantee of the embedded pack: a user who has installed
379
+ // the binary and nothing else gets ENTITY candidates on the first TAB.
380
+ // Every other pin on the embedded index runs in-process, where a bundler
381
+ // change that made `pack.index.generated.ts` unreachable FROM THE BINARY
382
+ // would leave the whole suite green.
383
+ const entities = spawnSync(
384
+ binary,
385
+ ["__complete", "--", "block", "lookup", "ds:global.component.but"],
386
+ { encoding: "utf-8", cwd: emptyCwd, env },
387
+ );
388
+ expect(entities.status).toBe(0);
389
+ expect(entities.stderr).toBe("");
390
+ expect(entities.stdout.split("\n").at(0)).toBe(
391
+ "ds:global.component.button",
392
+ );
347
393
 
348
- expect(result.status).toBe(0);
349
- expect(result.stdout).toBe("colophon\nconfig\n");
350
- expect(result.stderr).toBe("");
351
394
  // Storeless and config-free: no first-run marker, no config cache, no
352
- // store artifacts — the fast path must leave the environment untouched.
395
+ // store artifacts — the fast path must leave the environment untouched,
396
+ // after BOTH kinds of context.
397
+ expect(readdirSync(home)).toEqual([]);
353
398
  expect(readdirSync(xdgConfig)).toEqual([]);
354
399
  expect(readdirSync(xdgState)).toEqual([]);
355
400
  expect(readdirSync(xdgCache)).toEqual([]);
401
+ expect(readdirSync(emptyCwd)).toEqual([]);
356
402
  });
357
403
 
358
404
  it("the spawned fast path emits zero bytes for zero candidates, exit 0", () => {