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/README.md +94 -11
- package/bin/adsa.mjs +117 -16
- package/lib/badge.mjs +28 -12
- package/lib/config.mjs +123 -47
- package/lib/reference.mjs +32 -0
- package/lib/report.mjs +17 -12
- package/lib/scan.mjs +416 -61
- package/lib/score.mjs +19 -7
- package/lib/target.mjs +82 -0
- package/package.json +1 -1
- package/rubric/reference.json +293 -0
- package/skills/ds-audit/SKILL.md +37 -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,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 |
|
|
@@ -79,6 +114,39 @@ adsa rubric The nine dimensions in full
|
|
|
79
114
|
```
|
|
80
115
|
|
|
81
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.
|
|
82
150
|
|
|
83
151
|
## The skill
|
|
84
152
|
|
|
@@ -94,7 +162,7 @@ gets it wrong, `adsa.config.json`:
|
|
|
94
162
|
|
|
95
163
|
```json
|
|
96
164
|
{
|
|
97
|
-
"guides": ["guidelines"],
|
|
165
|
+
"guides": ["guidelines", "src/components"],
|
|
98
166
|
"source": ["src/components"],
|
|
99
167
|
"forbidden": ["lucide-react"],
|
|
100
168
|
"skip": ["patterns"],
|
|
@@ -105,6 +173,9 @@ gets it wrong, `adsa.config.json`:
|
|
|
105
173
|
`skip` is for dimensions that genuinely do not apply — a primitives-only library has
|
|
106
174
|
no page patterns, and a skipped dimension lowers the maximum instead of the score.
|
|
107
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
|
+
|
|
108
179
|
## Platforms
|
|
109
180
|
|
|
110
181
|
ADSA detects which platform a design system is built for from real evidence in the
|
|
@@ -145,23 +216,35 @@ not assessed — it never guesses a number.
|
|
|
145
216
|
|
|
146
217
|
## Try it
|
|
147
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
|
+
|
|
148
222
|
```bash
|
|
149
223
|
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
|
|
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
|
|
153
227
|
```
|
|
154
228
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
158
240
|
|
|
159
|
-
Small fixtures
|
|
160
|
-
|
|
161
|
-
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:
|
|
162
244
|
|
|
163
245
|
```bash
|
|
164
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
|
|
165
248
|
```
|
|
166
249
|
|
|
167
250
|
## 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, 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,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) {
|
|
@@ -167,14 +258,16 @@ function cmdAudit(dir, flags, out, json) {
|
|
|
167
258
|
const delta = compare(previous, file);
|
|
168
259
|
|
|
169
260
|
if (flags.json) {
|
|
170
|
-
json({ ...file, dimensions: scored.dimensions, delta });
|
|
261
|
+
json({ ...file, scanned: facts.scanned, dimensions: scored.dimensions, delta });
|
|
171
262
|
} else {
|
|
172
|
-
|
|
173
|
-
//
|
|
174
|
-
const
|
|
175
|
-
|
|
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)}`);
|
|
176
268
|
if (facts.workspace) out(` ${facts.workspace}`);
|
|
177
269
|
if (!flags.quiet) {
|
|
270
|
+
out(dim(scannedLine(facts)));
|
|
178
271
|
out("");
|
|
179
272
|
for (const d of scored.dimensions) {
|
|
180
273
|
const bar = d.skipped ? dim(" skip") : byScore(d.score, "█".repeat(d.score)) + dim("·".repeat(5 - d.score));
|
|
@@ -192,6 +285,8 @@ function cmdAudit(dir, flags, out, json) {
|
|
|
192
285
|
const move = `Since the last run: ${delta.from} → ${delta.to} (${delta.delta >= 0 ? "+" : ""}${delta.delta})`;
|
|
193
286
|
out(delta.delta > 0 ? green(move) : delta.delta < 0 ? red(move) : dim(move));
|
|
194
287
|
}
|
|
288
|
+
// A score out of 45 answers nothing until you know what 45 is worth.
|
|
289
|
+
out(dim(standing(scored.total).sentence));
|
|
195
290
|
}
|
|
196
291
|
}
|
|
197
292
|
|
|
@@ -206,7 +301,12 @@ function cmdAudit(dir, flags, out, json) {
|
|
|
206
301
|
const near = relative(process.cwd(), artefacts);
|
|
207
302
|
const shown = !near ? "." : near.startsWith("..") ? artefacts : near;
|
|
208
303
|
if (!flags.json && !flags.quiet) {
|
|
209
|
-
|
|
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);
|
|
210
310
|
// Say once what the directory is for, rather than leave someone to guess
|
|
211
311
|
// whether a new folder in their repository belongs in the commit.
|
|
212
312
|
if (fresh && !flags.out && exists(join(root, ".git"))) {
|
|
@@ -365,10 +465,11 @@ function cmdDoctor(dir, flags, out, json) {
|
|
|
365
465
|
const checks = [];
|
|
366
466
|
const add = (id, status, message, fix) => checks.push({ id, status, message, ...(fix ? { fix } : {}) });
|
|
367
467
|
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
|
-
|
|
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");
|
|
372
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");
|
|
373
474
|
facts.verification.workflows.length ? add("ci", "pass", `CI: ${facts.verification.workflows.join(", ")}.`) : add("ci", "warn", "No CI workflow found.", "adsa fix ci-workflow");
|
|
374
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
|
-
/**
|
|
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) {
|