@canonical/pragma-cli 0.35.0 → 0.36.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 (230) hide show
  1. package/README.md +2 -2
  2. package/bundled-skills/add-standard/SKILL.md +338 -0
  3. package/bundled-skills/adoption-a1-styles/SKILL.md +269 -0
  4. package/bundled-skills/adoption-a2-components/SKILL.md +291 -0
  5. package/bundled-skills/adoption-a3-forms/SKILL.md +260 -0
  6. package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +406 -0
  7. package/bundled-skills/anatomy-author/SKILL.md +1226 -0
  8. package/bundled-skills/design-auditor/SKILL.md +455 -0
  9. package/bundled-skills/implement-component/SKILL.md +333 -0
  10. package/bundled-skills/specify-component/SKILL.md +331 -0
  11. package/bundled-skills/specify-pattern/SKILL.md +292 -0
  12. package/dist/package.json +16 -15
  13. package/dist/pragma.conf.js +236 -34
  14. package/dist/pragma.conf.js.map +1 -1
  15. package/dist/src/bin.js +1 -1
  16. package/dist/src/bin.js.map +1 -1
  17. package/dist/src/capabilities/colophon/colophon.render.js +1 -1
  18. package/dist/src/capabilities/colophon/colophon.render.js.map +1 -1
  19. package/dist/src/capabilities/create/mount.js +4 -3
  20. package/dist/src/capabilities/create/mount.js.map +1 -1
  21. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js +60 -4
  22. package/dist/src/capabilities/doctor/checks/checkPackageRefs.js.map +1 -1
  23. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js +8 -8
  24. package/dist/src/capabilities/doctor/checks/checkShellCompletions.js.map +1 -1
  25. package/dist/src/capabilities/doctor/checks/harnessInventory.js +195 -0
  26. package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -0
  27. package/dist/src/capabilities/doctor/checks/index.js +6 -6
  28. package/dist/src/capabilities/doctor/checks/targetHealth.js +194 -50
  29. package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
  30. package/dist/src/capabilities/doctor/doctor.render.js +43 -64
  31. package/dist/src/capabilities/doctor/doctor.render.js.map +1 -1
  32. package/dist/src/capabilities/doctor/doctor.verb.js +6 -2
  33. package/dist/src/capabilities/doctor/doctor.verb.js.map +1 -1
  34. package/dist/src/capabilities/doctor/runChecks.js +14 -11
  35. package/dist/src/capabilities/doctor/runChecks.js.map +1 -1
  36. package/dist/src/capabilities/graph/query.render.js +2 -2
  37. package/dist/src/capabilities/graph/query.render.js.map +1 -1
  38. package/dist/src/capabilities/info/collectInfo.js +8 -2
  39. package/dist/src/capabilities/info/collectInfo.js.map +1 -1
  40. package/dist/src/capabilities/info/info.render.js +6 -2
  41. package/dist/src/capabilities/info/info.render.js.map +1 -1
  42. package/dist/src/capabilities/prompt/prompt.render.js +2 -2
  43. package/dist/src/capabilities/prompt/prompt.render.js.map +1 -1
  44. package/dist/src/capabilities/setup/buildPlan.js +43 -39
  45. package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
  46. package/dist/src/capabilities/setup/mode.js +2 -2
  47. package/dist/src/capabilities/setup/operations/index.js +4 -4
  48. package/dist/src/capabilities/setup/operations/index.js.map +1 -1
  49. package/dist/src/capabilities/setup/operations/setupGenerator.js +92 -45
  50. package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
  51. package/dist/src/capabilities/setup/operations/setupLsp.js +88 -36
  52. package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
  53. package/dist/src/capabilities/setup/operations/setupMcp.js +97 -51
  54. package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
  55. package/dist/src/capabilities/setup/operations/setupSkills.js +325 -47
  56. package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
  57. package/dist/src/capabilities/setup/plan.js.map +1 -1
  58. package/dist/src/capabilities/setup/plan.render.js +144 -66
  59. package/dist/src/capabilities/setup/plan.render.js.map +1 -1
  60. package/dist/src/capabilities/setup/setup.render.js +1 -1
  61. package/dist/src/capabilities/setup/setup.verb.js +86 -33
  62. package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
  63. package/dist/src/capabilities/setup/targets.js +110 -45
  64. package/dist/src/capabilities/setup/targets.js.map +1 -1
  65. package/dist/src/capabilities/shared/index.js +6 -5
  66. package/dist/src/capabilities/shared/index.js.map +1 -1
  67. package/dist/src/capabilities/shared/packageManager.js +300 -54
  68. package/dist/src/capabilities/shared/packageManager.js.map +1 -1
  69. package/dist/src/capabilities/skill/discover.js +97 -11
  70. package/dist/src/capabilities/skill/discover.js.map +1 -1
  71. package/dist/src/capabilities/skill/render.js +2 -2
  72. package/dist/src/capabilities/skill/render.js.map +1 -1
  73. package/dist/src/capabilities/sources/installSkills.js +145 -3
  74. package/dist/src/capabilities/sources/installSkills.js.map +1 -1
  75. package/dist/src/capabilities/sources/runUpdate.js +68 -2
  76. package/dist/src/capabilities/sources/runUpdate.js.map +1 -1
  77. package/dist/src/capabilities/sources/update.verb.js +3 -3
  78. package/dist/src/capabilities/sources/update.verb.js.map +1 -1
  79. package/dist/src/capabilities/upgrade/runUpgrade.js +43 -7
  80. package/dist/src/capabilities/upgrade/runUpgrade.js.map +1 -1
  81. package/dist/src/capabilities/upgrade/upgrade.render.js +10 -2
  82. package/dist/src/capabilities/upgrade/upgrade.render.js.map +1 -1
  83. package/dist/src/constants.js +6 -1
  84. package/dist/src/constants.js.map +1 -1
  85. package/dist/src/kernel/index.js +11 -2
  86. package/dist/src/kernel/index.js.map +1 -1
  87. package/dist/src/kernel/interactivity.js +68 -0
  88. package/dist/src/kernel/interactivity.js.map +1 -0
  89. package/dist/src/kernel/packs/renderPack.js +3 -3
  90. package/dist/src/kernel/packs/renderPack.js.map +1 -1
  91. package/dist/src/kernel/packs/resolveEntity.js +63 -12
  92. package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
  93. package/dist/src/kernel/packs/runBodies.js +57 -4
  94. package/dist/src/kernel/packs/runBodies.js.map +1 -1
  95. package/dist/src/kernel/packs/schema.js +54 -2
  96. package/dist/src/kernel/packs/schema.js.map +1 -1
  97. package/dist/src/kernel/packs/sparql/applyFilters.js +104 -5
  98. package/dist/src/kernel/packs/sparql/applyFilters.js.map +1 -1
  99. package/dist/src/kernel/packs/sparql/buildLookupQuery.js +306 -43
  100. package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
  101. package/dist/src/kernel/packs/types.js.map +1 -1
  102. package/dist/src/kernel/project/cli/buildProgram.js +1 -1
  103. package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
  104. package/dist/src/kernel/project/cli/dispatch.js +23 -23
  105. package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
  106. package/dist/src/kernel/project/cli/globalFlags.js +4 -1
  107. package/dist/src/kernel/project/cli/globalFlags.js.map +1 -1
  108. package/dist/src/kernel/project/cli/helpFormat.js +1 -2
  109. package/dist/src/kernel/project/cli/helpFormat.js.map +1 -1
  110. package/dist/src/kernel/project/cli/rootHelp.js +34 -4
  111. package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
  112. package/dist/src/kernel/project/mcp/registerVerb.js +16 -1
  113. package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
  114. package/dist/src/kernel/render/renderers.js +1 -1
  115. package/dist/src/kernel/render/style.js +29 -9
  116. package/dist/src/kernel/render/style.js.map +1 -1
  117. package/dist/src/kernel/render/vocabulary.js +174 -0
  118. package/dist/src/kernel/render/vocabulary.js.map +1 -0
  119. package/dist/src/kernel/spec/emitReference.js +1 -1
  120. package/dist/src/kernel/spec/emitReference.js.map +1 -1
  121. package/package.json +16 -15
  122. package/pragma.conf.ts +241 -34
  123. package/src/bin.ts +8 -1
  124. package/src/capabilities/block.parity.test.ts +25 -11
  125. package/src/capabilities/block.shipped.exec.test.ts +282 -0
  126. package/src/capabilities/block.tierRank.exec.test.ts +300 -0
  127. package/src/capabilities/colophon/colophon.render.ts +1 -2
  128. package/src/capabilities/colophon/colophon.test.ts +2 -2
  129. package/src/capabilities/create/createGrammar.test.ts +1 -1
  130. package/src/capabilities/create/mount.ts +3 -3
  131. package/src/capabilities/distribution.parity.test.ts +4 -0
  132. package/src/capabilities/doctor/checks/checkPackageRefs.ts +69 -4
  133. package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +6 -4
  134. package/src/capabilities/doctor/checks/checkShellCompletions.ts +8 -8
  135. package/src/capabilities/doctor/checks/harnessInventory.test.ts +219 -0
  136. package/src/capabilities/doctor/checks/harnessInventory.ts +298 -0
  137. package/src/capabilities/doctor/checks/index.ts +6 -6
  138. package/src/capabilities/doctor/checks/targetHealth.ts +229 -59
  139. package/src/capabilities/doctor/doctor.render.test.ts +108 -32
  140. package/src/capabilities/doctor/doctor.render.ts +56 -80
  141. package/src/capabilities/doctor/doctor.test.ts +247 -35
  142. package/src/capabilities/doctor/doctor.verb.ts +7 -2
  143. package/src/capabilities/doctor/runChecks.ts +14 -11
  144. package/src/capabilities/doctor/types.ts +15 -24
  145. package/src/capabilities/emptyRecovery.test.ts +10 -2
  146. package/src/capabilities/graph/query.render.ts +2 -2
  147. package/src/capabilities/info/collectInfo.ts +8 -1
  148. package/src/capabilities/info/info.render.ts +6 -2
  149. package/src/capabilities/info/info.test.ts +37 -1
  150. package/src/capabilities/info/types.ts +10 -2
  151. package/src/capabilities/prompt/prompt.render.ts +2 -2
  152. package/src/capabilities/renderVocabularySync.test.ts +82 -0
  153. package/src/capabilities/scopeTypeSync.test.ts +29 -15
  154. package/src/capabilities/setup/buildPlan.ts +45 -41
  155. package/src/capabilities/setup/index.ts +1 -1
  156. package/src/capabilities/setup/mode.ts +2 -2
  157. package/src/capabilities/setup/operations/index.ts +6 -1
  158. package/src/capabilities/setup/operations/setupGenerator.ts +144 -70
  159. package/src/capabilities/setup/operations/setupLsp.ts +129 -37
  160. package/src/capabilities/setup/operations/setupMcp.ts +132 -61
  161. package/src/capabilities/setup/operations/setupSkills.ts +392 -58
  162. package/src/capabilities/setup/plan.render.ts +171 -76
  163. package/src/capabilities/setup/plan.ts +17 -12
  164. package/src/capabilities/setup/setup.mcp.correctness.test.ts +9 -9
  165. package/src/capabilities/setup/setup.render.test.ts +50 -36
  166. package/src/capabilities/setup/setup.render.ts +1 -1
  167. package/src/capabilities/setup/setup.test.ts +881 -50
  168. package/src/capabilities/setup/setup.verb.ts +91 -33
  169. package/src/capabilities/setup/targets.ts +120 -52
  170. package/src/capabilities/setup/types.ts +10 -18
  171. package/src/capabilities/shared/index.ts +17 -6
  172. package/src/capabilities/shared/packageManager.test.ts +421 -6
  173. package/src/capabilities/shared/packageManager.ts +394 -59
  174. package/src/capabilities/skill/bundledSkills.test.ts +347 -0
  175. package/src/capabilities/skill/discover.ts +100 -11
  176. package/src/capabilities/skill/render.ts +2 -2
  177. package/src/capabilities/skill/skill.test.ts +17 -4
  178. package/src/capabilities/sources/installSkills.ts +181 -3
  179. package/src/capabilities/sources/runUpdate.ts +78 -2
  180. package/src/capabilities/sources/sources.test.ts +228 -0
  181. package/src/capabilities/sources/update.verb.ts +3 -3
  182. package/src/capabilities/surface.test.ts +2 -2
  183. package/src/capabilities/upgrade/runUpgrade.ts +45 -6
  184. package/src/capabilities/upgrade/types.ts +10 -2
  185. package/src/capabilities/upgrade/upgrade.render.ts +9 -2
  186. package/src/capabilities/upgrade/upgrade.test.ts +72 -0
  187. package/src/constants.ts +6 -1
  188. package/src/identity.test.ts +18 -5
  189. package/src/kernel/completion/entitySource.test.ts +15 -2
  190. package/src/kernel/completion/safety.test.ts +16 -9
  191. package/src/kernel/config/defaults.test.ts +2 -1
  192. package/src/kernel/index.ts +11 -2
  193. package/src/kernel/interactivity.test.ts +76 -0
  194. package/src/kernel/interactivity.ts +69 -0
  195. package/src/kernel/packs/compile.test.ts +92 -0
  196. package/src/kernel/packs/renderPack.ts +3 -3
  197. package/src/kernel/packs/resolveEntity.test.ts +75 -14
  198. package/src/kernel/packs/resolveEntity.ts +88 -24
  199. package/src/kernel/packs/runBodies.ts +84 -19
  200. package/src/kernel/packs/schema.ts +60 -4
  201. package/src/kernel/packs/sparql/applyFilters.ts +125 -4
  202. package/src/kernel/packs/sparql/buildLookupQuery.test.ts +290 -0
  203. package/src/kernel/packs/sparql/buildLookupQuery.ts +323 -45
  204. package/src/kernel/packs/toolDescription.test.ts +35 -1
  205. package/src/kernel/packs/types.ts +125 -2
  206. package/src/kernel/project/cli/buildProgram.test.ts +73 -2
  207. package/src/kernel/project/cli/buildProgram.ts +8 -1
  208. package/src/kernel/project/cli/dispatch.test.ts +5 -4
  209. package/src/kernel/project/cli/dispatch.ts +25 -24
  210. package/src/kernel/project/cli/globalFlags.ts +4 -1
  211. package/src/kernel/project/cli/helpFormat.ts +1 -2
  212. package/src/kernel/project/cli/rootHelp.ts +36 -3
  213. package/src/kernel/project/mcp/registerVerb.ts +20 -1
  214. package/src/kernel/render/renderers.ts +1 -1
  215. package/src/kernel/render/style.ts +32 -9
  216. package/src/kernel/render/vocabulary.ts +276 -0
  217. package/src/kernel/spec/emitReference.ts +1 -1
  218. package/src/kernel/spec/types.ts +35 -6
  219. package/src/testing/PARITY_GAPS.ts +9 -3
  220. package/src/testing/behavioral/docExamples.test.ts +4 -2
  221. package/src/testing/behavioral/errorMatrix.mcp.test.ts +110 -7
  222. package/src/testing/behavioral/journeys.firstInstall.test.ts +7 -2
  223. package/src/testing/behavioral/journeys.packageStories.test.ts +6 -3
  224. package/src/testing/eval/__snapshots__/eval.test.ts.snap +26 -1
  225. package/src/testing/eval/cases/readNouns.ts +245 -9
  226. package/src/testing/fixtures/blockGraph.ts +28 -13
  227. package/src/testing/fixtures/graph/canonical.ts +95 -12
  228. package/dist/src/capabilities/shared/bands.js +0 -16
  229. package/dist/src/capabilities/shared/bands.js.map +0 -1
  230. package/src/capabilities/shared/bands.ts +0 -16
