adsa-cli 0.1.1 → 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/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,41 @@ 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
+ | 33/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
+ | 13/45 | Radix Primitives · WorkOS | `radix-ui/primitives` | `radix-ui` |
64
+ | 13/45 | Untitled UI React | `untitleduico/react` | `@untitledui/react` |
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 33. 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
+
45
80
  ## The nine dimensions
46
81
 
47
82
  | | Dimension | The question |
@@ -58,6 +93,10 @@ the version checked out in front of them instead of reading files and guessing.
58
93
 
59
94
  `npx adsa-cli rubric` prints what 1, 3 and 5 mean for each.
60
95
 
96
+ `--out <dir>` puts every artifact of a run together somewhere else — the report, the
97
+ score, the badge and the history — and reads the previous score from there too, so a
98
+ repository can be audited without writing anything into it.
99
+
61
100
  ## Commands
62
101
 
63
102
  ```
@@ -75,6 +114,39 @@ adsa rubric The nine dimensions in full
75
114
  ```
76
115
 
77
116
  `--json` everywhere. `--dry-run` on `fix`. `--gate --min <n>` on `audit`.
117
+ `--workspace <name>` and `--no-workspace` on everything that reads a repository.
118
+
119
+ ## Monorepos
120
+
121
+ A design system usually is not the repository — it is one package in it, and the
122
+ things an agent needs are spread across both. `adsa` reads them where they are:
123
+
124
+ | What | Where it looks |
125
+ | :-- | :-- |
126
+ | Components, guides, tokens | The package being audited |
127
+ | `AGENTS.md`, `CLAUDE.md` | The package, then the repository that declares it |
128
+ | CI workflows, root scripts | The repository |
129
+ | MCP server, `llms.txt`, CLI, skills, commands | The repository, including sibling workspaces |
130
+
131
+ "The repository that declares it" is exact: an ancestor counts only when its
132
+ `workspaces` or `pnpm-workspace.yaml` actually lists this package. A design system
133
+ vendored inside an unrelated repo inherits nothing, because it was promised nothing.
134
+
135
+ Detection picks the workspace that looks most like the design system and says which
136
+ one it picked. When it picks wrong, `--workspace @acme/ui` names one by hand and
137
+ `--no-workspace` audits the directory exactly as given. Every run prints the
138
+ directories it read, because *not found* and *never looked* are otherwise the same
139
+ sentence — which is the mistake this tool exists to stop an agent from making.
140
+
141
+ ## Where guides can live
142
+
143
+ `guidelines/`, `docs/components/`, `docs/`, a Swift package's `.docc` bundle — and
144
+ next to the component itself, as `Button/Button.spec.md`, `Card/Card.docs.md` or
145
+ `Field/Field.guide.md`. A colocated guide is a deliberate layout, not a missing one.
146
+
147
+ An agent surface counts whether the repository commits it or serves it: an MCP
148
+ endpoint behind a route handler, an `llms.txt` generated from the live docs, and a
149
+ CLI published from a sibling package are all things an agent can query.
78
150
 
79
151
  ## The skill
80
152
 
@@ -90,7 +162,7 @@ gets it wrong, `adsa.config.json`:
90
162
 
91
163
  ```json
92
164
  {
93
- "guides": ["guidelines"],
165
+ "guides": ["guidelines", "src/components"],
94
166
  "source": ["src/components"],
95
167
  "forbidden": ["lucide-react"],
96
168
  "skip": ["patterns"],
@@ -101,6 +173,9 @@ gets it wrong, `adsa.config.json`:
101
173
  `skip` is for dimensions that genuinely do not apply — a primitives-only library has
102
174
  no page patterns, and a skipped dimension lowers the maximum instead of the score.
103
175
 
176
+ Naming `guides` yourself turns off the repository-level guide folder that a
177
+ workspace package otherwise inherits: an explicit list is a complete answer.
178
+
104
179
  ## Platforms
105
180
 
106
181
  ADSA detects which platform a design system is built for from real evidence in the
@@ -141,23 +216,35 @@ not assessed — it never guesses a number.
141
216
 
142
217
  ## Try it
143
218
 
219
+ One design system, in the three states of the loop. All three are in the repository,
220
+ and all three numbers are a command away:
221
+
144
222
  ```bash
