@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
@@ -86,6 +86,181 @@ export const readNounEvalCases: readonly EvalCase[] = [
86
86
  });
87
87
  },
88
88
  },
89
+ {
90
+ // The journey the reported failure actually walked: browse a category, take
91
+ // a published `name` VERBATIM, and ask for its content. Every row `list`
92
+ // publishes must be addressable by `lookup` — otherwise the two-step
93
+ // grammar the whole read surface is built on has a hole in it, and an agent
94
+ // that follows the tools' own output gets ENTITY_NOT_FOUND with empty
95
+ // suggestions and a recovery pointing back at the list it just read.
96
+ id: "tool-standard-list-names-are-addressable-by-lookup",
97
+ kind: "tool",
98
+ input:
99
+ "every name standard_list {category:react} publishes resolves through standard_lookup {name:[…], detail:detailed}, dos/donts included.",
100
+ async expect() {
101
+ await withCanonicalFixture(CANONICAL_CONFIG, async (mcp) => {
102
+ const list = await mcp.callTool("standard_list", { category: "react" });
103
+ assert.equal(list.ok, true);
104
+ const names = (list.data as { name: string }[]).map((row) => row.name);
105
+ assert.ok(names.length > 0, "expected standard_list to publish rows");
106
+ let withExamples = 0;
107
+ for (const name of names) {
108
+ const result = await mcp.callTool("standard_lookup", {
109
+ name: [name],
110
+ detail: "detailed",
111
+ });
112
+ assert.equal(
113
+ result.ok,
114
+ true,
115
+ `standard_lookup rejected the name standard_list published: ${name}`,
116
+ );
117
+ const entity = (result.data as { results: Record<string, unknown>[] })
118
+ .results[0];
119
+ assert.equal(entity?.name, name);
120
+ if (
121
+ (entity?.dos as unknown[] | undefined)?.length &&
122
+ (entity?.donts as unknown[] | undefined)?.length
123
+ ) {
124
+ withExamples += 1;
125
+ }
126
+ }
127
+ assert.ok(
128
+ withExamples > 0,
129
+ "expected at least one resolved standard to carry both dos and donts",
130
+ );
131
+ });
132
+ },
133
+ },
134
+ {
135
+ // The glob the lookup tool's own description advertises. It matched nothing
136
+ // on the shipped graph, because the candidate pool was the `cs:name`
137
+ // population and no asserted name contains a slash.
138
+ id: "tool-standard-lookup-advertised-glob-matches",
139
+ kind: "tool",
140
+ input:
141
+ 'standard_lookup {name:["react/component/*"]} — the glob the tool description advertises — resolves at least one standard.',
142
+ async expect() {
143
+ await withCanonicalFixture(CANONICAL_CONFIG, async (mcp) => {
144
+ const result = await mcp.callTool("standard_lookup", {
145
+ name: ["react/component/*"],
146
+ });
147
+ assert.equal(result.ok, true);
148
+ const results = (result.data as { results: unknown[] }).results;
149
+ assert.ok(
150
+ results.length > 0,
151
+ "the advertised glob must match at least one standard",
152
+ );
153
+ });
154
+ },
155
+ },
156
+ {
157
+ // The MIRROR of the addressability case above, and the reason the IRI-name
158
+ // fallback is DECLARED per story rather than inferred from a class
159
+ // constraint. `token list` requires `ds:tokenId`, so a `ds:Token` without
160
+ // one is a row it never publishes — and it must therefore be reachable by
161
+ // no name at all, and drawn by no sample. Inferring the fallback from the
162
+ // presence of `type: ds:Token` made it addressable and sampleable under an
163
+ // IRI-derived name the list never handed out: the same list/lookup
164
+ // disagreement as the standards defect, pointing the other way.
165
+ id: "tool-token-lookup-addresses-only-what-list-publishes",
166
+ kind: "tool",
167
+ input:
168
+ "a ds:Token carrying no ds:tokenId appears in no token_list row, resolves through no token_lookup name, and is drawn by no token_sample.",
169
+ async expect() {
170
+ await withCanonicalFixture(ALL_VISIBLE_CONFIG, async (mcp) => {
171
+ const list = await mcp.callTool("token_list");
172
+ assert.equal(list.ok, true);
173
+ const published = new Set(
174
+ (list.data as { name: string }[]).map((row) => row.name),
175
+ );
176
+ assert.ok(published.size > 0, "expected token_list to publish rows");
177
+ // The IRI-derived spellings the fallback would have minted for
178
+ // `ds:token.legacy.borderRadius` (local name, dots as slashes).
179
+ for (const derived of [
180
+ "token.legacy.borderRadius",
181
+ "token/legacy/borderRadius",
182
+ ]) {
183
+ assert.ok(
184
+ !published.has(derived),
185
+ `token_list must not publish ${derived}`,
186
+ );
187
+ const result = await mcp.callTool("token_lookup", {
188
+ name: [derived],
189
+ });
190
+ assert.equal(
191
+ result.ok,
192
+ false,
193
+ `token_lookup addressed ${derived}, a name token_list never published`,
194
+ );
195
+ assert.equal(
196
+ (result.error as { code: string }).code,
197
+ "ENTITY_NOT_FOUND",
198
+ );
199
+ }
200
+ // `sample`'s draw pool is the same population, so its reported total is
201
+ // the published population — not one entity wider.
202
+ const sample = await mcp.callTool("token_sample", {});
203
+ assert.equal(sample.ok, true);
204
+ assert.equal(
205
+ (sample.data as { totalCount: number }).totalCount,
206
+ published.size,
207
+ );
208
+ });
209
+ },
210
+ },
211
+ {
212
+ // A slug the graph declares with no standards filed under it. `standard
213
+ // categories` lists it at count 0 and the tool descriptions document a
214
+ // filtered list as a plain list, so asking for it is a calm empty answer —
215
+ // an INVALID_INPUT here tells an agent its own valid slug was a typo.
216
+ // Validity read off the RETURNED ROWS produced exactly that, because a
217
+ // category with no standards appears in no row.
218
+ id: "tool-standard-list-empty-category-is-a-calm-empty-list",
219
+ kind: "tool",
220
+ input:
221
+ "standard_list {category} for a real slug that no standard uses is ok:true with an empty list, not INVALID_INPUT.",
222
+ async expect() {
223
+ await withCanonicalFixture(CANONICAL_CONFIG, async (mcp) => {
224
+ const categories = await mcp.callTool("standard_categories");
225
+ assert.equal(categories.ok, true);
226
+ const empty = (categories.data as { name: string; count: string }[])
227
+ .filter((row) => Number(row.count) === 0)
228
+ .map((row) => row.name);
229
+ assert.ok(
230
+ empty.length > 0,
231
+ "expected standard_categories to report a zero-count category",
232
+ );
233
+ for (const slug of empty) {
234
+ const result = await mcp.callTool("standard_list", {
235
+ category: slug,
236
+ });
237
+ assert.equal(
238
+ result.ok,
239
+ true,
240
+ `standard_list rejected ${slug}, a slug standard_categories publishes`,
241
+ );
242
+ assert.deepEqual(result.data, []);
243
+ }
244
+ // The rejection itself still works, and still names the graph's whole
245
+ // vocabulary — including the categories no standard uses.
246
+ const rejected = await mcp.callTool("standard_list", {
247
+ category: "zzz-not-a-slug",
248
+ });
249
+ assert.equal(rejected.ok, false);
250
+ const error = rejected.error as {
251
+ code: string;
252
+ validOptions: string[];
253
+ };
254
+ assert.equal(error.code, "INVALID_INPUT");
255
+ for (const slug of empty) {
256
+ assert.ok(
257
+ error.validOptions.includes(slug),
258
+ `validOptions omitted the declared category ${slug}`,
259
+ );
260
+ }
261
+ });
262
+ },
263
+ },
89
264
  {
90
265
  id: "content-canonical-graph-has-4-components",
91
266
  kind: "content",
@@ -146,24 +321,85 @@ export const readNounEvalCases: readonly EvalCase[] = [
146
321
  },
147
322
  },
