adsa-cli 0.1.2 → 0.1.5

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/README.md CHANGED
@@ -34,7 +34,7 @@ fails when the score drops, so documentation debt cannot land quietly next to a
34
34
  feature. `adsa badge` prints the README line for your own score — this is what the
35
35
  example system in this repo earns after its fixes:
36
36
 
37
- ![Agent-ready 23/45](https://img.shields.io/badge/agent--ready-23%2F45-a2650b)
37
+ ![Agent-ready 23/45](https://img.shields.io/badge/agent--ready-23%2F45-a16207)
38
38
 
39
39
  **The experiment.** `adsa eval` gives an agent a real page to build, then counts what
40
40
  it invented. It is the only number that has ever changed anyone's mind.
@@ -42,6 +42,73 @@ it invented. It is the only number that has ever changed anyone's mind.
42
42
  **An MCP server.** `adsa mcp` serves the guides as tools, so agents query the docs of
43
43
  the version checked out in front of them instead of reading files and guessing.
44
44
 
45
+ ## What a score means
46
+
47
+ 13 public design systems, audited with this rubric at the commit named in
48
+ `rubric/reference.json`, on 2026-09-10:
49
+
50
+ | Score | System | Repository | Package audited |
51
+ | :-- | :-- | :-- | :-- |
52
+ | 35/45 | Astryx · Meta | `facebook/astryx` | `@astryxdesign/core` |
53
+ | 29/45 | Chakra UI | `chakra-ui/chakra-ui` | `@chakra-ui/react` |
54
+ | 25/45 | React Spectrum · Adobe | `adobe/react-spectrum` | `@react-spectrum/s2` |
55
+ | 25/45 | shadcn/ui | `shadcn-ui/ui` | `v4` |
56
+ | 23/45 | HeroUI | `heroui-inc/heroui` | `@heroui/react` |
57
+ | 23/45 | Polaris · Shopify | `Shopify/polaris` | `@shopify/polaris` |
58
+ | 21/45 | BoardUI | `BoardUI/boardui` | `boardui@0.5.5` — npx boardui add + skill, in the starter repo |
59
+ | 21/45 | Mantine | `mantinedev/mantine` | `@mantine/core` |
60
+ | 19/45 | Base UI · MUI | `mui/base-ui` | `@base-ui/react` |
61
+ | 19/45 | Carbon · IBM | `carbon-design-system/carbon` | `@carbon/react` |
62
+ | 17/45 | Primer · GitHub | `primer/react` | `@primer/react` |
63
+ | 15/45 | Untitled UI React | `untitleduico/react` | `@untitledui/react` |
64
+ | 13/45 | Radix Primitives · WorkOS | `radix-ui/primitives` | `radix-ui` |
65
+
66
+ Two things follow, and both are the point of publishing this table.
67
+
68
+ **45 is nobody's score.** The best-documented public design system reaches 35. The
69
+ bands are anchored to that measured field rather than to a wish: **31+ agent-ready**,
70
+ **24–30 good foundation**, **15–23 gaps to address**, **below 15 not ready**. A system
71
+ in the low twenties is in the middle of the field, not failing.
72
+
73
+ **This is not a ranking of design systems.** It measures one thing: what a coding
74
+ agent can find in the repository it is working in. A system whose documentation lives
75
+ on an excellent website scores low here and may well be the better system for people.
76
+
77
+ Every row is reproducible: clone the repository and run `npx adsa-cli audit <clone>`.
78
+ `npx adsa-cli reference` prints this table from the tool.
79
+
80
+ ## Systems that are not component libraries
81
+
82
+ Plenty of design systems are not a package of React components with a guide per
83
+ component, and that is a decision rather than an omission — a stylesheet and a class
84
+ vocabulary survives contact with engineers on four different stacks, which is why
85
+ small teams keep choosing it.
86
+
87
+ **A stylesheet is read as a surface.** When nothing is importable, the class
88
+ vocabulary is the API: the classes in the shipped stylesheet are counted as the
89
+ components, elements and modifiers fold into the block they belong to
90
+ (`card__title`, `card-footer` → `card`), utilities and state classes are left out, and
91
+ a class counts as documented when an example applies it — `.dialog` in prose, or
92
+ `class="dialog"` in an HTML block.
93
+
94
+ **Examples are read in the language they are written in.** HTML, CSS, Vue, Svelte and
95
+ Astro blocks count, alongside TypeScript, Swift and Kotlin.
96
+
97
+ **Tokens are read from the stylesheet, not from a heading.** Custom properties
98
+ declared at the root — including inside Tailwind's `@theme` — are named decisions in
99
+ the format the browser itself reads, exactly as an `.xcassets` catalog or an Android
100
+ `colors.xml` already was. A page documenting them counts whether or not its title
101
+ contains the word "token", and the hex inside `--brand: #0af` is a definition, not a
102
+ raw value to be penalised.
103
+
104
+ **A dimension that cannot apply is dropped from the maximum.** A repository with no
105
+ importable surface and no component source files at all is not asked whether every
106
+ component has a guide: `docs-coverage` and `docs-freshness` are skipped and the score
107
+ is out of 35, and the comparison against the field is recomputed on the same seven
108
+ dimensions. This only happens when the shape is unambiguous — a repository full of
109
+ `.tsx` files where the scan found no exports is this tool failing, and it keeps the
110
+ low score and says so on the `scanned:` line.
111
+
45
112
  ## The nine dimensions
46
113
 
47
114
  | | Dimension | The question |
@@ -79,6 +146,39 @@ adsa rubric The nine dimensions in full
79
146
  ```
80
147
 
81
148
  `--json` everywhere. `--dry-run` on `fix`. `--gate --min <n>` on `audit`.
149
+ `--workspace <name>` and `--no-workspace` on everything that reads a repository.
150
+
151
+ ## Monorepos
152
+
153
+ A design system usually is not the repository — it is one package in it, and the
154
+ things an agent needs are spread across both. `adsa` reads them where they are:
155
+
156
+ | What | Where it looks |
157
+ | :-- | :-- |
158
+ | Components, guides, tokens | The package being audited |
159
+ | `AGENTS.md`, `CLAUDE.md` | The package, then the repository that declares it |
160
+ | CI workflows, root scripts | The repository |
161
+ | MCP server, `llms.txt`, CLI, skills, commands | The repository, including sibling workspaces |
162
+
163
+ "The repository that declares it" is exact: an ancestor counts only when its
164
+ `workspaces` or `pnpm-workspace.yaml` actually lists this package. A design system
165
+ vendored inside an unrelated repo inherits nothing, because it was promised nothing.
166
+
167
+ Detection picks the workspace that looks most like the design system and says which
168
+ one it picked. When it picks wrong, `--workspace @acme/ui` names one by hand and
169
+ `--no-workspace` audits the directory exactly as given. Every run prints the
170
+ directories it read, because *not found* and *never looked* are otherwise the same
171
+ sentence — which is the mistake this tool exists to stop an agent from making.
172
+
173
+ ## Where guides can live
174
+
175
+ `guidelines/`, `docs/components/`, `docs/`, a Swift package's `.docc` bundle — and
176
+ next to the component itself, as `Button/Button.spec.md`, `Card/Card.docs.md` or
177
+ `Field/Field.guide.md`. A colocated guide is a deliberate layout, not a missing one.
178
+
179
+ An agent surface counts whether the repository commits it or serves it: an MCP
180
+ endpoint behind a route handler, an `llms.txt` generated from the live docs, and a
181
+ CLI published from a sibling package are all things an agent can query.
82
182
 
83
183
  ## The skill
84
184
 
@@ -94,7 +194,7 @@ gets it wrong, `adsa.config.json`:
94
194
 
95
195
  ```json
96
196
  {
97
- "guides": ["guidelines"],
197
+ "guides": ["guidelines", "src/components"],
98
198
  "source": ["src/components"],
99
199
  "forbidden": ["lucide-react"],
100
200
  "skip": ["patterns"],
@@ -105,6 +205,9 @@ gets it wrong, `adsa.config.json`:
105
205
  `skip` is for dimensions that genuinely do not apply — a primitives-only library has
106
206
  no page patterns, and a skipped dimension lowers the maximum instead of the score.
107
207
 
208
+ Naming `guides` yourself turns off the repository-level guide folder that a
209
+ workspace package otherwise inherits: an explicit list is a complete answer.
210
+
108
211
  ## Platforms
109
212
 
110
213
  ADSA detects which platform a design system is built for from real evidence in the
@@ -145,23 +248,35 @@ not assessed — it never guesses a number.
145
248
 
146
249
  ## Try it
147
250
 
251
+ One design system, in the three states of the loop. All three are in the repository,
252
+ and all three numbers are a command away:
253
+
148
254
  ```bash
149
255
  git clone https://github.com/nrmk13/ADSA && cd ADSA
150
- node bin/adsa.mjs audit example/design-system # 9/45
151
- node bin/adsa.mjs fix --all --cwd example/design-system
152
- node bin/adsa.mjs audit example/design-system # 23/45
256
+ node bin/adsa.mjs audit example/design-system-as-found # 11/45 — as most systems are
257
+ node bin/adsa.mjs audit example/design-system # 23/45 — after `fix --all`
258
+ node bin/adsa.mjs audit example/design-system-ready # 45/45 — after the briefs
153
259
  ```
154
260
 
155
- `example/agent-output` is what an agent built against that system before it was
156
- fixed. `adsa eval score example/agent-output --system example/design-system` counts
157
- the three components it invented.
261
+ The middle state is what the tool writes for you: instructions, an MCP entry, a gap
262
+ list, a CI workflow. The last state is what it cannot write for you — generated prop
263
+ tables, keyboard contracts, page patterns, and checks in CI that fail when a guide
264
+ starts lying. `example/design-system-ready/scripts` is that work, done: four checks,
265
+ each with a test in `test/checks.test.mjs` that breaks the docs on purpose and asserts
266
+ the check goes red. It is the only 45/45 anywhere, and it is a six-component system
267
+ with a person's whole afternoon in it.
268
+
269
+ `example/agent-output` is what an agent built against the first state.
270
+ `adsa eval score example/agent-output --system example/design-system` counts the three
271
+ components it invented.
158
272
 
159
- Small fixtures for the other platforms live under `test/fixtures/` — a React Native
160
- library, a Swift package documented with DocC, and a Kotlin/Compose library and
161
- run the same loop:
273
+ Small fixtures live under `test/fixtures/` — a React Native library, a Swift package
274
+ documented with DocC, a Kotlin/Compose library, and a pnpm monorepo whose guides sit
275
+ next to its components — and run the same loop:
162
276
 
163
277
  ```bash
164
278
  node bin/adsa.mjs audit test/fixtures/swift-ds
279
+ node bin/adsa.mjs audit test/fixtures/monorepo-ds # 39/45, audited from the root
165
280
  ```
166
281
 
167
282
  ## Licence
package/bin/adsa.mjs CHANGED
@@ -8,16 +8,20 @@
8
8
  * adsa eval init | score <dir> run the experiment and measure the result
9
9
  * adsa mcp serve the guides as MCP tools
10
10
  * adsa doctor is this repo wired up for agents
11
+ * adsa reference the field: what other design systems score
11
12
  * adsa badge the README badge for the committed score
12
13
  */
14
+ import { spawn } from "node:child_process";
13
15
  import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
14
- import { join, relative, resolve } from "node:path";
16
+ import { basename, join, relative, resolve } from "node:path";
15
17
  import { pathToFileURL } from "node:url";
16
- import { loadConfig, resolveTarget } from "../lib/config.mjs";
18
+ import { loadConfig, resolveRepoRoot } from "../lib/config.mjs";
19
+ import { resolveTarget } from "../lib/target.mjs";
17
20
  import { scan } from "../lib/scan.mjs";
18
21
  import { RUBRIC, score, scoreFile } from "../lib/score.mjs";
19
22
  import { buildTodo, html, markdown } from "../lib/report.mjs";
20
- import { badgeEndpoint, badgeMarkdown } from "../lib/badge.mjs";
23
+ import { band, badgeEndpoint, badgeMarkdown } from "../lib/badge.mjs";
24
+ import { REFERENCE, scoredIds, standing } from "../lib/reference.mjs";
21
25
  import { exists, isDir } from "../lib/fsx.mjs";
22
26
  import { DIR, appendHistory, compare, readHistory, write } from "../lib/history.mjs";
23
27
  import { FIXES, applyFix } from "../lib/fix.mjs";
@@ -42,6 +46,7 @@ Usage
42
46
  adsa eval score <dir> Measure what the agent actually built
43
47
  adsa mcp Serve the guides as MCP tools over stdio
44
48
  adsa doctor Check this repo's agent surface
49
+ adsa reference The measured field: public design systems, same rubric
45
50
  adsa badge Print the README badge for the committed score
46
51
  adsa rubric Print the nine dimensions and what each level means
47
52
 
@@ -53,13 +58,16 @@ Options
53
58
  --dry-run fix: show what would change, write nothing
54
59
  --system <dir> eval score: the design system the project should use
55
60
  --cwd <dir> fix: the repository to change (default: .)
61
+ --workspace <name> audit this workspace package, not the detected one
62
+ --no-workspace audit the given directory as it stands
63
+ --no-open audit: do not open the report in a browser
56
64
  --quiet Only the score line
57
65
  --full docs: keep the prose
58
66
  --color / --no-color Force ANSI colour on or off (default: on for a terminal)
59
67
  `;
60
68
 
61
69
  export function parseArgs(argv) {
62
- const flags = { out: null, json: false, gate: false, min: null, dryRun: false, system: null, quiet: false, list: false, all: false, cwd: null };
70
+ const flags = { out: null, json: false, gate: false, min: null, dryRun: false, system: null, quiet: false, list: false, all: false, cwd: null, workspace: null, noWorkspace: false };
63
71
  const positional = [];
64
72
  for (let i = 0; i < argv.length; i++) {
65
73
  const arg = argv[i];
@@ -72,6 +80,10 @@ export function parseArgs(argv) {
72
80
  else if (arg === "--color") flags.color = true;
73
81
  else if (arg === "--no-color") flags.color = false;
74
82
  else if (arg === "--all") flags.all = true;
83
+ else if (arg === "--open") flags.open = true;
84
+ else if (arg === "--no-open") flags.open = false;
85
+ else if (arg === "--no-workspace") flags.noWorkspace = true;
86
+ else if (arg === "--workspace") flags.workspace = argv[++i];
75
87
  else if (arg === "--out") flags.out = argv[++i];
76
88
  else if (arg === "--min") flags.min = Number(argv[++i]);
77
89
  else if (arg === "--system") flags.system = argv[++i];
@@ -83,12 +95,22 @@ export function parseArgs(argv) {
83
95
  return { command: positional[0], args: positional.slice(1), flags };
84
96
  }
85
97
 
98
+ /** A bad --workspace is the reader's mistake to fix, not a reason to audit something else. */
99
+ class TargetError extends Error {}
100
+
101
+ /** Set once per run so every command resolves the same target the reader asked for. */
102
+ let targetFlags = {};
103
+
86
104
  function load(dir) {
87
105
  const given = resolve(dir || ".");
88
- const target = resolveTarget(given);
106
+ const target = resolveTarget(given, targetFlags);
107
+ if (target.error) throw new TargetError(target.error);
89
108
  const root = target.dir;
90
109
  const config = loadConfig(root);
91
- const facts = scan(root, config);
110
+ // Agent instructions, CI and the agent surface belong to the repository that
111
+ // declares this package, not to the package directory.
112
+ const repoRoot = targetFlags.noWorkspace ? root : resolveRepoRoot(root) || root;
113
+ const facts = scan(root, config, repoRoot);
92
114
  facts.workspace = target.note;
93
115
  return { root, config, facts };
94
116
  }
@@ -109,6 +131,7 @@ export async function run(argv, io = {}) {
109
131
  const json = (v) => out(JSON.stringify(v, null, 2));
110
132
  const { command, args, flags } = parseArgs(argv);
111
133
  if (flags.color !== undefined) setColor(flags.color);
134
+ targetFlags = { workspace: flags.workspace, noWorkspace: flags.noWorkspace };
112
135
  if (flags.help || !command) {
113
136
  out(HELP);
114
137
  return 0;
@@ -118,6 +141,18 @@ export async function run(argv, io = {}) {
118
141
  return 1;
119
142
  }
120
143
 
144
+ try {
145
+ return await dispatch(command, args, flags, io, out, json);
146
+ } catch (error) {
147
+ if (error instanceof TargetError) {
148
+ out(red(error.message));
149
+ return 1;
150
+ }
151
+ throw error;
152
+ }
153
+ }
154
+
155
+ async function dispatch(command, args, flags, io, out, json) {
121
156
  switch (command) {
122
157
  case "audit":
123
158
  return cmdAudit(args[0], flags, out, json);
@@ -136,6 +171,8 @@ export async function run(argv, io = {}) {
136
171
  return cmdDocs(args, flags, out, json);
137
172
  case "doctor":
138
173
  return cmdDoctor(args[0], flags, out, json);
174
+ case "reference":
175
+ return cmdReference(flags, out, json);
139
176
  case "badge":
140
177
  return cmdBadge(args[0], flags, out, json);
141
178
  case "rubric":
@@ -146,6 +183,64 @@ export async function run(argv, io = {}) {
146
183
  }
147
184
  }
148
185
 
186
+ /**
187
+ * Which directories the run actually read. "Not found" and "never looked" print the
188
+ * same in a report, and a reader cannot tell a bad layout from a bad scan without it.
189
+ */
190
+ function scannedLine(facts) {
191
+ const s = facts.scanned;
192
+ // A stylesheet system has no source directory and is not supposed to: naming the
193
+ // file the classes were read from is the honest version of "source none found".
194
+ const source = s.shape === "classes" ? `classes from ${s.stylesheets.join(", ")}` : `source ${s.source.length ? s.source.join(", ") : "none found"}`;
195
+ const parts = [`guides ${s.guides.length ? s.guides.join(", ") : "none found"}`, source];
196
+ if (s.shape === "docs-only") parts.push("no component surface: two dimensions do not apply");
197
+ if (s.colocatedGuides) parts.push(`${s.colocatedGuides} colocated`);
198
+ if (facts.monorepo) parts.push(`repo-level files from ${basename(facts.repoRoot)}/`);
199
+ return ` scanned: ${parts.join(" · ")}`;
200
+ }
201
+
202
+ /**
203
+ * Whether to hand the report to the reader's browser. Opening a window is the kind of
204
+ * thing a tool should only do when a person is watching: a TTY, no CI environment
205
+ * variable, and not asked for machine output.
206
+ */
207
+ function shouldOpen(flags) {
208
+ if (flags.open === false || flags.json || flags.quiet || flags.gate) return false;
209
+ if (flags.open === true) return true;
210
+ if (process.env.CI || process.env.ADSA_NO_OPEN) return false;
211
+ return Boolean(process.stdout.isTTY);
212
+ }
213
+
214
+ function openInBrowser(file, out) {
215
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
216
+ try {
217
+ spawn(cmd, [pathToFileURL(file).href], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
218
+ } catch {
219
+ out(dim(" (could not open a browser — the path above is the file)"));
220
+ }
221
+ }
222
+
223
+ /* ------------------------------------------------------------ reference */
224
+
225
+ /** The measured field, so a reader can see what 45 is worth without leaving the terminal. */
226
+ function cmdReference(flags, out, json) {
227
+ if (flags.json) {
228
+ json(REFERENCE);
229
+ return 0;
230
+ }
231
+ out(`Public design systems audited with rubric ${REFERENCE.rubric}, ${REFERENCE.measured}:\n`);
232
+ for (const s of REFERENCE.systems) {
233
+ const label = `${s.name}${s.org ? ` · ${s.org}` : ""}`;
234
+ out(` ${String(s.total).padStart(2)}/${s.max} ${byScore(bandScore(s.total, s.max), label.padEnd(26))} ${dim(`${s.repo}@${s.commit} ${s.package}`)}`);
235
+ }
236
+ out(dim("\n Not a ranking of design systems: it measures what an agent can find in the repository."));
237
+ out(dim(" Reproduce any row with `npx adsa-cli audit <clone>`."));
238
+ return 0;
239
+ }
240
+
241
+ /** The 1/3/5 colour the bar chart uses, borrowed for a whole score. */
242
+ const bandScore = (total, max) => ({ ready: 5, good: 5, gaps: 3, unready: 1 })[band(total, max).id];
243
+
149
244
  /* ---------------------------------------------------------------- audit */
150
245
 
151
246
  function cmdAudit(dir, flags, out, json) {
@@ -167,14 +262,16 @@ function cmdAudit(dir, flags, out, json) {
167
262
  const delta = compare(previous, file);
168
263
 
169
264
  if (flags.json) {
170
- json({ ...file, dimensions: scored.dimensions, delta });
265
+ json({ ...file, scanned: facts.scanned, dimensions: scored.dimensions, delta });
171
266
  } else {
172
- const band = scored.total / scored.max;
173
- // Same bands the report uses, so the terminal and the HTML never disagree.
174
- const paint = (t) => (band >= 0.85 ? green(t) : band >= 0.35 ? yellow(t) : red(t));
175
- out(`${facts.name}${facts.version ? " " + facts.version : ""} agent readiness ${paint(`${scored.total}/${scored.max}`)}`);
267
+ // One band table, in lib/badge.mjs, so the terminal, the report and the
268
+ // badge never disagree about what a number means.
269
+ const verdict = band(scored.total, scored.max);
270
+ const paint = (t) => (verdict.id === "ready" || verdict.id === "good" ? green(t) : verdict.id === "gaps" ? yellow(t) : red(t));
271
+ out(`${facts.name}${facts.version ? " " + facts.version : ""} — agent readiness ${paint(`${scored.total}/${scored.max}`)} ${paint(verdict.label)}`);
176
272
  if (facts.workspace) out(` ${facts.workspace}`);
177
273
  if (!flags.quiet) {
274
+ out(dim(scannedLine(facts)));
178
275
  out("");
179
276
  for (const d of scored.dimensions) {
180
277
  const bar = d.skipped ? dim(" skip") : byScore(d.score, "█".repeat(d.score)) + dim("·".repeat(5 - d.score));
@@ -192,6 +289,8 @@ function cmdAudit(dir, flags, out, json) {
192
289
  const move = `Since the last run: ${delta.from} → ${delta.to} (${delta.delta >= 0 ? "+" : ""}${delta.delta})`;
193
290
  out(delta.delta > 0 ? green(move) : delta.delta < 0 ? red(move) : dim(move));
194
291
  }
292
+ // A score out of 45 answers nothing until you know what 45 is worth.
293
+ out(dim(standing(scored.total, scoredIds(scored)).sentence));
195
294
  }
196
295
  }
197
296
 
@@ -206,7 +305,12 @@ function cmdAudit(dir, flags, out, json) {
206
305
  const near = relative(process.cwd(), artefacts);
207
306
  const shown = !near ? "." : near.startsWith("..") ? artefacts : near;
208
307
  if (!flags.json && !flags.quiet) {
209
- out(dim(`\nReport: ${join(shown, "report.html")} · score: ${join(shown, "score.json")}`));
308
+ const report = join(artefacts, "report.html");
309
+ out(`\nReport: ${pathToFileURL(report).href}`);
310
+ out(dim(` ${join(shown, "report.html")} · score: ${join(shown, "score.json")}`));
311
+ // The report is the deliverable, so it opens. `--no-open` for a script, and
312
+ // it never opens on its own in CI or when the output is being piped.
313
+ if (shouldOpen(flags)) openInBrowser(report, out);
210
314
  // Say once what the directory is for, rather than leave someone to guess
211
315
  // whether a new folder in their repository belongs in the commit.
212
316
  if (fresh && !flags.out && exists(join(root, ".git"))) {
@@ -365,10 +469,11 @@ function cmdDoctor(dir, flags, out, json) {
365
469
  const checks = [];
366
470
  const add = (id, status, message, fix) => checks.push({ id, status, message, ...(fix ? { fix } : {}) });
367
471
  const agents = facts.agentFiles.find((a) => a.mentionsPackage);
368
- agents ? add("agent-docs", "pass", `${agents.file} references ${facts.name}.`) : add("agent-docs", "fail", "No agent instructions mention this system.", "adsa fix agents-md");
369
- facts.machine.declaredServers.includes("adsa") || facts.machine.mcpInPackage
370
- ? add("mcp", "pass", "An MCP server is registered for this repository.")
371
- : add("mcp", "warn", "No MCP server registered, so agents read files instead of querying.", "npx adsa-cli fix mcp-config");
472
+ agents ? add("agent-docs", "pass", `${agents.label || agents.file} references ${facts.name}.`) : add("agent-docs", "fail", "No agent instructions mention this system.", "adsa fix agents-md");
473
+ const served = facts.machine.servedMcp || [];
474
+ if (served.length) add("mcp", "pass", `This repository serves an MCP server: ${served.slice(0, 2).join(", ")}.`);
475
+ else if (facts.machine.declaredServers.includes("adsa") || facts.machine.mcpInPackage) add("mcp", "pass", "An MCP server is registered for this repository.");
476
+ else add("mcp", "warn", "No MCP server registered, so agents read files instead of querying.", "npx adsa-cli fix mcp-config");
372
477
  facts.gaps.file ? add("gaps", "pass", `${facts.gaps.file} lists known absences.`) : add("gaps", "fail", "Nothing states what the system does not have.", "adsa fix gaps-file");
373
478
  facts.verification.workflows.length ? add("ci", "pass", `CI: ${facts.verification.workflows.join(", ")}.`) : add("ci", "warn", "No CI workflow found.", "adsa fix ci-workflow");
374
479
  facts.config.file ? add("config", "pass", `${facts.config.file} present.`) : add("config", "warn", "No adsa.config.json — detection is doing the guessing.");
package/lib/badge.mjs CHANGED
@@ -1,19 +1,35 @@
1
- /** README badge. Static shields.io URL, plus an endpoint file for repos that prefer one.
1
+ /** The four verdicts, and the README badge that carries one of them.
2
2
  *
3
- * The four bands are the four verdicts the report uses, and the colours are the
4
- * page palette darkened until white text on them clears 4.5:1 — a badge is small
5
- * text on a solid fill, and it is the one artifact that ends up on someone else's
6
- * README. One scale, so the badge, the report and the page never disagree. */
7
- const COLORS = [
8
- [0.85, "166534", "agent-ready"],
9
- [0.6, "15803d", "good foundation"],
10
- [0.35, "a16207", "gaps to address"],
11
- [0, "b91c1c", "not ready"],
3
+ * The thresholds are not a curve and not a guess: the public design systems in `rubric/reference.json` were
4
+ * audited with this rubric (`rubric/reference.json`), and the field runs from 13/45
5
+ * to 33/45. Nothing reaches 40, and the systems people hold up as the good ones sit
6
+ * between 21 and 33 — so a scale where 38/45 was the first green band called every
7
+ * design system in the world unready, which is a scale that tells you nothing.
8
+ *
9
+ * So the bands are anchored to the measured field: *agent-ready* starts at the level
10
+ * the best-documented system reaches today, and the middle of the field is *gaps to
11
+ * address*, because the middle of the field is where an agent still guesses.
12
+ *
13
+ * The colours are the page palette darkened until white text on them clears 4.5:1 —
14
+ * a badge is small text on a solid fill, and it is the one artifact that ends up on
15
+ * somebody else's README. One scale, so the badge, the report, the terminal and the
16
+ * page never disagree.
17
+ */
18
+ export const BANDS = [
19
+ { floor: 0.68, id: "ready", label: "agent-ready", color: "166534", note: "At the level of the best-documented public design system measured." },
20
+ { floor: 0.53, id: "good", label: "good foundation", color: "15803d", note: "Above most public design systems. An agent can work here, with gaps." },
21
+ { floor: 0.33, id: "gaps", label: "gaps to address", color: "a16207", note: "Where most design systems sit. An agent fills the rest by guessing." },
22
+ { floor: 0, id: "unready", label: "not ready", color: "b91c1c", note: "An agent will invent most of what it needs." },
12
23
  ];
13
24
 
14
- export function badgeColor(total, max) {
25
+ /** @returns {{floor:number,id:string,label:string,color:string,note:string}} */
26
+ export function band(total, max) {
15
27
  const ratio = max ? total / max : 0;
16
- return COLORS.find(([floor]) => ratio >= floor)[1];
28
+ return BANDS.find((b) => ratio >= b.floor);
29
+ }
30
+
31
+ export function badgeColor(total, max) {
32
+ return band(total, max).color;
17
33
  }
18
34
 
19
35
  export function badgeMarkdown(total, max, link) {