mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-11 11:10:13 +01:00
Merge remote-tracking branch 'origin/main' into harness/factory-scorecard
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> # Conflicts: # CHANGELOG.md
This commit is contained in:
commit
dba5ad39ee
159 files changed
+14898
-363
No files matched your search
@@ -70,7 +70,7 @@ gstack/
|
||||
├── freeze/ # /freeze skill; bin/check-freeze.sh (PreToolUse edit-boundary hook; sources careful/bin/hook-extract.sh, fails closed)
|
||||
├── guard/, unfreeze/ # /guard (careful + freeze in one), /unfreeze
|
||||
├── gstack-upgrade/ # /gstack-upgrade skill + migrations/ (run after ./setup during an upgrade)
|
||||
├── bin/ # CLI utilities (gstack-render.ts = render a local HTML file through Aside or the engine, gstack-design-detect.ts = probe/scan through a user-installed impeccable engine; gstack-design-md.ts = open DESIGN.md check/convert/tokens/mark; gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, gstack-memorable, etc.)
|
||||
├── bin/ # CLI utilities (gstack-render.ts = render a local HTML file through Aside or the engine, gstack-design-detect.ts = probe/scan through a user-installed impeccable engine; gstack-design-md.ts = open DESIGN.md check/convert/tokens/mark; gstack-design-lint = the free deterministic design lint over HTML/CSS/components, --render measures in the browser (docs/design-lint.md); gstack-repo-mode, gstack-slug, gstack-config, gstack-wtree, gstack-evidence, gstack-issue-guard, gstack-relink, gstack-memorable, etc.)
|
||||
├── document-release/ # /document-release skill (every-ship pre-verification audit; standalone doc updates + Diataxis coverage map)
|
||||
├── document-generate/ # /document-generate skill (Diataxis doc generator: tutorial/how-to/reference/explanation)
|
||||
├── cso/ # /cso skill (OWASP Top 10 + STRIDE security audit)
|
||||
@@ -88,7 +88,7 @@ gstack/
|
||||
│ └── dist/ # Compiled binary
|
||||
├── agents-digest/ # Committed 2KB instruction-tier rules digest (gstack-AGENTS.md) for rules-reading hosts
|
||||
├── extension/ # Chrome extension (side panel + activity feed + CSS inspector)
|
||||
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
|
||||
├── lib/ # Shared libraries (aside-render.ts = local-HTML rendering, Aside first, engine fallback; design-catalog.ts = the typed design anti-pattern catalog every design skill renders from; design-lint/ = the static and render tiers behind bin/gstack-design-lint, ported from hallmark's sloplint; design-detect-contract.ts = detector sentinel vocabulary; design-md.ts = open DESIGN.md reader/writer; dom-dump-script.ts + generated dom-dump.js = rendered-DOM dump for the detector; review-evidence.ts = review-start receipt binding, computed freshness and shared-code snapshot eligibility; frontend-scope.ts; claude-bin.ts, error-handling.ts, worktree.ts, egress-receipt.ts, context-bill.ts, redact-engine.ts, tracker-guard.ts, version-source.ts, code-intelligence/)
|
||||
│ └── diagram-render/ # Vendored mermaid + excalidraw runtimes, built into one offline bundle the renderer loads
|
||||
├── patches/ # bun `patchedDependencies` patches (playwright-core windowsHide)
|
||||
├── docs/designs/ # Design documents (incl. IMPECCABLE_INTEROP.md = the design detector / catalog / open DESIGN.md record, and fork-port-residual-2026-09/ evaluation evidence)
|
||||
@@ -102,7 +102,7 @@ gstack/
|
||||
├── SKILL.md # Generated from SKILL.md.tmpl (don't edit directly)
|
||||
├── SKILL.md.tmpl # Template: edit this, run gen:skill-docs
|
||||
├── ETHOS.md # Builder philosophy (Boil the Ocean, Search Before Building)
|
||||
├── NOTICE.md # Third-party notices: material derived from impeccable and the DESIGN.md spec (both Apache-2.0)
|
||||
├── licenses/ # Verbatim license texts for the notices above (Apache-2.0.txt)
|
||||
├── NOTICE.md # Third-party notices: material derived from impeccable and the DESIGN.md spec (Apache-2.0), Elaya's design skills and hallmark's sloplint (MIT)
|
||||
├── licenses/ # Verbatim license texts for the notices above (Apache-2.0.txt, MIT-elaya.txt, MIT-hallmark.txt)
|
||||
└── package.json # Build scripts for browse
|
||||
```
|
||||
@@ -0,0 +1,391 @@
|
||||
# design-lint: the free, deterministic design check
|
||||
|
||||
`bin/gstack-design-lint` reads a page (or a directory of pages, stylesheets
|
||||
and components) and prints every machine-checkable design tell it finds, with
|
||||
a file and line, in under a second. With `--render` it also measures the page
|
||||
in gstack's own browser: the fold, the word budget per screen, contrast
|
||||
against the painted pixels, focus rings by tabbing, crowded headings, text
|
||||
painted under other elements, the share of the page that is photographs.
|
||||
Nothing is installed and nothing is paid for: the static tier is TypeScript
|
||||
over the files, and the render tier drives the same engine `gstack-render`
|
||||
picks (Aside when it is running, otherwise the browse daemon `./setup` built).
|
||||
|
||||
The rules are ported from [Nutlope/hallmark](https://github.com/Nutlope/hallmark)'s
|
||||
`sloplint` (MIT; see NOTICE.md and licenses/MIT-hallmark.txt), rewritten in
|
||||
gstack's voice and mapped onto gstack's severity words. The copy tier is
|
||||
`gstack-design-craft`'s content-realism and ship-floor check under the same
|
||||
contract. Phase 1 (this document) is the tool alone; /design-html and
|
||||
/design-review call it in Phase 2.
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
gstack-design-lint <file-or-dir ...> [--render] [--url <http://…>] [--scope page|component] [--job <job>] [--json] [--rules]
|
||||
```
|
||||
|
||||
- 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; a symlink is followed once and lint
|
||||
roots and the working directory are read through their real paths; a file
|
||||
over 2 MB is a bundle, not a page, and is skipped with a warning on stderr.
|
||||
A page or stylesheet the lint skipped (over the cap, unreadable,
|
||||
unparseable; a linked sheet or an inline `<style>` block included) is also
|
||||
one `internal-error` finding at ask, so a page whose CSS the lint could not
|
||||
read is not a pass; only a linked stylesheet that does not exist is warned
|
||||
about and otherwise ignored (its tokens are simply absent). A
|
||||
relative stylesheet href may not climb out of the project (the nearest
|
||||
directory holding a `package.json` or `.git`), by its path and by its real
|
||||
path, so a symlink inside the project that points outside it is not read
|
||||
either; outside any project it is bounded by the working directory the
|
||||
lint runs in, and only `.css`
|
||||
files are read.
|
||||
- Markup inside an HTML comment is dead for every rule, a commented-out
|
||||
`<style>` or `<script>` included; a comment that never closes runs to the
|
||||
end of the file, as it does in the browser. A tag start with no `>` (a stray
|
||||
`<img `) and any `<` that starts no tag (`<1`, `a < b`) are read as text
|
||||
and dropped from the markup, so a page whose tail is ten thousand of them
|
||||
costs its length; a `<!doctype>` or `<?xml?>` declaration stays. A tree nests at most 512 elements deep
|
||||
(the browser's own cap); a start tag deeper than that is read as empty. An
|
||||
open heading is closed by the next heading start tag, an open `<p>` by the
|
||||
next block start, and a repeated `<li>`, `<button>`, `<a>`, `<label>`,
|
||||
`<select>`, `<form>` or table cell by its next start tag, as the browser
|
||||
does, so a page of unclosed elements costs its length to lint. A nested
|
||||
CSS selector is expanded against its parent (`&` replaced, a bare selector
|
||||
descendant-joined) up to 8192 characters, and a sheet may store at most
|
||||
four times its length plus 64 KB of expanded selectors; past either bound
|
||||
the nested selector stands as written, so a sheet costs its length too.
|
||||
Nesting is read 64 levels deep; a rule nested below that is dropped, not
|
||||
parsed.
|
||||
- A page is linted with the stylesheets it links (`<link rel=stylesheet>`,
|
||||
`@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
|
||||
`globals.css` is not reported missing on every component.
|
||||
- `--render` measures every HTML page found; `--url` measures a running dev
|
||||
server (and implies `--render`). When no browser is available the render
|
||||
tier is skipped and the summary says why.
|
||||
- `--scope component` skips the page-only rules (the nav, the footer, the
|
||||
eyebrow, section gaps, the wireframe count) when linting a component alone.
|
||||
- `--job <job>` names what the visitor came to do (`book`, `buy`,
|
||||
`subscribe`, `attend`, `visit`, `hire`, `listen`, `join`, `try`,
|
||||
`sign up`): an email sign-up as the page's close is only a finding when the
|
||||
job is not one where a mailing list is native.
|
||||
- `--json` prints `{ summary, files, findings }`; `--rules` prints the table below.
|
||||
|
||||
## Output
|
||||
|
||||
One line per finding, then one summary line:
|
||||
|
||||
```
|
||||
LINT: <blocks|ask|polish|waived> <rule-id> <file>:<line> <evidence> | fix: <fix>
|
||||
LINT_SUMMARY: blocks=N ask=N waived=N polish=N render=<ran|skipped: reason>
|
||||
```
|
||||
|
||||
Page-supplied text in a finding never holds the line's own ` | fix: `
|
||||
separator (it is printed as ` / fix: `), so splitting a line on its first
|
||||
` | fix: ` always reads the fix; a program reading many fields should take
|
||||
`--json`.
|
||||
|
||||
Exit **2** when anything blocks, **1** when an ask is unanswered, **0**
|
||||
otherwise (**3** on a usage error). Findings sort blocks, ask, waived, polish,
|
||||
then by rule, file and line.
|
||||
|
||||
## Tiers
|
||||
|
||||
The lint uses gstack's severity words, not a fourth scale:
|
||||
|
||||
| Tier | Meaning | hallmark's word |
|
||||
|---|---|---|
|
||||
| **blocks** | Never waivable. Correctness, access, honesty and the bans: a card in a card, a purple-blue gradient, a missing focus ring, contrast under threshold, a reveal that hides content until JavaScript runs. | Floor |
|
||||
| **ask** | A default a model falls into. Fix it, or waive it on the record with a reason about this page. | Reflex |
|
||||
| **polish** | Craft detail invisible at arm's length. Printed, never blocks. | Finish |
|
||||
|
||||
`waived` marks an ask or polish finding a waiver answered. **Blocking and
|
||||
waived are never summed**: `blocks=0` is always a true sentence about the
|
||||
Floor. Three house rules hallmark holds at Floor stay at ask here on purpose:
|
||||
the eyebrow (`kicker-above-heading`; a figure caption or a plan name above a
|
||||
heading is often right), the 80-word screen (`screen-word-budget`; a Read-mode
|
||||
page legitimately exceeds it) and the dash (`em-dash-overuse`; gstack's rule is
|
||||
overuse, not a ban). A blocking rule the lint can only *suspect* (an invented
|
||||
metric it cannot prove invented) reports at ask so a person confirms it.
|
||||
|
||||
## Waivers
|
||||
|
||||
A finding is answered in a comment that names the rule and gives a reason
|
||||
about this page:
|
||||
|
||||
```html
|
||||
<!-- gstack-lint-disable kicker-above-heading: the chapter number is the reader's bookmark through a forty-page spec -->
|
||||
```
|
||||
```css
|
||||
/* gstack-lint-disable section-gap: the ledger separates its rows with rules, not air */
|
||||
```
|
||||
```tsx
|
||||
// gstack-lint-disable template-nav: the three links are the product's three modes
|
||||
```
|
||||
|
||||
`impeccable-disable <rule>: <reason>` 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; 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
|
||||
word on another waiver, or sharing no word of four letters or more with the
|
||||
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. A claim written in the page answers for that page,
|
||||
including a finding in a stylesheet it links; a second page linking the same
|
||||
sheet without answering still reports it, so the sheet's finding may print
|
||||
once waived and once at ask, each from its own page. Which of these a context
|
||||
gets follows its shape, not the `--scope` flag: one html page keeps page
|
||||
semantics under either scope. In a directory of components, or a lone
|
||||
component file beside a stylesheet (under either scope), each file's claim answers for that file alone,
|
||||
weighed against that file's own text and its own suspected rules, so a sibling
|
||||
component is never covered and a sibling that measures a blocking rule does
|
||||
not turn this file's suspected finding into the Floor; a stylesheet beside the
|
||||
components answers for itself as on a page. 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, or a "Meet <Name>" or three-word triad
|
||||
heading, 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 |
|
||||
|---|---|---|---|---|
|
||||
| `purple-blue-gradient` | blocks | 2a | purple-to-blue, purple-to-pink or cyan-on-dark gradient | one anchor hue and solid ink |
|
||||
| `gradient-text` | ask | 2b | gradient text | solid ink; carry emphasis with weight or a display face |
|
||||
| `pure-black-white` | blocks | 7a | #000 or #fff as a colour outside the token block | tint toward the anchor hue: chroma 0.005 or more |
|
||||
| `absolute-surface-token` | ask | 7b | a surface token that resolves to pure black or white | tint the paper |
|
||||
| `zero-chroma-neutrals` | ask | 22 | zero-chroma oklch() neutrals | give the neutrals chroma 0.005 or more |
|
||||
| `accent-footprint` | ask | 23 | accent colour past about 5% of a viewport | retreat the accent to actions and one signal |
|
||||
| `glow-wash` | ask | 29 | glow and wash: blurred orbs, a radial halo or a zero-offset coloured glow | a solid or grained surface; elevation by lightness, shadows with offset and blur |
|
||||
| `cream-palette` | ask | 65 | the cream paper, serif display and terracotta accent look | move one axis: the accent first, the paper second, the display last |
|
||||
| `framework-palette` | ask | 74 | a framework default palette (Tailwind indigo-500, slate-900 and kin) as a token or utility | a colour mixed for the page in OKLCH |
|
||||
| `overused-font` | ask | 1 | a training-data display face (Inter, Roboto, Fraunces, Playfair Display, Space Grotesk and the rest of gstack's catalog) or a system stack as the display | a display face chosen for the page |
|
||||
| `banned-font` | ask | catalog | a face gstack bans in any role (Papyrus, Comic Sans, Impact, Lobster, Raleway, Courier New and the rest of lib/design-catalog.ts BANNED_FONTS) | a face chosen for the page; the body face for body and labels |
|
||||
| `line-length` | ask | 25 | running prose outside 45 to 75 characters a line | max-width: 65ch |
|
||||
| `font-family-count` | ask | 37 | more than three font families | display, body and one outlier |
|
||||
| `italic-word-in-heading` | blocks | 38i | one italic word inside an otherwise roman heading | a line break, or the whole head set one way |
|
||||
| `italic-serif-display` | ask | 38ii | an italic display system | roman display, or declare it on --font-display with font-synthesis: none |
|
||||
| `accented-word` | ask | 38iii | one word in a heading set apart by colour, weight, a highlight or an underline | the whole head one way; a line break; a second line at a second size |
|
||||
| `caps-leading` | blocks | 55a | an uppercase display head that can wrap with line-height under 1.0 | line-height 1.0 at least (1.02 to 1.08 recommended) |
|
||||
| `caps-lockup-leading` | ask | 55b | a single-line caps lockup under 1.02 | line-height 1.02, or make sure it cannot wrap at 320px |
|
||||
| `monospace-costume` | ask | 64 | monospace on labels, captions or small type when the content is not code | labels in the body face, a step smaller, normal case |
|
||||
| `tiny-text` | ask | 75 | running text under 12px, functional text under 11px, or prose leaded under 1.3 (render) | body 16px at 1.5 to 1.65; labels one step smaller |
|
||||
| `wide-tracking` | blocks | 67 | the tracked-caps label: small text set uppercase or letter-spaced past 0.03em | the body face, a step smaller, sentence case, untracked |
|
||||
| `template-cards` | ask | 3 | three equal icon-above-heading tiles, an icon in a rounded tile, or a thick coloured side stripe | vary widths and heights, inline icons, hairline or no border |
|
||||
| `nested-cards` | blocks | 4 | a card inside a card | one containment layer |
|
||||
| `vh-height` | blocks | 6a | a hero sized in vh | dvh or svh |
|
||||
| `raw-spacing` | blocks | 24a | a raw padding, margin or gap length where a --space-* token exists | the token |
|
||||
| `off-scale-spacing` | ask | 24b | spacing off the page's scale | the scale; three optical nudges at most |
|
||||
| `overflow-x-unclipped` | ask | 34 | overflow-x missing on html and body, or a 100vw width | overflow-x: clip on both; width: 100% |
|
||||
| `horizontal-scroll` | blocks | 34 | horizontal scroll at a width between 320 and 1920 (render) | find the element wider than its viewport and cap it |
|
||||
| `hero-balance` | ask | 44a/44c | the hero's content sits off centre for its screen, or its visual stands apart from the text (render) | centre the content in its screen; put the visual beside the text |
|
||||
| `hero-fold` | blocks | 44b | the headline's bottom or the hero's first action below the 800px fold at 1280 wide; the lede and the visual are not measured (render) | pull the display clamp down, tighten leading, a two-line lede, trim padding |
|
||||
| `wide-container` | blocks | 44d | at 1920x1080 the container has no max-width, gutters under 120px, or a grid gains columns | cap 1360 to 1520px and centre; hold column counts; cap vw visuals |
|
||||
| `bare-fr-track` | blocks | 50 | an image-bearing grid track on bare 1fr | minmax(0, 1fr) |
|
||||
| `display-head-unwrapped` | blocks | 50 | a display head without overflow-wrap: break-word and min-width: 0 | overflow-wrap: break-word; min-width: 0 (never anywhere) |
|
||||
| `radio-tab-jump` | blocks | 53 | CSS-only radio tabs at position: absolute; top: 0 with no JS guard | radios in flow at zero size, or a handler with preventScroll |
|
||||
| `double-sticky` | blocks | 56 | two position: sticky; top: 0 elements | top: var(--banner-height) on the second |
|
||||
| `screen-word-budget` | ask | 58 | more than about 80 words in one 900px window, or a hero with more than two text blocks before its actions | headline, one sentence, actions; a section is a head block, then its composition |
|
||||
| `section-gap` | ask | 59 | a gap under 120px between sections at 1440, every gap the same, or more than nine sections (render) | 120px at least, varied; six to nine sections a page |
|
||||
| `heading-rhythm` | ask | 71 | a heading nearer what precedes it than what it heads (render) | at least twice the space above a head as below it |
|
||||
| `cramped-padding` | ask | 72 | text closer than 8px (or half an em) to the side of its filled box (render) | pad the box; a 16px gutter |
|
||||
| `thin-section` | ask | 77 | a section below the hero with no mass: a narrow body on a wide canvas (render) | widen the body, add a visual, set a statement, or merge the section |
|
||||
| `small-section-heads` | ask | 78 | the median h2 below the hero renders under 40px at 1440 (render) | h2 at the section size, item titles at the title size |
|
||||
| `dead-band` | ask | 79 | more than 160px of nothing between the hero's lowest content and the fold (render) | centre the content and grow the visual or the head; never fill with copy |
|
||||
| `hand-drawn-svg` | ask | 80 | an inline SVG of 120px or more with three curves or fills and no text: a drawn illustration (render) | a photograph, the product built, or type at scale |
|
||||
| `empty-picture` | ask | 81 | a slot or gradient block of 300x200px or more showing nothing but its wash (render) | the user's image, a credited photograph, or the product at full fidelity |
|
||||
| `counting-marks` | ask | 82 | 24 or more identical marks standing for a quantity (render) | set the number in type |
|
||||
| `photo-wall` | ask | 83 | photographs cover more than a third of the page below the hero (render) | keep the strongest pictures, set big; carry the rest in type, figures, the product, a table |
|
||||
| `email-band-close` | ask | 85 | the page ends on an email sign-up form (one or two fields, one of them the email; never a form with a password or a sign-in button) on a page whose job is not subscribe, join, attend, listen or visit | end on what the visitor came to do, or on the last strong section |
|
||||
| `aphoristic-cadence` | blocks | 19 | the rebuttal cadence three or more times ("Not a feature. A platform.") | say what the product does, in its own nouns |
|
||||
| `generic-hero-copy` | blocks | 19 | a filler opener ("Built for the modern team", "In today's landscape"), "quietly trusted by", or the catalog's "Welcome to" and "all-in-one solution" | say what the product does, in its own nouns |
|
||||
| `generic-cta-copy` | ask | 19/42 | a stock second action beside the primary (Learn more, Get started, Book a demo) | one action, or a second that names where it goes |
|
||||
| `em-dash-overuse` | ask | 19 | em or en dashes carrying the prose | a comma, a colon, a full stop or a line break |
|
||||
| `template-footer-line` | ask | 19 | a stock footer line (All rights reserved) | the page's own line, or nothing |
|
||||
| `template-nav` | ask | 42 | the reflex nav: wordmark left, inline links, a button right, hairline under | decide the nav and build another shape, or keep it on the record |
|
||||
| `template-footer` | ask | 42 | the reflex footer: four link columns, a social row, a copyright | decide the footer and build another shape, or keep it on the record |
|
||||
| `invented-proof` | blocks | 46a | an invented metric, testimonial, logo wall or case-study count | the user's number, a text slot, or nothing |
|
||||
| `drawn-browser-chrome` | blocks | 47a | a hand-drawn browser bar (traffic lights, URL pill) or OS window title bar | a real screenshot in a <figure>, or no chrome |
|
||||
| `device-frame` | ask | 47b | a phone, laptop, terminal or editor frame | one frame with real content inside, or none |
|
||||
| `imitation-material` | ask | 47c | styled divs standing in for a product, a photo edge or a physical object | the product built inside a slot, a real asset, or nothing |
|
||||
| `kicker-above-heading` | ask | 54 | the eyebrow: short inert type before a heading that only announces it | open the section another way; where the heading is figurative, the label becomes the heading |
|
||||
| `template-punctuation` | blocks | 63 | template punctuation: middle-dot meta strings, an arrow glued to link text, decorative status dots | line breaks, columns, hairlines; a verb as the label |
|
||||
| `wireframe-slots` | ask | 68 | more than five empty slots on one page, or four or more in one row | set the set in type; a slot that stays holds a real photo or the product |
|
||||
| `repeated-fact` | ask | 69 | one fact (a price, a time, a count) printed in four or more sections | say each fact once, where it does work |
|
||||
| `fake-send` | blocks | 70 | a form that prints success with no request behind it | validate, post nowhere and say so plainly until the endpoint exists |
|
||||
| `layout-transition` | blocks | 10a | transition: all, or animating width, height, top or left | list the properties; transform and opacity |
|
||||
| `spacing-transition` | ask | 10b | transitioning margin or padding | transform |
|
||||
| `hover-tells` | ask | 11 | the same lift on every card, two effects on one element, an image scaling on hover, or a hover on something inert | one signal per element, on the container, not the image |
|
||||
| `bounce-easing` | ask | 12 | bounce or overshoot easing on UI state | an exponential ease-out |
|
||||
| `focus-ring-fade` | blocks | 15 | a focus ring that fades in | instant: exclude outline and box-shadow from the transition |
|
||||
| `tooltip-focus-delay` | blocks | 17 | a tooltip whose focus delay equals its hover delay | hover 800 to 1000ms, focus 0 |
|
||||
| `autorotate-no-pause` | blocks | 18 | auto-rotating content with no pause on hover and focus | pause on hover and focus; a visible pause control past 5 seconds |
|
||||
| `missing-focus-visible` | blocks | 26a | an interactive element with no :focus-visible ring, or an outline removed with nothing in its place | a ring at 3:1 or better, instant |
|
||||
| `missing-states` | ask | 26b | no :active or :disabled state | all the states a control can be in |
|
||||
| `missing-reduced-motion` | blocks | 27 | a keyframe or transform transition without a prefers-reduced-motion fallback | add @media (prefers-reduced-motion: reduce) |
|
||||
| `autoplay-unmuted` | blocks | 28a | a video autoplaying without muted | autoplay muted loop playsinline |
|
||||
| `lazy-lcp` | blocks | 28a | loading="lazy" on the first image or the hero video | fetchpriority="high" on the LCP image |
|
||||
| `video-without-poster` | ask | 28b | a video without poster or a priority hint | add them |
|
||||
| `input-state-shift` | blocks | 39a | input states that break: a border that changes width, a focus ring built from border, disabled by opacity alone | constant border, outline, shared height, three disabled channels |
|
||||
| `control-height` | ask | 39b | control height outside 44 to 52px on a marketing page | the number |
|
||||
| `content-hidden-at-rest` | blocks | 62 | a reveal class hiding content until JavaScript runs | visible by default; hide only under a .js class, or animate from visible with @starting-style |
|
||||
| `emoji-decoration` | blocks | 30a | an emoji standing in for an icon | one icon library, custom SVG, or typography |
|
||||
| `two-icon-libraries` | ask | 30b | two icon libraries on one page | one library |
|
||||
| `unlabeled-decoration` | blocks | 33 | a decorative <svg>, <canvas> or CSS-art element with neither aria-hidden="true" nor a label | hide it or name it |
|
||||
| `broken-image` | blocks | 76 | an <img> with no source or one that does not load | the real file |
|
||||
| `stock-photo-hotlink` | blocks | 76 | a stock-site photo hotlink | the user's image, or a library photograph credited on the page |
|
||||
| `text-occlusion` | blocks | 73 | text painted under another element (render) | give the text its own space, or its own surface above the cover |
|
||||
| `low-contrast` | blocks | 40 | text, an icon, a focus ring or a field edge under its contrast threshold | fix the pair; body 4.5:1, large text and edges 3:1 |
|
||||
| `gray-on-color` | blocks | 41 | button text within a step of its fill, or grey text on a coloured surface | the paired ink token |
|
||||
| `literal-outside-tokens` | blocks | 48 | a colour or font-family literal outside the token block | lift it into the tokens |
|
||||
| `button-label-wraps` | blocks | 49a | a button label or primary nav link that wraps | shorten it; white-space: nowrap |
|
||||
| `link-wraps` | ask | 49b | a footer link, tab or breadcrumb that wraps | shorten it |
|
||||
| `straight-quotes` | polish | F1 | straight quotes in prose | curly quotes |
|
||||
| `three-dot-ellipsis` | polish | F2 | three dots for an ellipsis | the ellipsis character |
|
||||
| `balanced-display-head` | polish | F3 | a display head without text-wrap: balance | text-wrap: balance |
|
||||
| `two-primary-buttons` | polish | F4 | two filled primary buttons on one page, or side by side | one filled primary; the second as a text or outline action |
|
||||
| `icon-coin` | polish | F5 | an icon in a coloured coin | the icon inline, bare |
|
||||
| `display-figures-tabular` | polish | F6 | display-size figures without tabular numerals | font-variant-numeric: tabular-nums |
|
||||
| `stamped-recipe` | polish | F7 | one container recipe stamped on six or more selectors, or the pill radius on five or more non-controls | vary the recipe; pills on controls, tags and badges only |
|
||||
| `scroll-choreography-mix` | polish | F8 | more than one scroll-choreography pattern | one pattern a page |
|
||||
| `fake-avatar` | blocks | F9 | a fake-human avatar hotlink | the user's people, initials, or no portrait |
|
||||
| `stock-person-photo` | polish | F9 | a person-keyword stock photo URL | the user's photograph |
|
||||
| `page-invariants` | polish | F15 | page invariants broken: primary button style, container width, column gap or radius drifting | one recipe for each |
|
||||
| `unthemed-browser-surfaces` | polish | F16 | no ::selection, caret-color or scrollbar-color on a page with a committed palette | theme the browser surfaces |
|
||||
| `text-sliver` | polish | F17 | a text block squeezed into a sliver at 1440 (render) | give the block room or merge the columns |
|
||||
| `tiny-slot` | polish | F18 | a slot rendering under 40px in either dimension (render) | size the slot for what it will hold |
|
||||
| `font-weight-count` | polish | F19 | more than three font weights in running use (render) | three weights |
|
||||
| `lonely-grid-item` | polish | F20 | a grid or wrapping row leaving one item alone on its last row (render) | change the count or the columns |
|
||||
| `skipped-heading` | polish | F21 | a heading level skipped | step the levels one at a time |
|
||||
| `justified-text` | polish | F22 | justified text without hyphens: auto | hyphens: auto, or left-aligned |
|
||||
| `missing-lang` | polish | F23 | a document without lang | lang on <html> |
|
||||
| `hairline-under-shadow` | polish | F24 | a hairline border under a wide soft shadow on one surface | the shadow or the border, not both |
|
||||
| `placeholder-name` | blocks | craft | a placeholder name (John Doe) | use a supplied name, or one that fits the product in sample data |
|
||||
| `placeholder-brand` | blocks | craft | a placeholder brand (Acme, Nexus) | use a supplied customer, or a fitting name in sample data |
|
||||
| `lorem` | blocks | craft | lorem ipsum placeholder text | write real copy |
|
||||
| `marketing-buzzword` | ask | craft | a marketing buzzword | say the specific thing |
|
||||
| `round-number` | blocks | craft | a round invented number | use the supplied figure, or drop the claim |
|
||||
| `needs-proof` | blocks | craft | a [Needs proof] marker left in the page | supply the proof or remove the claim before publishing |
|
||||
| `oops` | ask | craft | "oops" as an error message | say what happened and what to do |
|
||||
| `scroll-listener` | ask | craft | a scroll event listener | use IntersectionObserver; read scroll inside requestAnimationFrame for a scroll-linked effect |
|
||||
| `dead-link` | ask | craft | a dead link (href="#") | link it, or render the control disabled |
|
||||
| `alert` | ask | craft | alert() for a message | show the message inline |
|
||||
| `img-no-alt` | blocks | craft | an <img> without alt | add alt text (alt="" when decorative) |
|
||||
| `missing-title` | blocks | craft | a document without a <title> | add a <title> |
|
||||
| `missing-meta-description` | ask | craft | no meta description | add <meta name="description"> |
|
||||
| `missing-favicon` | polish | craft | a document without a favicon | add a favicon <link rel="icon"> |
|
||||
| `missing-skip-link` | ask | craft | no skip link reaching <main> | add a "Skip to content" link first in <body> whose #id exists on <main> |
|
||||
| `missing-aria-current` | ask | craft | route navigation without aria-current | mark the current nav link aria-current="page" |
|
||||
|
||||
Source gates are Nutlope/hallmark `references/gates.md` at 7ec8682; `craft`
|
||||
marks a rule carried over from `gstack-design-craft` (Elaya-derived). Four
|
||||
ids are the lint's own: `waiver` (a refused waiver, ask), `house-style` (more
|
||||
than three waivers, polish), `render` (a page that did not render, blocks:
|
||||
an unrendered page is never a pass) and `internal-error` (a page or stylesheet the lint could not read, or a rule family that
|
||||
threw on a page, ask: the page is not a pass until the lint has read it).
|
||||
|
||||
## Token names the rules read
|
||||
|
||||
Several rules key on hallmark's token names, which are also the names
|
||||
`/design-html` writes: `--color-paper` (the ground; `cream-palette`,
|
||||
`cyan-on-dark` and the dark-paper checks read it), `--color-accent`
|
||||
(`accent-footprint`, `cream-palette`), `--font-display` / `--font-heading` /
|
||||
`--font-title` / `--font-hero` (`overused-font`), `--space-*` (`raw-spacing`,
|
||||
`off-scale-spacing`) and any `--color-*` or `--font-*` token
|
||||
(`literal-outside-tokens` fires only on a page that declares one, since a
|
||||
literal on a page with no tokens bypasses nothing; its Tailwind half, an
|
||||
arbitrary value like `bg-[#0b0b12]` or `font-['Inter']`, fires on any page,
|
||||
since Tailwind's theme is the token block it bypasses). A project that names its
|
||||
tokens `--bg` or `--brand` gets none of those checks and no signal that they
|
||||
did not run; Phase 2 reads the roles from DESIGN.md.
|
||||
|
||||
The token block is `:root`, `html`, a `[data-theme|mode|scheme]` scope, `@theme`,
|
||||
`@font-face` and anything in a `tokens.css`. Its values are read in one mode:
|
||||
the base block's last declaration wins, and a mode override (a token rule under
|
||||
`@media (prefers-color-scheme: …)` or a `[data-theme=…]` scope) only supplies
|
||||
tokens the base block never declares, so a page that carries both a light and
|
||||
a dark palette is measured in the mode it declares first, never a mix of the
|
||||
two. A custom property declared in any other scope (`.dark { --color-paper: … }`,
|
||||
a component-local `--accent`) is a token definition, not a literal, so
|
||||
`literal-outside-tokens` does not report it; the dark values of a class-scoped
|
||||
theme are not read either.
|
||||
|
||||
## What the lint does not cover
|
||||
|
||||
A clean `LINT_SUMMARY` is not the whole design checklist. These checklist and
|
||||
catalog items stay with the LLM pass in `/design-review` and are not rules
|
||||
here: `!important` in new CSS (the parser drops the flag), an h1 past 6rem
|
||||
(`oversized-h1`), tracking below -0.04em (`extreme-negative-tracking`), body
|
||||
text under 16px in a static stylesheet (`tiny-text` here is hallmark's 12px
|
||||
running-text floor, measured by the render tier), the `theater-slop-phrase`
|
||||
list beyond "built for the modern", the pulsing dot, a text container with
|
||||
no `max-width` (`line-length` reads a declared `ch` value), an uppercase
|
||||
paragraph at body size (`wide-tracking` reads caps at label size or tracked;
|
||||
a 16px untracked uppercase `<p>` passes), and a fixed px `width` on a
|
||||
container with no responsive handling (`wide-container` reads the opposite
|
||||
case). The copy tier
|
||||
reports at the document's first line with a count, because
|
||||
`gstack-design-craft` counts matches without positions; Phase 2 folds those
|
||||
patterns into the lint with lines. It reads the page with every open
|
||||
`<script>`, `<style>`, `<template>` or `<nav>` that nothing closes after it
|
||||
dropped, so a page of unclosed openers costs its length there too.
|
||||
`tooltip-focus-delay` reads the delay slot of a tooltip's `transition`
|
||||
(the second time in each comma part) or its `transition-delay`, 100ms and
|
||||
over; a `0.2s` or `200ms` fade is a duration, not a delay. 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`. 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, grade or shape the lint does not
|
||||
know (each such answer is one `render` finding naming the probe and width,
|
||||
and every well-formed finding beside it is kept); 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. A stylesheet shared by
|
||||
many pages is parsed once but its stylesheet-only rules run per page and are
|
||||
deduplicated at the end, so forty pages on one bundle cost forty passes over
|
||||
it. `overused-font` and `banned-font` match a catalog stem to the whole
|
||||
family name or its first words (`Cormorant Garamond`, `DM Serif Display`,
|
||||
`Comic Sans MS`), never to a longer word (`Interstate` is not `Inter`).
|
||||
Four checklist items are read more narrowly here than the LLM pass reads
|
||||
them: `zero-chroma-neutrals` reports every opaque `oklch(L 0 H)` literal
|
||||
whether or not the page has an anchor hue (hallmark's stamp carried the hue;
|
||||
a deliberately monochrome page waives it on the record); `missing-focus-visible` reports a removed outline only when no
|
||||
`:focus` rule anywhere on the page draws a ring (the per-element check is the
|
||||
render tier's focus walk); `off-scale-spacing` holds every px length to the
|
||||
4px scale whether or not DESIGN.md declares one (`raw-spacing` is the rule
|
||||
gated on `--space-*` tokens); and `double-sticky` pairs any two `top: 0`
|
||||
sticky selectors without reading their scroll containers, so a sticky table
|
||||
header inside an `overflow: auto` wrapper under a sticky nav reports and
|
||||
wants a waiver on the record. As in hallmark, a selector the heading pattern
|
||||
matches (`__title`, `__headline`, `display`) is read as display type unless
|
||||
its name is also a label's or a control's, so a 12px uppercase tracked
|
||||
`.card__title` is caught by the markup half only when its text is typed in
|
||||
capitals. `em-dash-overuse` does not count an en dash between two digits (a
|
||||
range).
|
||||
|
||||
## What is deliberately not ported
|
||||
|
||||
hallmark's theme stamp (the CSS comment that declares a theme, a posture and a
|
||||
hero shape) and every guard that reads it; the genre flag; the judged gates
|
||||
(the AI template shape, section rhythm, hover by judgement, toasts, the
|
||||
reviews); the `--before` / `--kept` redesign record (Phase 2); the photo index.
|
||||
+4
-2
@@ -16,8 +16,10 @@ it, so `headless` keeps its BLOCK semantics.
|
||||
|
||||
## What the session kind changes
|
||||
|
||||
`gstack-skill-start` prints `SESSION_KIND: unattended` and
|
||||
`UNATTENDED_SESSION: true`, then applies these rules for the whole skill:
|
||||
`gstack-skill-start` prints `SESSION_KIND: unattended`,
|
||||
`UNATTENDED_SESSION: true` and the `unattended-session` instruction block
|
||||
(`GSTACK_INSTRUCTION_BEGIN: unattended-session <session-id>` through
|
||||
`GSTACK_INSTRUCTION_END`), then applies these rules for the whole skill:
|
||||
|
||||
- **No prompts.** The update check, upgrade offer, telemetry and sync consent,
|
||||
onboarding tips and routing blocks are suppressed exactly as they are for
|
||||
|
||||
Reference in new issue
Block a user