148
323
  {
149
- id: "content-standard-list-length-equals-category-count-sum",
324
+ // The cross-surface count-parity invariant, stated for a HIERARCHY. Summing
325
+ // the category counts stopped being the right arithmetic the moment a
326
+ // parent counted its branch (a standard under `testing-unit` is counted by
327
+ // both `testing-unit` and `testing`); what must hold — and what actually
328
+ // catches the reported defect — is that each category's count and the rows
329
+ // `--category` returns for it are the same set. Before the roll-up,
330
+ // `testing` reported 1 and `standard list --category testing` returned 1 of
331
+ // the 8 standards in the branch: a silently wrong answer, exit 0.
332
+ id: "content-standard-category-counts-match-filtered-list",
150
333
  kind: "content",
151
334
  input:
152
- "standard_list length equals the sum of standard_categories counts (the cross-surface count-parity invariant).",
335
+ "every standard_categories count equals the length of standard_list {category: <slug>}, and a parent category answers for its whole branch.",
153
336
  async expect() {
154
337
  await withCanonicalFixture(CANONICAL_CONFIG, async (mcp) => {
155
338
  const list = await mcp.callTool("standard_list");
156
339
  const categories = await mcp.callTool("standard_categories");
157
- const listLength = (list.data as unknown[]).length;
158
- const categorySum = (categories.data as { count: string }[]).reduce(
159
- (sum, row) => sum + Number(row.count),
160
- 0,
340
+ const rows = categories.data as { name: string; count: string }[];
341
+ assert.ok(
342
+ (list.data as unknown[]).length > 0 && rows.length > 0,
343
+ "both surfaces must be non-empty for the parity invariant to bite",
344
+ );
345
+ let rolledUp = 0;
346
+ for (const row of rows) {
347
+ const filtered = await mcp.callTool("standard_list", {
348
+ category: row.name,
349
+ });
350
+ assert.equal(
351
+ (filtered.data as unknown[]).length,
352
+ Number(row.count),
353
+ `standard_list --category ${row.name} disagrees with its count`,
354
+ );
355
+ // A parent whose branch is bigger than its own direct membership:
356
+ // the roll-up is doing something, so this case cannot pass vacuously.
357
+ const direct = (list.data as { category?: string }[]).filter(
358
+ (r) => r.category === row.name,
359
+ ).length;
360
+ if (Number(row.count) > direct) rolledUp += 1;
361
+ }
362
+ assert.ok(
363
+ rolledUp > 0,
364
+ "expected at least one parent category to answer for its descendants",
365
+ );
366
+ });
367
+ },
368
+ },
369
+ {
370
+ // The reflexive half of `skos:broader*`. `broader+` looks equivalent and
371
+ // silently drops every standard filed DIRECTLY on the category asked for —
372
+ // one of the 8 under `testing` in the shipped graph.
373
+ id: "content-standard-category-rollup-keeps-the-direct-member",
374
+ kind: "content",
375
+ input:
376
+ "standard_list {category: <parent>} includes both the standard filed directly on the parent and those filed on its child.",
377
+ async expect() {
378
+ await withCanonicalFixture(CANONICAL_CONFIG, async (mcp) => {
379
+ const parent = await mcp.callTool("standard_list", {
380
+ category: "testing",
381
+ });
382
+ const child = await mcp.callTool("standard_list", {
383
+ category: "testing-unit",
384
+ });
385
+ const parentRows = parent.data as { name: string; category: string }[];
386
+ const childNames = new Set(
387
+ (child.data as { name: string }[]).map((r) => r.name),
388
+ );
389
+ assert.ok(
390
+ childNames.size > 0,
391
+ "expected the child category to be used",
161
392
  );
393
+ for (const name of childNames) {
394
+ assert.ok(
395
+ parentRows.some((r) => r.name === name),
396
+ `the parent category dropped its descendant ${name}`,
397
+ );
398
+ }
162
399
  assert.ok(
163
- listLength > 0,
164
- "standard_list must be non-empty for the count-parity invariant to be meaningful",
400
+ parentRows.some((r) => r.category === "testing"),
401
+ "the parent category dropped the standard filed directly on it",
165
402
  );
166
- assert.equal(listLength, categorySum);
167
403
  });
168
404
  },
169
405
  },
@@ -4,13 +4,26 @@
4
4
  *
5
5
  * Models the live ontology's shape faithfully but compactly: a `ds:UIBlock`
6
6
  * interface with `Component`/`Pattern`/`Subcomponent` subclasses, the block
7
- * detail properties (summary, whenToUse/whenNotToUse, guidelines, anatomy,
8
- * figmaLink), modifier families with values asserted ONLY in the reverse
7
+ * detail properties (summary, usage, guidelines, anatomy, figmaLink),
8
+ * modifier families with values asserted ONLY in the reverse
9
9
  * `ds:modifierFamily` direction (so the compiled inverse-union resolver is
10
10
  * exercised), block properties, and `ds:hasSubcomponent` scoped to `ds:Component`
11
11
  * (reached via a subtype-scoped fragment). Two blocks — Button and Modal —
12
- * carry the full spec so block content-parity asserts on a graph that DECLARES
13
- * whenToUse/whenNotToUse (which the live graph superseded with ds:usage).
12
+ * carry the full spec so block content-parity asserts on a graph shaped like
13
+ * the live one.
14
+ *
15
+ * The usage narrative is modelled as the live graph models it: ONE `ds:usage`
16
+ * literal per block, free-text Markdown carrying its own `### When to use` /
17
+ * `### When not to use` sub-sections. It used to declare the retired
18
+ * `ds:whenToUse`/`ds:whenNotToUse` pair instead, which is why the block parity
19
+ * suite went on asserting a "### When to use" heading that no real install had
20
+ * rendered since the ontology conflated the two into `ds:usage`.
21
+ *
22
+ * Both shapes the live pack has are here on purpose: Button carries a
23
+ * non-empty literal (126 of the 264 live blocks) and Modal an EMPTY one (the
24
+ * other 138), so the fixture proves the renderer both nests a body's own
25
+ * headings under the section heading AND prints no heading at all for an empty
26
+ * literal. A fixture that only ever carried prose could not tell those apart.
14
27
  */
15
28
 
16
29
  /** The prefixes the fixture store is built and queried with. */
@@ -56,8 +69,7 @@ ds:name a owl:DatatypeProperty ;
56
69
  ds:tier a owl:ObjectProperty, owl:FunctionalProperty ;
57
70
  rdfs:domain ds:UIBlock ; rdfs:range ds:Tier .
58
71
  ds:summary a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
59
- ds:whenToUse a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
60
- ds:whenNotToUse a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
72
+ ds:usage a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
61
73
  ds:guidelines a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
62
74
  ds:anatomyDsl a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
63
75
  ds:anatomyClassic a owl:DatatypeProperty ; rdfs:domain ds:UIBlock ; rdfs:range xsd:string .
@@ -82,8 +94,7 @@ ds:button a ds:Component ;
82
94
  ds:name "Button" ;
83
95
  ds:tier ds:global ;
84
96
  ds:summary "Primary action trigger with optional icon and label." ;
85
- ds:whenToUse "Use for the primary action on a view." ;
86
- ds:whenNotToUse "Do not use for navigation between pages." ;
97
+ ds:usage "Buttons trigger actions; links go places.\\n\\n### When to use\\n\\n- For the primary action on a view.\\n\\n### When not to use\\n\\n- For navigation between pages." ;
87
98
  ds:guidelines "Keep labels short and action-oriented." ;
88
99
  ds:anatomyDsl "root: button; children: label, icon" ;
89
100
  ds:anatomyClassic "Button > Label, Icon" ;
@@ -96,8 +107,7 @@ ds:modal a ds:Component ;
96
107
  ds:name "Modal" ;
97
108
  ds:tier ds:global ;
98
109
  ds:summary "Focused overlay dialog for a single task." ;
99
- ds:whenToUse "Use to interrupt for a critical confirmation." ;
100
- ds:whenNotToUse "Do not use for non-blocking notifications." ;
110
+ ds:usage "" ;
101
111
  ds:guidelines "Always provide an explicit close affordance." ;
102
112
  ds:anatomyDsl "root: dialog; children: header, body, footer" ;
103
113
  ds:hasModifierFamily ds:family.size ;
@@ -168,9 +178,14 @@ ${ROSTER_MEMBERS}
168
178
  *
169
179
  * It models the three shapes the addressing path gets wrong: two components
170
180
  * sharing one `ds:name` (declared zeta-first so the store's enumeration order
171
- * and IRI order DISAGREE — an unordered `LIMIT 1` picks the zeta one), and a
172
- * component carrying no `ds:name` at all (reachable only by IRI, the way 131 of
173
- * the 144 live code standards are).
181
+ * and IRI order DISAGREE), and a component carrying no `ds:name` at all
182
+ * (reachable only by IRI, the way 131 of the 144 live code standards are).
183
+ *
184
+ * The shared name is a SHAPE probe, not the live ambiguity: neither chip's tier
185
+ * outranks the other, so this fixture can only prove that both are returned in a
186
+ * total order — never which of them a reader means. Deciding that is a judgement
187
+ * about real tiers, and it is asserted where the real tiers are, against the
188
+ * shipped pack (`capabilities/block.tierRank.exec.test.ts`).
174
189
  */
