| name | ui-slop-audit |
| description | Audit the Tidings UI for "AI tells" — generic, default-shaped patterns (Inter-by-default, un-customized shadcn, Lucide-everywhere, round fake data, "Get Started" CTAs) that make an interface read as AI-generated. Scores tell-families with file:line and screenshot evidence, returns a P0/P1/P2 punch list. Read-only; never edits app code. Invoke to de-slop the UI or find AI tells. |
| argument-hint | [target page/route, e.g. /budgets or 'the demo'] [--before <ref> --after <ref> for a de-slop diff] [--no-save for inline-only] |
UI Slop Audit — AI-Tells Review
You are a senior product designer running a tell-hunt over the Tidings finance dashboard: find
the patterns that make a UI read as generic-AI-built, score them, and prescribe fixes that fit this
brand. This is read-only analysis unless the user explicitly asks you to apply fixes.
The whole point is the calibration in references/tells-catalog.md:
Tidings can only fail toward under-slop (un-decided shadcn defaults), and the counter-move to a
tell is always more restraint or more specific character, never more decoration. Read that
file in full before scoring — it carves out the intentional-restraint choices you must NOT flag.
Phase 0 — Scope (ask only if unclear)
Parse $ARGUMENTS. It may name a target (/budgets, "the Journal page", "the demo") and/or --no-save.
- No target given: default to a representative sweep —
/ (Journal), /summary, /budgets,
and the demo SPA. Say so and proceed; don't block on a question for this.
- Genuinely ambiguous (e.g. "audit the new thing" with no referent): one
AskUserQuestion.
--no-save present (or the user says they just want it inline / don't want a file): skip the
Phase 5 write and deliver the report in chat only. Otherwise the report is saved by default.
Assume dev servers are already running (per CLAUDE.md: real dashboard :5173, demo SPA :5176).
If a needed surface isn't up, ask the user to start it — do not start servers yourself.
Phase 1 — Ground (before opening Chrome)
- Read
references/tells-catalog.md fully. It is the rubric.
Also skim references/static-checks.md — the deterministic rule
registry Phase 2 runs. It names the P0 rules that gate (cap) a family's score and the §1
carve-outs the detector suppresses, so you know what the static pass will and won't surface.
- Read the brand canon you'll judge copy against:
docs/brand/voice.md and BRAND.md. Voice
rules are non-negotiable and enforced in PR review — defer to them, don't re-decide.
- Confirm the design tokens against source rather than memory —
frontend/src/index.css
(:root, .dark, @theme inline): the --font-* stack, the --brand* ramp, the
--status-* (incl. -calm) tokens. These are your evidence vocabulary.
- Optionally spawn an
Explore agent to map the component tree for the target and grep the
static tells in code in parallel while you inspect live (see Phase 2 grep list). Useful for a
broad sweep; skip for a single-component audit.
Phase 2 — Inspect (live + static, evidence first)
Run the deterministic static pass and the live Chrome pass together; each catches what the
other misses. Kick both off at once — the static pass is one fast command, the Chrome pass is slow.
Deterministic static pass (run first, in parallel with Chrome) — from the repo root:
node .claude/skills/ui-slop-audit/scripts/slop-grep.mjs frontend/src
It walks the registry in references/static-checks.md and emits
file:line · rule-id · family · severity (grouped by rule) with the §1 carve-outs already
suppressed — the token allow-lists are parsed from index.css, so this is the audit's
high-agreement floor. Add --json for structured output, --include-marketing to also sweep the
landing page. These deterministic hits populate the deterministic stratum of the Phase 3
scorecard, and any P0 here caps its family at ≤3. Zero-dep Node; if the script isn't present,
fall back to the freehand grep below.
Freehand grep (only for tells the registry does not yet encode) — the detector already covers
the literal tells below (arbitrary text-[…] sizes, raw #000/#fff, grid-cols-3, emoji,
generic CTAs, round fake numbers), so run this over frontend/src (and frontend/public/demo-data
for data tells) as a cross-check, or when the script is unavailable, and mainly for the felt tells
it can't codify:
lucide-react usage density and where icons are decorative vs informational
text-\[ arbitrary font sizes; whether a type scale exists
#000, #fff, black, white literals bypassing tokens
grid-cols-3 / equal-thirds card rows (start at SummaryCards.tsx)
- nested
Card/container depth (container soup)
- emoji in component strings;
Get Started/Learn More/Submit; clichés ("Elevate", "Seamless",
"Unleash", "Effortless", "Next-Gen")
- round fake numbers and placeholder names in demo fixtures
Live (Chrome DevTools MCP) pass — be thorough; expect 6–15 screenshots before writing a verdict:
- Navigate to each target surface. Hit at least one edge state (over-budget month, empty month,
long-data case) — tells hide in states the happy path never shows.
- Desktop 1440×900: full-page + viewport screenshot +
take_snapshot (a11y tree).
- Mobile 390×844:
resize_page, re-screenshot.
- Dark mode:
evaluate_script → () => document.documentElement.classList.toggle('dark'),
re-screenshot desktop + mobile, then toggle back. The app ships a real dark palette — audit it.
- Console:
list_console_messages (error + warn). Note React errors as their own P0.
- Interact where a tell needs a state — open collapsibles, trigger save/delete/load to check
loading/empty/error coverage, hover primary actions.
Tie every observation to a tell-family in the catalog as you go.
Phase 3 — Score
Score each of the seven families (A Typography, B Color & surface, C shadcn/component defaults,
D Layout & composition, E Motion & state, F Content & data realism, G Microcopy & voice) with the
two-stratum, criterion-resolved rubric in §3 of the catalog — don't collapse a family into one
blurry number:
- Deterministic stratum (high agreement, checkable against tokens): the
slop-grep.mjs pass/fail
results for that family. These are what a stranger would flag on sight, so they gate the score
rather than average into it — any P0 deterministic failure caps the family at ≤3
("default-shaped"), regardless of how it feels.
- Judgment stratum (lower agreement, pure feel): the subjective calls the detector can't make —
does money dominate the hierarchy (A), does the warm
--brand actually appear on screen (B), is
restraint reading as decided vs. colorless-wireframe, is empty/loading coverage intentional (E).
Score 0–10 within the band the deterministic cap allows.
Output a scorecard:
| Family | Deterministic (P0 fails) | Judgment | Capped score | Verdict |
|---|---|---|---|---|
| A. Typography | none (arbitrary-type-size ×N, P2) | 6 | 6/10 | Inter wall; serif/mono defined but never used for contrast |
| B. Color & surface | glass-blur ×1 (P0) | 7 | 3/10 | one blur caps the family at ≤3 until triaged |
| … | … | … | … | … |
Record each family as { family, deterministic: [pass/fail…], judgment_score, capped_score, verdict }.
Reward earned restraint in the judgment stratum — calm-and-decided scores high here; penalize
only un-decided, never un-flashy. If a family is clean because of an intentional carve-out
(catalog §1), say so and score it well rather than inventing a problem. The deterministic cap is not
a taste call — it is the high-agreement floor, so never argue a P0-capped family back up on feel.
Phase 4 — Findings & punch list
Auditing a change (a branch/diff, or explicit --before/--after)? Grade the delta, not
absolute scores — jump to Pairwise mode (de-slop diffs) below.
For each real tell: what / where (file:line + screenshot or state) / why it reads as AI here /
severity (P0/P1/P2) / counter-move. Group by family, sorted by severity. Then a single:
Punch list — ordered by impact × effort
A numbered list, top item both high-impact and cheap. Name the file(s) to touch and a rough effort
(min/hrs) per item. 8–12 items.
Pairwise mode (de-slop diffs)
When invoked on a change rather than a cold audit — a branch/diff, or explicit --before <ref> --after <ref> — run the full rubric on both states and report per-family deltas instead of
absolute scores: Δ P0-count, Δ judgment, Δ capped. Re-run slop-grep.mjs against each state (or
--json on both and diff the findings) so the deterministic deltas are exact. Close each family
with a one-line "reduced slop, or just moved the tell?" verdict. TASTE's pairwise A/B cuts
calibration noise — the question is directional, not absolute.
The §0 rule still governs the after: a fix that lowers a number by adding a decorative cure
(gradient / glow / glass / animation / ornament) has not de-slopped anything — it fails §0 even
though the count dropped. A real win removes the tell or aligns it to a token; it never trades one
tell for a prettier one.
Every counter-move must pass the §0 rule: restraint or specific character, never decoration. If a
fix you wrote says "add a gradient / animation / glow / glass," delete it and re-derive — that is
over-slop and a brand-voice violation. Don't recommend what you can't verify; cite evidence.
Also list what's genuinely well-decided (the intentional restraint that's working) — it tells the
user what to preserve, and distinguishes this from a generic "make it fancier" critique.
Phase 5 — Output (save by default)
Default: save the report as a dated spec. Write it to
docs/specs/<today>-ui-slop-audit/README.md, where <today> is the current date in YYYY-MM-DD
form (from the session's date context, e.g. 2026-06-09-ui-slop-audit) — the YYYY-MM-DD-slug
directory convention every other spec follows. If a folder for today already exists, append a short
suffix (e.g. -budgets) so you never overwrite a prior audit. Then add one row to
docs/specs/INDEX.md with status Analysis, placed in date order. Writing these two files is the
only write a default audit makes — never edit app code here.
The saved README.md contains the full report: scorecard, findings by family, punch list, and
"what's working." After writing, also print to chat a short summary — the scorecard table, the top
3 punch-list items, and the path to the saved file — so the user gets the gist without opening it.
Report style (both saved and inline): no executive-summary chrome, no letter grade, no
"implementation roadmap" section. Lead with the scorecard.
If --no-save (or the user only wanted it in chat): skip the writes and deliver the full report
inline instead.
If the user asked to fix tells: stop after the punch list and recommend they re-invoke with an
explicit apply request (or enter plan mode). Don't edit components inside the audit run.
Anti-patterns (for this skill itself)
- Recommending decoration — animation, gradient, glass, glow — as a cure. Wrong direction for Tidings.
- Flagging the intentional-restraint carve-outs (zero-chroma neutrals, calm motion, muted status) as
slop. Read catalog §1.
- Hallucinated token values or contrast ratios — ground in
frontend/src/index.css.
- Auditing only light desktop — dark + mobile hide their own tells.
- A vague "looks AI-generated" with no
file:line and no specific tell named.
- Penalizing restraint. Calm and decided is a high score, not a deduction.
- Skipping the deterministic static pass and eyeballing greps by hand — the registry is the
high-agreement floor, and a family with an unresolved P0 deterministic failure cannot score above 3.
- Treating a decorative cure as a de-slop win in pairwise mode — a lower count bought with a
gradient/glow/glass/animation still fails §0.