@fringetools/cli 0.0.0-stage → 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 Fringe contributors
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 CHANGED
@@ -1,3 +1,221 @@
1
- # Temporary Holding Version
1
+ # @fringetools/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `fringe` — a UX-quality checker for agent-written frontends. It scans JSX/TSX/HTML/CSS (and Vue/Svelte/Astro templates, text rules only) for the accessibility, state-handling, dark-pattern and "AI look" mistakes coding agents commonly make, gates the hard failures, and keeps a local history.
4
+
5
+ Package `@fringetools/cli` · binary `fringe` · Node ≥ 18 · zero config by default.
6
+
7
+ ## Quick start
8
+
9
+ In your project root:
10
+
11
+ ```sh
12
+ npx @fringetools/cli init # wires the Claude Code hook + installs the skill (nothing to install first)
13
+ npx @fringetools/cli check # run it by hand
14
+ ```
15
+
16
+ That is the whole setup. `init` adds a `PostToolUse` hook to `.claude/settings.json` (your existing settings are kept) and copies the skill to `.claude/skills/fringe/`. From then on, every time Claude Code edits a file, Fringe checks the project and feeds any accessibility errors back to the agent to fix.
17
+
18
+ Prefer a permanent command? `npm install -D @fringetools/cli`, then `npx fringe check`. Existing messy repo? Run `npx @fringetools/cli check --update-baseline` once so only *new* problems gate (see [Baseline](#baseline)).
19
+
20
+ > **Until the first npm release:** build from source — `pnpm install && pnpm --filter @fringetools/cli build`, then use `node packages/cli/dist/bin.js` in place of `npx @fringetools/cli`.
21
+
22
+ ## Commands
23
+
24
+ | Command | What it does |
25
+ |---|---|
26
+ | `fringe check [path]` | Scan a directory (default `.`) and print findings + health score. Exit `1` on any `error`. |
27
+ | `fringe check --json` | Full report as JSON, schema `fringe/check.v1` (stack, files, findings, counts, score). |
28
+ | `fringe check --claude-hook` | **For Claude Code hooks.** Silent when clean; on errors prints `{"decision":"block","reason":…}` (exit 0) so the agent sees and fixes them. |
29
+ | `fringe check --hook` | One-line `fringe/hook.v1` JSON for CI / pre-commit. Exits `1` on errors. Claude Code does **not** feed this back — use `--claude-hook` for agents. Wins over `--json`. |
30
+ | `fringe check --update-baseline` | Accept today's findings into `fringe.baseline.json` (commit it). Exit `0`. |
31
+ | `fringe check --record [--manifest <path>]` | Also append this run to `.fringe/ledger.jsonl`, optionally merging an agent flow manifest. |
32
+ | `fringe init [path]` | Add the Claude Code hook and install the skill. Safe to re-run; refuses to touch an invalid `settings.json`. |
33
+ | `fringe open [path] [--port <n>]` | Serve the ledger as a plain HTML page on `127.0.0.1:4173` (loopback only). Ctrl+C to stop. |
34
+ | `fringe --help` · `--version` | Usage / version. |
35
+
36
+ Notes: the first path is the check root (config + scan); extra paths are accepted and ignored so agent-invoked runs never crash. v0.1 takes directories, not single files. Unknown flags warn on stderr and are ignored.
37
+
38
+ Default (human) output:
39
+
40
+ ```
41
+ fringe · stack jsx,tailwind · 3 files
42
+ [ERROR] FR-201 /path/to/app/src/App.tsx:15 — <img> missing alt text (add alt="description" (or alt="" if decorative))
43
+ health 34/100 · 6 errors · 2 review · 0 signals
44
+ ```
45
+
46
+ When a baseline or inline suppressions are in play, the last line adds `· N baselined` / `· N suppressed`.
47
+
48
+ ## Exit codes
49
+
50
+ | Code | Meaning |
51
+ |---|---|
52
+ | `0` | Clean (no `error` findings), or a mode that never gates (`--claude-hook`, `--update-baseline`) |
53
+ | `1` | One or more `error`-severity findings (`check`, `--hook`, `--json`) |
54
+ | `2` | Internal failure — unreadable root, invalid config, corrupt `fringe.baseline.json`, invalid `.claude/settings.json`, unknown command. One line on stderr |
55
+
56
+ `review` and `signal` findings never change the exit code.
57
+
58
+ ## Health score
59
+
60
+ Each finding costs its weight (default error 10, review 3, signal 1). The total is scaled to a 10-file baseline (`× 10 / max(files, 10)`), so repo size doesn't flatten the score. Score = `100 − penalty` up to a penalty of 75; beyond that it decays smoothly (`25·75/penalty`) and never pins at 0. Repos under 10 files score exactly `100 − total`. Weights live in `fringe.config.json`.
61
+
62
+ ## Baseline
63
+
64
+ Adopting Fringe on a repo that already has problems? Don't fix 300 findings first.
65
+
66
+ ```sh
67
+ fringe check --update-baseline # writes fringe.baseline.json — commit it
68
+ fringe check # now only NEW findings count and gate
69
+ ```
70
+
71
+ Entries match on rule + file + message (not line numbers), so edits above a known finding don't bring it back. Each entry covers one finding: adding a second identical problem still gates. Fixed findings simply stop matching; rerun `--update-baseline` to shrink the file. A corrupt baseline exits `2` rather than passing silently.
72
+
73
+ ## Inline suppression
74
+
75
+ For a genuine false positive, put a comment on the finding's line or the line above, naming the rule(s):
76
+
77
+ ```tsx
78
+ {/* fringe-ignore FR-203 */}
79
+ <input type="search" />
80
+ ```
81
+
82
+ Also `// fringe-ignore FR-203, FR-202` and `<!-- fringe-ignore FR-203 -->`. Rule IDs are required (no blanket ignore), and every run reports `N suppressed`, so suppression is never invisible.
83
+
84
+ ## The Claude Code hook and skill
85
+
86
+ `fringe init` does this for you. By hand, add to `.claude/settings.json`:
87
+
88
+ ```json
89
+ {
90
+ "hooks": {
91
+ "PostToolUse": [
92
+ { "matcher": "Edit|Write", "hooks": [{ "type": "command", "command": "npx -y @fringetools/cli check --claude-hook" }] }
93
+ ]
94
+ }
95
+ }
96
+ ```
97
+
98
+ After each edit the hook runs; with errors it returns, for example:
99
+
100
+ ```json
101
+ {"decision":"block","reason":"Fringe found 2 accessibility error(s). Fix them before finishing:\n- FR-203 src/Signup.tsx:9 — input not associated with a label (add id + <label htmlFor>, or aria-label)\n…"}
102
+ ```
103
+
104
+ This was verified end to end with real agent runs: the agent receives the findings and fixes them (see [docs/architecture.md](../../docs/architecture.md)). Why `--claude-hook` and not `--hook`: Claude Code only passes a hook's output to the agent as `decision`/`reason` JSON on exit 0 (or stderr on exit 2); the CI contract `--hook` exits 1 and is ignored.
105
+
106
+ **The skill** (`.claude/skills/fringe/SKILL.md`, source: [skill/SKILL.md](skill/SKILL.md)) teaches the agent the ladder: *Flow* (cut what users don't need, with evidence in `.fringe/manifest.json`) → *Edges* (fix every hook finding, never silence a rule to get green) → *Polish* (tokens, scales, no AI look), then record the run. It is plain Markdown, so any agent platform that reads `AGENTS.md`/skill files can use it as-is.
107
+
108
+ For CI or pre-commit use `fringe check --hook` (or just the exit code):
109
+
110
+ ```json
111
+ {"schema":"fringe/hook.v1","errors":1,"review":0,"signals":0,"block":true,"reason":"FR-201: <img> missing alt text (src/App.tsx:12)"}
112
+ ```
113
+
114
+ ## Ledger, manifest and dashboard
115
+
116
+ `--record` appends one JSON line per run to `.fringe/ledger.jsonl` (gitignore it): id, timestamp, stack, files, counts, score, findings per rule (`byRule`), `suppressed`, optional `tokenSource` and agent `manifest`. The manifest (`.fringe/manifest.json`, written by the agent before `--record --manifest …`) is `{"flows":[{"action":"cut"|"kept","what":"…","evidence":"…"}]}` — flow decisions the agent *judged*, shown as such. `fringe open` renders the ledger (newest first, with flow cuts) at `http://127.0.0.1:4173`; reload to see new runs. A richer dashboard is on the roadmap.
117
+
118
+ ## Rules
119
+
120
+ Severity `error` fails the run; `review` and `signal` are heuristics that need a human look (they never change the exit code). The **WCAG** column cites the WCAG success criterion a finding is tagged with, when one applies.
121
+
122
+ ### Rung 2 — structural (a11y, interaction, dark patterns)
123
+
124
+ | Rule | Severity | WCAG | Flags |
125
+ |---|---|---|---|
126
+ | FR-201 | error | 1.1.1 | `<img>` missing `alt` |
127
+ | FR-202 | error | 4.1.2 | button/link without an accessible name |
128
+ | FR-203 | error | 3.3.2 | form input without associated label |
129
+ | FR-204 | error | 1.3.1 | heading level skips, multiple `<h1>` |
130
+ | FR-205 | error | 2.4.7 | `outline: none` with no `:focus-visible` alternative |
131
+ | FR-206 | error | 2.1.1 | `onClick` on a non-interactive element |
132
+ | FR-207 | error | 2.4.3 | `tabIndex` > 0 |
133
+ | FR-208 | review | | states coverage heuristic — data-fetch present (fetch/useQuery/useSWR) but no loading/empty/error branch |
134
+ | FR-209 | error | 4.1.2 | invalid/misspelled `aria-*` attributes |
135
+ | FR-210 | review | | dark pattern — fake countdown timer / manufactured urgency |
136
+ | FR-211 | review | | dark pattern — pre-checked consent checkbox |
137
+ | FR-212 | review | | dark pattern — confirm-shaming copy |
138
+ | FR-213 | review | | dark pattern — infinite scroll without opt-out |
139
+ | FR-214 | review | | dark pattern — bait-and-switch ("free", then a price) pricing |
140
+ | FR-215 | review | | generic error copy ("something went wrong") with no recovery action — no retry, link, or contact |
141
+
142
+ The dark-pattern taxonomy (FR-210–214) is adapted, with attribution, from [Design with Intent](https://designwithintent.ai)'s CC0 anti-pattern library.
143
+
144
+ ### Rung 3 — token adoption & slop signals
145
+
146
+ `signal` findings feed the health score but never gate. FR-301 counts and FR-364/366 are file-level; FR-368's CSS leg is per-declaration; the rest are per element/utility.
147
+
148
+ | Rule | Severity | WCAG | Flags |
149
+ |---|---|---|---|
150
+ | FR-301 | signal | | token adoption — file is dominated by hardcoded colors (Tailwind `bg-[#hex]` / CSS color declarations) over token uses (`var(--*)`, palette utilities) |
151
+ | FR-302 | signal | | off-scale font sizes — arbitrary `text-[17px]` utilities instead of the type scale |
152
+ | FR-303 | signal | | off-scale spacing/dimensions — arbitrary `p-[13px]` / `w-[111px]` style utilities |
153
+ | FR-304 | signal | 1.4.3 | contrast — CSS `color` on `background-color` pairs below AA 4.5:1 |
154
+ | FR-305 | signal | 2.5.8 | tiny hit area — `text-xs` button/link without `py-2`+ padding |
155
+ | FR-361 | signal | | italic serif display type — the AI-tell headline |
156
+ | FR-362 | signal | | hero eyebrow chip — uppercase, wide-tracked pill above a heading |
157
+ | FR-363 | signal | | card inside a card — nested rounded/shadowed boxes |
158
+ | FR-364 | signal | | icon-tile stack — three or more empty flex tiles in a file |
159
+ | FR-365 | signal | | thin border with wide shadow — elevation not matched to weight |
160
+ | FR-366 | signal | | status-chip soup — five or more labeled pills in a file |
161
+ | FR-367 | signal | | numbered section labels — "01 Discover" style kickers |
162
+ | FR-368 | signal | | the purple AI gradient — violet/indigo Tailwind gradient or its CSS hex equivalent |
163
+ | FR-369 | signal | | emoji in headings |
164
+
165
+ **FR-304 scope in v1:** contrast is checked only on CSS pairs — a `color` declaration and a `background-color` (or pure-color `background`) in the *same* CSS rule block. Tailwind utility classes in markup are not pair-checked, and translucent colors (alpha < 1) are skipped because contrast over an unknown backdrop is unknowable.
166
+
167
+ ## Configuration — `fringe.config.json`
168
+
169
+ Optional, at the check root. Everything defaults sensibly; all keys are optional.
170
+
171
+ ```json
172
+ {
173
+ "ignore": ["node_modules/**", "dist/**", "build/**", "out/**", "coverage/**", ".fringe/**", "*.min.*", "examples/**"],
174
+ "disable": ["FR-214"],
175
+ "weights": { "error": 10, "review": 3, "signal": 1 }
176
+ }
177
+ ```
178
+
179
+ - `ignore` — glob list matched against paths relative to the root. **Use the `dir/**` form to skip a directory** — a bare-dir glob like `"dist"` matches only the directory entry itself and does **not** prune the files inside it. (`node_modules` and dot-directories are always skipped.)
180
+ - `disable` — rule IDs to turn off.
181
+ - `weights` — per-finding penalties into the health score (see [Health score](#health-score)).
182
+ - `tokenSource` — where your design-token colors live (see below). Optional; when unset, FR-301 hints simply report `0 colors found`.
183
+
184
+ ### Token sources
185
+
186
+ `tokenSource` tells `fringe` where to read your palette so token-adoption findings (FR-301) can point at it. Three kinds are supported:
187
+
188
+ | `kind` | Reads | Best for |
189
+ |---|---|---|
190
+ | `css-vars` | every `--*` custom-property declaration in your scanned CSS/SCSS whose value parses as a color | hand-rolled CSS variable sheets |
191
+ | `theme-factory` | same `--*` collection as `css-vars`, kept as a distinct kind for framework theme sheets that compile to custom properties (e.g. Tailwind v4 `@theme { --color-*: ... }`) | framework theme files |
192
+ | `design-tokens-json` | a recursive walk of a JSON token file collecting `$value` strings that parse as colors | W3C design-tokens-format files |
193
+
194
+ Example — pointing at a tokens file:
195
+
196
+ ```json
197
+ {
198
+ "tokenSource": { "kind": "design-tokens-json", "path": "tokens/colors.json" }
199
+ }
200
+ ```
201
+
202
+ with `tokens/colors.json` shaped like:
203
+
204
+ ```json
205
+ {
206
+ "color": {
207
+ "bg": { "primary": { "$value": "#ffffff" } },
208
+ "text": { "primary": { "$value": "#0f172a" } }
209
+ }
210
+ }
211
+ ```
212
+
213
+ `path` is resolved relative to the check root (absolute paths work too). Gathered colors are deduped and reported in FR-301 hints (`tokenSource: 2 colors found`); an unreadable or malformed token file is not a crash — it just yields no tokens. `css-vars` and `theme-factory` need no `path`; they read the same CSS files the scan already covers.
214
+
215
+ ## What it looks for, and where
216
+
217
+ Scanned extensions: `.jsx .tsx .js .ts .html .css .scss .vue .svelte .astro`. The stack (React/Vue/Svelte/Tailwind/plain HTML) is auto-detected from `package.json` and file markers, reported in output, and recorded in the ledger. Structural rules run on parsed JSX/HTML/CSS; text-level rules also scan raw source so unsupported extensions still get coverage.
218
+
219
+ ## License
220
+
221
+ MIT
package/dist/bin.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/bin.js ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ runCli
4
+ } from "./chunk-AOUZJDQL.js";
5
+
6
+ // src/bin.ts
7
+ process.exitCode = await runCli(process.argv.slice(2));