Declaration-first UI spec (six-layer spec.md) shaped through an S0-S6 interactive session. Use when turning a short product/UI ask into six-layer spec.md, or when goal, edge-state, acceptance, or evidence requirements are missing before build.
Installs into .claude/skills of the current project.
Are you the author of Ux Spec?
Add the live security badge to your README. It updates with every re-scan.
[](https://www.skillsdirectory.com/skills/bandersnatch0x-ux-spec)
---
name: ux-spec
description: Declaration-first UI spec (six-layer spec.md) shaped through an S0-S6 interactive session. Use when turning a short product/UI ask into six-layer spec.md, or when goal, edge-state, acceptance, or evidence requirements are missing before build.
---
# ux-spec
Write a **six-layer `spec.md`**: the functional **declaration** for what must be true. Visual skin, tokens, and Badge-vs-Tag choices are out of scope.
Shaping runs as a session state machine (S0-S6) with append-only artifacts under `.scratch/<run>/shaping/`:
- `shaping-log.jsonl` — append-only event log (process authority; mirrors the decision-log philosophy)
- `queue.json` — derived state (pending questions / staged assumptions / open confirmations), always rebuildable from the log
Events (closed enum): `asked / answered / assumption_staged / confirm_presented / item_confirmed / item_rejected / item_revised / projected / suspended / resumed / superseded_by / archived`.
**Terminology:** In Chinese responses, use「成形(会话)」for "shaping (session)" — see `CONTEXT.md` glossary.
## Steps
### 0. Bind project contract — S0 intake assembly
When the host project has a persistent contract v1 (`contract.json` + optional `decisions.jsonl`), **bind-first** via `scripts/contract_v1.py` before writing L1–L6. Resurface every `assumed` / `open` field and any source-hash drift; `open` blocks dependent work; `assumed` needs explicit per-run acknowledgement. Reject unknown `schemaVersion` values. Do not invent layered inheritance or partial overrides (ADR-0019). Accepting a run spec may promote fields only as `assumed`/`open` — never as `decided` without named user confirmation in the decision log (ADR-0017).
S0 assembles the session inputs: persistent contract + decision log (bind semantics pre-check), project design baseline state, existing spec, reference contracts — and records the request **verbatim** as the first shaping-log event. Any state may `suspended`/`resumed`; on resume, rebuild `queue.json` from the log and re-ask only `asked`-without-`answered` items — already-`item_confirmed` values are never re-asked (revise only via `supersedes`). Resume is not queue replay alone: first re-run `bind_first` and diff the contract SHA against the first-bind snapshot. When the contract drifted while suspended, diff the affected fields — items grounded on a changed field lose their standing: affected unconfirmed items are voided and reopened in the queue, and a confirmed item on a drifted field loses its confirmed status (revise via `supersedes`); confirmations on unaffected fields are preserved (the append-only decision log loses nothing).
**Done when:** either no project contract exists, or bind-first recorded contract/decision-log SHAs and every unresolved or stale field was surfaced before authoring; the session log exists with the request recorded verbatim.
**Event append tool:** Use `python scripts/shaping_log.py append <log-path> --type <event> [--key value ...]` to append events to `shaping-log.jsonl`. Valid event types: `asked`, `answered`, `assumption_staged`, `confirm_presented`, `item_confirmed`, `item_rejected`, `item_revised`, `projected`, `suspended`, `resumed`, `superseded_by`, `archived`. Do not write temporary helper scripts — the official CLI handles all append operations.
**Derived state:** `queue.json` is always rebuilt from the log by `shaping_log.py`'s `derive_queue` (`resumed` events produce no queue items) — append an event, then re-derive; never hand-edit the queue. `contract_v1.py` is a **module-level API** (no CLI): bind/promote/decide are composed from skill-side Python, not invoked as a script.
**Output note:** When no project contract exists, S0 is a silent no-op (result records in shaping-log only). Do not narrate internal implementation details ("contract_v1", "模块级 API", "bind-first 空操作") to the user.
### 1. Shape requirements — S1-S6 session state machine
**S1 GRADE** — grade gaps on four consequence tiers: T1 consequential (goal / target user / success criteria / non-goals), T2 structural (materially different IA or primary-path alternatives), T3 visual-identity (register and route to the design-decision track; shaping never adjudicates), T4 local (inside confirmed declarations — agent-autonomous, never asked). Produce the question queue (T1 first; ≤3 questions per batch; every question names the downstream field(s)/L6 it changes) and the assumption plan. Questions with no downstream impact are not asked — they become T4 assumptions. When every required field already carries a contract value and grading surfaces no new gap (common on repeat runs), skip S2 and go directly to S3.
**S2 CLARIFY** — present batches (≤3), user answers; log `asked`/`answered` with each question's impact note. A refused T1 answer downgrades to an **explicit-risk assumption** into CP-C (never a silent `assumed`). After 2 consecutive batches without new T1 information, remaining T2/T3 items must convert to the assumption plan (fatigue cap; session soft cap 9 questions).
**S3 DRAFT** — draft L1-L6 from confirmed values + registered assumptions. A materially different IA / primary-path alternative surfaced while drafting becomes a T2 question back to S2 (max one such jump per drafting round); T4 local choices land directly in the draft. Draft the L2-L5 structured fields (per-page duty table, path table, per-page five-state matrix — see the template).
**S4 CONFIRM** — present confirmation batches: **CP-A** intent (≤4 items: goal / target user / success criteria wording / non-goals), **CP-B** structural (≤2 items, ≤3 options each; chosen value writes an `l2.*` field), **CP-C** assumptions (≤5 items, each with reason + risk + fallback). Per-item confirm / reject / revise — log `confirm_presented` / `item_confirmed` / `item_rejected` / `item_revised`. Rejected items re-enter the S2 queue alone; confirmed items are immutable (revision only via `supersedes`).
**S5 PROJECT** — project in the strict function order: `promote_fields` (registers `assumed` fields only) → `append_decision` per user-confirmed item → `apply_decisions` → write `spec.md` → `bind_first` (produces `contract-bind.json` + the assumed-ack list) → G7 drift check. Log one `projected` event carrying the mapping rows (decision id ↔ contract field ↔ spec section) — this record is what G9 checks. A G7 drift failure blocks the projection: return to S1 and re-grade with the new contract (already-confirmed items persist in the decision log; only the re-graded gaps re-enter the queue).
**S6 EXIT** — all five hold before the session may close: (a) bound subset has zero `open` fields; (b) every `assumed` field has this run's explicit ack; (c) L1 five fields + L6 criteria all valued and traceable (decided or acknowledged-assumed); (d) decisions.jsonl and contract have no unrecorded drift (G7); (e) spec.md has all six layers (G1). Then log `archived`. There is no silent-downgrade exit: either S6 passes or the session suspends with the open queue and reasons saved. Machine check: `python scripts/g9_shaping.py <shaping_dir>` — the argument is the **shaping directory** (`.scratch/<run>/shaping/`), not the run root.
From the ask, fix the user-visible goal, target user, in-scope scenes, **non-goals**, and always/ask/never boundaries. Ask only when a missing answer materially changes one of them; otherwise record a conservative assumption (CP-C visible). When `.scratch/<run>/reference/contract.md` exists (ADR-0011), **read it before writing L1–L6**. Fold its functional constraints, non-goals implied by Do not copy, and always/ask/never hints into L1 (and later L5/L6 edges). Cite the path; do not re-derive the screenshot from memory. The reference contract is input only — it does not replace any L1–L6 heading.
**Done when:** all five L1 fields are explicit — goal, target user, in-scope scenes, non-goals, and always/ask/never boundaries — with each assumption labeled as such; none left blank or implied; if a reference contract exists, its functional constraints are reflected (or an explicit rejected-with-reason note is recorded); and the S6 exit conditions hold with the session archived.
### 2. Expand L2–L4
- L2 regions and duties, plus the **Page duties** table (one owner duty per page)
- L3 states and transitions, plus the **Paths** table (ordered path rows `P1…Pn`)
- L4 control behavior per relevant state
Use the headings in [`references/spec-template.md`](references/spec-template.md).
**Done when:** every primary user job has a state path; every region has an owner duty; every page in the L5 matrix has a duty row.
### 3. Force L5–L6
- L5: empty, loading, error, permission — each with what the user can do next — plus the per-page **five-state matrix** (initial / loading / success / failure / empty enumerable per page)
- L6: checkable acceptance; every top-level item explicitly contains `Given`, then `When`, then `Then`, with the proof required for that item and a `(path: P<n>)` reference into the L3 path table
**L6 granularity (ADR-0016):** one top-level L6 item = one independently blocking **user-visible risk or outcome**. Three to seven items is a soft authoring budget — write more only with an explicit rationale that each extra item is independently blocking. There is **no** numeric validator gate. Accessibility and multi-stack proof attach to an existing criterion when they test the same risk; create a standalone accessibility L6 item only when the failure is independently blocking. Do **not** auto-generate accessibility or multi-stack L6 seeds. Validators count **every top-level list item** in the L6 section as a criterion and check `Given` → `When` → `Then` order on it — non-criterion content (e.g. a Design done definition) must be a plain paragraph, never a list item.
Evidence is criterion-shaped: visible states require rendered inspection at named target viewports; behavior requires an interaction trace or automated check; implementation health uses the relevant tests, type/lint checks, or affected build when available. Planning-only work names the future proof instead of claiming it exists. Where the proof is a runtime state, name the **capture seed** — the state to capture (e.g. "error-state screenshot") and the capture type. This is the seed the `observe*` step derives a capture plan from (`Given`/`When` → state+actions, `Then` → required); do not write selectors, URLs, or actions here — those are derived later. Capture contract v1 requires `schemaVersion: 1` and an explicit viewport at observe time (ADR-0018).
The host model may have no vision (text-only input): render artifacts are bound by **path reference** (manifest + ledger), never by viewing them. Machine assertions for review use the **text face** — HTML/CSS source, `a11y tree` text, and the action records inside the interaction trace (a Playwright trace **ZIP**; unzip before reading, the artifact is not JSON). A no-vision run follows this mode end to end; it is not a protocol downgrade, so do not write L6 proof that requires the composing model to look at a screenshot.
**Done when:** L5 is not a single word (“loading”); every L6 item is a top-level list item that uses `Given -> When -> Then` in that order, can be ticked pass/fail without taste debate, and says what evidence will prove it (naming the capture seed where the proof is a runtime state); every L6 item references a reachable path row; L6 items stay user-risk units rather than one row per evidence type.
### 4. Emit
Output the full `spec.md` using the template structure. Stop. Do not scaffold UI or pick components here. Refresh `shaping/queue.json` from the log before stopping.
**Done when:** one markdown spec exists containing every L1–L6 heading from the template, with steps 1–3 Done-when criteria still holding in the emitted file, ready for the next pipeline step (`ui-picker` / fill). Machine check right after writing spec.md: `python scripts/g1_spec.py .scratch/<run>/spec.md` — do not wait for the end-of-run `validate_run.py` sweep.
## Scope fence
| In | Out → |
| --- | --- | --- |
| Functional truth, flows, edges, acceptance | Color/type/motion → `design` / `craft-guard` |
| | Risk/secrets meaning → `domain` |
| | Component identity → `ui-picker` / `components` |