@hecer/yoke 0.2.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +112 -0
- package/README.md +557 -494
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/config.yaml +6 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/context/DECISIONS.md +9 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/.yoke/prd.yaml +38 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/bench-verify.mjs +15 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/package.json +9 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/src/index.mjs +48 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-1.test.mjs +24 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-2.test.mjs +28 -0
- package/bench/.runs/claude-2026-07-09T22-34-01/tests/STORY-3.test.mjs +25 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/config.yaml +6 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/.yoke/prd.yaml +32 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/bench-verify.mjs +15 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/package.json +9 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/src/index.mjs +3 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-1.test.mjs +24 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-2.test.mjs +28 -0
- package/bench/.runs/gemini-2026-07-09T22-34-02/tests/STORY-3.test.mjs +25 -0
- package/bench/README.md +42 -0
- package/bench/RESULTS.md +36 -0
- package/bench/fixtures/string-kit/.yoke/config.yaml +6 -0
- package/bench/fixtures/string-kit/.yoke/prd.yaml +32 -0
- package/bench/fixtures/string-kit/bench-verify.mjs +15 -0
- package/bench/fixtures/string-kit/package.json +9 -0
- package/bench/fixtures/string-kit/src/index.mjs +3 -0
- package/bench/fixtures/string-kit/tests/STORY-1.test.mjs +24 -0
- package/bench/fixtures/string-kit/tests/STORY-2.test.mjs +28 -0
- package/bench/fixtures/string-kit/tests/STORY-3.test.mjs +25 -0
- package/bench/results/claude-2026-07-09T22-34-01.json +40 -0
- package/bench/run.mjs +115 -0
- package/canon/loop/loop-spec.md +5 -1
- package/canon/manifest.yaml +3 -0
- package/canon/skills/yoke-retrofit/SKILL.md +1 -0
- package/canon/tools/claude-mem.md +15 -0
- package/canon/tools/ui-ux-pro-max.md +15 -0
- package/dist/cli.js +32 -6
- package/dist/loop/cleanup.js +44 -3
- package/dist/loop/loop.js +27 -2
- package/dist/loop/reporter.js +21 -3
- package/dist/loop/run-command.js +19 -6
- package/dist/loop/runner.js +130 -13
- package/dist/loop/watchdog.js +67 -17
- package/dist/retrofit/apply.js +10 -3
- package/dist/retrofit/config.js +2 -0
- package/dist/retrofit/gitignore.js +2 -0
- package/dist/retrofit/planners/claude.js +4 -1
- package/dist/retrofit/planners/gemini.js +2 -1
- package/dist/retrofit/preserve.js +51 -0
- package/dist/update/check.js +86 -0
- package/dist/update/refresh.js +28 -0
- package/dist/update/upgrade.js +30 -0
- package/docs/PUBLISHING.md +41 -0
- package/docs/superpowers/plans/2026-06-27-forge-a-canon.md +815 -0
- package/docs/superpowers/plans/2026-06-27-forge-b1-retrofit-claude.md +842 -0
- package/docs/superpowers/plans/2026-06-27-forge-b2-codex-gemini-tools.md +884 -0
- package/docs/superpowers/plans/2026-06-27-forge-c1-loop-engine.md +891 -0
- package/docs/superpowers/plans/2026-06-28-baustein-e-context-layer.md +981 -0
- package/docs/superpowers/plans/2026-06-28-forge-b3-settings-merge.md +364 -0
- package/docs/superpowers/plans/2026-06-28-forge-c2-loop-verify.md +502 -0
- package/docs/superpowers/plans/2026-06-28-forge-c3-multi-agent-runners.md +377 -0
- package/docs/superpowers/plans/2026-06-28-forge-c4-worktree-isolation.md +413 -0
- package/docs/superpowers/plans/2026-06-28-forge-c5-review-iteration.md +419 -0
- package/docs/superpowers/plans/2026-06-28-forge-cleanup-dep0190.md +233 -0
- package/docs/superpowers/plans/2026-06-28-forge-d-codegraph-choice-minimal-code.md +523 -0
- package/docs/superpowers/plans/2026-06-29-baustein-f-routing.md +258 -0
- package/docs/superpowers/plans/2026-06-29-baustein-g-loop-observability.md +1006 -0
- package/docs/superpowers/plans/2026-06-29-baustein-h-loop-robustness.md +374 -0
- package/docs/superpowers/plans/2026-06-30-baustein-i-visual-design-verification.md +450 -0
- package/docs/superpowers/plans/2026-07-02-baustein-j-cross-model-review-gstack-compose.md +645 -0
- package/docs/superpowers/plans/2026-07-02-baustein-k-zero-to-100-bootstrap.md +1024 -0
- package/docs/superpowers/plans/2026-07-02-baustein-m-flow-smoke-proofs.md +574 -0
- package/docs/superpowers/specs/2026-06-27-forge-cross-agent-harness-design.md +144 -0
- package/docs/superpowers/specs/2026-06-28-baustein-e-context-layer-design.md +146 -0
- package/docs/superpowers/specs/2026-06-29-baustein-f-routing-design.md +106 -0
- package/docs/superpowers/specs/2026-06-29-baustein-g-loop-observability-design.md +186 -0
- package/docs/superpowers/specs/2026-06-29-baustein-h-loop-robustness-design.md +113 -0
- package/docs/superpowers/specs/2026-06-30-baustein-i-visual-design-verification-design.md +98 -0
- package/docs/superpowers/specs/2026-07-02-baustein-j-cross-model-review-gstack-compose-design.md +137 -0
- package/docs/superpowers/specs/2026-07-02-baustein-k-zero-to-100-bootstrap-design.md +200 -0
- package/docs/superpowers/specs/2026-07-02-baustein-m-flow-smoke-proofs-design.md +155 -0
- package/docs/superpowers/specs/2026-07-10-companion-tools-decision.md +33 -0
- package/docs/superpowers/specs/2026-07-10-multi-agent-parallel-loop-design.md +99 -0
- package/package.json +8 -2
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# Baustein I — Visual & Design Verification Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Add a static AI-slop **design scanner** (`yoke design-scan`, gated on exit code) plus two canon skills (`unslop-ui`, `visual-verification`) so the loop's verify gate can cover visual/design quality and user-flow smoke, not just unit tests.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A pure `src/scan/design.ts` (curated tell set + `scanText`/`scanDir`) wired to a `yoke design-scan` CLI command (`runDesignScan`), plus markdown canon skills + manifest/attribution/README. No browser is embedded — flow-smoke/video is methodology in the `visual-verification` skill using the already-wired Playwright MCP.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Node.js + TypeScript (ESM, `.js` specifiers, strict), vitest. `npx vitest run`, `npx tsc --noEmit`.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## File Structure
|
|
14
|
+
|
|
15
|
+
| File | Change |
|
|
16
|
+
|------|--------|
|
|
17
|
+
| `src/scan/design.ts` (create) | `Tell`/`Finding`/`ScanResult` types, `TELLS`, `scanText`, `scanDir` |
|
|
18
|
+
| `src/cli.ts` (modify) | `runDesignScan` + `design-scan` case + usage |
|
|
19
|
+
| `canon/skills/unslop-ui/SKILL.md` (create) | design-tells rubric |
|
|
20
|
+
| `canon/skills/visual-verification/SKILL.md` (create) | verify-pipeline + flow-smoke + video-on-failure |
|
|
21
|
+
| `canon/manifest.yaml` (modify) | register both skills |
|
|
22
|
+
| `canon/skills/ATTRIBUTION.md` (modify) | credit vibecoded-design-tells (MIT) |
|
|
23
|
+
| `tests/scan/design.test.ts`, `tests/scan/design-cli.test.ts` (create) | scanner + CLI tests |
|
|
24
|
+
| `tests/canon/real-canon.test.ts` (modify) | assert both skills registered |
|
|
25
|
+
| `README.md` (modify) | Visual-verification section + catalog 24→26 + badge sync |
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
### Task 1: the static scanner — `src/scan/design.ts`
|
|
30
|
+
|
|
31
|
+
**Files:** Create `src/scan/design.ts`; Test `tests/scan/design.test.ts`
|
|
32
|
+
|
|
33
|
+
- [ ] **Step 1: Write the failing test**
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// tests/scan/design.test.ts
|
|
37
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest'
|
|
38
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'
|
|
39
|
+
import { join } from 'node:path'
|
|
40
|
+
import { tmpdir } from 'node:os'
|
|
41
|
+
import { scanText, scanDir, TELLS } from '../../src/scan/design.js'
|
|
42
|
+
|
|
43
|
+
describe('scanText', () => {
|
|
44
|
+
it('flags AI-purple hex and tailwind purple gradients', () => {
|
|
45
|
+
expect(scanText('color: #6c5ce7;').some(m => m.tell.name === 'ai-purple')).toBe(true)
|
|
46
|
+
expect(scanText('<div class="bg-gradient-to-r from-purple-500 to-violet-600">').some(m => m.tell.name === 'ai-purple')).toBe(true)
|
|
47
|
+
})
|
|
48
|
+
it('flags gradient hero text (clip-text + transparent)', () => {
|
|
49
|
+
expect(scanText('<h1 class="bg-clip-text text-transparent bg-gradient-to-r">').some(m => m.tell.name === 'gradient-clip-text')).toBe(true)
|
|
50
|
+
})
|
|
51
|
+
it('flags neon glow', () => {
|
|
52
|
+
expect(scanText('class="shadow-[0_0_20px_rgba(0,255,255,0.7)]"').some(m => m.tell.name === 'neon-glow')).toBe(true)
|
|
53
|
+
expect(scanText('box-shadow: 0 0 40px #0ff;').some(m => m.tell.name === 'neon-glow')).toBe(true)
|
|
54
|
+
})
|
|
55
|
+
it('flags gradient overload', () => {
|
|
56
|
+
expect(scanText('background: linear-gradient(90deg, #fff, #000);').some(m => m.tell.name === 'gradient-overload')).toBe(true)
|
|
57
|
+
})
|
|
58
|
+
it('does not flag clean, conventional styles', () => {
|
|
59
|
+
expect(scanText('class="rounded-lg border bg-white p-4 text-slate-800"')).toEqual([])
|
|
60
|
+
expect(scanText('color: #1d1d1f; background: #f5f5f7;')).toEqual([])
|
|
61
|
+
})
|
|
62
|
+
it('counts at most one finding per tell per line', () => {
|
|
63
|
+
const m = scanText('from-purple-500 via-purple-600 to-purple-700')
|
|
64
|
+
expect(m.filter(x => x.tell.name === 'ai-purple')).toHaveLength(1)
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
describe('scanDir', () => {
|
|
69
|
+
let dir: string
|
|
70
|
+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'yoke-scan-')) })
|
|
71
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
|
|
72
|
+
|
|
73
|
+
it('walks source files, aggregates a weighted score, and records file:line', () => {
|
|
74
|
+
writeFileSync(join(dir, 'a.css'), 'h1 { color: #6c5ce7; }\n') // ai-purple weight 2
|
|
75
|
+
writeFileSync(join(dir, 'b.tsx'), '\n<span class="shadow-[0_0_10px_#f0f]" />') // neon-glow weight 2, line 2
|
|
76
|
+
const res = scanDir(dir)
|
|
77
|
+
expect(res.score).toBe(4)
|
|
78
|
+
expect(res.findings.some(f => f.file.endsWith('a.css') && f.line === 1 && f.tell === 'ai-purple')).toBe(true)
|
|
79
|
+
expect(res.findings.some(f => f.file.endsWith('b.tsx') && f.line === 2 && f.tell === 'neon-glow')).toBe(true)
|
|
80
|
+
})
|
|
81
|
+
it('skips node_modules / dist / .yoke and non-source extensions', () => {
|
|
82
|
+
mkdirSync(join(dir, 'node_modules'), { recursive: true })
|
|
83
|
+
writeFileSync(join(dir, 'node_modules', 'x.css'), 'color:#6c5ce7;')
|
|
84
|
+
writeFileSync(join(dir, 'readme.md'), 'a #6c5ce7 mention')
|
|
85
|
+
expect(scanDir(dir).score).toBe(0)
|
|
86
|
+
})
|
|
87
|
+
it('accepts an injected tell set', () => {
|
|
88
|
+
writeFileSync(join(dir, 'a.css'), 'foo')
|
|
89
|
+
const tells = [{ name: 'foo', weight: 5, test: (l: string) => l.includes('foo'), hint: 'h' }]
|
|
90
|
+
expect(scanDir(dir, tells).score).toBe(5)
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('TELLS is the curated non-empty default set', () => {
|
|
95
|
+
expect(TELLS.map(t => t.name)).toEqual(
|
|
96
|
+
expect.arrayContaining(['ai-purple', 'gradient-clip-text', 'neon-glow', 'gradient-overload']),
|
|
97
|
+
)
|
|
98
|
+
})
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
- [ ] **Step 2: Run it to verify it fails**
|
|
102
|
+
|
|
103
|
+
Run: `npx vitest run tests/scan/design.test.ts`
|
|
104
|
+
Expected: FAIL — module missing.
|
|
105
|
+
|
|
106
|
+
- [ ] **Step 3: Implement** `src/scan/design.ts`
|
|
107
|
+
|
|
108
|
+
```typescript
|
|
109
|
+
import { readdirSync, readFileSync, statSync } from 'node:fs'
|
|
110
|
+
import { join, extname } from 'node:path'
|
|
111
|
+
|
|
112
|
+
export interface Tell {
|
|
113
|
+
name: string
|
|
114
|
+
weight: number
|
|
115
|
+
test: (line: string) => boolean
|
|
116
|
+
hint: string
|
|
117
|
+
}
|
|
118
|
+
export interface Match { line: number; tell: Tell; text: string }
|
|
119
|
+
export interface Finding { file: string; line: number; tell: string; hint: string; text: string }
|
|
120
|
+
export interface ScanResult { findings: Finding[]; score: number }
|
|
121
|
+
|
|
122
|
+
const AI_PURPLE_HEX = /#(6c5ce7|7c3aed|8b5cf6|a855f7|9333ea)\b/i
|
|
123
|
+
const AI_PURPLE_TW = /\b(from|via|to)-(purple|violet|fuchsia)-(4|5|6|7)00\b/i
|
|
124
|
+
const NEON_TW = /\b(shadow|drop-shadow)-\[0_0_/i
|
|
125
|
+
const NEON_CSS = /box-shadow:[^;]*\b0\s+0\s+\d{2,}px/i
|
|
126
|
+
const EMOJI = /\p{Extended_Pictographic}/u
|
|
127
|
+
const JSX_ICON_CTX = /<button|<a\s|aria-hidden|(icon|emoji)/i
|
|
128
|
+
|
|
129
|
+
export const TELLS: Tell[] = [
|
|
130
|
+
{ name: 'ai-purple', weight: 2, hint: 'AI-purple is the #1 vibecoded tell — pick a real brand color',
|
|
131
|
+
test: (l) => AI_PURPLE_HEX.test(l) || AI_PURPLE_TW.test(l) },
|
|
132
|
+
{ name: 'gradient-clip-text', weight: 2, hint: 'Gradient hero text reads as AI-slop — use a solid color + weight',
|
|
133
|
+
test: (l) => (/bg-clip-text/.test(l) && /text-transparent/.test(l)) || /-webkit-background-clip:\s*text/i.test(l) },
|
|
134
|
+
{ name: 'neon-glow', weight: 2, hint: 'Neon glow is a tell — use subtle, neutral elevation',
|
|
135
|
+
test: (l) => NEON_TW.test(l) || NEON_CSS.test(l) },
|
|
136
|
+
{ name: 'gradient-overload', weight: 1, hint: 'Gradients everywhere flatten hierarchy — use them sparingly',
|
|
137
|
+
test: (l) => /bg-gradient-to-/.test(l) || /linear-gradient\(/i.test(l) },
|
|
138
|
+
{ name: 'emoji-icon', weight: 1, hint: 'Emoji-as-icons is a tell — use a real icon set',
|
|
139
|
+
test: (l) => EMOJI.test(l) && JSX_ICON_CTX.test(l) },
|
|
140
|
+
]
|
|
141
|
+
|
|
142
|
+
// One match per (line, tell) at most, so a line with three purple classes counts once.
|
|
143
|
+
export function scanText(text: string, tells: Tell[] = TELLS): Match[] {
|
|
144
|
+
const matches: Match[] = []
|
|
145
|
+
const lines = text.split(/\r?\n/)
|
|
146
|
+
lines.forEach((line, i) => {
|
|
147
|
+
for (const tell of tells) {
|
|
148
|
+
if (tell.test(line)) matches.push({ line: i + 1, tell, text: line.trim().slice(0, 200) })
|
|
149
|
+
}
|
|
150
|
+
})
|
|
151
|
+
return matches
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const EXT = new Set(['.css', '.scss', '.tsx', '.jsx', '.ts', '.js', '.html', '.vue', '.svelte', '.astro'])
|
|
155
|
+
const SKIP = new Set(['node_modules', 'dist', '.next', 'build', '.yoke', 'coverage', '.git', 'out'])
|
|
156
|
+
|
|
157
|
+
function walk(dir: string, acc: string[]): void {
|
|
158
|
+
for (const entry of readdirSync(dir)) {
|
|
159
|
+
const full = join(dir, entry)
|
|
160
|
+
let s
|
|
161
|
+
try { s = statSync(full) } catch { continue }
|
|
162
|
+
if (s.isDirectory()) {
|
|
163
|
+
if (!SKIP.has(entry)) walk(full, acc)
|
|
164
|
+
} else if (EXT.has(extname(entry).toLowerCase())) {
|
|
165
|
+
acc.push(full)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function scanDir(dir: string, tells: Tell[] = TELLS): ScanResult {
|
|
171
|
+
const files: string[] = []
|
|
172
|
+
walk(dir, files)
|
|
173
|
+
const findings: Finding[] = []
|
|
174
|
+
let score = 0
|
|
175
|
+
for (const file of files) {
|
|
176
|
+
let text: string
|
|
177
|
+
try { text = readFileSync(file, 'utf8') } catch { continue }
|
|
178
|
+
for (const m of scanText(text, tells)) {
|
|
179
|
+
findings.push({ file, line: m.line, tell: m.tell.name, hint: m.tell.hint, text: m.text })
|
|
180
|
+
score += m.tell.weight
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return { findings, score }
|
|
184
|
+
}
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
- [ ] **Step 4: Run it green** — `npx vitest run tests/scan/design.test.ts` → PASS.
|
|
188
|
+
|
|
189
|
+
- [ ] **Step 5: Commit**
|
|
190
|
+
```bash
|
|
191
|
+
git add src/scan/design.ts tests/scan/design.test.ts
|
|
192
|
+
git commit -m "feat(scan): static AI-slop design-tell scanner (scanText/scanDir)"
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Task 2: `yoke design-scan` CLI
|
|
198
|
+
|
|
199
|
+
**Files:** Modify `src/cli.ts`; Test `tests/scan/design-cli.test.ts`
|
|
200
|
+
|
|
201
|
+
- [ ] **Step 1: Write the failing test**
|
|
202
|
+
|
|
203
|
+
```typescript
|
|
204
|
+
// tests/scan/design-cli.test.ts
|
|
205
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
|
|
206
|
+
import { mkdtempSync, writeFileSync, rmSync } from 'node:fs'
|
|
207
|
+
import { join } from 'node:path'
|
|
208
|
+
import { tmpdir } from 'node:os'
|
|
209
|
+
import { runDesignScan } from '../../src/cli.js'
|
|
210
|
+
|
|
211
|
+
let dir: string
|
|
212
|
+
beforeEach(() => { dir = mkdtempSync(join(tmpdir(), 'yoke-dscli-')) })
|
|
213
|
+
afterEach(() => { rmSync(dir, { recursive: true, force: true }) })
|
|
214
|
+
|
|
215
|
+
describe('runDesignScan', () => {
|
|
216
|
+
it('exits 0 when the slop score is within --max', () => {
|
|
217
|
+
writeFileSync(join(dir, 'a.css'), 'color: #6c5ce7;') // score 2
|
|
218
|
+
expect(runDesignScan(dir, { max: 4, report: false })).toBe(0)
|
|
219
|
+
})
|
|
220
|
+
it('exits 1 when the slop score exceeds --max', () => {
|
|
221
|
+
writeFileSync(join(dir, 'a.css'), 'color:#6c5ce7; box-shadow: 0 0 40px #0ff; background: linear-gradient(#fff,#000);') // 2+2+1 = 5
|
|
222
|
+
expect(runDesignScan(dir, { max: 4, report: false })).toBe(1)
|
|
223
|
+
})
|
|
224
|
+
it('always exits 0 in --report mode even with findings', () => {
|
|
225
|
+
writeFileSync(join(dir, 'a.css'), 'color:#6c5ce7; box-shadow: 0 0 40px #0ff;')
|
|
226
|
+
expect(runDesignScan(dir, { max: 0, report: true })).toBe(0)
|
|
227
|
+
})
|
|
228
|
+
it('exits 0 on a clean project', () => {
|
|
229
|
+
writeFileSync(join(dir, 'a.css'), 'color:#1d1d1f;')
|
|
230
|
+
expect(runDesignScan(dir, { max: 4, report: false })).toBe(0)
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
- [ ] **Step 2: Run it to verify it fails** — `npx vitest run tests/scan/design-cli.test.ts` → FAIL (`runDesignScan` not exported).
|
|
236
|
+
|
|
237
|
+
- [ ] **Step 3: Implement** in `src/cli.ts`
|
|
238
|
+
|
|
239
|
+
Add the import (with the other imports):
|
|
240
|
+
```typescript
|
|
241
|
+
import { scanDir } from './scan/design.js'
|
|
242
|
+
```
|
|
243
|
+
Add the exported function (near `runValidate`):
|
|
244
|
+
```typescript
|
|
245
|
+
export function runDesignScan(targetDir: string, opts: { max: number; report: boolean }): number {
|
|
246
|
+
const { findings, score } = scanDir(targetDir)
|
|
247
|
+
for (const f of findings) {
|
|
248
|
+
console.log(` ${f.file}:${f.line} ${f.tell} — ${f.hint}`)
|
|
249
|
+
}
|
|
250
|
+
const label = `Design scan: score ${score} (${findings.length} tell${findings.length === 1 ? '' : 's'}), budget ${opts.max}`
|
|
251
|
+
if (opts.report) { console.log(`${label} — report only`); return 0 }
|
|
252
|
+
if (score > opts.max) { console.log(`${label} — ✗ over budget`); return 1 }
|
|
253
|
+
console.log(`${label} — ✓`)
|
|
254
|
+
return 0
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
Add a `case` in `main`'s switch (before `default`):
|
|
258
|
+
```typescript
|
|
259
|
+
case 'design-scan': {
|
|
260
|
+
const targetDir = rest.find(a => !a.startsWith('-')) ?? '.'
|
|
261
|
+
const report = rest.includes('--report')
|
|
262
|
+
const maxArg = rest.find(a => a.startsWith('--max='))
|
|
263
|
+
const max = maxArg ? Number(maxArg.slice('--max='.length)) : 4
|
|
264
|
+
if (!Number.isFinite(max) || max < 0) { console.error(`Invalid --max value: ${maxArg}`); return 1 }
|
|
265
|
+
return runDesignScan(targetDir, { max, report })
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
Append ` | design-scan [dir] [--max=N] [--report]` to the top-level `default` usage string.
|
|
269
|
+
|
|
270
|
+
- [ ] **Step 4: Run green + full suite + types** — `npx vitest run && npx tsc --noEmit` → PASS, clean.
|
|
271
|
+
|
|
272
|
+
- [ ] **Step 5: Commit**
|
|
273
|
+
```bash
|
|
274
|
+
git add src/cli.ts tests/scan/design-cli.test.ts
|
|
275
|
+
git commit -m "feat(cli): yoke design-scan — gate on AI-slop design score"
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### Task 3: canon skills + manifest + attribution
|
|
281
|
+
|
|
282
|
+
**Files:** Create `canon/skills/unslop-ui/SKILL.md`, `canon/skills/visual-verification/SKILL.md`; Modify `canon/manifest.yaml`, `canon/skills/ATTRIBUTION.md`; Test `tests/canon/real-canon.test.ts`
|
|
283
|
+
|
|
284
|
+
- [ ] **Step 1: Write the failing test** — add to `tests/canon/real-canon.test.ts` (inside `describe('real canon')`):
|
|
285
|
+
```typescript
|
|
286
|
+
it('registers the visual verification skills', () => {
|
|
287
|
+
const manifest = loadManifest(join(repoRoot, 'canon', 'manifest.yaml'))
|
|
288
|
+
expect(manifest.skills.some(s => s.id === 'unslop-ui')).toBe(true)
|
|
289
|
+
expect(manifest.skills.some(s => s.id === 'visual-verification')).toBe(true)
|
|
290
|
+
})
|
|
291
|
+
```
|
|
292
|
+
Run `npx vitest run tests/canon/real-canon.test.ts` → FAIL.
|
|
293
|
+
|
|
294
|
+
- [ ] **Step 2: Create `canon/skills/unslop-ui/SKILL.md`**
|
|
295
|
+
```markdown
|
|
296
|
+
---
|
|
297
|
+
name: unslop-ui
|
|
298
|
+
description: Use when building or reviewing any UI — detect and remove the visual "tells" of AI-generated/vibecoded design (AI-purple gradients, gradient hero text, neon glow, emoji-as-icons, untouched shadcn defaults, centered-hero-plus-three-cards) so the result looks deliberately designed, not machine-default.
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
# Un-slop the UI
|
|
302
|
+
|
|
303
|
+
AI-built UIs are "recognizable on sight" — a homogeneous look from a handful of default choices.
|
|
304
|
+
Before finishing UI work, remove these tells. Run `yoke design-scan .` for the statically
|
|
305
|
+
detectable ones, then apply judgement for the structural ones the scanner can't see.
|
|
306
|
+
|
|
307
|
+
## The ranked tells (and the fix)
|
|
308
|
+
|
|
309
|
+
- **AI-purple gradients** (the #1 tell) — purple/violet gradients and `#6c5ce7`-family accents.
|
|
310
|
+
→ Choose one real brand color with intent; avoid purple-by-default.
|
|
311
|
+
- **Gradient hero text** — `bg-clip-text text-transparent` rainbow headings.
|
|
312
|
+
→ Solid color + type weight/scale for emphasis.
|
|
313
|
+
- **Neon glow** — `0 0 Npx` colored box-shadows / `shadow-[0_0_...]`.
|
|
314
|
+
→ Subtle, neutral elevation (small offset, low blur, low opacity).
|
|
315
|
+
- **Emoji-as-icons** — 🚀✨🔥 in buttons/nav/feature lists.
|
|
316
|
+
→ A real icon set (lucide, etc.), consistent stroke + size.
|
|
317
|
+
- **Untouched shadcn/Tailwind defaults** — default radius, default slate everywhere.
|
|
318
|
+
→ Set deliberate tokens (radius, spacing scale, one accent) so it doesn't look boilerplate.
|
|
319
|
+
- **Centered hero + three feature cards** — the canonical AI landing layout.
|
|
320
|
+
→ Vary rhythm: asymmetry, a real product shot, content density that fits the product.
|
|
321
|
+
- **Homogeneous spacing / no hierarchy** — everything the same size and gap.
|
|
322
|
+
→ Establish a type scale and spacing rhythm; make the primary action obviously primary.
|
|
323
|
+
|
|
324
|
+
## Rule
|
|
325
|
+
|
|
326
|
+
Treat `yoke design-scan .` as a gate (it exits non-zero over budget). Fix findings, then
|
|
327
|
+
eyeball the structural tells above. Distinctive, intentional > generic-but-safe.
|
|
328
|
+
|
|
329
|
+
*Rubric informed by the MIT-licensed research in [vibecoded-design-tells](https://github.com/JCarterJohnson/vibecoded-design-tells) (© Carter Johnson). Yoke implements the idea natively; no code/data copied.*
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
- [ ] **Step 3: Create `canon/skills/visual-verification/SKILL.md`**
|
|
333
|
+
```markdown
|
|
334
|
+
---
|
|
335
|
+
name: visual-verification
|
|
336
|
+
description: Use for any UI/web project — make the verify gate cover more than unit tests by composing a pipeline (types → unit → design-scan → flow-smoke) and driving a Playwright flow-smoke (render + no console errors + screenshot); capture video only on failure. Catches the unwired-page / runtime-crash / AI-slop bugs unit tests miss.
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
# Visual verification
|
|
340
|
+
|
|
341
|
+
Unit tests don't see a blank page, an unwired route, a runtime console error, or AI-slop design.
|
|
342
|
+
Make the loop's gate catch them by widening `verify`, since the loop trusts verify as truth.
|
|
343
|
+
|
|
344
|
+
## 1. Compose the verify pipeline
|
|
345
|
+
|
|
346
|
+
Set `verify.command` (in `.yoke/config.yaml`) to chain, fail-fast:
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
<typecheck> && <unit tests> && yoke design-scan . && <flow-smoke>
|
|
350
|
+
```
|
|
351
|
+
e.g. `tsc --noEmit && vitest run && yoke design-scan . && npm run smoke`. Any red step blocks the story.
|
|
352
|
+
|
|
353
|
+
## 2. Flow-smoke with the wired Playwright MCP
|
|
354
|
+
|
|
355
|
+
For the key user flows (home, signup/login, the primary action, checkout), against the running
|
|
356
|
+
dev server:
|
|
357
|
+
- load the route, assert it renders the expected landmark, and assert the **console has no errors**;
|
|
358
|
+
- take a screenshot of each for the record.
|
|
359
|
+
|
|
360
|
+
This is what catches "the page is wired wrong / it crashes on load" — the class of bug unit tests pass straight through.
|
|
361
|
+
|
|
362
|
+
## 3. Video only when necessary
|
|
363
|
+
|
|
364
|
+
Recording + analysing video is token-heavy. Capture a video of a flow **only when a flow-smoke
|
|
365
|
+
fails** (or when explicitly debugging a UX problem), then analyse that clip. Never record every run.
|
|
366
|
+
|
|
367
|
+
## Rule
|
|
368
|
+
|
|
369
|
+
Green pipeline = types + units + no design-slop over budget + key flows render without console
|
|
370
|
+
errors. Only then is the story actually done.
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
- [ ] **Step 4: Register in `canon/manifest.yaml`** — add after the `workflow` entry (or with the methodology skills):
|
|
374
|
+
```yaml
|
|
375
|
+
- { id: unslop-ui, path: skills/unslop-ui, kind: methodology }
|
|
376
|
+
- { id: visual-verification, path: skills/visual-verification, kind: methodology }
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
- [ ] **Step 5: Credit in `canon/skills/ATTRIBUTION.md`** — append:
|
|
380
|
+
```markdown
|
|
381
|
+
|
|
382
|
+
## Design-tell research
|
|
383
|
+
|
|
384
|
+
`unslop-ui` and the `yoke design-scan` tell set are informed by
|
|
385
|
+
[vibecoded-design-tells](https://github.com/JCarterJohnson/vibecoded-design-tells)
|
|
386
|
+
(MIT © 2026 Carter Johnson) — a data-ranked study of AI-generated-UI tells. Yoke implements the
|
|
387
|
+
idea natively in TypeScript and copies no code or data.
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
- [ ] **Step 6: Run tests** — `npx vitest run tests/canon/` → PASS (both skills registered, `validateCanon` zero-error). Confirm the new skills' single-line descriptions pass validation.
|
|
391
|
+
|
|
392
|
+
- [ ] **Step 7: Commit**
|
|
393
|
+
```bash
|
|
394
|
+
git add canon/skills/unslop-ui canon/skills/visual-verification canon/manifest.yaml canon/skills/ATTRIBUTION.md tests/canon/real-canon.test.ts
|
|
395
|
+
git commit -m "feat(canon): unslop-ui + visual-verification skills (credit vibecoded-design-tells)"
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
---
|
|
399
|
+
|
|
400
|
+
### Task 4: README (mandatory)
|
|
401
|
+
|
|
402
|
+
**Files:** Modify `README.md`
|
|
403
|
+
|
|
404
|
+
- [ ] **Step 1: Add a "Visual & design verification" section** after the autonomous-loop/Context-layer sections:
|
|
405
|
+
```markdown
|
|
406
|
+
## 🎨 Visual & design verification
|
|
407
|
+
|
|
408
|
+
Unit tests don't catch a blank page, an unwired route, or generic AI-slop design. Yoke adds two things:
|
|
409
|
+
|
|
410
|
+
- **`yoke design-scan [dir]`** — a static scanner for the visual *tells* of AI-generated UIs
|
|
411
|
+
(AI-purple gradients, gradient hero text, neon glow, emoji-as-icons, gradient overload). It
|
|
412
|
+
scores findings and **exits non-zero over budget** (`--max`, default 4; `--report` to list only),
|
|
413
|
+
so it drops straight into your verify pipeline.
|
|
414
|
+
- **`unslop-ui` + `visual-verification` skills** — the design rubric, plus how to compose a verify
|
|
415
|
+
pipeline (`types → units → design-scan → Playwright flow-smoke`) and capture video *only on failure*.
|
|
416
|
+
|
|
417
|
+
Because the loop trusts **verify as the source of truth**, widening `verify.command` to include the
|
|
418
|
+
scanner and a flow-smoke makes visual quality a real gate — not an afterthought.
|
|
419
|
+
|
|
420
|
+
*Tell set informed by the MIT-licensed [vibecoded-design-tells](https://github.com/JCarterJohnson/vibecoded-design-tells) research.*
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
- [ ] **Step 2: Update the skills catalog** — in the "What's in the canon" section: change the heading count from **24** to **26**; change the **Process / methodology** group count from **(13)** to **(15)**; add two rows to that table:
|
|
424
|
+
```markdown
|
|
425
|
+
| `unslop-ui` | Detect & remove AI-slop design tells (purple gradients, neon glow, emoji-icons…) |
|
|
426
|
+
| `visual-verification` | Widen verify to flow-smoke + design-scan; video only on failure |
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
- [ ] **Step 3: Sync the test-count badge** — run the full suite to get the current count, then update the three `tests-NNN` references (badge line, the "✅ NNN tests" highlight, and `npm test # vitest (NNN tests)`) to the new number.
|
|
430
|
+
|
|
431
|
+
Run: `npx vitest run` → note the count; update README.
|
|
432
|
+
|
|
433
|
+
- [ ] **Step 4: Commit**
|
|
434
|
+
```bash
|
|
435
|
+
git add README.md
|
|
436
|
+
git commit -m "docs: visual & design verification section + catalog 24→26 + test count"
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
---
|
|
440
|
+
|
|
441
|
+
## Self-Review
|
|
442
|
+
|
|
443
|
+
**Spec coverage:** scanner (`scanText`/`scanDir`/`TELLS`) → Task 1; `yoke design-scan` gate → Task 2; `unslop-ui` + `visual-verification` skills + manifest + attribution → Task 3; README + catalog + counts → Task 4. Flow-smoke/video are methodology in `visual-verification` (no browser embedded) per the spec.
|
|
444
|
+
|
|
445
|
+
**Placeholder scan:** No TBD/TODO; every code/markdown step is complete. The tell regexes and weights are concrete and match the test expectations (ai-purple 2, neon 2, clip 2, gradient 1, emoji 1).
|
|
446
|
+
|
|
447
|
+
**Type consistency:** `Tell`/`Match`/`Finding`/`ScanResult`, `scanText(text, tells?)`, `scanDir(dir, tells?)`, `TELLS`, `runDesignScan(dir, {max, report})` are defined once and used consistently. The CLI default `--max=4` matches the spec and the CLI tests (score 5 > 4 → exit 1; score 2 ≤ 4 → exit 0).
|
|
448
|
+
|
|
449
|
+
**Counts:** manifest gains 2 skills (24→26); README heading 26 and methodology group 13→15 (the README groups by provenance — the two new Yoke-authored methodology skills go in the Process/methodology group, keeping 13+2=15 there; 15+7 roles+4 yoke-native… note: re-verify the README's own grouping math sums to 26 when editing, adjusting whichever group label the new rows live under).
|
|
450
|
+
```
|