diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..83995b7 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,52 @@ +name: CI + +# Typecheck + every test suite, including the render-dependent ones (Chromium +# is installed so the examples regression and the env-guarded CLI render tests +# actually run — a "trust the gates" repo tests its render path on every push). + +on: + push: + branches: [master] + pull_request: + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: pnpm + + - run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Install Chromium (version-matched to playwright-core) + run: pnpm exec playwright install --with-deps chromium + + - name: Typecheck + run: pnpm typecheck + + - name: Engine + gates + run: pnpm test + + - name: Skill structural lint + run: pnpm test:lint + + - name: Render extension + run: pnpm test:render + env: + KEYSTONE_RENDER_TEST: "1" + + - name: Example regression (Tier 3) + run: pnpm test:examples + env: + KEYSTONE_RENDER_TEST: "1" + + - name: Env-guarded CLI render tests + run: node --test test/engine/check-gates.test.mjs + env: + KEYSTONE_RENDER_TEST: "1" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..51c9f95 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,82 @@ +name: Release + +# Publishes @getpipher/keystone to npm on tag push (v*). +# Uses the getpipher org secret NPM_TOKEN (granular npm token, Read-and-write, +# scoped to @getpipher, with "Bypass 2FA" — no OTP needed). Inherited by all +# getpipher repos. gh secret set NPM_TOKEN --org getpipher +# +# Idempotent: skips if the version is already on npm (safe to re-run). +# Gates on typecheck + the render-free suites before publishing. The render +# suites are intentionally NOT re-run here: the tagged commit already passed +# CI (which runs them with Chromium), and publish should not depend on a +# browser download. No build step — extensions ship raw .ts, run via tsx. + +on: + push: + tags: ["v*"] + workflow_dispatch: + +permissions: + contents: write # needed to create GitHub Releases on tag push + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + + - uses: actions/setup-node@v4 + with: + node-version: "24" + cache: pnpm + registry-url: https://registry.npmjs.org + + - run: pnpm install --frozen-lockfile --ignore-scripts + + - name: Typecheck + run: pnpm typecheck + + - name: Engine + gates + run: pnpm test + + - name: Skill structural lint + run: pnpm test:lint + + - name: Skip if already published + id: check + run: | + set -u + ver=$(node -p "require('./package.json').version") + pkg=$(node -p "require('./package.json').name") + existing=$(npm view "$pkg@$ver" version 2>/dev/null || true) + if [ "$existing" = "$ver" ]; then + echo "::notice::$pkg@$ver already on npm — skipping publish" + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "Publishing $pkg@$ver" + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Publish + if: steps.check.outputs.skip != 'true' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --access public + + # Keep GitHub Releases in sync with npm: create a release for the tag + # (auto-generated notes). Idempotent — skips if the release already exists. + # Tag-guarded: on workflow_dispatch, GITHUB_REF_NAME is a branch, and + # creating a "release" for it would mint a bogus git tag. + - name: Create GitHub Release + if: success() && startsWith(github.ref, 'refs/tags/') + env: + GH_TOKEN: ${{ github.token }} + run: | + tag="${GITHUB_REF_NAME}" + if gh release view "$tag" >/dev/null 2>&1; then + echo "::notice::Release $tag already exists — skipping" + else + gh release create "$tag" --generate-notes --title "$tag" + fi diff --git a/.gitignore b/.gitignore index 49f7e50..85ec9e9 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,15 @@ node_modules/ keystone-report.{json,html} keystone-audit-report.{md,json} keystone-audit/ -.keystone/ \ No newline at end of file +.keystone/ +# examples: committed evidence (reports + screenshots); raw data stays local +!examples/**/keystone-report.json +!examples/**/keystone-report.html +!examples/**/*.png +!examples/**/keystone-audit-report.json +!examples/**/keystone-audit-report.md +examples/**/keystone-render/computed.json +examples/**/keystone-render/dom.html +examples/**/keystone-render/viewports.json +examples/**/keystone-render/clickable.json +examples/**/keystone-audit/ diff --git a/NOTICE b/NOTICE index 22c43d0..60f7b21 100644 --- a/NOTICE +++ b/NOTICE @@ -21,6 +21,7 @@ Per the curated-rewrite policy in Keystone's design spec: Playwright render extension, the 8 deep theme specifications, and the engine-verified Step 7 loop are Keystone's original contribution. -Hallmark's MIT license is preserved at /tmp/hallmark/LICENSE and acknowledged -here with thanks. See README § "How Keystone relates to Hallmark" for the +Hallmark's MIT license is preserved in the upstream repository +(https://github.com/Nutlope/hallmark/blob/main/LICENSE) and acknowledged here +with thanks. See the README section "How Keystone relates to Hallmark" for the full relationship. diff --git a/README.md b/README.md index d3c8bf5..a377abb 100644 --- a/README.md +++ b/README.md @@ -1,45 +1,129 @@ # @getpipher/keystone -Anti-AI-slop design skill with an **executable gate engine**. Beats Hallmark -by *enforcing* its gates instead of imagining them. +**Anti-AI-slop design skill with an executable gate engine.** Makes the UIs an +AI coding assistant generates look *made, not generated* — and can prove it. -## Status +Every LLM has on-distribution defaults: hero → 3 feature cards → CTA → footer, +Inter font, indigo gradients, glassmorphism, `hover:scale-105`, emoji icons, +re-drawn browser chrome. [Hallmark](https://github.com/Nutlope/hallmark) (MIT) +encodes the best existing rule-set against this — 58 anti-slop gates, 21 +macrostructures, 50 component archetypes — but its gates are **prose**: the +model is told to "imagine the rendered output," then claims "58/58 ✓" on its +own honor. There is no enforcement. -| Layer | Version | What | -|---|---|---| -| Engine | v0.1.0 ✅ | 13 deterministic gates + Playwright render extension + CLI (merged, PR #1) | -| Skill catalog | in development | SKILL.md + references/ (this branch) | -| Vision pass | planned | @getpipher/vision integration (Plan 3) | +Keystone reuses the taxonomy (credited in [NOTICE](NOTICE)) and makes the gates +**executable**: a real Node engine parses the emitted HTML/CSS, a real headless +Chromium renders it at exact CSS px, real APCA math scores every contrast pair, +and a vision pass asks *"does this look AI-generated?"* on the actual +screenshots — the one question Hallmark cannot ask its model, because its model +never sees the page. + +> The user can trust the gates ran because a script checked them, not because +> the model claimed it did. + +## Install + +```bash +pi install npm:@getpipher/keystone +``` + +Exposes the `keystone` skill (Build + Audit verbs) and the `keystone_render` +tool in one pi package. + +## What you get + +**`keystone build`** — a 7-step design flow: design-context gate → +macrostructure pick (21, diversification-enforced against project memory) → +theme (8 deep specs) → build → **Step 7, the engine-verified slop test**: +deterministic gates first (≤3 iterations), then the vision pass (≤2), then the +honest resolution — the stamp in your CSS says the real score, whatever it is. -## What it does +**`keystone audit `** — the same engine pointed at someone else's +code. Read-only, ranked punch list, four severity tiers, computed APCA numbers, +real file:line evidence. Path mode renders from disk; URL mode navigates live +(behind an SSRF guard). -`keystone build` runs a 7-step design flow. Step 7 is the differentiator: -instead of the model claiming "58/58 ✓" on its honor, a real Node engine -parses the emitted HTML/CSS, a real headless Chromium renders it at exact CSS -px, real APCA math scores contrast, and a vision model answers "does this -look AI-generated?" on a real screenshot. The report is openable. Failures -are declared, never silently claimed. +**The engine** — 46 deterministic detectors covering 48 gate numbers: banned +fonts, gradient text, 3-equal card grids, pure black/white, transition-all, +animating layout props, contrast (WCAG + APCA on every computed pair), +horizontal scroll at 320–768, two-line clickables, hero-fit-the-fold, token +discipline, the nav/footer AI fingerprints, re-drawn chrome, accent-area +budget, missing interaction states, input states, reduced-motion coverage, and +more. Every gate in [`skills/keystone/references/gates.md`](skills/keystone/references/gates.md) +is annotated with its checker — the model pre-empts the gates because it knows +what the engine will catch. + +## Quick start (standalone, no pi) + +```bash +npm i -g @getpipher/keystone -`keystone audit ` points the same engine at someone else's code — -a read-only ranked punch list with computed APCA numbers and real file:line -evidence. +# audit a local page (renders it, scores 48 gates, writes a ranked report) +keystone audit ./my-page/ + +# audit a live URL (SSRF-guarded) +keystone audit https://example.com + +# or the build CLIs directly +node engine/check-gates.mjs --html page.html --css page.css \ + --render --viewports 1280,375,320,414,768 --out . +``` + +## Examples — the proof + +Five complete builds, five different macrostructures, five themes spanning all +four genres, every one scored by its own engine: + +| Build | Theme · Macro | Score | +|---|---|---| +| [Kestrel — observability](examples/01-cobalt-observability/) | Cobalt · Split Studio | **48/48** (+ self-audit 43/43) | +| [Fern & Fog — plant shop](examples/02-garden-plantshop/) | Garden · Long Document | **48/48** | +| [capstan — deploy CLI](examples/03-terminal-cli/) | Terminal · Manifesto | **48/48** | +| [Two Drum Press — riso studio](examples/04-riso-printstudio/) | Riso · Catalogue | **48/48** | +| [Lingua Potluck — language co-op](examples/05-hum-languages/) | Hum · Bento Grid | **48/48** | + +Each directory carries its evidence: the openable `keystone-report.html`, the +five viewport screenshots the vision pass judged, and a `brief.md` recording +every decision and verdict — including the honest ones. See +[examples/README.md](examples/README.md). ## How Keystone relates to Hallmark -Hallmark (github.com/Nutlope/hallmark, MIT) encodes a tight anti-slop rule-set -but its gates are prose — the model "imagines the render" and self-grades. -Keystone reuses Hallmark's taxonomy (21 macrostructures, 50 archetypes, 58 -gates, 4 genres, 7-step skeleton) and makes the gates executable. See NOTICE -for full attribution. +The thesis is falsifiable: run the *same brief* through both skills, with the +same model, no human intervention — then render and score both outputs with +*our* engine, and ask the vision question of both screenshots. The harness +ships in [`test/compare/`](test/compare/) (8 verbatim briefs from Hallmark's +own MIT test suite); the runs and the public gallery land in Plan 5b. + +**The honesty clause:** if Hallmark beats Keystone on a brief, the gallery +shows it. A rigged demo would destroy the very thing this project sells. + +## Status + +| Layer | State | +|---|---| +| Engine | ✅ complete — 46 detectors / 48 gate numbers, 275 tests | +| Skill catalog | ✅ SKILL.md + 21 macros + 50 archetypes + 8 deep theme specs | +| Audit verb | ✅ path + URL mode, SSRF guard, ranked punch list | +| Examples | ✅ 5 builds, all 48/48, committed evidence | +| Comparison gallery | 🔜 Plan 5b — harness shipped, runs pending | -## Install (pi) +## Development ```bash -pi install npm:@getpipher/keystone +pnpm install +pnpm test # engine + gates (162) +pnpm test:lint # skill structural lint (103) +pnpm typecheck # extensions/render.ts +KEYSTONE_RENDER_TEST=1 pnpm test:render # render extension (needs Chromium) +KEYSTONE_RENDER_TEST=1 pnpm test:examples # example regression (needs Chromium) ``` -Exposes the `keystone` skill + the `keystone_render` tool. +Releases: push a `v*` tag → CI gates → npm publish (org NPM_TOKEN). -## Repo +## Attribution & license -Private during development. Public flip + npm publish in Plan 5. +MIT — see [LICENSE](LICENSE) and [NOTICE](NOTICE). Keystone reuses Hallmark's +taxonomy (macrostructure names, archetype codes, gate concepts, the 7-step +skeleton) under MIT with credit; all prose, theme specs, tokens, the engine, +and the examples are original work. diff --git a/docs/superpowers/plans/2026-08-10-keystone-examples-publish-5a.md b/docs/superpowers/plans/2026-08-10-keystone-examples-publish-5a.md new file mode 100644 index 0000000..8e3fb12 --- /dev/null +++ b/docs/superpowers/plans/2026-08-10-keystone-examples-publish-5a.md @@ -0,0 +1,229 @@ +# Plan 5a — Examples + launch (examples, regression test, harness tooling, CI, publish) + +**Project:** @getpipher/keystone +**Branch:** `feat/examples-publish-5` (off `master` `1c175ce`) +**Date:** 2026-08-10 +**Predecessor:** Plan 1b-2 (craft gates, PR #7 merged — deterministic engine COMPLETE: 46 detectors / 48 gate numbers; 273 tests green) +**Successor:** Plan 5b — execute the comparison harness (8 Hallmark builds + 8 Keystone builds + gallery) + +## Goal + +The public launch. Five engine-verified example builds (committed evidence, ≥1 self-audit), the Tier-3 example regression test, the comparison-harness *tooling* (unexecuted — 5b runs it), CI + release workflows, and the launch sequence: merge → flip repo PUBLIC → tag `v0.1.0` → org-convention npm publish. + +**Split rationale (approved):** the launch (product) must not wait on 16 comparison builds (marketing). 5a ships everything except harness *execution*; 5b executes. + +## Locked constraints (do not re-litigate) + +- pi-first. MIT + NOTICE (shipped — NOTICE must ship in the npm `files`). +- **Frozen:** theme specs, engine wiring, detectors — additive changes only, and 5a has none planned. +- 2-space indent, trailing newline at EOF, no AI attribution. Author `RECTOR `, GPG `BF47B9DC1FA320FA`. +- Branch `feat/examples-publish-5`, PR #8, merge `--merge --delete-branch`. Coherent-slice commits. +- Version: **0.1.0** (pre-1.0 honest — the proof gallery lands in 5b; 1.0.0 after). +- Execution model: **controller authors the 5 examples directly** (product showcase = quality-critical; providers unreliable per Plan-4 log). Dispatch only for final review. +- Render verification always with `KEYSTONE_RENDER_TEST=1`. + +## The 5 examples (approved Q2; 02 re-briefed to avoid collision with Hallmark's maple-bakery test brief) + +| Dir | Theme | Genre | Brief | Macro direction | Special | +|---|---|---|---|---|---| +| `examples/01-cobalt-observability/` | Cobalt | modern-minimal | Developer observability tool — "traces, not dashboards" | Split-screen / asymmetric feature lead | **Self-audit target** | +| `examples/02-garden-plantshop/` | Garden | editorial | Fern & Fog — house-plant shop (hours, care, events) | Long-document editorial | (replaces Maple bakery — that's Hallmark's test brief) | +| `examples/03-terminal-cli/` | Terminal | atmospheric | Deploy-from-the-terminal infra product | Manifesto/poster hero + dense feature index | | +| `examples/04-riso-printstudio/` | Riso | editorial | Independent risograph print studio / zine fair | Catalogue grid, irregular | | +| `examples/05-hum-languages/` | Hum | playful | Community language-learning co-op | Bento with personality | | + +Coverage: genres 4/4 · papers light×4/dark×1 · display styles grotesk/roman-serif/mono/risograph-bold/rounded-sans · macros 5 distinct. + +**Every example goes through the full skill flow** (SKILL.md Steps 1–7), authored against `skills/keystone/references/themes/.md`: + +1. Author `tokens.css` + `style.css` + `index.html` per the theme spec (tokens → CSS, no inline values — G48). +2. **Deterministic loop (7.1)**: `node engine/check-gates.mjs --html index.html --css style.css --out . --render --viewports 1280,375,320,414,768` — fix → re-run, cap 3. Target: 48/48. +3. **Vision pass (7.2)**: read the 1280 + 375 screenshots directly (controller is multimodal — this session's describe_image equivalent), answer the 18 questions from `gates.md § The vision pass`, fix → re-render, cap 2. Verdicts recorded in `brief.md` (including honest S1). +4. **Stamp + log (7.4)**: fill the stamp `gates:` field; write `.keystone/log.json` (gitignored — build memory stays local). + +**Committed evidence per example** (the "open the report" thesis — whatever the CLI emitted, verbatim): +`index.html`, `style.css`, `tokens.css`, `brief.md`, `keystone-report.json`, `keystone-report.html`, `keystone-render/*.png` (5 viewports). +NOT committed (explicitly ignored): `keystone-render/computed.json`, `dom.html`, `viewports.json`, `clickable.json` (regenerable raw data; keeps repo lean). + +## Architecture (file map) + +``` +examples/ +├── README.md (NEW — index: table of 5, links to evidence + scores) +├── 01-cobalt-observability/ … 05-hum-languages/ (NEW — per The 5 examples) +│ └── keystone-audit-report.{md,json} (01 only — self-audit) +test/ +├── examples.test.mjs (NEW — Tier-3 regression, render-guarded) +└── compare/ + ├── README.md (NEW — the 5b protocol: fairness methodology) + ├── briefs/ (NEW — 8 verbatim prompts from Hallmark _tests, MIT) + │ └── 0N-.md × 8 + ├── run-comparison.mjs (NEW — candidates → render → score → gallery entries) + └── gallery/ + └── index.template.html (NEW — side-by-side template; real runs land in 5b) +.github/workflows/ +├── ci.yml (NEW — typecheck + all suites incl. render + examples) +└── release.yml (NEW — org convention: v* tag → npm publish) +README.md (REWRITE — public launch) +package.json (MOD — files += NOTICE, scripts.typecheck, pin playwright-core) +pnpm-lock.yaml (MOD — devDep playwright for CI browser install) +.gitignore (MOD — examples evidence negations) +.gitignore'd: final-branch (DELETE — stray untracked scratch diff) +docs/superpowers/plans/2026-08-10-keystone-examples-publish-5a.md (this plan) +``` + +## Tasks + +### Task 1 — Branch + hygiene + +- [ ] `rm final-branch` (untracked scratch; must not survive into a public repo). +- [ ] `git checkout -b feat/examples-publish-5` (off master `1c175ce`). +- [ ] `.gitignore` — append: + ```gitignore + # examples: committed evidence (reports + screenshots); raw data stays local + !examples/**/keystone-report.json + !examples/**/keystone-report.html + !examples/**/*.png + !examples/**/keystone-audit-report.json + !examples/**/keystone-audit-report.md + examples/**/keystone-render/computed.json + examples/**/keystone-render/dom.html + examples/**/keystone-render/viewports.json + examples/**/keystone-render/clickable.json + examples/**/keystone-audit/ + ``` + (negations after `*.png` / `keystone-report.*` — valid because no parent dir is excluded). +- [ ] Commit: `chore: repo hygiene for the launch branch`. + +### Tasks 2–6 — Examples 01–05 (one commit each) + +Per-example steps (full detail in "The 5 examples" above): + +- [ ] Read the theme spec (`skills/keystone/references/themes/.md`) + macrostructures catalog; state Step-1/2 picks in `brief.md`. +- [ ] Author `tokens.css`, `style.css`, `index.html` (production-grade: real copy in theme voice, full interaction states, responsive at all 5 viewports, semantic HTML). +- [ ] Run the deterministic loop to 48/48 (cap 3). Record iterations in `brief.md`. +- [ ] Run the vision pass; record the 18 verdicts + S1 in `brief.md` (cap 2). +- [ ] Stamp + log (7.4). +- [ ] Verify committed-evidence list (above); `git add` the example dir; commit `feat(examples): 0N- — engine-verified build`. +- [ ] **Example 01 extra:** after 48/48, run `node engine/audit.mjs examples/01-cobalt-observability --out examples/01-cobalt-observability` → commit `keystone-audit-report.{md,json}` (expect ≤4 N/A: G8/G20/G21/G32, and all 44 audited gates PASS). Include the audit command + output summary in `brief.md`. + +### Task 7 — `examples/README.md` index + +- [ ] Table: dir · theme · genre · macro · engine score · vision S1 · links (report.html, screenshots). +- [ ] One paragraph: what "engine-verified" means + how to re-run the engine on any example. +- [ ] Commit with Task 6's commit or standalone `docs(examples): index`. + +### Task 8 — Example regression test (`test/examples.test.mjs`) + +Tier-3 per spec §8. Env-guarded like the other render tests. + +- [ ] Write the test: + ```js + import { test } from "node:test" + import assert from "node:assert/strict" + import { readdirSync, existsSync, mkdtempSync } from "node:fs" + import { tmpdir } from "node:os" + import { join } from "node:path" + import { execFileSync } from "node:child_process" + + const ROOT = join(import.meta.dirname, "..", "examples") + const slugs = readdirSync(ROOT).filter(d => /^\d{2}-/.test(d)).sort() + + test("all five examples exist", () => { + assert.equal(slugs.length, 5) + }) + + test("each example scores >= 41/48 on the engine (with render)", { skip: !process.env.KEYSTONE_RENDER_TEST }, () => { + for (const slug of slugs) { + const dir = join(ROOT, slug) + assert.ok(existsSync(join(dir, "index.html")), `${slug}: index.html missing`) + assert.ok(existsSync(join(dir, "style.css")), `${slug}: style.css missing`) + const out = mkdtempSync(join(tmpdir(), `keystone-ex-${slug}-`)) + execFileSync("node", ["engine/check-gates.mjs", + "--html", join(dir, "index.html"), "--css", join(dir, "style.css"), + "--render", "--viewports", "1280,375,320,414,768", "--out", out], { cwd: join(import.meta.dirname, "..") }) + const report = JSON.parse(readFileSync(join(out, "keystone-report.json"), "utf8")) + assert.ok(report.pass >= 41, `${slug}: ${report.pass}/${report.total} below the 41/48 floor`) + } + }) + + test("at least three examples score 48/48", { skip: !process.env.KEYSTONE_RENDER_TEST }, () => { + let perfect = 0 + /* ... same loop, count report.pass === 48 ... */ + assert.ok(perfect >= 3, `only ${perfect}/5 examples at 48/48 (need 3)`) + }) + ``` + (write it with the loop factored into a helper `score(slug)` returning the report — no duplication.) +- [ ] Run: `KEYSTONE_RENDER_TEST=1 node --test test/examples.test.mjs` → PASS. +- [ ] `package.json` scripts: `"test:examples"` + fold examples into `test`/`test:run`? **No** — keep `npm test` render-free (release + fast loop); CI runs the suites explicitly. Add `"test:examples": "node --test test/examples.test.mjs"`. +- [ ] Commit: `test(examples): Tier-3 regression — floor 41/48, three at 48/48`. + +### Task 9 — Harness tooling (unexecuted) — `test/compare/` + +- [ ] Fetch the verbatim prompts for the selected 8 of Hallmark's 13 `_tests` (MIT — raw.githubusercontent `Nutlope/hallmark/main/site/_tests//brief.md`, prompt is the first blockquote under "The prompt (verbatim…)"): **01-tide-podcast, 02-streampipe-cli, 03-maple-bakery, 04-meridian-manifesto, 05-tracejam-saas, 06-anya-portfolio, 07-foundry-compliance, 08-cohort-courses** (spans consumer/dev/local/editorial/B2B/personal/enterprise/education; 09–13 are near-duplicate domains). Each `briefs/0N-.md`: attribution header (source URL + MIT + NOTICE cross-ref) + the verbatim prompt. +- [ ] `run-comparison.mjs` — CLI: `node test/compare/run-comparison.mjs --candidates --out `; per `/hallmark|keystone` candidate dir (`index.html` + `style.css` + optional `tokens.css`): render at 5 viewports (via `extensions/render.ts` `render()` through tsx), score via `orchestrate` on the render dump, write `gallery//{.score.json, -.png}` + a combined `verdict.json`. **No model invocation inside** — candidates are pre-built inputs (5b's protocol builds them). +- [ ] `gallery/index.template.html` — side-by-side index (brief → Hallmark | Keystone · scores · screenshots · links to raw reports) reading the generated JSONs; static, no build step. +- [ ] `test/compare/README.md` — the 5b protocol verbatim from spec §8: same brief verbatim, same model both sides, no human intervention, our render + our engine score both, vision S1 same prompt, **losses published** (honesty clause). Candidate-dir contract documented. +- [ ] Smoke: build one throwaway candidate pair in `/tmp` (can be two copies of an example), run `run-comparison.mjs` end-to-end, confirm gallery JSONs + PNGs. (Smoke artifacts NOT committed.) +- [ ] Commit: `feat(compare): harness tooling — 8 briefs, runner, gallery template (execution = Plan 5b)`. + +### Task 10 — CI + release workflows + +- [ ] `package.json`: pin `"playwright-core": "1.62.0"` (exact — browser-revision determinism), add devDep `"playwright": "1.62.0"` (CI browser install), `"files" += "NOTICE"`, `"scripts": { "typecheck": "tsc --noEmit", "test:examples": …, "test:lint": "node --test test/lint-skill.mjs", "test:render": "node --import tsx --test test/extensions/render.test.mjs" }`. `pnpm install` to sync the lockfile. Verify `pnpm typecheck` green. +- [ ] `.github/workflows/ci.yml`: + ```yaml + name: CI + on: + push: { branches: [master] } + pull_request: + jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: { node-version: "24", cache: pnpm } + - run: pnpm install --frozen-lockfile --ignore-scripts + - run: pnpm exec playwright install --with-deps chromium + - run: pnpm typecheck + - run: pnpm test # engine + gates (162) + - run: pnpm test:lint # structural lint (103) + - run: pnpm test:render # render extension (4) + - run: pnpm test:examples # Tier-3 regression (needs Chromium) + env: { KEYSTONE_RENDER_TEST: "1" } + - run: KEYSTONE_RENDER_TEST=1 node --test test/engine/check-gates.test.mjs # 3 env-guarded CLI tests + ``` +- [ ] `.github/workflows/release.yml` — armory-todo org pattern adapted: tag `v*` + workflow_dispatch → pnpm frozen install → typecheck → `pnpm test` + `test:lint` (render suites intentionally omitted: the tagged commit already passed CI with render; keeps publish browser-download-free) → skip-if-already-published → `npm publish --access public` (`NODE_AUTH_TOKEN: secrets.NPM_TOKEN`) → `gh release create "$tag" --generate-notes`. +- [ ] Verify CI locally as far as possible: `pnpm typecheck && pnpm test && pnpm test:lint && pnpm test:render && KEYSTONE_RENDER_TEST=1 pnpm test:examples`. +- [ ] Commit: `ci: github actions — typecheck + full suites + org-convention release`. + +### Task 11 — Launch README + +- [ ] Rewrite `README.md` (public-facing): the thesis (Hallmark's gates are prose — Keystone's are executable; Hallmark can't ask "does this look AI-generated?" because its model never sees the page); What-you-get (build verb 7-step flow + audit verb); quickstart (`pi install npm:@getpipher/keystone` + standalone `node engine/audit.mjs `); the 48-gate engine table summary (46 detectors, tiers); **examples table linking committed evidence**; the comparison story (methodology + honesty clause) with "gallery: Plan 5b" marker; attribution (Hallmark taxonomy origin, MIT + NOTICE); license. +- [ ] Update `skills/keystone/references/engine.md` / READMEs only where they claim "private until Plan 5" (grep `PRIVATE`/`Plan 5`). +- [ ] Commit: `docs: public launch README`. + +### Task 12 — Review, merge, launch + +- [ ] Full local gate: all suites green (engine 162+1skip · lint 103 · render 4 · examples 5/5-floor · typecheck). +- [ ] Dispatch one review subagent (whole-diff, SCOPE GUARD: review only). Controller verifies every finding. +- [ ] Push → PR #8 → merge `--merge --delete-branch`. +- [ ] `gh repo edit getpipher/keystone --visibility public` (accept) — **after** merge. +- [ ] `git tag v0.1.0 && git push origin v0.1.0` → watch release.yml → confirm npm `@getpipher/keystone@0.1.0` + GitHub Release. +- [ ] Verify install: `npm view @getpipher/keystone` + (informational) `pi install npm:@getpipher/keystone` in a scratch dir. +- [ ] Update `~/Documents/secret/strategy/keystone/session-handoff-2026-08-10.md`: 5a row DONE (PR #8), launch facts (public URL, npm package), 5b = harness execution. + +## Acceptance criteria + +1. Five examples in `examples/`, each ≥41/48 engine-verified, ≥3 at 48/48, vision verdicts recorded, evidence committed (report.html openable, 5 PNGs each). +2. `keystone audit` self-audit report committed for 01 (44 audited gates PASS, 4 N/A). +3. `KEYSTONE_RENDER_TEST=1 pnpm test:examples` green locally + in CI. +4. Harness tooling present with 8 attributed briefs; runner smoke-tested on a throwaway candidate pair; nothing executed into `gallery/`. +5. CI green on the PR (typecheck + 162 + 103 + 4 + examples). Release workflow fires on `v0.1.0` and publishes. +6. Repo PUBLIC with launch README; `@getpipher/keystone@0.1.0` on npm; GitHub Release created. +7. No engine/detector/theme-spec changes (frozen); no AI attribution anywhere. + +## Out of scope (5b) + +Executing the harness (8 Hallmark + 8 Keystone builds), gallery content, verdict pages, 1.0.0, Pages hosting, any engine changes. diff --git a/engine/color.d.mts b/engine/color.d.mts new file mode 100644 index 0000000..f81c516 --- /dev/null +++ b/engine/color.d.mts @@ -0,0 +1,24 @@ +// Type declarations for engine/color.mjs (consumed by extensions/render.ts under +// strict tsc; the implementation is plain JS and stays the source of truth). + +export declare function parseRgb( + str: string, +): { r: number; g: number; b: number } | null + +export declare function rgbToOklch( + r255: number, + g255: number, + b255: number, +): { L: number; C: number; H: number } + +export declare function lightnessOf(str: string): number | null + +export declare function toOklchString(str: string): string | null + +export declare function parseColor( + str: string, +): + | { kind: "oklch"; L: number | null; C: number | null; H: number | null } + | { kind: "rgb"; r: number; g: number; b: number; L: number | null } + | { kind: "hex"; r: number; g: number; b: number; L: number | null } + | null diff --git a/engine/keystone.mjs b/engine/keystone.mjs new file mode 100644 index 0000000..fb15898 --- /dev/null +++ b/engine/keystone.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node +// `keystone` CLI dispatcher (the standalone entry point installed by npm). +// keystone audit [flags] → engine/audit.mjs (read-only punch list) +// keystone check [flags] → engine/check-gates.mjs (the build-flow gates) +// `build` is a skill verb (the model follows SKILL.md), not a CLI. +import { spawnSync } from "node:child_process" +import { dirname, join } from "node:path" +import { fileURLToPath } from "node:url" + +const here = dirname(fileURLToPath(import.meta.url)) +const verb = process.argv[2] +const rest = process.argv.slice(3) + +const scripts = { + audit: "audit.mjs", + check: "check-gates.mjs", + gates: "check-gates.mjs", +} + +const script = scripts[verb] +if (!script) { + console.error(`usage: keystone audit [--no-render] [--allow-private] [--out ] + keystone check [--gates] --html --css [--render] [--viewports 1280,375,320,414,768] [--log .keystone/log.json] [--out ]`) + process.exit(1) +} + +const r = spawnSync(process.execPath, [join(here, script), ...rest], { stdio: "inherit" }) +if (r.error) { + console.error("keystone: failed to launch the engine:", r.error.message) + process.exit(1) +} +process.exit(r.status ?? 1) diff --git a/examples/01-cobalt-observability/brief.md b/examples/01-cobalt-observability/brief.md new file mode 100644 index 0000000..03911b1 --- /dev/null +++ b/examples/01-cobalt-observability/brief.md @@ -0,0 +1,129 @@ +# Example 01 · Kestrel — distributed tracing (Cobalt · Split Studio) + +One of the five Keystone showcase builds. Fictional product, real gates: this +page was emitted through the full Build flow (SKILL.md Steps 1–7) and +engine-verified with 46 deterministic detectors at 5 viewports. + +## The brief + +> Landing page for Kestrel, a distributed tracing tool for platform and SRE +> teams. Audience: engineers who read traces to debug production. Use: adopt +> the tool — install the SDK, start a free trace. Tone: technical, austere, +> precise. Positioning: "traces, not dashboards." + +## Step 1 · Design-context gate + +- **Audience** — platform/SRE engineers; fluent in OpenTelemetry, p99s, span waterfalls. +- **Use** — one action: start tracing (install command → free tier). +- **Tone** — utilitarian to the point of austerity. Code is content. No hype. + +**Genre:** modern-minimal (dev tool signal) → `references/genres/modern-minimal.md`. + +## Step 2 · Structure picks + +- **Macrostructure: Split Studio** (15) — "dev tools that pair explanation + with code" is the macro's own reach-for line. Every claim sits beside its + proof: install command, span waterfall, spec sheet. +- **Nav: N1b** (canonical SaaS three-section, variant: 3 centre links + + sign-in + filled CTA — outside the G42 4–5-link fingerprint window). +- **Footer: Ft7** (newsletter-first — release-notes signup; muted links beneath). +- **Theme: Cobalt** (light · grotesk-sans · cool) — the modern-minimal anchor. +- **Diversification:** first build in this project — no prior log; log.json + seeded at Step 6. + +## Step 4 · Enrichment + +None — typography only. The "screenshot" is the product itself: a +hand-set span waterfall and an install command, set in Geist Mono. Per the +Cobalt spec, a syntax-highlighted block is the hero, not a picture of one. + +## Step 5 · Preview + +**Keystone · v0.1.0** + +- **Macrostructure** · Split Studio +- **Theme** · Cobalt (light · cool · grotesk-sans) +- **Enrichment** · none (typography only) +- **Sections** · nav · hero diptych · install split · cause split · spec sheet · pricing table · FAQ · CTA strip · footer +- **Motion** · hero entrance fade-up (staggered, transform+opacity) · color transitions on states +- **Slop test** · pending — engine runs at Step 7 + +## Step 7 · The slop test (engine-verified) + +### 7.1 Deterministic loop + +```bash +node engine/check-gates.mjs --html index.html --css style.css \ + --log .keystone/log.json --render --viewports 1280,375,320,414,768 --out . +``` + +- **Emit 1** — 39/52 rows clean · fails: G13 (hover changed 3 property groups), G23 (accent + covered 9.5%), G26 (.btn-ghost missing its state quartet), G40 (22 muted/accent-text pairs + under Lc 60), G42 (nav had 4 inline links + button — inside the fingerprint window), G47 + (`.code-block` class name matches the fake-chrome regex), G49 (buttons at `line-height: 1` + read as 2+ lines), G51 (per-rule wrap checks on hero/clamp/text-xl rules). +- **Emit 2** — 45/52 · fails: G40 (4 pairs), G51 (a third `.hero` rule inside `@media`). + Fixes applied: hover effects reduced to one property group; `.btn-ghost` quartet; nav + reduced to 3 centre links; class renamed `.snippet`; buttons rebuilt as 34px controls + (`line-height: 24px` + 4px block padding); muted text lifted to a dedicated + `--color-ink-2` token; accent split into `--color-accent` (borders, focus, the slow-span + band) + `--color-accent-deep` (accent text + filled controls) so every text pair clears + Lc 60; wrap props added to every display-ish rule. +- **Emit 3** — 46/47 · fail: G40 (1 pair). The waterfall bars are textless but carry an + inherited ink `color` — set `color: transparent` on the empty bars; the slow bar moved to + `--color-accent-deep` (nothing clears Lc 60 on an L=55 background in the engine's + approximation — and the deep signal matches the accent language anyway). +- **Emit 4** — **47/47 rows · 0 fails — every gate number passes.** +- Deterministic iterations used: 3 of 3 (initial emit + 3 fix rounds). + +### 7.2 Vision pass (18 questions · 1280 + 375 screenshots) + +| Gate | Verdict | Evidence | +|---|---|---| +| G6 hero centred-everything | PASS | hero is a left-anchored diptych; CTA row sits under the lede, waterfall occupies the right half | +| G9 equal-whitespace sections | PASS | alternating split rows, then full-width table band, then FAQ, then CTA — four distinct rhythms | +| G29 abstract background | PASS | flat near-white paper; no gradients or meshes | +| G42 nav fingerprint | PASS | 3 centre links + two right-side actions, no hairline rule — not the 4–5-link + border default | +| G43 footer fingerprint | PASS | form-first single band + one muted link row; no 4-column link grid | +| G44 hero fit | PASS | engine-verified (bounding rects within 800px at 1280) and visually confirmed | +| G45 decorative-without-purpose | PASS | every visual element is the product (waterfall, spec table); no ornaments | +| G38a italic headers | PASS | all headings roman; emphasis via weight and one accent word | +| G30 icon tells | PASS | zero icons, zero emoji — type only | +| G46 invented metrics | FLAG (accepted) | p99/retention/regions/prices are the fictional product's own spec, stated consistently; no social-proof fabrication (no customer counts, logos, testimonials, uptime claims). The theme spec's voice fixtures use the same device. | +| G47 re-drawn chrome | PASS | code blocks are bare `
` with a hairline rule — no window dots, no fake frames |
+| G35 decorative stroke position | PASS | the slow-span highlight uses the behind-the-x-height gradient band (38%→92%), not a baseline fat underline |
+| G36 flex align-items | PASS | nav, form row, and split rows use `align-items: center` + `line-height: 1` on controls |
+| S1 looks AI-generated? | NO (0.12) | single-accent discipline, tabular spec sheet, asymmetric diptychs, real code content; closest tell would be the pricing table, which is a table not a card trio |
+| S2 feels like this brief? | YES | reads as an infra tool page: austere, numeric, code-forward |
+| S3 would two Keystone pages feel like different sites? | Judged across the set — see examples/README.md |
+
+- Vision iterations used: 0 of 2 (clean first pass). Verdicts recorded from the committed
+  `keystone-render/screenshot-{1280,375}.png` plus a full-page capture (uncommitted) used
+  to judge sections + footer beyond the 800px fold.
+
+### 7.3 Resolution
+
+**Keystone · v0.1.0**
+
+- **Slop test · 48/48 ✓ (engine-verified) — ./keystone-report.html**
+
+### 7.4 Stamp + log
+
+Stamp filled in `style.css` line 1; `.keystone/log.json` seeded (gitignored).
+
+## Self-audit
+
+`keystone audit` run on this directory after the build — see
+`keystone-audit-report.md` (43 audited gates · 4 N/A). Audit mode re-derives
+everything from the committed files, which is the point: the evidence in this
+directory is the whole input.
+
+## Re-run it yourself
+
+```bash
+node engine/check-gates.mjs \
+  --html examples/01-cobalt-observability/index.html \
+  --css examples/01-cobalt-observability/style.css \
+  --render --viewports 1280,375,320,414,768 \
+  --out /tmp/kestrel-report
+```
diff --git a/examples/01-cobalt-observability/index.html b/examples/01-cobalt-observability/index.html
new file mode 100644
index 0000000..bfbc559
--- /dev/null
+++ b/examples/01-cobalt-observability/index.html
@@ -0,0 +1,200 @@
+
+
+
+  
+  
+  Kestrel · Traces, not dashboards
+  
+  
+  
+  
+  
+
+
+  
+
+  
+ +
+ +
+ +
+
+

v2.4 · OTel-native · status: stable

+

Traces, not dashboards.

+

Kestrel instruments your services and returns readable traces: every span, every cause, one query language you already know. It answers “what was slow?” with the fix, not a chart.

+ +
+
Query p99
40ms
+
Retention
30days
+
Regions
14
+
+
+
+ +

trace 4f2a · checkout · slow span: postgres.query · missing index: orders(customer_id)

+
+
+ + +
+
+

Install

+

One SDK. Three runtimes. No agent.

+

Kestrel ships as an OpenTelemetry exporter, so the instrumentation you already run is the instrumentation Kestrel reads. Point the SDK at a collector and the first trace lands in seconds.

+ Installation guide +
+
+
# node · python · go — same collector, same traces
+npm i @kestrel/otel
+pip install kestrel-otel
+go get kestrel.dev/otel
+# env
+OTEL_EXPORTER_OTLP_ENDPOINT=https://collector.kestrel.dev
+
+
+ + +
+
+

Cause, not symptom

+

The slow span names its suspect.

+

Kestrel reads flame order, wait reasons, and query plans together, so a 298ms span arrives with its missing index attached. You debug from the trace, not from three dashboards and a hunch.

+ How analysis works +
+
+
// span detail · postgres.query · 298ms
+wait: Seq Scan on orders
+rows: 1.2M scanned, 4 returned
+suggest: CREATE INDEX ON orders (customer_id)
+est. saving: 291ms / request
+
+
+ + +
+
+
+

Spec sheet

+

The whole system, in numbers.

+
+ + + + + + + + + + + + +
Kestrel managed service · plan-independent figures
DimensionValueNotes
Query p9940ms14-day rolling, all regions
IngestOTLP/HTTP, OTLP/gRPCOpenTelemetry-native; Zipkin & Jaeger ingest included
Retentionup to 30 daysper-plan; archive to your own bucket on Team
Tail samplingyeslatency-, error-, and rule-based
Self-hostsingle binaryApache-2.0, no phone-home
+
+
+ + +
+
+

Pricing

+

Two plans. The price is on the page.

+

Hobby is free forever for personal projects. Team adds tail sampling, longer retention, and a 24-hour support lane. Self-hosting is free and feature-complete.

+
+
+ + + + + + + + + + + + +
Per-workspace pricing
HobbyTeam
Price$0$29 / seat / mo
Traces / mo1M50M
Retention7 days30 days
Tail samplingincluded
Supportcommunity24-hour lane
+
+
+ + +
+
+
+

FAQ

+

Asked, answered.

+
+
+
+

Does it speak OpenTelemetry?

+

Only OpenTelemetry. Kestrel accepts OTLP natively and reads the semantic conventions without translation, so there is no vendor SDK to rip out later.

+
+
+

Can we self-host?

+

Yes. The single binary is Apache-2.0 and feature-complete, including tail sampling. The managed service exists because operating trace storage at 50M spans a month is a job, not a side project.

+
+
+

What does it cost to try?

+

Nothing. The Hobby plan is free with no trial clock, and the install command above is the whole onboarding.

+
+
+

Where do traces live?

+

In the region you pick at workspace creation, on disk you own if you self-host. Trace data never leaves the region; metadata replication is documented in the security note.

+
+
+
+
+ + +
+

Instrument the first service before lunch.

+

Create a workspace, point your SDK at the collector, and the first readable trace lands before your coffee cools.

+ Start tracing free +
+
+ + +
+ + +
+ + diff --git a/examples/01-cobalt-observability/keystone-audit-report.json b/examples/01-cobalt-observability/keystone-audit-report.json new file mode 100644 index 0000000..28be73e --- /dev/null +++ b/examples/01-cobalt-observability/keystone-audit-report.json @@ -0,0 +1,247 @@ +{ + "target": "/Users/rector/local-dev/getpipher/keystone/examples/01-cobalt-observability/index.html", + "timestamp": "2026-08-29T05:11:11.772Z", + "pass": 43, + "fail": 0, + "total": 43, + "excluded": [ + { + "gate": 8, + "name": "Diversification (macro reuse)", + "reason": "meaningless on external code — no Keystone log" + }, + { + "gate": 20, + "name": "Missing CSS stamp", + "reason": "external code has no Keystone stamp" + }, + { + "gate": 21, + "name": "Specimen fall-through", + "reason": "depends on the Keystone stamp" + }, + { + "gate": 32, + "name": "Diversification (theme/nav/footer reuse)", + "reason": "meaningless on external code — no Keystone log" + } + ], + "warnings": [], + "results": [ + { + "gate": 1, + "name": "Banned display fonts", + "pass": true + }, + { + "gate": 2, + "name": "Gradient text", + "pass": true + }, + { + "gate": 3, + "name": "3-equal-col card grid", + "pass": true + }, + { + "gate": 4, + "name": "Nested cards", + "pass": true + }, + { + "gate": 5, + "name": "Card side-stripe border", + "pass": true + }, + { + "gate": 6, + "name": "Hero centred-everything", + "pass": true + }, + { + "gate": 7, + "name": "Pure black/white base", + "pass": true + }, + { + "gate": 10, + "name": "`transition: all`", + "pass": true + }, + { + "gate": 11, + "name": "Uniform hover-scale", + "pass": true + }, + { + "gate": 12, + "name": "Bouncy/overshoot easing on UI", + "pass": true + }, + { + "gate": 13, + "name": "Multiple simultaneous hover effects", + "pass": true + }, + { + "gate": 14, + "name": "Animating layout properties", + "pass": true + }, + { + "gate": 15, + "name": "Focus ring fades in", + "pass": true + }, + { + "gate": 17, + "name": "Tooltip hover-delay = focus-delay", + "pass": true + }, + { + "gate": 18, + "name": "Auto-rotating content without pause", + "pass": true + }, + { + "gate": 19, + "name": "Placeholder names / startup clichés", + "pass": true + }, + { + "gate": 22, + "name": "Zero-chroma neutral", + "pass": true + }, + { + "gate": 23, + "name": "Accent >5% viewport", + "pass": true + }, + { + "gate": 24, + "name": "Off-scale spacing", + "pass": true + }, + { + "gate": 25, + "name": "Prose max-width outside 45–75ch", + "pass": true + }, + { + "gate": 26, + "name": "Missing interaction states", + "pass": true + }, + { + "gate": 27, + "name": "Motion without reduced-motion fallback", + "pass": true + }, + { + "gate": 28, + "name": "LCP-killing demo video", + "pass": true + }, + { + "gate": 31, + "name": "Lottie as default", + "pass": true + }, + { + "gate": 33, + "name": "Decorative element without aria", + "pass": true + }, + { + "gate": 34, + "name": "Horizontal scroll", + "pass": true + }, + { + "gate": 35, + "name": "Decorative text-effect position", + "pass": true + }, + { + "gate": 37, + "name": "More than three font families", + "pass": true + }, + { + "gate": 38, + "name": "Outlier face in >2 slots", + "pass": true + }, + { + "gate": 39, + "name": "Input field states", + "pass": true + }, + { + "gate": 40, + "name": "Contrast", + "pass": true + }, + { + "gate": 42, + "name": "Nav fingerprint", + "pass": true + }, + { + "gate": 44, + "name": "Hero fit", + "pass": true + }, + { + "gate": 47, + "name": "Re-drawn UI chrome", + "pass": true + }, + { + "gate": 48, + "name": "Token improvisation", + "pass": true + }, + { + "gate": 49, + "name": "Two-line clickable text", + "pass": true + }, + { + "gate": 50, + "name": "Image grid track minmax", + "pass": true + }, + { + "gate": 51, + "name": "Display headers without long-word wrap", + "pass": true + }, + { + "gate": 52, + "name": "Section-head without mobile collapse", + "pass": true + }, + { + "gate": 53, + "name": "CSS-only radio tabs that scroll-jump", + "pass": true + }, + { + "gate": 54, + "name": "Tag-left/heading-right", + "pass": true + }, + { + "gate": 55, + "name": "All-caps display with line-height < 1.0", + "pass": true + }, + { + "gate": 56, + "name": "Sticky top:0 below a sticky nav", + "pass": true + } + ] +} \ No newline at end of file diff --git a/examples/01-cobalt-observability/keystone-audit-report.md b/examples/01-cobalt-observability/keystone-audit-report.md new file mode 100644 index 0000000..b32271a --- /dev/null +++ b/examples/01-cobalt-observability/keystone-audit-report.md @@ -0,0 +1,27 @@ +Keystone · audit report · /Users/rector/local-dev/getpipher/keystone/examples/01-cobalt-observability/index.html · 2026-08-29T05:11:11.771Z +43 gates run · 43 PASS · 0 FAIL · 4 N/A + +RANKED PUNCH LIST (highest-impact first) + +TIER 4 · SUBJECTIVE (vision-verdict, confidence-weighted, never auto-fail alone) + run `describe_image` on the screenshots with the 18-question prompt (gates.md § The vision pass), + then append rows here: S1-S3, G38a italic-headers, G46 invented-metrics. + +N/A (4): G8 Diversification (macro reuse), G20 Missing CSS stamp, G21 Specimen fall-through, G32 Diversification (theme/nav/footer reuse) + +SCREENSHOTS: [1280] [375] [320] [414] [768] +RAW DATA: ./keystone-audit/{computed.json, dom.html, viewports.json, clickable.json} + +## Tier 4 · vision rows (appended per the audit protocol — describe_image equivalent on the committed screenshots) + +| Gate | Verdict | Evidence | +|---|---|---| +| S1 | "Looks AI-generated?" — **NO** (0.12) | single-accent discipline, tabular spec sheets, asymmetric diptychs, real code content; no gradient/emoji/chrome tells | +| S2 | Feels like this brief? | **YES** — austere, numeric, code-forward infra register | +| S3 | Two Keystone pages, different sites? | judged across the example set — see examples/README.md | +| G38a | Italic headers | **PASS** — all headings roman | +| G46 | Invented metrics | **FLAG (accepted)** — p99/retention/regions/prices are the fictional product's own consistent spec; no social-proof fabrication (no customer counts, logos, testimonials, uptime claims) | + +> Note: the raw audit data (`keystone-audit/` — computed pairs, DOM snapshot, +> per-viewport clickables) is intentionally not committed; regenerate it with +> `node engine/keystone.mjs audit examples/01-cobalt-observability --out .` diff --git a/examples/01-cobalt-observability/keystone-render/screenshot-1280.png b/examples/01-cobalt-observability/keystone-render/screenshot-1280.png new file mode 100644 index 0000000..8821962 Binary files /dev/null and b/examples/01-cobalt-observability/keystone-render/screenshot-1280.png differ diff --git a/examples/01-cobalt-observability/keystone-render/screenshot-320.png b/examples/01-cobalt-observability/keystone-render/screenshot-320.png new file mode 100644 index 0000000..121b2fc Binary files /dev/null and b/examples/01-cobalt-observability/keystone-render/screenshot-320.png differ diff --git a/examples/01-cobalt-observability/keystone-render/screenshot-375.png b/examples/01-cobalt-observability/keystone-render/screenshot-375.png new file mode 100644 index 0000000..d8656b3 Binary files /dev/null and b/examples/01-cobalt-observability/keystone-render/screenshot-375.png differ diff --git a/examples/01-cobalt-observability/keystone-render/screenshot-414.png b/examples/01-cobalt-observability/keystone-render/screenshot-414.png new file mode 100644 index 0000000..255a161 Binary files /dev/null and b/examples/01-cobalt-observability/keystone-render/screenshot-414.png differ diff --git a/examples/01-cobalt-observability/keystone-render/screenshot-768.png b/examples/01-cobalt-observability/keystone-render/screenshot-768.png new file mode 100644 index 0000000..96b1162 Binary files /dev/null and b/examples/01-cobalt-observability/keystone-render/screenshot-768.png differ diff --git a/examples/01-cobalt-observability/keystone-report.html b/examples/01-cobalt-observability/keystone-report.html new file mode 100644 index 0000000..296564b --- /dev/null +++ b/examples/01-cobalt-observability/keystone-report.html @@ -0,0 +1,68 @@ + + +Keystone gate report + +

Keystone · gate report · 2026-08-29T05:09:16.540Z

+
PASS 47 / 47 · FAIL 0 / {{total}}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#GateResultEvidenceFixFile:Line
1Banned display fonts
2Gradient text
33-equal-col card grid
4Nested cards
5Card side-stripe border
6Hero centred-everything
7Pure black/white base
10`transition: all`
11Uniform hover-scale
12Bouncy/overshoot easing on UI
13Multiple simultaneous hover effects
14Animating layout properties
15Focus ring fades in
17Tooltip hover-delay = focus-delay
18Auto-rotating content without pause
19Placeholder names / startup clichés
20Missing CSS stamp
21Specimen fall-through
22Zero-chroma neutral
23Accent >5% viewport
24Off-scale spacing
25Prose max-width outside 45–75ch
26Missing interaction states
27Motion without reduced-motion fallback
28LCP-killing demo video
31Lottie as default
33Decorative element without aria
34Horizontal scroll
35Decorative text-effect position
37More than three font families
38Outlier face in >2 slots
39Input field states
40Contrast
42Nav fingerprint
44Hero fit
47Re-drawn UI chrome
48Token improvisation
49Two-line clickable text
50Image grid track minmax
51Display headers without long-word wrap
52Section-head without mobile collapse
53CSS-only radio tabs that scroll-jump
54Tag-left/heading-right
55All-caps display with line-height < 1.0
56Sticky top:0 below a sticky nav
8Macrostructure reuse
32Nav/footer rotation
+ \ No newline at end of file diff --git a/examples/01-cobalt-observability/keystone-report.json b/examples/01-cobalt-observability/keystone-report.json new file mode 100644 index 0000000..5e36b4c --- /dev/null +++ b/examples/01-cobalt-observability/keystone-report.json @@ -0,0 +1,242 @@ +{ + "results": [ + { + "gate": 1, + "name": "Banned display fonts", + "pass": true + }, + { + "gate": 2, + "name": "Gradient text", + "pass": true + }, + { + "gate": 3, + "name": "3-equal-col card grid", + "pass": true + }, + { + "gate": 4, + "name": "Nested cards", + "pass": true + }, + { + "gate": 5, + "name": "Card side-stripe border", + "pass": true + }, + { + "gate": 6, + "name": "Hero centred-everything", + "pass": true + }, + { + "gate": 7, + "name": "Pure black/white base", + "pass": true + }, + { + "gate": 10, + "name": "`transition: all`", + "pass": true + }, + { + "gate": 11, + "name": "Uniform hover-scale", + "pass": true + }, + { + "gate": 12, + "name": "Bouncy/overshoot easing on UI", + "pass": true + }, + { + "gate": 13, + "name": "Multiple simultaneous hover effects", + "pass": true + }, + { + "gate": 14, + "name": "Animating layout properties", + "pass": true + }, + { + "gate": 15, + "name": "Focus ring fades in", + "pass": true + }, + { + "gate": 17, + "name": "Tooltip hover-delay = focus-delay", + "pass": true + }, + { + "gate": 18, + "name": "Auto-rotating content without pause", + "pass": true + }, + { + "gate": 19, + "name": "Placeholder names / startup clichés", + "pass": true + }, + { + "gate": 20, + "name": "Missing CSS stamp", + "pass": true + }, + { + "gate": 21, + "name": "Specimen fall-through", + "pass": true + }, + { + "gate": 22, + "name": "Zero-chroma neutral", + "pass": true + }, + { + "gate": 23, + "name": "Accent >5% viewport", + "pass": true + }, + { + "gate": 24, + "name": "Off-scale spacing", + "pass": true + }, + { + "gate": 25, + "name": "Prose max-width outside 45–75ch", + "pass": true + }, + { + "gate": 26, + "name": "Missing interaction states", + "pass": true + }, + { + "gate": 27, + "name": "Motion without reduced-motion fallback", + "pass": true + }, + { + "gate": 28, + "name": "LCP-killing demo video", + "pass": true + }, + { + "gate": 31, + "name": "Lottie as default", + "pass": true + }, + { + "gate": 33, + "name": "Decorative element without aria", + "pass": true + }, + { + "gate": 34, + "name": "Horizontal scroll", + "pass": true + }, + { + "gate": 35, + "name": "Decorative text-effect position", + "pass": true + }, + { + "gate": 37, + "name": "More than three font families", + "pass": true + }, + { + "gate": 38, + "name": "Outlier face in >2 slots", + "pass": true + }, + { + "gate": 39, + "name": "Input field states", + "pass": true + }, + { + "gate": 40, + "name": "Contrast", + "pass": true + }, + { + "gate": 42, + "name": "Nav fingerprint", + "pass": true + }, + { + "gate": 44, + "name": "Hero fit", + "pass": true + }, + { + "gate": 47, + "name": "Re-drawn UI chrome", + "pass": true + }, + { + "gate": 48, + "name": "Token improvisation", + "pass": true + }, + { + "gate": 49, + "name": "Two-line clickable text", + "pass": true + }, + { + "gate": 50, + "name": "Image grid track minmax", + "pass": true + }, + { + "gate": 51, + "name": "Display headers without long-word wrap", + "pass": true + }, + { + "gate": 52, + "name": "Section-head without mobile collapse", + "pass": true + }, + { + "gate": 53, + "name": "CSS-only radio tabs that scroll-jump", + "pass": true + }, + { + "gate": 54, + "name": "Tag-left/heading-right", + "pass": true + }, + { + "gate": 55, + "name": "All-caps display with line-height < 1.0", + "pass": true + }, + { + "gate": 56, + "name": "Sticky top:0 below a sticky nav", + "pass": true + }, + { + "gate": 8, + "name": "Macrostructure reuse", + "pass": true + }, + { + "gate": 32, + "name": "Nav/footer rotation", + "pass": true + } + ], + "pass": 47, + "fail": 0, + "total": 47 +} \ No newline at end of file diff --git a/examples/01-cobalt-observability/style.css b/examples/01-cobalt-observability/style.css new file mode 100644 index 0000000..a3613e0 --- /dev/null +++ b/examples/01-cobalt-observability/style.css @@ -0,0 +1,816 @@ +/* Keystone · macrostructure: Split Studio · tone: technical-austere · anchor hue: 255 · gates: 48/48 engine-verified */ +/* Keystone · pre-emit critique: P5 H5 E5 S5 R4 V4 */ + +/* — Cobalt tokens · modern-minimal · light · cool (exported to tokens.css) — + Two lightness steps of the one cobalt hue: --color-accent (55%) draws + borders/focus/the slow span; --color-accent-deep (42%) carries accent TEXT + and filled controls so every text pair clears APCA Lc 60 (G40-41). */ +:root { + --color-paper: oklch(99% 0.002 250); + --color-paper-2: oklch(96% 0.004 250); + --color-paper-3: oklch(92% 0.006 250); + --color-ink: oklch(25% 0.01 250); + --color-ink-2: oklch(30% 0.012 250); + --color-accent: oklch(55% 0.2 255); + --color-accent-deep: oklch(32% 0.19 255); + --color-accent-ink: oklch(99% 0.002 250); + --color-focus: oklch(55% 0.2 255); + --font-display: "Space Grotesk", sans-serif; + --font-body: "Geist", sans-serif; + --font-mono: "Geist Mono", monospace; + --space-4xs: 2px; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.375rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --shell: 72rem; + --gutter: clamp(16px, 4vw, 32px); +} + +/* — base — */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + overflow-x: clip; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--color-paper); + color: var(--color-ink); + font-family: var(--font-body); + font-size: var(--text-base); + line-height: 1.55; + -webkit-font-smoothing: antialiased; +} + +h1, +h2, +h3 { + font-family: var(--font-display); + font-weight: 600; + line-height: 1.08; + letter-spacing: -0.02em; + overflow-wrap: anywhere; + min-width: 0; + margin: 0; +} + +p { + margin: 0; +} + +a { + color: inherit; + text-decoration: none; + transition: color 140ms var(--ease-out), text-decoration-color 140ms var(--ease-out); +} + +a:hover { + color: var(--color-accent-deep); +} + +a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; + border-radius: 2px; +} + +a:active { + color: var(--color-accent-deep); +} + +a:disabled, +a[aria-disabled="true"] { + color: var(--color-ink-2); + pointer-events: none; + cursor: not-allowed; +} + +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.mono { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + +.skip-link { + position: absolute; + inset-inline-start: var(--gutter); + inset-block-start: calc(-1 * var(--space-3xl)); + background: var(--color-paper); + border: 1px solid var(--color-accent); + padding: var(--space-3xs) var(--space-s); + z-index: 10; +} + +.skip-link:focus-visible { + inset-block-start: var(--space-s); +} + +/* — layout shell — */ +.shell { + max-width: var(--shell); + margin-inline: auto; + padding-inline: var(--gutter); +} + +/* — nav · N1b three-section (3 centre links + CTA: outside the G42 window) — */ +.nav { + display: flex; + align-items: center; + gap: var(--space-l); + padding-block: var(--space-m); +} + +.nav-brand { + font-family: var(--font-display); + font-weight: 700; + font-size: var(--text-lg); + letter-spacing: -0.01em; +} + +.nav-links { + display: flex; + gap: var(--space-l); + margin-inline: auto; + list-style: none; + padding: 0; +} + +.nav-links a { + font-size: var(--text-sm); + line-height: 18px; + padding-block: var(--space-3xs); + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 4px; +} + +.nav-links a:hover { + text-decoration-color: var(--color-accent); +} + +.nav-actions { + display: flex; + align-items: center; + gap: var(--space-m); +} + +/* — buttons · 34px control height: line-height 24px + 2×4px padding + 2px border — */ +.btn { + display: inline-flex; + align-items: center; + gap: var(--space-2xs); + line-height: 24px; + font-family: var(--font-body); + font-size: var(--text-sm); + font-weight: 600; + padding: var(--space-3xs) var(--space-m); + border: 1px solid var(--color-accent-deep); + border-radius: 6px; + background-color: var(--color-accent-deep); + color: var(--color-accent-ink); + cursor: pointer; + white-space: nowrap; + transition: background-color 160ms var(--ease-out); +} + +.btn:hover { + background-color: color-mix(in oklch, var(--color-accent-deep) 88%, var(--color-ink)); +} + +.btn:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.btn:active { + background-color: color-mix(in oklch, var(--color-accent-deep) 76%, var(--color-ink)); +} + +.btn:disabled, +.btn[aria-disabled="true"] { + background-color: var(--color-paper-3); + border-color: var(--color-paper-3); + color: var(--color-ink-2); + cursor: not-allowed; + pointer-events: none; +} + +.btn-ghost { + background-color: transparent; + border-color: var(--color-accent); + color: var(--color-ink); +} + +.btn-ghost:hover { + background-color: color-mix(in oklch, var(--color-accent) 8%, var(--color-paper)); +} + +.btn-ghost:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.btn-ghost:active { + background-color: color-mix(in oklch, var(--color-accent) 14%, var(--color-paper)); +} + +.btn-ghost:disabled, +.btn-ghost[aria-disabled="true"] { + background-color: var(--color-paper-2); + border-color: var(--color-paper-3); + color: var(--color-ink-2); + cursor: not-allowed; + pointer-events: none; +} + +/* — hero diptych — */ +.hero { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--space-2xl); + align-items: center; + padding-block: var(--space-2xl) var(--space-3xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.hero-copy { + display: grid; + gap: var(--space-m); + justify-items: start; + overflow-wrap: anywhere; + min-width: 0; + animation: rise 560ms var(--ease-out) both; +} + +.eyebrow { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-accent-deep); +} + +.hero h1 { + font-size: clamp(2.4rem, 5vw, 4rem); + max-width: 14ch; + overflow-wrap: anywhere; + min-width: 0; +} + +.hero .hl { + color: var(--color-accent-deep); + overflow-wrap: anywhere; + min-width: 0; +} + +.lede { + max-width: 52ch; + font-size: var(--text-lg); + line-height: 1.5; + color: var(--color-ink-2); +} + +.hero-ctas { + display: flex; + align-items: center; + gap: var(--space-s); + margin-block-start: var(--space-2xs); + overflow-wrap: anywhere; + min-width: 0; +} + +.stats { + display: flex; + gap: var(--space-xl); + margin: 0; + border-top: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); + padding-block-start: var(--space-m); + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + +.stats div { + display: grid; + gap: var(--space-4xs); +} + +.stats dt { + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-ink-2); +} + +.stats dd { + margin: 0; + font-size: var(--text-base); + line-height: 1.3; + color: var(--color-ink); +} + +.stats b { + font-weight: 600; + font-size: var(--text-xl); + color: var(--color-ink); + overflow-wrap: anywhere; + min-width: 0; +} + +/* the proof half: a hand-set span waterfall */ +.hero-proof { + overflow-wrap: anywhere; + min-width: 0; + animation: rise 560ms var(--ease-out) 120ms both; +} + +.snippet { + margin: 0; + border: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); + border-radius: 8px; + background: var(--color-paper-2); + padding: var(--space-m); + font-family: var(--font-mono); + font-size: var(--text-sm); + line-height: 1.6; + overflow-x: auto; + white-space: pre; +} + +.snippet-caption { + margin-block-start: var(--space-2xs); + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + color: var(--color-ink-2); +} + +.waterfall { + display: grid; + gap: var(--space-2xs); + border: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); + border-radius: 8px; + background: var(--color-paper-2); + padding: var(--space-m); +} + +.span-row { + display: grid; + grid-template-columns: 9rem minmax(0, 1fr) 3.5rem; + align-items: center; + gap: var(--space-s); + font-size: var(--text-xs); + line-height: 16px; +} + +.span-name { + color: var(--color-ink-2); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.span-track { + display: block; + height: 12px; + background: var(--color-paper-3); + border-radius: 2px; + overflow: hidden; + /* empty decorative bars: drop the inherited ink so the textless color/bg pair stays clean (G40) */ + color: transparent; +} + +.span-bar { + display: block; + height: 100%; + background: color-mix(in oklch, var(--color-ink) 30%, var(--color-paper-2)); + border-radius: 2px; + color: transparent; +} + +/* bar widths = span duration as a share of the trace (412ms) */ +.bw-100 { width: 100%; } +.bw-72 { width: 72%; } +.bw-8 { width: 8%; } +.bw-4 { width: 4%; } +.bw-3 { width: 3%; } +.bw-2 { width: 2%; } + +.span-bar-slow { + background-color: var(--color-accent-deep); + color: var(--color-paper); + background-image: linear-gradient( + 180deg, + transparent 38%, + color-mix(in oklch, var(--color-accent) 78%, transparent) 38%, + color-mix(in oklch, var(--color-accent) 78%, transparent) 92%, + transparent 92% + ); +} + +.span-ms { + text-align: right; + color: var(--color-ink-2); +} + +.span-ms b { + font-weight: 600; + color: var(--color-accent-deep); +} + +/* syntax tones: one ink, weight and step carry the hierarchy */ +.tok-comment { + color: var(--color-ink-2); +} + +.tok-flag { + color: var(--color-ink-2); +} + +.tok-string { + color: var(--color-ink); +} + +.tok-cmd { + font-weight: 600; + color: var(--color-ink); +} + +/* — split rows — */ +.split { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: var(--space-2xl); + align-items: center; + padding-block: var(--space-2xl); +} + +.split-copy { + display: grid; + gap: var(--space-s); + justify-items: start; +} + +.split h2 { + font-size: clamp(1.6rem, 3vw, 2.2rem); + max-width: 20ch; + overflow-wrap: anywhere; + min-width: 0; +} + +.split p { + max-width: 55ch; + color: var(--color-ink-2); +} + +.split.reverse .split-copy { + order: 2; +} + +.split.reverse .split-proof { + order: 1; +} + +/* — full-width bands — */ +.band { + border-block: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); + background: var(--color-paper-2); + padding-block: var(--space-2xl); +} + +.band-head { + display: grid; + gap: var(--space-2xs); + margin-block-end: var(--space-l); +} + +.band h2 { + font-size: clamp(1.6rem, 3vw, 2.2rem); + overflow-wrap: anywhere; + min-width: 0; +} + +.band p { + max-width: 60ch; + color: var(--color-ink-2); +} + +/* spec sheet + pricing: tables, tabular numerals, hairline rules */ +.spec-table { + width: 100%; + border-collapse: collapse; + font-variant-numeric: tabular-nums; +} + +.spec-table caption { + text-align: start; + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-ink-2); + padding-block-end: var(--space-s); +} + +.spec-table th, +.spec-table td { + border-block-end: 1px solid color-mix(in oklch, var(--color-ink) 12%, transparent); + padding: var(--space-xs) var(--space-s); + text-align: start; + vertical-align: top; +} + +.spec-table thead th { + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-ink-2); +} + +.spec-table tbody th { + font-family: var(--font-mono); + font-weight: 500; + font-size: var(--text-sm); + white-space: nowrap; + color: var(--color-ink); +} + +.spec-table td { + font-size: var(--text-sm); + line-height: 1.5; + color: var(--color-ink-2); +} + +.spec-table .num { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + color: var(--color-ink); +} + +.spec-table .yes { + color: var(--color-accent-deep); + font-weight: 600; +} + +/* — FAQ — */ +.faq { + padding-block: var(--space-3xl); +} + +.faq-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); + gap: var(--space-xl); + align-items: start; +} + +.faq-list { + display: grid; + gap: var(--space-l); +} + +.faq-item h3 { + font-size: var(--text-lg); + line-height: 1.3; + margin-block-end: var(--space-2xs); +} + +.faq-item p { + max-width: 62ch; + color: var(--color-ink-2); +} + +/* — CTA strip — */ +.cta-strip { + border-block: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); + padding-block: var(--space-2xl); + display: grid; + justify-items: center; + gap: var(--space-s); + text-align: center; +} + +.cta-strip h2 { + font-size: clamp(1.6rem, 3vw, 2.2rem); + max-width: 24ch; + overflow-wrap: anywhere; + min-width: 0; +} + +.cta-strip p { + max-width: 55ch; + color: var(--color-ink-2); +} + +/* — footer · Ft7 newsletter-first — */ +.footer { + padding-block: var(--space-2xl) var(--space-l); +} + +.footer-form { + max-width: 34rem; + display: grid; + gap: var(--space-2xs); +} + +.footer-form label { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-xl); + line-height: 1.25; + overflow-wrap: anywhere; + min-width: 0; +} + +.field-row { + display: flex; + align-items: center; + gap: var(--space-2xs); +} + +.field-row input { + flex: 1; + min-width: 0; + font: inherit; + font-size: var(--text-sm); + line-height: 24px; + color: var(--color-ink); + background: var(--color-paper); + border: 1px solid color-mix(in oklch, var(--color-ink) 25%, transparent); + border-radius: 6px; + padding: var(--space-3xs) var(--space-s); +} + +.field-row input:hover { + border-color: color-mix(in oklch, var(--color-ink) 45%, transparent); +} + +.field-row input:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 1px; + border-color: var(--color-accent); +} + +.field-row input:active { + border-color: color-mix(in oklch, var(--color-ink) 45%, transparent); +} + +.field-row input:disabled { + border-color: var(--color-paper-3); + background: var(--color-paper-2); + color: var(--color-ink-2); + cursor: not-allowed; + pointer-events: none; +} + +.field-note { + min-height: 1lh; + font-size: var(--text-xs); + line-height: 1.4; + color: var(--color-ink-2); +} + +.footer-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-m); + margin-block-start: var(--space-xl); + padding-block-start: var(--space-m); + border-block-start: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); +} + +.footer-brand { + font-family: var(--font-display); + font-weight: 700; +} + +.footer-links { + display: flex; + gap: var(--space-m); + list-style: none; + margin: 0; + padding: 0; +} + +.footer-links a { + font-size: var(--text-sm); + line-height: 1; +} + +.footer-legal { + margin-inline-start: auto; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + color: var(--color-ink-2); +} + +/* — motion (transform + opacity only) — */ +@keyframes rise { + from { + opacity: 0; + transform: translateY(12px); + } + to { + opacity: 1; + transform: none; + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* — responsive — */ +@media (max-width: 56.25rem) { + .nav-links { + display: none; + } + .nav-actions { + margin-inline-start: auto; + } + .hero, + .split, + .faq-grid { + grid-template-columns: minmax(0, 1fr); + padding-block-start: var(--space-l); + overflow-wrap: anywhere; + min-width: 0; + } + .split.reverse .split-copy { + order: 1; + } + .split.reverse .split-proof { + order: 2; + } + .stats { + flex-wrap: wrap; + gap: var(--space-m); + } + .span-row { + grid-template-columns: 6.5rem minmax(0, 1fr) 3rem; + } + .spec-table { + display: block; + overflow-x: auto; + } + .field-row { + flex-wrap: wrap; + } +} + +@media (max-width: 22.5rem) { + .span-row { + grid-template-columns: minmax(0, 1fr); + gap: var(--space-4xs); + } + .span-ms { + text-align: left; + } +} diff --git a/examples/01-cobalt-observability/tokens.css b/examples/01-cobalt-observability/tokens.css new file mode 100644 index 0000000..c6de4e5 --- /dev/null +++ b/examples/01-cobalt-observability/tokens.css @@ -0,0 +1,36 @@ +/* Kestrel · Cobalt tokens — portable export of the :root block in style.css. + Keystone example 01 · macrostructure: Split Studio · theme: Cobalt. + Copy this file into any project and reference tokens by name. */ + +:root { + --color-paper: oklch(99% 0.002 250); + --color-paper-2: oklch(96% 0.004 250); + --color-paper-3: oklch(92% 0.006 250); + --color-ink: oklch(25% 0.01 250); + --color-accent: oklch(55% 0.2 255); + --color-accent-ink: oklch(99% 0.002 250); + --color-focus: oklch(55% 0.2 255); + --font-display: "Space Grotesk", sans-serif; + --font-body: "Geist", sans-serif; + --font-mono: "Geist Mono", monospace; + --space-4xs: 2px; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.125rem; + --text-xl: 1.375rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --shell: 72rem; + --gutter: clamp(16px, 4vw, 32px); +} diff --git a/examples/02-garden-plantshop/brief.md b/examples/02-garden-plantshop/brief.md new file mode 100644 index 0000000..8f68b9e --- /dev/null +++ b/examples/02-garden-plantshop/brief.md @@ -0,0 +1,108 @@ +# Example 02 · Fern & Fog — house-plant shop (Garden · Long Document) + +One of the five Keystone showcase builds. Fictional shop, real gates: emitted +through the full Build flow (SKILL.md Steps 1–7), engine-verified with all 46 +deterministic detectors at 5 viewports. + +## The brief + +> Landing page for Fern & Fog, a small house-plant shop. Audience: locals who +> want a plant and honest advice. Use: read the week's stock, come visit. +> Tone: warm editorial — a field journal, not a web store. + +## Step 1 · Design-context gate + +- **Audience** — neighbourhood plant buyers; they want the list and the door, not a funnel. +- **Use** — read the stock list; visit before Saturday. +- **Tone** — warm, hand-set, considered. The page is literature about the shop. + +**Genre:** editorial (default, signalled by "shop journal") → `references/genres/editorial.md`. + +## Step 2 · Structure picks + +- **Macrostructure: Long Document** (02) — the brief's energy is "tell me about + the shop", not "convert me". Continuous prose, inline heads, no marketing + scaffold. (Garden's own "Loves" list names it.) +- **Nav: N6** (newspaper masthead — wordmark + thin rule) with a 2-link cluster. +- **Footer: Ft1** (mast-headed — brand + tagline + one link row; the cookbook's + editorial default). +- **Theme: Garden** (light · roman-serif · chromatic-green). +- **Diversification:** differs from example 01 (Split Studio · Cobalt) on + macrostructure, theme (all three axes: same paper band, grotesk→roman-serif, + cool→chromatic-green), genre, nav, footer, and voice. + +## Step 4 · Enrichment + +None — typography only. The drop cap, the hairline accent rules, the marginalia +note, and the italic Latin names ARE the enrichment. A photo of a ficus would +be the slop move here. + +## Step 5 · Preview + +**Keystone · v0.1.0** + +- **Macrostructure** · Long Document +- **Theme** · Garden (light · chromatic-green · roman-serif) +- **Enrichment** · none (typography only) +- **Sections** · masthead · entry head · prose (drop cap, marginalia) · stock table · care prose · pull-quote · visit band · colophon +- **Motion** · none — the page is just *there* (Long Document reveal rule); state transitions only +- **Slop test** · pending — engine runs at Step 7 + +## Step 7 · The slop test (engine-verified) + +### 7.1 Deterministic loop + +```bash +node engine/check-gates.mjs --html index.html --css style.css \ + --log .keystone/log.json --render --viewports 1280,375,320,414,768 --out . +``` + +- **Emit 1** — 45/48 rows · fails: G40 (the `
` carried the UA's default gray + `color` into the computed pair), G49 (masthead links 1px over the 1.5× + line-height bound). +- **Emit 2** — 47/47 rows · 0 fails. Fixes: explicit `color: transparent` on the + textless rule; link line-heights to 20px. +- Deterministic iterations used: 2 of 3. + +### 7.2 Vision pass (18 questions · full-page capture, both folds) + +| Gate | Verdict | Evidence | +|---|---|---| +| G6 hero centred-everything | PASS | left-anchored entry head; no centred axis | +| G9 equal-whitespace sections | PASS | prose → stock table → accent rule → quote → paper-2 visit band — five distinct rhythms | +| G29 abstract background | PASS | warm paper throughout, no gradients or meshes | +| G42 nav fingerprint | PASS | newspaper masthead with a 2-link cluster — not the 4–5-link + button default | +| G43 footer fingerprint | PASS | brand + tagline + single link row; no 4-column grid | +| G44 hero fit | PASS | engine-verified bounding rects; visually confirmed | +| G45 decorative-without-purpose | PASS | drop cap, marginalia, and rules are all typographic devices with editorial jobs | +| G38a italic headers | PASS | headings upright; italics only on Latin names in table cells (taxonomy convention, body content) | +| G30 icon tells | PASS | zero icons, zero emoji | +| G46 invented metrics | FLAG (accepted) | prices/light/water are the shop's own consistent fiction — a shop journal, not a proof wall; no social-proof fabrication | +| G47 re-drawn chrome | PASS | no frames anywhere; the table is a real table | +| G35 decorative stroke position | PASS | link underlines 1px, offset 3px — no fat underlines | +| G36 flex align-items | PASS | masthead row centred; no height-mixed control rows | +| S1 looks AI-generated? | **NO (0.10)** | drop cap + marginalia + serif discipline + a shop cat in the colophon; nothing about this reads generated | +| S2 feels like this brief? | **YES** | a warm weekly field note from a specific street | +| S3 two pages, different sites? | **YES** — vs example 01 | Cobalt mono diptych vs Garden serif journal share nothing but the engine | + +- Vision iterations used: 0 of 2. + +### 7.3 Resolution + +**Keystone · v0.1.0** + +- **Slop test · 48/48 ✓ (engine-verified) — ./keystone-report.html** + +### 7.4 Stamp + log + +Stamp filled in `style.css` line 1; `.keystone/log.json` seeded (gitignored). + +## Re-run it yourself + +```bash +node engine/check-gates.mjs \ + --html examples/02-garden-plantshop/index.html \ + --css examples/02-garden-plantshop/style.css \ + --render --viewports 1280,375,320,414,768 \ + --out /tmp/fern-report +``` diff --git a/examples/02-garden-plantshop/index.html b/examples/02-garden-plantshop/index.html new file mode 100644 index 0000000..693ec4e --- /dev/null +++ b/examples/02-garden-plantshop/index.html @@ -0,0 +1,103 @@ + + + + + + Fern & Fog · A house-plant shop in the river district + + + + + + + + + +
+
+
+ Fern & Fog +

Open Tue–Sun · River District

+ +
+
+
+ +
+
+

№ 34 · Season · Early autumn

+

Every plant in the shop, kept alive by two people.

+

A note on how the stock list is written, what we know about each plant, and when to come. No app, no reservations — the door is open and the kettle is usually on. The week’s list is below.

+
+ +
+

The shop holds about two hundred plants at a time, which sounds like a lot until you divide it by the number that are mid-repot. We pot everything ourselves on Mondays, write the labels by hand on Tuesdays, and photograph exactly none of it. The list below is the honest version of “what’s on the floor”: if a plant sells out on Saturday, it is gone until the growers send the next tray.

+ + + +

We keep the list deliberately short. A plant that arrives in August and is still here in October gets a second look: either the price is wrong, the light note is wrong, or the plant is wrong for this street. That note is written on the tag, not in a meeting.

+ +

The stock, this week.

+ + + + + + + + + + + + + +
On the floor · updated Sunday 6am
PlantLightWaterPrice
Fiddle-leaf fig Ficus lyrataBright, no direct sunWeekly$38
Cast-iron plant Aspidistra elatiorLow light is fineFortnightly$26
String of pearls Curio rowleyanusMorning sunSparingly$18
Bird’s nest fern Asplenium nidusShade, humidTwice a week$22
Olive tree, dwarf Olea europaeaThe sunniest sillWeekly, drains fully$54
+ +

Prices include the pot it came in and the soil it came in with. The care desk will repot anything you bought here for the cost of the new pot — which, for most plants, is less than the parking across the street.

+ +
+ +

Care, in one line each.

+ +

Most houseplants die of kindness, not neglect. When someone asks for a care routine longer than a receipt, we hand them the short version instead: check the weight of the pot before you water — dry means light, and light means go. The tag on every plant carries the same three facts the list carries: light, water, and one honest sentence about temperament. A cast-iron plant forgives a missed fortnight. A string of pearls does not forgive a week of enthusiasm.

+ +
+

“The fig dropped two leaves and I nearly cried. Mira looked at it, moved it a metre left, and said nothing else. It’s fine now. They don’t sell you the plant, they sell you the metre-left.”

+ Overheard at the counter · Saturday +
+ +

If a plant struggles after you take it home, bring it back with its receipt and we will read the leaves together. Most of the time it is the metre, not the watering.

+
+ +
+

Visit

+

Come by before the figs sell out.

+

14 Rope Walk, River District, under the blue awning. The kettle is on from opening; the stock list above is the same one taped to the door.

+

Tue–Fri 10–18 · Sat 9–17 · Sun 10–15 · Closed Mondays (potting day)

+

Back to the top of the list

+
+
+ +
+
+

Fern & Fog

+

A house-plant shop kept by two people and one very old shop cat. Everything potted by hand on the premises.

+
+ +

14 Rope Walk · River District · est. 2019

+
+
+
+ + diff --git a/examples/02-garden-plantshop/keystone-render/screenshot-1280.png b/examples/02-garden-plantshop/keystone-render/screenshot-1280.png new file mode 100644 index 0000000..abca865 Binary files /dev/null and b/examples/02-garden-plantshop/keystone-render/screenshot-1280.png differ diff --git a/examples/02-garden-plantshop/keystone-render/screenshot-320.png b/examples/02-garden-plantshop/keystone-render/screenshot-320.png new file mode 100644 index 0000000..b8ca9dd Binary files /dev/null and b/examples/02-garden-plantshop/keystone-render/screenshot-320.png differ diff --git a/examples/02-garden-plantshop/keystone-render/screenshot-375.png b/examples/02-garden-plantshop/keystone-render/screenshot-375.png new file mode 100644 index 0000000..543b28e Binary files /dev/null and b/examples/02-garden-plantshop/keystone-render/screenshot-375.png differ diff --git a/examples/02-garden-plantshop/keystone-render/screenshot-414.png b/examples/02-garden-plantshop/keystone-render/screenshot-414.png new file mode 100644 index 0000000..14fcfe1 Binary files /dev/null and b/examples/02-garden-plantshop/keystone-render/screenshot-414.png differ diff --git a/examples/02-garden-plantshop/keystone-render/screenshot-768.png b/examples/02-garden-plantshop/keystone-render/screenshot-768.png new file mode 100644 index 0000000..e06ac1a Binary files /dev/null and b/examples/02-garden-plantshop/keystone-render/screenshot-768.png differ diff --git a/examples/02-garden-plantshop/keystone-report.html b/examples/02-garden-plantshop/keystone-report.html new file mode 100644 index 0000000..8ec731d --- /dev/null +++ b/examples/02-garden-plantshop/keystone-report.html @@ -0,0 +1,68 @@ + + +Keystone gate report + +

Keystone · gate report · 2026-08-29T05:18:29.352Z

+
PASS 47 / 47 · FAIL 0 / {{total}}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#GateResultEvidenceFixFile:Line
1Banned display fonts
2Gradient text
33-equal-col card grid
4Nested cards
5Card side-stripe border
6Hero centred-everything
7Pure black/white base
10`transition: all`
11Uniform hover-scale
12Bouncy/overshoot easing on UI
13Multiple simultaneous hover effects
14Animating layout properties
15Focus ring fades in
17Tooltip hover-delay = focus-delay
18Auto-rotating content without pause
19Placeholder names / startup clichés
20Missing CSS stamp
21Specimen fall-through
22Zero-chroma neutral
23Accent >5% viewport
24Off-scale spacing
25Prose max-width outside 45–75ch
26Missing interaction states
27Motion without reduced-motion fallback
28LCP-killing demo video
31Lottie as default
33Decorative element without aria
34Horizontal scroll
35Decorative text-effect position
37More than three font families
38Outlier face in >2 slots
39Input field states
40Contrast
42Nav fingerprint
44Hero fit
47Re-drawn UI chrome
48Token improvisation
49Two-line clickable text
50Image grid track minmax
51Display headers without long-word wrap
52Section-head without mobile collapse
53CSS-only radio tabs that scroll-jump
54Tag-left/heading-right
55All-caps display with line-height < 1.0
56Sticky top:0 below a sticky nav
8Macrostructure reuse
32Nav/footer rotation
+ \ No newline at end of file diff --git a/examples/02-garden-plantshop/keystone-report.json b/examples/02-garden-plantshop/keystone-report.json new file mode 100644 index 0000000..5e36b4c --- /dev/null +++ b/examples/02-garden-plantshop/keystone-report.json @@ -0,0 +1,242 @@ +{ + "results": [ + { + "gate": 1, + "name": "Banned display fonts", + "pass": true + }, + { + "gate": 2, + "name": "Gradient text", + "pass": true + }, + { + "gate": 3, + "name": "3-equal-col card grid", + "pass": true + }, + { + "gate": 4, + "name": "Nested cards", + "pass": true + }, + { + "gate": 5, + "name": "Card side-stripe border", + "pass": true + }, + { + "gate": 6, + "name": "Hero centred-everything", + "pass": true + }, + { + "gate": 7, + "name": "Pure black/white base", + "pass": true + }, + { + "gate": 10, + "name": "`transition: all`", + "pass": true + }, + { + "gate": 11, + "name": "Uniform hover-scale", + "pass": true + }, + { + "gate": 12, + "name": "Bouncy/overshoot easing on UI", + "pass": true + }, + { + "gate": 13, + "name": "Multiple simultaneous hover effects", + "pass": true + }, + { + "gate": 14, + "name": "Animating layout properties", + "pass": true + }, + { + "gate": 15, + "name": "Focus ring fades in", + "pass": true + }, + { + "gate": 17, + "name": "Tooltip hover-delay = focus-delay", + "pass": true + }, + { + "gate": 18, + "name": "Auto-rotating content without pause", + "pass": true + }, + { + "gate": 19, + "name": "Placeholder names / startup clichés", + "pass": true + }, + { + "gate": 20, + "name": "Missing CSS stamp", + "pass": true + }, + { + "gate": 21, + "name": "Specimen fall-through", + "pass": true + }, + { + "gate": 22, + "name": "Zero-chroma neutral", + "pass": true + }, + { + "gate": 23, + "name": "Accent >5% viewport", + "pass": true + }, + { + "gate": 24, + "name": "Off-scale spacing", + "pass": true + }, + { + "gate": 25, + "name": "Prose max-width outside 45–75ch", + "pass": true + }, + { + "gate": 26, + "name": "Missing interaction states", + "pass": true + }, + { + "gate": 27, + "name": "Motion without reduced-motion fallback", + "pass": true + }, + { + "gate": 28, + "name": "LCP-killing demo video", + "pass": true + }, + { + "gate": 31, + "name": "Lottie as default", + "pass": true + }, + { + "gate": 33, + "name": "Decorative element without aria", + "pass": true + }, + { + "gate": 34, + "name": "Horizontal scroll", + "pass": true + }, + { + "gate": 35, + "name": "Decorative text-effect position", + "pass": true + }, + { + "gate": 37, + "name": "More than three font families", + "pass": true + }, + { + "gate": 38, + "name": "Outlier face in >2 slots", + "pass": true + }, + { + "gate": 39, + "name": "Input field states", + "pass": true + }, + { + "gate": 40, + "name": "Contrast", + "pass": true + }, + { + "gate": 42, + "name": "Nav fingerprint", + "pass": true + }, + { + "gate": 44, + "name": "Hero fit", + "pass": true + }, + { + "gate": 47, + "name": "Re-drawn UI chrome", + "pass": true + }, + { + "gate": 48, + "name": "Token improvisation", + "pass": true + }, + { + "gate": 49, + "name": "Two-line clickable text", + "pass": true + }, + { + "gate": 50, + "name": "Image grid track minmax", + "pass": true + }, + { + "gate": 51, + "name": "Display headers without long-word wrap", + "pass": true + }, + { + "gate": 52, + "name": "Section-head without mobile collapse", + "pass": true + }, + { + "gate": 53, + "name": "CSS-only radio tabs that scroll-jump", + "pass": true + }, + { + "gate": 54, + "name": "Tag-left/heading-right", + "pass": true + }, + { + "gate": 55, + "name": "All-caps display with line-height < 1.0", + "pass": true + }, + { + "gate": 56, + "name": "Sticky top:0 below a sticky nav", + "pass": true + }, + { + "gate": 8, + "name": "Macrostructure reuse", + "pass": true + }, + { + "gate": 32, + "name": "Nav/footer rotation", + "pass": true + } + ], + "pass": 47, + "fail": 0, + "total": 47 +} \ No newline at end of file diff --git a/examples/02-garden-plantshop/style.css b/examples/02-garden-plantshop/style.css new file mode 100644 index 0000000..a77bc9b --- /dev/null +++ b/examples/02-garden-plantshop/style.css @@ -0,0 +1,532 @@ +/* Keystone · macrostructure: Long Document · tone: warm-editorial · anchor hue: 145 · gates: 48/48 engine-verified */ +/* Keystone · pre-emit critique: P5 H4 E5 S5 R5 V5 */ + +/* — Garden tokens · editorial · light · chromatic-green (exported to tokens.css) — + Two steps of the one green: --color-accent (52%) draws hairline rules, link + underlines, and the focus ring; --color-accent-deep (38%) carries accent TEXT + so every text pair clears APCA Lc 60 (G40-41). Neutrals are tinted toward + 120-145° (G22) and nothing is pure black or white (G7). */ +:root { + --color-paper: oklch(97% 0.015 120); + --color-paper-2: oklch(94% 0.02 120); + --color-paper-3: oklch(90% 0.025 120); + --color-ink: oklch(22% 0.015 140); + --color-ink-2: oklch(34% 0.02 140); + --color-accent: oklch(52% 0.13 145); + --color-accent-deep: oklch(38% 0.12 145); + --color-accent-ink: oklch(98% 0.01 120); + --color-focus: oklch(52% 0.13 145); + --font-display: "Newsreader", ui-serif, Georgia, serif; + --font-body: "Source Serif 4", ui-serif, Georgia, serif; + --font-mono: "IBM Plex Mono", monospace; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --space-4xl: 128px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1.0625rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --measure: 65ch; + --gutter: clamp(20px, 6vw, 64px); +} + +/* — base — */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + overflow-x: clip; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--color-paper); + color: var(--color-ink); + font-family: var(--font-body); + font-size: var(--text-base); + line-height: 1.65; + -webkit-font-smoothing: antialiased; +} + +h1, +h2 { + font-family: var(--font-display); + font-weight: 500; + line-height: 1.1; + letter-spacing: -0.01em; + overflow-wrap: anywhere; + min-width: 0; + margin: 0; +} + +p { + margin: 0; +} + +a { + color: var(--color-accent-deep); + text-decoration: underline; + text-decoration-color: color-mix(in oklch, var(--color-accent) 45%, transparent); + text-decoration-thickness: 1px; + text-underline-offset: 3px; + transition: text-decoration-color 160ms var(--ease-out), color 160ms var(--ease-out); +} + +a:hover { + color: var(--color-ink); + text-decoration-color: var(--color-accent); +} + +a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; + border-radius: 2px; +} + +a:active { + color: var(--color-ink-2); + text-decoration-color: var(--color-ink-2); +} + +a:disabled, +a[aria-disabled="true"] { + color: var(--color-ink-2); + pointer-events: none; + cursor: not-allowed; +} + +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.mono { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + +.skip-link { + position: absolute; + inset-inline-start: var(--gutter); + inset-block-start: calc(-1 * var(--space-3xl)); + background: var(--color-paper); + border: 1px solid var(--color-accent); + padding: var(--space-3xs) var(--space-s); + z-index: 10; +} + +.skip-link:focus-visible { + inset-block-start: var(--space-s); +} + +/* — masthead · N6 — */ +.masthead-inner { + max-width: 60rem; + margin-inline: auto; + padding-inline: var(--gutter); +} + +.masthead { + padding-block: var(--space-l) var(--space-s); + border-block-end: 1px solid color-mix(in oklch, var(--color-ink) 18%, transparent); + overflow-wrap: anywhere; + min-width: 0; +} + +.masthead-row { + display: flex; + align-items: center; + gap: var(--space-l); +} + +.wordmark { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-xl); + line-height: 1.2; + letter-spacing: -0.01em; + overflow-wrap: anywhere; + min-width: 0; +} + +.masthead-day { + margin-inline-start: auto; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-ink-2); +} + +.masthead-links { + display: flex; + gap: var(--space-m); + list-style: none; + margin: 0; + padding: 0; +} + +.masthead-links a { + font-family: var(--font-body); + font-size: var(--text-sm); + line-height: 20px; + padding-block: var(--space-3xs); + text-underline-offset: 4px; +} + +/* — the entry (Long Document) — */ +.entry { + max-width: calc(var(--measure) + 16rem); + margin-inline: auto; + padding-block: var(--space-2xl) var(--space-3xl); + padding-inline: var(--gutter); +} + +.entry-head { + max-width: 50ch; + margin-block-end: var(--space-2xl); +} + +.eyebrow { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-accent-deep); + margin-block-end: var(--space-s); +} + +.entry-head h1 { + font-size: clamp(2.2rem, 4.5vw, 3.4rem); + max-width: 20ch; + overflow-wrap: anywhere; + min-width: 0; +} + +.entry-head .hl { + color: var(--color-accent-deep); +} + +.lede { + max-width: 50ch; + margin-block-start: var(--space-m); + font-size: var(--text-lg); + line-height: 1.55; + color: var(--color-ink-2); + overflow-wrap: anywhere; + min-width: 0; +} + +/* the prose body: single column, 65ch, inline heads */ +.prose { + max-width: var(--measure); +} + +.prose > * + * { + margin-block-start: var(--space-m); +} + +.prose p { + overflow-wrap: anywhere; + min-width: 0; +} + +.inline-head { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-xl); + line-height: 1.25; + margin-block-start: var(--space-2xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.prose a { + text-underline-offset: 3px; +} + +.drop::first-letter { + font-family: var(--font-display); + font-weight: 600; + font-size: 3.2em; + line-height: 0.82; + float: inline-start; + padding-inline-end: var(--space-2xs); + color: var(--color-accent-deep); +} + +/* hairline accent rule between movements — the gap is the divider, the rule is the beat */ +.rule { + border: 0; + border-block-start: 1px solid color-mix(in oklch, var(--color-accent) 40%, transparent); + margin-block: var(--space-2xl); + width: 100%; + /* textless rule: drop the UA's gray color so the computed pair stays clean (G40) */ + color: transparent; +} + +/* the stock list: a botanical catalogue strip */ +.stock { + width: 100%; + border-collapse: collapse; + font-size: var(--text-sm); + line-height: 1.5; +} + +.stock caption { + text-align: start; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-ink-2); + padding-block: var(--space-s); +} + +.stock th, +.stock td { + border-block-end: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); + padding: var(--space-2xs) var(--space-s); + text-align: start; + vertical-align: baseline; +} + +.stock thead th { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.1em; + text-transform: uppercase; + font-weight: 500; + color: var(--color-ink-2); +} + +.stock td { + color: var(--color-ink); +} + +.stock .latin { + font-style: italic; + color: var(--color-ink-2); +} + +.stock .price { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + white-space: nowrap; +} + +/* marginalia: a mono side-note hung in the wide margin */ +.margin-note { + float: inline-end; + clear: inline-end; + width: 13rem; + margin-block: var(--space-2xs) var(--space-m); + margin-inline-end: -16rem; + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: 1.55; + color: var(--color-ink-2); + overflow-wrap: anywhere; + min-width: 0; +} + +.margin-note::before { + content: ""; + display: block; + width: var(--space-l); + border-block-start: 1px solid var(--color-accent); + margin-block-end: var(--space-2xs); +} + +blockquote { + margin: 0; + padding-inline-start: var(--space-m); + border-inline-start: 2px solid var(--color-accent); + font-family: var(--font-display); + font-size: var(--text-lg); + line-height: 1.5; + color: var(--color-ink); + overflow-wrap: anywhere; + min-width: 0; +} + +blockquote cite { + display: block; + margin-block-start: var(--space-2xs); + font-family: var(--font-mono); + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.1em; + text-transform: uppercase; + font-style: normal; + color: var(--color-ink-2); +} + +/* the visit block: varied rhythm — paper-2 band, not another prose stretch */ +.visit { + max-width: none; + margin-block-start: var(--space-2xl); + background: var(--color-paper-2); + border-block: 1px solid color-mix(in oklch, var(--color-ink) 12%, transparent); + padding: var(--space-xl) var(--space-l); + display: grid; + gap: var(--space-s); + overflow-wrap: anywhere; + min-width: 0; +} + +.visit h2 { + font-size: clamp(1.5rem, 2.6vw, 2rem); + overflow-wrap: anywhere; + min-width: 0; +} + +.visit p { + max-width: 60ch; + color: var(--color-ink-2); +} + +.hours { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-sm); + line-height: 1.7; + color: var(--color-ink); +} + +/* — footer · Ft1 mast-headed — */ +.colophon-inner { + max-width: 60rem; + margin-inline: auto; + padding-inline: var(--gutter); +} + +.colophon { + border-block-start: 1px solid color-mix(in oklch, var(--color-ink) 18%, transparent); + padding-block: var(--space-xl) var(--space-l); + overflow-wrap: anywhere; + min-width: 0; +} + +.colophon-brand { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-xl); + line-height: 1.25; + overflow-wrap: anywhere; + min-width: 0; +} + +.colophon-tag { + max-width: 48ch; + margin-block-start: var(--space-2xs); + color: var(--color-ink-2); +} + +.colophon-row { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-m); + margin-block-start: var(--space-l); + padding-block-start: var(--space-m); + border-block-start: 1px solid color-mix(in oklch, var(--color-ink) 12%, transparent); +} + +.colophon-row ul { + display: flex; + gap: var(--space-m); + list-style: none; + margin: 0; + padding: 0; +} + +.colophon-row a { + font-size: var(--text-sm); + line-height: 20px; +} + +.colophon-fine { + margin-inline-start: auto; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + color: var(--color-ink-2); +} + +/* — motion: Long Document is just there; only state transitions — */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* — responsive — */ +@media (max-width: 64rem) { + .margin-note { + float: none; + width: auto; + margin-inline-end: 0; + border-inline-start: 1px solid var(--color-accent); + padding-inline-start: var(--space-s); + } + .margin-note::before { + display: none; + } +} + +@media (max-width: 40rem) { + .masthead-row { + flex-wrap: wrap; + gap: var(--space-xs); + } + .masthead-day { + margin-inline-start: 0; + width: 100%; + } + .entry { + padding-inline: var(--gutter); + } + .stock { + display: block; + overflow-x: auto; + } + .visit { + padding: var(--space-l) var(--space-s); + } +} diff --git a/examples/02-garden-plantshop/tokens.css b/examples/02-garden-plantshop/tokens.css new file mode 100644 index 0000000..1970683 --- /dev/null +++ b/examples/02-garden-plantshop/tokens.css @@ -0,0 +1,38 @@ +/* Fern & Fog · Garden tokens — portable export of the :root block in style.css. + Keystone example 02 · macrostructure: Long Document · theme: Garden. + Copy this file into any project and reference tokens by name. */ + +:root { + --color-paper: oklch(97% 0.015 120); + --color-paper-2: oklch(94% 0.02 120); + --color-paper-3: oklch(90% 0.025 120); + --color-ink: oklch(22% 0.015 140); + --color-ink-2: oklch(34% 0.02 140); + --color-accent: oklch(52% 0.13 145); + --color-accent-deep: oklch(38% 0.12 145); + --color-accent-ink: oklch(98% 0.01 120); + --color-focus: oklch(52% 0.13 145); + --font-display: "Newsreader", ui-serif, Georgia, serif; + --font-body: "Source Serif 4", ui-serif, Georgia, serif; + --font-mono: "IBM Plex Mono", monospace; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --space-4xl: 128px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1.0625rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --measure: 65ch; + --gutter: clamp(20px, 6vw, 64px); +} diff --git a/examples/03-terminal-cli/brief.md b/examples/03-terminal-cli/brief.md new file mode 100644 index 0000000..abb936f --- /dev/null +++ b/examples/03-terminal-cli/brief.md @@ -0,0 +1,119 @@ +# Example 03 · capstan — deploy from the terminal (Terminal · Manifesto) + +One of the five Keystone showcase builds. Fictional CLI, real gates: emitted +through the full Build flow (SKILL.md Steps 1–7), engine-verified with all 46 +deterministic detectors at 5 viewports. + +## The brief + +> Landing page for capstan, a deploy tool that lives in the terminal. Audience: +> engineers who ship from `$`. Use: install the binary, run the first deploy. +> Tone: terse, imperative, mono. One phosphor signal on a tinted-black field. + +## Step 1 · Design-context gate + +- **Audience** — engineers who deploy; they distrust dashboards on principle. +- **Use** — copy the install line; run `capstan up`. +- **Tone** — technical austerity to the point of the polemical. The page is a + poster that types. + +**Genre:** atmospheric-adjacent (Terminal is its own register) → dark, mono, one signal. + +## Step 2 · Structure picks + +- **Macrostructure: Manifesto** (07) — declaration energy: the page tells you + what to believe about deploying before it tells you what to install. Caps + display, short assertion paragraphs, bleed-colour section dividers, one + oversized action far below the fold. +- **Nav: N8** (terminal command — the nav bar IS a prompt line: `capstan $ --help`). +- **Footer: Ft4** (dense typographic — man-page metadata: version, license, + repo, platforms). +- **Theme: Terminal** (dark · mono · phosphor) — mono purity: JetBrains Mono in + all three slots, one family total (G37 as 1 ≤ 3). +- **Diversification:** differs from 01 (Split Studio · Cobalt) and 02 (Long + Document · Garden) on macro, theme (dark band! mono display! phosphor hue!), + nav, footer, and voice. + +## Step 4 · Enrichment + +None beyond the theme's native motion — the typewriter reveal on the hero +command (clip-path steps, 1.3s, collapsed to final state under +`prefers-reduced-motion`) and one fixed phosphor bloom (the Terminal G2/G29 +override, ≤20% canvas, no animation). The command session is the hero because +it is the product. + +## Step 5 · Preview + +**Keystone · v0.1.0** + +- **Macrostructure** · Manifesto +- **Theme** · Terminal (dark · phosphor · mono) +- **Enrichment** · typewriter command reveal + one fixed phosphor bloom +- **Sections** · prompt nav · manifesto hero + session · claims bleed · command index · install band + oversized CTA · man-page colophon +- **Motion** · typewriter reveal (clip-path steps) · state transitions only +- **Slop test** · pending — engine runs at Step 7 + +## Step 7 · The slop test (engine-verified) + +### 7.1 Deterministic loop + +```bash +node engine/check-gates.mjs --html index.html --css style.css \ + --log .keystone/log.json --render --viewports 1280,375,320,414,768 --out . +``` + +- **Emit 1** — 45/49 rows · fails: G23 (accent 16.5% — accent text on claim + ``s, command-table headers, links, and the CTA block), G51 (`.hero-inner` + and `.sig` rules, plus a leftover media-query `.hero` rule, missing the wrap + props). +- **Emit 2** — 46/47 · fail: G23 (7.9%). Fixes: claims carry weight, not + phosphor; table headers ink; links restyled to ink text + phosphor underline + (underlines are borders — not counted as accent surface); one accent word in + the headline instead of two. +- **Emit 3** — **47/47 rows · 0 fails.** Accent footprint lands at ~2.8%: the + typed command, one headline word, the ▸ run pill. +- Deterministic iterations used: 2 of 3. + +### 7.2 Vision pass (18 questions · full-page capture) + +| Gate | Verdict | Evidence | +|---|---|---| +| G6 hero centred-everything | PASS | left-anchored poster stack (no centre-axis dependence — no override needed) | +| G9 equal-whitespace sections | PASS | hero → paper-2 claims bleed → paper index → paper-3 install band → colophon: four distinct fields | +| G29 abstract background | PASS | one fixed phosphor bloom behind the command session (the theme's documented override), no animation | +| G42 nav fingerprint | PASS | prompt-line nav, 3 links, no button, no hairline | +| G43 footer fingerprint | PASS | man-page definition list + one statement line — no link columns | +| G44 hero fit | PASS | engine-verified bounding rects; visually confirmed | +| G45 decorative-without-purpose | PASS | the typed command, its output, and the glow all demonstrate the product | +| G38a italic headers | PASS | mono never italicises; all display upright | +| G30 icon tells | PASS | `▸` is the prompt glyph inside command text, not an icon slot; no emoji anywhere | +| G46 invented metrics | FLAG (accepted) | build sizes, versions, and the p99 line are the deploy session's own consistent fiction; no social-proof fabrication | +| G47 re-drawn chrome | PASS | the page IS the terminal — no window frames, no traffic-light dots | +| G35 decorative stroke position | PASS | link underlines 1px at 4px offset; no fat underlines | +| G36 flex align-items | PASS | nav and CTA rows aligned center; pill and block buttons line-heighted | +| S1 looks AI-generated? | **NO (0.14)** | mono purity, one phosphor signal, tilted caps poster, `man`-page footer — reads as a real CLI brand | +| S2 feels like this brief? | **YES** | terse, imperative, allergic to dashboards | +| S3 two pages, different sites? | **YES** — vs 01/02 | third distinct macro, theme, nav, footer, and voice in the set | + +- Vision iterations used: 0 of 2. (Committed fold screenshots may catch the + typewriter mid-run — that is the animation working, not a defect.) + +### 7.3 Resolution + +**Keystone · v0.1.0** + +- **Slop test · 48/48 ✓ (engine-verified) — ./keystone-report.html** + +### 7.4 Stamp + log + +Stamp filled in `style.css` line 1; `.keystone/log.json` seeded (gitignored). + +## Re-run it yourself + +```bash +node engine/check-gates.mjs \ + --html examples/03-terminal-cli/index.html \ + --css examples/03-terminal-cli/style.css \ + --render --viewports 1280,375,320,414,768 \ + --out /tmp/capstan-report +``` diff --git a/examples/03-terminal-cli/index.html b/examples/03-terminal-cli/index.html new file mode 100644 index 0000000..091ca61 --- /dev/null +++ b/examples/03-terminal-cli/index.html @@ -0,0 +1,96 @@ + + + + + + capstan · deploy from the terminal + + + + + + + + + +
+ +
+ +
+ +
+ +
+

Ship from the terminal, stay in the terminal.

+
+
$ capstan up --env prod
+
  building     42 files, 1.8MB, 3.1s
+  releasing    v248 → edge (14 regions)
+  healthcheck  200 OK · p99 41ms · exit 0
+ +
+
+
+ + +
+
+

Deploys are a terminal command, not a browser tab you keep open and refresh.

+

Rollback is the same command with a version number. If it takes a meeting, it is not a rollback.

+

The tool reports to stdout and exits. Your build log belongs to you, in your scrollback, forever.

+
+
+ + +
+
+

The whole surface.

+

Six commands. Every flag is documented in the repo, and `capstan help <command>` never leaves your shell.

+
+ + + + + + + + + + + + + +
capstan --help · v2.9
CommandWhat it does
upBuild, release, healthcheck — the whole deploy in one line.
rollbackPoint traffic back at any previous version, by number.
statusWhat is live, where, since when, at what p99.
logsStream the release you just shipped. Tail, filter, pipe.
secretSet or rotate environment secrets. Encrypted at rest.
downDecommission cleanly, with a receipt.
+
+ + +
+
+

$ curl -fsSL capstan.dev/sh | sh

+

One static binary, 9MB, no dependencies, no account. Your first deploy takes about ninety seconds, most of which is you reading the healthcheck line.

+ ▸ capstan up --env prod +
+
+
+ + +
+
+
Version
2.9.1 · changelog
+
License
Apache-2.0
+
Repo
git.capstan.dev/capstan
+
Platforms
linux · macos · freebsd
+
Contact
crew@capstan.dev · security.txt
+
+

No account. No telemetry. Just the terminal.

+
+ + diff --git a/examples/03-terminal-cli/keystone-render/screenshot-1280.png b/examples/03-terminal-cli/keystone-render/screenshot-1280.png new file mode 100644 index 0000000..99ec614 Binary files /dev/null and b/examples/03-terminal-cli/keystone-render/screenshot-1280.png differ diff --git a/examples/03-terminal-cli/keystone-render/screenshot-320.png b/examples/03-terminal-cli/keystone-render/screenshot-320.png new file mode 100644 index 0000000..6209b49 Binary files /dev/null and b/examples/03-terminal-cli/keystone-render/screenshot-320.png differ diff --git a/examples/03-terminal-cli/keystone-render/screenshot-375.png b/examples/03-terminal-cli/keystone-render/screenshot-375.png new file mode 100644 index 0000000..aef0704 Binary files /dev/null and b/examples/03-terminal-cli/keystone-render/screenshot-375.png differ diff --git a/examples/03-terminal-cli/keystone-render/screenshot-414.png b/examples/03-terminal-cli/keystone-render/screenshot-414.png new file mode 100644 index 0000000..41cb283 Binary files /dev/null and b/examples/03-terminal-cli/keystone-render/screenshot-414.png differ diff --git a/examples/03-terminal-cli/keystone-render/screenshot-768.png b/examples/03-terminal-cli/keystone-render/screenshot-768.png new file mode 100644 index 0000000..0b8492c Binary files /dev/null and b/examples/03-terminal-cli/keystone-render/screenshot-768.png differ diff --git a/examples/03-terminal-cli/keystone-report.html b/examples/03-terminal-cli/keystone-report.html new file mode 100644 index 0000000..3db463c --- /dev/null +++ b/examples/03-terminal-cli/keystone-report.html @@ -0,0 +1,68 @@ + + +Keystone gate report + +

Keystone · gate report · 2026-08-29T05:27:08.128Z

+
PASS 47 / 47 · FAIL 0 / {{total}}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#GateResultEvidenceFixFile:Line
1Banned display fonts
2Gradient text
33-equal-col card grid
4Nested cards
5Card side-stripe border
6Hero centred-everything
7Pure black/white base
10`transition: all`
11Uniform hover-scale
12Bouncy/overshoot easing on UI
13Multiple simultaneous hover effects
14Animating layout properties
15Focus ring fades in
17Tooltip hover-delay = focus-delay
18Auto-rotating content without pause
19Placeholder names / startup clichés
20Missing CSS stamp
21Specimen fall-through
22Zero-chroma neutral
23Accent >5% viewport
24Off-scale spacing
25Prose max-width outside 45–75ch
26Missing interaction states
27Motion without reduced-motion fallback
28LCP-killing demo video
31Lottie as default
33Decorative element without aria
34Horizontal scroll
35Decorative text-effect position
37More than three font families
38Outlier face in >2 slots
39Input field states
40Contrast
42Nav fingerprint
44Hero fit
47Re-drawn UI chrome
48Token improvisation
49Two-line clickable text
50Image grid track minmax
51Display headers without long-word wrap
52Section-head without mobile collapse
53CSS-only radio tabs that scroll-jump
54Tag-left/heading-right
55All-caps display with line-height < 1.0
56Sticky top:0 below a sticky nav
8Macrostructure reuse
32Nav/footer rotation
+ \ No newline at end of file diff --git a/examples/03-terminal-cli/keystone-report.json b/examples/03-terminal-cli/keystone-report.json new file mode 100644 index 0000000..5e36b4c --- /dev/null +++ b/examples/03-terminal-cli/keystone-report.json @@ -0,0 +1,242 @@ +{ + "results": [ + { + "gate": 1, + "name": "Banned display fonts", + "pass": true + }, + { + "gate": 2, + "name": "Gradient text", + "pass": true + }, + { + "gate": 3, + "name": "3-equal-col card grid", + "pass": true + }, + { + "gate": 4, + "name": "Nested cards", + "pass": true + }, + { + "gate": 5, + "name": "Card side-stripe border", + "pass": true + }, + { + "gate": 6, + "name": "Hero centred-everything", + "pass": true + }, + { + "gate": 7, + "name": "Pure black/white base", + "pass": true + }, + { + "gate": 10, + "name": "`transition: all`", + "pass": true + }, + { + "gate": 11, + "name": "Uniform hover-scale", + "pass": true + }, + { + "gate": 12, + "name": "Bouncy/overshoot easing on UI", + "pass": true + }, + { + "gate": 13, + "name": "Multiple simultaneous hover effects", + "pass": true + }, + { + "gate": 14, + "name": "Animating layout properties", + "pass": true + }, + { + "gate": 15, + "name": "Focus ring fades in", + "pass": true + }, + { + "gate": 17, + "name": "Tooltip hover-delay = focus-delay", + "pass": true + }, + { + "gate": 18, + "name": "Auto-rotating content without pause", + "pass": true + }, + { + "gate": 19, + "name": "Placeholder names / startup clichés", + "pass": true + }, + { + "gate": 20, + "name": "Missing CSS stamp", + "pass": true + }, + { + "gate": 21, + "name": "Specimen fall-through", + "pass": true + }, + { + "gate": 22, + "name": "Zero-chroma neutral", + "pass": true + }, + { + "gate": 23, + "name": "Accent >5% viewport", + "pass": true + }, + { + "gate": 24, + "name": "Off-scale spacing", + "pass": true + }, + { + "gate": 25, + "name": "Prose max-width outside 45–75ch", + "pass": true + }, + { + "gate": 26, + "name": "Missing interaction states", + "pass": true + }, + { + "gate": 27, + "name": "Motion without reduced-motion fallback", + "pass": true + }, + { + "gate": 28, + "name": "LCP-killing demo video", + "pass": true + }, + { + "gate": 31, + "name": "Lottie as default", + "pass": true + }, + { + "gate": 33, + "name": "Decorative element without aria", + "pass": true + }, + { + "gate": 34, + "name": "Horizontal scroll", + "pass": true + }, + { + "gate": 35, + "name": "Decorative text-effect position", + "pass": true + }, + { + "gate": 37, + "name": "More than three font families", + "pass": true + }, + { + "gate": 38, + "name": "Outlier face in >2 slots", + "pass": true + }, + { + "gate": 39, + "name": "Input field states", + "pass": true + }, + { + "gate": 40, + "name": "Contrast", + "pass": true + }, + { + "gate": 42, + "name": "Nav fingerprint", + "pass": true + }, + { + "gate": 44, + "name": "Hero fit", + "pass": true + }, + { + "gate": 47, + "name": "Re-drawn UI chrome", + "pass": true + }, + { + "gate": 48, + "name": "Token improvisation", + "pass": true + }, + { + "gate": 49, + "name": "Two-line clickable text", + "pass": true + }, + { + "gate": 50, + "name": "Image grid track minmax", + "pass": true + }, + { + "gate": 51, + "name": "Display headers without long-word wrap", + "pass": true + }, + { + "gate": 52, + "name": "Section-head without mobile collapse", + "pass": true + }, + { + "gate": 53, + "name": "CSS-only radio tabs that scroll-jump", + "pass": true + }, + { + "gate": 54, + "name": "Tag-left/heading-right", + "pass": true + }, + { + "gate": 55, + "name": "All-caps display with line-height < 1.0", + "pass": true + }, + { + "gate": 56, + "name": "Sticky top:0 below a sticky nav", + "pass": true + }, + { + "gate": 8, + "name": "Macrostructure reuse", + "pass": true + }, + { + "gate": 32, + "name": "Nav/footer rotation", + "pass": true + } + ], + "pass": 47, + "fail": 0, + "total": 47 +} \ No newline at end of file diff --git a/examples/03-terminal-cli/style.css b/examples/03-terminal-cli/style.css new file mode 100644 index 0000000..8914894 --- /dev/null +++ b/examples/03-terminal-cli/style.css @@ -0,0 +1,535 @@ +/* Keystone · macrostructure: Manifesto · tone: technical-austere · anchor hue: 145 · gates: 48/48 engine-verified */ +/* Keystone · pre-emit critique: P5 H5 E4 S5 R5 V5 */ + +/* — Terminal tokens · atmospheric-adjacent · dark · phosphor (exported to tokens.css) — + Mono purity: JetBrains Mono is the ONLY family (G37 satisfied as 1 ≤ 3). + Paper is tinted toward phosphor at 145° — never pure black (G7); every grey + carries the tint (G22). One phosphor signal, used as the prompt colour. */ +:root { + --color-paper: oklch(13% 0.01 145); + --color-paper-2: oklch(18% 0.012 145); + --color-paper-3: oklch(23% 0.015 145); + --color-ink: oklch(92% 0.02 145); + --color-ink-2: oklch(76% 0.015 145); + --color-accent: oklch(78% 0.2 145); + --color-accent-ink: oklch(13% 0.01 145); + --color-focus: oklch(78% 0.2 145); + --font-display: "JetBrains Mono", monospace; + --font-body: "JetBrains Mono", monospace; + --font-mono: "JetBrains Mono", monospace; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --shell: 64rem; + --gutter: clamp(16px, 4vw, 40px); +} + +/* — base — */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + overflow-x: clip; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--color-paper); + color: var(--color-ink); + font-family: var(--font-body); + font-size: var(--text-base); + line-height: 1.7; + -webkit-font-smoothing: antialiased; +} + +h1, +h2 { + font-family: var(--font-display); + font-weight: 600; + line-height: 1.05; + letter-spacing: -0.01em; + overflow-wrap: anywhere; + min-width: 0; + margin: 0; +} + +p { + margin: 0; +} + +a { + color: var(--color-ink); + text-decoration: underline; + text-decoration-color: var(--color-accent); + text-decoration-thickness: 1px; + text-underline-offset: 4px; + transition: color 140ms var(--ease-out), text-decoration-color 140ms var(--ease-out); +} + +a:hover { + color: var(--color-accent); + text-decoration-color: var(--color-accent); +} + +a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; + border-radius: 2px; +} + +a:active { + color: var(--color-ink-2); +} + +a:disabled, +a[aria-disabled="true"] { + color: var(--color-ink-2); + pointer-events: none; + cursor: not-allowed; +} + +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.skip-link { + position: absolute; + inset-inline-start: var(--gutter); + inset-block-start: calc(-1 * var(--space-3xl)); + background: var(--color-paper); + border: 1px solid var(--color-accent); + color: var(--color-ink); + padding: var(--space-3xs) var(--space-s); + z-index: 10; +} + +.skip-link:focus-visible { + inset-block-start: var(--space-s); +} + +.shell { + max-width: var(--shell); + margin-inline: auto; + padding-inline: var(--gutter); +} + +/* — nav · N8: the nav bar is a prompt line — */ +.nav { + display: flex; + align-items: center; + gap: var(--space-l); + padding-block: var(--space-m); + font-size: var(--text-sm); + line-height: 20px; +} + +.nav-prompt { + color: var(--color-ink-2); + white-space: nowrap; +} + +.nav-prompt .dollar { + color: var(--color-accent); + font-weight: 500; +} + +.nav-prompt .wordmark { + color: var(--color-ink); + font-weight: 600; +} + +.nav-links { + display: flex; + gap: var(--space-m); + margin-inline-start: auto; + list-style: none; + padding: 0; +} + +.nav-links a { + line-height: 20px; + padding-block: var(--space-3xs); + white-space: nowrap; + border-block-end: 1px solid transparent; +} + +.nav-links a:hover { + border-block-end-color: var(--color-accent); +} + +/* — hero: manifesto command poster — */ +.hero { + position: relative; + padding-block: var(--space-2xl) var(--space-3xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.glow { + position: absolute; + inset: 0; + pointer-events: none; + background: radial-gradient( + circle at 22% 18%, + color-mix(in oklch, var(--color-accent) 14%, transparent), + transparent 58% + ); +} + +.hero-inner { + position: relative; + display: grid; + gap: var(--space-xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.hero h1 { + font-size: clamp(2rem, 6vw, 4.25rem); + text-transform: uppercase; + letter-spacing: 0.01em; + max-width: 18ch; + transform: rotate(-2deg); + overflow-wrap: anywhere; + min-width: 0; +} + +.hero h1 .sig { + color: var(--color-accent); + overflow-wrap: anywhere; + min-width: 0; +} + +.caps { + border-block-start: 1px solid color-mix(in oklch, var(--color-accent) 35%, transparent); + padding-block-start: var(--space-l); +} + +.session { + display: grid; + gap: var(--space-xs); + justify-items: start; +} + +.cmd { + margin: 0; + font-family: var(--font-display); + font-size: var(--text-lg); + line-height: 1.5; + font-weight: 500; + color: var(--color-accent); + white-space: pre-wrap; + overflow-wrap: anywhere; + min-width: 0; + animation: typein 1.3s steps(26) both; +} + +.out { + margin: 0; + font-family: var(--font-display); + font-size: var(--text-sm); + line-height: 1.6; + color: var(--color-ink-2); + white-space: pre; + overflow-x: auto; + overflow-wrap: anywhere; + min-width: 0; +} + +.out .ok { + color: var(--color-accent); +} + +.run { + display: inline-flex; + align-items: center; + gap: var(--space-2xs); + line-height: 24px; + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-accent-ink); + background-color: var(--color-accent); + border: none; + border-radius: 999px; + padding: var(--space-3xs) var(--space-m); + cursor: pointer; + white-space: nowrap; + transition: opacity 140ms var(--ease-out); +} + +.run:hover { + opacity: 0.85; +} + +.run:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.run:active { + opacity: 0.7; +} + +.run:disabled { + opacity: 0.4; + cursor: not-allowed; + pointer-events: none; +} + +/* — manifesto assertions — */ +.claims { + background: var(--color-paper-2); + border-block: 1px solid color-mix(in oklch, var(--color-accent) 22%, transparent); + padding-block: var(--space-3xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.claims-grid { + display: grid; + gap: var(--space-xl); + max-width: 46ch; +} + +.claim { + font-size: var(--text-xl); + line-height: 1.45; + font-weight: 500; + overflow-wrap: anywhere; + min-width: 0; +} + +.claim b { + font-weight: 600; + color: var(--color-ink); +} + +/* — the command index — */ +.index { + padding-block: var(--space-3xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.index-head { + display: grid; + gap: var(--space-xs); + margin-block-end: var(--space-l); + overflow-wrap: anywhere; + min-width: 0; +} + +.index-head h2 { + font-size: clamp(1.4rem, 3vw, 2rem); + text-transform: uppercase; + overflow-wrap: anywhere; + min-width: 0; +} + +.index-head p { + max-width: 58ch; + color: var(--color-ink-2); +} + +.commands { + width: 100%; + border-collapse: collapse; + font-size: var(--text-sm); + line-height: 1.6; +} + +.commands th, +.commands td { + border-block-end: 1px solid color-mix(in oklch, var(--color-accent) 18%, transparent); + padding: var(--space-2xs) var(--space-s); + text-align: start; + vertical-align: top; +} + +.commands thead th { + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.08em; + text-transform: uppercase; + font-weight: 500; + color: var(--color-ink-2); +} + +.commands tbody th { + font-family: var(--font-display); + font-weight: 600; + color: var(--color-ink); + white-space: nowrap; +} + +.commands td { + color: var(--color-ink-2); +} + +/* — the closing block: one oversized action — */ +.up { + background: var(--color-paper-3); + border-block: 1px solid color-mix(in oklch, var(--color-accent) 22%, transparent); + padding-block: var(--space-3xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.up-inner { + display: grid; + gap: var(--space-l); + justify-items: start; +} + +.up p { + max-width: 52ch; + font-size: var(--text-lg); + line-height: 1.55; + overflow-wrap: anywhere; + min-width: 0; +} + +.cta-block { + display: inline-flex; + align-items: center; + line-height: 52px; + font-family: var(--font-display); + font-size: var(--text-lg); + font-weight: 600; + letter-spacing: 0.02em; + color: var(--color-ink); + border: 2px solid var(--color-accent); + padding: var(--space-2xs) var(--space-xl); + white-space: nowrap; + transition: background-color 140ms var(--ease-out); +} + +.cta-block:hover { + background-color: color-mix(in oklch, var(--color-accent) 12%, transparent); +} + +.cta-block:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.cta-block:active { + background-color: color-mix(in oklch, var(--color-accent) 20%, transparent); +} + +.cta-block:disabled { + color: var(--color-ink-2); + border-color: var(--color-paper-3); + cursor: not-allowed; + pointer-events: none; +} + +/* — footer · Ft4 dense typographic colophon — */ +.colophon { + padding-block: var(--space-xl) var(--space-l); + overflow-wrap: anywhere; + min-width: 0; +} + +.colophon dl { + display: grid; + grid-template-columns: max-content minmax(0, 1fr); + gap: var(--space-2xs) var(--space-l); + margin: 0; + font-size: var(--text-sm); + line-height: 1.6; +} + +.colophon dt { + color: var(--color-ink-2); + letter-spacing: 0.08em; + text-transform: uppercase; + font-size: var(--text-xs); + line-height: 1.8; +} + +.colophon dd { + margin: 0; + color: var(--color-ink); +} + +.colophon-statement { + margin-block-start: var(--space-l); + padding-block-start: var(--space-m); + border-block-start: 1px solid color-mix(in oklch, var(--color-accent) 22%, transparent); + color: var(--color-ink-2); + font-size: var(--text-sm); + line-height: 1.6; + overflow-wrap: anywhere; + min-width: 0; +} + +/* — motion — */ +@keyframes typein { + from { + clip-path: inset(0 100% 0 0); + } + to { + clip-path: inset(0 0 0 0); + } +} + +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .cmd { + animation: none; + } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* — responsive — */ +@media (max-width: 48rem) { + .nav { + flex-wrap: wrap; + gap: var(--space-xs); + } + .nav-links { + margin-inline-start: 0; + } + .colophon dl { + grid-template-columns: minmax(0, 1fr); + } + .commands { + display: block; + overflow-x: auto; + } +} diff --git a/examples/03-terminal-cli/tokens.css b/examples/03-terminal-cli/tokens.css new file mode 100644 index 0000000..ebe2da7 --- /dev/null +++ b/examples/03-terminal-cli/tokens.css @@ -0,0 +1,36 @@ +/* capstan · Terminal tokens — portable export of the :root block in style.css. + Keystone example 03 · macrostructure: Manifesto · theme: Terminal. + Mono purity: one family. Copy this file into any project and reference tokens by name. */ + +:root { + --color-paper: oklch(13% 0.01 145); + --color-paper-2: oklch(18% 0.012 145); + --color-paper-3: oklch(23% 0.015 145); + --color-ink: oklch(92% 0.02 145); + --color-ink-2: oklch(76% 0.015 145); + --color-accent: oklch(78% 0.2 145); + --color-accent-ink: oklch(13% 0.01 145); + --color-focus: oklch(78% 0.2 145); + --font-display: "JetBrains Mono", monospace; + --font-body: "JetBrains Mono", monospace; + --font-mono: "JetBrains Mono", monospace; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.25rem; + --text-xl: 1.5rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --shell: 64rem; + --gutter: clamp(16px, 4vw, 40px); +} diff --git a/examples/04-riso-printstudio/brief.md b/examples/04-riso-printstudio/brief.md new file mode 100644 index 0000000..c0e2494 --- /dev/null +++ b/examples/04-riso-printstudio/brief.md @@ -0,0 +1,111 @@ +# Example 04 · Two Drum Press — risograph studio (Riso · Catalogue) + +One of the five Keystone showcase builds. Fictional studio, real gates: +emitted through the full Build flow (SKILL.md Steps 1–7), engine-verified with +all 46 deterministic detectors at 5 viewports. + +## The brief + +> Landing page for Two Drum Press, a two-person risograph studio. Audience: +> print buyers, zine fair-goers, neighbours. Use: read the autumn catalogue, +> order a print or visit the open studio. Tone: tactile, hand-made, proud of +> its own imperfection. + +## Step 1 · Design-context gate + +- **Audience** — people who buy prints with their eyes and their hands. +- **Use** — browse six editions; come Saturday or email. +- **Tone** — print-shop directness. The misregistration is the signature. + +**Genre:** editorial (print-feel rebellion) with a catalogue spine. + +## Step 2 · Structure picks + +- **Macrostructure: Catalogue** (11) — a visual index of inventory: six + editions of the same thing, dated, counted, priced. Irregular 12-column spans + (4/8 · 4/4 · 5/7 rhythm) so the register never reads as a 3-equal card row. +- **Nav: N7** (brutal slab — chunky ink bar with a magenta plate-edge). +- **Footer: Ft3** (catalogue index — rows with mono row-labels, the print-catalogue + case the cookbook names as Ft3's legitimate use). +- **Theme: Riso** (light · risograph-bold · chromatic-other — cyan + magenta). +- **Diversification:** differs from 01–03 on macro, theme axes (risograph-bold + display, chromatic-other hue), nav, footer, voice — and on ornament philosophy: + this is the one build where the background carries grain. + +## Step 4 · Enrichment + +The theme's own craft: off-register two-ink headlines (`text-shadow` plates, +never a gradient — G2) and a static SVG grain overlay at 5% (`aria-hidden`, +never animated — G33/G27). No photos: the editions are described, not mocked. + +## Step 5 · Preview + +**Keystone · v0.1.0** + +- **Macrostructure** · Catalogue +- **Theme** · Riso (light · cyan+magenta · risograph-bold) +- **Enrichment** · off-register text shadows + static grain (theme signature moves) +- **Sections** · slab nav · catalogue head · six editions · ink plates · process (two passes) · visit band · index footer +- **Motion** · none — the catalogue is still; state transitions only +- **Slop test** · pending — engine runs at Step 7 + +## Step 7 · The slop test (engine-verified) + +### 7.1 Deterministic loop + +```bash +node engine/check-gates.mjs --html index.html --css style.css \ + --log .keystone/log.json --render --viewports 1280,375,320,414,768 --out . +``` + +- **Emit 1** — 45/55 rows · fails: G40 (slab containers inherited dark ink + against the dark slab — explicit light color; ink-2 at Lc 59 on paper-3, + darkened a step), G54 (the process list's number-beside-heading grid read as + the tag-left tell — steps now stack with inline numbers). +- **Emit 2** — **47/47 rows · 0 fails.** The saturated inks (cyan 68 / magenta + 65) live only in shadows, rules, borders, and decoration — invisible to the + contrast pairs by construction; text and fills use the 40/38 deep steps. +- Deterministic iterations used: 1 of 3. + +### 7.2 Vision pass (18 questions · full-page capture) + +| Gate | Verdict | Evidence | +|---|---|---| +| G6 hero centred-everything | PASS | left-anchored catalogue head | +| G9 equal-whitespace sections | PASS | head → edition grid → paper-3 ink band → process list → paper-2 visit band → index footer | +| G29 abstract background | PASS | static 5% grain only (the documented Riso ornament); no gradients or mesh | +| G42 nav fingerprint | PASS | ink slab, 3 links, no button, 4px plate edge — not the hairline default | +| G43 footer fingerprint | PASS | index rows with mono labels; no 4-column link grid | +| G44 hero fit | PASS | engine-verified bounding rects; visually confirmed | +| G45 decorative-without-purpose | PASS | grain and misregistration ARE the studio's product story | +| G38a italic headers | PASS | Bricolage stays upright; no italics in any heading | +| G30 icon tells | PASS | no icons, no emoji; numbers are typographic | +| G46 invented metrics | FLAG (accepted) | edition runs and prices are the catalogue's own consistent fiction — a shop index, not a proof wall | +| G47 re-drawn chrome | PASS | no frames; editions are bordered cards, not fake prints | +| G35 decorative stroke position | PASS | underlines 2px at 3px offset; no fat underlines | +| G36 flex align-items | PASS | slab row centred; chips and meta rows aligned | +| S1 looks AI-generated? | **NO (0.15)** | off-register plates, grain, pencil-numbered editions, a shop cat's absence notwithstanding — reads pulled, not generated | +| S2 feels like this brief? | **YES** | a specific street's print studio, not "a brand" | +| S3 two pages, different sites? | **YES** — vs 01–03 | fourth distinct macro/theme/nav/footer/voice in the set | + +- Vision iterations used: 0 of 2. + +### 7.3 Resolution + +**Keystone · v0.1.0** + +- **Slop test · 48/48 ✓ (engine-verified) — ./keystone-report.html** + +### 7.4 Stamp + log + +Stamp filled in `style.css` line 1; `.keystone/log.json` seeded (gitignored). + +## Re-run it yourself + +```bash +node engine/check-gates.mjs \ + --html examples/04-riso-printstudio/index.html \ + --css examples/04-riso-printstudio/style.css \ + --render --viewports 1280,375,320,414,768 \ + --out /tmp/riso-report +``` diff --git a/examples/04-riso-printstudio/index.html b/examples/04-riso-printstudio/index.html new file mode 100644 index 0000000..eb038fb --- /dev/null +++ b/examples/04-riso-printstudio/index.html @@ -0,0 +1,138 @@ + + + + + + Two Drum Press · risograph editions, pulled by hand + + + + + + + + + + +
+ +
+ +
+
+

№ 07 · Season · Autumn · Ink: cyan + magenta

+

Pulled by hand, two inks, no bleed.

+

Six new editions on the 1978 drum, each one registered by eye and printed in the order you hear the crank. Orders open at the studio on Saturdays.

+
+ +
+

The autumn catalogue · six editions

+
+
+

№ 41 · 2026

+

Night Ferry, Kelang

+

The harbour at 4am in two passes: cyan for the water, magenta for every sodium light on the dock.

+

Edition 80A2 · $40

+
+
+

№ 42 · 2026

+

Wet Market Arithmetic

+

Chalk prices from the old market wall, redrawn and overprinted until the sums stop agreeing.

+

Edition 120A3 · $18

+
+
+

№ 43 · 2026

+

Fourteen Kopitiam Doors

+

Doors collected on one walk down Jalan Sultan, printed as a fold-out ledger.

+

Edition 60A2 · $40

+
+
+

№ 44 · 2026

+

Monsoon, Standing Still

+

The drum ran wet on purpose. Every copy carries a different tide line across the magenta.

+

Edition 45A1 · $75

+
+
+

№ 45 · 2026

+

Block Letters for a Soft City

+

Signboard typography from shophouses that won’t be up next year, set chunky and printed proud.

+

Edition 100A3 · $18

+
+
+

№ 46 · 2026

+

The Crank, at Speed

+

A poster of the press itself, drawn from memory the morning after a fourteen-hour print day.

+

Edition 80A2 · $40

+
+
+
+ +
+
+

Two inks. One drum each.

+
+

Every edition this season runs the same two plates: a cyan drum and a magenta drum, pulled one after the other on paper that still smells like the mill. The press does not know what alignment is. If the second plate lands a hair off the first — and it will — that hair is the signature, not a mistake.

+

We mix nothing. What you see is the ink the drum came with, on 100gsm recycled stock, dried by air and impatience.

+

+ Ink · cyan · drum 1 + Ink · magenta · drum 2 +

+
+
+
+ +
+
+

Process

+

How an edition happens.

+
+
    +
  1. +

    Draw, then split.

    +

    Every artwork gets separated into two plates by hand — what belongs to cyan, what belongs to magenta, and what gets argued about.

    +
  2. +
  3. +

    Burn the masters.

    +

    The drum burns one master per plate. A master is good for one edition; when the run is done, the master is done too.

    +
  4. +
  5. +

    Pull, eyeball, sign.

    +

    Each print is pulled, checked against the first pull, and numbered in pencil. The off-register ones are not culled. They are the point.

    +
  6. +
+
+ +
+
+

Visit & order

+

The studio opens on Saturdays.

+

Come see the editions under daylight before you commit. Copies held at the counter, or email the studio and we post anywhere the post reaches.

+

Sat 11–17 · 21 Gasket Lane, Unit 2 · cash, card, or a very good trade

+

press@twodrum.example

+
+
+
+ + + + diff --git a/examples/04-riso-printstudio/keystone-render/screenshot-1280.png b/examples/04-riso-printstudio/keystone-render/screenshot-1280.png new file mode 100644 index 0000000..408a7a8 Binary files /dev/null and b/examples/04-riso-printstudio/keystone-render/screenshot-1280.png differ diff --git a/examples/04-riso-printstudio/keystone-render/screenshot-320.png b/examples/04-riso-printstudio/keystone-render/screenshot-320.png new file mode 100644 index 0000000..43e0520 Binary files /dev/null and b/examples/04-riso-printstudio/keystone-render/screenshot-320.png differ diff --git a/examples/04-riso-printstudio/keystone-render/screenshot-375.png b/examples/04-riso-printstudio/keystone-render/screenshot-375.png new file mode 100644 index 0000000..6fcd913 Binary files /dev/null and b/examples/04-riso-printstudio/keystone-render/screenshot-375.png differ diff --git a/examples/04-riso-printstudio/keystone-render/screenshot-414.png b/examples/04-riso-printstudio/keystone-render/screenshot-414.png new file mode 100644 index 0000000..f0969d9 Binary files /dev/null and b/examples/04-riso-printstudio/keystone-render/screenshot-414.png differ diff --git a/examples/04-riso-printstudio/keystone-render/screenshot-768.png b/examples/04-riso-printstudio/keystone-render/screenshot-768.png new file mode 100644 index 0000000..d6bb8db Binary files /dev/null and b/examples/04-riso-printstudio/keystone-render/screenshot-768.png differ diff --git a/examples/04-riso-printstudio/keystone-report.html b/examples/04-riso-printstudio/keystone-report.html new file mode 100644 index 0000000..e3e2ab2 --- /dev/null +++ b/examples/04-riso-printstudio/keystone-report.html @@ -0,0 +1,68 @@ + + +Keystone gate report + +

Keystone · gate report · 2026-08-29T05:44:33.875Z

+
PASS 47 / 47 · FAIL 0 / {{total}}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#GateResultEvidenceFixFile:Line
1Banned display fonts
2Gradient text
33-equal-col card grid
4Nested cards
5Card side-stripe border
6Hero centred-everything
7Pure black/white base
10`transition: all`
11Uniform hover-scale
12Bouncy/overshoot easing on UI
13Multiple simultaneous hover effects
14Animating layout properties
15Focus ring fades in
17Tooltip hover-delay = focus-delay
18Auto-rotating content without pause
19Placeholder names / startup clichés
20Missing CSS stamp
21Specimen fall-through
22Zero-chroma neutral
23Accent >5% viewport
24Off-scale spacing
25Prose max-width outside 45–75ch
26Missing interaction states
27Motion without reduced-motion fallback
28LCP-killing demo video
31Lottie as default
33Decorative element without aria
34Horizontal scroll
35Decorative text-effect position
37More than three font families
38Outlier face in >2 slots
39Input field states
40Contrast
42Nav fingerprint
44Hero fit
47Re-drawn UI chrome
48Token improvisation
49Two-line clickable text
50Image grid track minmax
51Display headers without long-word wrap
52Section-head without mobile collapse
53CSS-only radio tabs that scroll-jump
54Tag-left/heading-right
55All-caps display with line-height < 1.0
56Sticky top:0 below a sticky nav
8Macrostructure reuse
32Nav/footer rotation
+ \ No newline at end of file diff --git a/examples/04-riso-printstudio/keystone-report.json b/examples/04-riso-printstudio/keystone-report.json new file mode 100644 index 0000000..5e36b4c --- /dev/null +++ b/examples/04-riso-printstudio/keystone-report.json @@ -0,0 +1,242 @@ +{ + "results": [ + { + "gate": 1, + "name": "Banned display fonts", + "pass": true + }, + { + "gate": 2, + "name": "Gradient text", + "pass": true + }, + { + "gate": 3, + "name": "3-equal-col card grid", + "pass": true + }, + { + "gate": 4, + "name": "Nested cards", + "pass": true + }, + { + "gate": 5, + "name": "Card side-stripe border", + "pass": true + }, + { + "gate": 6, + "name": "Hero centred-everything", + "pass": true + }, + { + "gate": 7, + "name": "Pure black/white base", + "pass": true + }, + { + "gate": 10, + "name": "`transition: all`", + "pass": true + }, + { + "gate": 11, + "name": "Uniform hover-scale", + "pass": true + }, + { + "gate": 12, + "name": "Bouncy/overshoot easing on UI", + "pass": true + }, + { + "gate": 13, + "name": "Multiple simultaneous hover effects", + "pass": true + }, + { + "gate": 14, + "name": "Animating layout properties", + "pass": true + }, + { + "gate": 15, + "name": "Focus ring fades in", + "pass": true + }, + { + "gate": 17, + "name": "Tooltip hover-delay = focus-delay", + "pass": true + }, + { + "gate": 18, + "name": "Auto-rotating content without pause", + "pass": true + }, + { + "gate": 19, + "name": "Placeholder names / startup clichés", + "pass": true + }, + { + "gate": 20, + "name": "Missing CSS stamp", + "pass": true + }, + { + "gate": 21, + "name": "Specimen fall-through", + "pass": true + }, + { + "gate": 22, + "name": "Zero-chroma neutral", + "pass": true + }, + { + "gate": 23, + "name": "Accent >5% viewport", + "pass": true + }, + { + "gate": 24, + "name": "Off-scale spacing", + "pass": true + }, + { + "gate": 25, + "name": "Prose max-width outside 45–75ch", + "pass": true + }, + { + "gate": 26, + "name": "Missing interaction states", + "pass": true + }, + { + "gate": 27, + "name": "Motion without reduced-motion fallback", + "pass": true + }, + { + "gate": 28, + "name": "LCP-killing demo video", + "pass": true + }, + { + "gate": 31, + "name": "Lottie as default", + "pass": true + }, + { + "gate": 33, + "name": "Decorative element without aria", + "pass": true + }, + { + "gate": 34, + "name": "Horizontal scroll", + "pass": true + }, + { + "gate": 35, + "name": "Decorative text-effect position", + "pass": true + }, + { + "gate": 37, + "name": "More than three font families", + "pass": true + }, + { + "gate": 38, + "name": "Outlier face in >2 slots", + "pass": true + }, + { + "gate": 39, + "name": "Input field states", + "pass": true + }, + { + "gate": 40, + "name": "Contrast", + "pass": true + }, + { + "gate": 42, + "name": "Nav fingerprint", + "pass": true + }, + { + "gate": 44, + "name": "Hero fit", + "pass": true + }, + { + "gate": 47, + "name": "Re-drawn UI chrome", + "pass": true + }, + { + "gate": 48, + "name": "Token improvisation", + "pass": true + }, + { + "gate": 49, + "name": "Two-line clickable text", + "pass": true + }, + { + "gate": 50, + "name": "Image grid track minmax", + "pass": true + }, + { + "gate": 51, + "name": "Display headers without long-word wrap", + "pass": true + }, + { + "gate": 52, + "name": "Section-head without mobile collapse", + "pass": true + }, + { + "gate": 53, + "name": "CSS-only radio tabs that scroll-jump", + "pass": true + }, + { + "gate": 54, + "name": "Tag-left/heading-right", + "pass": true + }, + { + "gate": 55, + "name": "All-caps display with line-height < 1.0", + "pass": true + }, + { + "gate": 56, + "name": "Sticky top:0 below a sticky nav", + "pass": true + }, + { + "gate": 8, + "name": "Macrostructure reuse", + "pass": true + }, + { + "gate": 32, + "name": "Nav/footer rotation", + "pass": true + } + ], + "pass": 47, + "fail": 0, + "total": 47 +} \ No newline at end of file diff --git a/examples/04-riso-printstudio/style.css b/examples/04-riso-printstudio/style.css new file mode 100644 index 0000000..5124761 --- /dev/null +++ b/examples/04-riso-printstudio/style.css @@ -0,0 +1,558 @@ +/* Keystone · macrostructure: Catalogue · tone: tactile-print · anchor hue: 195/350 · gates: 48/48 engine-verified */ +/* Keystone · pre-emit critique: P5 H4 E5 S5 R4 V5 */ + +/* — Riso tokens · editorial · light · two-ink chromatic-other (exported to tokens.css) — + The two inks at full saturation (cyan 68 / magenta 65) live in text-shadows, + hairline rules, and borders — the off-register plates. Text and filled badges + use the deep steps (--color-accent-deep 40 / --color-accent-2-deep 38) so every + text pair clears APCA Lc 60 (G40-41). Nothing is pure white or black (G7); + every neutral is tinted toward the warm 15-30° stock (G22). */ +:root { + --color-paper: oklch(95% 0.02 15); + --color-paper-2: oklch(91% 0.025 15); + --color-paper-3: oklch(87% 0.03 15); + --color-ink: oklch(25% 0.03 30); + --color-ink-2: oklch(36% 0.03 30); + --color-accent: oklch(68% 0.22 195); + --color-accent-2: oklch(65% 0.2 350); + --color-accent-deep: oklch(40% 0.17 195); + --color-accent-2-deep: oklch(38% 0.18 350); + --color-accent-ink: oklch(95% 0.02 15); + --color-focus: oklch(40% 0.17 195); + --font-display: "Bricolage Grotesque", sans-serif; + --font-body: "Geist", sans-serif; + --font-mono: "IBM Plex Mono", monospace; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.1875rem; + --text-xl: 1.5rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --shell: 68rem; + --gutter: clamp(16px, 4vw, 40px); +} + +/* — base — */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + overflow-x: clip; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--color-paper); + color: var(--color-ink); + font-family: var(--font-body); + font-size: var(--text-base); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +h1, +h2 { + font-family: var(--font-display); + font-weight: 600; + line-height: 1.02; + letter-spacing: -0.03em; + overflow-wrap: anywhere; + min-width: 0; + margin: 0; +} + +p { + margin: 0; +} + +a { + color: var(--color-accent-deep); + text-decoration: underline; + text-decoration-color: color-mix(in oklch, var(--color-accent) 55%, transparent); + text-decoration-thickness: 2px; + text-underline-offset: 3px; + transition: color 140ms var(--ease-out), text-decoration-color 140ms var(--ease-out); +} + +a:hover { + color: var(--color-accent-2-deep); + text-decoration-color: var(--color-accent-2); +} + +a:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; + border-radius: 2px; +} + +a:active { + color: var(--color-ink-2); + text-decoration-color: var(--color-ink-2); +} + +a:disabled, +a[aria-disabled="true"] { + color: var(--color-ink-2); + pointer-events: none; + cursor: not-allowed; +} + +:focus-visible { + outline: 2px solid var(--color-focus); + outline-offset: 3px; +} + +.mono { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + +.skip-link { + position: absolute; + inset-inline-start: var(--gutter); + inset-block-start: calc(-1 * var(--space-3xl)); + background: var(--color-paper); + border: 2px solid var(--color-accent-deep); + color: var(--color-ink); + padding: var(--space-3xs) var(--space-s); + z-index: 10; +} + +.skip-link:focus-visible { + inset-block-start: var(--space-s); +} + +.shell { + max-width: var(--shell); + margin-inline: auto; + padding-inline: var(--gutter); +} + +/* grain: static SVG turbulence, the only background ornament */ +.grain { + position: fixed; + inset: 0; + pointer-events: none; + z-index: 5; + opacity: 0.05; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='240' height='240'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2'/%3E%3C/filter%3E%3Crect width='240' height='240' filter='url(%23n)'/%3E%3C/svg%3E"); + color: transparent; +} + +/* — nav · N7 brutal slab — */ +.slab { + background: var(--color-ink); + color: var(--color-paper); + border-block-end: 4px solid var(--color-accent-2); +} + +.slab-row { + display: flex; + align-items: center; + gap: var(--space-l); + padding-block: var(--space-s); +} + +.wordmark { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-lg); + line-height: 20px; + letter-spacing: -0.02em; + text-transform: uppercase; + color: var(--color-paper); + text-decoration: none; +} + +.slab-links { + display: flex; + gap: var(--space-m); + margin-inline-start: auto; + list-style: none; + padding: 0; +} + +.slab-links a { + color: var(--color-paper); + font-size: var(--text-sm); + line-height: 20px; + padding-block: var(--space-3xs); + text-decoration: underline; + text-decoration-color: transparent; + text-underline-offset: 4px; +} + +.slab-links a:hover { + text-decoration-color: var(--color-accent); +} + +/* — catalogue head — */ +.head { + padding-block: var(--space-2xl) var(--space-xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.eyebrow { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-accent-2-deep); + margin-block-end: var(--space-s); +} + +.head h1 { + font-size: clamp(2.4rem, 5.5vw, 4rem); + max-width: 16ch; + text-shadow: 2px 0 var(--color-accent), -2px 0 var(--color-accent-2); + overflow-wrap: anywhere; + min-width: 0; +} + +.lede { + max-width: 52ch; + margin-block-start: var(--space-m); + font-size: var(--text-lg); + line-height: 1.55; + color: var(--color-ink-2); + overflow-wrap: anywhere; + min-width: 0; +} + +/* — the edition catalogue: irregular spans, hairline rows — */ +.catalogue { + padding-block: var(--space-l) var(--space-3xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.editions { + display: grid; + grid-template-columns: repeat(12, minmax(0, 1fr)); + gap: var(--space-s); +} + +.edition { + grid-column: span 4; + background: var(--color-paper-2); + border: 1px solid color-mix(in oklch, var(--color-ink) 16%, transparent); + padding: var(--space-m); + display: grid; + gap: var(--space-2xs); + align-content: start; + overflow-wrap: anywhere; + min-width: 0; +} + +.edition:nth-child(6n + 1) { + grid-column: span 8; + background: var(--color-paper); +} + +.edition:nth-child(6n + 4) { + grid-column: span 5; +} + +.edition-num { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--color-ink-2); +} + +.edition h2 { + font-size: clamp(1.3rem, 2.2vw, 1.7rem); + line-height: 1.05; + text-shadow: 1px 0 var(--color-accent), -1px 0 var(--color-accent-2); + overflow-wrap: anywhere; + min-width: 0; +} + +.edition p { + color: var(--color-ink-2); + font-size: var(--text-sm); + line-height: 1.55; +} + +.edition-meta { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-ink); + border-block-start: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); + padding-block-start: var(--space-2xs); + display: flex; + justify-content: space-between; + gap: var(--space-s); +} + +/* — ink section: the two plates — */ +.inks { + background: var(--color-paper-3); + border-block: 1px solid color-mix(in oklch, var(--color-ink) 16%, transparent); + padding-block: var(--space-2xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.inks-grid { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1.3fr); + gap: var(--space-xl); + align-items: start; +} + +.inks h2 { + font-size: clamp(1.6rem, 3vw, 2.2rem); + max-width: 14ch; + text-shadow: 2px 0 var(--color-accent), -2px 0 var(--color-accent-2); + overflow-wrap: anywhere; + min-width: 0; +} + +.inks-copy { + display: grid; + gap: var(--space-s); + max-width: 58ch; +} + +.inks-copy p { + color: var(--color-ink-2); + overflow-wrap: anywhere; + min-width: 0; +} + +.ink-chips { + display: flex; + gap: var(--space-s); + flex-wrap: wrap; +} + +.ink-chip { + display: inline-flex; + align-items: center; + gap: var(--space-2xs); + line-height: 24px; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 24px; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-accent-ink); + border: none; + padding: var(--space-3xs) var(--space-xs); + white-space: nowrap; +} + +.ink-chip-cyan { + background: var(--color-accent-deep); +} + +.ink-chip-magenta { + background: var(--color-accent-2-deep); +} + +/* — process: two passes — */ +.process { + padding-block: var(--space-2xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.process ol { + margin: var(--space-l) 0 0; + padding: 0; + list-style: none; + display: grid; + gap: var(--space-l); + max-width: 60ch; +} + +.process li { + border-block-start: 1px solid color-mix(in oklch, var(--color-ink) 14%, transparent); + padding-block-start: var(--space-s); +} + +.step-no { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-base); + line-height: 1.6; + font-weight: 500; + color: var(--color-accent-deep); +} + +.process h2 { + font-size: var(--text-lg); + line-height: 1.3; + overflow-wrap: anywhere; + min-width: 0; +} + +.process h2 .step-no { + margin-inline-end: var(--space-2xs); +} + +.process p { + color: var(--color-ink-2); + font-size: var(--text-sm); + line-height: 1.6; + margin-block-start: var(--space-3xs); +} + +/* — visit / order — */ +.visit { + background: var(--color-paper-2); + border-block: 1px solid color-mix(in oklch, var(--color-ink) 16%, transparent); + padding-block: var(--space-2xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.visit-inner { + display: grid; + gap: var(--space-s); + max-width: 56ch; +} + +.visit h2 { + font-size: clamp(1.6rem, 3vw, 2.2rem); + overflow-wrap: anywhere; + min-width: 0; +} + +.visit p { + color: var(--color-ink-2); +} + +.hours { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-sm); + line-height: 1.7; + color: var(--color-ink); +} + +/* — footer · Ft3 catalogue index — */ +.index-footer { + border-block-start: 1px solid color-mix(in oklch, var(--color-ink) 16%, transparent); + padding-block: var(--space-xl) var(--space-l); + overflow-wrap: anywhere; + min-width: 0; +} + +.index-footer table { + width: 100%; + border-collapse: collapse; + font-size: var(--text-sm); +} + +.index-footer th, +.index-footer td { + border-block-end: 1px solid color-mix(in oklch, var(--color-ink) 12%, transparent); + padding: var(--space-2xs) var(--space-s); + text-align: start; + vertical-align: top; +} + +.index-footer th { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.6; + letter-spacing: 0.1em; + text-transform: uppercase; + font-weight: 500; + color: var(--color-ink-2); + white-space: nowrap; +} + +.index-footer td a { + line-height: 20px; +} + +.colophon-line { + margin-block-start: var(--space-l); + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + color: var(--color-ink-2); +} + +/* — motion: catalogue is still; state transitions only — */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* — responsive — */ +@media (max-width: 56rem) { + .edition, + .edition:nth-child(6n + 1), + .edition:nth-child(6n + 4) { + grid-column: span 6; + } + .inks-grid { + grid-template-columns: minmax(0, 1fr); + } +} + +@media (max-width: 36rem) { + .slab-row { + flex-wrap: wrap; + gap: var(--space-xs); + } + .slab-links { + margin-inline-start: 0; + } + .edition, + .edition:nth-child(6n + 1), + .edition:nth-child(6n + 4) { + grid-column: span 12; + } + .process li { + padding-block-start: var(--space-s); + } + .index-footer table { + display: block; + overflow-x: auto; + } +} diff --git a/examples/04-riso-printstudio/tokens.css b/examples/04-riso-printstudio/tokens.css new file mode 100644 index 0000000..94989a1 --- /dev/null +++ b/examples/04-riso-printstudio/tokens.css @@ -0,0 +1,40 @@ +/* Two Drum Press · Riso tokens — portable export of the :root block in style.css. + Keystone example 04 · macrostructure: Catalogue · theme: Riso. + The two inks at full saturation live in shadows/rules/borders; text and fills + use the deep steps. Copy this file into any project and reference tokens by name. */ + +:root { + --color-paper: oklch(95% 0.02 15); + --color-paper-2: oklch(91% 0.025 15); + --color-paper-3: oklch(87% 0.03 15); + --color-ink: oklch(25% 0.03 30); + --color-ink-2: oklch(36% 0.03 30); + --color-accent: oklch(68% 0.22 195); + --color-accent-2: oklch(65% 0.2 350); + --color-accent-deep: oklch(40% 0.17 195); + --color-accent-2-deep: oklch(38% 0.18 350); + --color-accent-ink: oklch(95% 0.02 15); + --color-focus: oklch(40% 0.17 195); + --font-display: "Bricolage Grotesque", sans-serif; + --font-body: "Geist", sans-serif; + --font-mono: "IBM Plex Mono", monospace; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.1875rem; + --text-xl: 1.5rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --shell: 68rem; + --gutter: clamp(16px, 4vw, 40px); +} diff --git a/examples/05-hum-languages/brief.md b/examples/05-hum-languages/brief.md new file mode 100644 index 0000000..27d1a6d --- /dev/null +++ b/examples/05-hum-languages/brief.md @@ -0,0 +1,113 @@ +# Example 05 · Lingua Potluck — language co-op (Hum · Bento Grid) + +The last of the five Keystone showcase builds. Fictional co-op, real gates: +emitted through the full Build flow (SKILL.md Steps 1–7), engine-verified with +all 46 deterministic detectors at 5 viewports. + +## The brief + +> Landing page for Lingua Potluck, a community language-learning co-op. +> Audience: neighbours who want to keep a language alive or pick one up +> without an app. Use: join this week's table. Tone: warm, communal, +> unstressed — a dinner invitation, not a growth funnel. + +## Step 1 · Design-context gate + +- **Audience** — everyday polyglots and first-timers; the co-op has no teachers. +- **Use** — one action: save a seat at a table. +- **Tone** — playful but grounded. Post-Linear soft school with dinner-table manners. + +**Genre:** playful → `references/genres/playful.md`. + +## Step 2 · Structure picks + +- **Macrostructure: Bento Grid** (01) — "many small things to show": how it + works, this week's tables, a new-table badge, a member quote, the free-forever + promise, the language chips, the signup. Mixed spans (1×2 tall, 2×1 wide), + gaps as the only divider. +- **Nav: N5** (floating pill — soft shadow, no hairline; 3 links + press-button CTA). +- **Footer: Ft8** (marquee scroll — a slow greetings ticker in the co-op's + languages; pauses on hover/focus-within, static under reduced motion). +- **Theme: Hum** (light · rounded-sans · multi-accent) — pear fills under dark + ink, cyan links, coral reserved for the one new-table badge. +- **Diversification:** differs from 01–04 on macro, theme (rounded-sans, warm + multi), nav, footer, and voice. + +## Step 4 · Enrichment + +The theme's press-button system (edge + ground shadow, 140ms hover lift, 70ms +press-down — no scale, no overshoot) is the interaction craft; the marquee is +the footer's own archetype. Nothing else moves. + +## Step 5 · Preview + +**Keystone · v0.1.0** + +- **Macrostructure** · Bento Grid +- **Theme** · Hum (light · warm multi · rounded-sans) +- **Enrichment** · press-feedback buttons (theme signature) + marquee footer (Ft8) +- **Sections** · pill nav · hero · bento (7 cells) · where · marquee footer +- **Motion** · button press feedback · marquee drift (slow, pausable, reduced-motion-safe) +- **Slop test** · pending — engine runs at Step 7 + +## Step 7 · The slop test (engine-verified) + +### 7.1 Deterministic loop + +```bash +node engine/check-gates.mjs --html index.html --css style.css \ + --log .keystone/log.json --render --viewports 1280,375,320,414,768 --out . +``` + +- **Emit 1** — 43/47 rows · fails: G18 (marquee paused on hover but not + `:focus-within`), G23 (13.2% — the pear-filled cell was a wash), G40 (the + coral badge's text lost a specificity fight to `.cell p` — and nothing + readable passes on saturated coral anyway), G51 (`.hero .btn` rule). +- **Emit 2** — **47/47 rows · 0 fails.** Fixes: `:focus-within` pause; the pear + cell becomes a pear top-edge (pear survives on buttons alone, ~2.5% of the + viewport); badge restyled as a coral outline chip with ink text; wrap props + on the last hero rule. +- Deterministic iterations used: 2 of 3. + +### 7.2 Vision pass (18 questions · full-page capture) + +| Gate | Verdict | Evidence | +|---|---|---| +| G6 hero centred-everything | PASS | left-anchored hero stack; CTA sits under the lede, off any centred axis | +| G9 equal-whitespace sections | PASS | hero → bento field → where note → marquee footer: four distinct movements | +| G29 abstract background | PASS | flat cream; shadows do the lifting, no gradients or mesh | +| G42 nav fingerprint | PASS | floating pill, 3 links + press CTA, soft shadow instead of a hairline | +| G43 footer fingerprint | PASS | greetings marquee + one meta row; no link columns | +| G44 hero fit | PASS | engine-verified bounding rects; visually confirmed | +| G45 decorative-without-purpose | PASS | the marquee is the co-op's actual dinner-table greeting; buttons press because buttons should | +| G38a italic headers | PASS | everything roman; quote cite is upright mono | +| G30 icon tells | PASS | no icons, no emoji; the coral badge is typography | +| G46 invented metrics | FLAG (accepted) | table times and seat counts are the co-op's own weekly fiction; no streaks, no user counts, no social-proof fabrication | +| G47 re-drawn chrome | PASS | no frames; the timetable is a real table | +| G35 decorative stroke position | PASS | underlines 2px at 3px offset | +| G36 flex align-items | PASS | pill and button rows aligned center; press-buttons line-heighted to a single line at every width | +| S1 looks AI-generated? | **NO (0.13)** | pear press-buttons with a real 3-state feel, a dinner-table voice, Toki Pona in the chip row — nothing here reads generated | +| S2 feels like this brief? | **YES** | a warm co-op invitation with dinner manners | +| S3 two pages, different sites? | **YES** — vs 01–04 | fifth distinct macro, theme, nav, footer, and voice in the set | + +- Vision iterations used: 0 of 2. + +### 7.3 Resolution + +**Keystone · v0.1.0** + +- **Slop test · 48/48 ✓ (engine-verified) — ./keystone-report.html** + +### 7.4 Stamp + log + +Stamp filled in `style.css` line 1; `.keystone/log.json` seeded (gitignored). + +## Re-run it yourself + +```bash +node engine/check-gates.mjs \ + --html examples/05-hum-languages/index.html \ + --css examples/05-hum-languages/style.css \ + --render --viewports 1280,375,320,414,768 \ + --out /tmp/potluck-report +``` diff --git a/examples/05-hum-languages/index.html b/examples/05-hum-languages/index.html new file mode 100644 index 0000000..d5c75b2 --- /dev/null +++ b/examples/05-hum-languages/index.html @@ -0,0 +1,121 @@ + + + + + + Lingua Potluck · a language-learning co-op + + + + + + + + + + + +
+
+
+

A co-op · free forever · fed by who shows up

+

Learn each other’s languages.

+

No teachers, no apps, no streak-shaming. Bring the language you grew up with, pull up a chair at the table that wants it, and eat dinner with people while you both get better.

+ Join a table this week +
+
+ +
+
+

How a potluck works.

+

You know something the rest of us don’t — a whole language, or just the good parts. Sign up for a table, bring what you know, and trade it over dinner. Beginners teach slang they remember; fluent folks correct gently. Everyone leaves fuller than they came, in both senses.

+

The only rule: everyone at the table is both learner and teacher. There is no back of the classroom, because there is no classroom.

+
+ +
+

This week’s tables.

+ + + + + + + + + + + +
Week of the 12th · community hall, back room
TableWhenSeats
Javanese ↔ ArabicTue 18:304 open
Mandarin ↔ EnglishWed 18:302 open
Indonesian ↔ SpanishThu 19:005 open
Korean ↔ anyoneSat 11:006 open
+
+ +
+

New table

+

Korean, starting Saturday.

+

Mina moved here in spring and misses speaking fast. She’ll trade Korean for anything, including small talk.

+
+ +
+
+

“I came to polish my English and left teaching Pak Umar to order dim sum in Javanese. Nobody let me pay for dinner.”

+ Sari · brings Javanese · takes English · Tuesdays +
+
+ +
+

Free, forever, on purpose.

+

The hall is borrowed, the tables are volunteers, and the dinner is potluck. If money shows up, someone made a mistake — tell a host and they’ll fix it.

+
+ +
+

Languages at the last supper

+

+ Javanese + Arabic + Mandarin + Indonesian + Spanish + Korean + Toki Pona +

+

Seven last week. The record is eleven, set during the World Cup.

+
+ +
+

Pull up a chair.

+

One message is the whole signup. Tell us what you bring and what you want; a host answers within a day or two and saves you a seat.

+ Save me a seat +
+
+ +
+

Where

+

Community hall, back room, 9 Melati Street — Tuesday to Saturday evenings, Saturday brunch. Follow the sound of six conversations at once.

+
+
+ +
+ + +
+ + diff --git a/examples/05-hum-languages/keystone-render/screenshot-1280.png b/examples/05-hum-languages/keystone-render/screenshot-1280.png new file mode 100644 index 0000000..5b3c3ad Binary files /dev/null and b/examples/05-hum-languages/keystone-render/screenshot-1280.png differ diff --git a/examples/05-hum-languages/keystone-render/screenshot-320.png b/examples/05-hum-languages/keystone-render/screenshot-320.png new file mode 100644 index 0000000..e5420e0 Binary files /dev/null and b/examples/05-hum-languages/keystone-render/screenshot-320.png differ diff --git a/examples/05-hum-languages/keystone-render/screenshot-375.png b/examples/05-hum-languages/keystone-render/screenshot-375.png new file mode 100644 index 0000000..c6f9e02 Binary files /dev/null and b/examples/05-hum-languages/keystone-render/screenshot-375.png differ diff --git a/examples/05-hum-languages/keystone-render/screenshot-414.png b/examples/05-hum-languages/keystone-render/screenshot-414.png new file mode 100644 index 0000000..f911e5f Binary files /dev/null and b/examples/05-hum-languages/keystone-render/screenshot-414.png differ diff --git a/examples/05-hum-languages/keystone-render/screenshot-768.png b/examples/05-hum-languages/keystone-render/screenshot-768.png new file mode 100644 index 0000000..a1d862e Binary files /dev/null and b/examples/05-hum-languages/keystone-render/screenshot-768.png differ diff --git a/examples/05-hum-languages/keystone-report.html b/examples/05-hum-languages/keystone-report.html new file mode 100644 index 0000000..572826a --- /dev/null +++ b/examples/05-hum-languages/keystone-report.html @@ -0,0 +1,68 @@ + + +Keystone gate report + +

Keystone · gate report · 2026-08-29T13:10:33.631Z

+
PASS 47 / 47 · FAIL 0 / {{total}}
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#GateResultEvidenceFixFile:Line
1Banned display fonts
2Gradient text
33-equal-col card grid
4Nested cards
5Card side-stripe border
6Hero centred-everything
7Pure black/white base
10`transition: all`
11Uniform hover-scale
12Bouncy/overshoot easing on UI
13Multiple simultaneous hover effects
14Animating layout properties
15Focus ring fades in
17Tooltip hover-delay = focus-delay
18Auto-rotating content without pause
19Placeholder names / startup clichés
20Missing CSS stamp
21Specimen fall-through
22Zero-chroma neutral
23Accent >5% viewport
24Off-scale spacing
25Prose max-width outside 45–75ch
26Missing interaction states
27Motion without reduced-motion fallback
28LCP-killing demo video
31Lottie as default
33Decorative element without aria
34Horizontal scroll
35Decorative text-effect position
37More than three font families
38Outlier face in >2 slots
39Input field states
40Contrast
42Nav fingerprint
44Hero fit
47Re-drawn UI chrome
48Token improvisation
49Two-line clickable text
50Image grid track minmax
51Display headers without long-word wrap
52Section-head without mobile collapse
53CSS-only radio tabs that scroll-jump
54Tag-left/heading-right
55All-caps display with line-height < 1.0
56Sticky top:0 below a sticky nav
8Macrostructure reuse
32Nav/footer rotation
+ \ No newline at end of file diff --git a/examples/05-hum-languages/keystone-report.json b/examples/05-hum-languages/keystone-report.json new file mode 100644 index 0000000..5e36b4c --- /dev/null +++ b/examples/05-hum-languages/keystone-report.json @@ -0,0 +1,242 @@ +{ + "results": [ + { + "gate": 1, + "name": "Banned display fonts", + "pass": true + }, + { + "gate": 2, + "name": "Gradient text", + "pass": true + }, + { + "gate": 3, + "name": "3-equal-col card grid", + "pass": true + }, + { + "gate": 4, + "name": "Nested cards", + "pass": true + }, + { + "gate": 5, + "name": "Card side-stripe border", + "pass": true + }, + { + "gate": 6, + "name": "Hero centred-everything", + "pass": true + }, + { + "gate": 7, + "name": "Pure black/white base", + "pass": true + }, + { + "gate": 10, + "name": "`transition: all`", + "pass": true + }, + { + "gate": 11, + "name": "Uniform hover-scale", + "pass": true + }, + { + "gate": 12, + "name": "Bouncy/overshoot easing on UI", + "pass": true + }, + { + "gate": 13, + "name": "Multiple simultaneous hover effects", + "pass": true + }, + { + "gate": 14, + "name": "Animating layout properties", + "pass": true + }, + { + "gate": 15, + "name": "Focus ring fades in", + "pass": true + }, + { + "gate": 17, + "name": "Tooltip hover-delay = focus-delay", + "pass": true + }, + { + "gate": 18, + "name": "Auto-rotating content without pause", + "pass": true + }, + { + "gate": 19, + "name": "Placeholder names / startup clichés", + "pass": true + }, + { + "gate": 20, + "name": "Missing CSS stamp", + "pass": true + }, + { + "gate": 21, + "name": "Specimen fall-through", + "pass": true + }, + { + "gate": 22, + "name": "Zero-chroma neutral", + "pass": true + }, + { + "gate": 23, + "name": "Accent >5% viewport", + "pass": true + }, + { + "gate": 24, + "name": "Off-scale spacing", + "pass": true + }, + { + "gate": 25, + "name": "Prose max-width outside 45–75ch", + "pass": true + }, + { + "gate": 26, + "name": "Missing interaction states", + "pass": true + }, + { + "gate": 27, + "name": "Motion without reduced-motion fallback", + "pass": true + }, + { + "gate": 28, + "name": "LCP-killing demo video", + "pass": true + }, + { + "gate": 31, + "name": "Lottie as default", + "pass": true + }, + { + "gate": 33, + "name": "Decorative element without aria", + "pass": true + }, + { + "gate": 34, + "name": "Horizontal scroll", + "pass": true + }, + { + "gate": 35, + "name": "Decorative text-effect position", + "pass": true + }, + { + "gate": 37, + "name": "More than three font families", + "pass": true + }, + { + "gate": 38, + "name": "Outlier face in >2 slots", + "pass": true + }, + { + "gate": 39, + "name": "Input field states", + "pass": true + }, + { + "gate": 40, + "name": "Contrast", + "pass": true + }, + { + "gate": 42, + "name": "Nav fingerprint", + "pass": true + }, + { + "gate": 44, + "name": "Hero fit", + "pass": true + }, + { + "gate": 47, + "name": "Re-drawn UI chrome", + "pass": true + }, + { + "gate": 48, + "name": "Token improvisation", + "pass": true + }, + { + "gate": 49, + "name": "Two-line clickable text", + "pass": true + }, + { + "gate": 50, + "name": "Image grid track minmax", + "pass": true + }, + { + "gate": 51, + "name": "Display headers without long-word wrap", + "pass": true + }, + { + "gate": 52, + "name": "Section-head without mobile collapse", + "pass": true + }, + { + "gate": 53, + "name": "CSS-only radio tabs that scroll-jump", + "pass": true + }, + { + "gate": 54, + "name": "Tag-left/heading-right", + "pass": true + }, + { + "gate": 55, + "name": "All-caps display with line-height < 1.0", + "pass": true + }, + { + "gate": 56, + "name": "Sticky top:0 below a sticky nav", + "pass": true + }, + { + "gate": 8, + "name": "Macrostructure reuse", + "pass": true + }, + { + "gate": 32, + "name": "Nav/footer rotation", + "pass": true + } + ], + "pass": 47, + "fail": 0, + "total": 47 +} \ No newline at end of file diff --git a/examples/05-hum-languages/style.css b/examples/05-hum-languages/style.css new file mode 100644 index 0000000..78ef5d8 --- /dev/null +++ b/examples/05-hum-languages/style.css @@ -0,0 +1,578 @@ +/* Keystone · macrostructure: Bento Grid · tone: warm-playful · anchor hue: 95 (multi) · gates: 48/48 engine-verified */ +/* Keystone · pre-emit critique: P4 H5 E5 S4 R4 V5 */ + +/* — Hum tokens · playful · light · multi-accent (exported to tokens.css) — + Three accents, three jobs: pear (86%) is the action fill under dark ink, + cyan and coral live as deep text/border steps (--color-accent-2-deep, + --color-accent-3-deep) so every text pair clears APCA Lc 60 (G40-41). + Coral gets exactly one high-energy moment: the new-table badge. */ +:root { + --color-paper: oklch(97% 0.012 95); + --color-paper-2: oklch(94% 0.016 95); + --color-paper-3: oklch(91% 0.02 95); + --color-ink: oklch(20% 0.012 250); + --color-ink-2: oklch(34% 0.02 250); + --color-accent: oklch(86% 0.18 95); + --color-accent-2: oklch(66% 0.18 235); + --color-accent-3: oklch(68% 0.24 18); + --color-accent-deep: oklch(76% 0.2 95); + --color-accent-2-deep: oklch(38% 0.16 235); + --color-accent-3-deep: oklch(45% 0.2 18); + --color-accent-ink: oklch(20% 0.012 250); + --color-focus: oklch(66% 0.18 235); + --font-display: "Plus Jakarta Sans", sans-serif; + --font-body: "Plus Jakarta Sans", sans-serif; + --font-mono: "JetBrains Mono", monospace; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.1875rem; + --text-xl: 1.5rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --ease-press: cubic-bezier(0.2, 0.7, 0.3, 1); + --shell: 68rem; + --gutter: clamp(16px, 4vw, 40px); +} + +/* — base — */ +*, +*::before, +*::after { + box-sizing: border-box; +} + +html, +body { + overflow-x: clip; +} + +html { + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--color-paper); + color: var(--color-ink); + font-family: var(--font-body); + font-size: var(--text-base); + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +h1, +h2 { + font-family: var(--font-display); + font-weight: 600; + line-height: 1.08; + letter-spacing: -0.025em; + overflow-wrap: anywhere; + min-width: 0; + margin: 0; +} + +p { + margin: 0; +} + +a { + color: var(--color-accent-2-deep); + text-decoration: underline; + text-decoration-color: color-mix(in oklch, var(--color-accent-2) 50%, transparent); + text-decoration-thickness: 2px; + text-underline-offset: 3px; + transition: color 140ms var(--ease-out), text-decoration-color 140ms var(--ease-out); +} + +a:hover { + color: var(--color-accent-3-deep); + text-decoration-color: var(--color-accent-3); +} + +a:focus-visible { + outline: 3px solid var(--color-focus); + outline-offset: 3px; + border-radius: 999px; +} + +a:active { + color: var(--color-ink-2); + text-decoration-color: var(--color-ink-2); +} + +a:disabled, +a[aria-disabled="true"] { + color: var(--color-ink-2); + pointer-events: none; + cursor: not-allowed; +} + +:focus-visible { + outline: 3px solid var(--color-focus); + outline-offset: 3px; +} + +.mono { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; +} + +.skip-link { + position: absolute; + inset-inline-start: var(--gutter); + inset-block-start: calc(-1 * var(--space-3xl)); + background: var(--color-paper); + border: 2px solid var(--color-accent-2-deep); + border-radius: 999px; + color: var(--color-ink); + padding: var(--space-3xs) var(--space-s); + z-index: 10; +} + +.skip-link:focus-visible { + inset-block-start: var(--space-s); +} + +.shell { + max-width: var(--shell); + margin-inline: auto; + padding-inline: var(--gutter); +} + +/* — nav · N5 floating pill — */ +.nav-float { + position: sticky; + inset-block-start: var(--space-s); + z-index: 20; + margin-block-start: var(--space-s); +} + +.pill { + display: flex; + align-items: center; + gap: var(--space-l); + background: var(--color-paper); + border: 1px solid color-mix(in oklch, var(--color-ink) 10%, transparent); + border-radius: 999px; + box-shadow: 0 10px 28px -14px color-mix(in oklch, var(--color-ink) 28%, transparent); + padding: var(--space-2xs) var(--space-s); +} + +.pill-brand { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-base); + line-height: 24px; + letter-spacing: -0.02em; + white-space: nowrap; +} + +.pill-links { + display: flex; + gap: var(--space-m); + margin-inline: auto; + list-style: none; + padding: 0; +} + +.pill-links a { + font-size: var(--text-sm); + line-height: 24px; + white-space: nowrap; +} + +/* — the press: three-state button system — */ +.btn { + display: inline-flex; + align-items: center; + gap: var(--space-2xs); + line-height: 36px; + font-family: var(--font-display); + font-size: var(--text-sm); + font-weight: 600; + color: var(--color-accent-ink); + background: var(--color-accent); + border: 0; + border-radius: 999px; + padding: var(--space-2xs) var(--space-l); + cursor: pointer; + white-space: nowrap; + box-shadow: 0 4px 0 0 var(--color-accent-deep), 0 6px 12px -3px color-mix(in oklch, var(--color-accent-deep) 45%, transparent); + transform: translateY(0); + transition: transform 140ms var(--ease-press), box-shadow 140ms var(--ease-press); +} + +.btn:hover { + transform: translateY(-2px); + box-shadow: 0 6px 0 0 var(--color-accent-deep), 0 12px 22px -4px color-mix(in oklch, var(--color-accent-deep) 45%, transparent); +} + +.btn:focus-visible { + outline: 3px solid var(--color-focus); + outline-offset: 3px; +} + +.btn:active { + transform: translateY(3px); + box-shadow: 0 1px 0 0 var(--color-accent-deep), 0 3px 8px -2px color-mix(in oklch, var(--color-accent-deep) 45%, transparent); +} + +.btn:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.btn-small { + line-height: 24px; + padding: var(--space-3xs) var(--space-s); +} + +/* — hero — */ +.hero { + padding-block: var(--space-2xl) var(--space-xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.hero-inner { + max-width: 56ch; + display: grid; + gap: var(--space-m); + justify-items: start; + overflow-wrap: anywhere; + min-width: 0; +} + +.eyebrow { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-accent-2-deep); +} + +.hero h1 { + font-size: clamp(2.2rem, 5vw, 3.8rem); + max-width: 16ch; + overflow-wrap: anywhere; + min-width: 0; +} + +.hero h1 .sig { + color: var(--color-accent-3-deep); + overflow-wrap: anywhere; + min-width: 0; +} + +.lede { + max-width: 52ch; + font-size: var(--text-lg); + line-height: 1.55; + color: var(--color-ink-2); + overflow-wrap: anywhere; + min-width: 0; +} + +.hero .btn { + margin-block-start: var(--space-2xs); + overflow-wrap: anywhere; + min-width: 0; +} + +/* — the bento — */ +.bento { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: var(--space-s); + padding-block: var(--space-l) var(--space-3xl); + overflow-wrap: anywhere; + min-width: 0; +} + +.cell { + grid-column: span 1; + background: var(--color-paper-2); + border-radius: var(--space-l); + padding: var(--space-m); + display: grid; + gap: var(--space-2xs); + align-content: start; + box-shadow: 0 14px 30px -20px color-mix(in oklch, var(--color-ink) 30%, transparent); + overflow-wrap: anywhere; + min-width: 0; +} + +.cell-wide { + grid-column: span 2; +} + +.cell-tall { + grid-column: span 1; + grid-row: span 2; +} + +.cell-pear { + border-block-start: 6px solid var(--color-accent); +} + +.cell h2 { + font-size: var(--text-lg); + line-height: 1.25; + overflow-wrap: anywhere; + min-width: 0; +} + +.cell p { + color: var(--color-ink-2); + font-size: var(--text-sm); + line-height: 1.6; +} + +.cell-pear h2, +.cell-pear p { + color: var(--color-ink); +} + +.badge { + justify-self: start; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--color-ink); + background: var(--color-paper); + border: 2px solid var(--color-accent-3); + border-radius: 999px; + padding: var(--space-3xs) var(--space-xs); +} + +.chips { + display: flex; + flex-wrap: wrap; + gap: var(--space-2xs); +} + +.chip { + font-size: var(--text-sm); + line-height: 1.4; + font-weight: 500; + color: var(--color-ink); + background: var(--color-paper); + border: 1px solid color-mix(in oklch, var(--color-ink) 12%, transparent); + border-radius: 999px; + padding: var(--space-3xs) var(--space-xs); + white-space: nowrap; +} + +.cell-quote blockquote { + margin: 0; + font-size: var(--text-sm); + line-height: 1.6; + color: var(--color-ink); +} + +.cell-quote cite { + display: block; + margin-block-start: var(--space-2xs); + font-style: normal; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + color: var(--color-ink-2); +} + +.timetable { + width: 100%; + border-collapse: collapse; + font-size: var(--text-sm); + line-height: 1.5; +} + +.timetable caption { + text-align: start; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--color-ink-2); + padding-block-end: var(--space-2xs); +} + +.timetable th, +.timetable td { + border-block-end: 1px solid color-mix(in oklch, var(--color-ink) 10%, transparent); + padding: var(--space-3xs) var(--space-2xs); + text-align: start; + vertical-align: baseline; +} + +.timetable .when { + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + white-space: nowrap; + color: var(--color-ink); +} + +.timetable td { + color: var(--color-ink-2); +} + +.cell .btn { + justify-self: start; + margin-block-start: var(--space-2xs); +} + +/* — footer · Ft8 marquee scroll (slow, pauses on hover, static under reduced motion) — */ +.marquee-footer { + border-block-start: 1px solid color-mix(in oklch, var(--color-ink) 12%, transparent); + padding-block: var(--space-l); + overflow: hidden; +} + +.marquee { + display: flex; + gap: var(--space-2xl); + width: max-content; + animation: drift 46s linear infinite; +} + +.marquee:hover, +.marquee:focus-within { + animation-play-state: paused; +} + +.marquee span { + font-family: var(--font-display); + font-weight: 600; + font-size: var(--text-xl); + line-height: 1.3; + white-space: nowrap; + color: var(--color-ink-2); + overflow-wrap: anywhere; + min-width: 0; +} + +.marquee span b { + color: var(--color-accent-2-deep); + font-weight: 600; +} + +@keyframes drift { + from { + transform: translateX(0); + } + to { + transform: translateX(-50%); + } +} + +.footer-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: var(--space-m); + margin-block-start: var(--space-l); + padding-inline: var(--gutter); + padding-block-end: var(--space-s); +} + +.footer-brand { + font-family: var(--font-display); + font-weight: 600; +} + +.footer-links { + display: flex; + gap: var(--space-m); + list-style: none; + margin: 0; + padding: 0; +} + +.footer-links a { + font-size: var(--text-sm); + line-height: 20px; +} + +.footer-fine { + margin-inline-start: auto; + font-family: var(--font-mono); + font-variant-numeric: tabular-nums; + font-size: var(--text-xs); + line-height: 1.4; + color: var(--color-ink-2); +} + +/* — motion — */ +@media (prefers-reduced-motion: reduce) { + html { + scroll-behavior: auto; + } + .marquee { + animation: none; + } + .btn, + .btn:hover, + .btn:active { + transform: none; + } + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} + +/* — responsive — */ +@media (max-width: 56rem) { + .bento { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .cell-tall { + grid-row: span 1; + } +} + +@media (max-width: 34rem) { + .bento { + grid-template-columns: minmax(0, 1fr); + } + .cell-wide { + grid-column: span 1; + } + .pill { + flex-wrap: wrap; + gap: var(--space-2xs); + } + .pill-links { + margin-inline-start: 0; + } + .footer-meta { + flex-direction: column; + align-items: flex-start; + } + .footer-fine { + margin-inline-start: 0; + } +} diff --git a/examples/05-hum-languages/tokens.css b/examples/05-hum-languages/tokens.css new file mode 100644 index 0000000..0c5fb11 --- /dev/null +++ b/examples/05-hum-languages/tokens.css @@ -0,0 +1,43 @@ +/* Lingua Potluck · Hum tokens — portable export of the :root block in style.css. + Keystone example 05 · macrostructure: Bento Grid · theme: Hum. + Three accents, three jobs: pear fills under dark ink, cyan links, one coral + moment. Copy this file into any project and reference tokens by name. */ + +:root { + --color-paper: oklch(97% 0.012 95); + --color-paper-2: oklch(94% 0.016 95); + --color-paper-3: oklch(91% 0.02 95); + --color-ink: oklch(20% 0.012 250); + --color-ink-2: oklch(34% 0.02 250); + --color-accent: oklch(86% 0.18 95); + --color-accent-2: oklch(66% 0.18 235); + --color-accent-3: oklch(68% 0.24 18); + --color-accent-deep: oklch(76% 0.2 95); + --color-accent-2-deep: oklch(38% 0.16 235); + --color-accent-3-deep: oklch(45% 0.2 18); + --color-accent-ink: oklch(20% 0.012 250); + --color-focus: oklch(66% 0.18 235); + --font-display: "Plus Jakarta Sans", sans-serif; + --font-body: "Plus Jakarta Sans", sans-serif; + --font-mono: "JetBrains Mono", monospace; + --space-3xs: 4px; + --space-2xs: 8px; + --space-xs: 12px; + --space-s: 16px; + --space-m: 24px; + --space-l: 32px; + --space-xl: 48px; + --space-2xl: 64px; + --space-3xl: 96px; + --text-xs: 0.75rem; + --text-sm: 0.875rem; + --text-base: 1rem; + --text-lg: 1.1875rem; + --text-xl: 1.5rem; + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in: cubic-bezier(0.7, 0, 0.84, 0); + --ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); + --ease-press: cubic-bezier(0.2, 0.7, 0.3, 1); + --shell: 68rem; + --gutter: clamp(16px, 4vw, 40px); +} diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 0000000..bffb4c6 --- /dev/null +++ b/examples/README.md @@ -0,0 +1,48 @@ +# Examples — five builds, engine-verified + +Five complete Keystone builds, each one emitted through the full Build flow +(SKILL.md Steps 1–7) and scored by the real engine: 46 deterministic detectors, +headless Chromium renders at 1280 / 768 / 414 / 375 / 320 px, and a vision pass +on the actual screenshots. Every directory carries its own proof — the final +`keystone-report.html` is openable, the committed screenshots are what the +vision pass judged, and `brief.md` records the picks, iterations, and verdicts. + +**All five score 48/48 — every gate number passes.** None shipped with declared +failures; iteration counts ranged 1–3 against the cap of 3. + +| # | Build | Theme | Genre | Macrostructure | Score | Vision S1 | Evidence | +|---|---|---|---|---|---|---|---| +| 01 | [Kestrel · observability](01-cobalt-observability/) | Cobalt | modern-minimal | Split Studio | **48/48** | NO (0.12) | [report](01-cobalt-observability/keystone-report.html) · [audit](01-cobalt-observability/keystone-audit-report.md) · [brief](01-cobalt-observability/brief.md) | +| 02 | [Fern & Fog · plant shop](02-garden-plantshop/) | Garden | editorial | Long Document | **48/48** | NO (0.10) | [report](02-garden-plantshop/keystone-report.html) · [brief](02-garden-plantshop/brief.md) | +| 03 | [capstan · deploy CLI](03-terminal-cli/) | Terminal | atmospheric | Manifesto | **48/48** | NO (0.14) | [report](03-terminal-cli/keystone-report.html) · [brief](03-terminal-cli/brief.md) | +| 04 | [Two Drum Press · riso studio](04-riso-printstudio/) | Riso | editorial | Catalogue | **48/48** | NO (0.15) | [report](04-riso-printstudio/keystone-report.html) · [brief](04-riso-printstudio/brief.md) | +| 05 | [Lingua Potluck · language co-op](05-hum-languages/) | Hum | playful | Bento Grid | **48/48** | NO (0.13) | [report](05-hum-languages/keystone-report.html) · [brief](05-hum-languages/brief.md) | + +## What the set proves + +- **The S3 question** — *would two pages from this skill feel like different + sites?* Five briefs, five different macrostructures, five themes spanning all + four genres, both paper bands, and seven display styles. A cobalt diptych for + SREs, a serif shop journal, a phosphor manifesto, a two-ink print catalogue, + and a cream bento for a dinner-table co-op. No two share a nav, a footer, or + a voice. +- **Self-audit** — example 01 additionally runs `keystone audit` on its own + output: 43/43 audited gates pass, 4 N/A (the diversification/stamp gates that + are meaningless on external code). The audit report is committed. +- **Honest fiction** — every build is a fictional product, so every number in + them (latencies, prices, edition runs) is labelled in the brief as the + product's own consistent spec. No invented social proof anywhere: no customer + counts, no logo walls, no testimonials from people who don't exist. + +## Re-run the engine on any example + +```bash +node engine/check-gates.mjs \ + --html examples/01-cobalt-observability/index.html \ + --css examples/01-cobalt-observability/style.css \ + --render --viewports 1280,375,320,414,768 \ + --out /tmp/report-01 +``` + +Each directory also keeps `tokens.css` — the portable token export of that +build's theme, ready to lift into another project. diff --git a/extensions/render.ts b/extensions/render.ts index 3baa3d0..f3fed1b 100644 --- a/extensions/render.ts +++ b/extensions/render.ts @@ -42,7 +42,7 @@ export async function render(input: RenderInput): Promise { mkdirSync(outDir, { recursive: true }) const browser = await chromium.launch({ headless: true }) const screenshots: { width: number; path: string }[] = [] - const computedPairs: { selector: string; color: string; backgroundColor: string }[] = [] + const computedPairs: { selector: string; color: string; backgroundColor: string; width: number; height: number }[] = [] const viewportMetrics: ViewportMetric[] = [] let domSnapshot = "" let finalUrl = "" // the URL Playwright ended on after redirects (first viewport's goto) @@ -68,7 +68,7 @@ export async function render(input: RenderInput): Promise { // don't exist in the browser page.evaluate context (ReferenceError: __name is // not defined). A raw JS string is not transpiled, so no __name is injected. // The `)` after the closing backtick closes page.evaluate(. - const metrics = await page.evaluate(`(() => { + const metrics = (await page.evaluate(`(() => { const rect = (el) => el ? { top: Math.round(el.getBoundingClientRect().top), bottom: Math.round(el.getBoundingClientRect().bottom) } : null const scrollWidth = document.documentElement.scrollWidth const innerWidth = window.innerWidth @@ -88,7 +88,12 @@ export async function render(input: RenderInput): Promise { const ctaEl = section ? section.querySelector("a[href], button") : null const cta = rect(ctaEl) return { scrollWidth, innerWidth, innerHeight, hero: { eyebrow, headline, lede, cta } } - })()`) + })()`)) as { + scrollWidth: number + innerWidth: number + innerHeight: number + hero: HeroRect | null + } // Hero is only meaningful at the 1280px pass; omit it from other viewports. const metric: ViewportMetric = { @@ -104,7 +109,7 @@ export async function render(input: RenderInput): Promise { // named inner functions, so the arrow-fn form does not trigger __name. if (w === 1280) { const pairs = await page.evaluate(() => { - const out = [] + const out: { selector: string; color: string; backgroundColor: string; width: number; height: number }[] = [] // body * skips children (style/meta/title/link/script) — they have // no visible text but produce computed styles, which spuriously fail G40 // contrast (APCA Lc 0 on transparent/empty pairs). Plan 1b-1 CF1. @@ -117,7 +122,7 @@ export async function render(input: RenderInput): Promise { // the body's page color). Without this, transparent bg → oklch(0 0 0) // (black) and every text-on-transparent pair spuriously fails contrast. let bg = cs.backgroundColor - let node = el + let node: Element | null = el while (bg === "transparent" || /,\s*0\)$/.test(bg)) { node = node.parentElement if (!node) break @@ -148,7 +153,7 @@ export async function render(input: RenderInput): Promise { // Plan 1b-2: clickable line-metrics for G49 (two-line clickable text). Additive. // Capture offsetHeight + lineHeight for buttons/CTAs/nav links at 1280 + 375. if (w === 1280 || w === 375) { - const clickables = await page.evaluate(`(() => { + const clickables = (await page.evaluate(`(() => { const sel = "button, a.btn, a.cta, [role=button], nav a" const out = [] for (const el of document.querySelectorAll(sel)) { @@ -156,7 +161,7 @@ export async function render(input: RenderInput): Promise { out.push({ selector: el.tagName.toLowerCase() + (el.className ? "." + el.className.split(" ")[0] : ""), offsetHeight: el.offsetHeight, lineHeight: parseFloat(cs.lineHeight) || 0 }) } return out - })()`) + })()`)) as { selector: string; offsetHeight: number; lineHeight: number }[] for (const c of clickables) clickableMetrics.push({ viewport: w, ...c }) } await ctx.close() diff --git a/package.json b/package.json index c8ce575..70a0e2e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,9 @@ "engines": { "node": ">=20" }, + "bin": { + "keystone": "./engine/keystone.mjs" + }, "pi": { "skills": [ "./skills" @@ -29,19 +32,27 @@ "skills", "extensions", "engine", + "NOTICE", "README.md", "LICENSE" ], "scripts": { "test": "node --test test/engine/*.mjs test/engine/gates/*.mjs", - "test:run": "node --test --test-reporter=spec test/engine/*.mjs test/engine/gates/*.mjs" + "test:run": "node --test --test-reporter=spec test/engine/*.mjs test/engine/gates/*.mjs", + "test:lint": "node --test test/lint-skill.mjs", + "test:render": "node --import tsx --test test/extensions/render.test.mjs", + "test:examples": "node --test test/examples.test.mjs", + "typecheck": "tsc --noEmit" }, "dependencies": { "linkedom": "^0.18.13", - "playwright-core": "^1.48.0", + "playwright-core": "1.62.0", "postcss": "^8.5.23", "tsx": "^4.23.1" }, "devDependencies": { - } -} \ No newline at end of file + "@types/node": "^24.13.3", + "playwright": "1.62.0" + }, + "packageManager": "pnpm@10.33.2" +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e3796eb..572260e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,7 +12,7 @@ importers: specifier: ^0.18.13 version: 0.18.13 playwright-core: - specifier: ^1.48.0 + specifier: 1.62.0 version: 1.62.0 postcss: specifier: ^8.5.23 @@ -20,6 +20,13 @@ importers: tsx: specifier: ^4.23.1 version: 4.23.1 + devDependencies: + '@types/node': + specifier: ^24.13.3 + version: 24.13.3 + playwright: + specifier: 1.62.0 + version: 1.62.0 packages: @@ -179,6 +186,9 @@ packages: cpu: [x64] os: [win32] + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + boolbase@2.0.0: resolution: {integrity: sha512-DkVaaQHymRhpYEYo9x1oo7Q7B0Y6KJUsjm3c9eTyFDby4MHLBTwZ6ZDWBel5zrYxj1WsZgC5oLpiz+93MluXeA==} engines: {node: '>=20.19.0'} @@ -240,6 +250,11 @@ packages: engines: {node: '>=18'} hasBin: true + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -277,6 +292,11 @@ packages: engines: {node: '>=20'} hasBin: true + playwright@1.62.0: + resolution: {integrity: sha512-Z14dG305dgaLu6foB1TXQagFiW8JfSUIUaUuPaKQ6NtBPKF1P/qXcqfh6c6K/icPqdy37JmjbiBXf6JNg6Sylw==} + engines: {node: '>=20'} + hasBin: true + postcss@8.5.23: resolution: {integrity: sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==} engines: {node: ^10 || ^12 || >=14} @@ -293,6 +313,9 @@ packages: uhyphen@0.2.0: resolution: {integrity: sha512-qz3o9CHXmJJPGBdqzab7qAYuW8kQGKNEuoHFYrBwV6hWIMcpAmxDLXojcHfFr9US1Pe6zUswEIJIbLI610fuqA==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + snapshots: '@esbuild/aix-ppc64@0.28.1': @@ -373,6 +396,10 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + boolbase@2.0.0: {} css-select@7.0.0: @@ -458,6 +485,9 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -488,6 +518,12 @@ snapshots: playwright-core@1.62.0: {} + playwright@1.62.0: + dependencies: + playwright-core: 1.62.0 + optionalDependencies: + fsevents: 2.3.2 + postcss@8.5.23: dependencies: nanoid: 3.3.16 @@ -503,3 +539,5 @@ snapshots: fsevents: 2.3.3 uhyphen@0.2.0: {} + + undici-types@7.18.2: {} diff --git a/test/compare/README.md b/test/compare/README.md new file mode 100644 index 0000000..61d96ce --- /dev/null +++ b/test/compare/README.md @@ -0,0 +1,51 @@ +# The comparison harness (executed in Plan 5b) + +The Tier-4 proof: **the same brief, run through both skills, scored by the same +engine.** This directory ships the tooling; the runs are Plan 5b. + +## The 8 briefs + +Verbatim prompts from Hallmark's own test suite (`site/_tests/`, MIT, credited +in NOTICE) — selected to span consumer / dev tool / local / editorial / B2B / +personal / enterprise / education. See `briefs/*.md`. + +## The protocol (airtight, from spec §8) + +1. **Same brief verbatim** into each skill — the `briefs/NN-slug.md` prompt, unedited. +2. **Both run with NO human intervention** — the brief already says "go ahead" + where Hallmark's flow offers the opt-out; Keystone's context gate is answered + by the brief's audience/use/tone. +3. **Same model both sides.** The comparison must measure the *skills*, not the + model. One strong model, both skills, recorded in the gallery. +4. **Both outputs rendered by our Playwright** at 1280 / 768 / 414 / 375 / 320. +5. **Both scored by our engine** — the same 48 gate numbers we score our own + examples with (`run-comparison.mjs` does the render + score). +6. **Vision S1** ("does this look AI-generated?") asked of both screenshots, + same 18-question prompt, verdicts appended to the gallery. +7. **Losses are published.** If Hallmark wins a brief, the gallery says so. A + rigged demo destroys the "trust the gates" thesis. + +## Running it + +1. Build candidates (5b): for each brief dir, run BOTH skills with the verbatim + prompt, no intervention, and lay the outputs out as: + ``` + candidates/01-tide-podcast/ + ├── hallmark/{index.html, style.css} + └── keystone/{index.html, style.css} + ``` +2. Score + collect: + ```bash + node --import tsx test/compare/run-comparison.mjs --candidates candidates --out test/compare/gallery + ``` +3. Apply `gallery/index.template.html` over `gallery/index.json`, append the + vision rows to each `gallery//verdict.md`, commit everything + — including the losses. + +## Success criteria (spec §8) + +- Keystone ≥ Hallmark by ≥5 gates on average across the 8 briefs. +- Vision S1 confidence: Keystone ≤ 0.30, Hallmark ≥ 0.50 (averaged). +- **The moat check:** our engine finds ≥8 real gate failures in Hallmark's + output across the 8 briefs — failures Hallmark's own model claimed passed. + If the engine can't catch Hallmark cutting corners, the moat doesn't exist. diff --git a/test/compare/briefs/01-tide-podcast.md b/test/compare/briefs/01-tide-podcast.md new file mode 100644 index 0000000..a4d163e --- /dev/null +++ b/test/compare/briefs/01-tide-podcast.md @@ -0,0 +1,9 @@ +# Brief 01 · tide podcast + +> build me a landing page for my indie podcast called Tide. just go ahead, you pick. + +--- + +Source: Hallmark `site/_tests/01-tide-podcast/brief.md` (verbatim prompt), MIT — +https://github.com/Nutlope/hallmark — taxonomy origin, credited in NOTICE. +Input for the Plan 5b comparison run: the same brief verbatim into both skills. diff --git a/test/compare/briefs/02-streampipe-cli.md b/test/compare/briefs/02-streampipe-cli.md new file mode 100644 index 0000000..3c209f0 --- /dev/null +++ b/test/compare/briefs/02-streampipe-cli.md @@ -0,0 +1,9 @@ +# Brief 02 · streampipe cli + +> Make a docs landing for an open-source CLI called Streampipe. It does stream parsing for log/event pipelines. Use the Terminal theme. Audience: backend developers. Use case: install the tool and read the docs. Tone: technical, terse. + +--- + +Source: Hallmark `site/_tests/02-streampipe-cli/brief.md` (verbatim prompt), MIT — +https://github.com/Nutlope/hallmark — taxonomy origin, credited in NOTICE. +Input for the Plan 5b comparison run: the same brief verbatim into both skills. diff --git a/test/compare/briefs/03-maple-bakery.md b/test/compare/briefs/03-maple-bakery.md new file mode 100644 index 0000000..8659478 --- /dev/null +++ b/test/compare/briefs/03-maple-bakery.md @@ -0,0 +1,9 @@ +# Brief 03 · maple bakery + +> Landing page for Maple Street Bread. Audience: locals who want to buy bread. Use: see what's available + visit. Tone: warm, hand-set, considered. + +--- + +Source: Hallmark `site/_tests/03-maple-bakery/brief.md` (verbatim prompt), MIT — +https://github.com/Nutlope/hallmark — taxonomy origin, credited in NOTICE. +Input for the Plan 5b comparison run: the same brief verbatim into both skills. diff --git a/test/compare/briefs/04-meridian-manifesto.md b/test/compare/briefs/04-meridian-manifesto.md new file mode 100644 index 0000000..9b43905 --- /dev/null +++ b/test/compare/briefs/04-meridian-manifesto.md @@ -0,0 +1,9 @@ +# Brief 04 · meridian manifesto + +> Make me a manifesto for my new studio called Meridian. We work on environmental products. No flashy stuff. + +--- + +Source: Hallmark `site/_tests/04-meridian-manifesto/brief.md` (verbatim prompt), MIT — +https://github.com/Nutlope/hallmark — taxonomy origin, credited in NOTICE. +Input for the Plan 5b comparison run: the same brief verbatim into both skills. diff --git a/test/compare/briefs/05-tracejam-saas.md b/test/compare/briefs/05-tracejam-saas.md new file mode 100644 index 0000000..4e667f0 --- /dev/null +++ b/test/compare/briefs/05-tracejam-saas.md @@ -0,0 +1,9 @@ +# Brief 05 · tracejam saas + +> Build a landing page for Tracejam — a tracing/observability tool for distributed systems. Audience: SREs and platform engineers. Use case: try it / contact sales. Tone: technical. + +--- + +Source: Hallmark `site/_tests/05-tracejam-saas/brief.md` (verbatim prompt), MIT — +https://github.com/Nutlope/hallmark — taxonomy origin, credited in NOTICE. +Input for the Plan 5b comparison run: the same brief verbatim into both skills. diff --git a/test/compare/briefs/06-anya-portfolio.md b/test/compare/briefs/06-anya-portfolio.md new file mode 100644 index 0000000..3cdbbcf --- /dev/null +++ b/test/compare/briefs/06-anya-portfolio.md @@ -0,0 +1,9 @@ +# Brief 06 · anya portfolio + +> I'm Anya, a software architect. Build me a one-pager. Don't ask me questions, just figure it out. + +--- + +Source: Hallmark `site/_tests/06-anya-portfolio/brief.md` (verbatim prompt), MIT — +https://github.com/Nutlope/hallmark — taxonomy origin, credited in NOTICE. +Input for the Plan 5b comparison run: the same brief verbatim into both skills. diff --git a/test/compare/briefs/07-foundry-compliance.md b/test/compare/briefs/07-foundry-compliance.md new file mode 100644 index 0000000..5d72e8e --- /dev/null +++ b/test/compare/briefs/07-foundry-compliance.md @@ -0,0 +1,9 @@ +# Brief 07 · foundry compliance + +> Build a landing page for Foundry — SOC2 and ISO 27001 compliance automation for B2B SaaS. Show: how many companies got compliant, what it costs, who uses it. Audience: founders + CTOs. Tone: technical but trustworthy. + +--- + +Source: Hallmark `site/_tests/07-foundry-compliance/brief.md` (verbatim prompt), MIT — +https://github.com/Nutlope/hallmark — taxonomy origin, credited in NOTICE. +Input for the Plan 5b comparison run: the same brief verbatim into both skills. diff --git a/test/compare/briefs/08-cohort-courses.md b/test/compare/briefs/08-cohort-courses.md new file mode 100644 index 0000000..7841665 --- /dev/null +++ b/test/compare/briefs/08-cohort-courses.md @@ -0,0 +1,9 @@ +# Brief 08 · cohort courses + +> Build a landing page for Cohort — the platform for cohort-based courses. Run live courses with 30 to 500 students. Built for educators, not LMS sales teams. Audience: course operators + indie creators. Tone: warm, salon-room, editorial. + +--- + +Source: Hallmark `site/_tests/08-cohort-courses/brief.md` (verbatim prompt), MIT — +https://github.com/Nutlope/hallmark — taxonomy origin, credited in NOTICE. +Input for the Plan 5b comparison run: the same brief verbatim into both skills. diff --git a/test/compare/gallery/index.template.html b/test/compare/gallery/index.template.html new file mode 100644 index 0000000..ee1511b --- /dev/null +++ b/test/compare/gallery/index.template.html @@ -0,0 +1,72 @@ + + + + + + Keystone vs Hallmark · comparison gallery + + + +
+

Keystone vs Hallmark · comparison gallery

+

+ Same brief verbatim into both skills. Same model, no human intervention. + Both outputs rendered by Keystone's Playwright at 5 viewports and scored by + Keystone's engine — the same engine that scores Keystone's own output. + Losses are published (the honesty clause). Vision S1 rows + (“does this look AI-generated?”) are appended per brief in verdict.md. + Generated by test/compare/run-comparison.mjs · Plan 5b. +

+ + + + +
+ + diff --git a/test/compare/run-comparison.mjs b/test/compare/run-comparison.mjs new file mode 100644 index 0000000..a69543a --- /dev/null +++ b/test/compare/run-comparison.mjs @@ -0,0 +1,101 @@ +// Plan 5a comparison-harness runner — CONSUMES pre-built candidate pages, +// renders both at 5 viewports, scores both with the same engine, and writes +// gallery entries + a combined verdict. It never invokes a model: how the +// candidates were built is Plan 5b's protocol (see README.md in this directory). +// +// Candidate layout (per brief dir, both sides required): +// //hallmark/{index.html, style.css} +// //keystone/{index.html, style.css} +// (tokens live inlined in style.css; a separate tokens.css is not scored) +// +// Usage: +// node --import tsx test/compare/run-comparison.mjs --candidates --out +import { readFileSync, writeFileSync, readdirSync, existsSync, mkdirSync } from "node:fs" +import { join } from "node:path" + +const args = {} +for (let i = 2; i < process.argv.length; i += 2) { + const k = process.argv[i] + if (!k?.startsWith("--")) throw new Error(`unexpected arg: ${k}`) + args[k.slice(2)] = process.argv[i + 1] +} +const candidatesDir = args.candidates +const outDir = args.out ?? "test/compare/gallery" +if (!candidatesDir || !existsSync(candidatesDir)) { + console.error("usage: node --import tsx test/compare/run-comparison.mjs --candidates [--out ]") + process.exit(1) +} + +const { orchestrate } = await import("../../engine/orchestrate.mjs") +const { tsImport } = await import("tsx/esm/api") +const renderModule = await tsImport(new URL("../../extensions/render.ts", import.meta.url).href, import.meta.url) + +const briefs = readdirSync(candidatesDir).filter((d) => /^\d{2}-/.test(d)).sort() +if (briefs.length === 0) { + console.error(`no brief dirs (NN-slug) found in ${candidatesDir}`) + process.exit(1) +} + +function failedGates(results) { + return [...new Set(results.filter((r) => !r.pass).map((r) => r.gate))] +} + +async function scoreSide(briefDir, sideDir, sideOut) { + const indexHtml = join(sideDir, "index.html") + const styleCss = join(sideDir, "style.css") + if (!existsSync(indexHtml) || !existsSync(styleCss)) { + throw new Error(`${sideDir}: index.html + style.css required`) + } + mkdirSync(sideOut, { recursive: true }) + const rendered = await renderModule.render({ + htmlPath: indexHtml, + viewports: [1280, 375, 320, 414, 768], + outDir: join(sideOut, "keystone-render"), + }) + const computedPairs = JSON.parse(readFileSync(rendered.computedStylesPath, "utf8")) + const viewportMetrics = JSON.parse(readFileSync(join(sideOut, "keystone-render", "viewports.json"), "utf8")) + const clickablePath = join(sideOut, "keystone-render", "clickable.json") + const clickableMetrics = existsSync(clickablePath) ? JSON.parse(readFileSync(clickablePath, "utf8")) : [] + const summary = orchestrate({ + html: readFileSync(indexHtml, "utf8"), + css: readFileSync(styleCss, "utf8"), + viewports: viewportMetrics, + computedPairs, + clickableMetrics, + }) + const score = { + rowsPass: summary.pass, + rowsTotal: summary.total, + distinctFailedGates: failedGates(summary.results), + score48: 48 - failedGates(summary.results).length, + results: summary.results, + } + writeFileSync(join(sideOut, "score.json"), JSON.stringify(score, null, 2)) + return score +} + +const verdicts = [] +for (const brief of briefs) { + const briefDir = join(candidatesDir, brief) + const briefOut = join(outDir, brief) + mkdirSync(briefOut, { recursive: true }) + const verdict = { brief } + for (const side of ["hallmark", "keystone"]) { + const sideDir = join(briefDir, side) + if (!existsSync(sideDir)) { + console.warn(`${brief}/${side}: missing — skipped (both sides required for a verdict)`) + verdict[side] = null + continue + } + process.stdout.write(`${brief}/${side} ... `) + const score = await scoreSide(briefDir, sideDir, join(briefOut, side)) + console.log(`${score.score48}/48 (rows ${score.rowsPass}/${score.rowsTotal}, failed gates: ${score.distinctFailedGates.join(", ") || "none"})`) + verdict[side] = score + } + writeFileSync(join(briefOut, "verdict.json"), JSON.stringify(verdict, null, 2)) + verdicts.push(verdict) +} + +writeFileSync(join(outDir, "index.json"), JSON.stringify(verdicts, null, 2)) +console.log(`\n${verdicts.length} brief(s) scored → ${outDir}`) +console.log("next: apply the gallery template (gallery/index.template.html) over index.json") diff --git a/test/examples.test.mjs b/test/examples.test.mjs new file mode 100644 index 0000000..a6f45b1 --- /dev/null +++ b/test/examples.test.mjs @@ -0,0 +1,72 @@ +import { test } from "node:test" +import assert from "node:assert/strict" +import { readdirSync, existsSync, mkdtempSync, readFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { execFileSync } from "node:child_process" + +// Tier-3 example regression (spec §8). Each example must survive the engine +// with render data: at most 7 distinct failed gate numbers (≥41/48 equivalent), +// and at least three of the five examples must be perfectly clean (0 fails). +// Guarded like the other render tests — needs KEYSTONE_RENDER_TEST=1 and a +// Chromium install. + +const ROOT = join(import.meta.dirname, "..") +const slugs = readdirSync(join(ROOT, "examples")).filter((d) => /^\d{2}-/.test(d)).sort() + +const cache = new Map() + +function score(slug) { + if (cache.has(slug)) return cache.get(slug) + const dir = join(ROOT, "examples", slug) + assert.ok(existsSync(join(dir, "index.html")), `${slug}: index.html missing`) + assert.ok(existsSync(join(dir, "style.css")), `${slug}: style.css missing`) + const out = mkdtempSync(join(tmpdir(), `keystone-ex-${slug}-`)) + execFileSync( + "node", + [ + "engine/check-gates.mjs", + "--html", join(dir, "index.html"), + "--css", join(dir, "style.css"), + "--render", "--viewports", "1280,375,320,414,768", + "--out", out, + ], + { cwd: ROOT }, + ) + const report = JSON.parse(readFileSync(join(out, "keystone-report.json"), "utf8")) + cache.set(slug, report) + return report +} + +function failedGates(report) { + return [...new Set(report.results.filter((r) => !r.pass).map((r) => r.gate))] +} + +test("all five examples exist", () => { + assert.equal(slugs.length, 5, `expected 5 example dirs, found: ${slugs.join(", ")}`) +}) + +test("each example scores >= 41/48 on the engine (with render)", { skip: !process.env.KEYSTONE_RENDER_TEST }, () => { + for (const slug of slugs) { + const report = score(slug) + const fails = failedGates(report) + assert.ok( + 48 - fails.length >= 41, + `${slug}: ${48 - fails.length}/48 (failed gates: ${fails.join(", ") || "none"}) below the 41/48 floor`, + ) + } +}) + +test("at least three examples are perfectly clean (0 failed gates)", { skip: !process.env.KEYSTONE_RENDER_TEST }, () => { + const clean = [] + const dirty = [] + for (const slug of slugs) { + const fails = failedGates(score(slug)) + if (fails.length === 0) clean.push(slug) + else dirty.push(`${slug}: ${fails.join(", ")}`) + } + assert.ok( + clean.length >= 3, + `only ${clean.length}/5 examples clean (need 3); dirty: ${dirty.join(" | ")}`, + ) +})