# Design (https://blode.co/taste-lint/docs/design) Rule, unit, request, and finding contracts, plus the pipeline map. ## Contracts ### Rule One YAML file per rule at `data/rules//.yaml`. `id` equals the filename. Fields and their meaning live in `src/types.ts` (`Rule`) and are validated fail-closed by `src/rules/validate.ts`. An invalid rule aborts before any request is made. A rule writes only what cannot be derived. The loader fills in `domain` (from the category), `tier` (from which of `mechanical` and `question` are present) and, for every unit kind but `source`, `scope.include` (from the unit kinds: paragraph and heading come from Markdown, jsx-text and class lists from TSX and JSX, attribute strings from both). Every rule skips tests, stories, and changelogs. `scope.exclude` adds to that list. - A mechanical section alone runs in code (`tier: mechanical`). Raw-source regex rules only nominate candidates; their matches do not prove the source skill's conclusion. - A question alone sends one question per matching unit (`tier: jev`). - Both together make the mechanical part a candidate filter (`tier: both`). Only units it fires on are sent to Jev, and the mechanical hit never surfaces alone. - A rule is YAML data (`data/rules//.yaml`: regex, phrases, `absent`, a Jev question) or a code object (`src/rules/code/*.ts`: the same fields with `check(unit)` in place of `mechanical`). Anything that counts, compares, or measures is a code rule. The loader returns both kinds in one list. - `mechanical.absent` pairs with `regex`: the rule fires only when `regex` matches and `absent` matches nowhere in the unit (a file with a `
` and no focus call). It is how rg's `--files-without-match` pipelines port. - `thresholds.act` and `thresholds.review` are per rule. Findings at or above `act` fail the run, between the two print as review notes, below are silent. - `severity` (`major` or `minor`) says how bad a finding is if real. It is independent of probability. - `source` points at the exact file and line the rule was harvested from. `handWritten` lists the keys `scripts/port-rules.ts` must not overwrite. - Raw-source candidates require `review.applicability`, `review.exceptions`, `review.evidence`, and `review.verification`. They remain review-only, including under tuning overlays. Reports mark them `assessment: candidate`; their probability describes a pattern match, not confidence in a defect. `eval` treats them as unknown until an evidence-based detector replaces the search. Agent handoffs omit defect confidence for candidates. - `review.sourceHash` records the reviewed skill document. `port-rules --check` detects changes to that document, including exceptions and verification guidance, and requires a procedure review. - `data/rules/tuning.json` overlays `thresholds.act` and `status` per rule and is written only by `tune --write`. ### Unit `src/types.ts` (`Unit`). One extracted piece of text, one class list, or (kind `source`) one whole TSX, JSX, or CSS file for mechanical rules that pattern-match raw markup. A raw-source regex is a candidate search. A source rule with a question gives Jev bounded complete-file context; oversized input stays unknown. Each has `file`, 1-based `line`/`column`, UTF-16 source offsets, `fixRanges` (the prose inside that slice a fix may rewrite: JSX text pieces, string literal insides, Markdown text nodes; absent when nothing can be rewritten safely), `inCode`, `context` (heading above, doc type, element, role) and, for class lists, resolved typography plus text-bearing neighbours. `id` is a stable hash used for cache keys and SARIF fingerprints. ### Jev request `src/map/jev.ts` speaks `POST /v1/systemone` with `\{ model, state, questions \}`, or the same request through Vercel AI Gateway's evaluation route (model in a header, `boolean` for `noul`) when only `AI_GATEWAY_API_KEY` is set. State is a labelled string built from the unit and only the context keys the batched questions asked for. Questions use the noul primitive with `criteria.true` and `criteria.false` as `\{ what, examples \}` objects, which is the structured form the API accepts. The response is validated fail-closed. Bodies are never logged. ### Finding `src/types.ts` (`Finding`): rule id, category, domain, severity, band, probability, unit provenance, message, evidence, fix hint. ## Pipeline `src/cli.ts` (Commander) calls `src/lint.ts`, which runs extract, judge, reduce, report. - `src/rules/`: fail-closed loader and validator, taxonomy copied from taste-training `content/categories.ts`, question builder for the wire format. `src/rules/code/`: the rules that count, compare, or measure, as `Rule` objects with a `check` attached (`typography.ts`, `classes.ts`). `loadRules` returns them beside the YAML rules under the same tuning overlay. - `src/extract/`: units from Markdown and MDX (mdast), TSX (oxc-parser), Tailwind class lists, style-capture output, and one `source` unit per TSX, JSX, or CSS file. - `src/map/`: `plan.ts` (which rules apply to which unit, mechanical checks), `judge.ts` (the one stage lint and eval share: plan, answer questions from cache or live, report abstentions), one request per unit with every matching question, sha256 cache under `results/cache`, token bucket limiter, the fetch client with its two transports. - `src/reduce/`: bands, dedupe, scorecard, deterministic fixes, `mechanical.ts` (regex and phrase matching plus the helpers code rules share). - `src/report/`: tty, JSON, SARIF. `src/eval/`: corpus loader, precision and recall with Wilson intervals, calibration, threshold tuning, McNemar A/B. - `data/rules//.yaml` shipped rules. `data/rules/tuning.json` written only by `tune --write`. `data/corpus/*.jsonl` labelled units. `data/rule-drafts/` never loaded. `scripts/port-rules.ts` writes draft scaffolds, including static searches; a regex alone cannot ship a reviewed procedure. `--check` verifies every shipped rule against its source. ## Glossary One name per concept. Substituting a synonym splits the concept across the code. - **Unit**: one extracted piece of text or one class list (`src/types.ts` `Unit`). A corpus row is an **item** until `unitFromItem` turns it into a unit. - **Unresolved**: a value the extractor could not resolve (a Tailwind theme token, a missing class list). Lives on `ResolvedTypography.unresolved` and in `UnresolvedError`. - **Unknown**: the finding-level outcome when a rule cannot decide for a unit, usually because a value was unresolved or a precondition was unmet. Reported, never counted as pass or fail. - **Band**: how sure a finding is: `act`, `review`, or `silent`. The eval reports the share of labelled items in the review band as the review rate. - **Severity**: how bad a finding is if real: `major` or `minor`. Set by the rule, independent of the band. - **Mechanical / Jev / both**: the rule tiers. `both` means the mechanical part filters candidates and Jev decides. ## fix Every rule carries `fix.hint`, printed under the finding for a person or an agent. Nothing in this package calls a text model. `fix.function` names a function in `src/reduce/fixes.ts` that `--fix` applies to each of the unit's `fixRanges`, never to quotes, braces, expressions, or inline code around them. A unit without ranges is reported and left for a hand fix. Each fix function matches exactly what its rule's `mechanical.regex` flags. ## Non-goals for v1 - No `choice` or `score` rules. The schema reserves the field. - No Tailwind `@theme` parsing. Unknown tokens are `unresolved`, never guessed. - No composite taste score. The scorecard is counts by category and domain. ## Run evidence and coverage Preparation in `src/map/judge.ts` is shared by lint preview, execution, and eval. A prepared judgement retains eligibility, mechanical negatives, and skipped reasons. Execution owns answers and unknowns. Renderers own grouping and display limits. The canonical policy result is `LintResult.summary`, computed from all rule findings before presentation grouping. - **Skipped**: a rule does not apply to the unit. Excluded from evaluated negatives. - **Negative**: an applicable mechanical check or candidate filter evaluated without a violation. - **Request**: one logical provider evaluation. Several source units may share it when questions and state are identical. - **Attempt**: one HTTP try, including retries owned by the provider adapter. - **Cached answer**: one rule probability reused from persisted successful work. JSON v1 retains its grouped fields and adds complete `ruleFindings`, `ruleScorecard`, `summary`, `scope`, and `coverage`. The compatibility scorecard's old unit totals remain. Use coverage for eligible denominators. TypeSafe source references, provider distinctions, and contract checks are in [TypeSafe](https://blode.co/taste-lint/docs/typesafe). ## Repository evidence and skill packs A run owns one repository-facts service. Source units carry non-enumerable parsed facts and root-contained file access. Extraction JSON excludes those capabilities. Markdown structure and actual JavaScript/TypeScript imports drive structural code rules. Missing evidence produces unknowns. README, skill, plan, and explicit personal-writing profiles keep artifact-specific checks scoped. Personal comparison rules request named context fields supplied by the caller. Missing or oversized context prevents scheduling that comparison. Preparation determines request work once. Execution reuses it. Recorder failures remain local failures rather than provider errors. See [skill packs](https://blode.co/taste-lint/docs/skill-packs) for configuration and current limits. # Introduction (https://blode.co/taste-lint/docs) Catch AI slop before you ship. Scan product UI, writing, and agent instructions with Jev. # Taste Lint Catch AI slop before you ship. Scan your project with [Jev by TypeSafe AI](https://docs.typesafe.ai/introduction). Local checks handle measurable rules. Jev judges meaning and returns probabilities. ## Install ```bash npx taste-lint@latest init ``` Requires Node 24.11 or later. Run it from your project directory. Init installs locally and adds a scan script. ## Quickstart Create a [Vercel AI Gateway key](https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys). Then: ```bash export AI_GATEWAY_API_KEY="your-vercel-ai-gateway-key" npm run taste ``` No taste-lint account or config. AI checks send selected text and rule context to Vercel AI Gateway, billed to your account. Repeat runs reuse cached answers. ## What it checks - **Product interfaces:** copy, typography, interaction, and motion in JSX, TSX, and CSS. - **Writing:** Markdown, MDX, and READMEs with `--profile writing`. - **Agent instructions:** AGENTS.md and skills with `--profile instructions`. Rules come from [Agent Skills](https://github.com/mblode/agent-skills) and [Taste Training](https://blode.co/taste-training). ## Next - [Quickstart](https://blode.co/taste-lint/docs/quickstart) - [Scans](https://blode.co/taste-lint/docs/scans) - [Usage](https://blode.co/taste-lint/docs/usage) # Page audits (https://blode.co/taste-lint/docs/page-audits) Review and improve a page across disciplines using real evidence and Jev. Run `taste-lint scan guide`, or ask your coding agent to audit a page using `node_modules/taste-lint/data/audit/SKILL.md`. It gathers the evidence, writes the audit input and uses Taste Lint to review proposed improvements across UI, typography, copywriting, interaction, motion and SEO. The audit starts with the page's purpose, audience and intended character. Findings include what to change, why it matters, what to preserve and how to check the result. The terminal groups findings by page region and consolidates repeated corrections. JSON and agent exports retain every finding. ```sh taste-lint scan --audit results/audit/before/audit.json --dry-run taste-lint scan --audit results/audit/before/audit.json --save results/audit/before/report.json taste-lint scan export results/audit/before/report.json --out results/audit/handoff.json ``` Use the packaged `data/audit/example.json` and `data/audit/capture.js` as the input example and browser capture helper. Run from the source repository, or supply `--root`. The audit reads its named artifacts and source references; ordinary source scans remain available separately. For automation, `scan guide`, `scan export` and `scan verify` accept `--output json` before or after the subcommand. Exports name the page state and evidence artifacts, plus a source location when supplied. Locate the corresponding source before editing a finding without one. ## Evidence and judgments Every audit names one URL, state and viewport. Store its artifacts together in a private directory. Exact quotes must occur in the referenced text artifact. Source paths cannot escape the repository; artifacts cannot escape the audit directory, including through symlinks. Reports record artifact hashes and the observer's identity. Jev evaluates proposed problems and clean review summaries against the brief. It receives attributed text observations, not images. Screenshot interpretation belongs to the named reviewer. When a capture or full HTML document is supplied, existing computed typography and document SEO checks also run. A proposal whose evidence is missing or too large remains unknown. Page-audit rules remain advisory and are not calibrated quality scores. | Lens | Required observation kind | | --- | --- | | UI | Screenshot | | Typography | Computed styles or screenshot | | Copywriting | DOM, full document or screenshot | | Interaction | Recorded interaction | | Motion | Recorded motion | | SEO | Document or deployed-response observations saved as a document artifact | Each lens is explicitly reviewed, not assessed or not applicable with a reason. Missing lens reviews and unresolved evidence make a page audit incomplete (exit 2). Complete means the declared review ran; it does not establish exhaustive coverage or independent truth of the observer's claims. Public SEO checks are not automatically applicable to private application pages. ## Verify repairs Preserve before artifacts. Capture the same state and viewport in a separate after directory, keeping the brief unchanged. Reassess the same proposals or record clean reviews, then save the after scan: ```sh taste-lint scan --audit results/audit/after/audit.json --baseline results/audit/before/report.json --save results/audit/after/report.json taste-lint scan verify results/audit/before/report.json --after results/audit/after/report.json --evidence results/audit/after/verification.json --out results/audit/verified.json ``` The agent workflow documents the verification JSON. Every visible before finding needs a repeated procedure, result, preserved behavior and artifact references. Passed checks require appropriate fresh evidence; a static screenshot cannot verify motion or interaction. Stale hashes, changed page identity, incomplete scans, omitted checks and still-present findings prevent a successful verification claim. Disappeared findings remain unverified until these checks are recorded. Verification is attributed to the supplied observer. Taste Lint validates the record and artifact integrity; the coding agent performs the actual checks and edits. Reports and exports contain source and page evidence, so keep private artifacts out of Git. # Quickstart (https://blode.co/taste-lint/docs/quickstart) Install taste-lint and run your first scan with a Vercel AI Gateway key. ## 1. Install From your project directory: ```bash npx taste-lint@latest init ``` Pass `--agent` to also write agent instructions, or `--dry-run` to preview setup. ## 2. Set a gateway key Create a [Vercel AI Gateway API key](https://vercel.com/docs/ai-gateway/authentication-and-byok/api-keys), then: ```bash export AI_GATEWAY_API_KEY="your-vercel-ai-gateway-key" ``` ## 3. Scan ```bash npm run taste ``` Useful flags: | Option | What it does | | ---------------- | ---------------------------------------------------- | | `--dry-run` | Preview scope and estimated cost without model calls | | `--output json` | Save findings for scripts and agents | | `--output sarif` | Export findings for code review tools | `taste-lint scan --help` lists every option. See [Scans](https://blode.co/taste-lint/docs/scans) and [Usage](https://blode.co/taste-lint/docs/usage). # Scans (https://blode.co/taste-lint/docs/scans) Profiles, bands, baselines, and what fails a run. `scan` wraps the same evaluator as `lint` with a profile, review history, and a bounded report. `lint` keeps its existing reporting contract. Start with a preview: ```sh taste-lint scan . --profile product --dry-run taste-lint scan profiles ``` ## Profiles and scope | Profile | Objective | | --- | --- | | product (default) | Six focused checks for application recovery, copy and broad transitions | | writing | Content, consumer README, public Markdown, and active documentation | | instructions | AGENTS.md, CLAUDE.md, skills, and implementation plans | | architecture | TS/JS/package configuration and declared repository contracts | | all | Full catalog over every supported input | Scoped profiles skip build outputs, archived docs, and `.captain` reports. Writing also skips agent instructions and plans. Product skips test, spec, and story components. Pass explicit targets and `--exclude` to narrow a profile. `--only` explicitly selects rules within its domain, including checks omitted from the default product selection. `all` is the escape hatch for a full audit. Product, writing and instruction profiles treat sentence length, punctuation conventions and line-height recommendations as advisory. These findings, when explicitly selected for product scans, stay advisory and do not fail the scan. The `all` profile and `lint` retain strict rule policy. It treats `content/writing` as personal prose. Repository `docTypes` overrides win. Sentence length is still measured in code. JSON includes the selected files, profile, coverage, estimates, and diagnostics. No selected files exits 2. TTY shows at most five rule groups with representative locations and counts. Full JSON keeps every finding. ## Baselines and review decisions ```sh taste-lint scan . --profile product --save results/product.json taste-lint scan . --profile product --baseline results/product.json --new-only --save results/product-next.json taste-lint scan review results/product.json FINGERPRINT --decisions results/review.json --status dismissed --reason 'Intentional project convention' taste-lint scan review results/product.json FINGERPRINT --decisions results/review.json --status open taste-lint scan . --profile product --decisions results/review.json ``` A baseline must be a completed scan of the same root, targets, profile, rules, model, and evaluation mode. Review files are bound to that policy too. Save the next scan to another path. A dry run cannot become a baseline. Failed or unresolved checks cannot mark a finding resolved. Parse failures and missing graph coverage leave prior findings unverified. Fingerprints use rule ID, relative file, normalised unit content, and occurrence. Inserting unrelated lines keeps identity. Renaming a file or editing the affected text creates a new one. Repeated identical text is distinguished by occurrence. Findings keep independent severity, confidence, lifecycle, and reviewer decision fields. `--new-only` gates new findings only. Without it, existing undismissed act findings still fail. Dismissals need a reason. Reopening overrides an inherited dismissal. Review decisions do not change probability, severity, thresholds, or calibration labels. ## Changed-code reporting and reviewdog ```sh taste-lint scan . --profile product --since origin/main --output sarif > results/scan.sarif reviewdog -f=sarif -reporter=github-pr-check < results/scan.sarif ``` The diff compares the named commit with the working tree, including staged, unstaged, and untracked files. Analysis still uses the full selected-file context. Changed lines filter reporting and the exit policy. Unchanged model states reuse the answer cache. Graph findings without exact line positions filter by changed file. SARIF includes the content fingerprint as `tasteLint/v1`. Posting with reviewdog is a separate, explicit step. taste-lint does not create comments or PRs. ## Context and calibration The repetition rule receives the containing Markdown section and judges the target paragraph in that context. Missing or oversized sections abstain. Section questions use separate requests, so extra context does not enlarge unrelated questions. Cache replay merges the answers for the same unit. ```sh taste-lint scan . --profile writing --dry-run --samples results/blind.json # Codex labels the samples using the workflow below. mkdir -p results/labeled taste-lint scan labels results/blind.json --out results/labeled/review.jsonl taste-lint eval --corpus results/labeled --split holdout ``` Version 2 samples include source text and the full rule rubric, with examples and requested context, and with predictions hidden. Version 1 label files still import. Sampling is deterministic across rule, document type, and score band. It includes negative judgments and candidate-filter negatives. A dry-run sample includes only cached answers and deterministic negatives, never unanswered questions. Duplicate evidence is sampled once. Identical text with different context or rubrics stays separate. Splits group by repository and source file. This is a stratified diagnostic sample. Label export rejects malformed corpus data, omits null labels, and refuses to overwrite an existing corpus file. It never invents human labels. Use held-out evidence before promoting rules. Mocks and scan volume do not establish semantic accuracy. Personal facts and profile checks stay available through `lint --writing-context`. scan does not load private profiles on its own. ## Arbitrary utility values `craft-arbitrary-value-class` uses code to select literal values, then Jev to judge whether their visual role merits a named design token. Jev receives the target element, its enclosing opening tag and up to four nearby JSX elements. Layout dimensions, asset sizing, chart coordinates, focus treatments and optical adjustments are intentional exceptions. Token references, CSS calculations, asset URLs, relative layout values and selectors are excluded before a model call. Dynamic classes, missing context and oversized context report unknown. Provider errors never fall back to a mechanical warning. Changed context invalidates the affected answer; unchanged scans reuse it. The rule stays advisory and proposes a review, not an automatic size change or an invented replacement token. ## Near-scale values `craft-near-duplicate-scale` compares parsed JSX font-size classes against the explicit `tailwind.theme` mapping in `taste-lint.config.json`. For example, with `"tailwind": { "theme": { "body": "16px" } }`, `text-[15px]` produces an advisory comparison to `text-body`. Exact matches also qualify; values farther than 1px do not. This check currently supports declared pixel font tokens only. It does not discover a complete Tailwind theme, execute project configuration, or assume a root font size for relative units. Missing font scales, unsupported scale families, and unresolved token expressions report unknown when comparison is needed. Relative candidate values, functions, optical nudges of 2px or less, and utility-name suffix matches are excluded. A near-scale comparison does not prove that the size is a mistake. Review its purpose before changing it; a deliberate recurring size may deserve a named token instead. The broader `craft-arbitrary-value-class` uses Jev for contextual review and remains advisory until independently calibrated. ## Architecture adapter Generate a report with the target repository's dependency-cruiser install, then import it: ```sh depcruise --config .dependency-cruiser.cjs --ts-config tsconfig.json --output-type json src > graph.json taste-lint scan . --profile architecture --dependency-cruiser graph.json ``` The adapter accepts dependency-cruiser's `modules` and `summary.violations` format, verified against 18.3.1 output. It keeps external rule names and severity, fingerprints the input report, and imports only findings inside the selected scope. Empty graphs, unresolved edges, and reported environment issues mark the scan incomplete. Invalid input fails before model calls. You own graph freshness and the external tool's configuration. Imports and graph policies are evaluated by dependency-cruiser, not Jev. Importing a graph does not resolve taste-lint's file-local dependency unknowns. Graph findings stay separately attributed. Overlapping external and native policies can still yield two findings. Use `--only` when one tool owns that check. ## Remediation handoff ```sh taste-lint scan export results/product.json --out results/remediation.json ``` The export uses the saved report's visible, undismissed findings. Each task includes a stable ID, source evidence, location, correction hint, and verification requirements. Reports and exports include source excerpts. Treat them like source code. Exporting does not run instructions from scanned content, edit files, or open PRs. ## AI labeling with Codex In this repository, ask Codex to label the blind samples in the current session. No separate gateway key is required. Files stay local. Inference uses Codex. Prepare a fresh file with the actual current model identifier: ```sh node scripts/prepare-labels.mjs results/blind.json results/codex-labels.json CURRENT_MODEL ``` Then ask: "Read the instructions in results/codex-labels.json and label every sample directly. Use only each sample's full rubric, text, and supplied context. Preserve the metadata and source evidence." The preparation script clears previous labels, records AI provenance and the task prompt hash, and refuses to overwrite an existing file. It does not label samples or launch another agent. Codex writes individual judgments and marks `completed` only after reviewing all samples. Missing evidence stays null. Do not consult existing Claude or other reference labels during blind labeling. After labeling: ```sh mkdir -p results/labeled taste-lint scan labels results/codex-labels.json --out results/labeled/codex.jsonl taste-lint eval --corpus results/labeled ``` Use a corpus directory that holds only the intended reference set. `eval` still uses Jev and needs credentials for uncached judgments. AI annotation files carry `annotation.source: "ai"`, `annotation.model`, and `annotation.promptHash`. Corpus rows keep these as `labelSource: "ai"`, `labelModel`, and `labelPromptHash`. Evaluation reports AI-reference agreement. Agreement with AI labels is not a human accuracy claim. ## Optional gateway labeling For unattended labeling through a separately configured provider: ```sh node scripts/label-samples.mjs results/blind.json results/ai-labels.json anthropic/claude-sonnet-4.6 ``` This script needs `AI_GATEWAY_API_KEY`. It sends sample text, criteria, and supplied context to the chosen model. It keeps validated labels and provenance, not raw provider responses. Failed batches leave a `.partial` checkpoint. Null means abstention and is omitted from the corpus. ## Evidence before promotion Run `taste-lint eval coverage --corpus results/labeled` before spending on evaluation. It reports positives, negatives, holdout balance, document types, repositories, and source/text overlap with no credentials or network. `--output json` keeps per-rule detail. Coverage counts describe the dataset. They do not establish accuracy. `taste-lint tune --write` chooses thresholds from dev data only, then checks that choice against holdout data. Promotion needs both label classes, the configured minimum item count, complete scoring, no shared source files or duplicate text across splits, and a held-out Wilson precision lower bound above the configured floor. A failed gate keeps the rule review-only and reports why. Provider failures block tuning writes. Do not keep changing rules against the same holdout and still treat it as unseen. Move used examples into development data and collect a fresh holdout. Keep AI reference provenance visible. Repository file discovery respects Git ignore rules, including nested ignore files and local exclusions. Tracked files stay eligible even when an ignore pattern matches them. Ignored build trees are skipped before filesystem inspection, so broken symlinks there do not abort a source scan. Non-Git directories use the built-in and configured exclusions. ## Search and agent discovery `taste-lint scan . --profile discovery` checks existing static HTML, robots.txt, and llms.txt artifacts. It validates page titles, empty descriptions and canonicals, conflicting canonical declarations, JSON-LD syntax, sitemap directive URLs, and agent-index structure and size. Common email-template directories are excluded. All new discovery checks are advisory. These checks do not evaluate Next.js metadata source as deployed HTML, fetch URLs, validate schema.org eligibility, or infer missing generated routes. Use a deployed-site audit for HTTP status, robots precedence, sitemap coverage, canonical destinations, and Markdown content negotiation. An empty discovery scan is incomplete. Point scans at actual artifacts and exclude unrelated saved pages. The source-pattern rules `craft-affordance-mismatch` and `craft-virtualize-large-lists` live in `data/rule-drafts`. Hover styling does not prove an inert interaction, and a mapped list does not establish its size. Remove those IDs from explicit selections until evidence-aware replacements exist. Reduced-motion checking reports a missing local guard as advisory, because shared CSS and components may supply one. Unrelated reduced-motion classes are not proof that animation is guarded. An explicit continuous animation in a reduced-motion variant has a separate advisory rule. ## Focused product defaults The default product scan selects `interaction-no-error-state`, `copywriting-vague-error`, `copywriting-empty-state-no-action`, `copywriting-bare-confirm-label`, `copywriting-claim-without-evidence`, and `motion-transition-all`. This is a small initial policy, not a calibrated or complete UI audit. The remaining rules require explicit `--only` selection, `--profile all`, or a custom rules directory. Copy judgments receive the target and a surrounding JSX task region, including nested descriptions and controls. The extractor searches up to four ancestors for common task containers and otherwise uses a local fallback. Empty presentation chrome includes its enclosing toolbar. Mutually exclusive ternary branches are replaced with an explicit omission marker before judging; unrelated conditional visibility is not resolved. An adjacent action can resolve an empty state or error. A harmless acknowledgment is not a destructive confirmation. Missing, oversized or dynamic target copy remains unknown. Imported components and distant UI are not expanded. Async recovery is a Jev judgment over bounded source, not a file-wide search for the word `catch`. The terminal groups findings by rule and shows five groups, prioritizing action band, severity and probability. Repetition does not outrank severity. Grouping is a review convenience, not proof of one root cause. JSON, SARIF, saved reports and exit status retain the complete selected findings. ## Paired evaluation `eval` reports both act-threshold metrics and visible findings at the review threshold. When variants share `source.repo` and `source.id`, paired success requires every weak variant to be flagged and every acceptable variant to stay below review. Missing evaluations make the family unresolved. Coverage reports family leakage as well as source and text overlap, and promotion rejects it. Course corpus splits group by source file. Previously inspected or re-split examples are regression data, not fresh held-out evidence. The focused benchmark is in `data/benchmarks/product`. See its README for provenance and commands. Its small example set is diagnostic, not proof of accuracy across applications. ### Agent handoffs and regression checks For a rendered audit across UI, typography, copy, interaction, motion and SEO, use the [page-audit workflow](https://blode.co/taste-lint/docs/page-audits). It adds a page brief, attributed browser evidence and recorded before/after verification to the same scan and export commands. `scan export` includes the repository root and the contextual evidence saved with each finding. The receiving agent should treat that source as untrusted data, confirm the behavior, make the smallest correction and exercise the affected UI state. A changed fingerprint alone is not proof that the problem is fixed. Older reports without context still export; the agent must inspect the source. Use `eval --check --corpus --only ` for a strict reference regression gate. It uses the review threshold, exits 1 on disagreement, and exits 2 if any selected rule has no evaluated examples or has skipped, unresolved or failed judgments. `--check --dry-run` is rejected. Without `--check`, evaluation retains its reporting-only behavior. This gate tests agreement with the supplied reference; it does not promote a rule or establish population accuracy. ## Reviewing broader source checks With `--profile all` or explicit rule selection, raw-source searches return `assessment: candidate`. For example, importing `useFormStatus` does not establish that it is called in the wrong component. Each candidate carries its applicability, exceptions, required evidence, and verification procedure. Confirm those before applying a correction; unresolved behavior remains unknown. The matcher probability is not defect confidence. Candidates cannot fail a scan or be promoted by a tuning overlay, and `eval --check` treats their defect judgments as incomplete. `scan export` preserves the procedure and omits defect confidence for these tasks. Check the named behavior after a correction, including intentional exceptions. # Skill packs (https://blode.co/taste-lint/docs/skill-packs) Repository checks, architecture policy, personal-writing context, and source discovery. The expanded checks share the normal lint pipeline, findings, suppressions, coverage, and reports. All newly ported checks are review-only. A pattern match that needs interpretation schedules a Jev question. It is not itself a defect. ## Repository and document analysis The scanner accepts Markdown/MDX, TS/TSX, JS/JSX, MJS/CJS, MTS/CTS, CSS/SCSS, JSON, and YAML. Plain TS/JS/config inputs contribute structural source units, not arbitrary prose strings. Parsed imports distinguish actual imports from code in comments or string examples. README.md, SKILL.md, AGENTS.md/CLAUDE.md, and docs/plans Markdown get document profiles automatically. Profile overrides in `taste-lint.config.json` take precedence. `personal`, `pr`, and `slides` require explicit configuration. Declaring a profile does not imply every check for that artifact is implemented. ```json { "docTypes": [{ "glob": "drafts/**/*.md", "type": "personal" }] } ``` Personal-writing rules include rhetorical patterns and comparisons against explicitly supplied facts, voice guidance, and drafting instructions. Pass `--writing-context context.json` with any of `facts`, `profile`, and `instructions` as nonempty strings. Missing or oversized comparison context produces unknowns. No private Ghostwriter profile is loaded automatically. These checks do not establish that text was generated by AI. ```json { "facts": "The deadline is Friday.", "profile": "Warm and direct.", "instructions": "Ask for feedback." } ``` Context applies only to `personal` documents. Live lint sends requested context to the configured provider. `--dry-run` makes no requests. Explicitly printing request state can reveal supplied context. Repository facts use the nearest package manifest and resolve local file references inside the scan root. They do not execute documented commands, generated scripts, or skill instructions. Unbuilt output and unresolvable aliases produce unknowns when they prevent a decision. JSONC configurations currently need a parser adapter and are unknown to JSON configuration rules. ## Declared architecture Only explicit policy enables the module-boundary, deprecation, and generated-header checks: ```json { "architecture": { "boundaries": [ { "from": "src/data/**", "disallow": "src/http/**", "reason": "Data code must not import transport handlers" } ], "deprecatedImports": { "old-sdk": "new-sdk" }, "generated": ["src/generated/**"] } } ``` Boundary patterns match literal import specifiers and normalised repository-relative targets for relative imports. They do not resolve arbitrary TypeScript aliases. Match an alias explicitly, such as `@http/**`, or use a graph tool for resolved dependency policies. These checks do not prove acyclicity, architectural quality, or distributed correctness. ## Discover source skills Inventory every source collection without generating or activating rules: ```sh taste-lint rules discover ../agent-skills ../ghostwriter /Users/mblode/.agents/skills/copywriting ``` The command includes entrypoints, `rules/`, `rules-arch/`, `rules-ax/`, references, and guidelines. It omits evaluations, scripts, launcher metadata, dependencies, and symlinks. The output includes section locations and existing source-path citations. A citation is provenance, not proof that every standard in that source is implemented. An uncited source remains `needs-triage`. The existing `port-rules --write` remains a limited three-collection generator. Discovery stays separate so newly found standards do not become active TODO questions. ## Verification Offline regression tests cover positive and negative examples, routing, scope, unknowns, path containment, import parsing, configuration, and report integration. Mocked Jev results verify the contract, not semantic accuracy. Promotion still needs labelled evaluation. Browser flows, calibrated voice fidelity, distributed operations, complete import graphs, and external site state need extra evidence adapters. An absence-of-keyword check is not a substitute for those. # TypeSafe / Jev (https://blode.co/taste-lint/docs/typesafe) How Jev by TypeSafe AI powers judgment calls. Verified against TypeSafe's official documentation on 20 September 2026. These are concrete contracts, not a claim of certification or universal best practice. - [Introduction](https://docs.typesafe.ai/introduction): independent, atomic questions share a state and are composed by code. Taste-lint keeps one rule per judgment and batches independent questions. Request sharing never merges source provenance. - [Noul](https://docs.typesafe.ai/primitives/noul): the number is the probability of yes. It is not severity, a quality score, or the separate confidence field returned by other primitives. A high value must mean the named condition is present; an advisory styling judgment is not proof of a defect. The report preserves individual probabilities before presentation grouping. - [How to build](https://docs.typesafe.ai/concepts/how-to-build-with-system-one): deterministic work stays in code. State supplies relevant context. Applicability, counting, thresholds, and fixes remain deterministic. Existing labelled text state is retained to preserve cache semantics. Changing it to structured state needs an evaluation and a deliberate cache invalidation, not a cosmetic migration. - [Confidence](https://docs.typesafe.ai/confidence): action thresholds depend on the domain and stakes. Jev rules stay review-only until the existing corpus/tuning gate supports promotion. The diagnostic agent-labelled sample is not enough evidence to change thresholds. - [API reference](https://docs.typesafe.ai/api): direct requests use POST /v1/systemone and bearer authentication. The adapter validates probabilities and usage, retries 429 and 529 with bounded backoff, and does not retry authentication errors. Tests cover overload, malformed responses, and attempts. Provider bodies and credentials are never persisted. The Vercel gateway is a separate transport contract. TypeSafe's direct API docs do not certify the gateway's evaluation route. The existing adapter and its wire-format tests remain the owner. No new SDK or provider dependency was added. ## Run contract Preparation decides applicability once. Execution consumes the prepared work, consults the answer cache, and reports outcomes. Both lint and eval use this seam. Skipped checks never become negative labels. Unknown means an applicable check could not be resolved. It is not a pass. `summary.failing` is computed from complete rule findings, severity, and fail-on policy. Text and exit codes consume that result. JSON v1 retains grouped `findings` and the legacy `scorecard`. Additive `ruleFindings`, `ruleScorecard`, and `coverage` expose the complete evidence and eligible denominators. SARIF contains all unsuppressed rule findings. `usage.requests` counts successful logical requests. `usage.attempts` counts HTTP attempts reported by the adapter (one assumed per call for a custom evaluator without telemetry). `usage.cached` counts reused answers. `usage.sharedAnswers` counts answers fanned out within the same run. Cost covers known successful usage. It is not a guarantee about provider billing for failed attempts. Cache writes are atomic. A write failure does not erase an answer already received. Default text output shows at most 20 act examples and 10 review examples, with omitted counts and a full report path. `--verbose` expands the view. `--progress` enables throttled plain progress on stderr when piped. TTY runs show it automatically. Structured stdout stays parseable, including expected argument and config errors. Incomplete reports include a shell-quoted rerun command with absolute root/results paths, without repeating fixes or bypassing the cache. ## Contextual style review Code selects literal arbitrary utilities. Jev judges whether they express reusable visual styling or an intentional constraint, using bounded local JSX context. The judgment does not assume an existing theme token or calculate size differences. Questions using this context are isolated from unrelated copy and typography questions. The existing cache, samples and corpus replay preserve the same state. `data/corpus/semantic-style.jsonl` contains eight synthetic AI-labeled development examples. Use `taste-lint eval --only craft-arbitrary-value-class` to inspect them. They check the contract; they are not a real-project holdout or sufficient evidence for promotion. Model-free tests separately verify candidate selection, context limits, cache invalidation and provider failures. ## Checks The same `npm run verify:full` runs locally and in CI. Regression coverage lives in `src/__tests__/run-contract.test.ts`, `src/__tests__/cli.test.ts`, `src/__tests__/execution.test.ts`, and the existing provider/eval suites. Tests never call a live model. # Usage (https://blode.co/taste-lint/docs/usage) CLI options, findings, rule packs, and rendered mode. ## Example findings ```text PASS: 161 active rules in data/rules notes.md [MINOR] typography-straight-quotes (p=1.00) notes.md:3:1 Straight quotes in rendered copy 2 matches: "'", "'" Fix: Replace with the matching curly mark. Opening after whitespace or at the start, closing otherwise; an apostrophe is always the right single quote. [MINOR?] copywriting-claim-without-evidence (p=0.95) notes.md:3:1 Quality claimed, nothing the reader could check 3 matches: "fast", "powerful", "seamless"; p=0.95 Fix: Replace the adjective with the mechanism, the number or the standard it stands for. If none exists, cut the sentence. Units: 3 | Rules: 161 | Act: 1 | Review: 4 | Unknown: 0 Jev: 2 requests, 0 cached answers, 7238 input tokens, $0.0003 FAIL - 1 finding in the act band ``` ## Two answers per finding - **Severity:** how bad the finding is if real, `major` or `minor`. Set by the rule, never by the model. - **Band:** how sure the tool is. `act` fails the run (mechanical hits, or Jev at or above the rule's act threshold). `review` prints a note with a `?`. Below that is silent. - **Cost:** eligible questions about one unit are batched into requests. Preview estimated cost with `--dry-run`. Runs report usage and reuse cached answers. Current rates are in the [Vercel AI Gateway model catalog](https://vercel.com/ai-gateway/models). ## Rule packs - **Typography:** straight quotes, dashes, ellipses, primes, and units from `typography-audit`, plus size, weight, tracking, and line-height checks over resolved Tailwind classes or computed styles. - **Copywriting:** claims without evidence, vague errors, friction CTAs, hedges, and register shifts from `docs-writing` and the ui-design copy guideline, plus Every's published AI-tell checker: 19 of its 21 questions, one rule each, with phrase candidates from the MIT `cw-ai-check` skill where it has them. Not ported: `uniform_cadence` (sentence-length arithmetic) and `formatting_overuse` (needs headings and bullets a paragraph never sees). The two authorship verdicts are excluded on purpose. taste-lint reports defects, not authorship. - **Interaction and craft:** the static checks of `ui-design/rules` as whole-file patterns (focus traps, error and empty states, target size, i18n, lazy loading) plus the shadcn/lint class hygiene rules (raw palette colours, arbitrary values, interpolated class strings). - **Motion and product:** the `ui-animation` flag-on-sight table (ease-in, linear easing, transitions over 300ms, `transition-all`, entrances from scale zero, no reduced-motion variant) and the two deterministic `product-design` rules. Every rule names the file and line of the skill or lesson it came from. `taste-lint rules list` prints tier, status, and category per rule. ## Rendered mode ```bash taste-lint lint --url `example.com/pricing` --selector main ``` Runs [style-capture](https://www.npmjs.com/package/style-capture) in headless Chromium and lints computed styles: real pixel sizes, line heights, weights, and letter-spacing. The typography rules judge what the reader sees. `--capture file.json` lints a saved capture. ## API ```typescript import { runLint } from "taste-lint"; const result = await runLint({ root: process.cwd(), targets: ["content"] }); console.log(result.scorecard.byDomain, result.usage.costUsd); ``` `runLint` takes the same options as the `lint` command and returns findings, unknowns, the scorecard, and usage. `taste-lint schema` prints every command, flag, and default as JSON. `--output json` turns an error into a `\{ error, code, message \}` envelope on stdout. ## Agent skill ```bash npx skills add mblode/taste-lint ``` Installs the `taste-lint` skill for Claude Code, Codex, Cursor, and OpenCode: how to read a finding, the dry-run-first workflow, and the gotchas. ## Options | Flag | Default | Description | | --- | --- | --- | | `--dry-run` | | Plan and print units, requests, and estimated cost without calling Jev | | `--only ` | | Comma-separated rule ids | | `--exclude ` | | Comma-separated globs to skip, added to `taste-lint.config.json` | | `--fail-on ` | `minor` | Lowest severity that fails the run | | `--fix` | | Apply deterministic fixes (curly quotes, ellipsis, multiplication sign, unit spaces) to act-band findings | | `--output ` | `tty` | `tty`, `json`, or `sarif` | | `--url ` | | Lint a rendered page through style-capture | `taste-lint eval` scores every rule against its labelled corpus (precision, recall, Wilson intervals, a calibration table). `taste-lint tune` picks act thresholds from the dev split, and promotes a rule only when that threshold also clears the precision lower bound on an independent holdout. Both label classes, enough evaluated items, complete scoring, and source/text separation are required. `taste-lint eval coverage` reports class balance and split leakage without API calls. Add `--output json` to coverage or evaluation for structured results. ## Reading a repository run The default text report summarises scope, top rules, and up to 30 examples. Use `--verbose` for the full list. Every completed or incomplete run also saves a complete JSON report and prints its path. JSON v1 keeps the original grouped `findings`. `ruleFindings` preserves every rule's evidence. `coverage` counts eligible checks. `summary.failing` respects `--fail-on`, and with run completeness determines the exit code. ```bash taste-lint lint --root ../my-site apps/web --dry-run taste-lint lint --root ../my-site apps/web --progress --output json > audit.json ``` Progress goes to stderr. A fully cached run needs no API key. An incomplete report includes a retry command that reuses successful answers. Cost is reported from known usage, excluding any unreported provider billing for failures. Configuration is optional at `taste-lint.config.json` in the scan root. Its editor schema ships at `node_modules/taste-lint/data/config.schema.json`. Unknown fields and invalid types fail before evaluation. Select the scope explicitly. Documentation and agent instructions stay included when you request a whole repository. ```json { "$schema": "./node_modules/taste-lint/data/config.schema.json", "exclude": ["docs/archive/**"], "docTypes": [ { "glob": "apps/web/content/writing/**/*.mdx", "type": "explanation" } ] } ``` See [TypeSafe](https://blode.co/taste-lint/docs/typesafe) for the Jev contracts. See [skill packs](https://blode.co/taste-lint/docs/skill-packs) for repository checks, architecture policy, personal-writing context, and source discovery. Use `taste-lint scan . --profile product --dry-run` to preview a focused scan. [Scans](https://blode.co/taste-lint/docs/scans) covers profiles, baselines, review decisions, changed-code SARIF, calibration samples, graph-tool reports, and remediation exports. Run `taste-lint schema` to discover commands as JSON. It includes positional arguments, required options, value types, allowed choices and defaults. An option's `required` field describes whether it must be supplied; `value` describes whether the flag takes a value. Negated flags such as `--no-install` include their positive option name and default. ## Project setup Run `npx taste-lint@latest init` in a directory with package.json. Setup detects the package manager from packageManager or a lockfile, installs Taste Lint as a development dependency, and adds `taste` to run the scan. React, Next.js, Vue, Svelte, and Astro dependencies select the product profile. Other projects start with writing. You can edit the scripts to choose another profile. Existing scripts and dependencies are preserved. Repeating setup adds only missing scripts. Options: - `--root ` selects a project directory, including an individual workspace package. - `--pm npm|pnpm|yarn|bun` overrides detection. Conflicting lockfiles require an explicit choice. - `--dry-run` previews setup without writes or installation. - `--no-install` adds scripts without running the package manager. - `--agent` appends a marked section to AGENTS.md once, preserving existing guidance. Setup does not request or store an API key. Set `AI_GATEWAY_API_KEY` in your environment before running the AI script. For a monorepo, run setup in the package you want to scan. Setup does not traverse workspace packages.