adsa-cli 0.1.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Nazar Melnyk
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,169 @@
1
+ <img src="assets/cover.png" alt="Agentic Design System" width="100%">
2
+
3
+ # ADSA
4
+
5
+ [adsa.space](https://adsa.space) — agentic design system audit
6
+
7
+ Your design system is documented for people. An agent is not a person: it cannot ask
8
+ a colleague, it cannot tell *this does not exist* from *I have not found it yet*, and
9
+ when it cannot find something it invents a component and moves on, confidently.
10
+
11
+ ADSA scores a design system out of 45 on how well a coding agent can use it, writes
12
+ the report, and then fixes what it can.
13
+
14
+ ```bash
15
+ npx adsa-cli audit # score this repo, write the report
16
+ npx adsa-cli fix --all # apply what can be applied, brief the rest
17
+ npx adsa-cli audit # measure again
18
+ ```
19
+
20
+ That loop is the whole product. Everything below is detail.
21
+
22
+ ## What you get
23
+
24
+ **A score with its evidence.** Nine dimensions, each 1, 3 or 5, each quoting the
25
+ files that produced the number, so you can disagree with the reading rather than the
26
+ verdict. Written to `.adsa/report.html`, `.adsa/report.md` and `.adsa/score.json`.
27
+
28
+ **A to-do list that runs.** Every item is a command, not advice. Five fixes write
29
+ files. Five write a task brief for your agent, because they need your repo's own
30
+ stack and a generic codemod would do them badly.
31
+
32
+ **A gate, and a badge.** `.adsa/score.json` is committed and `adsa audit --gate`
33
+ fails when the score drops, so documentation debt cannot land quietly next to a
34
+ feature. `adsa badge` prints the README line for your own score — this is what the
35
+ example system in this repo earns after its fixes:
36
+
37
+ ![Agent-ready 23/45](https://img.shields.io/badge/agent--ready-23%2F45-a2650b)
38
+
39
+ **The experiment.** `adsa eval` gives an agent a real page to build, then counts what
40
+ it invented. It is the only number that has ever changed anyone's mind.
41
+
42
+ **An MCP server.** `adsa mcp` serves the guides as tools, so agents query the docs of
43
+ the version checked out in front of them instead of reading files and guessing.
44
+
45
+ ## The nine dimensions
46
+
47
+ | | Dimension | The question |
48
+ | :-- | :-- | :-- |
49
+ | 1 | Agent instructions | When an agent opens this repo, does anything tell it how to use the system? |
50
+ | 2 | Machine surface | Can an agent query the system, or must it read files and guess? |
51
+ | 3 | Docs coverage | Does every component a consumer can import have a guide? |
52
+ | 4 | Docs freshness | If a guide drifts from the code, does anything notice? |
53
+ | 5 | Tokens | Are colour, spacing, radius and motion documented as named decisions? |
54
+ | 6 | Patterns | Is there anything above component level — how a real page is assembled? |
55
+ | 7 | Accessibility docs | Do the guides say how a component behaves for assistive technology? |
56
+ | 8 | Verification | Can an agent check its own work before calling it done? |
57
+ | 9 | Gap handling | What happens when the system genuinely does not have the thing? |
58
+
59
+ `npx adsa-cli rubric` prints what 1, 3 and 5 mean for each.
60
+
61
+ ## Commands
62
+
63
+ ```
64
+ adsa audit [dir] Score, write report.html, report.md, score.json
65
+ adsa fix <id> | --all Apply a fix, or write its task brief
66
+ adsa fix --list What each fix does
67
+ adsa search "<task>" Which component do I need for this
68
+ adsa docs <component> That component's guide, prose stripped
69
+ adsa eval init Write the experiment task for an agent
70
+ adsa eval score <dir> Measure what the agent actually built
71
+ adsa mcp Serve the guides as MCP tools over stdio
72
+ adsa doctor Is this repo wired up for agents
73
+ adsa badge The README badge for the committed score
74
+ adsa rubric The nine dimensions in full
75
+ ```
76
+
77
+ `--json` everywhere. `--dry-run` on `fix`. `--gate --min <n>` on `audit`.
78
+
79
+ ## The skill
80
+
81
+ For Claude Code, `skills/ds-audit` runs the whole audit as a conversation: it asks
82
+ where the system is, whether a consumer project exists, which agent your team uses
83
+ and whether there is a Figma library — then runs the tool, reads the guides itself,
84
+ runs the experiment and writes the findings. Copy it into `.claude/skills/`.
85
+
86
+ ## Configuration
87
+
88
+ None required. Everything the tool can read from the repo, it reads. When detection
89
+ gets it wrong, `adsa.config.json`:
90
+
91
+ ```json
92
+ {
93
+ "guides": ["guidelines"],
94
+ "source": ["src/components"],
95
+ "forbidden": ["lucide-react"],
96
+ "skip": ["patterns"],
97
+ "minScore": 30
98
+ }
99
+ ```
100
+
101
+ `skip` is for dimensions that genuinely do not apply — a primitives-only library has
102
+ no page patterns, and a skipped dimension lowers the maximum instead of the score.
103
+
104
+ ## Platforms
105
+
106
+ ADSA detects which platform a design system is built for from real evidence in the
107
+ repo, and audits it accordingly:
108
+
109
+ | Platform | Detected from | Components | Tokens |
110
+ | :-- | :-- | :-- | :-- |
111
+ | React / web | `react`, `vue` or `svelte` in `package.json` | Exported symbols or `exports` subpaths | Guide prose, raw Tailwind/hex in examples |
112
+ | React Native | `react-native` or `expo` in `package.json` | Same as web — RN is still TSX | Guide prose, or raw `StyleSheet`/NativeWind values |
113
+ | Swift / SwiftUI / UIKit | `Package.swift`, an `.xcodeproj`/`.xcworkspace`, or several `.swift` files | `public struct X: View` / `open class X: UIView` | Guide prose, or named colours in an `.xcassets` catalog |
114
+ | Kotlin / Jetpack Compose | A Gradle build next to `.kt` files | `@Composable fun X(...)` | Guide prose, or named colours in `colors.xml`/`themes.xml` |
115
+
116
+ `facts.platform` reports the strongest match as `primary` and every other platform
117
+ it saw real evidence for as `detected` — a monorepo with a web app and a native
118
+ shell is not forced into one label, it is reported honestly as both.
119
+
120
+ Guides are always markdown, wherever they live: `guidelines/`, `docs/`, or — for a
121
+ Swift package — its own `.docc` bundle, found automatically rather than requiring
122
+ `adsa.config.json` to list it.
123
+
124
+ Per platform, the agent-instructions and tokens fixes write in that platform's own
125
+ vocabulary (SwiftUI modifiers and `swift build`/`swift test`, Compose semantics and
126
+ `./gradlew test lint`, `StyleSheet`/NativeWind and Metro), and the accessibility
127
+ dimension asks about VoiceOver/Dynamic Type or TalkBack/content description instead
128
+ of demanding "keyboard" from a platform that has none. A dimension that genuinely
129
+ does not apply is skipped — via `adsa.config.json`'s `skip` — not scored down.
130
+
131
+ ## What it supports honestly
132
+
133
+ Detection is built for React, TypeScript and markdown guides — that path is the
134
+ most mature — and works on any repo where guides live in markdown and components
135
+ are exported from source. Vue and Svelte are detected but less well covered. React
136
+ Native, Swift and Kotlin/Compose detection is newer: components, tokens and
137
+ accessibility evidence are read from real files (see the table above), but the
138
+ freshness fixes (`prop-tables`, `examples-check`) still assume a typed TypeScript
139
+ component library, and their briefs say so. Anything it cannot assess it reports as
140
+ not assessed — it never guesses a number.
141
+
142
+ ## Try it
143
+
144
+ ```bash
145
+ 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
149
+ ```
150
+
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.
154
+
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:
158
+
159
+ ```bash
160
+ node bin/adsa.mjs audit test/fixtures/swift-ds
161
+ ```
162
+
163
+ ## Licence
164
+
165
+ MIT.
166
+
167
+ ---
168
+
169
+ *From one designer to designers with love <3*
package/bin/adsa.mjs ADDED
@@ -0,0 +1,411 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * adsa — score a design system on how well coding agents can use it, then fix what
4
+ * is missing.
5
+ *
6
+ * adsa audit [dir] score it, write the report
7
+ * adsa fix <id> | --all apply a fix, or write its task brief
8
+ * adsa eval init | score <dir> run the experiment and measure the result
9
+ * adsa mcp serve the guides as MCP tools
10
+ * adsa doctor is this repo wired up for agents
11
+ * adsa badge the README badge for the committed score
12
+ */
13
+ import { mkdirSync, realpathSync, writeFileSync } from "node:fs";
14
+ import { join, resolve } from "node:path";
15
+ import { pathToFileURL } from "node:url";
16
+ import { loadConfig, resolveTarget } from "../lib/config.mjs";
17
+ import { scan } from "../lib/scan.mjs";
18
+ import { RUBRIC, score, scoreFile } from "../lib/score.mjs";
19
+ import { buildTodo, html, markdown } from "../lib/report.mjs";
20
+ import { badgeEndpoint, badgeMarkdown } from "../lib/badge.mjs";
21
+ import { DIR, appendHistory, compare, readHistory, write } from "../lib/history.mjs";
22
+ import { FIXES, applyFix } from "../lib/fix.mjs";
23
+ import { evaluate, taskFile } from "../lib/eval.mjs";
24
+ import { findGuide, searchGuides, serve } from "../lib/mcp.mjs";
25
+ import { dense } from "../lib/dense.mjs";
26
+ import { readJson } from "../lib/fsx.mjs";
27
+ import { byScore, detectColor, dim, green, red, setColor, yellow } from "../lib/color.mjs";
28
+
29
+ const HELP = `adsa — agentic design system audit
30
+
31
+ npx adsa-cli <command> without installing
32
+ adsa <command> once the package is installed
33
+
34
+ Usage
35
+ adsa audit [dir] Score the design system in dir (default: .)
36
+ adsa search "<task>" Which component do I need for this
37
+ adsa docs <component> That component's guide, prose stripped
38
+ adsa fix <id> Apply one fix, or write its task brief
39
+ adsa fix --list What each fix does and which are automatic
40
+ adsa eval init Write the experiment task for an agent
41
+ adsa eval score <dir> Measure what the agent actually built
42
+ adsa mcp Serve the guides as MCP tools over stdio
43
+ adsa doctor Check this repo's agent surface
44
+ adsa badge Print the README badge for the committed score
45
+ adsa rubric Print the nine dimensions and what each level means
46
+
47
+ Options
48
+ --json Machine-readable output
49
+ --out <dir> Where to write the report (default: .adsa)
50
+ --gate Exit non-zero if the score dropped, or is below --min
51
+ --min <n> Minimum acceptable total for --gate
52
+ --dry-run fix: show what would change, write nothing
53
+ --system <dir> eval score: the design system the project should use
54
+ --cwd <dir> fix: the repository to change (default: .)
55
+ --quiet Only the score line
56
+ --full docs: keep the prose
57
+ --color / --no-color Force ANSI colour on or off (default: on for a terminal)
58
+ `;
59
+
60
+ export function parseArgs(argv) {
61
+ const flags = { out: null, json: false, gate: false, min: null, dryRun: false, system: null, quiet: false, list: false, all: false, cwd: null };
62
+ const positional = [];
63
+ for (let i = 0; i < argv.length; i++) {
64
+ const arg = argv[i];
65
+ if (arg === "--json") flags.json = true;
66
+ else if (arg === "--gate") flags.gate = true;
67
+ else if (arg === "--dry-run") flags.dryRun = true;
68
+ else if (arg === "--quiet") flags.quiet = true;
69
+ else if (arg === "--list") flags.list = true;
70
+ else if (arg === "--full") flags.full = true;
71
+ else if (arg === "--color") flags.color = true;
72
+ else if (arg === "--no-color") flags.color = false;
73
+ else if (arg === "--all") flags.all = true;
74
+ else if (arg === "--out") flags.out = argv[++i];
75
+ else if (arg === "--min") flags.min = Number(argv[++i]);
76
+ else if (arg === "--system") flags.system = argv[++i];
77
+ else if (arg === "--cwd") flags.cwd = argv[++i];
78
+ else if (arg === "--help" || arg === "-h") flags.help = true;
79
+ else if (arg.startsWith("-")) flags.unknown = arg;
80
+ else positional.push(arg);
81
+ }
82
+ return { command: positional[0], args: positional.slice(1), flags };
83
+ }
84
+
85
+ function load(dir) {
86
+ const given = resolve(dir || ".");
87
+ const target = resolveTarget(given);
88
+ const root = target.dir;
89
+ const config = loadConfig(root);
90
+ const facts = scan(root, config);
91
+ facts.workspace = target.note;
92
+ return { root, config, facts };
93
+ }
94
+
95
+ /** Auditing the wrong directory produces a confident, meaningless number. Say so instead. */
96
+ function looksLikeADesignSystem(facts) {
97
+ if (facts.components.length >= 3 || facts.guides.length >= 2) return true;
98
+ // A small native design system (Swift, Kotlin) can be real with fewer files than a
99
+ // web one. Strong platform evidence plus at least one real component or guide is
100
+ // enough to not reject it just for being small.
101
+ const platform = facts.platform;
102
+ if (platform && platform.primary !== "web" && platform.detected.length && (facts.components.length >= 1 || facts.guides.length >= 1)) return true;
103
+ return false;
104
+ }
105
+
106
+ export async function run(argv, io = {}) {
107
+ const out = (s) => (io.stdout ?? process.stdout).write(s + "\n");
108
+ const json = (v) => out(JSON.stringify(v, null, 2));
109
+ const { command, args, flags } = parseArgs(argv);
110
+ if (flags.color !== undefined) setColor(flags.color);
111
+ if (flags.help || !command) {
112
+ out(HELP);
113
+ return 0;
114
+ }
115
+ if (flags.unknown) {
116
+ out(`Unknown option ${flags.unknown}\n\n${HELP}`);
117
+ return 1;
118
+ }
119
+
120
+ switch (command) {
121
+ case "audit":
122
+ return cmdAudit(args[0], flags, out, json);
123
+ case "fix":
124
+ return cmdFix(args, flags, out, json);
125
+ case "eval":
126
+ return cmdEval(args, flags, out, json);
127
+ case "mcp": {
128
+ const ctx = load(args[0]);
129
+ await serve(io.stdin ?? process.stdin, io.stdout ?? process.stdout, ctx);
130
+ return 0;
131
+ }
132
+ case "search":
133
+ return cmdSearch(args, flags, out, json);
134
+ case "docs":
135
+ return cmdDocs(args, flags, out, json);
136
+ case "doctor":
137
+ return cmdDoctor(args[0], flags, out, json);
138
+ case "badge":
139
+ return cmdBadge(args[0], flags, out, json);
140
+ case "rubric":
141
+ return cmdRubric(flags, out, json);
142
+ default:
143
+ out(`Unknown command "${command}".\n\n${HELP}`);
144
+ return 1;
145
+ }
146
+ }
147
+
148
+ /* ---------------------------------------------------------------- audit */
149
+
150
+ function cmdAudit(dir, flags, out, json) {
151
+ const { root, config, facts } = load(dir);
152
+ if (!looksLikeADesignSystem(facts)) {
153
+ out(`${facts.name}: found ${facts.components.length} importable components and ${facts.guides.length} guides in ${root}.`);
154
+ out("That does not look like a design system. Point adsa at the package that exports the components,");
155
+ out("or set `source` and `guides` in adsa.config.json.");
156
+ return 1;
157
+ }
158
+ const scored = score(facts, config);
159
+ const outDir = flags.out || DIR;
160
+ const file = scoreFile(facts, scored);
161
+ const previous = readJson(join(root, DIR, "score.json"));
162
+ const delta = compare(previous, file);
163
+
164
+ if (flags.json) {
165
+ json({ ...file, dimensions: scored.dimensions, delta });
166
+ } else {
167
+ const band = scored.total / scored.max;
168
+ // Same bands the report uses, so the terminal and the HTML never disagree.
169
+ const paint = (t) => (band >= 0.85 ? green(t) : band >= 0.35 ? yellow(t) : red(t));
170
+ out(`${facts.name}${facts.version ? " " + facts.version : ""} — agent readiness ${paint(`${scored.total}/${scored.max}`)}`);
171
+ if (facts.workspace) out(` ${facts.workspace}`);
172
+ if (!flags.quiet) {
173
+ out("");
174
+ for (const d of scored.dimensions) {
175
+ const bar = d.skipped ? dim(" skip") : byScore(d.score, "█".repeat(d.score)) + dim("·".repeat(5 - d.score));
176
+ out(` ${String(d.skipped ? "—" : d.score).padStart(2)}/5 ${bar} ${d.skipped ? dim(d.title) : d.title}`);
177
+ }
178
+ out("");
179
+ const todo = buildTodo(scored);
180
+ if (todo.length) {
181
+ // The command once, then the ids. Three full npx lines is 90 columns
182
+ // of mostly repeated prefix.
183
+ const rest = todo.length - 1;
184
+ out(dim(`Next: npx adsa-cli fix ${todo[0].fix}${rest ? ` · ${rest} more in the report` : ""}`));
185
+ }
186
+ if (delta) {
187
+ const move = `Since the last run: ${delta.from} → ${delta.to} (${delta.delta >= 0 ? "+" : ""}${delta.delta})`;
188
+ out(delta.delta > 0 ? green(move) : delta.delta < 0 ? red(move) : dim(move));
189
+ }
190
+ }
191
+ }
192
+
193
+ const history = appendHistory(root, { date: file.generatedAt, total: file.total, max: file.max, dimensions: file.dimensions });
194
+ write(join(root, DIR, "score.json"), file);
195
+ write(join(root, DIR, "badge.json"), badgeEndpoint(file.total, file.max));
196
+ const reportDir = join(root, outDir);
197
+ mkdirSync(reportDir, { recursive: true });
198
+ writeFileSync(join(reportDir, "report.html"), html(facts, scored, history));
199
+ writeFileSync(join(reportDir, "report.md"), markdown(facts, scored));
200
+ if (!flags.json && !flags.quiet) out(dim(`\nReport: ${join(outDir, "report.html")} · score: ${join(DIR, "score.json")}`));
201
+
202
+ if (flags.gate) {
203
+ const floor = flags.min ?? config.minScore ?? (previous ? previous.total : null);
204
+ if (floor != null && file.total < floor) {
205
+ out(red(`\nGate: ${file.total}/${file.max} is below ${floor}.`));
206
+ return 1;
207
+ }
208
+ }
209
+ return 0;
210
+ }
211
+
212
+ /* ------------------------------------------------------------------ fix */
213
+
214
+ function cmdFix(args, flags, out, json) {
215
+ if (flags.list || (!args.length && !flags.all)) {
216
+ if (flags.json) return json(Object.entries(FIXES).map(([id, f]) => ({ id, kind: f.kind, title: f.title, dimension: f.dimension }))), 0;
217
+ out("Fixes\n");
218
+ for (const [id, fix] of Object.entries(FIXES)) {
219
+ out(` ${id.padEnd(16)} ${fix.kind === "apply" ? "writes files " : "writes a brief"} ${fix.title}`);
220
+ }
221
+ out("\n A brief is a task specification for an agent to execute: those fixes need");
222
+ out(" your repository's own stack, and a generic codemod would do them badly.");
223
+ return 0;
224
+ }
225
+ const { root, config, facts } = load(flags.cwd || ".");
226
+ const ids = flags.all ? Object.keys(FIXES) : args;
227
+ const actions = [];
228
+ for (const id of ids) {
229
+ try {
230
+ actions.push(...applyFix(id, facts, config, { dryRun: flags.dryRun }).map((a) => ({ ...a, fix: id })));
231
+ } catch (error) {
232
+ out(String(error.message));
233
+ return 1;
234
+ }
235
+ }
236
+ if (flags.json) {
237
+ json({ dryRun: flags.dryRun, actions });
238
+ return 0;
239
+ }
240
+ for (const a of actions) {
241
+ if (a.action === "note") out(` note ${a.note}`);
242
+ else out(` ${(flags.dryRun ? "would " + a.action : a.action).padEnd(6)} ${a.file}${a.note ? ` — ${a.note}` : ""}`);
243
+ }
244
+ const briefs = actions.filter((a) => a.file.includes(".adsa/fixes"));
245
+ if (briefs.length && !flags.dryRun) out(`\nHand ${briefs.length === 1 ? "the brief" : "these briefs"} to your agent, then re-run \`adsa audit\`.`);
246
+ return 0;
247
+ }
248
+
249
+ /* ----------------------------------------------------------------- eval */
250
+
251
+ function cmdEval(args, flags, out, json) {
252
+ const sub = args[0];
253
+ if (sub === "init") {
254
+ const { root, config, facts } = load(args[1]);
255
+ const file = join(DIR, "eval", "TASK.md");
256
+ if (!flags.dryRun) {
257
+ mkdirSync(join(root, DIR, "eval"), { recursive: true });
258
+ writeFileSync(join(root, file), taskFile(facts, config));
259
+ }
260
+ out(` create ${file} — give this to an agent in a fresh session`);
261
+ return 0;
262
+ }
263
+ if (sub === "score") {
264
+ const project = args[1];
265
+ if (!project) {
266
+ out("Usage: adsa eval score <project-dir> [--system <design-system-dir>]");
267
+ return 1;
268
+ }
269
+ const { config, facts } = load(flags.system || ".");
270
+ const result = evaluate(resolve(project), facts, config);
271
+ if (flags.json) {
272
+ json(result);
273
+ return result.invented.length ? 1 : 0;
274
+ }
275
+ out(`${result.project}`);
276
+ out(` against ${result.system} — ${plural(result.files, "file")}, ${plural(result.systemImports, "import")}, ${plural(result.used.length, "component")} used`);
277
+ out(` invented: ${result.invented.length ? red(String(result.invented.length)) : green("0")}`);
278
+ for (const i of result.invented.slice(0, 12)) out(` ${red(i.name)} — ${i.file} (from ${i.from})`);
279
+ if (result.forbiddenImports.length) out(` forbidden packages: ${[...new Set(result.forbiddenImports.map((f) => f.package))].join(", ")}`);
280
+ if (result.rawPalette.length) out(` raw palette classes: ${result.rawPalette.length}, first ${result.rawPalette[0].value} at ${result.rawPalette[0].file}:${result.rawPalette[0].line}`);
281
+ if (result.localUiFolder.length) out(` local component folder: ${result.localUiFolder.join(", ")}`);
282
+ out(`\n ${result.invented.length ? result.verdict : green(result.verdict)}`);
283
+ return result.invented.length ? 1 : 0;
284
+ }
285
+ out("Usage: adsa eval init | adsa eval score <dir>");
286
+ return 1;
287
+ }
288
+
289
+ /* ----------------------------------------------------------- search/docs */
290
+
291
+ function cmdSearch(args, flags, out, json) {
292
+ const { facts } = load(flags.cwd || ".");
293
+ const query = args.join(" ");
294
+ const results = searchGuides(facts, query, 10);
295
+ if (flags.json) {
296
+ json(results);
297
+ return results.length ? 0 : 1;
298
+ }
299
+ if (!results.length) {
300
+ out(`Nothing in ${facts.name} matched "${query}".`);
301
+ out(facts.gaps.file ? `Check ${facts.gaps.file} before assuming it exists.` : "Nothing lists what this system lacks, so ask rather than inventing it.");
302
+ return 1;
303
+ }
304
+ for (const r of results) out(` ${r.slug.padEnd(22)} ${r.title || ""} (${r.path})`);
305
+ return 0;
306
+ }
307
+
308
+ function cmdDocs(args, flags, out, json) {
309
+ const { facts } = load(flags.cwd || ".");
310
+ const name = args.join(" ");
311
+ const guide = findGuide(facts, name);
312
+ if (!guide) {
313
+ const near = searchGuides(facts, name, 5).map((r) => r.slug);
314
+ out(`No guide for "${name}".${near.length ? ` Closest: ${near.join(", ")}.` : ""}`);
315
+ return 1;
316
+ }
317
+ const body = flags.full ? guide.body : dense(guide.body);
318
+ if (flags.json) {
319
+ json({ path: guide.path, title: guide.title, body });
320
+ return 0;
321
+ }
322
+ out(body);
323
+ return 0;
324
+ }
325
+
326
+ /** One file, not 1 files. The output is read by people. */
327
+ function plural(n, word) {
328
+ return `${n} ${word}${n === 1 ? "" : "s"}`;
329
+ }
330
+
331
+ /* --------------------------------------------------------------- doctor */
332
+
333
+ function cmdDoctor(dir, flags, out, json) {
334
+ const { facts, config } = load(dir);
335
+ const checks = [];
336
+ const add = (id, status, message, fix) => checks.push({ id, status, message, ...(fix ? { fix } : {}) });
337
+ const agents = facts.agentFiles.find((a) => a.mentionsPackage);
338
+ agents ? add("agent-docs", "pass", `${agents.file} references ${facts.name}.`) : add("agent-docs", "fail", "No agent instructions mention this system.", "adsa fix agents-md");
339
+ facts.machine.declaredServers.includes("adsa") || facts.machine.mcpInPackage
340
+ ? add("mcp", "pass", "An MCP server is registered for this repository.")
341
+ : add("mcp", "warn", "No MCP server registered, so agents read files instead of querying.", "npx adsa-cli fix mcp-config");
342
+ 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");
343
+ facts.verification.workflows.length ? add("ci", "pass", `CI: ${facts.verification.workflows.join(", ")}.`) : add("ci", "warn", "No CI workflow found.", "adsa fix ci-workflow");
344
+ facts.config.file ? add("config", "pass", `${facts.config.file} present.`) : add("config", "warn", "No adsa.config.json — detection is doing the guessing.");
345
+ const ok = !checks.some((c) => c.status === "fail");
346
+ if (flags.json) {
347
+ json({ ok, checks });
348
+ return ok ? 0 : 1;
349
+ }
350
+ for (const c of checks) {
351
+ const tint = c.status === "pass" ? green : c.status === "warn" ? yellow : red;
352
+ out(` ${tint(c.status.toUpperCase().padEnd(4))} ${c.id.padEnd(11)} ${c.message}`);
353
+ if (c.fix) out(dim(` ${" ".repeat(11)} → ${c.fix}`));
354
+ }
355
+ return ok ? 0 : 1;
356
+ }
357
+
358
+ /* ---------------------------------------------------------------- badge */
359
+
360
+ function cmdBadge(dir, flags, out, json) {
361
+ const root = resolve(dir || ".");
362
+ const file = readJson(join(root, DIR, "score.json"));
363
+ if (!file) {
364
+ out("No .adsa/score.json yet. Run `npx adsa-cli audit` first.");
365
+ return 1;
366
+ }
367
+ if (flags.json) {
368
+ json(badgeEndpoint(file.total, file.max));
369
+ return 0;
370
+ }
371
+ out(badgeMarkdown(file.total, file.max));
372
+ return 0;
373
+ }
374
+
375
+ /* --------------------------------------------------------------- rubric */
376
+
377
+ function cmdRubric(flags, out, json) {
378
+ if (flags.json) {
379
+ json(RUBRIC);
380
+ return 0;
381
+ }
382
+ out(`Agent readiness, rubric ${RUBRIC.version} — ${RUBRIC.dimensions.length} dimensions, ${RUBRIC.max} points\n`);
383
+ for (const d of RUBRIC.dimensions) {
384
+ out(`${d.title}`);
385
+ out(` ${d.question}`);
386
+ for (const level of ["1", "3", "5"]) out(` ${level} ${d.levels[level]}`);
387
+ out("");
388
+ }
389
+ return 0;
390
+ }
391
+
392
+ /* ----------------------------------------------------------------- main */
393
+
394
+ function isMain() {
395
+ if (!process.argv[1]) return false;
396
+ try {
397
+ return pathToFileURL(realpathSync(process.argv[1])).href === import.meta.url;
398
+ } catch {
399
+ return false;
400
+ }
401
+ }
402
+
403
+ if (isMain()) {
404
+ setColor(detectColor());
405
+ run(process.argv.slice(2))
406
+ .then((code) => process.exit(code))
407
+ .catch((error) => {
408
+ process.stderr.write(`adsa: ${error.stack || error.message}\n`);
409
+ process.exit(1);
410
+ });
411
+ }
package/lib/badge.mjs ADDED
@@ -0,0 +1,28 @@
1
+ /** README badge. Static shields.io URL, plus an endpoint file for repos that prefer one.
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"],
12
+ ];
13
+
14
+ export function badgeColor(total, max) {
15
+ const ratio = max ? total / max : 0;
16
+ return COLORS.find(([floor]) => ratio >= floor)[1];
17
+ }
18
+
19
+ export function badgeMarkdown(total, max, link) {
20
+ const url = `https://img.shields.io/badge/agent--ready-${total}%2F${max}-${badgeColor(total, max)}`;
21
+ const img = `![Agent-ready ${total}/${max}](${url})`;
22
+ return link ? `[${img}](${link})` : img;
23
+ }
24
+
25
+ /** shields.io endpoint schema, so a badge can read the committed score directly. */
26
+ export function badgeEndpoint(total, max) {
27
+ return { schemaVersion: 1, label: "agent-ready", message: `${total}/${max}`, color: `#${badgeColor(total, max)}` };
28
+ }
package/lib/color.mjs ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * ANSI colour. Off by default so piped output, CI logs and tests stay plain text;
3
+ * the bin turns it on when it is talking to a real terminal.
4
+ */
5
+ let enabled = false;
6
+
7
+ export function setColor(on) {
8
+ enabled = Boolean(on);
9
+ }
10
+
11
+ /** A real terminal, unless the environment says otherwise. Honours NO_COLOR and FORCE_COLOR. */
12
+ export function detectColor(stream = process.stdout, env = process.env) {
13
+ if (env.NO_COLOR !== undefined && env.NO_COLOR !== "") return false;
14
+ if (env.FORCE_COLOR !== undefined && env.FORCE_COLOR !== "0") return true;
15
+ if (env.TERM === "dumb") return false;
16
+ return Boolean(stream && stream.isTTY);
17
+ }
18
+
19
+ const wrap = (open, close) => (text) => (enabled ? `\x1b[${open}m${text}\x1b[${close}m` : String(text));
20
+
21
+ export const red = wrap(31, 39);
22
+ export const green = wrap(32, 39);
23
+ export const yellow = wrap(33, 39);
24
+ export const dim = wrap(2, 22);
25
+ export const bold = wrap(1, 22);
26
+
27
+ /** 1 is a problem, 3 is a compromise, 5 is done. The bars carry that without a legend. */
28
+ export function byScore(score, text) {
29
+ if (score >= 5) return green(text);
30
+ if (score >= 3) return yellow(text);
31
+ return red(text);
32
+ }