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.
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/PKG-INFO +164 -20
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/README.md +163 -19
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/agents/tiny-spec-build-reviewer.md +50 -4
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/pyproject.toml +1 -1
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny-spec-build/SKILL.md +4 -2
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny-spec-create/SKILL.md +12 -1
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny-spec-plan/SKILL.md +4 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny_spec/__init__.py +1 -1
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/.gitignore +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/LICENSE +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/agents/tiny-spec-build-executor.md +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/docs/eval/README.md +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny-spec-breakdown/SKILL.md +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny-spec-prd/SKILL.md +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny-spec-run/SKILL.md +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny-spec-tasks/SKILL.md +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny_spec/cli.py +0 -0
- {tiny_spec-0.3.0 → tiny_spec-0.4.0}/tiny_spec/manifest.json +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tiny-spec
|
|
3
|
-
Version: 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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
169
|
+
Wireframes usually get read once and forgotten. tiny-spec turns them into two durable
|
|
170
|
+
things — a 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
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
132
|
+
Wireframes usually get read once and forgotten. tiny-spec turns them into two durable
|
|
133
|
+
things — a 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,
|
|
77
|
-
|
|
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
|
|
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)
|
|
@@ -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,
|
|
83
|
-
|
|
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).
|
|
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
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|