mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-10 02:30:39 +01:00
feat(design-html): craft section and free craft check, adapted from Elaya design skills (GSTA-92)
Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
64d82101ca
commit
fd54f12eb8
15 files changed
+502
-14433
No files matched your search
@@ -0,0 +1,118 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
import { BUZZWORDS, checkCraft, formatCraft, visibleText } from '../lib/design-craft';
|
||||
import { catalogEntry } from '../lib/design-catalog';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const BIN = path.join(ROOT, 'bin', 'gstack-design-craft');
|
||||
|
||||
const DIRTY = `<!doctype html><html><head><style>.hero{height:100vh;transition: all .3s}</style></head><body>
|
||||
<nav><a href="#">Docs</a><a href="/">Home</a><a href="/pricing">Pricing</a></nav>
|
||||
<h1>Elevate your workflow seamlessly</h1><p>Trusted by Acme and John Doe. 99.99% uptime, 50% faster, 10,000+ happy users. [Needs proof: logo]</p>
|
||||
<img src="a.png"><p>Oops! Something broke. Lorem ipsum.</p>
|
||||
<script>window.addEventListener('scroll', () => {}); alert('hi'); showAlert(1);</script></body></html>`;
|
||||
|
||||
const CLEAN = `<!doctype html><html lang="en"><head><title>Ledgerline: close the books in 2 days</title>
|
||||
<meta name="description" content="Close faster."><meta property="og:image" content="/og.png"><link rel="icon" href="/favicon.svg">
|
||||
<style>.hero{min-height:100vh;min-height:100dvh;transition: transform .4s var(--ease-out)} .w{width:100%}</style></head>
|
||||
<body><a href="#main">Skip to content</a><a href="#pricing">Pricing</a><nav><a href="/" aria-current="page">Home</a><a href="/pricing">Pricing</a></nav>
|
||||
<main id="main"><h1>Close the books in 2 days, not 9</h1><p>Teams at Halvorsen Freight cut month-end by 47.2%. Save 20% yearly. 100% open source. 30-day refund.</p><img src="x.png" alt=""></main>
|
||||
<script>const io = new IntersectionObserver(() => {}); window.showAlert = () => {};</script></body></html>`;
|
||||
|
||||
function run(file?: string) {
|
||||
const r = Bun.spawnSync([BIN, ...(file ? [file] : [])], { stdout: 'pipe', stderr: 'pipe', timeout: 30_000 });
|
||||
return { code: r.exitCode, out: r.stdout.toString(), err: r.stderr.toString() };
|
||||
}
|
||||
|
||||
describe('design-craft check', () => {
|
||||
test('a page with every tell reports each one with its count', () => {
|
||||
const counts = Object.fromEntries(checkCraft(DIRTY).map(f => [f.id, f.count]));
|
||||
expect(counts).toEqual({
|
||||
'placeholder-name': 1, 'placeholder-brand': 1, lorem: 1, 'marketing-buzzword': 2, 'round-number': 3, 'needs-proof': 1, oops: 1,
|
||||
'scroll-listener': 1, 'transition-all': 1, 'dead-link': 1, alert: 1, 'img-no-alt': 1, 'vh-height': 1,
|
||||
'missing-title': 1, 'missing-meta-description': 1, 'missing-favicon': 1,
|
||||
'missing-skip-link': 1, 'missing-aria-current': 1,
|
||||
});
|
||||
});
|
||||
|
||||
test('a finished page is clean: CSS 100%, a 100vh line with its 100dvh fallback, in-page anchors, a showAlert helper and inlined Pretext are not flagged', () => {
|
||||
expect(checkCraft(CLEAN)).toEqual([]);
|
||||
const pretext = fs.readFileSync(path.join(ROOT, 'design-html', 'vendor', 'pretext.js'), 'utf-8');
|
||||
expect(checkCraft(CLEAN.replace('</body>', `<script type="module">${pretext}</script></body>`))).toEqual([]);
|
||||
expect(formatCraft([])).toBe('CRAFT_RESULT: clean');
|
||||
});
|
||||
|
||||
test('BUZZWORDS covers every word the catalog marketing-buzzword heuristic names', () => {
|
||||
const heuristic = catalogEntry('marketing-buzzword')?.heuristic ?? '';
|
||||
const words = heuristic.replace(/^.*?for /, '').replace(/\.$/, '').split(/,\s*/);
|
||||
expect(words.length).toBeGreaterThan(5);
|
||||
for (const word of words) expect(BUZZWORDS).toContain(word);
|
||||
expect(checkCraft('<p>Unlocking seamless, next gen workflows that elevate teams</p>').map(f => [f.id, f.count])).toEqual([['marketing-buzzword', 4]]);
|
||||
expect(checkCraft('<p>Take the elevator to the elevation map; streamers sold here</p>')).toEqual([]);
|
||||
});
|
||||
|
||||
test('the skip link must reach an existing id, and aria-current is required only for route navigation', () => {
|
||||
const ids = (html: string) => checkCraft(html).map(f => f.id);
|
||||
expect(ids(CLEAN.replace('id="main"', 'id="content"'))).toEqual(['missing-skip-link']);
|
||||
const sectionNav = CLEAN.replace('<a href="/" aria-current="page">Home</a><a href="/pricing">Pricing</a>', '<a href="#features">Features</a><a href="#faq">FAQ</a>');
|
||||
expect(ids(sectionNav)).toEqual([]);
|
||||
expect(ids(CLEAN.replace(' aria-current="page"', ''))).toEqual(['missing-aria-current']);
|
||||
});
|
||||
|
||||
test('attribute look-alikes, encoded markers and 100vh ordering are read correctly', () => {
|
||||
const ids = (html: string) => checkCraft(html).map(f => f.id);
|
||||
expect(ids('<img src="a.png" data-alt="x">')).toEqual(['img-no-alt']);
|
||||
expect(ids('<p>[Needs proof: logo]</p>')).toEqual(['needs-proof']);
|
||||
expect(ids(CLEAN.replace('id="main"', 'data-id="main"'))).toEqual(['missing-skip-link']);
|
||||
expect(ids('<style>.a{min-height:100dvh;min-height:100vh}</style>')).toEqual(['vh-height']);
|
||||
expect(ids('<style>.a{min-height:100vh !important;min-height:100dvh}</style>')).toEqual(['vh-height']);
|
||||
});
|
||||
|
||||
test('every word and name the craft section bans is one the checker flags', () => {
|
||||
const section = fs.readFileSync(path.join(ROOT, 'design-html', 'sections', 'craft.md.tmpl'), 'utf-8');
|
||||
const buzz = section.match(/No buzzwords: ([^\n]+)\.\n/)?.[1].replace(/"/g, '').split(/,\s*/) ?? [];
|
||||
expect(buzz.length).toBeGreaterThan(10);
|
||||
for (const word of buzz) expect(BUZZWORDS).toContain(word);
|
||||
const names = section.match(/No placeholder people or brands \(([^)]+)\)/)?.[1].split(/,\s*/) ?? [];
|
||||
expect(names.length).toBe(5);
|
||||
for (const name of names) expect(checkCraft(`<p>${name}</p>`).map(f => f.id)).toEqual([/Doe/.test(name) ? 'placeholder-name' : 'placeholder-brand']);
|
||||
});
|
||||
|
||||
test('skip link is judged by position and target; alert() counts only in script; svg text is copy', () => {
|
||||
const ids = (html: string) => checkCraft(html).map(f => f.id);
|
||||
expect(ids(CLEAN.replace('>Skip to content<', '>Zum Inhalt springen<'))).toEqual([]);
|
||||
expect(ids(CLEAN.replace('<a href="#main">Skip to content</a>', ''))).toEqual(['missing-skip-link']);
|
||||
expect(ids('<p>Set a price alert (free)</p>')).toEqual([]);
|
||||
expect(ids('<button onclick="alert(1)">Go</button>')).toEqual(['alert']);
|
||||
expect(ids('<svg><text>Acme</text></svg>')).toEqual(['placeholder-brand']);
|
||||
});
|
||||
|
||||
test('copy rules read visible text only; head rules skip a fragment', () => {
|
||||
expect(visibleText('<style>.a{content:"Acme"}</style><script>// John Doe</script><p>Halvorsen</p>')).toBe(' Halvorsen ');
|
||||
expect(checkCraft('<section><h2>Pricing</h2><a href="/buy">Start free trial</a></section>')).toEqual([]);
|
||||
});
|
||||
|
||||
test('the CLI prints CRAFT_CHECK lines and exits 1 on tells, 0 when clean, 2 on usage', () => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'design-craft-'));
|
||||
try {
|
||||
fs.writeFileSync(path.join(dir, 'dirty.html'), DIRTY);
|
||||
fs.writeFileSync(path.join(dir, 'clean.html'), CLEAN);
|
||||
const dirty = run(path.join(dir, 'dirty.html'));
|
||||
expect(dirty.code).toBe(1);
|
||||
expect(dirty.out).toContain('CRAFT_CHECK: scroll-listener 1 ');
|
||||
expect(dirty.out.trim().split('\n').at(-1)).toBe('CRAFT_RESULT: 18 issue(s)');
|
||||
const clean = run(path.join(dir, 'clean.html'));
|
||||
expect(clean).toMatchObject({ code: 0, out: 'CRAFT_RESULT: clean\n' });
|
||||
expect(run().code).toBe(2);
|
||||
expect(run(path.join(dir, 'missing.html')).code).toBe(2);
|
||||
fs.writeFileSync(path.join(dir, 'empty.html'), ' \n');
|
||||
expect(run(path.join(dir, 'empty.html')).code).toBe(2);
|
||||
fs.writeFileSync(path.join(dir, 'page.tsx'), 'export const P = () => <p>Acme</p>;');
|
||||
expect(run(path.join(dir, 'page.tsx'))).toMatchObject({ code: 2, out: '' });
|
||||
} finally {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -736,8 +736,8 @@ do not launch the downstream skill or open a browser.`,
|
||||
// (D3A: read-on-demand doctrine, requiredReads-guarded + loading eval)
|
||||
'design-html': {
|
||||
skill: 'design-html',
|
||||
expectedSections: ['doctrine.md', 'pretext-patterns.md', 'detector-install-offer.md'],
|
||||
requiredReads: ['doctrine.md', 'pretext-patterns.md'],
|
||||
expectedSections: ['doctrine.md', 'pretext-patterns.md', 'craft.md', 'detector-install-offer.md'],
|
||||
requiredReads: ['doctrine.md', 'pretext-patterns.md', 'craft.md'],
|
||||
scenario:
|
||||
'Walk /design-html in SIMULATION — do not run bash, start servers, launch a browser, or take screenshots. Treat Step 0 as already resolved: no CEO plan, no approved mockup, no variants, no DESIGN.md, no prior finalized.html — freeform mode (Case C option D), screen name "pricing", the user wants a pricing page for a developer-tools SaaS (dark, dense, three tiers, monospace-leaning). Do NOT use AskUserQuestion — proceed with the stated assumptions. Read each pointed section before doing its step, then execute Steps 1-3: produce the implementation spec, state the chosen Pretext tier and why, and generate the complete Pretext-native HTML — include the HTML in your report instead of writing files. Stop there: skip Step 3.5, Step 4, and Step 5.',
|
||||
staticInvariants: {
|
||||
@@ -759,7 +759,7 @@ do not launch the downstream skill or open a browser.`,
|
||||
gateAfterStop: undefined, // operational skill, no plan-mode gate
|
||||
},
|
||||
behavioral: 'prompt',
|
||||
maxSkeletonBytes: 56_200, // measured 55,262 (2026-09-09): the detector install offer pointer + its sections-table row (the brief itself lives in sections/detector-install-offer.md); before that 54,545 for the review-cycle trust prose, the Slop Gate's Decisions-Log clause, and the blacklist header's override sentence + Honest Work wave merged (W2/W3 gate integrity in review/ship, W4 kernel + DONE line in every tier-2+ skill, W5/W7 in spec/ship): re-measured on the integrated tree (2026-10-10).
|
||||
maxSkeletonBytes: 56_200, // measured 55,786 (2026-10-25): the craft section pointer, its index row and the Step 4 craft-check line (GSTA-92); before that 55,262 (2026-09-09): the detector install offer pointer + its sections-table row (the brief itself lives in sections/detector-install-offer.md); before that 54,545 for the review-cycle trust prose, the Slop Gate's Decisions-Log clause, and the blacklist header's override sentence + Honest Work wave merged (W2/W3 gate integrity in review/ship, W4 kernel + DONE line in every tier-2+ skill, W5/W7 in spec/ship): re-measured on the integrated tree (2026-10-10).
|
||||
minUnionBytes: 57_500, // Phase 4 wave 4; measured union 58,682
|
||||
mustContain: ["Don't make me think", "Users scan, they don't read", 'The Goodwill Reservoir', 'PRETEXT API CHEATSHEET', 'Pattern 3: Text around obstacles'],
|
||||
},
|
||||
|
||||
@@ -1316,8 +1316,8 @@ describeIfSelected('Design HTML slop gate E2E', ['design-html-slop-gate'], () =>
|
||||
testConcurrentIfSelected('design-html-slop-gate', async () => {
|
||||
const result = await runSkillTest({
|
||||
prompt: `Read design-html-gate.md: the /design-html detector probe block and its "Slop Gate (bounded, never a loop)" step.
|
||||
finalized.html in this directory is the finished page. Run the probe (--host claude), then the slop gate on finalized.html exactly as written: one surgical fix pass over the non-advisory findings, one rescan, then stop.
|
||||
Write ${workDir}/gate-output.md listing what you fixed and every remaining finding as accepted-with-reason, each tagged with its [rule-id]. Do not take screenshots, do not run npx, do not scan more than twice.`,
|
||||
finalized.html in this directory is the finished page. Run the probe (--host claude), then the slop gate on finalized.html exactly as written: one surgical fix pass over the non-advisory findings, one rescan. Then the craft check line that follows it, the same way, then stop.
|
||||
Write ${workDir}/gate-output.md listing what you fixed and every remaining finding as accepted-with-reason, each tagged with its [rule-id] or craft id. Do not take screenshots, do not run npx, do not run either check more than twice.`,
|
||||
workingDirectory: workDir,
|
||||
maxTurns: 20,
|
||||
timeout: CAPTURE_MS,
|
||||
@@ -1332,6 +1332,9 @@ Write ${workDir}/gate-output.md listing what you fixed and every remaining findi
|
||||
const scans = result.toolCalls.filter(c => c.tool === 'Bash' && /gstack-design-detect\.ts scan /.test(String(c.input?.command ?? '')));
|
||||
expect(scans.length).toBeGreaterThanOrEqual(1);
|
||||
expect(scans.length).toBeLessThanOrEqual(2);
|
||||
const craftRuns = result.toolCalls.filter(c => c.tool === 'Bash' && /gstack-design-craft\b/.test(String(c.input?.command ?? '')));
|
||||
expect(craftRuns.length).toBeGreaterThanOrEqual(1);
|
||||
expect(craftRuns.length).toBeLessThanOrEqual(2);
|
||||
const outPath = path.join(workDir, 'gate-output.md');
|
||||
expect(fs.existsSync(outPath)).toBe(true);
|
||||
const out = fs.readFileSync(outPath, 'utf-8').toLowerCase();
|
||||
|
||||
Reference in new issue
Block a user