@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
@@ -0,0 +1,146 @@
1
+ /**
2
+ * The domain vocabulary this distribution declares, validated once at load.
3
+ *
4
+ * The kernel reads a graph whose vocabulary it must not name. Two of its reads
5
+ * need domain terms: the pack index projects an alternative-name property into
6
+ * `altNames` (what a family is addressed by, hence what completes and what a
7
+ * lookup resolves), and the MCP prompt surface materializes prompt entities
8
+ * with a generated SELECT. Both take their terms from here, and the bundled
9
+ * capability packs that query the same terms read the same declaration — so
10
+ * complete and resolve cannot drift apart.
11
+ *
12
+ * A DISTRIBUTION DECLARATION, not a config layer field, and deliberately so.
13
+ * Both readers sit where the config layer is unreachable: the storeless
14
+ * `__complete` fast path builds a completion ref at module load, and the index
15
+ * is built inside the pack builder. Made layerable, a user setting it in their
16
+ * project config would change nothing anywhere — silence, which is the exact
17
+ * failure a dead config field is. A fork ships its own binary: it edits the
18
+ * declaration and rebuilds.
19
+ *
20
+ * Terms must be PREFIXED NAMES whose prefix the distribution BINDS, and
21
+ * {@link parseVocabulary} rejects anything else by name. They are interpolated
22
+ * into SPARQL, where an absolute IRI written bare is a parse error and an
23
+ * unbound prefix is either a parse error or a silently empty result — and a
24
+ * failed read is indistinguishable from an empty graph, which is how a
25
+ * malformed declaration would otherwise surface as "this distribution has no
26
+ * prompts". Both halves matter: rejecting the shape alone still lets a
27
+ * one-character typo (`ds:name` → `dss:name`) pass startup and then drop every
28
+ * `altNames` from the index while reporting a built store as unbuilt. Failing
29
+ * at module load is correct for a compiled-in build defect: the distribution
30
+ * config's own layer validation (`config/defaults.ts`) already throws the same
31
+ * way.
32
+ *
33
+ * Standard vocabulary (`rdfs:label`, `rdfs:comment`) is deliberately absent —
34
+ * it is the same in every graph, and the index already treats it as universal.
35
+ */
36
+
37
+ import conf, { vocabulary } from "../../pragma.conf.js";
38
+ import { PragmaError } from "./error/PragmaError.js";
39
+
40
+ /** The domain terms the kernel reads a distribution's graph with. */
41
+ export interface DeclaredVocabulary {
42
+ /**
43
+ * The property an entity family is addressed by, beside `rdfs:label` — the
44
+ * tokens the index offers as candidates and a bespoke lookup matches on.
45
+ */
46
+ readonly altName: string;
47
+ /** The prompt entity shape the MCP prompt surface reads. */
48
+ readonly prompt: {
49
+ /** The class every prompt entity carries. */
50
+ readonly type: string;
51
+ /** The template body, with `{{arg}}` placeholders. */
52
+ readonly body: string;
53
+ /** Links a prompt to one of its declared arguments. */
54
+ readonly argument: string;
55
+ /** An argument's name. */
56
+ readonly argName: string;
57
+ /** Whether an argument is required. */
58
+ readonly argRequired: string;
59
+ };
60
+ }
61
+
62
+ /** A prefixed name — `prefix:local`, the only form a generated query can read. */
63
+ const PREFIXED_NAME = /^[A-Za-z][A-Za-z0-9_-]*:[A-Za-z_][A-Za-z0-9_.-]*$/;
64
+
65
+ /**
66
+ * The distribution config file, quoted in the diagnostics.
67
+ *
68
+ * The literal, not `${BIN_NAME}.conf.ts`: the import above names this file, and
69
+ * so do four other modules, so a fork CANNOT rename it — deriving the name from
70
+ * the fork's would send its maintainer to a path that is not on disk.
71
+ * `config/defaults.ts` names the same file for the same reason.
72
+ */
73
+ const CONF_FILE = "pragma.conf.ts";
74
+
75
+ /**
76
+ * Hold one declared term to a form a generated query can actually read: the
77
+ * `prefix:local` shape, AND a prefix the distribution binds.
78
+ *
79
+ * @param field - The term's path in the declaration, for the message.
80
+ * @param term - The declared value.
81
+ * @param prefixes - The distribution's declared namespace bindings.
82
+ * @param source - The file the declaration was read from.
83
+ * @throws PragmaError CONFIG_ERROR when the term is not `prefix:local`, or
84
+ * names a prefix `prefixes` does not bind.
85
+ */
86
+ function assertReadableTerm(
87
+ field: string,
88
+ term: string,
89
+ prefixes: Readonly<Record<string, string>>,
90
+ source: string,
91
+ ): void {
92
+ const reject = (problem: string, fix: string): never => {
93
+ throw PragmaError.configError(
94
+ `Invalid vocabulary in ${source}: \`${field}\` ${problem}, not ${JSON.stringify(term)}.`,
95
+ { recovery: { message: `In ${source}, ${fix}` } },
96
+ );
97
+ };
98
+ if (!PREFIXED_NAME.test(term)) {
99
+ reject(
100
+ "must be a prefixed name like `ex:thing`",
101
+ `write \`${field}\` as \`<prefix>:<local>\`. These terms are interpolated into queries, which cannot read an absolute IRI here.`,
102
+ );
103
+ }
104
+ const prefix = term.slice(0, term.indexOf(":"));
105
+ if (!Object.hasOwn(prefixes, prefix)) {
106
+ reject(
107
+ `must use a prefix \`prefixes\` binds, and nothing binds \`${prefix}:\``,
108
+ `either bind \`${prefix}\` under \`prefixes\` or write \`${field}\` with a prefix that is bound. An unbound prefix makes the generated query fail, which a read cannot tell apart from an empty graph.`,
109
+ );
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Validate a declared vocabulary.
115
+ *
116
+ * @param raw - A distribution's `vocabulary` export.
117
+ * @param prefixes - That distribution's `prefixes`, which must bind every
118
+ * prefix the terms use.
119
+ * @param source - The file it was declared in, quoted in errors.
120
+ * @returns The same declaration, once every term is readable.
121
+ * @throws PragmaError CONFIG_ERROR naming the offending field and its value.
122
+ */
123
+ export function parseVocabulary(
124
+ raw: DeclaredVocabulary,
125
+ prefixes: Readonly<Record<string, string>>,
126
+ source: string,
127
+ ): DeclaredVocabulary {
128
+ assertReadableTerm("altName", raw.altName, prefixes, source);
129
+ for (const [term, value] of Object.entries(raw.prompt)) {
130
+ assertReadableTerm(`prompt.${term}`, value, prefixes, source);
131
+ }
132
+ return raw;
133
+ }
134
+
135
+ /**
136
+ * This distribution's validated vocabulary.
137
+ *
138
+ * The declaration is type-checked at this call — a fork that drops or mistypes
139
+ * a term fails `tsc`, and one that writes an unreadable term fails here at
140
+ * startup rather than at the read that would have swallowed it.
141
+ */
142
+ export const VOCABULARY = parseVocabulary(
143
+ vocabulary,
144
+ conf.prefixes ?? {},
145
+ CONF_FILE,
146
+ );
@@ -78,13 +78,19 @@ export const PARITY_GAPS: readonly ParityGapEntry[] = [
78
78
  id: "generic-renderers-not-bespoke-templates",
79
79
  area: "render",
80
80
  description:
81
- "Every noun's plain/llm output — including the one hand-written verb, `block list` — renders through the SAME generic list/lookup renderer (`kernel/render/renderers.ts`), driven by declarative column/field/section metadata. There are no per-noun hand-authored render templates left to keep in parity.",
81
+ "Every noun's plain/llm output renders through the SAME generic list/lookup renderer (`kernel/render/renderers.ts`), driven by declarative column/field/section metadata. There are no per-noun hand-authored render templates left to keep in parity (the last hand-written verb, `block list`, became declared content in L-OPEN-9).",
82
82
  },
83
83
  {
84
84
  id: "block-lookup-not-tier-scoped",
85
85
  area: "block",
86
86
  description:
87
- "`block lookup` resolves by `ds:name` GLOBALLY, with no tier or channel filtering — only the hand-written `block list` is tier/channel-aware (PR3 Risk5, `capabilities/block/tierChain.ts`). A same-named block declared at two tiers cannot be disambiguated by `block lookup` alone (the old shell's tier-scoped lookup disambiguation has no v2 equivalent).",
87
+ "`block lookup` resolves by `ds:name` GLOBALLY, with no tier or channel filtering. A same-named block declared at two tiers cannot be disambiguated by plain name (the old shell's tier-scoped lookup disambiguation has no v2 equivalent) — address it by prefixed IRI instead. Since L-OPEN-9 NOTHING is tier/channel-aware: the hand-written, tier-chain-filtered `block list` was removed with its `tierChain.ts`.",
88
+ },
89
+ {
90
+ id: "block-list-unfiltered",
91
+ area: "block",
92
+ description:
93
+ "L-OPEN-9 (owner-signed): `block list` is the block story's compiled, UNFILTERED list — it shows every block, including experimental/alpha ones, for everyone, until filtering returns in declared form. The old tier-chain inheritance, channel visibility, and `--all-tiers` escape were hand-written filtering the pack grammar has no term for, and the ruling removed them rather than growing the grammar to keep them. `config.tier` consequently scopes nothing (reported by `config show`/`info` only); `config.channel` keeps only its npm dist-tag role for `upgrade`/`info`.",
88
94
  },
89
95
  {
90
96
  id: "graph-query-deferred",
@@ -40,7 +40,7 @@ the emitted set legitimately grows PR-by-PR.
40
40
  | `agentSession.mcp.test.ts` | A multi-step agent MCP session (list → pick → lookup) + the SPARQL escape hatch. |
41
41
  | `mcpSurface.test.ts` | Every MCP tool the live catalog exposes is callable and well-formed — the "surface ⊆ covenant, exercised" proof. |
42
42
  | `disclosure.test.ts` | The cross-noun disclosure sweep (mechanism itself is PR3's/kernel's protected unit test — this only sweeps the noun SET). |
43
- | `journeys.cli.test.ts` | The `block list` tier-chain/channel journey (the one hand-written verb). |
43
+ | `journeys.cli.test.ts` | The `block list` journey — since L-OPEN-9 it pins that the declared list is UNFILTERED: four tier/channel configs, one answer. |
44
44
  | `completion.test.ts` | The THIN main-line `__complete` candidate contract (§6 — see "Completion" below). |
45
45
 
46
46
  Plus `testing/eval/` (the eval harness + seed cases) and `testing/PARITY_GAPS.ts`
@@ -71,7 +71,7 @@ import { CANONICAL_TTL, CANONICAL_CONFIG } from "../fixtures/graph/canonical.js"
71
71
 
72
72
  const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL, config: CANONICAL_CONFIG });
73
73
  try {
74
- // fixture.cwd carries a REAL pragma.lock.json — bootRuntime(flags, fixture.cwd),
74
+ // fixture.cwd is pointed at a REAL built pack — bootRuntime(flags, fixture.cwd),
75
75
  // executeVerb(...), projectMcp(modules, fixture.cwd), and (if you must)
76
76
  // runCli(args, { cwd: fixture.cwd }) all resolve the SAME cached pack.
77
77
  } finally {
@@ -80,7 +80,7 @@ try {
80
80
  ```
81
81
 
82
82
  Use `packRuntime.ts#buildFixtureRuntime` (PR3's helper) instead when you only
83
- need ONE surface (a direct `verb.run()` call) — it's cheaper (no lock file).
83
+ need ONE surface (a direct `verb.run()` call) — it's cheaper (no real build).
84
84
  Reach for `bootFixtureRuntime` specifically when you need CLI and MCP (or a
85
85
  spawn) to agree on the same store.
86
86
 
@@ -4,7 +4,7 @@
4
4
  * The full completion engine (`kernel/completion`: parse → resolve → rank, the
5
5
  * spec-derived model, the entity seam) owns the unit-level guarantees; this
6
6
  * pins the behaviour end-to-end against the LIVE capabilities and — for the
7
- * entity tier — against a REAL built pack index on disk (the locked-pack branch
7
+ * entity tier — against a REAL built pack index on disk (the built-pack branch
8
8
  * of the storeless reader, which the engine's own tests exercise only via the
9
9
  * embedded fallback). Parameterized over the live read surface so noun/verb
10
10
  * renames never churn it.
@@ -89,21 +89,34 @@ describe("flags are kebab-cased at emission (the live, testable guarantee)", ()
89
89
  });
90
90
  });
91
91
 
92
- describe("entity params resolve from a real built pack index (B10)", () => {
93
- it("a lookup's entity positional completes from the built pack index", async () => {
92
+ describe("entity params resolve from the project's built pack, not the embedded snapshot (B10)", () => {
93
+ it("a lookup's entity positional offers the built pack's subjects and none of the snapshot's", async () => {
94
94
  const fixture = await bootFixtureRuntime({
95
95
  ttl: CANONICAL_TTL,
96
96
  config: ALL_VISIBLE_CONFIG,
97
97
  });
98
98
  try {
99
- // The locked-pack branch of the storeless reader: a real index.json on
99
+ // The built-pack branch of the storeless reader: a real index.json on
100
100
  // disk under the fixture cwd, read without booting the store.
101
101
  const all = await runComplete(
102
102
  ["block", "lookup", ""],
103
103
  capabilities,
104
104
  indexCompletionEnv(fixture.cwd),
105
105
  );
106
- expect(all.length).toBeGreaterThan(0);
106
+ // The four `ds:Component` subjects the canonical fixture declares
107
+ // (`blockGraph.ts`'s Button/Modal plus `canonical.ts`'s LXD Panel and
108
+ // Beta Widget) — the fixture's own data, not a recomputation. Stating
109
+ // the SET is what makes the distribution's own snapshot names provably
110
+ // absent: a pointer read that regressed to the embedded fallback would
111
+ // complete another graph's names while every read answered correctly,
112
+ // and the old `narrowed.length < all.length` heuristic only noticed by
113
+ // the accident that no snapshot name contains `ds:b`.
114
+ expect([...all].sort()).toEqual([
115
+ "ds:betaWidget",
116
+ "ds:button",
117
+ "ds:lxdPanel",
118
+ "ds:modal",
119
+ ]);
107
120
 
108
121
  const narrowed = await runComplete(
109
122
  ["block", "lookup", "ds:b"],
@@ -111,7 +124,7 @@ describe("entity params resolve from a real built pack index (B10)", () => {
111
124
  indexCompletionEnv(fixture.cwd),
112
125
  );
113
126
  // Every ranked candidate carries the partial (case-insensitive), and the
114
- // filter is strictly narrowing — the index tier really ran.
127
+ // filter is strictly narrowing — the ranker really ran over this index.
115
128
  expect(
116
129
  narrowed.every((name) => name.toLowerCase().includes("ds:b")),
117
130
  ).toBe(true);
@@ -32,9 +32,10 @@ import { listVerbs, lookupVerbs } from "./liveReadSurface.js";
32
32
 
33
33
  /**
34
34
  * List verbs with a STRING or ENUM (narrowable, non-boolean) flag — e.g.
35
- * `standard_list --category`. Excludes boolean escape hatches like `block
36
- * list --all-tiers`, which reject a string value at the schema layer rather
37
- * than narrowing to an empty result.
35
+ * `standard_list --category`. Boolean flags are excluded: they reject a string
36
+ * value at the schema layer rather than narrowing to an empty result. (`block
37
+ * list --all-tiers` used to be the example; it retired with the hand-written
38
+ * filtering in L-OPEN-9.)
38
39
  */
39
40
  const filteredListVerbs = listVerbs
40
41
  .map((v) => {
@@ -61,7 +61,7 @@ const MATRIX: readonly {
61
61
  },
62
62
  {
63
63
  label: "recovery (mcp-only, no cli)",
64
- error: PragmaError.storeUnavailable("no lock", {
64
+ error: PragmaError.storeUnavailable("the store has not been built", {
65
65
  recovery: { message: "Please report this issue." },
66
66
  }),
67
67
  },
@@ -37,14 +37,15 @@ describe("exit-code covenant, spawn-observed (A4, e2e)", () => {
37
37
  expect(result.stderr).toContain('Invalid format "yaml"');
38
38
  });
39
39
 
40
- it("3 — store unavailable (configured packages, never built)", () => {
40
+ it("3 — store unavailable (configured packs, never built)", () => {
41
41
  const cwd = mkdtempSync(join(tmpdir(), "pragma-exit3-"));
42
- // A pragma.config.ts with custom `packages` flips `origins.packages` to
43
- // "project" — with no `pragma.lock.json`, resolveSources refuses to boot
44
- // (no network probing) rather than fall back to the embedded pack.
42
+ // A pragma.config.ts with custom `packs` flips `origins.packs` to
43
+ // "project" — with nothing built for this project, resolveSources refuses
44
+ // to boot (no network probing) rather than serve the embedded pack, which
45
+ // is a DIFFERENT graph from the one this project asked for.
45
46
  writeFileSync(
46
47
  join(cwd, "pragma.config.ts"),
47
- 'export default { packages: [{ name: "never-built", source: "file:///nonexistent" }] };\n',
48
+ 'export default { packs: [{ name: "never-built", source: "file:///nonexistent" }] };\n',
48
49
  );
49
50
  const result = runCli(["block", "list"], { cwd });
50
51
  expect(result.exitCode).toBe(3);
@@ -2,8 +2,8 @@
2
2
  * Harness-foundation smoke tests (commit 1).
3
3
  *
4
4
  * Validates the framework pieces PR4 adds — NOT per-noun behavior (that's
5
- * commits 2/4's job). Proves: (1) `bootFixtureRuntime` boots a real,
6
- * lock-backed store that BOTH the CLI and MCP surfaces resolve identically
5
+ * commits 2/4's job). Proves: (1) `bootFixtureRuntime` boots a real, built-pack
6
+ * store that BOTH the CLI and MCP surfaces resolve identically
7
7
  * (via the shared canonical graph, one real lookup); (2) the compiler/runtime
8
8
  * is ontology-agnostic — a pack over the foreign `ex:` recipe namespace
9
9
  * registers its 2 tools and resolves on both surfaces; (3) `runCli` can spawn
@@ -12,7 +12,7 @@
12
12
 
13
13
  import { homedir, tmpdir } from "node:os";
14
14
  import { describe, expect, it } from "vitest";
15
- import { blockModule } from "../../capabilities/block/index.js";
15
+ import { storyModules } from "../../capabilities/distribution.js";
16
16
  import { VERSION } from "../../constants.js";
17
17
  import { compilePack } from "../../kernel/packs/compile.js";
18
18
  import { verbKey } from "../../kernel/packs/uniqueness.js";
@@ -31,8 +31,13 @@ import { plain } from "../helpers/golden.js";
31
31
  import { assertCliMcpParity } from "../helpers/parity.js";
32
32
  import { runCli } from "../helpers/runCli.js";
33
33
 
34
+ const blockModule = storyModules.get("block");
35
+ if (!blockModule) {
36
+ throw new Error('pragma.conf.ts declares no story for "block"');
37
+ }
38
+
34
39
  describe("bootFixtureRuntime — the shared canonical graph (commit-1 smoke)", () => {
35
- it("boots a lock-backed store both CLI and MCP resolve identically", async () => {
40
+ it("boots a built-pack store both CLI and MCP resolve identically", async () => {
36
41
  const fixture = await bootFixtureRuntime({
37
42
  ttl: CANONICAL_TTL,
38
43
  config: CANONICAL_CONFIG,
@@ -68,7 +73,11 @@ describe("foreign-namespace generic-pack proof (compiler is DS-agnostic)", () =>
68
73
  try {
69
74
  const recipeModule: CapabilityModule = {
70
75
  name: "recipe",
71
- verbs: compilePack(recipePack, "test:recipe", RECIPE_PREFIXES),
76
+ verbs: compilePack(
77
+ recipePack,
78
+ { label: "test:recipe", origin: "package" },
79
+ RECIPE_PREFIXES,
80
+ ),
72
81
  };
73
82
  expect(recipeModule.verbs.map((v) => verbKey(v.path)).sort()).toEqual([
74
83
  "recipe list",
@@ -1,6 +1,5 @@
1
1
  /**
2
- * B9 — the tier-chain / channel CLI journey (PR3 Risk5's ONE hand-written
3
- * verb, `block list`).
2
+ * B9 — the `block list` CLI journey, re-pinned on the DECLARED contract.
4
3
  *
5
4
  * BACKING ADAPTATION: the plan lists this as "fixture + spawn"; the PR4 quality
6
5
  * bar (R7 — keep the spawn-e2e layer to exactly A1-A4/A7) overrides that, so
@@ -9,42 +8,86 @@
9
8
  * against the fixture, rather than spawning. It still exercises the true CLI
10
9
  * seam (typed params -> dispatch -> render), just without forking a process.
11
10
  *
11
+ * CONTRACT CHANGE (L-OPEN-9): this journey used to enumerate four different
12
+ * answers — one per tier config, one per channel, one for `--all-tiers`. That
13
+ * hand-written filtering is gone; `block list` is the block story's compiled,
14
+ * UNFILTERED list. So the journey pins the honest NEW behaviour, which is a
15
+ * STRONGER claim than the old one: the SAME graph under FOUR different configs
16
+ * gives ONE answer, containing every block in the store. `config.tier` and
17
+ * `config.channel` no longer reach the query at all, and the owner-signed
18
+ * consequence — experimental/alpha blocks are visible to everyone — is asserted
19
+ * here rather than left to prose.
20
+ *
12
21
  * CONTENT ADAPTATION (R2 — verified against the live `block lookup`, not
13
22
  * assumed): the plan's wording describes an unscoped `block lookup` returning
14
23
  * multiple tier-disambiguated matches — that mechanism doesn't exist in v2
15
24
  * (`block lookup` resolves by name GLOBALLY with no tier awareness at all;
16
- * PARITY_GAPS `block-lookup-not-tier-scoped`). Only `block list` is
17
- * tier/channel-aware, so this journey is entirely about `block list` under
18
- * different tier configs on the SAME canonical graph.
25
+ * PARITY_GAPS `block-lookup-not-tier-scoped`). Since L-OPEN-9 neither block
26
+ * verb is tier-aware, so both halves of the noun now agree.
19
27
  */
20
28
 
21
29
  import { afterAll, describe, expect, it } from "vitest";
22
- import { blockModule } from "../../capabilities/block/index.js";
30
+ import { storyModules } from "../../capabilities/distribution.js";
23
31
  import { verbKey } from "../../kernel/packs/uniqueness.js";
24
32
  import { executeVerb } from "../../kernel/project/cli/dispatch.js";
25
33
  import { bootRuntime } from "../../kernel/runtime/boot.js";
26
34
  import type { VerbSpec } from "../../kernel/spec/types.js";
27
35
  import {
36
+ ALL_VISIBLE_CONFIG,
28
37
  CANONICAL_CONFIG,
29
38
  CANONICAL_TTL,
39
+ FILTERED_CONFIG,
30
40
  } from "../fixtures/graph/canonical.js";
31
41
  import {
32
42
  bootFixtureRuntime,
33
43
  type FixtureGraph,
44
+ type FixtureGraphOptions,
34
45
  } from "../helpers/fixtureGraph.js";
35
46
  import { JSON_FLAGS, NO_MUTATION } from "../helpers/parity.js";
36
47
 
48
+ const blockModule = storyModules.get("block");
49
+ if (!blockModule) {
50
+ throw new Error('pragma.conf.ts declares no story for "block"');
51
+ }
52
+
37
53
  const listVerb = blockModule.verbs.find(
38
54
  (v) => verbKey(v.path) === "block list",
39
55
  ) as VerbSpec;
40
56
 
41
- async function blockListNames(
42
- fixture: FixtureGraph,
43
- allTiers = false,
44
- ): Promise<string[]> {
57
+ /**
58
+ * Every block the canonical fixture carries: two global components, one scoped
59
+ * to `apps/lxd`, one gated to the `beta` release channel, and one UNTIERED
60
+ * subcomponent. Under the hand-written verb NO single invocation returned all
61
+ * five — the tier chain hid `LXD Panel`, the channel hid `Beta Widget`, and only
62
+ * `--all-tiers` revealed `Button Icon`. The declared list returns all five, and
63
+ * returns them whatever the config says.
64
+ */
65
+ const EVERY_BLOCK = [
66
+ "Beta Widget",
67
+ "Button",
68
+ "Button Icon",
69
+ "LXD Panel",
70
+ "Modal",
71
+ ];
72
+
73
+ const fixtures: FixtureGraph[] = [];
74
+ afterAll(async () => {
75
+ await Promise.all(fixtures.map((f) => f.dispose()));
76
+ });
77
+
78
+ /** Boot a tracked canonical fixture under `config`, for auto-disposal. */
79
+ async function bootWith(
80
+ config: FixtureGraphOptions["config"],
81
+ ): Promise<FixtureGraph> {
82
+ const fixture = await bootFixtureRuntime({ ttl: CANONICAL_TTL, config });
83
+ fixtures.push(fixture);
84
+ return fixture;
85
+ }
86
+
87
+ async function blockListNames(fixture: FixtureGraph): Promise<string[]> {
45
88
  const out = await executeVerb(
46
89
  listVerb,
47
- { allTiers },
90
+ {},
48
91
  NO_MUTATION,
49
92
  bootRuntime(JSON_FLAGS, fixture.cwd),
50
93
  );
@@ -52,80 +95,78 @@ async function blockListNames(
52
95
  return rows.map((r) => r.name).sort();
53
96
  }
54
97
 
55
- const fixtures: FixtureGraph[] = [];
56
- afterAll(async () => {
57
- await Promise.all(fixtures.map((f) => f.dispose()));
58
- });
98
+ describe("block list — the declared, unfiltered list (B9, in-process CLI dispatch)", () => {
99
+ it("with no tier configured, lists every block in the store", async () => {
100
+ expect(await blockListNames(await bootWith(CANONICAL_CONFIG))).toEqual(
101
+ EVERY_BLOCK,
102
+ );
103
+ });
59
104
 
60
- describe("block list — tier-chain inheritance (B9, in-process CLI dispatch)", () => {
61
- it("unscoped (no tier configured) sees every tier", async () => {
62
- const fixture = await bootFixtureRuntime({
63
- ttl: CANONICAL_TTL,
64
- config: CANONICAL_CONFIG, // channel: normal, no tier set
65
- });
66
- fixtures.push(fixture);
67
- // normal channel drops the beta-only block; no tier set -> every tier.
68
- expect(await blockListNames(fixture)).toEqual([
69
- "Button",
70
- "LXD Panel",
71
- "Modal",
72
- ]);
105
+ it("a configured tier does NOT narrow the list — tier scopes nothing", async () => {
106
+ // The old verb answered ["Button", "Modal"] here: `global` excluded the
107
+ // apps/lxd block, and the required `ds:tier` join hid the untiered
108
+ // subcomponent. Nothing filters now, so the tier config is inert.
109
+ const fixture = await bootWith({ tier: "global", channel: "normal" });
110
+ expect(await blockListNames(fixture)).toEqual(EVERY_BLOCK);
73
111
  });
74
112
 
75
- it("scoped to the global tier excludes the apps/lxd-only block", async () => {
76
- const fixture = await bootFixtureRuntime({
77
- ttl: CANONICAL_TTL,
78
- config: { tier: "global", channel: "normal" },
79
- });
80
- fixtures.push(fixture);
81
- expect(await blockListNames(fixture)).toEqual(["Button", "Modal"]);
113
+ it("the deepest tier in the chain gets the same answer as the shallowest", async () => {
114
+ // Tier-chain inheritance was the point of the hand-written query
115
+ // (`apps/lxd` -> [global, apps, apps/lxd]). With no filtering there is no
116
+ // chain to walk, so `apps/lxd` and `global` are indistinguishable.
117
+ expect(await blockListNames(await bootWith(FILTERED_CONFIG))).toEqual(
118
+ EVERY_BLOCK,
119
+ );
82
120
  });
121
+ });
83
122
 
84
- it("scoped to apps/lxd includes its OWN block plus the inherited chain", async () => {
85
- const fixture = await bootFixtureRuntime({
86
- ttl: CANONICAL_TTL,
87
- config: { tier: "apps/lxd", channel: "normal" },
88
- });
89
- fixtures.push(fixture);
90
- // resolveTierChain("apps/lxd") = [global, apps, apps/lxd] — Button/Modal
91
- // (global) are inherited; LXD Panel is the tier's own block.
92
- expect(await blockListNames(fixture)).toEqual([
93
- "Button",
94
- "LXD Panel",
95
- "Modal",
96
- ]);
123
+ describe("block list — channel visibility is gone too (B9)", () => {
124
+ it("the `normal` channel shows the beta-gated block — the signed-off consequence", async () => {
125
+ // `ds:betaWidget` carries `ds:release ds:beta`. The hand-written verb hid it
126
+ // from every channel but `prerelease`; the declared list shows it to
127
+ // everyone. This is the owner-signed visible consequence of L-OPEN-9,
128
+ // pinned so it stays a decision on record rather than a silent regression.
129
+ const names = await blockListNames(await bootWith(CANONICAL_CONFIG));
130
+ expect(names).toContain("Beta Widget");
97
131
  });
98
132
 
99
- it("--all-tiers ignores a configured tier scope", async () => {
100
- const fixture = await bootFixtureRuntime({
101
- ttl: CANONICAL_TTL,
102
- config: { tier: "global", channel: "normal" },
103
- });
104
- fixtures.push(fixture);
105
- // Scoped (no flag) excludes LXD Panel; --all-tiers restores it AND reveals
106
- // the untiered Button Icon subcomponent the scoped view omits (A2).
107
- expect(await blockListNames(fixture, false)).toEqual(["Button", "Modal"]);
108
- expect(await blockListNames(fixture, true)).toEqual([
109
- "Button",
110
- "Button Icon",
111
- "LXD Panel",
112
- "Modal",
113
- ]);
133
+ it("`prerelease` — once the only config that saw everything — is now unremarkable", async () => {
134
+ expect(await blockListNames(await bootWith(ALL_VISIBLE_CONFIG))).toEqual(
135
+ EVERY_BLOCK,
136
+ );
114
137
  });
115
138
  });
116
139
 
117
- describe("block list — channel visibility (B9)", () => {
118
- it("prerelease channel is the one config where all 4 components list", async () => {
119
- const fixture = await bootFixtureRuntime({
120
- ttl: CANONICAL_TTL,
121
- config: { channel: "prerelease" },
122
- });
123
- fixtures.push(fixture);
124
- expect(await blockListNames(fixture)).toEqual([
125
- "Beta Widget",
126
- "Button",
127
- "LXD Panel",
128
- "Modal",
140
+ describe("block list — the row shape the declared columns emit (B9)", () => {
141
+ it("derives name/type/tier in SPARQL, and leaves an untiered block's tier unset", async () => {
142
+ const fixture = await bootWith(CANONICAL_CONFIG);
143
+ const out = await executeVerb(
144
+ listVerb,
145
+ {},
146
+ NO_MUTATION,
147
+ bootRuntime(JSON_FLAGS, fixture.cwd),
148
+ );
149
+ const rows = JSON.parse(out.stdout as string).data as {
150
+ name: string;
151
+ type?: string;
152
+ tier?: string;
153
+ uri: string;
154
+ modifiers?: string;
155
+ }[];
156
+ const byName = new Map(rows.map((row) => [row.name, row]));
157
+
158
+ // `type` is the LOWERCASED local name of the matched class — the BIND that
159
+ // replaced the hand-written `normalizeType`.
160
+ expect(byName.get("Button")?.type).toBe("component");
161
+ expect(byName.get("Button Icon")?.type).toBe("subcomponent");
162
+ // `tier` is the tier IRI's local name; an untiered block simply has none.
163
+ expect(byName.get("Button")?.tier).toBe("global");
164
+ expect(byName.get("Button Icon")?.tier).toBeUndefined();
165
+ expect(byName.get("Button")?.uri).toBe("https://ds.canonical.com/button");
166
+ // GROUP_CONCAT of the modifier family names (order-independent).
167
+ expect(byName.get("Button")?.modifiers?.split(", ").sort()).toEqual([
168
+ "density",
169
+ "importance",
129
170
  ]);
130
171
  });
131
172
  });