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 +98 -11
- package/bin/adsa.mjs +143 -27
- package/lib/badge.mjs +28 -12
- package/lib/config.mjs +123 -47
- package/lib/history.mjs +9 -7
- 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 |
|
|
@@ -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
|
|
147
|
-
node bin/adsa.mjs
|
|
148
|
-
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
|
|
149
227
|
```
|
|
150
228
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
|
156
|
-
|
|
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,
|
|
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 {
|
|
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>
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
169
|
-
//
|
|
170
|
-
const
|
|
171
|
-
|
|
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(
|
|
195
|
-
write(join(
|
|
196
|
-
write(join(
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
writeFileSync(join(
|
|
200
|
-
|
|
201
|
-
|
|
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.
|
|
355
|
-
|
|
356
|
-
|
|
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
|
-
/**
|
|
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) {
|