175
190
  export const AMBIGUOUS_TTL = `
176
191
  ds:apps a ds:Tier ; ds:name "apps" .
@@ -26,6 +26,16 @@
26
26
  * ours to pin): 4 `ds:Component` (Button, Modal, LXD Panel, Beta Widget),
27
27
  * `importance` family with value `primary`, code standard
28
28
  * `code/function/purity`.
29
+ *
30
+ * Two of those anchors exist to pin an ABSENCE, and are load-bearing for the
31
+ * "declared, not inferred" halves of the read grammar:
32
+ * - `ds:token.legacy.borderRadius` — a `ds:Token` with no `ds:tokenId`, which
33
+ * `token list` therefore never publishes and `token lookup`/`token sample`
34
+ * must never reach (`PackLookup.nameFallback` is declared by `standard`
35
+ * alone).
36
+ * - `cs:archive` — a `cs:Category` with no standards, which `standard
37
+ * categories` reports at count 0 and `standard list --category archive` must
38
+ * answer with a calm empty list rather than INVALID_INPUT.
29
39
  */
30
40
 
31
41
  import { BLOCK_PREFIXES, BLOCK_TTL } from "../blockGraph.js";
@@ -76,13 +86,46 @@ ds:token.spacing.medium a ds:Token ;
76
86
  ds:tokenType ds:type.spacing ;
77
87
  ds:valueLight "16px" ;
78
88
  ds:valueDark "16px" .
89
+
90
+ # A ds:Token carrying NO ds:tokenId — the entity "token list" does not publish,
91
+ # because its query REQUIRES the id. It is here to pin what a lookup may NOT
92
+ # reach: "token lookup" addresses by ds:tokenId, and a story whose list requires
93
+ # its "by" property must not become addressable (or sampleable) under a name
94
+ # derived from its IRI. See PackLookup.nameFallback, declared by the "standard"
95
+ # story alone — the one story whose list DOES publish such a name.
96
+ ds:token.legacy.borderRadius a ds:Token ;
97
+ ds:tokenType ds:type.spacing ;
98
+ ds:valueLight "4px" ;
99
+ ds:valueDark "4px" .
79
100
  `;
