samey-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/CHANGELOG.md +43 -0
- package/LICENSE +21 -0
- package/README.md +224 -2
- package/dist/ai/local-model.d.ts +26 -0
- package/dist/ai/local-model.js +91 -0
- package/dist/audit.d.ts +18 -0
- package/dist/audit.js +250 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +166 -0
- package/dist/config.d.ts +34 -0
- package/dist/config.js +40 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +5 -0
- package/dist/render/crawl.d.ts +34 -0
- package/dist/render/crawl.js +337 -0
- package/dist/render/extract.d.ts +8 -0
- package/dist/render/extract.js +441 -0
- package/dist/render/interact.d.ts +8 -0
- package/dist/render/interact.js +76 -0
- package/dist/render/network.d.ts +33 -0
- package/dist/render/network.js +92 -0
- package/dist/render/robots.d.ts +8 -0
- package/dist/render/robots.js +61 -0
- package/dist/render/server.d.ts +15 -0
- package/dist/render/server.js +174 -0
- package/dist/report/html.d.ts +2 -0
- package/dist/report/html.js +368 -0
- package/dist/report/prompt.d.ts +12 -0
- package/dist/report/prompt.js +62 -0
- package/dist/report/terminal.d.ts +6 -0
- package/dist/report/terminal.js +73 -0
- package/dist/rules/content.d.ts +8 -0
- package/dist/rules/content.js +227 -0
- package/dist/rules/functional.d.ts +17 -0
- package/dist/rules/functional.js +467 -0
- package/dist/rules/helpers.d.ts +11 -0
- package/dist/rules/helpers.js +29 -0
- package/dist/rules/index.d.ts +13 -0
- package/dist/rules/index.js +100 -0
- package/dist/rules/normalize.d.ts +18 -0
- package/dist/rules/normalize.js +114 -0
- package/dist/rules/static.d.ts +9 -0
- package/dist/rules/static.js +314 -0
- package/dist/rules/types.d.ts +39 -0
- package/dist/rules/types.js +16 -0
- package/dist/rules/visual.d.ts +15 -0
- package/dist/rules/visual.js +463 -0
- package/dist/score.d.ts +31 -0
- package/dist/score.js +66 -0
- package/dist/security/fetch.d.ts +26 -0
- package/dist/security/fetch.js +86 -0
- package/dist/security/net-policy.d.ts +46 -0
- package/dist/security/net-policy.js +178 -0
- package/dist/static/scan.d.ts +15 -0
- package/dist/static/scan.js +191 -0
- package/dist/types.d.ts +316 -0
- package/dist/types.js +1 -0
- package/dist/util/color.d.ts +25 -0
- package/dist/util/color.js +180 -0
- package/dist/util/text.d.ts +22 -0
- package/dist/util/text.js +106 -0
- package/package.json +69 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Renamed
|
|
6
|
+
- The project is now **samey** (was unslop): the npm name `unslop` belongs to an unrelated tool. The command, report branding, output folder (`.samey/`), and config file (`samey.config.json`) all changed. The npm package is `samey-cli` (npm rejects `samey` as too similar to existing packages); the installed command is `samey`.
|
|
7
|
+
|
|
8
|
+
### Security
|
|
9
|
+
- All browser requests are intercepted and performed by a Node client that validates the URL, resolves DNS, rejects names with any non-public address, and pins the connection to the validated address (DNS rebinding).
|
|
10
|
+
- Redirects are re-validated hop by hop; loops and long chains are stopped. Per-response (20 MB) and per-crawl (250 MB, 3,000 requests) limits.
|
|
11
|
+
- Loopback, private, link-local, reserved, and cloud metadata addresses are blocked, including IPv4-mapped, NAT64, and 6to4 forms and numeric host tricks.
|
|
12
|
+
- Service workers, downloads, pop-ups, and (in normal mode) WebSockets are blocked; stray Chromium traffic goes to a dead proxy.
|
|
13
|
+
- New `--dev` flag required to audit localhost or private `--url` targets. Metadata addresses stay blocked.
|
|
14
|
+
- Secrets in excerpts are redacted; source paths in findings must be relative, inside the project, and in the scanned file set.
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Every audit writes `.samey/fix-prompt.md`: a ready-to-paste prompt for Claude Code, Cursor or Codex, measured defects first, then design observations, each with page, selector, file:line and the fix. `samey prompt` prints it (`--only-defects` to skip observations). The report's copy buttons use the same text.
|
|
18
|
+
- `samey setup` installs Chromium with samey's bundled Playwright.
|
|
19
|
+
- The NovaFlow before/after example (`examples/novaflow`) and a demo GIF in the README.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
- HTML document boilerplate (doctype, head, meta tags) shared by every page was reported as copy-pasted component markup.
|
|
23
|
+
- Sites that redirect (bare domain to `www`, `http` to `https`, moved pages) failed every page with `ERR_PROXY_CONNECTION_FAILED`: Chromium followed redirects from intercepted responses outside the guard. Navigation redirects are now re-checked and followed by the crawler, subresource redirects are followed hop by hop in Node, and a redirecting start URL adopts its final origin.
|
|
24
|
+
- Requests samey itself blocked (for example a video over the 20 MB response cap) were reported as failed assets and console errors on the site. They are now listed only under coverage.
|
|
25
|
+
- Redirected pages are recorded under the URL they landed on and are not audited twice.
|
|
26
|
+
- Reports and progress output show local paths relative to home or the working directory instead of the full path.
|
|
27
|
+
|
|
28
|
+
### Changed (breaking)
|
|
29
|
+
- Score is inverted to match the brief: 0 = no slop indicators, 100 = many or severe. Lower is better.
|
|
30
|
+
- Scoring uses `p = s × c × w` per finding and `100 × (1 − e^(−P/100))` per category, with brief weights: visual 40%, content 20%, functional 15%, accessibility 15%, technical 10%. Unmeasured categories are excluded and the rest renormalized.
|
|
31
|
+
- Grade labels ("Distinct", "Generic", ...) removed until calibrated.
|
|
32
|
+
- Finding schema: `description` (was `message`), numeric `confidence` 0..1, `fingerprint`, `verified`, `ruleVersion`, `routes`. Evidence uses `type`, `route`, `excerpt`, `screenshotPath`, `alsoOn`.
|
|
33
|
+
- Report adds `schemaVersion`, `scanId`, `project`, `versions`, `viewports`, `failedRoutes`, `coverage.limitations`, `coverage.blockedRequests`.
|
|
34
|
+
- `--fail-under` replaced by `--fail-above`. `--report` added as an alias for `--format`.
|
|
35
|
+
|
|
36
|
+
### Added
|
|
37
|
+
- Deduplication by fingerprint; identical evidence across routes collapses into one item, penalized once.
|
|
38
|
+
- Per-rule penalty weights; convention-based rules weigh less.
|
|
39
|
+
- A single modest stock gradient is reported as `info` with no penalty.
|
|
40
|
+
|
|
41
|
+
## 0.1.0
|
|
42
|
+
|
|
43
|
+
- First vertical slice: CLI, repository scan, Playwright crawl at two viewports, 37 rules, scoring, self-contained HTML and JSON reports, optional local Ollama critique, three fixtures, tests.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 samey 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,225 @@
|
|
|
1
|
-
#
|
|
1
|
+
# samey
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**Is your AI-built site samey?** samey audits a website built with Claude Code, Cursor, Codex or any other coding agent, shows you what makes it look like every other one, and writes a prompt your agent can use to fix it.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npx samey-cli audit .
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Or install it once with `npm i -g samey-cli` and run `samey audit .`.
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
> **Early release.** The rules work and are tested, but the scoring weights have not been calibrated against human judgment yet. If samey flags something that looks fine to you, or misses something obvious, please [open an issue](https://github.com/azkaiftikhar01/samey/issues) with the URL or a screenshot. That feedback is exactly what will make it accurate.
|
|
14
|
+
|
|
15
|
+
## What it catches
|
|
16
|
+
|
|
17
|
+
- **Stock visuals:** the blue-violet-pink gradient, gradient headline text, glows and blurred blobs, emoji used as icons, everything centered.
|
|
18
|
+
- **Sameness:** the same three-card grid in every section, the hero-logos-features-testimonials-pricing-FAQ sequence, different pages built from one skeleton.
|
|
19
|
+
- **Template copy:** "Unlock the power of seamless...", lorem ipsum, Jane Doe at Acme, unsourced "Trusted by 10,000+ teams".
|
|
20
|
+
- **Things that are simply broken:** links to `#`, buttons that do nothing, broken links and images, console errors, pages that scroll sideways on a phone.
|
|
21
|
+
- **Accessibility gaps:** buttons without names, images without alt text, unlabeled inputs, tiny touch targets.
|
|
22
|
+
|
|
23
|
+
Every finding has evidence you can check: the page and viewport, the CSS selector, a screenshot crop with the element outlined, and the source file and line when samey can trace it. Measured defects are kept separate from design observations, which are heuristics that need your judgment. Popular conventions are not penalized on their own.
|
|
24
|
+
|
|
25
|
+
samey runs entirely on your machine: no account, no API key, no cloud. It measures what is on the page; it does not try to guess who or what wrote it.
|
|
26
|
+
|
|
27
|
+
## Before and after
|
|
28
|
+
|
|
29
|
+

|
|
30
|
+
|
|
31
|
+
| | Slop score | Findings |
|
|
32
|
+
| --- | ---: | ---: |
|
|
33
|
+
| [Before](examples/novaflow/before) | 53.3 | 26 |
|
|
34
|
+
| [After](examples/novaflow/after) | 0 | 0 |
|
|
35
|
+
|
|
36
|
+
The before site is a deliberately generic landing page. samey produced [this fix prompt](examples/novaflow/fix-prompt.md) for it, and the after site is that site rewritten from the prompt by a coding agent: one deliberate accent colour instead of the gradient, a product preview and a worked example instead of three-card grids, a pricing table, real links, and the invented testimonials and user counts removed rather than reworded. Run both yourself:
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx samey-cli audit examples/novaflow/before
|
|
40
|
+
npx samey-cli audit examples/novaflow/after
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
A score of 0 means samey found nothing to flag, not that a site is perfect. It is a small demo site; real sites will rarely reach zero.
|
|
44
|
+
|
|
45
|
+
## Quick start
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx samey-cli setup # once: downloads the Chromium build samey uses (about 150 MB)
|
|
49
|
+
|
|
50
|
+
npx samey-cli audit . # a static build (out/, dist/, build/) or plain HTML
|
|
51
|
+
npx samey-cli audit . --serve "npm run dev" --port 3000 # samey starts your dev server, audits, stops it
|
|
52
|
+
npx samey-cli audit --url https://your-site.com # a deployed site
|
|
53
|
+
npx samey-cli audit . --url http://localhost:3000 --dev # a dev server you already started
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each run writes three files to `.samey/` (add it to your `.gitignore`):
|
|
57
|
+
|
|
58
|
+
- `report.html`: the full report, self-contained, open it in any browser
|
|
59
|
+
- `fix-prompt.md`: a ready-to-paste prompt for your coding agent
|
|
60
|
+
- `report.json`: everything, for scripts and agents
|
|
61
|
+
|
|
62
|
+
## Fix it with your coding agent
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx samey-cli prompt | pbcopy # macOS; or open .samey/fix-prompt.md
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Paste it into Claude Code, Cursor or Codex. The prompt lists measured defects first, then design observations to weigh, each with the page, selector, file and line, and the fix. It tells the agent to ask you for anything only you can provide, like real testimonials or a brand colour, instead of inventing it. Then run `npx samey-cli audit` again to confirm.
|
|
69
|
+
|
|
70
|
+
Prefer to pick? Tick findings in the HTML report and use "Copy fix prompt", or run `npx samey-cli prompt --only-defects` to skip the design observations.
|
|
71
|
+
|
|
72
|
+
## Commands
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
samey audit [path] [options] audit a project, a URL, or both (npx samey-cli audit ...)
|
|
76
|
+
samey prompt [path] print the fix prompt from the last audit
|
|
77
|
+
samey setup install the browser samey uses
|
|
78
|
+
samey rules list every rule
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
| Option | Default | Description |
|
|
82
|
+
| --- | --- | --- |
|
|
83
|
+
| `-u, --url <url>` | | Audit a running site. Overrides any local server. |
|
|
84
|
+
| `--dev` | | Development mode: lets `--url` point at localhost or a private network address. |
|
|
85
|
+
| `--serve [command]` | | Start the dev server with this command (`npm run dev` if omitted). This runs project code, so it only happens when you pass the flag. |
|
|
86
|
+
| `--port <port>` | 4173 | Port the dev server listens on. `PORT` is set in its environment. |
|
|
87
|
+
| `--max-routes <n>` | 10 | Maximum pages to render. Skipped pages are listed in the report. |
|
|
88
|
+
| `--timeout <seconds>` | 30 | Navigation timeout per page. |
|
|
89
|
+
| `--route <path...>` | | Extra routes to audit. |
|
|
90
|
+
| `--format <html\|json\|both>` | both | Which reports to write. `--report` is an alias. |
|
|
91
|
+
| `-o, --out <dir>` | `<path>/.samey` | Output directory. |
|
|
92
|
+
| `--static-only` | | Only scan source files; do not launch a browser. |
|
|
93
|
+
| `--no-screenshots` | | Skip screenshots. |
|
|
94
|
+
| `--no-interact` | | Do not click any buttons. |
|
|
95
|
+
| `--ignore-robots` | | Crawl pages disallowed by robots.txt. Only for sites you own. |
|
|
96
|
+
| `--ignore <rules...>` | | Rule ids to skip (see `samey rules`). |
|
|
97
|
+
| `--fail-above <score>` | | Exit with code 1 if the slop score is above this. For CI. |
|
|
98
|
+
| `--model <name>` | | Local Ollama vision model for an advisory critique. |
|
|
99
|
+
| `--model-endpoint <url>` | `http://127.0.0.1:11434` | Must be a loopback address. |
|
|
100
|
+
| `--no-ai` | | Never use a model, even if one is configured. |
|
|
101
|
+
| `--json` | | Print the JSON report to stdout. |
|
|
102
|
+
| `-q, --quiet` | | Only print the final summary. |
|
|
103
|
+
|
|
104
|
+
Exit codes: `0` success, `1` score above `--fail-above`, `2` the audit could not run (bad arguments, a blocked or unreachable URL, no page could be rendered, dev server failed to start).
|
|
105
|
+
|
|
106
|
+
### Config file
|
|
107
|
+
|
|
108
|
+
`samey.config.json` in the project root sets defaults. Command-line flags win.
|
|
109
|
+
|
|
110
|
+
```json
|
|
111
|
+
{
|
|
112
|
+
"maxRoutes": 15,
|
|
113
|
+
"routes": ["/pricing", "/docs"],
|
|
114
|
+
"ignoreRules": ["centered-everything"],
|
|
115
|
+
"failAbove": 30,
|
|
116
|
+
"serve": "pnpm dev",
|
|
117
|
+
"port": 5173
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
`serve` in the config file is only used when you also pass `--serve` on the command line, so cloning an untrusted repository and running `samey audit .` never executes its code.
|
|
122
|
+
|
|
123
|
+
## How the target is chosen
|
|
124
|
+
|
|
125
|
+
1. `--url` if given. Public addresses only, unless you pass `--dev`.
|
|
126
|
+
2. Otherwise, with `--serve`, the dev server samey starts.
|
|
127
|
+
3. Otherwise, a static build in the project (`out/`, `dist/`, `build/`, `_site/`), or the project root if it is plain HTML. samey serves it read-only on 127.0.0.1.
|
|
128
|
+
4. Otherwise only static analysis runs, and the report says so. Categories that need a rendered page show n/a rather than a perfect score.
|
|
129
|
+
|
|
130
|
+
Routes are discovered from links, `sitemap.xml`, `--route`, and route files in the project (Next.js `app/` and `pages/`, SvelteKit, Astro, React Router paths). Dynamic routes like `/blog/[slug]` are skipped.
|
|
131
|
+
|
|
132
|
+
## What it checks
|
|
133
|
+
|
|
134
|
+
Run `samey rules` for the full list. In summary:
|
|
135
|
+
|
|
136
|
+
**Visual genericness**: blue-violet-pink gradients (including gradient text), decorative glows, blur blobs and glass panels, one card composition reused across sections, consecutive sections with identical layout, different pages built from the same skeleton, the stock hero-logos-features-testimonials-pricing-FAQ-CTA sequence, centered everything, emoji used as icons.
|
|
137
|
+
|
|
138
|
+
**Copy and credibility**: stock marketing phrases ("unlock the power", "seamless", "next-generation"), placeholder content (lorem ipsum, Jane Doe, Acme, example.com, placeholder image services), repeated paragraphs, unsourced user counts and ratings, testimonials with only a name and title.
|
|
139
|
+
|
|
140
|
+
**Functionality**: pages that fail to load, broken internal links, console errors, failed requests, links to `#`, buttons that do nothing when clicked, broken images, missing titles and descriptions.
|
|
141
|
+
|
|
142
|
+
**Responsive and accessibility**: horizontal scrolling, touch targets under 24px, text under 12px on mobile, controls without accessible names, images without alt, unlabeled inputs, heading structure, vague link text.
|
|
143
|
+
|
|
144
|
+
**Design consistency**: too many font sizes, radii, shadows, or font families; spacing off a 4px scale; hard-coded values bypassing tokens; markup copy-pasted instead of extracted into components.
|
|
145
|
+
|
|
146
|
+
Static checks on the source tree mirror several rendered checks. When both fire, the source lines are attached to the rendered finding as evidence instead of being reported twice.
|
|
147
|
+
|
|
148
|
+
### Safe click testing
|
|
149
|
+
|
|
150
|
+
On each page, samey clicks up to six visible buttons to see whether anything happens. It never clicks links (those are checked by the crawler), anything inside a form, submit buttons, or anything labelled like delete, buy, pay, checkout, subscribe, log out, send, confirm, and similar. Use `--no-interact` to turn this off.
|
|
151
|
+
|
|
152
|
+
## Scoring
|
|
153
|
+
|
|
154
|
+
The score counts detected quality problems. It is not a probability that AI wrote the site.
|
|
155
|
+
|
|
156
|
+
Each finding adds a penalty `p = s × c × w`:
|
|
157
|
+
|
|
158
|
+
- `s`, severity: info 0, low 0.25, medium 0.5, high 0.75, critical 1
|
|
159
|
+
- `c`, confidence: a number from 0 to 1
|
|
160
|
+
- `w`, the rule's weight: 40 by default, lower for rules about conventions (centered layouts, the landing-page sequence, unverified claims), higher for hard failures (a page that does not load: 80)
|
|
161
|
+
|
|
162
|
+
Within a category the penalties add up to `P`, and the category score is `100 × (1 − e^(−P/100))`, so a pile of minor findings approaches 100 but never passes it. The overall score is the weighted average of the measured categories:
|
|
163
|
+
|
|
164
|
+
| Category | Weight |
|
|
165
|
+
| --- | ---: |
|
|
166
|
+
| Visual genericness | 40% |
|
|
167
|
+
| Content genericness | 20% |
|
|
168
|
+
| Functional defects | 15% |
|
|
169
|
+
| Accessibility and usability | 15% |
|
|
170
|
+
| Technical quality | 10% |
|
|
171
|
+
|
|
172
|
+
Categories that could not be measured (for example functional defects in a static-only run) are shown as "not measured" and left out, with the remaining weights renormalized. Identical evidence on several pages, such as a broken link in a shared footer, counts once. The same set of findings always produces the same score. Weights and penalties are starting hypotheses, versioned in the report (`versions.scoring`), and have not yet been calibrated against human judgments, so there are deliberately no labels like "good" or "bad".
|
|
173
|
+
|
|
174
|
+
## Network safety
|
|
175
|
+
|
|
176
|
+
Every request the browser makes is intercepted and performed by samey itself, so the audited page cannot use the browser to reach places it should not:
|
|
177
|
+
|
|
178
|
+
- Only `http` and `https`. URLs with embedded credentials are refused.
|
|
179
|
+
- In normal mode only public addresses are reachable. Loopback, private (including carrier-grade NAT), link-local, multicast, documentation, and other reserved ranges are blocked, including IPv4-mapped, NAT64, and 6to4 IPv6 forms and numeric tricks like `http://2130706433/`.
|
|
180
|
+
- Hostnames are resolved before connecting, every returned address must be allowed, and the connection is pinned to the checked address, which defeats DNS rebinding.
|
|
181
|
+
- Every redirect hop is checked again. Redirect loops and chains longer than 5 hops are stopped.
|
|
182
|
+
- Responses are capped at 20 MB each and 250 MB per crawl, with 3,000 requests at most.
|
|
183
|
+
- Service workers, downloads, and pop-up windows are blocked. WebSockets are blocked in normal mode. Anything Chromium tries to send outside the interception (preconnects, WebRTC) goes to a closed local port.
|
|
184
|
+
- Cloud metadata addresses (169.254.169.254 and others) are never reachable, even with `--dev`.
|
|
185
|
+
- With `--dev`, loopback and private addresses are allowed so you can audit a local server. When samey starts the server itself (static build or `--serve`), only loopback is allowed.
|
|
186
|
+
- Crawling stays on the starting origin. Pages that redirect to another site are skipped.
|
|
187
|
+
|
|
188
|
+
Blocked requests are listed in the report's coverage section. Text on the audited page is treated as content, never as instructions.
|
|
189
|
+
|
|
190
|
+
## Optional local model
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
ollama pull qwen2.5vl:7b
|
|
194
|
+
samey audit . --model qwen2.5vl:7b
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
samey sends up to three desktop screenshots to the model and asks for specific visual observations. Only loopback endpoints are accepted, so screenshots never leave your machine. Model observations are marked advisory, given confidence 0.3, and never change the score. No model has been benchmarked with samey yet; the model name is recorded in the report. If no model is configured or the server is not running, the audit runs normally and the report notes it.
|
|
198
|
+
|
|
199
|
+
## For coding agents
|
|
200
|
+
|
|
201
|
+
`report.json` is designed to be read by agents directly. See [AGENTS.md](AGENTS.md) for its shape and a suggested fixing order.
|
|
202
|
+
|
|
203
|
+
## Limits
|
|
204
|
+
|
|
205
|
+
- Ten routes and two viewports by default. Large sites are sampled, not exhausted.
|
|
206
|
+
- Accessibility checks are samey's own, not axe-core yet, and technical quality covers page metadata only (no Lighthouse yet). See PLAN.md.
|
|
207
|
+
- Heuristics are tuned to avoid false positives on plain, well-built pages, but they are heuristics. Low-confidence findings are worth a look, not an automatic fix.
|
|
208
|
+
- Single-page apps that render routes without real URLs only get their first screen audited.
|
|
209
|
+
- samey never modifies your source files.
|
|
210
|
+
|
|
211
|
+
## Development
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npm install
|
|
215
|
+
npx playwright install chromium
|
|
216
|
+
npm test # unit, security, and end-to-end tests
|
|
217
|
+
npm run typecheck
|
|
218
|
+
npm run dev -- audit fixtures/sloppy-site # run from source
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
`fixtures/sloppy-site` is a deliberately generic multi-page site, `fixtures/clean-site` is a plain site that should produce no genericness findings, and `fixtures/react-app` is a source tree for static analysis. See [PLAN.md](PLAN.md) for status and next steps.
|
|
222
|
+
|
|
223
|
+
## License
|
|
224
|
+
|
|
225
|
+
MIT
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { DraftFinding } from '../rules/types.js';
|
|
2
|
+
import type { CrawlResult } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Optional visual critique by a local, open-weight vision model served by Ollama.
|
|
5
|
+
* Only loopback endpoints are accepted, so screenshots never leave the machine.
|
|
6
|
+
* Model output is advisory: it is shown in the report but never changes the score.
|
|
7
|
+
*/
|
|
8
|
+
export interface ModelOptions {
|
|
9
|
+
model: string;
|
|
10
|
+
endpoint: string;
|
|
11
|
+
timeoutMs: number;
|
|
12
|
+
maxPages: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function assertLoopback(endpoint: string): URL;
|
|
15
|
+
interface ModelFinding {
|
|
16
|
+
title: string;
|
|
17
|
+
observation: string;
|
|
18
|
+
recommendation: string;
|
|
19
|
+
}
|
|
20
|
+
export declare function parseModelOutput(raw: string): ModelFinding[];
|
|
21
|
+
export declare function checkModel(opts: ModelOptions): Promise<{
|
|
22
|
+
ok: boolean;
|
|
23
|
+
note?: string;
|
|
24
|
+
}>;
|
|
25
|
+
export declare function critiqueScreens(crawl: CrawlResult, shotsDir: string, opts: ModelOptions): Promise<DraftFinding[]>;
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const LOOPBACK = new Set(['localhost', '127.0.0.1', '::1', '[::1]']);
|
|
4
|
+
export function assertLoopback(endpoint) {
|
|
5
|
+
let u;
|
|
6
|
+
try {
|
|
7
|
+
u = new URL(endpoint);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
throw new Error(`Invalid model endpoint: ${endpoint}`);
|
|
11
|
+
}
|
|
12
|
+
if (!LOOPBACK.has(u.hostname)) {
|
|
13
|
+
throw new Error(`Model endpoint must be on this machine (localhost/127.0.0.1/::1). Refusing to send screenshots to ${u.hostname}.`);
|
|
14
|
+
}
|
|
15
|
+
return u;
|
|
16
|
+
}
|
|
17
|
+
const PROMPT = `You are reviewing a screenshot of a web page for design quality. Focus only on what is visible.
|
|
18
|
+
List up to 5 specific, observable problems that make the page look generic, template-like, or unfinished:
|
|
19
|
+
for example weak visual hierarchy, stock gradients, interchangeable hero layout, decorative effects without purpose,
|
|
20
|
+
cramped or inconsistent spacing, unclear primary action. Do not guess who made the site. Do not praise.
|
|
21
|
+
Respond with JSON only: {"findings":[{"title":"short title","observation":"what you see and where","recommendation":"concrete fix"}]}`;
|
|
22
|
+
export function parseModelOutput(raw) {
|
|
23
|
+
const start = raw.indexOf('{');
|
|
24
|
+
const end = raw.lastIndexOf('}');
|
|
25
|
+
if (start < 0 || end <= start)
|
|
26
|
+
return [];
|
|
27
|
+
try {
|
|
28
|
+
const parsed = JSON.parse(raw.slice(start, end + 1));
|
|
29
|
+
if (!Array.isArray(parsed.findings))
|
|
30
|
+
return [];
|
|
31
|
+
return parsed.findings
|
|
32
|
+
.filter((f) => !!f && typeof f === 'object' && typeof f.title === 'string' && typeof f.observation === 'string')
|
|
33
|
+
.map((f) => ({ title: f.title.slice(0, 100), observation: f.observation.slice(0, 500), recommendation: String(f.recommendation ?? '').slice(0, 400) }))
|
|
34
|
+
.slice(0, 5);
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export async function checkModel(opts) {
|
|
41
|
+
const base = assertLoopback(opts.endpoint);
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(new URL('/api/tags', base), { signal: AbortSignal.timeout(3000) });
|
|
44
|
+
if (!res.ok)
|
|
45
|
+
return { ok: false, note: `endpoint returned HTTP ${res.status}` };
|
|
46
|
+
const body = (await res.json());
|
|
47
|
+
const names = (body.models ?? []).map((m) => m.name);
|
|
48
|
+
if (!names.some((n) => n === opts.model || n.split(':')[0] === opts.model)) {
|
|
49
|
+
return { ok: false, note: `model "${opts.model}" is not pulled (available: ${names.join(', ') || 'none'}). Run: ollama pull ${opts.model}` };
|
|
50
|
+
}
|
|
51
|
+
return { ok: true };
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return { ok: false, note: `no local model server at ${opts.endpoint}` };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
export async function critiqueScreens(crawl, shotsDir, opts) {
|
|
58
|
+
const base = assertLoopback(opts.endpoint);
|
|
59
|
+
const findings = [];
|
|
60
|
+
const pages = crawl.pages.filter((p) => p.desktop?.screenshot).slice(0, opts.maxPages);
|
|
61
|
+
for (const p of pages) {
|
|
62
|
+
const img = readFileSync(path.join(shotsDir, p.desktop.screenshot)).toString('base64');
|
|
63
|
+
const res = await fetch(new URL('/api/generate', base), {
|
|
64
|
+
method: 'POST',
|
|
65
|
+
headers: { 'content-type': 'application/json' },
|
|
66
|
+
body: JSON.stringify({ model: opts.model, prompt: PROMPT, images: [img], stream: false, format: 'json', options: { temperature: 0.2 } }),
|
|
67
|
+
signal: AbortSignal.timeout(opts.timeoutMs),
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok)
|
|
70
|
+
continue;
|
|
71
|
+
const body = (await res.json());
|
|
72
|
+
for (const mf of parseModelOutput(body.response ?? '')) {
|
|
73
|
+
findings.push({
|
|
74
|
+
ruleId: 'model-critique',
|
|
75
|
+
ruleVersion: 1,
|
|
76
|
+
verified: false,
|
|
77
|
+
title: mf.title,
|
|
78
|
+
category: 'visual',
|
|
79
|
+
severity: 'info',
|
|
80
|
+
confidence: 0.3,
|
|
81
|
+
source: 'model',
|
|
82
|
+
advisory: true,
|
|
83
|
+
description: mf.observation,
|
|
84
|
+
recommendation: mf.recommendation || 'Review the screenshot and decide whether this applies.',
|
|
85
|
+
evidence: [{ type: 'screenshot', route: p.path, viewport: 'desktop', screenshotPath: `shots/${p.desktop.screenshot}`, detail: `${opts.model} reviewing the desktop screenshot` }],
|
|
86
|
+
routes: [p.path],
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return findings;
|
|
91
|
+
}
|
package/dist/audit.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { AuditOptions } from './config.js';
|
|
2
|
+
import type { AuditReport } from './types.js';
|
|
3
|
+
export declare function toolVersion(): string;
|
|
4
|
+
export interface AuditResult {
|
|
5
|
+
report: AuditReport;
|
|
6
|
+
files: {
|
|
7
|
+
html?: string;
|
|
8
|
+
json?: string;
|
|
9
|
+
prompt?: string;
|
|
10
|
+
};
|
|
11
|
+
ruleErrors: {
|
|
12
|
+
ruleId: string;
|
|
13
|
+
message: string;
|
|
14
|
+
}[];
|
|
15
|
+
}
|
|
16
|
+
export declare class AuditError extends Error {
|
|
17
|
+
}
|
|
18
|
+
export declare function runAudit(opts: AuditOptions): Promise<AuditResult>;
|