145
223
  git clone https://github.com/nrmk13/ADSA && cd ADSA
146
- node bin/adsa.mjs audit example/design-system # 9/45
147
- node bin/adsa.mjs fix --all --cwd example/design-system
148
- node bin/adsa.mjs audit example/design-system # 23/45
224
+ node bin/adsa.mjs audit example/design-system-as-found # 11/45 — as most systems are
225
+ node bin/adsa.mjs audit example/design-system # 23/45 — after `fix --all`
226
+ node bin/adsa.mjs audit example/design-system-ready # 45/45 — after the briefs
149
227
  ```
150
228
 
151
- `example/agent-output` is what an agent built against that system before it was
152
- fixed. `adsa eval score example/agent-output --system example/design-system` counts
153
- the three components it invented.
229
+ The middle state is what the tool writes for you: instructions, an MCP entry, a gap
230
+ list, a CI workflow. The last state is what it cannot write for you — generated prop
231
+ tables, keyboard contracts, page patterns, and checks in CI that fail when a guide
232
+ starts lying. `example/design-system-ready/scripts` is that work, done: four checks,
233
+ each with a test in `test/checks.test.mjs` that breaks the docs on purpose and asserts
234
+ the check goes red. It is the only 45/45 anywhere, and it is a six-component system
235
+ with a person's whole afternoon in it.
236
+
237
+ `example/agent-output` is what an agent built against the first state.
238
+ `adsa eval score example/agent-output --system example/design-system` counts the three
239
+ components it invented.
154
240
 
155
- Small fixtures for the other platforms live under `test/fixtures/` — a React Native
156
- library, a Swift package documented with DocC, and a Kotlin/Compose library and
157
- run the same loop:
241
+ Small fixtures live under `test/fixtures/` — a React Native library, a Swift package
242
+ documented with DocC, a Kotlin/Compose library, and a pnpm monorepo whose guides sit
243
+ next to its components — and run the same loop:
158
244
 
159
245
  ```bash
160
246
  node bin/adsa.mjs audit test/fixtures/swift-ds