80
101
 
81
- /** The `cs:` (code standards) section — reused verbatim from `standard/parity.test.ts`'s
82
- * fixture shape so `code/function/purity`/`react/component/*` are stable, shared
83
- * anchor names rather than a second invented set. */
102
+ /**
103
+ * The `cs:` (code standards) section — shaped like the SHIPPED code-standards
104
+ * graph, not like a convenience fixture.
105
+ *
106
+ * Two facts about the real data are reproduced here on purpose, because the
107
+ * eval harness was blind to both while every fixture standard carried a
108
+ * `cs:name`:
109
+ *
110
+ * 1. **Most standards carry NO `cs:name`.** The shipped graph asserts it on 22
111
+ * of 156 (~13%); one of the eight standards below carries one (~13%). The
112
+ * ontology says so deliberately — `cs:name` is "an optional human-readable
113
+ * display title" that "never participates in identity", the canonical
114
+ * identifier being the compact IRI. So the name `standard list` publishes for
115
+ * the other seven is SYNTHESIZED from the IRI local name
116
+ * (`cs:react.component.props` → `react/component/props`), and a lookup that
117
+ * cannot resolve a synthesized name cannot resolve the graph.
118
+ * 2. **Categories are a two-level SKOS tree.** `cs:testing.unit skos:broader
119
+ * cs:testing`, exactly as the shipped graph declares for `testing.*` and
120
+ * `ui_blocks.nojs`, and one standard sits DIRECTLY on the parent — the case a
121
+ * non-reflexive roll-up (`skos:broader+`) silently drops.
122
+ *
123
+ * Anchor names (`code/function/purity`, `react/component/*`) are unchanged; they
124
+ * are simply reached the way the real graph reaches them.
125
+ */
84
126
  const CS_TTL = `
85
127
  @prefix cs: <http://pragma.canonical.com/codestandards#> .
128
+ @prefix skos: <http://www.w3.org/2004/02/skos/core#> .
86
129
 
87
130
  cs:CodeStandard a owl:Class .
88
131
  cs:Category a owl:Class .
@@ -97,27 +140,66 @@ cs:dont a owl:ObjectProperty ; rdfs:domain cs:CodeStandard ; rdfs:range cs:Examp
97
140
  cs:language a owl:DatatypeProperty ; rdfs:domain cs:Example ; rdfs:range xsd:string .
98
141
  cs:code a owl:DatatypeProperty ; rdfs:domain cs:Example ; rdfs:range xsd:string .
99
142
 
100
- cs:cat.react a cs:Category ; cs:slug "react" .
101
- cs:cat.code a cs:Category ; cs:slug "code" .
102
-
143
+ # ---- Categories: flat roots, plus one two-level branch (testing) ----
144
+ cs:react a cs:Category ; rdfs:label "React" ; cs:slug "react" .
145
+ cs:code a cs:Category ; rdfs:label "Code" ; cs:slug "code" .
146
+ cs:turtle a cs:Category ; rdfs:label "Turtle" ; cs:slug "turtle" .
147
+ cs:testing a cs:Category ; rdfs:label "Testing" ; cs:slug "testing" .
148
+ cs:testing.unit a cs:Category ;
149
+ rdfs:label "Unit testing" ;
150
+ cs:slug "testing-unit" ;
151
+ skos:broader cs:testing .
152
+
153
+ # A category the graph DECLARES with no standards filed under it. The
154
+ # "standard categories" verb lists it with count 0, so
155
+ # "standard list --category archive" must be the documented calm empty list — a
156
+ # real slug, an empty answer. Validity read off the returned ROWS instead of the
157
+ # graph called it INVALID_INPUT.
158
+ cs:archive a cs:Category ;
159
+ rdfs:label "Archive" ;
160
+ cs:slug "archive" .
161
+
162
+ # ---- Standards. Seven of eight carry NO cs:name (the shipped ~13% split). ----
103
163
  cs:react.component.props a cs:CodeStandard ;
104
- cs:name "react/component/props" ;
105
164
  cs:description "Type component props explicitly." ;
106
- cs:hasCategory cs:cat.react ;
165
+ cs:hasCategory cs:react ;
107
166
  cs:do [ a cs:Example ; cs:description "Do type props" ; cs:language "tsx" ; cs:code "interface P {}" ] ;
108
167
  cs:dont [ a cs:Example ; cs:description "Avoid any" ; cs:language "tsx" ; cs:code "props: any" ] .
109
168
 
110
169
  cs:react.component.structure a cs:CodeStandard ;
111
- cs:name "react/component/structure" ;
112
170
  cs:description "Keep folder structure flat." ;
113
- cs:hasCategory cs:cat.react ;
171
+ cs:hasCategory cs:react ;
114
172
  cs:extends cs:react.component.props ;
115
173
  cs:do [ a cs:Example ; cs:description "Do flatten" ; cs:language "text" ; cs:code "src/Button.tsx" ] .
116
174
 
117
175
  cs:code.function.purity a cs:CodeStandard ;
118
- cs:name "code/function/purity" ;
119
176
  cs:description "Prefer pure functions." ;
120
- cs:hasCategory cs:cat.code .
177
+ cs:hasCategory cs:code .
178
+
179
+ # The standard sitting DIRECTLY on the parent category: a skos:broader+
180
+ # roll-up loses it, a reflexive skos:broader* keeps it.
181
+ cs:testing.smoke a cs:CodeStandard ;
182
+ cs:description "Keep one end-to-end smoke path green." ;
183
+ cs:hasCategory cs:testing .
184
+
185
+ cs:testing.unit.isolation a cs:CodeStandard ;
186
+ cs:description "Isolate the unit under test." ;
187
+ cs:hasCategory cs:testing.unit .
188
+
189
+ cs:testing.unit.naming a cs:CodeStandard ;
190
+ cs:description "Name a unit test for the behaviour it pins." ;
191
+ cs:hasCategory cs:testing.unit ;
192
+ cs:do [ a cs:Example ; cs:description "Do name the behaviour" ; cs:language "ts" ; cs:code "it(\\"rejects an empty batch\\")" ] .
193
+
194
+ cs:testing.unit.fixtures a cs:CodeStandard ;
195
+ cs:description "Share one fixture per behavioural claim." ;
196
+ cs:hasCategory cs:testing.unit .
197
+
198
+ # The ~13%: a display title that adds something the IRI does not.
199
+ cs:turtle.naming.local_name_casing a cs:CodeStandard ;
200
+ cs:name "Turtle local-name casing" ;
201
+ cs:description "Use snake_case for multi-word local-name segments." ;
202
+ cs:hasCategory cs:turtle .
121
203
  `;
