@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
@@ -15,6 +15,7 @@ import type {
15
15
  SectionDef,
16
16
  } from "./contracts.js";
17
17
  import { DEFAULT_PREFIX_MAP } from "./prefixes.js";
18
+ import { defaultStyle, type RenderStyle, styleFor } from "./style.js";
18
19
 
19
20
  type RenderMode = "plain" | "llm";
20
21
 
@@ -104,27 +105,40 @@ export function renderListLlm<T>(
104
105
  return lines.join("\n");
105
106
  }
106
107
 
108
+ /**
109
+ * Render an entity lookup as plain text.
110
+ *
111
+ * @param entity - The looked-up entity.
112
+ * @param options - The lookup layout (title, fields, sections).
113
+ * @param style - TTY styling; defaults to the process style. On a color-capable
114
+ * terminal the title is bold, its `═` rule dim, and field labels cyan; off a
115
+ * TTY the styler is inert, so the output is byte-identical to the plain form.
116
+ * @returns The formatted lookup block.
117
+ */
107
118
  export function renderLookupPlain<T>(
108
119
  entity: T,
109
120
  options: RenderLookupOptions<T>,
121
+ style: RenderStyle = defaultStyle(),
110
122
  ): string {
111
- return renderLookup(entity, options, "plain");
123
+ return renderLookup(entity, options, "plain", style);
112
124
  }
113
125
 
114
126
  export function renderLookupLlm<T>(
115
127
  entity: T,
116
128
  options: RenderLookupOptions<T>,
117
129
  ): string {
118
- return renderLookup(entity, options, "llm");
130
+ // The condensed form is a byte-frozen agent contract — never styled.
131
+ return renderLookup(entity, options, "llm", styleFor(false));
119
132
  }
120
133
 
