mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-01 06:24:41 +01:00
Merge remote-tracking branch 'origin/main' into gsta-96-preamble-hoist
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> # Conflicts: # CHANGELOG.md
This commit is contained in:
commit
0475923323
18 files changed
+505
-12
No files matched your search
@@ -32,6 +32,23 @@
|
||||
### Gate integrity
|
||||
- No threshold, budget, timeout, trial count, case kind or assertion weakened. The section heading "Completeness Principle — Boil the Ocean" is unchanged, so every skip list that names it still matches.
|
||||
|
||||
## [1.91.225.0] - 2026-10-26
|
||||
|
||||
**/design-html builds landing pages around one visitor action and pages that look finished, without inventing proof; adapted from Elaya's design skills (GSTA-92).** Freeform /design-html asked for purpose, audience and feel, then built. It never asked for the one action the page exists for or where the traffic comes from, and nothing caught a page that shipped with invented customers and numbers, a dead `href="#"`, a scroll-listener reveal, `100vh`, or no description, favicon or working skip link. Now every run reads a craft section at Step 1, and a free checker counts the tells at Step 4. Adapted from Elaya's [ai-design-skills](https://github.com/elayadesign/ai-design-skills) (landing-page-design, design-to-motion) and [redesign-skill](https://github.com/elayadesign/redesign-skill), MIT, © 2026 Elaya. Thank you, Elaya.
|
||||
|
||||
### Changed
|
||||
- **/design-html reads `sections/craft.md` on every run (Step 1).** The spec opens with `Surface: Persuade` or `Surface: other`. A Persuade page (landing, pricing, waitlist, campaign, comparison) asks at most three multiple-choice questions (primary action, traffic source, published or reference) and states the rest in one `Assumed:` line; picks one of four archetypes with a reason; keeps the hero budget (one proof signal only when real proof exists, never a metric row) and chooses the rest of the page from the archetype and objections; gives the primary action real invalid, pending, failure and success states. Every page: claims (testimonials, logos, results, prices, guarantees) come only from the user, and missing proof is left out or marked `[Needs proof: …]`; illustrative data looks real; a ship floor scoped to full documents (title, description, favicon, share tags on shareable pages, `og:image` only from a supplied URL, a skip link that reaches `<main>`, landmarks, alt text, inline validation, no dead links, `aria-current` on route navigation); scale snapping with readability floors and the nested-radius rule; balanced line breaks; motion rules for the one authored moment (content visible without script, `IntersectionObserver`, cheap properties only, no `scale(0)`, named curves, `100dvh`).
|
||||
- **Step 4** runs `bin/gstack-design-craft` after the Slop Gate, whether or not the detector is installed, with the gate's contract: one fix pass, one rerun, the rest accepted with a reason, once more when the refinement loop ends. The hand-off opens with a "Before this can ship" block: proof markers, production dependencies, exceptions, and every claim the check changed.
|
||||
- **/design-shotgun**: a landing, pricing or campaign screen's variants also differ in page archetype, keep the one primary action identical, and carry their section outline as a caption.
|
||||
|
||||
### Added
|
||||
- **`bin/gstack-design-craft`** (`lib/design-craft.ts`): prints `CRAFT_CHECK: <id> <count> <fix>` per tell and `CRAFT_RESULT: clean` (exit 0) or `<n> issue(s)` (exit 1); exit 2 on a non-HTML, empty or unreadable file. Copy rules read decoded visible text; `alert()` counts only in scripts; head and navigation rules run only on a full document. Buzzwords report under the catalog's `marketing-buzzword` id.
|
||||
- **NOTICE.md** and `licenses/MIT-elaya.txt`. Not carried over, because they conflict with gstack's design rules: Elaya's fixed fonts (Geist and Poppins are on gstack's overused-display list), palette, spacing values and icon sets, gradient hero text, mandatory scroll reveals and tagline section, and the island nav.
|
||||
|
||||
### Tests
|
||||
- `test/design-craft.test.ts`: every tell with its count; a finished page, inlined Pretext, a `100vh` line with its `100dvh` fallback, discount copy and in-page anchors stay clean; look-alike attributes, encoded markers, `!important` and reversed fallbacks; skip links judged by position and target in any language; the craft section's banned words and names all flagged; CLI exit codes.
|
||||
- `design-html-slop-gate` also requires the craft check to run once or twice; the design-html carve guard requires the craft section read.
|
||||
|
||||
## [1.91.224.0] - 2026-10-25
|
||||
|
||||
**`/context-restore` now says when commits landed after a checkpoint was saved, instead of offering finished steps as the plan.** `/context-save` recorded the branch, a timestamp and the modified files, but not the commit, so a checkpoint saved before a step was committed came back with that step still listed under Remaining Work. Ported from #3316 by @soyames, with their commit kept.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# Third-Party Notices
|
||||
|
||||
gstack is MIT. The files below contain material derived from Apache-2.0 works,
|
||||
rewritten in gstack's voice and modified. Rewriting is not an exemption: the
|
||||
license text is in `licenses/Apache-2.0.txt`, and each derived file carries a
|
||||
notice that it was changed. Unmodified copies (the rule-registry fixture, the
|
||||
gstack is MIT. The files below contain material derived from Apache-2.0 and MIT
|
||||
works, rewritten in gstack's voice and modified. Rewriting is not an exemption:
|
||||
the license texts are in `licenses/`, and each file derived from an Apache-2.0
|
||||
work carries a notice that it was changed. Unmodified copies (the rule-registry fixture, the
|
||||
license text) carry no header and are listed here instead.
|
||||
|
||||
## impeccable — Copyright Paul Bakaus — Apache License 2.0
|
||||
@@ -29,6 +29,27 @@ against the checksum pinned in `lib/design-detect-contract.ts`, and recorded in
|
||||
the egress ledger first. gstack does not audit the engine's network behavior; the
|
||||
wrapper refuses URL targets so gstack never asks it to touch the network.
|
||||
|
||||
## ai-design-skills and redesign-skill — Copyright (c) 2026 Elaya — MIT License
|
||||
|
||||
https://github.com/elayadesign/ai-design-skills (`skills/landing-page-design`,
|
||||
`skills/design-to-motion`, at 38bbe576) and
|
||||
https://github.com/elayadesign/redesign-skill (`skills/redesign-existing-projects`,
|
||||
at 64627f6e). License text: `licenses/MIT-elaya.txt`.
|
||||
|
||||
Derived, modified (rules rewritten in gstack's voice and reconciled with its
|
||||
design rules; Elaya's fixed fonts, colors, spacing values, icon sets and
|
||||
signature components are not carried over):
|
||||
|
||||
- `design-html/sections/craft.md.tmpl`: the Persuade intake, page archetypes,
|
||||
page structure, conversion and copy rules, indexing and build order from
|
||||
landing-page-design Part A; the content-realism, ship-floor, scale-snapping,
|
||||
line-break and motion rules from landing-page-design Part B, the website
|
||||
context of design-to-motion and the redesign-skill audit; and the generated
|
||||
`design-html/SKILL.md` / `sections/craft.md`.
|
||||
- `lib/design-craft.ts` and `bin/gstack-design-craft`: the content-realism and
|
||||
ship-floor tells, turned into a free check.
|
||||
- `design-shotgun/SKILL.md.tmpl`: page archetypes as a variant axis.
|
||||
|
||||
## DESIGN.md specification — Copyright Google LLC — Apache License 2.0
|
||||
|
||||
https://github.com/google-labs-code/design.md
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# gstack digest v1.91.224.0 — regenerate/re-copy after upgrading gstack
|
||||
# gstack digest v1.91.225.0 — regenerate/re-copy after upgrading gstack
|
||||
|
||||
Behavioral rules from gstack (https://github.com/garrytan/gstack), compressed
|
||||
for agent hosts without a full skill install. The full skills add workflows,
|
||||
|
||||
Executable
+37
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env -S bun --no-env-file --config=/dev/null
|
||||
// gstack-design-craft — free craft check for a generated page (/design-html Step 4).
|
||||
//
|
||||
// Usage: gstack-design-craft <file.html>
|
||||
//
|
||||
// Prints one "CRAFT_CHECK: <id> <count> <fix>" line per tell, then
|
||||
// "CRAFT_RESULT: clean" (exit 0) or "CRAFT_RESULT: <n> issue(s)" (exit 1).
|
||||
// Exit 2: usage error, a non-HTML file, an unreadable or empty file. Head and
|
||||
// navigation rules run only on a full HTML document; a fragment gets the copy
|
||||
// and code rules alone.
|
||||
|
||||
import { readFileSync } from 'fs';
|
||||
import { checkCraft, formatCraft } from '../lib/design-craft';
|
||||
|
||||
const file = process.argv[2];
|
||||
if (!file) {
|
||||
console.error('Usage: gstack-design-craft <file.html>');
|
||||
process.exit(2);
|
||||
}
|
||||
if (!/\.html?$/i.test(file)) {
|
||||
console.error(`gstack-design-craft: ${file} is not an HTML file; check the rendered HTML preview instead`);
|
||||
process.exit(2);
|
||||
}
|
||||
let source: string;
|
||||
try {
|
||||
source = readFileSync(file, 'utf-8');
|
||||
} catch (err) {
|
||||
console.error(`gstack-design-craft: cannot read ${file}: ${(err as Error).message}`);
|
||||
process.exit(2);
|
||||
}
|
||||
if (source.trim() === '') {
|
||||
console.error(`gstack-design-craft: ${file} is empty; nothing was checked`);
|
||||
process.exit(2);
|
||||
}
|
||||
const findings = checkCraft(source);
|
||||
console.log(formatCraft(findings));
|
||||
process.exit(findings.length === 0 ? 0 : 1);
|
||||
@@ -116,6 +116,7 @@ sections. Read a section in full before doing its step; do not work from memory.
|
||||
|------|-------------------|
|
||||
| analyzing the design or making any layout/visual decision (Step 1 onward) — the UX-principles doctrine governs every design choice | `sections/doctrine.md` |
|
||||
| writing the finalized HTML in Step 3 — the Pretext wiring patterns and API cheatsheet are the required reference for all text-layout code | `sections/pretext-patterns.md` |
|
||||
| writing the Step 1 implementation spec — the Persuade intake and archetype, and the craft floor every page ships with | `sections/craft.md` |
|
||||
| the Setup probe printed DESIGN_DETECTOR_INSTALL_OFFER — ask the user once whether gstack may download impeccable's engine (checksum-pinned, receipted) before any other step | `sections/detector-install-offer.md` |
|
||||
|
||||
---
|
||||
@@ -285,6 +286,9 @@ After routing, output a brief context summary:
|
||||
|
||||
## Step 1: Design Analysis
|
||||
|
||||
> **STOP.** Before writing the Step 1 implementation spec — the Persuade intake and archetype, and the craft floor every page ships with, Read `~/.claude/skills/gstack/design-html/sections/craft.md` and execute it
|
||||
> in full. Do not work from memory — that section is the source of truth for this step.
|
||||
|
||||
1. If `$D` is available (`DESIGN_READY`), extract a structured implementation spec:
|
||||
```bash
|
||||
$D prompt --image <approved-variant.png> --output json
|
||||
@@ -408,6 +412,7 @@ For framework output, save to:
|
||||
- `prefers-color-scheme` media query for dark mode
|
||||
- `prefers-reduced-motion` for animation respect
|
||||
- Real content extracted from the mockup (never lorem ipsum)
|
||||
- The craft section's ship floor, scale-snapped values and motion rules
|
||||
|
||||
**Never include by default (AI slop blacklist):** an approved mockup that carries one, a DESIGN.md blessing, or an explicit user ask overrides it; say the tradeoff once.
|
||||
- Purple/blue gradients as default <!-- ai-color-palette -->
|
||||
@@ -476,6 +481,8 @@ _DJ=$(mktemp "${TMPDIR:-/tmp}/gstack-detect.XXXXXX"); bun --no-env-file --config
|
||||
|
||||
Exit 2 → one surgical fix pass over the non-advisory rules in the `DETECT_TOP` block, then scan once more. Whatever remains, present the page with those findings listed as accepted-with-reason: a pattern the approved mockup contains, a value DESIGN.md's tokens bless or a pattern its Decisions Log or Do's and Don'ts records as intentional, or an inline `<!-- impeccable-disable <rule>: <reason> -->` the user agreed to. One pass, not a loop. Any other first line from the probe: skip, no ceremony.
|
||||
|
||||
Then, always: the craft section's check, `~/.claude/skills/gstack/bin/gstack-design-craft <path-to-finalized.html>`, one fix pass and one rerun, as above.
|
||||
|
||||
### Verification Screenshots
|
||||
|
||||
Take verification screenshots at 3 viewports. One `gstack-render` call serves
|
||||
|
||||
@@ -156,6 +156,8 @@ After routing, output a brief context summary:
|
||||
|
||||
## Step 1: Design Analysis
|
||||
|
||||
{{SECTION:craft}}
|
||||
|
||||
1. If `$D` is available (`DESIGN_READY`), extract a structured implementation spec:
|
||||
```bash
|
||||
$D prompt --image <approved-variant.png> --output json
|
||||
@@ -278,6 +280,7 @@ For framework output, save to:
|
||||
- `prefers-color-scheme` media query for dark mode
|
||||
- `prefers-reduced-motion` for animation respect
|
||||
- Real content extracted from the mockup (never lorem ipsum)
|
||||
- The craft section's ship floor, scale-snapped values and motion rules
|
||||
|
||||
**Never include by default (AI slop blacklist):** an approved mockup that carries one, a DESIGN.md blessing, or an explicit user ask overrides it; say the tradeoff once.
|
||||
- Purple/blue gradients as default <!-- ai-color-palette -->
|
||||
@@ -338,6 +341,8 @@ kill <PID> 2>/dev/null || true
|
||||
|
||||
{{DESIGN_DETECTOR:gate}}
|
||||
|
||||
Then, always: the craft section's check, `{{BIN_DIR}}/gstack-design-craft <path-to-finalized.html>`, one fix pass and one rerun, as above.
|
||||
|
||||
### Verification Screenshots
|
||||
|
||||
Take verification screenshots at 3 viewports. One `gstack-render` call serves
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
<!-- AUTO-GENERATED from craft.md.tmpl — do not edit directly -->
|
||||
<!-- Regenerate: bun run gen:skill-docs -->
|
||||
## Craft: Persuade pages and the craft floor
|
||||
|
||||
**Persuade pages** applies when the page exists to get one action from a visitor: a landing, marketing, pricing, waitlist, campaign or comparison page. Write `Surface: Persuade` or `Surface: other` as the spec's first line; on `other`, skip to Craft floor, which applies to every page. An approved mockup wins on structure and visuals; this section fills only what it leaves open.
|
||||
|
||||
### Persuade pages
|
||||
|
||||
**One offer → one audience → one primary action.**
|
||||
|
||||
**Intake.** When the context does not settle them, ask one AskUserQuestion with at most three multiple-choice questions: the primary action, the traffic source, and published or design reference. Assume the rest in one `Assumed:` line the user corrects in the refinement loop:
|
||||
- the conversion and the offer: exactly what they get
|
||||
- who it is for, the problem they came with, their top three objections, what they already know
|
||||
- the proof the user can supply (named customers, attributed quotes, real numbers) and the risk terms the business offers
|
||||
|
||||
**Archetype.** Pick one and say why in the spec's layout type: **classic hero + sections** (default; the product reads from one screenshot), **long-form story** (the visitor needs educating or is skeptical), **minimal conversion** (high-intent traffic or a small offer), **comparison** (search intent is "X vs Y").
|
||||
|
||||
**Structure.** Above the fold at 375×667 and on desktop, inside the hero budget: a headline naming outcome and audience, one sentence on how, one primary CTA, one proof signal only when real proof exists (named logos or one attributed quote; a metric row stays banned), one product visual. Below it, choose modules and order from the archetype and the objections. The long-form menu: problem → solution, three to five benefits, how it works in three steps, proof, an FAQ of six to twelve real objections, the risk reversal, a final CTA identical to the top. A minimal page stays minimal; a comparison page leads with the comparison.
|
||||
|
||||
**Conversion rules.**
|
||||
- A page behind an ad repeats the ad's promise and tone in the hero.
|
||||
- One primary CTA; nothing competes with it above the fold.
|
||||
- Benefit first and specific: "Cut weekly reporting from 4 hours to 15 minutes", never "save time".
|
||||
- Show the risk reversal the business offers (free plan, trial, no card, cancel anytime). Never invent one.
|
||||
- Proof sits beside the claim it supports. A high-friction offer moves the FAQ up.
|
||||
|
||||
**The primary action works.** A real destination (supplied URL or form endpoint) and every state: invalid (inline error, input kept), pending (control disabled so it cannot submit twice, announced through `aria-live`), failure (what happened, input kept, retry) and success (what happens next). With no backend, simulate the states and say so in a code comment and your summary.
|
||||
|
||||
**Copy.** A CTA is a verb plus what they get ("Start free trial"). Draft two or three headlines in the product's voice and keep the most specific one that names outcome and audience; "{outcome} without {pain}" is a test, not a template.
|
||||
|
||||
**Indexing.** Record the publication decision; keep the site's policy when known. A page the user keeps out of search gets `<meta name="robots" content="noindex">`; an indexed page gets a real title and description, and its FAQ ships as FAQPage JSON-LD.
|
||||
|
||||
**Build order.** One section at a time, top down; a refinement edits one section.
|
||||
|
||||
### Craft floor (every page)
|
||||
|
||||
**Claims come from the user, never from you.** Testimonials, customer names and logos, results, uptime, prices and guarantees appear only when supplied. Missing proof: leave the slot out, or mark it `[Needs proof: …]` in the warning color so it cannot ship by accident. A believable invented claim is worse than an obvious placeholder.
|
||||
|
||||
**Illustrative content looks real.** Sample data in product shots and UI (table rows, chart values, people in a demo inbox):
|
||||
- No placeholder people or brands (John Doe, Jane Doe, Acme, Nexus, SmartFlow); distinct avatars; varied dates.
|
||||
- No round sample numbers (99.99%, 50%, 1,000+ customers, 10x). In a claim, a round number signals it was made up: use the supplied figure or drop it.
|
||||
- No buzzwords: elevate, seamless, effortless, supercharge, streamline, revolutionize, unlock, empower, unleash, next-gen, game-changer, delve, tapestry, "in the world of".
|
||||
- Sentence-case headings. Errors say what happened and what to do, never "Oops!". No exclamation marks in success messages.
|
||||
|
||||
**Ship floor.** Head tags apply to a full HTML document, not a component or fragment. Every full document has `<title>`, a meta description, a favicon (an inline SVG data URI from the brand mark when no file is supplied), and on a page meant for sharing `og:title` and `og:description`; `og:image` only from a supplied URL. A "Skip to content" link is the first focusable element, hidden until focused, targeting the `id` of `<main>`. Landmarks (`<header>`, `<nav>`, `<main>`, `<footer>`); alt text on meaningful images, `alt=""` on decorative ones. A marketing footer links privacy and terms at supplied URLs. Required fields and email format validate inline, wired with `aria-invalid` and `aria-describedby`. Every link goes somewhere real: no `href="#"`, no made-up path; a control with no target renders disabled. Navigation to other pages marks the current one `aria-current="page"`; in-page section links do not.
|
||||
|
||||
**Values come from the scale.** Every font size, space and radius is a step of the system's scale (DESIGN.md, else the spec), chosen by role. An off-scale value snaps to the nearest step that keeps its role, usually the one below, with that step's line height; never below 16px body text or a 44px touch target, and an approved mockup's measured values stand. A shape nested in another with a gap under 32px gets inner radius = outer − gap when that exceeds 2px.
|
||||
|
||||
**Line breaks.** Browser-wrapped headings get `text-wrap: balance`, body `text-wrap: pretty`. Break display lines where the thought breaks; never one word alone on the last line.
|
||||
|
||||
**Motion.** These govern the one authored motion moment and state transitions; they license no more motion.
|
||||
- Content is visible by default; script adds the hidden start state only after the observer is set up (`.motion-ready` on `<html>`), so a script failure or a crawler sees everything and the CTA never waits on an animation.
|
||||
- Reveal with `IntersectionObserver` (`threshold: 0`, bottom `rootMargin` about −20%, once). Never an unthrottled `window.addEventListener('scroll')`; scroll-linked effects read scroll inside `requestAnimationFrame`.
|
||||
- Animate `transform`, `opacity`, `filter`, `clip-path` only; blur 14px or less.
|
||||
- No object grows from `scale(0)` (start at 0.85 or more); only lines and bars grow from zero. A data bar fills as a left-to-right `clip-path` wipe, 0.7–1.1s.
|
||||
- Stagger in reading order inside one section (30–80ms), never across the page.
|
||||
- Curves are named tokens: DESIGN.md's, else `cubic-bezier(0.16, 1, 0.3, 1)` for entrances and `cubic-bezier(0.65, 0, 0.35, 1)` for wipes; `linear` only for constant motion.
|
||||
- Loops pause off-screen. `prefers-reduced-motion: reduce` shows the final state. Full-height sections use `min-height: 100dvh`, not `100vh`.
|
||||
|
||||
### Craft check
|
||||
|
||||
At Step 4, after the Slop Gate and before the screenshots, run the free checker on a vanilla HTML output (for framework output, say it is unavailable):
|
||||
|
||||
```bash
|
||||
~/.claude/skills/gstack/bin/gstack-design-craft <path-to-finalized.html>
|
||||
```
|
||||
|
||||
It prints one `CRAFT_CHECK: <id> <count> <fix>` line per tell (the realism rules above, `[Needs proof:` markers, scroll listeners, `100vh`, `transition: all`, `href="#"`, `alert()`, missing alt; on a full document a missing title, description, favicon, working skip link, or `aria-current` on route navigation), then `CRAFT_RESULT: clean` or `CRAFT_RESULT: <n> issue(s)`. No `CRAFT_RESULT` line: say the check was unavailable and continue. Same contract as the Slop Gate: one surgical fix pass, one rerun, the rest listed as accepted-with-reason (a real customer named Nexus, a supplied 50% figure). Rerun once when the refinement loop exits; report only new lines.
|
||||
|
||||
**Before this can ship.** Open the Step 4 hand-off, and repeat before Step 5's "Copy to project": every `[Needs proof: …]` marker, the dependencies the page cannot own (legal URLs, form endpoint, og:image, indexing, analytics and cookie consent where required), the accepted exceptions, and every claim the check made you change.
|
||||
|
||||
Adapted from Elaya's [ai-design-skills](https://github.com/elayadesign/ai-design-skills) and [redesign-skill](https://github.com/elayadesign/redesign-skill) (MIT, © 2026 Elaya), rewritten for gstack. See NOTICE.md.
|
||||
@@ -0,0 +1,70 @@
|
||||
## Craft: Persuade pages and the craft floor
|
||||
|
||||
**Persuade pages** applies when the page exists to get one action from a visitor: a landing, marketing, pricing, waitlist, campaign or comparison page. Write `Surface: Persuade` or `Surface: other` as the spec's first line; on `other`, skip to Craft floor, which applies to every page. An approved mockup wins on structure and visuals; this section fills only what it leaves open.
|
||||
|
||||
### Persuade pages
|
||||
|
||||
**One offer → one audience → one primary action.**
|
||||
|
||||
**Intake.** When the context does not settle them, ask one AskUserQuestion with at most three multiple-choice questions: the primary action, the traffic source, and published or design reference. Assume the rest in one `Assumed:` line the user corrects in the refinement loop:
|
||||
- the conversion and the offer: exactly what they get
|
||||
- who it is for, the problem they came with, their top three objections, what they already know
|
||||
- the proof the user can supply (named customers, attributed quotes, real numbers) and the risk terms the business offers
|
||||
|
||||
**Archetype.** Pick one and say why in the spec's layout type: **classic hero + sections** (default; the product reads from one screenshot), **long-form story** (the visitor needs educating or is skeptical), **minimal conversion** (high-intent traffic or a small offer), **comparison** (search intent is "X vs Y").
|
||||
|
||||
**Structure.** Above the fold at 375×667 and on desktop, inside the hero budget: a headline naming outcome and audience, one sentence on how, one primary CTA, one proof signal only when real proof exists (named logos or one attributed quote; a metric row stays banned), one product visual. Below it, choose modules and order from the archetype and the objections. The long-form menu: problem → solution, three to five benefits, how it works in three steps, proof, an FAQ of six to twelve real objections, the risk reversal, a final CTA identical to the top. A minimal page stays minimal; a comparison page leads with the comparison.
|
||||
|
||||
**Conversion rules.**
|
||||
- A page behind an ad repeats the ad's promise and tone in the hero.
|
||||
- One primary CTA; nothing competes with it above the fold.
|
||||
- Benefit first and specific: "Cut weekly reporting from 4 hours to 15 minutes", never "save time".
|
||||
- Show the risk reversal the business offers (free plan, trial, no card, cancel anytime). Never invent one.
|
||||
- Proof sits beside the claim it supports. A high-friction offer moves the FAQ up.
|
||||
|
||||
**The primary action works.** A real destination (supplied URL or form endpoint) and every state: invalid (inline error, input kept), pending (control disabled so it cannot submit twice, announced through `aria-live`), failure (what happened, input kept, retry) and success (what happens next). With no backend, simulate the states and say so in a code comment and your summary.
|
||||
|
||||
**Copy.** A CTA is a verb plus what they get ("Start free trial"). Draft two or three headlines in the product's voice and keep the most specific one that names outcome and audience; "{outcome} without {pain}" is a test, not a template.
|
||||
|
||||
**Indexing.** Record the publication decision; keep the site's policy when known. A page the user keeps out of search gets `<meta name="robots" content="noindex">`; an indexed page gets a real title and description, and its FAQ ships as FAQPage JSON-LD.
|
||||
|
||||
**Build order.** One section at a time, top down; a refinement edits one section.
|
||||
|
||||
### Craft floor (every page)
|
||||
|
||||
**Claims come from the user, never from you.** Testimonials, customer names and logos, results, uptime, prices and guarantees appear only when supplied. Missing proof: leave the slot out, or mark it `[Needs proof: …]` in the warning color so it cannot ship by accident. A believable invented claim is worse than an obvious placeholder.
|
||||
|
||||
**Illustrative content looks real.** Sample data in product shots and UI (table rows, chart values, people in a demo inbox):
|
||||
- No placeholder people or brands (John Doe, Jane Doe, Acme, Nexus, SmartFlow); distinct avatars; varied dates.
|
||||
- No round sample numbers (99.99%, 50%, 1,000+ customers, 10x). In a claim, a round number signals it was made up: use the supplied figure or drop it.
|
||||
- No buzzwords: elevate, seamless, effortless, supercharge, streamline, revolutionize, unlock, empower, unleash, next-gen, game-changer, delve, tapestry, "in the world of".
|
||||
- Sentence-case headings. Errors say what happened and what to do, never "Oops!". No exclamation marks in success messages.
|
||||
|
||||
**Ship floor.** Head tags apply to a full HTML document, not a component or fragment. Every full document has `<title>`, a meta description, a favicon (an inline SVG data URI from the brand mark when no file is supplied), and on a page meant for sharing `og:title` and `og:description`; `og:image` only from a supplied URL. A "Skip to content" link is the first focusable element, hidden until focused, targeting the `id` of `<main>`. Landmarks (`<header>`, `<nav>`, `<main>`, `<footer>`); alt text on meaningful images, `alt=""` on decorative ones. A marketing footer links privacy and terms at supplied URLs. Required fields and email format validate inline, wired with `aria-invalid` and `aria-describedby`. Every link goes somewhere real: no `href="#"`, no made-up path; a control with no target renders disabled. Navigation to other pages marks the current one `aria-current="page"`; in-page section links do not.
|
||||
|
||||
**Values come from the scale.** Every font size, space and radius is a step of the system's scale (DESIGN.md, else the spec), chosen by role. An off-scale value snaps to the nearest step that keeps its role, usually the one below, with that step's line height; never below 16px body text or a 44px touch target, and an approved mockup's measured values stand. A shape nested in another with a gap under 32px gets inner radius = outer − gap when that exceeds 2px.
|
||||
|
||||
**Line breaks.** Browser-wrapped headings get `text-wrap: balance`, body `text-wrap: pretty`. Break display lines where the thought breaks; never one word alone on the last line.
|
||||
|
||||
**Motion.** These govern the one authored motion moment and state transitions; they license no more motion.
|
||||
- Content is visible by default; script adds the hidden start state only after the observer is set up (`.motion-ready` on `<html>`), so a script failure or a crawler sees everything and the CTA never waits on an animation.
|
||||
- Reveal with `IntersectionObserver` (`threshold: 0`, bottom `rootMargin` about −20%, once). Never an unthrottled `window.addEventListener('scroll')`; scroll-linked effects read scroll inside `requestAnimationFrame`.
|
||||
- Animate `transform`, `opacity`, `filter`, `clip-path` only; blur 14px or less.
|
||||
- No object grows from `scale(0)` (start at 0.85 or more); only lines and bars grow from zero. A data bar fills as a left-to-right `clip-path` wipe, 0.7–1.1s.
|
||||
- Stagger in reading order inside one section (30–80ms), never across the page.
|
||||
- Curves are named tokens: DESIGN.md's, else `cubic-bezier(0.16, 1, 0.3, 1)` for entrances and `cubic-bezier(0.65, 0, 0.35, 1)` for wipes; `linear` only for constant motion.
|
||||
- Loops pause off-screen. `prefers-reduced-motion: reduce` shows the final state. Full-height sections use `min-height: 100dvh`, not `100vh`.
|
||||
|
||||
### Craft check
|
||||
|
||||
At Step 4, after the Slop Gate and before the screenshots, run the free checker on a vanilla HTML output (for framework output, say it is unavailable):
|
||||
|
||||
```bash
|
||||
{{BIN_DIR}}/gstack-design-craft <path-to-finalized.html>
|
||||
```
|
||||
|
||||
It prints one `CRAFT_CHECK: <id> <count> <fix>` line per tell (the realism rules above, `[Needs proof:` markers, scroll listeners, `100vh`, `transition: all`, `href="#"`, `alert()`, missing alt; on a full document a missing title, description, favicon, working skip link, or `aria-current` on route navigation), then `CRAFT_RESULT: clean` or `CRAFT_RESULT: <n> issue(s)`. No `CRAFT_RESULT` line: say the check was unavailable and continue. Same contract as the Slop Gate: one surgical fix pass, one rerun, the rest listed as accepted-with-reason (a real customer named Nexus, a supplied 50% figure). Rerun once when the refinement loop exits; report only new lines.
|
||||
|
||||
**Before this can ship.** Open the Step 4 hand-off, and repeat before Step 5's "Copy to project": every `[Needs proof: …]` marker, the dependencies the page cannot own (legal URLs, form endpoint, og:image, indexing, analytics and cookie consent where required), the accepted exceptions, and every claim the check made you change.
|
||||
|
||||
Adapted from Elaya's [ai-design-skills](https://github.com/elayadesign/ai-design-skills) and [redesign-skill](https://github.com/elayadesign/redesign-skill) (MIT, © 2026 Elaya), rewritten for gstack. See NOTICE.md.
|
||||
@@ -16,6 +16,12 @@
|
||||
"title": "Pretext wiring patterns + API reference",
|
||||
"trigger": "writing the finalized HTML in Step 3 — the Pretext wiring patterns and API cheatsheet are the required reference for all text-layout code"
|
||||
},
|
||||
{
|
||||
"id": "craft",
|
||||
"file": "craft.md",
|
||||
"title": "Craft: Persuade pages and the craft floor",
|
||||
"trigger": "writing the Step 1 implementation spec — the Persuade intake and archetype, and the craft floor every page ships with"
|
||||
},
|
||||
{
|
||||
"id": "detector-install-offer",
|
||||
"file": "detector-install-offer.md",
|
||||
|
||||
@@ -384,6 +384,9 @@ Draw on DESIGN.md, taste memory, and the user's request to make each concept dis
|
||||
exists, it decides what varies: keep its fonts and palette and vary layout and
|
||||
composition. Without one, vary font family, color palette, and layout approach. If two
|
||||
variants look like siblings, rework the weaker one with a deliberately different direction.
|
||||
For a landing, pricing or campaign screen, also vary the page archetype (classic hero,
|
||||
long-form story, minimal conversion, comparison), keep the one primary action identical,
|
||||
and caption each variant with its section outline.
|
||||
|
||||
Concrete test: if someone could swap the headline text between two variants without
|
||||
noticing, they're too similar. Variants should feel like separate design teams made
|
||||
|
||||
@@ -219,6 +219,9 @@ Draw on DESIGN.md, taste memory, and the user's request to make each concept dis
|
||||
exists, it decides what varies: keep its fonts and palette and vary layout and
|
||||
composition. Without one, vary font family, color palette, and layout approach. If two
|
||||
variants look like siblings, rework the weaker one with a deliberately different direction.
|
||||
For a landing, pricing or campaign screen, also vary the page archetype (classic hero,
|
||||
long-form story, minimal conversion, comparison), keep the one primary action identical,
|
||||
and caption each variant with its section outline.
|
||||
|
||||
Concrete test: if someone could swap the headline text between two variants without
|
||||
noticing, they're too similar. Variants should feel like separate design teams made
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
/**
|
||||
* Free craft check for a generated page (design-html Step 4). Counts the tells
|
||||
* that make a page read as generated or unfinished: placeholder content, AI
|
||||
* cliché copy, scroll listeners, 100vh, dead links, and the missing ship-floor
|
||||
* tags. Copy checks read the visible text only (script, style and tags
|
||||
* removed); code checks read the source. Head checks run only on a full
|
||||
* document. Adapted from Elaya's landing-page-design and redesign-skill
|
||||
* (MIT); see NOTICE.md. Buzzwords report under the catalog's
|
||||
* `marketing-buzzword` id, and BUZZWORDS covers every word its heuristic
|
||||
* names (test-enforced), so the two lists cannot drift.
|
||||
*/
|
||||
|
||||
/** The catalog's marketing-buzzword words plus Elaya's AI cliché words. */
|
||||
export const BUZZWORDS: readonly string[] = [
|
||||
'seamless', 'effortless', 'supercharge', 'streamline', 'revolutionize', 'unlock', 'empower', 'elevate',
|
||||
'unleash', 'next-gen', 'game-changer', 'delve', 'tapestry', 'in the world of',
|
||||
];
|
||||
|
||||
function buzzwordPattern(): RegExp {
|
||||
const stem = (w: string) => w.replace(/-/g, '[- ]').replace(/e$/, '');
|
||||
return new RegExp(`\\b(?:${BUZZWORDS.map(stem).join('|')})(?:e|es|ed|ing|ly|er|ers|s|d|eration)?\\b`, 'gi');
|
||||
}
|
||||
|
||||
export interface CraftFinding {
|
||||
id: string;
|
||||
count: number;
|
||||
fix: string;
|
||||
}
|
||||
|
||||
interface CountRule {
|
||||
id: string;
|
||||
on: 'source' | 'text' | 'script';
|
||||
pattern: RegExp;
|
||||
fix: string;
|
||||
}
|
||||
|
||||
interface PresenceRule {
|
||||
id: string;
|
||||
pattern: RegExp;
|
||||
fix: string;
|
||||
}
|
||||
|
||||
const COUNT_RULES: CountRule[] = [
|
||||
{ id: 'placeholder-name', on: 'text', pattern: /\b(?:John|Jane) Doe\b/gi, fix: 'use a supplied name, or one that fits the product in sample data' },
|
||||
{ id: 'placeholder-brand', on: 'text', pattern: /\b(?:Acme|Nexus|SmartFlow)\b/g, fix: 'use a supplied customer, or a fitting name in sample data' },
|
||||
{ id: 'lorem', on: 'text', pattern: /\blorem ipsum\b/gi, fix: 'write real copy' },
|
||||
{ id: 'marketing-buzzword', on: 'text', pattern: buzzwordPattern(), fix: 'say the specific thing' },
|
||||
{ id: 'round-number', on: 'text', pattern: /(?<![\d.])(?:99\.9+|[1-9]0|100)%\s+(?:uptime|faster|more|less|fewer|better|growth|increase|reduction|of (?:teams|users|customers|companies))\b|\b(?:\d+0|\d+(?:,\d{3})+)\+?\s+(?:happy |active )?(?:users|customers|teams|companies)\b|(?<![\d.])10x\s+(?:faster|more|better)\b/gi, fix: 'use the supplied figure, or drop the claim' },
|
||||
{ id: 'needs-proof', on: 'text', pattern: /\[Needs proof:/g, fix: 'supply the proof or remove the claim before publishing' },
|
||||
{ id: 'oops', on: 'text', pattern: /\boops\b/gi, fix: 'say what happened and what to do' },
|
||||
{ id: 'scroll-listener', on: 'source', pattern: /addEventListener\(\s*['"`]scroll['"`]/g, fix: 'use IntersectionObserver; read scroll inside requestAnimationFrame for a scroll-linked effect' },
|
||||
{ id: 'transition-all', on: 'source', pattern: /\btransition\s*:\s*all\b|\btransition-all\b/g, fix: 'list the transitioned properties' },
|
||||
{ id: 'dead-link', on: 'source', pattern: /\bhref\s*=\s*["']#["']/g, fix: 'link it, or render the control disabled' },
|
||||
{ id: 'alert', on: 'script', pattern: /(?<![\w.$])(?:window\.)?alert\s*\(/g, fix: 'show the message inline' },
|
||||
{ id: 'img-no-alt', on: 'source', pattern: /<img\b(?![^>]*(?<![\w-])alt\s*=)[^>]*>/gi, fix: 'add alt text (alt="" when decorative)' },
|
||||
];
|
||||
|
||||
const HEAD_RULES: PresenceRule[] = [
|
||||
{ id: 'missing-title', pattern: /<title>\s*[^<\s][^<]*<\/title>/i, fix: 'add a <title>' },
|
||||
{ id: 'missing-meta-description', pattern: /<meta\b[^>]*\bname\s*=\s*["']description["']/i, fix: 'add <meta name="description">' },
|
||||
{ id: 'missing-favicon', pattern: /<link\b[^>]*\brel\s*=\s*["'][^"']*\bicon\b/i, fix: 'add a favicon <link rel="icon">' },
|
||||
];
|
||||
|
||||
export function visibleText(html: string): string {
|
||||
return html
|
||||
.replace(/<!--[\s\S]*?-->/g, ' ')
|
||||
.replace(/<(script|style|template)\b[\s\S]*?<\/\1>/gi, ' ')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/&#(\d+);/g, (_, n) => String.fromCodePoint(Number(n)))
|
||||
.replace(/&#x([0-9a-f]+);/gi, (_, n) => String.fromCodePoint(parseInt(n, 16)))
|
||||
.replace(/&(nbsp|lt|gt|quot|apos|amp);/g, (_, e) => ({ nbsp: ' ', lt: '<', gt: '>', quot: '"', apos: "'", amp: '&' })[e as string] as string)
|
||||
.replace(/\s+/g, ' ');
|
||||
}
|
||||
|
||||
export function checkCraft(html: string): CraftFinding[] {
|
||||
const text = visibleText(html);
|
||||
const scripts = [...html.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi), ...html.matchAll(/\bon[a-z]+\s*=\s*"([^"]*)"/gi)].map(m => m[1]).join('\n');
|
||||
const findings: CraftFinding[] = [];
|
||||
for (const rule of COUNT_RULES) {
|
||||
const count = (rule.on === 'text' ? text : rule.on === 'script' ? scripts : html).match(rule.pattern)?.length ?? 0;
|
||||
if (count > 0) findings.push({ id: rule.id, count, fix: rule.fix });
|
||||
}
|
||||
const vh = [...html.matchAll(/\b((?:min-|max-)?height)\s*:\s*100vh\b(\s*!important)?([^}]*)/g)]
|
||||
.filter(m => m[2] || !new RegExp(`(?<![\\w-])${m[1]}\\s*:\\s*100[dsl]vh\\b`).test(m[3])).length
|
||||
+ (html.match(/\b(?:min-)?h-screen\b/g)?.length ?? 0);
|
||||
if (vh > 0) findings.push({ id: 'vh-height', count: vh, fix: 'use min-height: 100dvh (a 100vh line followed by a 100dvh fallback is fine)' });
|
||||
if (!/<head\b/i.test(html)) return findings;
|
||||
for (const rule of HEAD_RULES) {
|
||||
if (!rule.pattern.test(html)) findings.push({ id: rule.id, count: 1, fix: rule.fix });
|
||||
}
|
||||
const body = html.slice(html.search(/<body\b/i) + 1);
|
||||
const firstFocusable = body.match(/<(?:a\b[^>]*\bhref|button|input|select|textarea)\b[^>]*>/i)?.[0] ?? '';
|
||||
const skipTarget = firstFocusable.match(/^<a\b[^>]*\bhref\s*=\s*["']#([^"']+)["']/i)?.[1];
|
||||
const main = html.match(/<main\b[^>]*(?<![\w-])id\s*=\s*["']([^"']+)["']/i)?.[1]
|
||||
?? html.match(/<[a-z]+\b[^>]*\brole\s*=\s*["']main["'][^>]*(?<![\w-])id\s*=\s*["']([^"']+)["']/i)?.[1];
|
||||
if (!skipTarget || skipTarget !== main) {
|
||||
findings.push({ id: 'missing-skip-link', count: 1, fix: 'add a "Skip to content" link first in <body> whose #id exists on <main>' });
|
||||
}
|
||||
const routeLinks = html.match(/<nav\b[\s\S]*?<\/nav>/gi)?.join('').match(/<a\b[^>]*\bhref\s*=\s*["'](?!#)[^"']+["']/gi)?.length ?? 0;
|
||||
if (routeLinks >= 2 && !/\baria-current\s*=\s*["']page["']/i.test(html)) {
|
||||
findings.push({ id: 'missing-aria-current', count: 1, fix: 'mark the current nav link aria-current="page"' });
|
||||
}
|
||||
return findings;
|
||||
}
|
||||
|
||||
export function formatCraft(findings: CraftFinding[]): string {
|
||||
const lines = findings.map(f => `CRAFT_CHECK: ${f.id} ${f.count} ${f.fix}`);
|
||||
lines.push(findings.length === 0 ? 'CRAFT_RESULT: clean' : `CRAFT_RESULT: ${findings.length} issue(s)`);
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Elaya
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "gstack",
|
||||
"version": "1.91.224",
|
||||
"version": "1.91.225",
|
||||
"description": "Garry's Stack — Claude Code skills + fast headless browser. One repo, one install, entire AI engineering workflow.",
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { BUZZWORDS, checkCraft, formatCraft, visibleText } from '../lib/design-craft';
|
||||
import { catalogEntry } from '../lib/design-catalog';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const BIN = path.join(ROOT, 'bin', 'gstack-design-craft');
|
||||
|
||||
const DIRTY = `<!doctype html><html><head><style>.hero{height:100vh;transition: all .3s}</style></head><body>
|
||||
<nav><a href="#">Docs</a><a href="/">Home</a><a href="/pricing">Pricing</a></nav>
|
||||
<h1>Elevate your workflow seamlessly</h1><p>Trusted by Acme and John Doe. 99.99% uptime, 50% faster, 10,000+ happy users. [Needs proof: logo]</p>
|
||||
<img src="a.png"><p>Oops! Something broke. Lorem ipsum.</p>
|
||||
<script>window.addEventListener('scroll', () => {}); alert('hi'); showAlert(1);</script></body></html>`;
|
||||
|
||||
const CLEAN = `<!doctype html><html lang="en"><head><title>Ledgerline: close the books in 2 days</title>
|
||||
<meta name="description" content="Close faster."><meta property="og:image" content="/og.png"><link rel="icon" href="/favicon.svg">
|
||||
<style>.hero{min-height:100vh;min-height:100dvh;transition: transform .4s var(--ease-out)} .w{width:100%}</style></head>
|
||||
<body><a href="#main">Skip to content</a><a href="#pricing">Pricing</a><nav><a href="/" aria-current="page">Home</a><a href="/pricing">Pricing</a></nav>
|
||||
<main id="main"><h1>Close the books in 2 days, not 9</h1><p>Teams at Halvorsen Freight cut month-end by 47.2%. Save 20% yearly. 100% open source. 30-day refund.</p><img src="x.png" alt=""></main>
|
||||
<script>const io = new IntersectionObserver(() => {}); window.showAlert = () => {};</script></body></html>`;
|
||||
|
||||
function run(file?: string) {
|
||||
const r = Bun.spawnSync([BIN, ...(file ? [file] : [])], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 });
|
||||
return { code: r.exitCode, out: r.stdout.toString(), err: r.stderr.toString() };
|
||||
}
|
||||
|
||||
describe('design-craft check', () => {
|
||||
test('a page with every tell reports each one with its count', () => {
|
||||
const counts = Object.fromEntries(checkCraft(DIRTY).map(f => [f.id, f.count]));
|
||||
expect(counts).toEqual({
|
||||
'placeholder-name': 1, 'placeholder-brand': 1, lorem: 1, 'marketing-buzzword': 2, 'round-number': 3, 'needs-proof': 1, oops: 1,
|
||||
'scroll-listener': 1, 'transition-all': 1, 'dead-link': 1, alert: 1, 'img-no-alt': 1, 'vh-height': 1,
|
||||
'missing-title': 1, 'missing-meta-description': 1, 'missing-favicon': 1,
|
||||
'missing-skip-link': 1, 'missing-aria-current': 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('a finished page is clean: CSS 100%, a 100vh line with its 100dvh fallback, in-page anchors, a showAlert helper and inlined Pretext are not flagged', () => {
|
||||
expect(checkCraft(CLEAN)).toEqual([]);
|
||||
const pretext = fs.readFileSync(path.join(ROOT, 'design-html', 'vendor', 'pretext.js'), 'utf-8');
|
||||
expect(checkCraft(CLEAN.replace('</body>', `<script type="module">${pretext}</script></body>`))).toEqual([]);
|
||||
expect(formatCraft([])).toBe('CRAFT_RESULT: clean');
|
||||
});
|
||||
|
||||
test('BUZZWORDS covers every word the catalog marketing-buzzword heuristic names', () => {
|
||||
const heuristic = catalogEntry('marketing-buzzword')?.heuristic ?? '';
|
||||
const words = heuristic.replace(/^.*?for /, '').replace(/\.$/, '').split(/,\s*/);
|
||||
expect(words.length).toBeGreaterThan(5);
|
||||
for (const word of words) expect(BUZZWORDS).toContain(word);
|
||||
expect(checkCraft('<p>Unlocking seamless, next gen workflows that elevate teams</p>').map(f => [f.id, f.count])).toEqual([['marketing-buzzword', 4]]);
|
||||
expect(checkCraft('<p>Take the elevator to the elevation map; streamers sold here</p>')).toEqual([]);
|
||||
});
|
||||
|
||||
test('the skip link must reach an existing id, and aria-current is required only for route navigation', () => {
|
||||
const ids = (html: string) => checkCraft(html).map(f => f.id);
|
||||
expect(ids(CLEAN.replace('id="main"', 'id="content"'))).toEqual(['missing-skip-link']);
|
||||
const sectionNav = CLEAN.replace('<a href="/" aria-current="page">Home</a><a href="/pricing">Pricing</a>', '<a href="#features">Features</a><a href="#faq">FAQ</a>');
|
||||
expect(ids(sectionNav)).toEqual([]);
|
||||
expect(ids(CLEAN.replace(' aria-current="page"', ''))).toEqual(['missing-aria-current']);
|
||||
});
|
||||
|
||||
test('attribute look-alikes, encoded markers and 100vh ordering are read correctly', () => {
|
||||
const ids = (html: string) => checkCraft(html).map(f => f.id);
|
||||
expect(ids('<img src="a.png" data-alt="x">')).toEqual(['img-no-alt']);
|
||||
expect(ids('<p>[Needs proof: logo]</p>')).toEqual(['needs-proof']);
|
||||
expect(ids(CLEAN.replace('id="main"', 'data-id="main"'))).toEqual(['missing-skip-link']);
|
||||
expect(ids('<style>.a{min-height:100dvh;min-height:100vh}</style>')).toEqual(['vh-height']);
|
||||
expect(ids('<style>.a{min-height:100vh !important;min-height:100dvh}</style>')).toEqual(['vh-height']);
|
||||
});
|
||||
|
||||
test('every word and name the craft section bans is one the checker flags', () => {
|
||||
const section = fs.readFileSync(path.join(ROOT, 'design-html', 'sections', 'craft.md.tmpl'), 'utf-8');
|
||||
const buzz = section.match(/No buzzwords: ([^\n]+)\.\n/)?.[1].replace(/"/g, '').split(/,\s*/) ?? [];
|
||||
expect(buzz.length).toBeGreaterThan(10);
|
||||
for (const word of buzz) expect(BUZZWORDS).toContain(word);
|
||||
const names = section.match(/No placeholder people or brands \(([^)]+)\)/)?.[1].split(/,\s*/) ?? [];
|
||||
expect(names.length).toBe(5);
|
||||
for (const name of names) expect(checkCraft(`<p>${name}</p>`).map(f => f.id)).toEqual([/Doe/.test(name) ? 'placeholder-name' : 'placeholder-brand']);
|
||||
});
|
||||
|
||||
test('skip link is judged by position and target; alert() counts only in script; svg text is copy', () => {
|
||||
const ids = (html: string) => checkCraft(html).map(f => f.id);
|
||||
expect(ids(CLEAN.replace('>Skip to content<', '>Zum Inhalt springen<'))).toEqual([]);
|
||||
expect(ids(CLEAN.replace('<a href="#main">Skip to content</a>', ''))).toEqual(['missing-skip-link']);
|
||||
expect(ids('<p>Set a price alert (free)</p>')).toEqual([]);
|
||||
expect(ids('<button onclick="alert(1)">Go</button>')).toEqual(['alert']);
|
||||
expect(ids('<svg><text>Acme</text></svg>')).toEqual(['placeholder-brand']);
|
||||
});
|
||||
|
||||
test('copy rules read visible text only; head rules skip a fragment', () => {
|
||||
expect(visibleText('<style>.a{content:"Acme"}</style><script>// John Doe</script><p>Halvorsen</p>')).toBe(' Halvorsen ');
|
||||
expect(checkCraft('<section><h2>Pricing</h2><a href="/buy">Start free trial</a></section>')).toEqual([]);
|
||||
});
|
||||
|
||||
test('the CLI prints CRAFT_CHECK lines and exits 1 on tells, 0 when clean, 2 on usage', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-craft-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'dirty.html'), DIRTY);
|
||||
fs.writeFileSync(path.join(dir, 'clean.html'), CLEAN);
|
||||
const dirty = run(path.join(dir, 'dirty.html'));
|
||||
expect(dirty.code).toBe(1);
|
||||
expect(dirty.out).toContain('CRAFT_CHECK: scroll-listener 1 ');
|
||||
expect(dirty.out.trim().split('\n').at(-1)).toBe('CRAFT_RESULT: 18 issue(s)');
|
||||
const clean = run(path.join(dir, 'clean.html'));
|
||||
expect(clean).toMatchObject({ code: 0, out: 'CRAFT_RESULT: clean\n' });
|
||||
expect(run().code).toBe(2);
|
||||
expect(run(path.join(dir, 'missing.html')).code).toBe(2);
|
||||
fs.writeFileSync(path.join(dir, 'empty.html'), ' \n');
|
||||
expect(run(path.join(dir, 'empty.html')).code).toBe(2);
|
||||
fs.writeFileSync(path.join(dir, 'page.tsx'), 'export const P = () => <p>Acme</p>;');
|
||||
expect(run(path.join(dir, 'page.tsx'))).toMatchObject({ code: 2, out: '' });
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -736,8 +736,8 @@ do not launch the downstream skill or open a browser.`,
|
||||
// (D3A: read-on-demand doctrine, requiredReads-guarded + loading eval)
|
||||
'design-html': {
|
||||
skill: 'design-html',
|
||||
expectedSections: ['doctrine.md', 'pretext-patterns.md', 'detector-install-offer.md'],
|
||||
requiredReads: ['doctrine.md', 'pretext-patterns.md'],
|
||||
expectedSections: ['doctrine.md', 'pretext-patterns.md', 'craft.md', 'detector-install-offer.md'],
|
||||
requiredReads: ['doctrine.md', 'pretext-patterns.md', 'craft.md'],
|
||||
scenario:
|
||||
'Walk /design-html in SIMULATION — do not run bash, start servers, launch a browser, or take screenshots. Treat Step 0 as already resolved: no CEO plan, no approved mockup, no variants, no DESIGN.md, no prior finalized.html — freeform mode (Case C option D), screen name "pricing", the user wants a pricing page for a developer-tools SaaS (dark, dense, three tiers, monospace-leaning). Do NOT use AskUserQuestion — proceed with the stated assumptions. Read each pointed section before doing its step, then execute Steps 1-3: produce the implementation spec, state the chosen Pretext tier and why, and generate the complete Pretext-native HTML — include the HTML in your report instead of writing files. Stop there: skip Step 3.5, Step 4, and Step 5.',
|
||||
staticInvariants: {
|
||||
@@ -759,7 +759,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
gateAfterStop: undefined, // operational skill, no plan-mode gate
|
||||
},
|
||||
behavioral: 'prompt',
|
||||
maxSkeletonBytes: 56_200, // measured 55,262 (2026-09-09): the detector install offer pointer + its sections-table row (the brief itself lives in sections/detector-install-offer.md); before that 54,545 for the review-cycle trust prose, the Slop Gate's Decisions-Log clause, and the blacklist header's override sentence + Honest Work wave merged (W2/W3 gate integrity in review/ship, W4 kernel + DONE line in every tier-2+ skill, W5/W7 in spec/ship): re-measured on the integrated tree (2026-10-10).
|
||||
maxSkeletonBytes: 56_200, // measured 55,786 (2026-10-25): the craft section pointer, its index row and the Step 4 craft-check line (GSTA-92); before that 55,262 (2026-09-09): the detector install offer pointer + its sections-table row (the brief itself lives in sections/detector-install-offer.md); before that 54,545 for the review-cycle trust prose, the Slop Gate's Decisions-Log clause, and the blacklist header's override sentence + Honest Work wave merged (W2/W3 gate integrity in review/ship, W4 kernel + DONE line in every tier-2+ skill, W5/W7 in spec/ship): re-measured on the integrated tree (2026-10-10).
|
||||
minUnionBytes: 57_500, // Phase 4 wave 4; measured union 58,682
|
||||
mustContain: ["Don't make me think", "Users scan, they don't read", 'The Goodwill Reservoir', 'PRETEXT API CHEATSHEET', 'Pattern 3: Text around obstacles'],
|
||||
},
|
||||
|
||||
@@ -1316,8 +1316,8 @@ describeIfSelected('Design HTML slop gate E2E', ['design-html-slop-gate'], () =>
|
||||
testConcurrentIfSelected('design-html-slop-gate', async () => {
|
||||
const result = await runSkillTest({
|
||||
prompt: `Read design-html-gate.md: the /design-html detector probe block and its "Slop Gate (bounded, never a loop)" step.
|
||||
finalized.html in this directory is the finished page. Run the probe (--host claude), then the slop gate on finalized.html exactly as written: one surgical fix pass over the non-advisory findings, one rescan, then stop.
|
||||
Write ${workDir}/gate-output.md listing what you fixed and every remaining finding as accepted-with-reason, each tagged with its [rule-id]. Do not take screenshots, do not run npx, do not scan more than twice.`,
|
||||
finalized.html in this directory is the finished page. Run the probe (--host claude), then the slop gate on finalized.html exactly as written: one surgical fix pass over the non-advisory findings, one rescan. Then the craft check line that follows it, the same way, then stop.
|
||||
Write ${workDir}/gate-output.md listing what you fixed and every remaining finding as accepted-with-reason, each tagged with its [rule-id] or craft id. Do not take screenshots, do not run npx, do not run either check more than twice.`,
|
||||
workingDirectory: workDir,
|
||||
maxTurns: 20,
|
||||
timeout: CAPTURE_MS,
|
||||
@@ -1332,6 +1332,9 @@ Write ${workDir}/gate-output.md listing what you fixed and every remaining findi
|
||||
const scans = result.toolCalls.filter(c => c.tool === 'Bash' && /gstack-design-detect\.ts scan /.test(String(c.input?.command ?? '')));
|
||||
expect(scans.length).toBeGreaterThanOrEqual(1);
|
||||
expect(scans.length).toBeLessThanOrEqual(2);
|
||||
const craftRuns = result.toolCalls.filter(c => c.tool === 'Bash' && /gstack-design-craft['"]?\s+[^\s;|&]*\.html\b/.test(String(c.input?.command ?? '')));
|
||||
expect(craftRuns.length).toBeGreaterThanOrEqual(1);
|
||||
expect(craftRuns.length).toBeLessThanOrEqual(2);
|
||||
const outPath = path.join(workDir, 'gate-output.md');
|
||||
expect(fs.existsSync(outPath)).toBe(true);
|
||||
const out = fs.readFileSync(outPath, 'utf-8').toLowerCase();
|
||||
|
||||
Reference in new issue
Block a user