122
204
 
123
205
  /** The `ds:Prompt` workflow templates — the ONE source both the `prompt_list`/
@@ -255,6 +337,7 @@ ds:implementation.svelte-ds-global.button a ds:ImplementationObject ;
255
337
  export const CANONICAL_PREFIXES: Readonly<Record<string, string>> = {
256
338
  ...BLOCK_PREFIXES,
257
339
  cs: "http://pragma.canonical.com/codestandards#",
340
+ skos: "http://www.w3.org/2004/02/skos/core#",
258
341
  };
259
342
 
260
343
  /** The full canonical Turtle: PR3's `BLOCK_TTL` verbatim, plus the sections above. */
@@ -1,16 +0,0 @@
1
- /**
2
- * User-facing labels for the two config bands, shared by the setup recap
3
- * ({@link ../setup/setup.render}) and the doctor report
4
- * ({@link ../doctor/doctor.render}) so ONE consistent vocabulary appears across
5
- * both. The pair matches the CLI grammar users already meet elsewhere: the
6
- * `--global`/`--local` flags, the `--scope project|global|both` enum, config
7
- * show's `[global]`/`[project]` origin markers, and the `setup` manifest's
8
- * `[global]`/`[project]` lines. (Earlier these two surfaces said MACHINE/PROJECT
9
- * — a third term for the same bands; this is the single source that unifies it.)
10
- */
11
- /** The user-facing display label for each of the two config bands. */
12
- export const BAND_LABELS = {
13
- global: "Global",
14
- project: "Project",
15
- };
16
- //# sourceMappingURL=bands.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"bands.js","sourceRoot":"","sources":["../../../../src/capabilities/shared/bands.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,sEAAsE;AACtE,MAAM,CAAC,MAAM,WAAW,GAAyC;IAC/D,MAAM,EAAE,QAAQ;IAChB,OAAO,EAAE,SAAS;CACnB,CAAC"}
@@ -1,16 +0,0 @@
1
- /**
2
- * User-facing labels for the two config bands, shared by the setup recap
3
- * ({@link ../setup/setup.render}) and the doctor report
4
- * ({@link ../doctor/doctor.render}) so ONE consistent vocabulary appears across
5
- * both. The pair matches the CLI grammar users already meet elsewhere: the
6
- * `--global`/`--local` flags, the `--scope project|global|both` enum, config
7
- * show's `[global]`/`[project]` origin markers, and the `setup` manifest's
8
- * `[global]`/`[project]` lines. (Earlier these two surfaces said MACHINE/PROJECT
9
- * — a third term for the same bands; this is the single source that unifies it.)
10
- */
11
-
12
- /** The user-facing display label for each of the two config bands. */
13
- export const BAND_LABELS: Record<"project" | "global", string> = {
14
- global: "Global",
15
- project: "Project",
16
- };