@brunosps00/dev-workflow 2.6.0 → 2.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.
Files changed (56) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +72 -0
  3. package/README.md +7 -4
  4. package/lib/doctor.js +17 -2
  5. package/lib/init.js +16 -6
  6. package/lib/migrate-skills.js +189 -39
  7. package/lib/removed-bundled-skills.js +4 -2
  8. package/lib/skill-registry.js +12 -2
  9. package/lib/uninstall.js +61 -5
  10. package/package.json +1 -1
  11. package/scaffold/en/agent-instructions.md +1 -1
  12. package/scaffold/en/commands/dw-brainstorm.md +2 -2
  13. package/scaffold/en/commands/dw-bugfix.md +1 -1
  14. package/scaffold/en/commands/dw-commit.md +1 -1
  15. package/scaffold/en/commands/dw-dockerize.md +2 -2
  16. package/scaffold/en/commands/dw-functional-doc.md +1 -1
  17. package/scaffold/en/commands/dw-harness-audit.md +1 -1
  18. package/scaffold/en/commands/dw-intel.md +1 -1
  19. package/scaffold/en/commands/dw-learn.md +1 -1
  20. package/scaffold/en/commands/dw-new-project.md +3 -3
  21. package/scaffold/en/commands/dw-plan.md +2 -2
  22. package/scaffold/en/commands/dw-qa.md +6 -5
  23. package/scaffold/en/commands/dw-redesign-ui.md +3 -3
  24. package/scaffold/en/commands/dw-review.md +2 -2
  25. package/scaffold/en/commands/dw-skill-health.md +24 -1
  26. package/scaffold/en/commands/dw-update.md +28 -11
  27. package/scaffold/en/references/invariants.md +4 -0
  28. package/scaffold/en/templates/project-onepager.md +1 -1
  29. package/scaffold/pt-br/agent-instructions.md +1 -1
  30. package/scaffold/pt-br/commands/dw-brainstorm.md +2 -2
  31. package/scaffold/pt-br/commands/dw-bugfix.md +1 -1
  32. package/scaffold/pt-br/commands/dw-commit.md +1 -1
  33. package/scaffold/pt-br/commands/dw-dockerize.md +2 -2
  34. package/scaffold/pt-br/commands/dw-functional-doc.md +1 -1
  35. package/scaffold/pt-br/commands/dw-harness-audit.md +1 -1
  36. package/scaffold/pt-br/commands/dw-intel.md +1 -1
  37. package/scaffold/pt-br/commands/dw-learn.md +1 -1
  38. package/scaffold/pt-br/commands/dw-new-project.md +3 -3
  39. package/scaffold/pt-br/commands/dw-plan.md +2 -2
  40. package/scaffold/pt-br/commands/dw-qa.md +6 -5
  41. package/scaffold/pt-br/commands/dw-redesign-ui.md +3 -3
  42. package/scaffold/pt-br/commands/dw-review.md +2 -2
  43. package/scaffold/pt-br/commands/dw-skill-health.md +24 -1
  44. package/scaffold/pt-br/commands/dw-update.md +28 -11
  45. package/scaffold/pt-br/references/invariants.md +5 -0
  46. package/scaffold/pt-br/templates/project-onepager.md +1 -1
  47. package/scaffold/scripts/hooks/session-cost.mjs +62 -21
  48. package/scaffold/skills/docker-compose-recipes/references/compose-composition.md +1 -1
  49. package/scaffold/skills/dw-llm-eval/SKILL.md +3 -1
  50. package/scaffold/skills/dw-llm-eval/references/integration-with-dev-workflow-commands-detail.md +2 -2
  51. package/scaffold/skills/dw-llm-eval/references/reference-dataset.md +57 -3
  52. package/scaffold/skills/dw-ui-discipline/SKILL.md +8 -5
  53. package/scaffold/skills/dw-ui-discipline/references/accessibility-floor.md +20 -8
  54. package/scaffold/skills/dw-ui-discipline/references/content-stress.md +246 -0
  55. package/scaffold/skills/dw-ui-discipline/references/motion.md +205 -35
  56. package/scaffold/skills/dw-ui-discipline/references/responsive.md +44 -2
