tiny-spec 0.3.0__tar.gz → 0.4.0__tar.gz

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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tiny-spec
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: A tiny, opinionated take on spec-driven development.
5
5
  Project-URL: Homepage, https://github.com/GrayMa77er/tiny-spec
6
6
  Project-URL: Source, https://github.com/GrayMa77er/tiny-spec
@@ -121,6 +121,11 @@ Or collapse the three planning steps into one and go straight to building:
121
121
  **stops before `tiny-spec-build`** — that's where you actually review the work — and
122
122
  it writes nothing itself, it only delegates.
123
123
 
124
+ **Building from a mockup?** There is no design flag to pass. Commit your exports to
125
+ `design/` before the run and `tiny-spec-create` — whether you invoke it directly or
126
+ reach it through `tiny-spec-run` — reads what's there and asks which screens this
127
+ ticket covers. [Designs, if you have them](#designs-if-you-have-them) walks through it.
128
+
124
129
  Re-run `install` any time to update; `tiny-spec uninstall` removes only what it
125
130
  installed. Each skill is copied (not symlinked) so every install is
126
131
  self-contained.
@@ -161,25 +166,164 @@ stale, since they were reviewed against a document that wasn't there.
161
166
 
162
167
  ### Designs, if you have them
163
168
 
164
- Drop your exported wireframes in a `design/` directory and `tiny-spec-create` will
165
- look at them actually look, they are read as images. From all of them together it
166
- proposes one coherent **design system** (spacing scale, type scale, color roles) for
167
- your approval and writes it into the constitution, then describes each screen as a
168
- `D<n>` entry in `SPEC.md`: its layout, its elements with a selector each, and the
169
- states it must render — all in those token names.
170
-
171
- The point is what happens at build time. Tag a task with `design: D1` and the
172
- reviewer renders that surface, measures the selectors the entry names, and **fails the
173
- task** on a value that isn't on your scale, an element that never got built, or a
174
- state the design calls for and the code doesn't render. It measures numbers rather
175
- than diffing screenshots, because pixel diffs go flaky and get muted. Tasks without a
176
- `design:` tag are graded exactly as before.
177
-
178
- No Figma token, no plugin, no design SaaS a view-only Figma works fine, since the
179
- committed export is what the agents read and the `source:` link keeps the trail back.
180
- Change an export and its recorded hash stops matching, which marks the spec stale the
181
- same way editing a requirement does. Skip the whole thing for a CLI or a library; the
182
- constitution simply has no design section.
169
+ Wireframes usually get read once and forgotten. tiny-spec turns them into two durable
170
+ thingsa project-wide token system in the constitution, and a per-screen entry in
171
+ the spec so "does this look right?" becomes something the reviewer can fail a task
172
+ on. Skip all of it for a CLI or a library; the constitution simply has no design
173
+ section.
174
+
175
+ **Designs enter by convention, not by argument.** No skill takes a design flag or a
176
+ path parameter. `tiny-spec-create` reads every file in `design/` at your project root
177
+ directly, or when `tiny-spec-run` reaches it and asks which screens this ticket
178
+ covers; you can also just hand it paths during the interview. Change an export later and
179
+ re-running `create` (or `run`) re-hashes it, marking the spec stale exactly like editing
180
+ a requirement.
181
+
182
+ **1. Commit your exports.** Any format an agent can read — a Figma export, an HTML
183
+ mockup, an Excalidraw file, a photo of a whiteboard.
184
+
185
+ ```
186
+ your-project/
187
+ design/
188
+ signup.png
189
+ dashboard.png
190
+ ```
191
+
192
+ **2. `tiny-spec-create` looks at them and proposes a design system.** Actually looks —
193
+ they are read as images. It infers *one* coherent scale across all of them rather than
194
+ measuring each screen separately, tells you what it rounded ("your wireframes had 19px
195
+ and 21px — proposing `space.5`=20px for both"), and on your approval writes it into
196
+ `.spec/constitution.md`:
197
+
198
+ ```markdown
199
+ ## Design system
200
+ - color: `color.surface.base` #FFFFFF · `color.text.default` #111111
201
+ `color.text.muted` #6B7280 · `color.text.danger` #B91C1C
202
+ - space: `space.1`=4px · `space.2`=8px · `space.4`=16px · `space.6`=24px
203
+ - type: `type.heading.lg` 24px/600/1.25 · `type.body` 16px/400/1.5
204
+ `type.caption` 13px/400/1.4
205
+ - states: every interactive surface defines default, focus, disabled, loading, empty, error
206
+ ```
207
+
208
+ Since it is project-wide, every screen from here on speaks this vocabulary — and a
209
+ redesign edits this one table instead of every file.
210
+
211
+ **3. Each screen becomes a `D<n>` in `SPEC.md`**, written in those token names:
212
+
213
+ ```markdown
214
+ ## Design
215
+
216
+ - D1 — Signup form
217
+ - source: figma.com/file/abc#node-12:34 (view-only)
218
+ - export: design/signup.png
219
+ - sha256: d21d6330648c504edeb924b5398bf7fb6485d3a4c1907e43d800970f39622a1d
220
+ - layout: single centered column, max 420px; title → field → error → submit
221
+ - elements:
222
+ - card `[data-testid="signup-card"]` → space.6 padding, color.surface.base
223
+ - title `[data-testid="signup-title"]` → type.heading.lg, space.4 below
224
+ - field `[data-testid="signup-email"]` → type.body, space.2 below
225
+ - error `[data-testid="signup-error"]` → type.caption, color.text.danger
226
+ - states: error (caption under the field), loading (spinner replaces button label)
227
+ ```
228
+
229
+ The selectors are a **contract**, not a hint — the reviewer measures exactly these, so
230
+ your markup carries them verbatim. Prefer test ids over CSS classes: classes get
231
+ renamed by refactors and mangled by CSS-in-JS, and a selector that silently stops
232
+ matching is the failure this exists to prevent.
233
+
234
+ **4. Tag the tasks that build the surface** — and only those, not the API call behind
235
+ it. This is your blast radius:
236
+
237
+ ```
238
+ - [ ] T4 — Build the signup form
239
+ - acceptance: submitting a valid email advances to the verify step
240
+ - design: D1
241
+ ```
242
+
243
+ **5. The reviewer measures it, then looks at it, and fails on both:**
244
+
245
+ ```
246
+ DESIGN: D1 — Signup form
247
+ [data-testid="signup-card"] padding 19px — not on the space.* scale (4/8/16/24)
248
+ [data-testid="signup-email"] MISSING FROM DOM — never built, or renamed
249
+ [data-testid="signup-error"] rgb(204,0,0) — color.text.danger is #B91C1C
250
+ state "loading" never renders: button label stays "Continue", no spinner
251
+ judge: read states default, error — compared against design/signup.png
252
+ state "error": signup-error sits behind the card — every token right, and the
253
+ user sees nothing where the export shows the red caption
254
+ state "default": submit label "Create account" truncates to "Create acco…"
255
+ FINDINGS:
256
+ - flag: title/field gap feels tight (on-scale — does not fail the task)
257
+ ```
258
+
259
+ Numbers first, and never a screenshot diff — pixel comparison goes flaky on font
260
+ antialiasing and teams end up muting it. But no measurement catches an element that is
261
+ present, on-token, and still not on screen — occluded, clipped, truncated, or the same
262
+ color as what's behind it. So the last step reads a screenshot of each state next to
263
+ your export and grades presence, legibility, and correspondence. **Where a number
264
+ already settled the question the eye may only flag** — on-scale-but-cramped is never a
265
+ fail — which keeps the two halves from contradicting each other. Measurable or visible
266
+ violations fail; taste comes back as `flag:` notes so a bounded fix loop can't thrash. A
267
+ task with no `design:` tag is graded exactly as before.
268
+
269
+ <details>
270
+ <summary>The <code>visual:</code> command (you write this once)</summary>
271
+
272
+ tiny-spec ships no script — it can't know your stack. Write one, put it in the
273
+ constitution's **Verification commands**, and the reviewer runs it:
274
+
275
+ ```
276
+ ## Verification commands
277
+ - test: `npm test`
278
+ - visual: `node visual.mjs`
279
+ ```
280
+
281
+ ```js
282
+ // visual.mjs — node visual.mjs '<selector>' ['<selector>'...]
283
+ import { chromium } from 'playwright';
284
+
285
+ const b = await chromium.launch({ channel: 'chrome' }); // your Chrome, no download
286
+ const p = await b.newPage({ viewport: { width: 900, height: 700 } });
287
+ await p.goto('http://localhost:3000/signup'); // your dev server + route
288
+
289
+ for (const sel of process.argv.slice(2)) {
290
+ const el = await p.$(sel);
291
+ if (!el) { console.log(`${sel}\n MISSING FROM DOM`); continue; } // required
292
+ console.log(sel, JSON.stringify(await el.evaluate(n => {
293
+ const c = getComputedStyle(n), r = n.getBoundingClientRect();
294
+ return { padding: c.padding, margin: c.margin, fontSize: c.fontSize,
295
+ fontWeight: c.fontWeight, lineHeight: c.lineHeight, color: c.color,
296
+ background: c.backgroundColor, border: c.border,
297
+ opacity: c.opacity, visibility: c.visibility,
298
+ top: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) };
299
+ })));
300
+ }
301
+
302
+ const state = process.env.STATE ?? 'default'; // however you drive states
303
+ const shot = `/tmp/visual-${state}.png`;
304
+ await p.screenshot({ path: shot, fullPage: true });
305
+ console.log('SCREENSHOT', state, shot); // this line arms the judge
306
+
307
+ await b.close();
308
+ ```
309
+
310
+ The missing-selector branch **must print something** — that is what turns a renamed
311
+ element into a failure instead of a silent skip. `top` is what the layout-order check
312
+ reads, and `opacity`/`visibility` are worth printing because they turn the cheapest kind
313
+ of invisible element into a numeric failure. The `SCREENSHOT` line is what the reviewer
314
+ reads back to look at the render and catch the rest — occlusion, clipping, truncation,
315
+ same-color-on-same-color — none of which any single property reports. Drop it and the
316
+ numeric half still gates exactly as before, with the reviewer reporting `judge: not run`
317
+ rather than quietly skipping it. Without a `visual:` command at all, a task tagged
318
+ `design:` raises a blocker rather than passing quietly.
319
+
320
+ </details>
321
+
322
+ **No Figma token, plugin, or design SaaS.** A view-only account works fine: the
323
+ committed export is what the agents read, and `source:` keeps the trail back. Change
324
+ an export and its recorded `sha256` stops matching, which marks the spec stale exactly
325
+ like editing a requirement — so a design that moved under finished work can't pass
326
+ unnoticed.
183
327
 
184
328
  `tiny-spec-build` walks the task list top to bottom. Each task runs through one loop:
185
329
 
@@ -84,6 +84,11 @@ Or collapse the three planning steps into one and go straight to building:
84
84
  **stops before `tiny-spec-build`** — that's where you actually review the work — and
85
85
  it writes nothing itself, it only delegates.
86
86
 
87
+ **Building from a mockup?** There is no design flag to pass. Commit your exports to
88
+ `design/` before the run and `tiny-spec-create` — whether you invoke it directly or
89
+ reach it through `tiny-spec-run` — reads what's there and asks which screens this
90
+ ticket covers. [Designs, if you have them](#designs-if-you-have-them) walks through it.
91
+
87
92
  Re-run `install` any time to update; `tiny-spec uninstall` removes only what it
88
93
  installed. Each skill is copied (not symlinked) so every install is
89
94
  self-contained.
@@ -124,25 +129,164 @@ stale, since they were reviewed against a document that wasn't there.
124
129
 
125
130
  ### Designs, if you have them
126
131
 
127
- Drop your exported wireframes in a `design/` directory and `tiny-spec-create` will
128
- look at them actually look, they are read as images. From all of them together it
129
- proposes one coherent **design system** (spacing scale, type scale, color roles) for
130
- your approval and writes it into the constitution, then describes each screen as a
131
- `D<n>` entry in `SPEC.md`: its layout, its elements with a selector each, and the
132
- states it must render — all in those token names.
133
-
134
- The point is what happens at build time. Tag a task with `design: D1` and the
135
- reviewer renders that surface, measures the selectors the entry names, and **fails the
136
- task** on a value that isn't on your scale, an element that never got built, or a
137
- state the design calls for and the code doesn't render. It measures numbers rather
138
- than diffing screenshots, because pixel diffs go flaky and get muted. Tasks without a
139
- `design:` tag are graded exactly as before.
140
-
141
- No Figma token, no plugin, no design SaaS a view-only Figma works fine, since the
142
- committed export is what the agents read and the `source:` link keeps the trail back.
143
- Change an export and its recorded hash stops matching, which marks the spec stale the
144
- same way editing a requirement does. Skip the whole thing for a CLI or a library; the
145
- constitution simply has no design section.
132
+ Wireframes usually get read once and forgotten. tiny-spec turns them into two durable
133
+ thingsa project-wide token system in the constitution, and a per-screen entry in
134
+ the spec so "does this look right?" becomes something the reviewer can fail a task
135
+ on. Skip all of it for a CLI or a library; the constitution simply has no design
136
+ section.
137
+
138
+ **Designs enter by convention, not by argument.** No skill takes a design flag or a
139
+ path parameter. `tiny-spec-create` reads every file in `design/` at your project root
140
+ directly, or when `tiny-spec-run` reaches it and asks which screens this ticket
141
+ covers; you can also just hand it paths during the interview. Change an export later and
142
+ re-running `create` (or `run`) re-hashes it, marking the spec stale exactly like editing
143
+ a requirement.
144
+
145
+ **1. Commit your exports.** Any format an agent can read — a Figma export, an HTML
146
+ mockup, an Excalidraw file, a photo of a whiteboard.
147
+
148
+ ```
149
+ your-project/
150
+ design/
151
+ signup.png
152
+ dashboard.png
153
+ ```
154
+
155
+ **2. `tiny-spec-create` looks at them and proposes a design system.** Actually looks —
156
+ they are read as images. It infers *one* coherent scale across all of them rather than
157
+ measuring each screen separately, tells you what it rounded ("your wireframes had 19px
158
+ and 21px — proposing `space.5`=20px for both"), and on your approval writes it into
159
+ `.spec/constitution.md`:
160
+
161
+ ```markdown
162
+ ## Design system
163
+ - color: `color.surface.base` #FFFFFF · `color.text.default` #111111
164
+ `color.text.muted` #6B7280 · `color.text.danger` #B91C1C
165
+ - space: `space.1`=4px · `space.2`=8px · `space.4`=16px · `space.6`=24px
166
+ - type: `type.heading.lg` 24px/600/1.25 · `type.body` 16px/400/1.5
167
+ `type.caption` 13px/400/1.4
168
+ - states: every interactive surface defines default, focus, disabled, loading, empty, error
169
+ ```
170
+
171
+ Since it is project-wide, every screen from here on speaks this vocabulary — and a
172
+ redesign edits this one table instead of every file.
173
+
174
+ **3. Each screen becomes a `D<n>` in `SPEC.md`**, written in those token names:
175
+
176
+ ```markdown
177
+ ## Design
178
+
179
+ - D1 — Signup form
180
+ - source: figma.com/file/abc#node-12:34 (view-only)
181
+ - export: design/signup.png
182
+ - sha256: d21d6330648c504edeb924b5398bf7fb6485d3a4c1907e43d800970f39622a1d
183
+ - layout: single centered column, max 420px; title → field → error → submit
184
+ - elements:
185
+ - card `[data-testid="signup-card"]` → space.6 padding, color.surface.base
186
+ - title `[data-testid="signup-title"]` → type.heading.lg, space.4 below
187
+ - field `[data-testid="signup-email"]` → type.body, space.2 below
188
+ - error `[data-testid="signup-error"]` → type.caption, color.text.danger
189
+ - states: error (caption under the field), loading (spinner replaces button label)
190
+ ```
191
+
192
+ The selectors are a **contract**, not a hint — the reviewer measures exactly these, so
193
+ your markup carries them verbatim. Prefer test ids over CSS classes: classes get
194
+ renamed by refactors and mangled by CSS-in-JS, and a selector that silently stops
195
+ matching is the failure this exists to prevent.
196
+
197
+ **4. Tag the tasks that build the surface** — and only those, not the API call behind
198
+ it. This is your blast radius:
199
+
200
+ ```
201
+ - [ ] T4 — Build the signup form
202
+ - acceptance: submitting a valid email advances to the verify step
203
+ - design: D1
204
+ ```
205
+
206
+ **5. The reviewer measures it, then looks at it, and fails on both:**
207
+
208
+ ```
209
+ DESIGN: D1 — Signup form
210
+ [data-testid="signup-card"] padding 19px — not on the space.* scale (4/8/16/24)
211
+ [data-testid="signup-email"] MISSING FROM DOM — never built, or renamed
212
+ [data-testid="signup-error"] rgb(204,0,0) — color.text.danger is #B91C1C
213
+ state "loading" never renders: button label stays "Continue", no spinner
214
+ judge: read states default, error — compared against design/signup.png
215
+ state "error": signup-error sits behind the card — every token right, and the
216
+ user sees nothing where the export shows the red caption
217
+ state "default": submit label "Create account" truncates to "Create acco…"
218
+ FINDINGS:
219
+ - flag: title/field gap feels tight (on-scale — does not fail the task)
220
+ ```
221
+
222
+ Numbers first, and never a screenshot diff — pixel comparison goes flaky on font
223
+ antialiasing and teams end up muting it. But no measurement catches an element that is
224
+ present, on-token, and still not on screen — occluded, clipped, truncated, or the same
225
+ color as what's behind it. So the last step reads a screenshot of each state next to
226
+ your export and grades presence, legibility, and correspondence. **Where a number
227
+ already settled the question the eye may only flag** — on-scale-but-cramped is never a
228
+ fail — which keeps the two halves from contradicting each other. Measurable or visible
229
+ violations fail; taste comes back as `flag:` notes so a bounded fix loop can't thrash. A
230
+ task with no `design:` tag is graded exactly as before.
231
+
232
+ <details>
233
+ <summary>The <code>visual:</code> command (you write this once)</summary>
234
+
235
+ tiny-spec ships no script — it can't know your stack. Write one, put it in the
236
+ constitution's **Verification commands**, and the reviewer runs it:
237
+
238
+ ```
239
+ ## Verification commands
240
+ - test: `npm test`
241
+ - visual: `node visual.mjs`
242
+ ```
243
+
244
+ ```js
245
+ // visual.mjs — node visual.mjs '<selector>' ['<selector>'...]
246
+ import { chromium } from 'playwright';
247
+
248
+ const b = await chromium.launch({ channel: 'chrome' }); // your Chrome, no download
249
+ const p = await b.newPage({ viewport: { width: 900, height: 700 } });
250
+ await p.goto('http://localhost:3000/signup'); // your dev server + route
251
+
252
+ for (const sel of process.argv.slice(2)) {
253
+ const el = await p.$(sel);
254
+ if (!el) { console.log(`${sel}\n MISSING FROM DOM`); continue; } // required
255
+ console.log(sel, JSON.stringify(await el.evaluate(n => {
256
+ const c = getComputedStyle(n), r = n.getBoundingClientRect();
257
+ return { padding: c.padding, margin: c.margin, fontSize: c.fontSize,
258
+ fontWeight: c.fontWeight, lineHeight: c.lineHeight, color: c.color,
259
+ background: c.backgroundColor, border: c.border,
260
+ opacity: c.opacity, visibility: c.visibility,
261
+ top: Math.round(r.top), w: Math.round(r.width), h: Math.round(r.height) };
262
+ })));
263
+ }
264
+
265
+ const state = process.env.STATE ?? 'default'; // however you drive states
266
+ const shot = `/tmp/visual-${state}.png`;
267
+ await p.screenshot({ path: shot, fullPage: true });
268
+ console.log('SCREENSHOT', state, shot); // this line arms the judge
269
+
270
+ await b.close();
271
+ ```
272
+
273
+ The missing-selector branch **must print something** — that is what turns a renamed
274
+ element into a failure instead of a silent skip. `top` is what the layout-order check
275
+ reads, and `opacity`/`visibility` are worth printing because they turn the cheapest kind
276
+ of invisible element into a numeric failure. The `SCREENSHOT` line is what the reviewer
277
+ reads back to look at the render and catch the rest — occlusion, clipping, truncation,
278
+ same-color-on-same-color — none of which any single property reports. Drop it and the
279
+ numeric half still gates exactly as before, with the reviewer reporting `judge: not run`
280
+ rather than quietly skipping it. Without a `visual:` command at all, a task tagged
281
+ `design:` raises a blocker rather than passing quietly.
282
+
283
+ </details>
284
+
285
+ **No Figma token, plugin, or design SaaS.** A view-only account works fine: the
286
+ committed export is what the agents read, and `source:` keeps the trail back. Change
287
+ an export and its recorded `sha256` stops matching, which marks the spec stale exactly
288
+ like editing a requirement — so a design that moved under finished work can't pass
289
+ unnoticed.
146
290
 
147
291
  `tiny-spec-build` walks the task list top to bottom. Each task runs through one loop:
148
292
 
@@ -73,15 +73,50 @@ acceptance and the constitution — verified, not inferred?**
73
73
  source is not evidence**, and it false-passes routinely (a comment saying the
74
74
  loading state is missing contains "loading"). A surface that renders its happy
75
75
  path and nothing else is a fail, not a nit.
76
- - Finally, `Read` the export image as a cross-check for what numbers can't catch
77
- (a missing element, wrong order, wrong hierarchy).
76
+ - Finally, **judge the render against the export.** Everything above proves the
77
+ numbers are right. None of it can see an element that is present, on-token, and
78
+ invisible — so now look. `Read` each `SCREENSHOT <state> <path>` the `visual:`
79
+ command printed, `Read` the `D<n>`'s `export:` image, and grade **every state you
80
+ have a screenshot for**, not just the happy path, on four lines:
81
+
82
+ 1. **Presence** — is every `elements:` row actually *visible* in the render? At
83
+ `opacity: 0`, zero height, clipped out of view, hidden behind a sibling, or the
84
+ same color as its background, an element passes every measurement above and is
85
+ not there. **FAIL** — this is why the step exists.
86
+ 2. **Legibility & occlusion** — text clipped, truncated mid-word, overlapping
87
+ another element, or on a background it can't be read against. **FAIL.**
88
+ 3. **Correspondence** — does the render show the same screen as the export: the
89
+ same regions, in the reading order `layout:` names? A whole region missing is a
90
+ **FAIL**; a stylistic difference is a **flag**.
91
+ 4. **Hierarchy & polish** — emphasis, balance, crowding, alignment. **Always a
92
+ flag**, never a fail.
93
+
94
+ Three rules bound it:
95
+
96
+ - **The numbers beat your eye on anything they already measured.** Padding that is
97
+ on the `space.*` scale but looks cramped is a `flag:`. A color that is exactly
98
+ its token but looks washed out is a `flag:`. You may fail only on what
99
+ measurement *cannot* see. Contradicting your own numbers sends the executor a
100
+ task it cannot fix, and the loop is bounded at two attempts.
101
+ - **This is still not a pixel diff.** The export is usually a wireframe — judge
102
+ structure and legibility, never visual identity.
103
+ - **Cite what you saw.** Name the state whose screenshot the finding came from and
104
+ what was in it ("state `error`: caption present in DOM but renders at opacity 0").
105
+ An uncited visual claim reads as an opinion and gets ignored.
106
+
107
+ **If the command printed no `SCREENSHOT` line**, do not run this sub-step and do
108
+ not eyeball a substitute. Grade on steps 1–3 above, write `judge: not run — visual:
109
+ emitted no SCREENSHOT line` in your `DESIGN:` section, and add a `flag:` saying the
110
+ command should screenshot each state it drives and print `SCREENSHOT <state>
111
+ <path>`. This is **not** a fail — unlike a missing `visual:` command, the gate did
112
+ run; only its last cross-check was unavailable.
78
113
 
79
114
  ## Verdict rules
80
115
 
81
116
  - **`PASS`** — the gate is green AND you exercised the acceptance end-to-end with
82
117
  real input AND the observed effect matches AND no invariant/DoD violation — AND,
83
- on a task carrying `design:`, step 4 ran and found nothing measurable wrong. Only
84
- this is a pass.
118
+ on a task carrying `design:`, step 4 ran and found nothing measurable *or visible*
119
+ wrong. Only this is a pass.
85
120
  - **`FAIL`** — anything short of the above: a red gate, an invariant violated, the
86
121
  acceptance not observably met, or you couldn't exercise it end-to-end. When torn,
87
122
  **fail** — never round up. List concrete, actionable findings so the executor
@@ -95,10 +130,19 @@ carrying `design:`:
95
130
  color/spacing where a token exists, an order or arrangement that contradicts
96
131
  `layout:`, or a state the `D<n>` entry names that doesn't render. These are
97
132
  objective and an executor can fix them from your numbers.
133
+ - **`FAIL` on what you saw** in a screenshot and could not have measured: an element
134
+ that renders invisibly (opacity 0, zero height, clipped, occluded, same color as its
135
+ background), text that clips or overlaps, or a region the export has that the render
136
+ lacks. Equally objective — name the state, and the executor can fix it from your
137
+ description.
98
138
  - **Flag, don't fail, on taste.** "The hierarchy feels off", "spacing looks cramped
99
139
  but is on-scale" — put it in `FINDINGS` prefixed `flag:` and pass if everything
100
140
  measurable is green. The fix loop is bounded at two attempts; burning it on a
101
141
  subjective disagreement means the task blocks on something no executor can resolve.
142
+ - **Where the numbers already answered the question, your eye may only flag.** This is
143
+ the line between the two fail lists above: measurement wins on values it took, sight
144
+ wins on what measurement can't reach. A finding that contradicts your own numbers is
145
+ a `flag:`, never a `FAIL`.
102
146
  - The "when torn, fail" rule still governs steps 1–3 unchanged. It does **not** apply
103
147
  to a subjective visual impression.
104
148
 
@@ -119,6 +163,8 @@ ACCEPTANCE: <how you exercised it + the observed effect, or why you couldn't>
119
163
  DESIGN: <omit unless the task carried `design:`. The D<n> checked, the measurements
120
164
  you read back vs the tokens they should match, and which states you exercised —
121
165
  or "blocker: no `visual:` command in the constitution">
166
+ judge: <the states whose screenshots you read and what you saw in each — or
167
+ "not run — visual: emitted no SCREENSHOT line">
122
168
  FINDINGS:
123
169
  - <each invariant/DoD/acceptance problem, concrete and actionable> (omit if PASS)
124
170
  - flag: <subjective visual note — does not fail the task> (only with a DESIGN section)
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "tiny-spec"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "A tiny, opinionated take on spec-driven development."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -79,8 +79,10 @@ It runs the real gate end-to-end, checks the code against the constitution's
79
79
  **Definition of Done** and **invariants**, confirms the **acceptance** actually
80
80
  holds (exercised, not inferred), and returns `VERDICT: PASS | FAIL` + findings.
81
81
  On a `design:` task it also runs the constitution's `visual:` command, measures the
82
- selectors the `D<n>` names against the Design system tokens, and exercises the states
83
- it names failing on measurable deviations and merely flagging subjective ones.
82
+ selectors the `D<n>` names against the Design system tokens, exercises the states it
83
+ names, and finally **looks at the render beside the design export** — catching what
84
+ numbers can't reach, like an element that measures perfectly and shows up invisible.
85
+ It fails on what it measured or saw, and merely flags what is taste.
84
86
 
85
87
  > Why independent: unit-green ≠ working, and the author is the worst judge of its
86
88
  > own blind spots. The reviewer running the gate from a clean state is the
@@ -215,7 +215,11 @@ If `.spec/` does not exist:
215
215
  - visual: `...` # optional; only if there is a Design system above. The command
216
216
  that boots the UI so the reviewer can read back computed styles and geometry
217
217
  (e.g. a Playwright script that navigates to a route and prints
218
- getComputedStyle/getBoundingClientRect for the selectors it is given). Required
218
+ getComputedStyle/getBoundingClientRect for the selectors it is given). It also
219
+ saves a screenshot of each state it drives and prints `SCREENSHOT <state> <path>`
220
+ for each — that line is what lets the reviewer look at the render next to the
221
+ design export and catch what numbers can't (an element that is on-token but
222
+ invisible, clipped text). Scratch files, gitignored. Required
219
223
  before any task may carry a `design:` reference — a task that names one with no
220
224
  `visual:` command here is a blocker, not a silent pass.
221
225
  >
@@ -249,6 +253,13 @@ in `SPEC.md`. Skip the whole section for a CLI, library, or headless service.
249
253
  computed styles (a Playwright script, a dev-server URL plus a snippet). Record it
250
254
  in **Verification commands**. Without it no task can carry a `design:` reference.
251
255
 
256
+ Ask that it **also screenshot each state it drives** and print
257
+ `SCREENSHOT <state> <path>` — one `page.screenshot({ path })` call next to the
258
+ measuring it already does, not a second script. That line is what lets the reviewer
259
+ look at the render beside the export, which is the only way to catch an element that
260
+ measures perfectly and renders invisibly. If the user says no, that's fine: the gate
261
+ still works on numbers alone and the reviewer reports the judge as not run.
262
+
252
263
  4. **Write one `D<n>` entry per screen this ticket touches**, with the source link,
253
264
  the committed export path, and its `sha256` (`shasum -a 256 <file>` — this is the
254
265
  staleness signal, so compute it, never invent it). Describe layout, elements, and
@@ -65,6 +65,10 @@ a scale from the wireframes; your job is to make it enforceable:
65
65
  - **Confirm the `visual:` command actually runs.** It is the gate for every task
66
66
  carrying `design:`; an aspirational command means the visual gate silently never
67
67
  fires. If it doesn't work from a clean checkout, fix it or drop the `design:` refs.
68
+ **Check it prints a `SCREENSHOT <state> <path>` line per state it drives** — that is
69
+ what lets the reviewer judge the render against the design export, and a constitution
70
+ written before that existed won't have it. Add the `page.screenshot({ path })` call;
71
+ it is one line, and without it the reviewer grades on numbers alone and says so.
68
72
  - Add the scale as a **guiding invariant** where it matters ("no raw color or spacing
69
73
  values in UI code — reference a token"), since that is the line a reviewer fails on.
70
74
 
@@ -5,4 +5,4 @@ into your Claude Code config directory (``~/.claude/`` by default). It adds no
5
5
  runtime behavior to the suite itself.
6
6
  """
7
7
 
8
- __version__ = "0.3.0"
8
+ __version__ = "0.4.0"
File without changes
File without changes
File without changes
File without changes