121
134
  function renderLookup<T>(
122
135
  entity: T,
123
136
  options: RenderLookupOptions<T>,
124
137
  mode: RenderMode,
138
+ style: RenderStyle,
125
139
  ): string {
126
140
  const title = options.title(entity);
127
- const fields = renderLookupFields(entity, options, mode);
141
+ const fields = renderLookupFields(entity, options, mode, style);
128
142
  const sections = renderLookupSections(entity, options, mode);
129
143
 
130
144
  if (mode === "llm") {
@@ -132,13 +146,17 @@ function renderLookup<T>(
132
146
  }
133
147
 
134
148
  const headingRule = "═".repeat(Math.max(title.length, 24));
135
- return [title, headingRule, "", ...fields, ...sections].join("\n").trimEnd();
149
+ const head = style.enabled
150
+ ? [style.bold(title), style.dim(headingRule)]
151
+ : [title, headingRule];
152
+ return [...head, "", ...fields, ...sections].join("\n").trimEnd();
136
153
  }
137
154
 
138
155
  function renderLookupFields<T>(
139
156
  entity: T,
140
157
  options: RenderLookupOptions<T>,
141
158
  mode: RenderMode,
159
+ style: RenderStyle,
142
160
  ): string[] {
143
161
  const prefixes = options.prefixes ?? DEFAULT_PREFIX_MAP;
144
162
  return options.fields.flatMap((field) => {
@@ -148,9 +166,9 @@ function renderLookupFields<T>(
148
166
  }
149
167
 
150
168
  const formatted = formatInlineValue(value, prefixes);
151
- return mode === "llm"
152
- ? [`- ${field.label}: ${formatted}`]
153
- : [` ${field.label}: ${formatted}`];
169
+ if (mode === "llm") return [`- ${field.label}: ${formatted}`];
170
+ const label = style.enabled ? style.cyan(field.label) : field.label;
171
+ return [` ${label}: ${formatted}`];
154
172
  });
155
173
  }
156
174
 
@@ -0,0 +1,31 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { defaultStyle, styleFor } from "./style.js";
3
+
4
+ describe("styleFor", () => {
5
+ it("passes every color through unchanged when disabled", () => {
6
+ const style = styleFor(false);
7
+ expect(style.enabled).toBe(false);
8
+ expect(style.bold("x")).toBe("x");
9
+ expect(style.dim("x")).toBe("x");
10
+ expect(style.cyan("x")).toBe("x");
11
+ expect(style.green("x")).toBe("x");
12
+ expect(style.yellow("x")).toBe("x");
13
+ });
14
+
15
+ it("reports enabled and drives chalk when enabled", () => {
16
+ const style = styleFor(true);
17
+ expect(style.enabled).toBe(true);
18
+ // A non-TTY test process leaves chalk at level 0, so the text survives
19
+ // verbatim; what the beautified renderers gate on is `enabled`.
20
+ expect(style.cyan("x")).toContain("x");
21
+ });
22
+ });
23
+
24
+ describe("defaultStyle", () => {
25
+ it("is disabled when stdout is not a TTY (keeps piped output byte-stable)", () => {
26
+ // The vitest runner's stdout is never a TTY, so the beautify seam is inert
27
+ // here — the guarantee that piped / agent output is unstyled and stable.
28
+ expect(process.stdout.isTTY).not.toBe(true);
29
+ expect(defaultStyle().enabled).toBe(false);
30
+ });
31
+ });
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Terminal-styling seam for the human (TTY) output path.
3
+ *
4
+ * The condensed/agent (`llm`) and JSON paths are byte-frozen contracts; only an
5
+ * interactive terminal earns color and alignment. A {@link RenderStyle} carries
6
+ * that decision as data so a pure renderer can consult it without reading the
7
+ * process itself: {@link defaultStyle} probes chalk's own TTY/env detection (the
8
+ * same signal the help system leans on), so piped / redirected / MCP output —
9
+ * where `enabled` is false — renders byte-for-byte as before, while an attended
10
+ * terminal renders with `enabled: true`. Passing an explicit style keeps the
11
+ * beautified renderers deterministically testable in both modes.
12
+ */
13
+
14
+ import chalk from "chalk";
15
+
16
+ /** The palette + gate a renderer consults to beautify the human path. */
17
+ export interface RenderStyle {
18
+ /** Whether to align + colorize — true only on a color-capable TTY. */
19
+ readonly enabled: boolean;
20
+ /** Emphasize a heading or primary label. */
21
+ bold(text: string): string;
22
+ /** De-emphasize secondary text (rules, markers, paths, punctuation). */
23
+ dim(text: string): string;
24
+ /** Highlight a value or identifier. */
25
+ cyan(text: string): string;
26
+ /** Tint a healthy / up-to-date status. */
27
+ green(text: string): string;
28
+ /** Tint a status that wants attention. */
29
+ yellow(text: string): string;
30
+ }
31
+
32
+ /** The identity styler — every function returns its input unchanged. */
33
+ const PLAIN_STYLE: RenderStyle = {
34
+ enabled: false,
35
+ bold: (text) => text,
36
+ dim: (text) => text,
37
+ cyan: (text) => text,
38
+ green: (text) => text,
39
+ yellow: (text) => text,
40
+ };
41
+
42
+ /**
43
+ * Build a {@link RenderStyle} for a known color decision.
44
+ *
45
+ * @param enabled - True to colorize + align via chalk; false for the identity
46
+ * styler (byte-identical to the pre-beautify plain output).
47
+ * @returns A style whose functions color, or pass through, accordingly.
48
+ */
49
+ export function styleFor(enabled: boolean): RenderStyle {
50
+ if (!enabled) return PLAIN_STYLE;
51
+ return {
52
+ enabled: true,
53
+ bold: (text) => chalk.bold(text),
54
+ dim: (text) => chalk.dim(text),
55
+ cyan: (text) => chalk.cyan(text),
56
+ green: (text) => chalk.green(text),
57
+ yellow: (text) => chalk.yellow(text),
58
+ };
59
+ }
60
+
61
+ /**
62
+ * The active style for this process's stdout.
63
+ *
64
+ * Gated on BOTH signals: stdout must be an attended TTY, so piped / redirected /
65
+ * MCP output stays byte-stable even where `supports-color` fires off a TTY (CI
66
+ * sets `GITHUB_ACTIONS`, which chalk honors); AND chalk must report a usable
67
+ * color level, so `NO_COLOR` on a real terminal still yields the plain form.
68
+ *
69
+ * @returns A colorizing style on a color-capable TTY, else the identity styler.
70
+ * @note Impure — reads `process.stdout.isTTY` and chalk's color level.
71
+ */
72
+ export function defaultStyle(): RenderStyle {
73
+ return styleFor(process.stdout.isTTY === true && chalk.level > 0);
74
+ }
@@ -39,6 +39,7 @@ import {
39
39
  MANIFEST_FILE,
40
40
  type Manifest,
41
41
  SCHEMA_FILE,
42
+ STORIES_FILE,
42
43
  } from "./types.js";