@@ -37,6 +37,7 @@ Each case is one line in `cases.jsonl`:
37
37
  ```json
38
38
  {
39
39
  "id": "case-001",
40
+ "split": "dev",
40
41
  "input": {
41
42
  "user_message": "I want to cancel my subscription",
42
43
  "user_context": { "tier": "premium", "tenure_months": 18 }
@@ -60,6 +61,7 @@ Each case is one line in `cases.jsonl`:
60
61
  Fields:
61
62
 
62
63
  - **`id`** — stable identifier (you'll reference it in regression reports).
64
+ - **`split`** — `dev` (you tune against it) or `holdout` (you never do). See "Held-out split".
63
65
  - **`input`** — what the system receives. Match the production input shape exactly.
64
66
  - **`expected`** — for rungs 1-3, the deterministic expected output or state change. For rung 4, the rubric-target (what a 5/5 answer would do).
65
67
  - **`rubric_criteria`** — which rubric dimensions this case exercises.
@@ -129,18 +131,70 @@ Process:
129
131
  4. **Categorize** — difficulty, tags, rubric criteria.
130
132
  5. **Promote** to `cases.jsonl` after review.
131
133
 
134
+ ## Baseline before the change
135
+
136
+ > Eval-first, from LangChain's [eval-driven development](https://www.langchain.com/blog/what-is-eval-driven-development)
137
+ > post (2026-11-09): the eval is written and run before the change it measures.
138
+
139
+ When a task adds or changes an AI behavior, the eval exists before the implementation:
140
+
141
+ 1. **Spec the behavior, one at a time.** Four lines, definite enough to score pass/fail: when it should
142
+ happen, what the system does, what it must not do, and which existing behaviors must keep working.
143
+ "The answer is helpful" is not a spec.
144
+ 2. **Add the cases** for it: the new behavior, its must-not boundary (the near miss that should NOT
145
+ trigger it), and the existing cases that cover the keep-working list.
146
+ 3. **Run the baseline** on the current code, before any change, and save it as
147
+ `runs/<YYYY-MM-DD>-baseline.jsonl`.
148
+ 4. **Read the baseline.** New-behavior cases must fail there. A new case that already passes does not test
149
+ the change: rewrite it or drop it. Existing cases that pass are the regression set for this change.
150
+ 5. Implement, re-run, compare case by case (see "Detecting drift").
151
+
152
+ Each line of a run file is one case:
153
+
154
+ ```json
155
+ {"id": "case-001", "split": "dev", "pass": true, "score": 4, "commit": "a1b2c3d", "dataset_version": "2.3"}
156
+ ```
157
+
158
+ `pass` is required (the verdict against the case's oracle); `score` only when a rung-4 judge produced one.
159
+
160
+ ## Held-out split
161
+
162
+ Iterating until every case passes overfits the system to the dataset: the prompt learns the cases, not the
163
+ behavior. Mark about 20% of cases `"split": "holdout"`, spread across difficulty and tags, and:
164
+
165
+ - iterate against `dev` only — never read a holdout failure to decide the next prompt tweak;
166
+ - run `holdout` before merge to main and before deploy;
167
+ - if `dev` improves and `holdout` stays flat or drops, report overfitting instead of the improvement.
168
+
169
+ A holdout case that was used to tune is no longer held out: move it to `dev` and add a fresh one.
170
+
132
171
  ## Sampling for regression runs
133
172
 
134
173
  You don't need to re-run the entire dataset every time. Smart sampling:
135
174
 
136
- - **PR-time:** random sample of 30% + all "high difficulty" cases + any case added in the last 30 days. Fast feedback.
137
- - **Pre-merge to main:** full dataset.
175
+ - **PR-time:** `dev` split only — random sample of 30% + all "high difficulty" cases + any case added in the last 30 days. Fast feedback.
176
+ - **Pre-merge to main:** full dataset, `holdout` included.
138
177
  - **Nightly:** full dataset + judge re-calibration check.
139
178
  - **Pre-deploy:** full dataset + manual eyeball on 10 random outputs.
140
179
 
141
180
  ## Detecting drift
142
181
 
143
- After each run, compare against the prior run on the SAME cases:
182
+ After each run, compare against the prior run on the SAME cases — **case by case first, averages
183
+ second.** An average can rise while a case that used to pass breaks: the change fixed five things and
184
+ overwrote one behavior someone depends on.
185
+
186
+ ```
187
+ Run 2026-05-08 vs 2026-05-01 (dataset 2.3, 24 cases):
188
+ flipped pass → fail: case-007 (dev), case-019 (holdout)
189
+ flipped fail → pass: case-021, case-022, case-023
190
+ unchanged: 19
191
+ ```
192
+
193
+ The gate: **any case that passed in the prior run and fails now blocks**, whatever the average says, until
194
+ it is fixed or accepted in writing (PR or techspec: case id, why the old behavior no longer holds). A
195
+ fail → pass flip on a case nobody was working on is also worth a look: it may be judge drift, not progress.
196
+
197
+ Then the metric deltas:
144
198
 
145
199
  ```
