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 +126 -11
- package/bin/adsa.mjs +121 -16
- package/lib/badge.mjs +28 -12
- package/lib/config.mjs +123 -47
- package/lib/reference.mjs +49 -0
- package/lib/report.mjs +17 -12
- package/lib/scan.mjs +643 -72
- package/lib/score.mjs +44 -11
- package/lib/target.mjs +82 -0
- package/package.json +1 -1
- package/rubric/reference.json +293 -0
- package/skills/ds-audit/SKILL.md +44 -2
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
|
-

|
|
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
|
|
151
|
-
node bin/adsa.mjs
|
|
152
|
-
node bin/adsa.mjs audit example/design-system
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
160
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
|
|
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
|
-
|
|
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.
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
/**
|
|
1
|
+
/** The four verdicts, and the README badge that carries one of them.
|
|
2
2
|
*
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|