package/pragma.conf.ts CHANGED
@@ -97,8 +97,30 @@ const designSystemStories: readonly PackDefinition[] = [
97
97
  // Base level mirrors the old summary view (name/tier/summary); the default is
98
98
  // `detailed`, matching the old CLI which rendered anatomy and modifiers
99
99
  // without a flag. A derived name that maps onto no schema field is omitted
100
- // (OPTIONAL parity), so a graph lacking whenToUse/whenNotToUse degrades
101
- // gracefully.
100
+ // (OPTIONAL parity), so a graph missing one of these properties degrades
101
+ // gracefully rather than erroring.
102
+ //
103
+ // That graceful degradation is also how this story went silent: it used to
104
+ // read `ds:whenToUse`/`ds:whenNotToUse`, which the ontology RETIRED in favour
105
+ // of a single `ds:usage` — "Subsumes the former whenToUse/whenNotToUse" is
106
+ // that property's own skos:definition. The shipped pack asserts `ds:usage` on
107
+ // all 264 blocks and neither retired term on any of them, so every
108
+ // `block lookup` on every install rendered no usage narrative at all —
109
+ // silently, because a name that maps onto no schema field is exactly the case
110
+ // OPTIONAL parity swallows. The retired terms are NOT kept as a fallback:
111
+ // `ds:usage` subsumes them, so a pack carrying both would print the same
112
+ // guidance twice, and a declaration no shipped graph can satisfy is
113
+ // untestable by construction — which is what let the silence ship.
114
+ // `block.shipped.exec.test.ts` now holds every graph term declared here — the
115
+ // identity property, fields, sections, expand relations, and every term
116
+ // selected beneath them, walked recursively — to being DEFINED as a property
117
+ // by the SHIPPED ontology. Defined, not asserted, and the difference is the
118
+ // whole point: a term the ontology never defines is a story bug that can
119
+ // never render, on any block, on any install — this defect. A term it defines
120
+ // that no instance asserts yet (`ds:figmaLink` today) is a content gap
121
+ // upstream, which the OPTIONAL parity above degrades over gracefully and
122
+ // which this pack has no business failing on. `ds:usage` alone is ALSO held
123
+ // to being asserted, on every block, because its absence is what shipped.
102
124
  //
103
125
  // Disclosure declares the FULL canonical ladder `[summary, standard,
104
126
  // detailed]` — the same set `standard` declares — so a config
@@ -121,14 +143,33 @@ const designSystemStories: readonly PackDefinition[] = [
121
143
  " VALUES ?class { ds:Component ds:Pattern ds:Layout ds:Subcomponent }",
122
144
  " ?uri a ?class .",
123
145
  " OPTIONAL { ?uri ds:name ?dsName }",
124
- " OPTIONAL { ?uri ds:tier ?tierUri }",
146
+ " OPTIONAL { ?uri ds:tier ?tierUri . OPTIONAL { ?tierUri ds:name ?tierName } }",
125
147
  " OPTIONAL { ?uri ds:hasModifierFamily ?family . ?family ds:name ?modName }",
126
148
  ' BIND(COALESCE(?dsName, REPLACE(STR(?uri), "^.*[/#]", "")) AS ?name)',
127
149
  ' BIND(LCASE(REPLACE(STR(?class), "^.*[/#]", "")) AS ?type)',
128
150
  ' BIND(REPLACE(STR(?tierUri), "^.*[/#]", "") AS ?tier)',
151
+ // `ORDER BY ?name` alone is not a total order, and 25 names here are
152
+ // shared by two or three blocks. SPARQL says nothing about tied rows,
153
+ // so the store's scan decided: ONE run gave `Button` launchpad-first
154
+ // and `CheckboxInput` global-first. Tier depth, then `STR(?uri)`,
155
+ // makes it total — the shallower tier leads its name group, and equal
156
+ // depths fall back to a key that cannot tie.
157
+ //
158
+ // The depth is the same one `lookup` ranks by, spelled out because a
159
+ // list query is declared TEXT the kernel does not compose. Only the
160
+ // depth: a browse ordered by NAME is not the place to re-litigate which
161
+ // block the name MEANS (that is `lookup`'s ranking, which also weighs
162
+ // the block's type), and every row here already prints its own type.
163
+ //
164
+ // The depth-0 IF is the store's, not style: oxigraph raises on
165
+ // `0.2 * 0`, and a raising BIND leaves `?tierRank` unbound — which is
166
+ // exactly how a top-tier block silently sorted last (see
167
+ // `sparqlProduct` in kernel/packs/sparql/buildLookupQuery.ts).
168
+ ' BIND(STRLEN(?tierName) - STRLEN(REPLACE(?tierName, "/", "")) AS ?tierDepth)',
169
+ " BIND(COALESCE(IF(?tierDepth = 0, 1, 1 - (0.2 * ?tierDepth)), 0) AS ?tierRank)",
129
170
  "}",
130
- "GROUP BY ?uri ?name ?type ?tier",
131
- "ORDER BY ?name",
171
+ "GROUP BY ?uri ?name ?type ?tier ?tierRank",
172
+ "ORDER BY ?name DESC(?tierRank) STR(?uri)",
132
173
  ].join("\n"),
133
174
  columns: [
134
175
  { field: "name", label: "Name" },
@@ -153,9 +194,41 @@ const designSystemStories: readonly PackDefinition[] = [
153
194
  // for the query "button" the 86 `…-close_button` subcomponents used to
154
195
  // outrank `ds:global.component.button` on the alphabet alone. Weighting
155
196
  // them below 1 sinks every one of them under every component at equal
156
- // match score, and lowers their `annotations.priority` in the resource
157
- // listing by the same declaration.
197
+ // match score, lowers their `annotations.priority` in the resource
198
+ // listing, and — since a name resolve now RANKS rather than picks — sorts
199
+ // them under every component a shared name also reaches.
158
200
  weights: { "ds:Subcomponent": 0.6 },
201
+ // The other half of that judgement, and the reason it is a PRODUCT and
202
+ // not a tiebreak. 25 live block names are shared by two or three blocks
203
+ // across tiers, and `weights` alone cannot separate two components; the
204
+ // alphabet decided, so `Button` answered with Launchpad's and never
205
+ // mentioned the global one. A tier's DEPTH is what ranks it: `Global` is
206
+ // depth 1 and worth 1, `Apps/Launchpad` is depth 2 and worth 0.8, so
207
+ //
208
+ // Button global component 1 × 1 > launchpad component 1 × 0.8
209
+ // TextInput global SUBcomponent 0.6 × 1 < launchpad component 1 × 0.8
210
+ //
211
+ // — the global block wins where both are whole blocks, and the editorial
212
+ // rule that a whole component beats a part survives the addition. A pure
213
+ // tier tiebreak would have inverted the second case.
214
+ //
215
+ // DERIVED, never enumerated: the depth is the `/` count in the tier's
216
+ // OWN `ds:name` (`"Apps/Launchpad"`), not in its IRI (`ds:apps_launchpad`
217
+ // — the slash exists only in the name), so a tier added upstream tomorrow
218
+ // is ranked correctly without editing anything here.
219
+ //
220
+ // Declared as DATA beside `weights` for the same reason `weights` is:
221
+ // editorial judgement the ontology has not yet made belongs in the config
222
+ // layer, which "wins every harvest" when upstream is silent. `asserted`
223
+ // is the exit: the day `ds:tierRank` is asserted upstream it takes
224
+ // precedence over the derived depth (`COALESCE`), and this whole entry is
225
+ // deleted with no code change.
226
+ scopeWeight: {
227
+ via: "ds:tier",
228
+ by: "ds:name",
229
+ falloff: 0.2,
230
+ asserted: "ds:tierRank",
231
+ },
159
232
  graphqlType: "UIBlock",
160
233
  fields: [
161
234
  { name: "tier", property: "ds:tier", label: "Tier" },
@@ -168,16 +241,19 @@ const designSystemStories: readonly PackDefinition[] = [
168
241
  ],
169
242
  sections: [
170
243
  { name: "summary", property: "ds:summary", label: "Summary" },
244
+ // ONE section, because `ds:usage` is ONE property: its literal is
245
+ // free-text Markdown carrying its own `### When to use` / `### When not
246
+ // to use` sub-sections (78 and 71 of the 126 non-empty ones do), so
247
+ // splitting it back into two headings here would mean parsing prose the
248
+ // graph deliberately keeps whole. The renderer demotes a body's own ATX
249
+ // headings below the section heading, so those sub-sections nest UNDER
250
+ // `### Usage` instead of colliding with it. Half the blocks assert an
251
+ // EMPTY `ds:usage` (138 of 264); the renderer skips empty sections, so
252
+ // those print no heading rather than an empty one.
171
253
  {
172
- name: "whenToUse",
173
- property: "ds:whenToUse",
174
- label: "When to use",
175
- level: "detailed",
176
- },
177
- {
178
- name: "whenNotToUse",
179
- property: "ds:whenNotToUse",
180
- label: "When not to use",
254
+ name: "usage",
255
+ property: "ds:usage",
256
+ label: "Usage",
181
257
  level: "detailed",
182
258
  },
183
259
  {
@@ -246,7 +322,7 @@ const designSystemStories: readonly PackDefinition[] = [
246
322
  sample: {
247
323
  fixedCount: true,
248
324
  toolDescription:
249
- "Return randomly selected complete design-system blocks as exemplars. Use BEFORE writing queries to see actual data shapes, anatomy, and property names.",
325
+ "Return randomly selected complete design-system blocks as exemplars. Use BEFORE writing queries to see actual data shapes, anatomy, and property names. Example: block_sample {}.",
250
326
  },
251
327
  },
252
328
  },
@@ -303,7 +379,7 @@ const designSystemStories: readonly PackDefinition[] = [
303
379
  sample: {
304
380
  fixedCount: true,
305
381
  toolDescription:
306
- "Return randomly selected complete design tokens (with theme values) as exemplars. Use BEFORE writing queries to see actual data shapes.",
382
+ "Return randomly selected complete design tokens (with theme values) as exemplars. Use BEFORE writing queries to see actual data shapes. Example: token_sample {}.",
307
383
  },
308
384
  },
309
385
  },
@@ -361,7 +437,7 @@ const designSystemStories: readonly PackDefinition[] = [
361
437
  sample: {
362
438
  fixedCount: true,
363
439
  toolDescription:
364
- "Return randomly selected complete modifier families (with value lists) as exemplars. Use BEFORE writing queries to see actual data shapes.",
440
+ "Return randomly selected complete modifier families (with value lists) as exemplars. Use BEFORE writing queries to see actual data shapes. Example: modifier_sample {}.",
365
441
  },
366
442
  },
367
443
  },
@@ -421,7 +497,7 @@ const conceptStory: PackDefinition = {
421
497
  noun: "concept",
422
498
  description: "List design-system concepts.",
423
499
  toolDescription:
424
- "List design-system concepts — long-form foundations, how-to guides, and decision guides not bound to a single UI block. Optionally filter by type or search.",
500
+ 'List design-system concepts — long-form foundations, how-to guides, and decision guides not bound to a single UI block. Optionally filter by type or search. Example: concept_list { type: "Explanation" }.',
425
501
  list: {
426
502
  query: [
427
503
  "SELECT ?uri ?name ?type ?summary",
@@ -443,6 +519,20 @@ const conceptStory: PackDefinition = {
443
519
  {
444
520
  param: "type",
445
521
  variable: "type",
522
+ // `ds:ConceptType` is the vocabulary; the concepts are the population.
523
+ // The shipped graph declares six types and uses two, so validating
524
+ // against the rows rejected "Decision guide" — a type the ontology
525
+ // declares — as an invalid argument instead of answering with the
526
+ // empty list it actually has.
527
+ vocabulary: {
528
+ query: [
529
+ "SELECT DISTINCT ?name WHERE {",
530
+ " ?conceptType a ds:ConceptType ;",
531
+ " ds:name ?name .",
532
+ "}",
533
+ ].join("\n"),
534
+ variable: "name",
535
+ },
446
536
  description: "Filter by concept type (e.g. Explanation, How-to guide).",
447
537
  },
448
538
  ],
@@ -463,7 +553,7 @@ const conceptStory: PackDefinition = {
463
553
  description:
464
554
  "Look up a concept's full documentation by name, IRI, or glob.",
465
555
  toolDescription:
466
- "Get a design-system concept's full Markdown documentation. Address concepts by name, prefixed name (ds:concept.…), absolute IRI, or glob pattern.",
556
+ 'Get a design-system concept\'s full Markdown documentation. Address concepts by the name concept_list publishes, by prefixed name (ds:concept.…), by absolute IRI, or by a glob. Example: concept_lookup { name: ["Foundations: Grid"] }.',
467
557
  fields: [
468
558
  { name: "type", property: "ds:conceptType/ds:name", label: "Type" },
469
559
  { name: "tier", property: "ds:tier", label: "Tier" },
@@ -544,14 +634,34 @@ const implementationStory: PackDefinition = {
544
634
  { field: "uri", label: "IRI" },
545
635
  ],
546
636
  filters: [
637
+ // Both vocabularies are read off `ds:ImplementationLibrary`, the subject
638
+ // that carries them — the same terms `implementation libraries`
639
+ // enumerates. A library that implements nothing yet still exists, and
640
+ // asking for it is an empty answer rather than a bad argument.
547
641
  {
548
642
  param: "platform",
549
643
  variable: "platform",
644
+ vocabulary: {
645
+ query: [
646
+ "SELECT DISTINCT ?platform WHERE {",
647
+ " ?lib a ds:ImplementationLibrary ;",
648
+ " ds:platform ?platform .",
649
+ "}",
650
+ ].join("\n"),
651
+ },
550
652
  description: "Filter by platform (e.g. react, svelte, typescript).",
551
653
  },
552
654
  {
553
655
  param: "library",
554
656
  variable: "library",
657
+ vocabulary: {
658
+ query: [
659
+ "SELECT DISTINCT ?library WHERE {",
660
+ " ?lib a ds:ImplementationLibrary ;",
661
+ " ds:libraryName ?library .",
662
+ "}",
663
+ ].join("\n"),
664
+ },
555
665
  description: "Filter by implementation library name.",
556
666
  },
557
667
  ],
@@ -618,20 +728,44 @@ const codeStandardsStories: readonly PackDefinition[] = [
618
728
  noun: "standard",
619
729
  description: "List all code standards.",
620
730
  toolDescription:
621
- "List code standards. Optionally filter by category or search term.",
731
+ 'List code standards: one ROW per standard — its IRI, name, category and description — not the standards themselves. Take a row\'s `name` VERBATIM to standard_lookup for the dos and donts. Optionally filter by category slug (a parent slug answers for its whole branch; standard_categories lists them) or by search term. Example: standard_list { category: "react" }.',
622
732
  list: {
623
733
  query: [
734
+ // `?category` is the LEAF a standard is filed under — what a row
735
+ // displays. `?categories` is that leaf plus every ancestor, which is
736
+ // what `--category` matches against, so asking for a parent answers for
737
+ // the whole branch. The traversal is written ONCE, here, rather than
738
+ // repeated by each consumer.
624
739
  "SELECT ?uri ?name ?category ?description",
740
+ ' (GROUP_CONCAT(DISTINCT ?ancestorSlug; SEPARATOR=" ") AS ?categories)',
625
741
  "WHERE {",
626
742
  " ?uri a cs:CodeStandard ;",
627
743
  " cs:description ?description .",
628
- " OPTIONAL { ?uri cs:name ?n . }",
744
+ // `rdfs:label`, matching `lookup.by` below. Both COALESCE over the
745
+ // SAME property with the SAME IRI fallback, and that agreement IS the
746
+ // two-step grammar: a row's `name` goes VERBATIM to lookup. Keyed on
747
+ // different properties they diverge for exactly the entities carrying
748
+ // one and not the other — 16 standards in the shipped snapshot
749
+ // published `Turtle local-name casing` from `cs:name` while lookup,
750
+ // reading `rdfs:label`, bound the derived slug and answered
751
+ // ENTITY_NOT_FOUND. Change one, change both.
752
+ " OPTIONAL { ?uri rdfs:label ?n . }",
629
753
  ' BIND(COALESCE(?n, REPLACE(STRAFTER(STR(?uri), "#"), "\\\\.", "/")) AS ?name)',
630
754
  " OPTIONAL {",
631
755
  " ?uri cs:hasCategory ?cat .",
632
756
  " ?cat cs:slug ?category .",
757
+ // `skos:broader` is deliberately NOT transitive, and this store
758
+ // (oxigraph) has no reasoner — so `skos:broaderTransitive` returns zero
759
+ // rows and hand-asserting it would be materialisation wearing a
760
+ // standards badge. `broader*` is the only thing that works.
761
+ // The `*` is REFLEXIVE and that is load-bearing: `+` would silently
762
+ // drop every standard filed DIRECTLY on the category being asked for
763
+ // (one of the 8 under `testing` today). Do not "correct" it.
764
+ " ?cat skos:broader* ?ancestor .",
765
+ " ?ancestor cs:slug ?ancestorSlug .",
633
766
  " }",
634
767
  "}",
768
+ "GROUP BY ?uri ?name ?category ?description",
635
769
  "ORDER BY ?name",
636
770
  ].join("\n"),
637
771
  columns: [
@@ -643,28 +777,64 @@ const codeStandardsStories: readonly PackDefinition[] = [
643
777
  filters: [
644
778
  {
645
779
  param: "category",
646
- variable: "category",
647
- description: "Filter by category name.",
780
+ variable: "categories",
781
+ match: "set",
782
+ // The SAME terms `standard categories` enumerates (`cs:Category` /
783
+ // `cs:slug`), so the slugs that surface hands out are exactly the
784
+ // slugs this filter accepts. Read from the graph rather than the
785
+ // returned ROWS: a category the graph declares with no standards
786
+ // filed under it is reported by `standard categories` with count 0,
787
+ // and asking for it must be the documented calm empty list, not
788
+ // INVALID_INPUT.
789
+ vocabulary: {
790
+ query: [
791
+ "SELECT DISTINCT ?slug WHERE {",
792
+ " ?cat a cs:Category ;",
793
+ " cs:slug ?slug .",
794
+ "}",
795
+ ].join("\n"),
796
+ variable: "slug",
797
+ },
798
+ description:
799
+ "Filter by category slug. A parent category answers for its whole branch.",
648
800
  },
649
801
  ],
650
802
  search: {
651
803
  variables: ["name", "description"],
652
804
  description: "Search in name and description.",
653
805
  },
806
+ // Deliberately NOT `sources update`: the code standards ship in the
807
+ // embedded snapshot and answer with no update at all, so the generic
808
+ // build hint is actively wrong here. An empty result on this noun means
809
+ // the filter, and the vocabulary it must be drawn from lives in the graph
810
+ // — which is what `standard categories` reads out.
811
+ emptyRecovery: {
812
+ message:
813
+ "Category slugs come from the graph, and a parent slug answers for its whole branch — read them out rather than guessing.",
814
+ cli: "standard categories",
815
+ },
654
816
  },
655
817
  verbs: [
656
818
  {
657
819
  verb: "categories",
658
- description: "List all standard categories with counts.",
659
- toolDescription: "List all code standard categories.",
820
+ description:
821
+ "List all standard categories with counts (a parent counts its whole branch).",
822
+ toolDescription:
823
+ "List all code standard categories with the number of standards each covers. Categories are a hierarchy: a parent's count includes every descendant, and `standard_list { category }` answers for the same set. Use this to pick a valid slug before filtering. Example: standard_categories {}.",
660
824
  query: [
661
- "SELECT ?name (COUNT(?standard) AS ?count)",
825
+ // The same reflexive roll-up `list` filters with — written the other
826
+ // way round (every category whose broader-chain reaches ?cat), so the
827
+ // count a category reports and the rows `--category` returns are the
828
+ // same set. `COUNT(DISTINCT ?standard)`: a standard reachable by two
829
+ // paths is still one standard.
830
+ "SELECT ?name (COUNT(DISTINCT ?standard) AS ?count)",
662
831
  "WHERE {",
663
832
  " ?cat a cs:Category ;",
664
833
  " cs:slug ?name .",
665
834
  " OPTIONAL {",
835
+ " ?descendant skos:broader* ?cat .",
666
836
  " ?standard a cs:CodeStandard ;",
667
- " cs:hasCategory ?cat .",
837
+ " cs:hasCategory ?descendant .",
668
838
  " }",
669
839
  "}",
670
840
  "GROUP BY ?name",
@@ -674,16 +844,43 @@ const codeStandardsStories: readonly PackDefinition[] = [
674
844
  { field: "name", label: "Category" },
675
845
  { field: "count", label: "Standards" },
676
846
  ],
847
+ // Also not `sources update`. The categories ride the same embedded
848
+ // snapshot, so zero rows means the store did not load — a health
849
+ // question, not a staleness one.
850
+ emptyRecovery: {
851
+ message:
852
+ "The code standards ship with the CLI itself, so no categories at all means the store did not load — not that it is out of date.",
853
+ cli: "doctor",
854
+ },
677
855
  },
678
856
  ],
679
857
  lookup: {
680
858
  source: "sparql",
681
- by: "cs:name",
859
+ // `rdfs:label`, not `cs:name`. The pack pin above moved to v0.1.5, which
860
+ // retired the bespoke `cs:name` for the standard property every RDF
861
+ // consumer already reads — measured against that tag: 0 standards carry
862
+ // `cs:name`, 148 of 148 carry exactly one `rdfs:label`. Left keyed on the
863
+ // retired property, this story would still ANSWER — every title would
864
+ // simply stop reaching a reader and each row would fall back to its
865
+ // IRI-derived name, with nothing raised. That is the same silent shape as
866
+ // reading a retired `ds:whenToUse`, one noun over.
867
+ by: "rdfs:label",
868
+ // The fallback stays, and is now near-inert rather than load-bearing: it
869
+ // covered the ~87% of standards that carried no display title at all, and
870
+ // v0.1.5 gives every one of them a curated label. It remains declared
871
+ // because a pack is data — a future release that drops a label should
872
+ // degrade to the IRI-derived name, not become unaddressable.
873
+ //
874
+ // Deliberately NOT declared on `block`/`token`/`tier`/`concept`: each of
875
+ // those lists REQUIRES its `by` property, so an entity without one is a
876
+ // row they never publish, and making it addressable (or sampleable) here
877
+ // would be the mirror of the defect this repairs.
878
+ nameFallback: "iri",
682
879
  type: "cs:CodeStandard",
683
880
  description:
684
- "Look up detailed information for a standard by name, IRI, or glob.",
881
+ "Look up one or more standards by name, IRI, or glob. --detail standard adds the dos, --detail detailed adds the don'ts.",
685
882
  toolDescription:
686
- "Get detailed information about one or more code standards including dos and donts with code examples. Address standards by name, prefixed name (cs:…), absolute IRI, or glob pattern (react/component/*).",
883
+ 'Get one or more code standards in full, with dos and don\'ts as code examples. `detail` DEFAULTS to "summary", which returns neither: pass detail: "standard" for the dos and detail: "detailed" for dos AND don\'ts. Address a standard by the name standard_list publishes (`react/component/tsdoc`), by prefixed name (`cs:react.component.tsdoc`), by absolute IRI, or by a glob over any of those. Example: standard_lookup { name: ["react/component/tsdoc"], detail: "detailed" }.',
687
884
  fields: [
688
885
  {
689
886
  name: "category",
@@ -729,7 +926,7 @@ const codeStandardsStories: readonly PackDefinition[] = [
729
926
  description:
730
927
  "Return randomly selected complete standard instances as exemplars for shape discovery.",
731
928
  toolDescription:
732
- "Return 1–5 randomly selected complete code standard instances as exemplars. Use BEFORE writing queries to see actual data shapes, property names, and value formats. Each call returns different instances.",
929
+ "Return 1–5 randomly selected complete code standard instances as exemplars. Use BEFORE writing queries to see actual data shapes, property names, and value formats. Each call returns different instances. Example: standard_sample { count: 2 }.",
733
930
  },
734
931
  },
735
932
  },
@@ -785,9 +982,19 @@ Made by the Canonical Webteam — https://canonical.com.`,
785
982
  name: "@canonical/anatomy-dsl",
786
983
  source: "git+https://github.com/canonical/anatomy-dsl.git#main",
787
984
  },
985
+ // Pinned to a TAG, and the only source here that is. The standards are the
986
+ // pack an agent is most likely to quote back at a human as policy, so which
987
+ // revision answered a query has to be recoverable — a floating ref makes
988
+ // "the CLI told me this was the rule" unfalsifiable. The other three still
989
+ // float: they are read for shape and identity, where the newest answer is
990
+ // the right one.
991
+ //
992
+ // Bump this deliberately. `sources update` resolves the tag, so a new
993
+ // upstream release reaches users only when this line moves — which is the
994
+ // point, and the cost.
788
995
  {
789
996
  name: "@canonical/code-standards",
790
- source: "git+https://github.com/canonical/web-code-standards.git#main",
997
+ source: "git+https://github.com/canonical/web-code-standards.git#v0.1.5",
791
998
  stories: codeStandardsStories,
792
999
  },
793
1000
  // The implementation graph: ds:ImplementationLibrary / ds:ImplementationObject
package/src/bin.ts CHANGED
@@ -204,7 +204,14 @@ async function main(): Promise<void> {
204
204
  .flatMap((module) => [...module.verbs])
205
205
  .filter((verb) => !verb.hidden);
206
206
  process.stdout.write(
207
- `${formatRootHelp(BIN_NAME, PROGRAM_DESCRIPTION, live, VERSION, ISSUES_URL)}\n`,
207
+ `${formatRootHelp(
208
+ BIN_NAME,
209
+ PROGRAM_DESCRIPTION,
210
+ live,
211
+ VERSION,
212
+ ISSUES_URL,
213
+ globalFlags,
214
+ )}\n`,
208
215
  );
209
216
  // The front door is a read, so it is where the un-set-up hint belongs: the
210
217
  // machine's state is the presence of the global config, and nothing is
@@ -17,8 +17,11 @@
17
17
  * deleted `capabilities/block/parity.test.ts`. Button and Modal resolve with
18
18
  * the same content a direct SPARQL oracle returns (summary, guidance,
19
19
  * anatomy, tier, modifier families with values, properties, subcomponents).
20
- * The fixture DECLARES whenToUse/whenNotToUse (which the live graph superseded
21
- * with ds:usage), so those sections are exercised here.
20
+ * The fixture declares the usage narrative the way the live graph does — one
21
+ * `ds:usage` literal — so what is exercised here is the path a real install
22
+ * takes. It formerly declared the retired whenToUse/whenNotToUse pair, and
23
+ * that divergence is exactly why this suite could assert a "### When to use"
24
+ * heading no install had rendered in months.
22
25
  */
23
26
 
24
27
  import { afterAll, beforeAll, describe, expect, it } from "vitest";
@@ -139,9 +142,9 @@ ds:Component a owl:Class .
139
142
  const rows = (await verb("list").run({}, emptyRt)) as PackRow[];
140
143
  expect(rows).toEqual([]);
141
144
  // The same semantics, on the empty-state seam: the calm message + its
142
- // runnable hint now reach the user via stderr (`emptyNotice`), keeping
145
+ // runnable hint now reach the user via stderr (`notice`), keeping
143
146
  // plain stdout pure data.
144
- const notice = verb("list").output.formatters.emptyNotice?.(rows);
147
+ const notice = verb("list").output.formatters.notice?.(rows);
145
148
  expect(notice).toContain("No blocks in the store.");
146
149
  expect(notice).toContain("pragma sources update");
147
150
  } finally {
@@ -159,10 +162,7 @@ describe("block lookup — Button content parity (GraphQL, detailed)", () => {
159
162
 
160
163
  // Guidance + anatomy sections — each cross-checked against the graph.
161
164
  expect(button.summary).toBe(await oracle(`${DS}button`, "ds:summary"));
162
- expect(button.whenToUse).toBe(await oracle(`${DS}button`, "ds:whenToUse"));
163
- expect(button.whenNotToUse).toBe(
164
- await oracle(`${DS}button`, "ds:whenNotToUse"),
165
- );
165
+ expect(button.usage).toBe(await oracle(`${DS}button`, "ds:usage"));
166
166
  expect(button.guidelines).toBe(
167
167
  await oracle(`${DS}button`, "ds:guidelines"),
168
168
  );
@@ -200,7 +200,13 @@ describe("block lookup — Button content parity (GraphQL, detailed)", () => {
200
200
  const out = (await verb("lookup").run({ name: ["Button"] }, rt)) as never;
201
201
  const llm = verb("lookup").output.formatters.llm(out);
202
202
  expect(llm).toContain("## Button");
203
- expect(llm).toContain("### When to use");
203
+ expect(llm).toContain("### Usage");
204
+ // The usage literal's OWN headings are demoted below the section heading
205
+ // they now live under, so `### When to use` renders as a CHILD of Usage
206
+ // rather than a sibling that reads as an empty section.
207
+ expect(llm).toContain("#### When to use");
208
+ expect(llm).toContain("#### When not to use");
209
+ expect(llm).not.toContain("\n### When to use\n");
204
210
  expect(llm).toContain("### Anatomy (DSL)");
205
211
  expect(llm).toContain("### Modifier Families");
206
212
  expect(llm).toContain("primary");
@@ -213,7 +219,15 @@ describe("block lookup — Modal content parity (GraphQL, detailed)", () => {
213
219
  expect(modal.uri).toBe(`${DS}modal`);
214
220
  expect(modal.name).toBe("Modal");
215
221
  expect(modal.summary).toBe(await oracle(`${DS}modal`, "ds:summary"));
216
- expect(modal.whenToUse).toBe(await oracle(`${DS}modal`, "ds:whenToUse"));
222
+ // Modal asserts an EMPTY `ds:usage` — the shape 138 of the 264 live blocks
223
+ // have. It resolves (parity with the oracle) and renders NOTHING: an empty
224
+ // literal must not print a bare "### Usage" heading with no body under it.
225
+ expect(modal.usage).toBe(await oracle(`${DS}modal`, "ds:usage"));
226
+ expect(modal.usage).toBe("");
227
+ const modalLlm = verb("lookup").output.formatters.llm(
228
+ (await verb("lookup").run({ name: ["Modal"] }, rt)) as never,
229
+ );
230
+ expect(modalLlm).not.toContain("### Usage");
217
231
  const families = modal.modifierFamilies as {
218
232
  name: string;
219
233
  values?: string[];
@@ -237,7 +251,7 @@ describe("block lookup — disclosure trims to the base view at summary", () =>
237
251
  const button = out.results.at(0) as Record<string, unknown>;
238
252
  expect(button.name).toBe("Button");
239
253
  expect(button.summary).toBeDefined();
240
- expect(button.whenToUse).toBeUndefined();
254
+ expect(button.usage).toBeUndefined();
241
255
  expect(button.anatomyDsl).toBeUndefined();
242
256
  expect(button.modifierFamilies).toBeUndefined();
243
257
  expect(button.figmaLink).toBeUndefined();