146
200
  Run 2026-05-08 vs 2026-05-01:
@@ -33,12 +33,13 @@ For the four grounding questions, read `references/the-four-grounding-questions-
33
33
  | Any UI work (the full version of the grounding) | `references/hard-gate.md` |
34
34
  | Reviewing or proposing a design | `references/visual-slop.md` (14 patterns + specific anti-default values) |
35
35
  | Designing state coverage | `references/state-matrix.md` |
36
+ | Stress-testing with worst-case data (long names, `1 members`, 1,000 rows, 320px, 200% zoom) | `references/content-stress.md` |
36
37
  | Interactive widget (button, form, modal, anything clickable) | `references/accessibility-floor.md` |
37
38
  | No design authority exists in the project | `references/curated-defaults.md` (palettes / fonts / scales) |
38
39
  | Choosing or reviewing a palette / dark mode | `references/color-oklch.md` (OKLCH, tinted neutrals, contrast) |
39
40
  | Fonts, sizing, hierarchy, line length | `references/type-scale.md` (modular scale, pairing, measure) |
40
- | Adding animation or transitions | `references/motion.md` (easing, duration, reduced-motion) |
41
- | Cross-device / breakpoints / touch | `references/responsive.md` (mobile-first, container queries) |
41
+ | Adding, reviewing or removing animation | `references/motion.md` (frequency gate, easing, performance, removal, review triggers) |
42
+ | Cross-device / touch / mobile web | `references/responsive.md` (mobile-first, container queries, mobile fixes) |
42
43
  | Any user-facing copy (labels, errors, empty states) | `references/ux-writing.md` |
43
44
 
44
45
  ## The 14 visual-slop patterns (full catalog in `references/visual-slop.md`)
@@ -74,7 +75,8 @@ Before any interactive widget ships:
74
75
  - [ ] No keyboard traps.
75
76
  - [ ] Touch targets ≥24×24 CSS pixels (≥44×44 recommended on mobile).
76
77
  - [ ] Heading hierarchy is semantic (no skipped levels).
77
- - [ ] `prefers-reduced-motion` honored.
78
+ - [ ] `prefers-reduced-motion` honored (gentler, not zero).
79
+ - [ ] Zoom never disabled (no `user-scalable=no` / `maximum-scale=1`); content survives 200% zoom.
78
80
 
79
81
  Full verification recipes in `references/accessibility-floor.md`. `/dw-review --code-only` rejects the verdict if any interactive widget ships without these.
80
82
 
@@ -90,7 +92,8 @@ In all bend cases, document the bend in the PR (one line). "I skipped the state
90
92
 
91
93
  - `/dw-redesign-ui` runs the grounding end-to-end. Steps 4 (propose) and 7 (validate) consult this skill explicitly.
92
94
  - `/dw-plan techspec` UI sections must answer the 4 grounding questions and reference the state matrix.
93
- - `/dw-review --code-only` checks UI diffs against the 14 visual-slop patterns and the accessibility floor.
95
+ - `/dw-review --code-only` checks UI diffs against the 14 visual-slop patterns, the accessibility floor, `motion.md` §9 triggers and `content-stress.md` static signals.
96
+ - `/dw-qa` (UI mode) runs the content-stress pass.
94
97
  - `/dw-functional-doc` records the surface-job and scene sentences in the overview for each screen.
95
98
 
96
99
  ## Why this approach
@@ -108,5 +111,5 @@ When invoked directly or by a harness, return or merge this block:
108
111
  - **Evidence:** design source read, four grounding answers, states checked, screenshots, and accessibility checks.
109
112
  - **Artifacts:** UI proposal, design notes, state matrix, screenshot path, or QA finding.
110
113
  - **Decisions:** palette/type/component choices, state handling, and accepted deviations.
111
- - **Risks:** training-data defaults, missing states, contrast/focus failures, unreadable hierarchy, or mobile overflow.
114
+ - **Risks:** training-data defaults, missing states, contrast/focus failures, unreadable hierarchy, worst-case data breaks, high-frequency motion, or mobile overflow.
112
115
  - **Next Step:** exact design/code/QA action or missing input to gather.
@@ -152,24 +152,34 @@ Screen readers switch pronunciation based on this.
152
152
 
153
153
  ### 10. Reduced motion respect
154
154
 
155
- Honor `prefers-reduced-motion`:
155
+ Honor `prefers-reduced-motion`. It means fewer and gentler animations, not zero: drop movement (slide,
156
+ scale, parallax), keep the opacity/color changes that show a state change.
156
157
 
157
158
  ```css
