adsa-cli 0.1.2 → 0.1.4

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.
package/lib/score.mjs CHANGED
@@ -10,6 +10,8 @@ const here = dirname(fileURLToPath(import.meta.url));
10
10
  export const RUBRIC = JSON.parse(readFileSync(join(here, "..", "rubric", "rubric.json"), "utf8"));
11
11
 
12
12
  const pct = (n) => `${Math.round(n * 100)}%`;
13
+ /** Evidence lines end up on a landing page and in a pull request. They read as English. */
14
+ const plural = (n, one, many = `${one}s`) => `${n} ${n === 1 ? one : many}`;
13
15
 
14
16
  /** What "accessibility documentation" means in the vocabulary of each platform. */
15
17
  const A11Y_LABEL = {
@@ -22,10 +24,13 @@ const A11Y_LABEL = {
22
24
  const SCORERS = {
23
25
  "agent-instructions"(f) {
24
26
  const files = f.agentFiles.filter((a) => a.lines > 3);
25
- const main = files.find((a) => a.mentionsPackage) || files[0];
27
+ // A file in the package's own directory is about the package. Requiring the
28
+ // name in the text is the right question for a monorepo root, and the wrong one
29
+ // for the AGENTS.md a single-package repository keeps beside its components.
30
+ const main = files.find((a) => a.mentionsPackage) || files.find((a) => a.scope !== "repo") || files[0];
26
31
  if (!main) return { score: 1, evidence: ["No AGENTS.md, CLAUDE.md or equivalent in the repository."] };
27
- const ev = [`${main.file}, ${main.lines} lines.`];
28
- if (!main.mentionsPackage) {
32
+ const ev = [`${main.label || main.file}, ${main.lines} lines.`];
33
+ if (!main.mentionsPackage && main.scope === "repo") {
29
34
  ev.push(`It never mentions ${f.name}, so an agent gets no design-system rules from it.`);
30
35
  return { score: 1, evidence: ev };
31
36
  }
@@ -46,15 +51,22 @@ const SCORERS = {
46
51
  "machine-surface"(f) {
47
52
  const m = f.machine;
48
53
  const ev = [];
54
+ const served = m.servedMcp || [];
55
+ const siblings = m.siblingClis || [];
56
+ const commands = m.commands || [];
49
57
  if (m.mcpInPackage) ev.push(`An MCP server ships inside the package (bin: ${m.binNames.join(", ")}).`);
58
+ if (served.length) ev.push(`This repository serves an MCP server: ${served.slice(0, 3).join(", ")}.`);
50
59
  if (m.declaredServers.length) ev.push(`${m.mcpConfigs.join(", ")} registers ${m.declaredServers.join(", ")}.`);
51
60
  if (m.cliShipped && !m.mcpInPackage) ev.push(`A CLI ships with the package (bin: ${m.binNames.join(", ")}).`);
61
+ if (siblings.length) ev.push(`A CLI ships from ${siblings.map((c) => `${c.name} (${c.bin.join(", ")})`).slice(0, 2).join(", ")}.`);
52
62
  if (m.llmsTxt.length) ev.push(`${m.llmsTxt.join(", ")} present.`);
53
63
  if (m.skills.length) ev.push(`${m.skills.length} agent skill${m.skills.length === 1 ? "" : "s"} published.`);
64
+ if (commands.length) ev.push(`${commands.length} agent command${commands.length === 1 ? "" : "s"} published.`);
54
65
  if (m.storybookMcp) ev.push("@storybook/addon-mcp is installed, which answers from a running Storybook in this repo only.");
55
66
  if (!ev.length) ev.push("No MCP server, no docs CLI, no llms.txt, no published skills: an agent has to read files.");
56
- const score = m.mcpInPackage || m.declaredServers.length ? 5 : m.cliShipped || m.llmsTxt.length || m.skills.length || m.storybookMcp ? 3 : 1;
57
- return { score, evidence: ev };
67
+ const mcp = m.mcpInPackage || m.declaredServers.length || served.length;
68
+ const queryable = m.cliShipped || siblings.length || m.llmsTxt.length || m.skills.length || commands.length || m.storybookMcp;
69
+ return { score: mcp ? 5 : queryable ? 3 : 1, evidence: ev };
58
70
  },
59
71
 
60
72
  "docs-coverage"(f) {
@@ -74,7 +86,7 @@ const SCORERS = {
74
86
  const ev = [`${f.guides.length} guides, ${s.blocks} code blocks, ${s.guidesWithPropTable} with a prop table.`];
75
87
  if (s.guidesGenerated) ev.push(`${s.guidesGenerated} guides carry a generated marker.`);
76
88
  else ev.push("No guide is marked as generated, so every table is hand-maintained.");
77
- if (s.unknownCount) ev.push(`${s.unknownCount} imports in the guides name symbols this repo does not export, e.g. ${s.unknownImports.slice(0, 3).map((u) => `${u.name} in ${u.guide}`).join("; ")}.`);
89
+ if (s.unknownCount) ev.push(`${plural(s.unknownCount, "import")} in the guides ${s.unknownCount === 1 ? "names a symbol" : "name symbols"} this repo does not export, e.g. ${s.unknownImports.slice(0, 3).map((u) => `${u.name} in ${u.guide}`).join("; ")}.`);
78
90
  else ev.push("Every symbol imported in the guides exists in the source.");
79
91
  ev.push(s.compiledInCi ? "CI checks the guides against the code." : "Nothing compares the guides to the code.");
80
92
  let score = s.compiledInCi && s.guidesGenerated ? 5 : s.guidesGenerated || s.guidesWithPropTable ? 3 : 1;
@@ -123,7 +135,7 @@ const SCORERS = {
123
135
 
124
136
  verification(f) {
125
137
  const v = f.verification;
126
- const ev = [`${v.testFiles} test files, ${v.storyFiles} stories.`];
138
+ const ev = [`${plural(v.testFiles, "test file")}, ${plural(v.storyFiles, "story", "stories")}.`];
127
139
  const bits = [v.testScript && "test", v.typecheckScript && "typecheck", v.lintScript && "lint"].filter(Boolean);
128
140
  ev.push(bits.length ? `Scripts: ${bits.join(", ")}.` : "No test, typecheck or lint script.");
129
141
  if (v.workflows.length) ev.push(`CI: ${v.workflows.join(", ")}.`);
package/lib/target.mjs ADDED
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Which directory to audit. A design system usually lives in a monorepo, and the
3
+ * package the reader means is rarely the directory they typed: they hand over the
4
+ * repository and expect the tool to find the components.
5
+ *
6
+ * Getting this wrong is the worst thing this tool can do. Every dimension is then
7
+ * measured from inside the wrong folder, and "no agent instructions in the
8
+ * repository" means "none in a subfolder of a repository that has them".
9
+ */
10
+ import { join, basename } from "node:path";
11
+ import { isDir, readJson, rel } from "./fsx.mjs";
12
+ import { countGuides, workspaceDirs } from "./config.mjs";
13
+ import { countComponents } from "./scan.mjs";
14
+
15
+ /**
16
+ * @param {string} root the directory the reader named
17
+ * @param {{workspace?:string, noWorkspace?:boolean}} options `--workspace` / `--no-workspace`
18
+ * @returns {{ dir:string, note:string|null, candidates:Array<object>, error?:string }}
19
+ */
20
+ export function resolveTarget(root, options = {}) {
21
+ if (options.noWorkspace) return { dir: root, note: null, candidates: [] };
22
+
23
+ if (options.workspace) {
24
+ const all = profile(workspaceDirs(root), true);
25
+ const wanted = all.find((c) => c.name === options.workspace || basename(c.dir) === options.workspace || rel(root, c.dir) === options.workspace);
26
+ if (!wanted) {
27
+ const known = all.map((c) => c.name).join(", ") || "none";
28
+ return { dir: root, note: null, candidates: all, error: `No workspace matches "${options.workspace}". This repository has: ${known}.` };
29
+ }
30
+ return { dir: wanted.dir, note: `workspace: audited ${wanted.name} (--workspace)`, candidates: all };
31
+ }
32
+
33
+ // A root that publishes the components itself is the package, whatever else the
34
+ // repository keeps beside it: its own export map is what a consumer installs.
35
+ const pkg = readJson(join(root, "package.json"));
36
+ const rootExports = Object.keys(pkg?.exports || {}).filter((k) => k.startsWith("./")).length;
37
+ if (rootExports >= 5 || isDir(join(root, "guidelines"))) return { dir: root, note: null, candidates: [] };
38
+
39
+ const workspaces = workspaceDirs(root);
40
+ if (!workspaces.length) return { dir: root, note: null, candidates: [] };
41
+
42
+ // Published packages first. Only when none of them exports a component does a
43
+ // private workspace count: a repository that keeps its components in its own docs
44
+ // app — a registry, a showcase — still keeps them somewhere, and reporting a
45
+ // repository full of components as having none is the worse answer.
46
+ const published = profile(workspaces, false);
47
+ const all = published.some((c) => c.components > 0) ? published : profile(workspaces, true);
48
+ const withComponents = all.filter((c) => c.components > 0);
49
+ if (!withComponents.length) return { dir: root, note: null, candidates: all };
50
+
51
+ const best = withComponents[0];
52
+ const runnerUp = withComponents[1];
53
+ const note = `workspace: audited ${best.name}, ${best.components} components${runnerUp ? ` (next: ${runnerUp.name}, ${runnerUp.components})` : ""}`;
54
+ return { dir: best.dir, note, candidates: withComponents };
55
+ }
56
+
57
+ /**
58
+ * Rank the packages by what actually distinguishes a design system: the number of
59
+ * components a consumer can import from it. Ranking by the presence of a `docs/`
60
+ * folder — what this did before — hands the audit to whichever package happens to
61
+ * keep markdown next to it, and a codemod package with thirty migration notes beat
62
+ * the component library every time.
63
+ *
64
+ * `explicit` is a name the reader typed, so nothing is filtered out: a private
65
+ * workspace is still a package somebody can ask to audit.
66
+ */
67
+ /** `"private": "true"` is in the wild, and reads as published to a strict check. */
68
+ const isPrivate = (pkg) => pkg.private === true || pkg.private === "true";
69
+
70
+ function profile(dirs, explicit) {
71
+ const out = [];
72
+ for (const dir of dirs) {
73
+ const pkg = readJson(join(dir, "package.json"));
74
+ if (!pkg || (isPrivate(pkg) && !explicit)) continue;
75
+ const components = countComponents(dir);
76
+ const guides = countGuides(dir);
77
+ const exports_ = Object.keys(pkg.exports || {}).filter((k) => k.startsWith("./")).length;
78
+ if (!explicit && !components && !guides && !exports_) continue;
79
+ out.push({ dir, name: pkg.name || basename(dir), components, guides, exports: exports_ });
80
+ }
81
+ return out.sort((a, b) => b.components - a.components || b.guides - a.guides || b.exports - a.exports);
82
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "adsa-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Score a design system on how well coding agents can use it, then fix what is missing.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,293 @@
1
+ {
2
+ "note": "Public design systems audited with this rubric, at the commit named. Reproduce with `npx adsa-cli audit <clone>`.",
3
+ "rubric": "1.0",
4
+ "measured": "2026-09-10",
5
+ "systems": [
6
+ {
7
+ "name": "Astryx",
8
+ "org": "Meta",
9
+ "repo": "facebook/astryx",
10
+ "commit": "5447429bac",
11
+ "package": "@astryxdesign/core",
12
+ "version": "0.5.4",
13
+ "packageDir": "packages/core",
14
+ "total": 33,
15
+ "max": 45,
16
+ "dimensions": {
17
+ "agent-instructions": 3,
18
+ "machine-surface": 5,
19
+ "docs-coverage": 3,
20
+ "docs-freshness": 3,
21
+ "tokens": 3,
22
+ "patterns": 5,
23
+ "a11y": 3,
24
+ "verification": 5,
25
+ "gap-handling": 3
26
+ }
27
+ },
28
+ {
29
+ "name": "Chakra UI",
30
+ "org": "",
31
+ "repo": "chakra-ui/chakra-ui",
32
+ "commit": "67abe9fb80",
33
+ "package": "@chakra-ui/react",
34
+ "version": "3.37.0",
35
+ "packageDir": "packages/react",
36
+ "total": 29,
37
+ "max": 45,
38
+ "dimensions": {
39
+ "agent-instructions": 3,
40
+ "machine-surface": 5,
41
+ "docs-coverage": 3,
42
+ "docs-freshness": 3,
43
+ "tokens": 5,
44
+ "patterns": 5,
45
+ "a11y": 1,
46
+ "verification": 3,
47
+ "gap-handling": 1
48
+ }
49
+ },
50
+ {
51
+ "name": "React Spectrum",
52
+ "org": "Adobe",
53
+ "repo": "adobe/react-spectrum",
54
+ "commit": "4693fcc844",
55
+ "package": "@react-spectrum/s2",
56
+ "version": "1.7.1",
57
+ "packageDir": "packages/@react-spectrum/s2",
58
+ "total": 25,
59
+ "max": 45,
60
+ "dimensions": {
61
+ "agent-instructions": 3,
62
+ "machine-surface": 5,
63
+ "docs-coverage": 3,
64
+ "docs-freshness": 3,
65
+ "tokens": 3,
66
+ "patterns": 3,
67
+ "a11y": 1,
68
+ "verification": 3,
69
+ "gap-handling": 1
70
+ }
71
+ },
72
+ {
73
+ "name": "shadcn/ui",
74
+ "org": "",
75
+ "repo": "shadcn-ui/ui",
76
+ "commit": "3ba91b1cc8",
77
+ "package": "v4",
78
+ "version": "0.1.0",
79
+ "packageDir": "apps/v4",
80
+ "total": 25,
81
+ "max": 45,
82
+ "dimensions": {
83
+ "agent-instructions": 1,
84
+ "machine-surface": 5,
85
+ "docs-coverage": 3,
86
+ "docs-freshness": 3,
87
+ "tokens": 3,
88
+ "patterns": 5,
89
+ "a11y": 1,
90
+ "verification": 3,
91
+ "gap-handling": 1
92
+ }
93
+ },
94
+ {
95
+ "name": "HeroUI",
96
+ "org": "",
97
+ "repo": "heroui-inc/heroui",
98
+ "commit": "98553cefdc",
99
+ "package": "@heroui/react",
100
+ "version": "3.2.4",
101
+ "packageDir": "packages/react",
102
+ "total": 23,
103
+ "max": 45,
104
+ "dimensions": {
105
+ "agent-instructions": 3,
106
+ "machine-surface": 3,
107
+ "docs-coverage": 1,
108
+ "docs-freshness": 1,
109
+ "tokens": 3,
110
+ "patterns": 5,
111
+ "a11y": 3,
112
+ "verification": 3,
113
+ "gap-handling": 1
114
+ }
115
+ },
116
+ {
117
+ "name": "Polaris",
118
+ "org": "Shopify",
119
+ "repo": "Shopify/polaris",
120
+ "commit": "3f7954ae42",
121
+ "package": "@shopify/polaris",
122
+ "version": "13.10.1",
123
+ "packageDir": "polaris-react",
124
+ "total": 23,
125
+ "max": 45,
126
+ "dimensions": {
127
+ "agent-instructions": 1,
128
+ "machine-surface": 3,
129
+ "docs-coverage": 1,
130
+ "docs-freshness": 3,
131
+ "tokens": 5,
132
+ "patterns": 5,
133
+ "a11y": 1,
134
+ "verification": 3,
135
+ "gap-handling": 1
136
+ }
137
+ },
138
+ {
139
+ "name": "BoardUI",
140
+ "org": "",
141
+ "repo": "BoardUI/boardui",
142
+ "commit": "3e76e28261",
143
+ "package": "boardui@0.5.5",
144
+ "packageDir": ".",
145
+ "via": "npx boardui add + skill, in the starter repo",
146
+ "total": 21,
147
+ "max": 45,
148
+ "dimensions": {
149
+ "agent-instructions": 3,
150
+ "machine-surface": 3,
151
+ "docs-coverage": 1,
152
+ "docs-freshness": 1,
153
+ "tokens": 3,
154
+ "patterns": 5,
155
+ "a11y": 1,
156
+ "verification": 3,
157
+ "gap-handling": 1
158
+ }
159
+ },
160
+ {
161
+ "name": "Mantine",
162
+ "org": "",
163
+ "repo": "mantinedev/mantine",
164
+ "commit": "61049ecd95",
165
+ "package": "@mantine/core",
166
+ "version": "9.6.1",
167
+ "packageDir": "packages/@mantine/core",
168
+ "total": 21,
169
+ "max": 45,
170
+ "dimensions": {
171
+ "agent-instructions": 3,
172
+ "machine-surface": 3,
173
+ "docs-coverage": 3,
174
+ "docs-freshness": 1,
175
+ "tokens": 3,
176
+ "patterns": 3,
177
+ "a11y": 1,
178
+ "verification": 3,
179
+ "gap-handling": 1
180
+ }
181
+ },
182
+ {
183
+ "name": "Base UI",
184
+ "org": "MUI",
185
+ "repo": "mui/base-ui",
186
+ "commit": "9f4551b4cb",
187
+ "package": "@base-ui/react",
188
+ "version": "1.8.0",
189
+ "packageDir": "packages/react",
190
+ "total": 19,
191
+ "max": 45,
192
+ "dimensions": {
193
+ "agent-instructions": 1,
194
+ "machine-surface": 3,
195
+ "docs-coverage": 1,
196
+ "docs-freshness": 3,
197
+ "tokens": 1,
198
+ "patterns": 5,
199
+ "a11y": 1,
200
+ "verification": 3,
201
+ "gap-handling": 1
202
+ }
203
+ },
204
+ {
205
+ "name": "Carbon",
206
+ "org": "IBM",
207
+ "repo": "carbon-design-system/carbon",
208
+ "commit": "12277c649e",
209
+ "package": "@carbon/react",
210
+ "version": "1.116.0",
211
+ "packageDir": "packages/react",
212
+ "total": 19,
213
+ "max": 45,
214
+ "dimensions": {
215
+ "agent-instructions": 3,
216
+ "machine-surface": 3,
217
+ "docs-coverage": 1,
218
+ "docs-freshness": 1,
219
+ "tokens": 3,
220
+ "patterns": 3,
221
+ "a11y": 1,
222
+ "verification": 3,
223
+ "gap-handling": 1
224
+ }
225
+ },
226
+ {
227
+ "name": "Primer",
228
+ "org": "GitHub",
229
+ "repo": "primer/react",
230
+ "commit": "4dc92ecc50",
231
+ "package": "@primer/react",
232
+ "version": "38.38.0",
233
+ "packageDir": "packages/react",
234
+ "total": 17,
235
+ "max": 45,
236
+ "dimensions": {
237
+ "agent-instructions": 3,
238
+ "machine-surface": 5,
239
+ "docs-coverage": 1,
240
+ "docs-freshness": 1,
241
+ "tokens": 1,
242
+ "patterns": 1,
243
+ "a11y": 1,
244
+ "verification": 3,
245
+ "gap-handling": 1
246
+ }
247
+ },
248
+ {
249
+ "name": "Radix Primitives",
250
+ "org": "WorkOS",
251
+ "repo": "radix-ui/primitives",
252
+ "commit": "f7ecd5ab16",
253
+ "package": "radix-ui",
254
+ "version": "1.6.7",
255
+ "packageDir": "packages/react/radix-ui",
256
+ "total": 13,
257
+ "max": 45,
258
+ "dimensions": {
259
+ "agent-instructions": 3,
260
+ "machine-surface": 1,
261
+ "docs-coverage": 1,
262
+ "docs-freshness": 1,
263
+ "tokens": 1,
264
+ "patterns": 1,
265
+ "a11y": 1,
266
+ "verification": 3,
267
+ "gap-handling": 1
268
+ }
269
+ },
270
+ {
271
+ "name": "Untitled UI React",
272
+ "org": "",
273
+ "repo": "untitleduico/react",
274
+ "commit": "c981a73bcd",
275
+ "package": "@untitledui/react",
276
+ "version": "0.0.0",
277
+ "packageDir": ".",
278
+ "total": 13,
279
+ "max": 45,
280
+ "dimensions": {
281
+ "agent-instructions": 3,
282
+ "machine-surface": 1,
283
+ "docs-coverage": 1,
284
+ "docs-freshness": 1,
285
+ "tokens": 1,
286
+ "patterns": 1,
287
+ "a11y": 1,
288
+ "verification": 3,
289
+ "gap-handling": 1
290
+ }
291
+ }
292
+ ]
293
+ }
@@ -49,6 +49,23 @@ npx adsa-cli audit <path> # score, report, .adsa/score.json
49
49
  npx adsa-cli audit <path> --json # the same, machine-readable
50
50
  ```
51
51
 
52
+ **Show the tool's own output.** It prints a coloured bar per dimension, the band the
53
+ score falls in, and where that score sits against the public design systems in the reference set. Paste
54
+ that block as it came out. Do not rebuild it as a markdown table: your table drops the
55
+ bars, the band and the standing, and it is one retyping away from being wrong about a
56
+ number the reader could otherwise have checked.
57
+
58
+ The run opens `report.html` and prints its `file://` link. Do not ask whether to open
59
+ it, and do not offer to fix everything before the reader has read anything — the
60
+ report is the deliverable, and what to fix first is a decision it exists to inform.
61
+
62
+ **Check the scan line before you believe the score.** `scanned:` says which directories
63
+ were read and which package was picked out of a monorepo. If the guides say `none
64
+ found` and you can see a documentation site in the repository, the audit is measuring
65
+ the wrong folder: fix it with `--workspace <name>` or a `guides` entry in
66
+ `adsa.config.json`, and re-run before reporting anything. A wrong scope reads as an
67
+ undocumented system, and that is the one mistake that makes the whole audit worthless.
68
+
52
69
  Read `.adsa/report.md` and the evidence under every dimension. The score is a
53
70
  starting point, not the finding.
54
71
 
@@ -80,8 +97,21 @@ so every invention is a gap the system never declared.
80
97
 
81
98
  ## Phase 4 — Compare
82
99
 
83
- Say where the system stands against systems that have done this work, by trait, not
84
- by vibe. The ones worth checking, and what each is known for:
100
+ The reference set is measured, not remembered: `rubric/reference.json` in the tool
101
+ holds every public design system measured with this rubric, at a named commit, and
102
+ the terminal already told the reader where their score sits in it. Quote that, and
103
+ reproduce any figure you are unsure of with `npx adsa-cli audit <clone>`.
104
+
105
+ Two things to say out loud, because a reader will otherwise assume the opposite:
106
+
107
+ - **45 is nobody's score.** The best-documented public system measured reaches 33.
108
+ A number in the twenties is the middle of the field, not a failing grade.
109
+ - **This is not a ranking of design systems.** It measures what a coding agent can
110
+ find in the repository. A system whose documentation lives on an excellent website
111
+ scores low here and may be the better design system for people.
112
+
113
+ Then say where the system stands by trait, not by vibe. The ones worth checking, and
114
+ what each is known for:
85
115
 
86
116
  | System | What to look at |
87
117
  | :-- | :-- |
@@ -111,6 +141,9 @@ Then say what to do first, in one sentence, and stop. Do not pad.
111
141
 
112
142
  ## Phase 6 — Fix
113
143
 
144
+ Only after the reader has seen the findings, and one fix at a time unless they ask
145
+ for more. `fix --all` is theirs to run, not yours to propose as the first move.
146
+
114
147
  ```bash
115
148
  npx adsa-cli fix --list # what is automatic and what is a brief
116
149
  npx adsa-cli fix agents-md # writes files
@@ -126,6 +159,8 @@ After the fixes: `npx adsa-cli audit` again. The delta is the deliverable.
126
159
  ## Rules
127
160
 
128
161
  - Never invent a finding. Every claim names a file, a line or a command output.
162
+ - Never retype the tool's output. Paste it.
163
+ - A score without its band and its standing is a number nobody can act on.
129
164
  - Report a dimension you could not assess as not assessed. Do not guess a number.
130
165
  - Quote the repository, not your memory of similar repositories.
131
166
  - The score belongs to a version. Say which one you audited.