diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b2dda782..c2792afd9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -**A free, deterministic design lint ships with gstack: `bin/gstack-design-lint` checks a page's HTML, CSS and components for the tells that make it read as machine-made, in under a second, and with `--render` measures what only a browser can.** Until now gstack's only mechanical design check was the opt-in impeccable engine most users never install, plus `gstack-design-craft`'s seventeen copy and ship-floor regexes; everything else in the Slop Gate was prose the model judged itself. The lint ports the roughly fifty-five machine-checkable gates of [Nutlope/hallmark](https://github.com/Nutlope/hallmark)'s `sloplint` (MIT; see NOTICE.md) into TypeScript under `lib/design-lint/`, rewritten in gstack's voice and mapped onto gstack's severity words. This release is the tool alone (Phase 1 of plans/absorb-hallmark.md, approved by Garry on 2026-10-26); /design-html and /design-review start calling it in Phase 2, so no skill prose changes here. +**A free, deterministic design lint ships with gstack: `bin/gstack-design-lint` checks a page's HTML, CSS and components for the tells that make it read as machine-made, in under a second, and with `--render` measures what only a browser can.** Until now gstack's only mechanical design check was the opt-in impeccable engine most users never install, plus `gstack-design-craft`'s seventeen copy and ship-floor regexes; everything else in the Slop Gate was prose the model judged itself. The lint ports the roughly fifty-five machine-checkable gates of [Nutlope/hallmark](https://github.com/Nutlope/hallmark)'s `sloplint` (MIT; see NOTICE.md) into TypeScript under `lib/design-lint/`, rewritten in gstack's voice and mapped onto gstack's severity words. This release is the tool alone (Phase 1 of the hallmark absorption plan, approved by Garry on 2026-10-26); /design-html and /design-review start calling it in Phase 2, so no skill prose changes here. ### Added - **`bin/gstack-design-lint [--render] [--url] [--scope page|component] [--job ] [--json] [--rules]`**: reads `.html`, `.css`, `.jsx`, `.tsx`, `.vue`, `.svelte` and `.astro` (Tailwind class strings included, `cn()`/`clsx()`/`cva()` calls too), links each page to the stylesheets it reaches through `` and `@import`, resolves CSS variables, converts colours to OKLCH and computes contrast, and prints one line per finding: `LINT: : | fix: `, then `LINT_SUMMARY: blocks=N ask=N waived=N polish=N render=`. Exit 2 on blocks, 1 on an unanswered ask, 0 otherwise. `docs/design-lint.md` is the contract and lists every rule. diff --git a/docs/design-lint.md b/docs/design-lint.md index 2925fca43..754f55da8 100644 --- a/docs/design-lint.md +++ b/docs/design-lint.md @@ -26,7 +26,8 @@ gstack-design-lint [--render] [--url ] [--scope pa - Reads `.html`, `.css`, `.jsx`, `.tsx`, `.vue`, `.svelte` and `.astro`. Tailwind class strings in components are read too, including `cn()`, `clsx()`, `cva()` and their kin. `node_modules`, dot folders and build - output below the top level are skipped. + output below the top level are skipped; a symlink is followed once; a file + over 2 MB is a bundle, not a page, and is skipped with a warning on stderr. - A page is linted with the stylesheets it links (``, `@import`, and anything they reach); a directory of components shares the project's tokens and global state styles, so a focus ring declared once in @@ -92,7 +93,9 @@ about this page: `impeccable-disable : ` is accepted as an alias, so a comment that already silences the impeccable engine answers the same tell here. The lint refuses, each as its own `waiver` finding at ask: a rule it does not -know; a **blocking** rule (the Floor is never waivable; the one exception is a +know; the lint's own rules (`render`, `waiver`, `house-style`, +`internal-error`: a fallback face or a failed measurement is never signed off); +a **blocking** rule (the Floor is never waivable; the one exception is a blocking rule the lint could only *suspect* on this page, which it reports at ask and which a waiver answers like any ask); a reason under 24 characters, boilerplate ("by design", "intentional", "looks better"), reused word for @@ -101,6 +104,14 @@ page's own text; a second waiver of one rule. More than three waivers on one page is a house style and belongs in DESIGN.md: reported once at polish as `house-style`. +A claim written in a stylesheet answers for that stylesheet on every page +that links it, and only for findings located in it: the shared-word check does +not apply (a stylesheet has no prose), so the claim cannot reach a finding in +the page's own markup. Two claims on one line are both read. A blocking copy +tell (`generic-hero-copy`) is the Floor inside the hero, the section or header +that holds the h1; the same line in body copy further down is suspected and +reported at ask, so a waiver can answer it. + ## Rules | Rule | Tier | Source gate | The tell | The fix | @@ -267,7 +278,20 @@ reports at the document's first line with a count, because patterns into the lint with lines. Where a rule id is shared with `lib/design-catalog.ts`, the tier here follows hallmark's gate (several catalog `ask` rows are `blocks` here); Phase 2 reconciles the two before a -catalog row is marked `detect: lint`. +catalog row is marked `detect: lint`. Also outside the lint: `system-ui` or +`-apple-system` as the body's first face (`system-font-primary`; the +`overused-font` rule reads display tokens and heading selectors, as hallmark's +gate does), a gradient fill on the primary button (`gradient-cta`; +`purple-blue-gradient` reports the banned hue pairs only, and an +indigo-to-violet ramp needs stops 12° or more apart), `text-align: center` +density, one large radius on everything, and backdrop blur on more than one +container (`glow-wash` reports a blur over a translucent near-white or +near-black fill; the zero-offset coloured glow is read from 8px in a Tailwind +class and past 20px in a stylesheet, hallmark's two thresholds). The render +tier's probes run in the page's own JavaScript, so a page that redefines them +is caught only when it hands back a rule or grade the lint does not know; a +page that answers every probe with nothing passes the render tier, which is why +the static tier stands on its own and a URL target is measured, never trusted. ## What is deliberately not ported diff --git a/test/design-lint-recorded.test.ts b/test/design-lint-recorded.test.ts index a44df7816..2339e030c 100644 --- a/test/design-lint-recorded.test.ts +++ b/test/design-lint-recorded.test.ts @@ -4,7 +4,7 @@ * the page annotates that the lint has a rule for is reported, the clean * fixture is not, and both agree with hallmark's own sloplint on the same * files (checked by hand at hallmark 7ec8682; the gate set is identical). - * Phase 1 of plans/absorb-hallmark.md, item D7: the free regression that a + * Phase 1 of the hallmark absorption plan, item D7: the free regression that a * recorded output keeps linting the same way. */ import { describe, expect, test } from 'bun:test';