43
44
 
44
45
  /** One RDF source to build into a pack. */
@@ -53,10 +54,17 @@ export interface BuildPackInput {
53
54
  export interface BuildPackOptions {
54
55
  readonly name: string;
55
56
  readonly version: string;
56
- /** The config `packages` ref (verbatim) or a label. */
57
+ /** The config `packs` ref (verbatim) or a label. */
57
58
  readonly sourceRef: string;
58
59
  /** Prefixes the store (and every query) is built with. */
59
60
  readonly prefixes?: Readonly<Record<string, string>>;
61
+ /**
62
+ * The `stories/*.json` files the resolved packages ship, as raw text. Hash
63
+ * inputs like any other source — a story-only edit is a new pack, and a set
64
+ * of packages shipping none keeps the hash it had before stories existed, so
65
+ * no cached pack is invalidated by rehashing.
66
+ */
67
+ readonly stories?: readonly BuildPackInput[];
60
68
  }
61
69
 
62
70
  /** The outcome of a pack build (or cache hit). */
@@ -72,7 +80,8 @@ export interface BuildPackResult {
72
80
  * Build (or reuse) the pack for a set of source inputs.
73
81
  *
74
82
  * @param inputs - The RDF sources (path + content).
75
- * @param options - Provenance and the prefixes to build with.
83
+ * @param options - Provenance, the prefixes to build with, and the packages'
84
+ * carried read stories.
76
85
  * @returns The pack directory, its content hash, and whether it was reused.
77
86
  * @note Impure — creates a store, compiles the schema, writes the pack.
78
87
  */
@@ -80,8 +89,12 @@ export async function buildPack(
80
89
  inputs: readonly BuildPackInput[],
81
90
  options: BuildPackOptions,
82
91
  ): Promise<BuildPackResult> {
92
+ const stories = options.stories ?? [];
83
93
  const hash = await contentHash(
84
- inputs.map((input) => ({ path: input.path, content: input.content })),
94
+ [...inputs, ...stories].map((input) => ({
95
+ path: input.path,
96
+ content: input.content,
97
+ })),
85
98
  );
86
99
  const dir = packDir(hash);
87
100
 
@@ -121,6 +134,29 @@ export async function buildPack(
121
134
  const index = await buildIndex(store, store.prefixes, hash);
122
135
  writeFileSync(join(temp, INDEX_FILE), JSON.stringify(index));
123
136
 
137
+ // Written UNCONDITIONALLY (`[]` when the packages ship none): the artifact
138
+ // set is FIVE AND ONLY FIVE, and `packIsComplete` gates on it, so a pack
139
+ // whose hash covers stories always holds them.
140
+ writeFileSync(
141
+ join(temp, STORIES_FILE),
142
+ JSON.stringify(
143
+ stories.map((story) => ({
144
+ source: story.path,
145
+ content: story.content,
146
+ })),
147
+ ),
148
+ );
149
+
150
+ const tripleCount = await countTriples(store);
151
+ // Distinct abox subjects — the same figure `entityTotal` reports (A10),
152
+ // so `sources status` reads it from the manifest without parsing the
153
+ // whole index.json. Keep this in sync with `entityTotal` in entitySource.
154
+ const entityCount = new Set(
155
+ index.entities
156
+ .filter((entity) => entity.box === "abox")
157
+ .map((entity) => entity.uri ?? entity.name),
158
+ ).size;
159
+
124
160
  const manifest: Manifest = {
125
161
  name: options.name,
126
162
  version: options.version,
@@ -128,6 +164,8 @@ export async function buildPack(
128
164
  contentHash: hash,
129
165
  prefixes: { ...store.prefixes },
130
166
  createdAt: new Date().toISOString(),
167
+ tripleCount,
168
+ entityCount,
131
169
  };
132
170
  // Written LAST — the completeness marker for the directory.
133
171
  writeFileSync(join(temp, MANIFEST_FILE), JSON.stringify(manifest));
@@ -146,3 +184,19 @@ export async function buildPack(
146
184
  if (existsSync(temp)) rmSync(temp, { recursive: true, force: true });
147
185
  }
148
186
  }
187
+
188
+ /**
189
+ * Count a store's triples (a cheap aggregate over the union graph).
190
+ *
191
+ * @param store - The booted store.
192
+ * @returns The number of triples.
193
+ * @note Impure — runs a COUNT query against the store.
194
+ */
195
+ async function countTriples(
196
+ store: Awaited<ReturnType<typeof createStore>>,
197
+ ): Promise<number> {
198
+ const result = (await store.query(
199
+ "SELECT (COUNT(*) AS ?n) WHERE { ?s ?p ?o }" as never,
200
+ )) as import("@canonical/ke").SelectResult;
201
+ return Number(result.bindings.at(0)?.n ?? 0);
202
+ }
@@ -0,0 +1,149 @@
1
+ import { createStore } from "@canonical/ke";
2
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
3
+ import {
4
+ OWL_EXPORT_ABOX_SUBJECTS,
5
+ OWL_EXPORT_PREFIXES,
6
+ OWL_EXPORT_TTL,
7
+ } from "../../../testing/fixtures/graph/owlExport.js";
8
+ import { entityTotal } from "../../completion/entitySource.js";
9
+ import { VOCABULARY } from "../../vocabulary.js";
10
+ import { buildIndex } from "./buildIndex.js";
11
+ import type { PackIndex } from "./types.js";
12
+
13
+ type Store = import("@canonical/ke").Store;
14
+
15
+ /**
16
+ * The counters/classifiers `buildIndex` builds are masked by the embedded
17
+ * single-typed toy graph; the multi-typed OWL-export fixture is where the
18
+ * backlog-A bugs bite. Each `it` fails on the pre-fix code and passes after.
19
+ */
20
+ describe("buildIndex — real-data counters/classifiers (backlog A)", () => {
21
+ let store: Store;
22
+ let index: PackIndex;
23
+
24
+ beforeAll(async () => {
25
+ store = await createStore({
26
+ sources: [{ content: OWL_EXPORT_TTL, path: "owl-export.ttl" }],
27
+ prefixes: OWL_EXPORT_PREFIXES,
28
+ });
29
+ index = await buildIndex(store, OWL_EXPORT_PREFIXES, "test-hash");
30
+ });
31
+
32
+ afterAll(() => {
33
+ store.dispose();
34
+ });
35
+
36
+ const byName = (name: string): PackIndex["entities"][number][] =>
37
+ index.entities.filter((entity) => entity.name === name);
38
+
39
+ it("A1: entityTotal counts DISTINCT abox subjects, not the raw multiset", () => {
40
+ // Every individual once — despite Protégé's `owl:NamedIndividual` dual-typing.
41
+ expect(entityTotal(index)).toBe(OWL_EXPORT_ABOX_SUBJECTS.length);
42
+
43
+ // The raw multiset the old code summed is strictly larger: each individual
44
+ // double-counts under `owl:NamedIndividual`, plus the class/property
45
+ // meta-buckets pile on top.
46
+ const rawSum = Object.values(index.instanceCountByType).reduce(
47
+ (sum, n) => sum + n,
48
+ 0,
49
+ );
50
+ expect(rawSum).toBeGreaterThan(entityTotal(index));
51
+
52
+ // The abox names match the oracle exactly.
53
+ const aboxNames = index.entities
54
+ .filter((entity) => entity.box === "abox")
55
+ .map((entity) => entity.name);
56
+ expect([...new Set(aboxNames)].sort()).toEqual([
57
+ ...OWL_EXPORT_ABOX_SUBJECTS,
58
+ ]);
59
+ });
60
+
61
+ it("A3: a multi-domain individual gets a deterministic (lexical) primary type", () => {
62
+ // ex:toggle is both ex:Interactive and ex:FormControl; the smaller wins,
63
+ // every build/engine — never whichever type the store returned first.
64
+ const toggle = byName("ex:toggle").at(0);
65
+ expect(toggle?.type).toBe("ex:FormControl");
66
+ expect(toggle?.box).toBe("abox");
67
+ });
68
+
69
+ it("A6: a blank-node rdf:type is ignored, never a garbage primary type", () => {
70
+ // ex:field is typed by an anonymous class too; the blank node must not win.
71
+ expect(byName("ex:field").at(0)?.type).toBe("ex:FormControl");
72
+ // No entity type, no listed type, and no instance-count bucket is a blank node.
73
+ for (const entity of index.entities) {
74
+ expect(entity.type.startsWith("_:")).toBe(false);
75
+ for (const type of entity.types ?? []) {
76
+ expect(type.startsWith("_:")).toBe(false);
77
+ }
78
+ }
79
+ for (const key of Object.keys(index.instanceCountByType)) {
80
+ expect(key.startsWith("_:")).toBe(false);
81
+ }
82
+ });
83
+
84
+ it("A7: a multilingual label resolves to @en, deterministically", () => {
85
+ // "Button"@en / "Bouton"@fr → Button; the @lang no longer flips the pick.
86
+ expect(byName("ds:button").at(0)?.label).toBe("Button");
87
+ expect(byName("ex:toggle").at(0)?.label).toBe("Toggle");
88
+ expect(byName("ds:datePicker").at(0)?.label).toBe("Date Picker");
89
+ });
90
+
91
+ it("A8: an OWL-punned subject emits BOTH its tbox and abox facet", () => {
92
+ // ex:Slider is a class (tbox) AND an ex:Category individual (abox) — both
93
+ // must survive, so its abox membership stays completable.
94
+ const facets = byName("ex:Slider");
95
+ expect(facets).toHaveLength(2);
96
+ expect(facets.map((facet) => facet.box).sort()).toEqual(["abox", "tbox"]);
97
+ expect(facets.find((facet) => facet.box === "tbox")?.type).toBe(
98
+ "owl:Class",
99
+ );
100
+ expect(facets.find((facet) => facet.box === "abox")?.type).toBe(
101
+ "ex:Category",
102
+ );
103
+ });
104
+ });
105
+
106
+ /**
107
+ * The alternative-name property is DECLARED by the distribution, so a pack whose
108
+ * prefix map does not bind its prefix is an ordinary case — a third-party pack
109
+ * outside that namespace — not an error. Resolving the term strictly would throw
110
+ * there and take the whole pack build down with it.
111
+ */
112
+ describe("buildIndex — the declared alternative-name property", () => {
113
+ let store: Store;
114
+
115
+ beforeAll(async () => {
116
+ store = await createStore({
117
+ sources: [{ content: OWL_EXPORT_TTL, path: "owl-export.ttl" }],
118
+ prefixes: OWL_EXPORT_PREFIXES,
119
+ });
120
+ });
121
+
122
+ afterAll(() => {
123
+ store.dispose();
124
+ });
125
+
126
+ it("projects it into altNames when the pack binds its prefix", async () => {
127
+ const index = await buildIndex(store, OWL_EXPORT_PREFIXES, "bound");
128
+ const named = index.entities.filter((entity) => entity.altNames);
129
+ expect(named.length).toBeGreaterThan(0);
130
+ expect(named.at(0)?.altNames?.length).toBeGreaterThan(0);
131
+ });
132
+
133
+ it("still builds, carrying no altNames, when the pack binds no such prefix", async () => {
134
+ // Unbind the prefix the DECLARATION uses, whatever it is — the subject here
135
+ // is the declared property, so naming a prefix would couple this case to
136
+ // today's values and quietly stop testing anything if they moved.
137
+ const declared = VOCABULARY.altName.slice(
138
+ 0,
139
+ VOCABULARY.altName.indexOf(":"),
140
+ );
141
+ const { [declared]: _unbound, ...withoutDomain } = OWL_EXPORT_PREFIXES;
142
+ const index = await buildIndex(store, withoutDomain, "unbound");
143
+ // The graph is unchanged, so every entity is still indexed…
144
+ expect(index.entities.length).toBeGreaterThan(0);
145
+ // …they simply carry no alternative names, exactly as a pack that never
146
+ // authored the property would.
147
+ expect(index.entities.filter((entity) => entity.altNames)).toEqual([]);
148
+ });
149
+ });