158
159
  @media (prefers-reduced-motion: reduce) {
159
- *, *::before, *::after {
160
- animation-duration: 0.01ms !important;
161
- transition-duration: 0.01ms !important;
162
- }
160
+ .drawer { transition: opacity 200ms ease; transform: none; }
163
161
  }
164
162
  ```
165
163
 
166
164
  Or in Tailwind:
167
165
  ```html
168
- <div className="transition-all motion-reduce:transition-none">
166
+ <div className="transition-transform motion-reduce:transition-none">
169
167
  ```
170
168
 
169
+ A global `animation-duration: 0.01ms !important` reset is an acceptable project-wide fallback, as long as
170
+ every state change stays visible. Details in `motion.md` §7.
171
+
171
172
  Vestibular disorders trigger on parallax, large slide-ins, infinite scroll animations.
172
173
 
174
+ ### 11. Zoom is never disabled
175
+
176
+ `user-scalable=no` and `maximum-scale=1` in the viewport meta block pinch-zoom for low-vision users (WCAG
177
+ 1.4.4 Resize Text). They are usually added to stop iOS zooming into inputs; fix the cause instead —
178
+ inputs at `font-size: 16px` or more (`responsive.md`, "Mobile platform fixes").
179
+
180
+ Content must also survive **200% browser zoom** without clipping or overlapping: fixed heights on text
181
+ boxes are the usual culprit (`content-stress.md`, Step 4).
182
+
173
183
  ## Verification recipes
174
184
 
175
185
  ### Automated (CI)
@@ -195,8 +205,9 @@ Run on every PR. Block merge on violations.
195
205
  1. Open the changed page.
196
206
  2. Tab through with keyboard only. Verify focus visible at every stop.
197
207
  3. Run axe DevTools, fix violations.
198
- 4. Toggle prefers-reduced-motion in DevTools rendering panel. Verify animations stop.
199
- 5. Test with screen reader (VoiceOver on Mac, NVDA on Windows) on at least one critical flow.
208
+ 4. Toggle prefers-reduced-motion in DevTools rendering panel. Verify movement stops and state changes stay visible.
209
+ 5. Zoom the page to 200%. Verify nothing clips, overlaps or becomes unreachable.
210
+ 6. Test with screen reader (VoiceOver on Mac, NVDA on Windows) on at least one critical flow.
200
211
 
201
212
  ### Spot checks
202
213
 
@@ -221,5 +232,6 @@ LLM-produced UI commonly fails on:
221
232
  - `outline: none` with no replacement focus state.
222
233
  - Modals that trap focus only on success path, not on error.
223
234
  - Auto-playing carousels with no pause control.
235
+ - `user-scalable=no` / `maximum-scale=1` in the viewport meta.
224
236
 
225
237
  `/dw-review --code-only` runs axe-style checks on the diff for these.
@@ -0,0 +1,246 @@
1
+ # Content stress — break the UI with realistic worst-case data
2
+
3
+ > Adapted from the `break-ui` skill in [`emilkowalski/skills`](https://github.com/emilkowalski/skills)
4
+ > (MIT © 2026 Emil Kowalski). The worst-case catalog, failure signatures and truncate/wrap/clamp rule come
5
+ > from there, condensed. Injection at the data boundary, severity mapping and routing into `/dw-qa`,
6
+ > `/dw-review` and `/dw-redesign-ui` are written for dev-workflow.
7
+
8
+ The state matrix (`state-matrix.md`) asks *which states* a surface has. This reference asks the other
9
+ question: what happens when the data inside a state is the data real users actually produce. Demo data is
10
+ chosen, usually without anyone noticing, to make the design look good: `Jane Doe`, `jane@acme.com`,
11
+ `12 members`, every optional field filled in. Production has `Aleksandra Wiśniewska-Kowalczyk`, an email
12
+ with a 40-character domain, an intern called `Jo`, and a workspace of 1,284 members.
13
+
14
+ ## Two failure modes (the first is worse)
15
+
16
+ 1. **Nonsense data.** `"aaaaaaaaaaaaaaaaaaaaaaaa"` and 5,000-character names prove nothing, and the team rightly
17
+ stops listening. Every worst-case value must be something a real user could produce, or the actual
18
+ limit the backend accepts.
19
+ 2. **Stopping at long text.** The breaks that ship are usually the short ones: the one-letter name, the
20
+ missing avatar, the count of exactly 1 (`1 members`), the empty list, the badge whose text got
21
+ translated.
22
+
23
+ ## Rules
24
+
25
+ 1. **Plausible or schema-backed, never random.** Each value is a realistic example or the real limit from
26
+ the validation schema (Zod, Yup, Valibot), the database column, the API contract or the form
27
+ `maxLength`. No limit found is itself a finding: record the field as `unbounded`. A 50-character input
28
+ that saves to a 255-character column will eventually receive 255 characters through an import or the
29
+ API.
30
+ 2. **Change the data, not the component.** The worst case enters through the same boundary the demo data
31
+ does: the fixture, the mock, the story args, the API stub, or a network route in the browser test. Never
32
+ hand-edit markup or CSS to produce a break; that tests the edit, not the component.
33
+ 3. **One dataset, many failures.** Spread different failures across the first rows of a list (row 1 the
34
+ long name, row 2 the unbreakable email, row 3 the one-letter name), the way real data mixes them.
35
+ 4. **Dev-only.** A worst-case switch (`?data=worst`, a story, a prototype route) never ships to production.
36
+ 5. **Report before fixing.** Some breaks are design decisions (truncate or wrap? hide the role or reserve
37
+ its line?). List every break with a proposed fix; fixing is the owning command's call.
38
+ 6. **Keep the fixture.** It is the regression test for the next person who touches the component.
39
+ 7. **Fixture domains:** use `example.com`, `example.org` or `.test` so a fixture never points at a real
40
+ inbox or site.
41
+
42
+ ## Step 1 — map the surface
43
+
44
+ List every value the component renders, with its source and limit:
45
+
46
+ | Field | Source | Type | Limit | Optional? |
47
+ |---|---|---|---|---|
48
+ | `name` | `member.name` | string | 255 (`schema.ts:14`) | No |
49
+ | `email` | `member.email` | string | unbounded | No |
50
+ | `role` | `member.title` | string | 120 | Yes |
51
+ | `count` | `workspace.memberCount` | int | — | No |
52
+
53
+ Include the values people forget: counts in headers, relative timestamps, badge and status text, button
54
+ labels that come from data, tooltips, avatar images, and the list itself (its length is a value too).
55
+
56
+ **Done when** every rendered value is in the table with a source and a limit or `unbounded`.
57
+
58
+ ## Step 2 — build the worst case from the catalog
59
+
60
+ Pick from the rows that apply to the fields in the table.
61
+
62
+ ### People and names
63
+
64
+ | Value | Breaks |
65
+ |---|---|
66
+ | `Aleksandra Wiśniewska-Kowalczyk` | Long, hyphenated, diacritics; wraps, and the hyphen is a break point |
67
+ | `Christopher Alexander Montgomery III` | Suffix; naive first + last initials give `CI` |
68
+ | `Jo` / `J` | Name column mostly empty; naive initials, tiny click target when the name is the link |
69
+ | `Đặng Thị Ngọc Hân` | Stacked diacritics clipped by tight `line-height` + `overflow: hidden` |
70
+ | `王秀英` | CJK, no spaces; "first + last word" initials finds one word |
71
+ | `نور الهدى عبد الرحمن` | RTL; punctuation and icons land on the wrong side without `dir="auto"` |
72
+ | `María José de la Cruz y Fernández` | Lowercase particles; initials and last-name sorting |
73
+ | `🦊 Fox` / `👩🏽‍💻 Priya` | Emoji first: `.charAt(0)` returns half a surrogate pair; ZWJ sequences split when sliced |
74
+ | ` Sam Lee ` | Leading, trailing, repeated spaces; initials from empty words |
75
+ | *(missing)* | Only an email exists; the UI must fall back to something |
76
+
77
+ ### Emails, URLs, identifiers (unbreakable strings)
78
+
79
+ | Value | Breaks |
80
+ |---|---|
81
+ | `bartholomew.fitzgerald@northwind-industries-holdings.example.com` | Pushes every sibling off the row without `overflow-wrap: anywhere` |
82
+ | `a@b.co` | Layout that assumed a long email looks empty |
83
+ | `first.last+billing-notifications@example.com` | Validation rejecting `+`; truncation hiding the meaningful part |
84
+ | A 100-character URL with path and query | Overflow; end-truncation hides the part that differs |
85
+ | `9f8e7d6c-5b4a-4c3d-8e2f-1a0b9c8d7e6f` | UUID; middle-truncation candidate |
86
+ | `Q3 Board Deck — FINAL (revised) v12 [approved].pdf` | End-truncation hides version and extension |
87
+
88
+ ### Labels and copy from data
89
+
90
+ | Value | Breaks |
91
+ |---|---|
92
+ | `Senior Product Design Engineer, Platform Infrastructure` | Wraps to three lines in a secondary slot |
93
+ | `Invitation expired 12 days ago` | Long status badge squeezes the name column |
94
+ | `Benachrichtigungseinstellungen` | 30-character German compound, no break opportunity |
95
+ | French or German translation of a short label | UI copy runs ~30% longer in translation |
96
+ | Twelve tags on one item / one 45-character tag | Tag wall without a `+N` overflow; a tag wider than its container |
97
+ | `Untitled` / empty string / ` ` | Collapsed heading, zero-height row |
98
+ | `<script>alert(1)</script>` / `&amp;` / `**bold**` | Must render as literal text |
99
+ | Newline inside a single-line field | Doubles row height or is silently dropped |
100
+ | A 2,000-character pasted description | Clamp, "show more", textarea growth |
101
+
102
+ ### Numbers and money
103
+
104
+ | Value | Breaks |
105
+ |---|---|
106
+ | `0` / `1` | Zero states, division by zero in percentages; `1 members`, `1 days ago` |
107
+ | `1284` / `1000000` | Thousands separator; badge width (consider compact `1M` where precision does not matter) |
108
+ | `12345678.9` as currency | Overflows totals columns |
109
+ | `-42.5`, `142%`, `-3%` | Sign and color logic; bars and meters past their bounds |
110
+ | `0.1 + 0.2`, `null`, `undefined`, `NaN` | Rendered raw |
111
+ | `1.284` (de-DE) vs `1,284` (en-US) | Hardcoded separators are wrong for half the world |
112
+ | A value that changes live (`99` → `100`) | Width jump and jitter without `tabular-nums` |
113
+
114
+ ### Collections
115
+
116
+ | Value | Breaks |
117
+ |---|---|
118
+ | 0 items / 1 item | The empty state (and whether one exists); grids that look broken with one card |
119
+ | Exactly page size, and page size + 1 | `Showing 40 of 40`, an empty page 2 |
120
+ | 1,000+ items unpaginated | Scroll performance, memory, `Showing 40 of 1,284` copy |
121
+ | One item 10× larger than the others | Rows and masonry stretching to the tallest item |
122
+ | Items with identical names | Lists where the name is the only distinguishing field |
123
+
124
+ ### Time
125
+
126
+ | Value | Breaks |
127
+ |---|---|
128
+ | Now | `0 seconds ago` instead of "just now" |
129
+ | 12 days, 11 months, 3 years ago | Relative time past about a week should become an absolute date |
130
+ | A future date | `-3 days ago` |
131
+ | `1970-01-01` | A zero timestamp shown as a real date |
132
+ | `2025-12-31T23:30:00-08:00` | A different day in UTC and in the user's timezone |
133
+
134
+ Use `Intl.RelativeTimeFormat` and `Intl.DateTimeFormat`, not hand-built strings.
135
+
136
+ ### Images and media
137
+
138
+ | Value | Breaks |
139
+ |---|---|
140
+ | Avatar URL that 404s / no avatar | Broken-image icon; the initials fallback's size and color parity |
141
+ | 4000×200 or 200×4000 image | Distortion without `object-fit: cover`; blown-out card height |
142
+ | Dark or transparent logo in dark mode | Invisible on the background |
143
+ | Slow image without dimensions | Layout shift without fixed size or `aspect-ratio` |
144
+
145
+ ### States mixed into one list
146
+
147
+ Partial data (some optional fields filled, others not), every enum status at once, a row without
148
+ permission, the current user in the list (`You`, actions that should not apply to yourself), and an API
149
+ error rendered raw (`TypeError: Cannot read properties of undefined`).
150
+
151
+ ## Step 3 — inject at the boundary
152
+
153
+ Pick the first that exists, in this order:
154
+
155
+ 1. The project's own mock layer: fixtures, MSW handlers, Storybook stories, a seed script.
156
+ 2. A network route in the browser test (`page.route()` in Playwright) returning the worst-case payload.
157
+ This changes no application code and is the default for `/dw-qa`.
158
+ 3. A dev-only switch (`?data=worst` read where the fixture is chosen), only when the owning command is
159
+ allowed to edit code (`/dw-redesign-ui` implement step, `/dw-run`).
160
+
161
+ Also cover the cases one dataset cannot hold: **empty** (0 items), **one** (single item, every count at
162
+ exactly 1) and **huge** (the realistic upper bound, 1,000+ rows when unpaginated).
163
+
164
+ ## Step 4 — break it
165
+
166
+ View the worst case and check each failure signature:
167
+
168
+ - at the component's **real container width** (a sidebar list is not a full-page list), then at **320px**
169
+ and at the **widest** layout it supports;
170
+ - at **200% browser zoom** (or a raised root font size), because text grows and fixed boxes do not;
171
+ - in **dark mode** and **RTL** (`dir="rtl"` on a wrapper) when the product supports either;
172
+ - on **touch**: hover-only actions (the `•••` that appears on hover) are unreachable.
173
+
174
+ Screenshot each state when browser tooling exists. Without it, reason from the CSS and say which findings
175
+ were seen and which were inferred.
176
+
177
+ ### Failure signatures
178
+
179
+ | What you see | Cause | Fix |
180
+ |---|---|---|
181
+ | Avatar or icon squished into an oval | Flex child shrinking | `flex-shrink: 0` on fixed-size boxes |
182
+ | Text overflows its box | Flex/grid child keeps `min-width: auto` | `min-width: 0` on the text column (`minmax(0, 1fr)` in grid) |
183
+ | Email or URL runs past the edge | No break opportunities | `overflow-wrap: anywhere` on that element |
184
+ | Trailing action pushed off-screen | Middle content took all the space | `min-width: 0` on the middle, `flex-shrink: 0` on the action |
185
+ | Badge wraps onto two lines | Badge allowed to shrink | `white-space: nowrap; flex-shrink: 0`, and decide what yields instead |
186
+ | Avatar adrift next to a three-line name | `align-items: center` on rows of varying height | `align-items: flex-start` once text can wrap |
187
+ | Heading breaks mid-word | `word-break: break-all` | `overflow-wrap: anywhere` breaks only when it must |
188
+ | Wrong initials (`J` for Jo, `CI`, `�`) | `.split(' ')[0][0]`, `.charAt(0)` | Grapheme clusters via `Intl.Segmenter`, first + last word, fallback icon |
189
+ | Orphaned `—` or empty line | Placeholder rendered for a missing optional field | Omit the line, or reserve its height on purpose |
190
+ | `1 members`, `0 member` | Hardcoded plural | `Intl.PluralRules` or per-count strings |
191
+ | Numbers jitter, columns misalign | Proportional figures | `font-variant-numeric: tabular-nums` |
192
+ | `1284`, `1,284.000000001`, `NaN` | Raw number rendered | `Intl.NumberFormat` with the user's locale; guard null |
193
+ | Translated button label overflows | Fixed-width button | `min-width` and content width, never a fixed `width` |
194
+ | Diacritics or tall scripts clipped | Tight `line-height` + `overflow: hidden` | Looser `line-height`, no clipping on text boxes |
195
+ | Broken-image icon in the avatar | No error fallback | Fall back to initials; `object-fit: cover` |
196
+ | Truncated text with no way to read it | `text-overflow: ellipsis` and nothing else | `title` or tooltip, plus the full value elsewhere |
197
+ | Last row cut mid-glyph | Fixed height without scroll affordance | Visible scrollbar or fade mask; confirm `overflow` is intended |
198
+ | 1,000 rows stutter | Every row rendered | Virtualize or paginate, and say which |
199
+ | Raw `<b>`, `&amp;`, `**text**` on screen | Wrong escaping layer | Escape once, at render; never `dangerouslySetInnerHTML` user data |
200
+
201
+ ### Truncate, wrap or clamp — decide per field
202
+
203
+ - **Wrap** text the user needs whole to identify something (names, titles in a detail view). Two lines is
204
+ usually fine; four means the column is too narrow.
205
+ - **Truncate at the end** secondary metadata whose start carries the meaning (role, description, message
206
+ preview), always with a way to see the full value.
207
+ - **Truncate in the middle** when items differ at the *end*: file names, emails sharing a domain, paths,
208
+ hashes. End-truncation makes them identical.
209
+ - **Clamp** (`line-clamp: 2`) multi-line previews in cards, so card heights stay predictable.
210
+ - **Never truncate** numbers, amounts, dates, or anything the user compares.
211
+
212
+ ## Severity mapping
213
+
214
+ | Content-stress class | dev-workflow severity | Meaning |
215
+ |---|---|---|
216
+ | Broken | high | Content unreadable, action unreachable, or wrong data shown |
217
+ | Ugly | medium | Readable but visibly wrong: squished avatar, wrapped badge |
218
+ | Fragile | low | Fine now, one realistic step from breaking: no limit, no fallback |
219
+
220
+ Raise to critical only when the break hides or misstates money, permissions, or destructive actions.
221
+
222
+ ## Static signals for code review
223
+
224
+ `/dw-review` cannot render, but these patterns in a UI diff are candidates (each still goes through
225
+ `dw-review-rigor`):
226
+
227
+ - Initials built with `.split(' ')`, `[0]`, `.charAt(0)` or `.slice(0, 1)` on a name.
228
+ - A count interpolated next to a fixed plural noun (`` `${n} members` ``).
229
+ - `text-overflow: ellipsis` / `truncate` with no `title`, tooltip or detail path.
230
+ - A fixed `width` on a button or badge whose label comes from data or translation.
231
+ - A flex row with an avatar or trailing action and no `flex-shrink: 0`, or a text column with no
232
+ `min-width: 0`.
233
+ - Numbers rendered without `Intl.NumberFormat` / dates without `Intl.DateTimeFormat`.
234
+ - `dangerouslySetInnerHTML` (or `v-html`, `innerHTML`) fed by user or API data — also a security finding.
235
+ - A new string field rendered in UI with no limit in the schema (`Fragile`).
236
+
237
+ ## Output
238
+
239
+ One row per break, worst first:
240
+
241
+ | # | Severity | Field | Worst-case value | What happens | Fix (`file:line`) |
242
+ |---|---|---|---|---|---|
243
+
244
+ Then **Decisions for you** (breaks with more than one right answer, one line each with a recommendation)
245
+ and **What held up** (worst cases the component already handles — proof the test was real, and a list of
246
+ what not to touch).