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 +21 -0
- package/README.md +169 -0
- package/bin/adsa.mjs +411 -0
- package/lib/badge.mjs +28 -0
- package/lib/color.mjs +32 -0
- package/lib/config.mjs +103 -0
- package/lib/dense.mjs +21 -0
- package/lib/eval.mjs +140 -0
- package/lib/fix.mjs +260 -0
- package/lib/fsx.mjs +94 -0
- package/lib/history.mjs +41 -0
- package/lib/mcp.mjs +162 -0
- package/lib/report.mjs +716 -0
- package/lib/scan.mjs +607 -0
- package/lib/score.mjs +197 -0
- package/package.json +41 -0
- package/rubric/rubric.json +115 -0
- package/skills/ds-audit/SKILL.md +135 -0
- package/templates/AGENTS.android.md.tmpl +22 -0
- package/templates/AGENTS.md.tmpl +17 -0
- package/templates/AGENTS.react-native.md.tmpl +21 -0
- package/templates/AGENTS.swift.md.tmpl +23 -0
- package/templates/GAPS.md.tmpl +17 -0
- package/templates/briefs/a11y-docs.md +35 -0
- package/templates/briefs/coverage-gate.md +23 -0
- package/templates/briefs/examples-check.md +37 -0
- package/templates/briefs/patterns-doc.md +32 -0
- package/templates/briefs/prop-tables.md +41 -0
- package/templates/ci/adsa.yml +19 -0
- package/templates/mcp/mcp.json +8 -0
- package/templates/tokens.android.md.tmpl +32 -0
- package/templates/tokens.md.tmpl +31 -0
- package/templates/tokens.native.md.tmpl +32 -0
- package/templates/tokens.swift.md.tmpl +32 -0
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
|
+

|
|
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 = ``;
|
|
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
|
+
}
|