247
+ node bin/adsa.mjs audit test/fixtures/monorepo-ds # 39/45, audited from the root
161
248
  ```
162
249
 
163
250
  ## Licence
package/bin/adsa.mjs CHANGED
@@ -8,17 +8,21 @@
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, 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";
21
- import { isDir } from "../lib/fsx.mjs";
23
+ import { band, badgeEndpoint, badgeMarkdown } from "../lib/badge.mjs";
24
+ import { REFERENCE, standing } from "../lib/reference.mjs";
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";
24
28
  import { evaluate, taskFile } from "../lib/eval.mjs";
@@ -42,24 +46,28 @@ 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
 
48
53
  Options
49
54
  --json Machine-readable output
50
- --out <dir> Where to write the report (default: .adsa)
55
+ --out <dir> Write every artifact here instead of .adsa/
51
56
  --gate Exit non-zero if the score dropped, or is below --min
52
57
  --min <n> Minimum acceptable total for --gate
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,60 @@ 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
+ const parts = [`guides ${s.guides.length ? s.guides.join(", ") : "none found"}`, `source ${s.source.length ? s.source.join(", ") : "none found"}`];
193
+ if (s.colocatedGuides) parts.push(`${s.colocatedGuides} colocated`);
194
+ if (facts.monorepo) parts.push(`repo-level files from ${basename(facts.repoRoot)}/`);
195
+ return ` scanned: ${parts.join(" · ")}`;
196
+ }
197
+
198
+ /**
199
+ * Whether to hand the report to the reader's browser. Opening a window is the kind of
200
+ * thing a tool should only do when a person is watching: a TTY, no CI environment
201
+ * variable, and not asked for machine output.
202
+ */
203
+ function shouldOpen(flags) {
204
+ if (flags.open === false || flags.json || flags.quiet || flags.gate) return false;
205
+ if (flags.open === true) return true;
206
+ if (process.env.CI || process.env.ADSA_NO_OPEN) return false;
207
+ return Boolean(process.stdout.isTTY);
208
+ }
209
+
210
+ function openInBrowser(file, out) {
211
+ const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
212
+ try {
213
+ spawn(cmd, [pathToFileURL(file).href], { stdio: "ignore", detached: true, shell: process.platform === "win32" }).unref();
214
+ } catch {
215
+ out(dim(" (could not open a browser — the path above is the file)"));
216
+ }
217
+ }
218
+
219
+ /* ------------------------------------------------------------ reference */
220
+
221
+ /** The measured field, so a reader can see what 45 is worth without leaving the terminal. */
222
+ function cmdReference(flags, out, json) {
223
+ if (flags.json) {
224
+ json(REFERENCE);
225
+ return 0;
226
+ }
227
+ out(`Public design systems audited with rubric ${REFERENCE.rubric}, ${REFERENCE.measured}:\n`);
228
+ for (const s of REFERENCE.systems) {
229
+ const label = `${s.name}${s.org ? ` · ${s.org}` : ""}`;
230
+ out(` ${String(s.total).padStart(2)}/${s.max} ${byScore(bandScore(s.total, s.max), label.padEnd(26))} ${dim(`${s.repo}@${s.commit} ${s.package}`)}`);
231
+ }
232
+ out(dim("\n Not a ranking of design systems: it measures what an agent can find in the repository."));
233
+ out(dim(" Reproduce any row with `npx adsa-cli audit <clone>`."));
234
+ return 0;
235
+ }
236
+
237
+ /** The 1/3/5 colour the bar chart uses, borrowed for a whole score. */
238
+ const bandScore = (total, max) => ({ ready: 5, good: 5, gaps: 3, unready: 1 })[band(total, max).id];
239
+
149
240
  /* ---------------------------------------------------------------- audit */
150
241
 
151
242
  function cmdAudit(dir, flags, out, json) {
@@ -157,20 +248,26 @@ function cmdAudit(dir, flags, out, json) {
157
248
  return 1;
158
249
  }
159
250
  const scored = score(facts, config);
160
- const outDir = flags.out || DIR;
251
+ // Everything one run writes lands together. --out is read from where the command
252
+ // was typed, so an absolute path is absolute and a relative one is relative to
253
+ // the reader, not to the repository being audited.
254
+ const artefacts = flags.out ? resolve(process.cwd(), flags.out) : join(root, DIR);
255
+ const fresh = !exists(join(artefacts, "score.json"));
161
256
  const file = scoreFile(facts, scored);
162
- const previous = readJson(join(root, DIR, "score.json"));
257
+ const previous = readJson(join(artefacts, "score.json"));
163
258
  const delta = compare(previous, file);
164
259
 
165
260
  if (flags.json) {
166
- json({ ...file, dimensions: scored.dimensions, delta });
261
+ json({ ...file, scanned: facts.scanned, dimensions: scored.dimensions, delta });
167
262
  } else {
168
- const band = scored.total / scored.max;
169
- // Same bands the report uses, so the terminal and the HTML never disagree.
170
- const paint = (t) => (band >= 0.85 ? green(t) : band >= 0.35 ? yellow(t) : red(t));
171
- out(`${facts.name}${facts.version ? " " + facts.version : ""} agent readiness ${paint(`${scored.total}/${scored.max}`)}`);
263
+ // One band table, in lib/badge.mjs, so the terminal, the report and the
264
+ // badge never disagree about what a number means.
265
+ const verdict = band(scored.total, scored.max);
266
+ const paint = (t) => (verdict.id === "ready" || verdict.id === "good" ? green(t) : verdict.id === "gaps" ? yellow(t) : red(t));
267
+ out(`${facts.name}${facts.version ? " " + facts.version : ""} — agent readiness ${paint(`${scored.total}/${scored.max}`)} ${paint(verdict.label)}`);
172
268
  if (facts.workspace) out(` ${facts.workspace}`);
173
269
  if (!flags.quiet) {
270
+ out(dim(scannedLine(facts)));
174
271
  out("");
175
272
  for (const d of scored.dimensions) {
176
273
  const bar = d.skipped ? dim(" skip") : byScore(d.score, "█".repeat(d.score)) + dim("·".repeat(5 - d.score));
@@ -188,17 +285,35 @@ function cmdAudit(dir, flags, out, json) {
188
285
  const move = `Since the last run: ${delta.from} → ${delta.to} (${delta.delta >= 0 ? "+" : ""}${delta.delta})`;
189
286
  out(delta.delta > 0 ? green(move) : delta.delta < 0 ? red(move) : dim(move));
190
287
  }
288
+ // A score out of 45 answers nothing until you know what 45 is worth.
289
+ out(dim(standing(scored.total).sentence));
191
290
  }
192
291
  }
193
292
 
194
- const history = appendHistory(root, { date: file.generatedAt, total: file.total, max: file.max, dimensions: file.dimensions });
195
- write(join(root, DIR, "score.json"), file);
196
- write(join(root, DIR, "badge.json"), badgeEndpoint(file.total, file.max));
197
- const reportDir = join(root, outDir);
198
- mkdirSync(reportDir, { recursive: true });
199
- writeFileSync(join(reportDir, "report.html"), html(facts, scored, history));
200
- writeFileSync(join(reportDir, "report.md"), markdown(facts, scored));
201
- if (!flags.json && !flags.quiet) out(dim(`\nReport: ${join(outDir, "report.html")} · score: ${join(DIR, "score.json")}`));
293
+ const history = appendHistory(artefacts, { date: file.generatedAt, total: file.total, max: file.max, dimensions: file.dimensions });
294
+ write(join(artefacts, "score.json"), file);
295
+ write(join(artefacts, "badge.json"), badgeEndpoint(file.total, file.max));
296
+ mkdirSync(artefacts, { recursive: true });
297
+ writeFileSync(join(artefacts, "report.html"), html(facts, scored, history));
298
+ writeFileSync(join(artefacts, "report.md"), markdown(facts, scored));
299
+ // A path that climbs out of the current directory reads better absolute than as
300
+ // a ladder of "..".
301
+ const near = relative(process.cwd(), artefacts);
302
+ const shown = !near ? "." : near.startsWith("..") ? artefacts : near;
303
+ if (!flags.json && !flags.quiet) {
304
+ const report = join(artefacts, "report.html");
305
+ out(`\nReport: ${pathToFileURL(report).href}`);
306
+ out(dim(` ${join(shown, "report.html")} · score: ${join(shown, "score.json")}`));
307
+ // The report is the deliverable, so it opens. `--no-open` for a script, and
308
+ // it never opens on its own in CI or when the output is being piped.
309
+ if (shouldOpen(flags)) openInBrowser(report, out);
310
+ // Say once what the directory is for, rather than leave someone to guess
311
+ // whether a new folder in their repository belongs in the commit.
312
+ if (fresh && !flags.out && exists(join(root, ".git"))) {
313
+ out(dim(" score.json, badge.json and history.json are meant to be committed — the badge and the gate read them."));
314
+ out(dim(" report.html and report.md are rewritten every run; ignore them if you would rather not carry them."));
315
+ }
316
+ }
202
317
 
203
318
  if (flags.gate) {
204
319
  const floor = flags.min ?? config.minScore ?? (previous ? previous.total : null);
@@ -350,10 +465,11 @@ function cmdDoctor(dir, flags, out, json) {
350
465
  const checks = [];
351
466
  const add = (id, status, message, fix) => checks.push({ id, status, message, ...(fix ? { fix } : {}) });
352
467
  const agents = facts.agentFiles.find((a) => a.mentionsPackage);
353
- agents ? add("agent-docs", "pass", `${agents.file} references ${facts.name}.`) : add("agent-docs", "fail", "No agent instructions mention this system.", "adsa fix agents-md");
354
- facts.machine.declaredServers.includes("adsa") || facts.machine.mcpInPackage
355
- ? add("mcp", "pass", "An MCP server is registered for this repository.")
356
- : add("mcp", "warn", "No MCP server registered, so agents read files instead of querying.", "npx adsa-cli fix mcp-config");
468
+ 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");
469
+ const served = facts.machine.servedMcp || [];
470
+ if (served.length) add("mcp", "pass", `This repository serves an MCP server: ${served.slice(0, 2).join(", ")}.`);
471
+ else if (facts.machine.declaredServers.includes("adsa") || facts.machine.mcpInPackage) add("mcp", "pass", "An MCP server is registered for this repository.");
472
+ else add("mcp", "warn", "No MCP server registered, so agents read files instead of querying.", "npx adsa-cli fix mcp-config");
357
473
  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");
358
474
  facts.verification.workflows.length ? add("ci", "pass", `CI: ${facts.verification.workflows.join(", ")}.`) : add("ci", "warn", "No CI workflow found.", "adsa fix ci-workflow");
359
475
  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) {