@mmerterden/multi-agent-pipeline 16.23.0 → 16.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,52 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [16.25.0] - 2026-09-09
20
+
21
+ ### Added
22
+
23
+ - **`features/design-conformance.md` - the component walk, ported as doctrine rather than as a tool.** An upstream corporate screen-QA skill landed with a catalog worth having, and the pipeline already owns this problem in `/multi-agent:design-check`. Copying the skill would have produced a second design auditor that drifts from the first, so what came across is the part that is portable: the catalog and the three mechanisms that turn "I looked at it" into a countable result.
24
+
25
+ **Enumerate first.** Build the component inventory once, before any comparison, with elements nested inside buttons, fields and cards exploded into their own rows - an inventory built by kind drops a button silently, and with it the button height and its label. Then fill every cell: a blank one is a fail-to-verify, never a pass.
26
+
27
+ **Measure, never read the token.** A `[MEASURE]` item compares the design against measured pixels or the resolved code value, never against the design tool's node box or the implementation's own layout token. The token is the thing under test; comparing it against itself is how a 48pt field ships against a 56pt design with every check green.
28
+
29
+ **Converge per component.** One pass catches roughly one defect on a component - the wrong font, and not that same label's wrong colour and inset. Each component is re-checked until two consecutive attempts find nothing new, capped at attempt 8 with a `did-not-converge` warning. The screen is done when every component converged, not when the walk reached the bottom.
30
+
31
+ Plus the four scope tags (`[MEASURE]` / `[CAPTURE+]` / `[A11Y]` / `[DYNAMIC]`, and a `[DYNAMIC]` item may never be silently passed), the 30 rule groups, and the bidirectional presence gate - which finds not only what the design has and the implementation lacks, but the neutral decoration the implementation adds that a one-directional colour comparison cannot see.
32
+
33
+ ### Changed
34
+
35
+ - **`design-check` no longer halts on a module without mock support.** The mock gate gains a snapshot lane: a screen that can be rendered from its own view model is auditable even when nothing can toggle a variant at runtime. The trade is stated rather than hidden - snapshots reach modules mocks cannot, and cannot flip an in-app variant - and a declined or impossible snapshot still halts rather than fabricating a comparison.
36
+
37
+ - **The report says what it did not check.** The per-variant record gains `walk`: how many components were inventoried, how many converged, which ones hit the cap, and every `[DYNAMIC]` / `[CAPTURE+]` item that was not run, with its reason. An empty not-verified list on a run that never captured a dark or mirrored pass is a false clean, so the skip is listed instead.
38
+
39
+ - **`derivedSkillSources` records what was reviewed and refused, not only what was taken.** New `derivationNote`, `excluded` and `notDerived` fields. `tools/openapi-regen` is marked deliberately-not-derived at 0.45.0 with its reason: the skill's value is being a driver around three named scripts in one repo, so renaming its brand words leaves a skill pointing at scripts nobody else has. Without that record, every drift review rediscovers the same candidate and re-argues it.
40
+
41
+ - **`smoke-design-conformance.sh`** - 26 checks. Half of them guard the doctrine against being trimmed back into a glance; the other half name the project assets that must never arrive: a vendored colour catalog carrying cabin-class names, a typography catalog carrying licensed font families, product screen names, upstream script names. Those leak by content and carry no brand word, so a gate that greps for a company name would pass them.
42
+
43
+ ## [16.24.0] - 2026-09-09
44
+
45
+ ### Added
46
+
47
+ - **A UI fix now carries its picture.** Three changed files in a diff do not tell a reviewer what was wrong, and they do not tell a tester what to look for. When `taskType` is `bugfix` and a UI file changed, the run produces a before/after pair; when the work is design (a `component` task, a Figma reference, or a UI file that did not exist before), it produces the after. The verdict comes from `taskType` plus the changed-file list, mechanically, not from the model's reading of the task - the UI-file test is per stack and written down.
48
+
49
+ **Before is the reporter's screenshot, or nothing.** Rebuilding the pre-fix state to photograph it costs a second build and a second launch on every UI bug, to recreate evidence the reporter usually attached already. So Phase 0 keeps the issue's own image attachments and that is the whole of it; no image on the ticket is a recorded gap, not a search.
50
+
51
+ **After is captured in Phase 3, not Phase 5.** Phase 5 is the natural home - the simulator is already up - and it is dropped by every `autopilot` and `--local` entry, so a capture living only there produces nothing for exactly the runs nobody watched. It happens instead right after the build+test gate goes green, which is in every mode's phase set. `capture-evidence.sh` owns the naming, cleans the status bar (two shots of one screen otherwise differ by the clock, which makes every "after" look like a change) and downscales to 1242px.
52
+
53
+ - **The flow video, in three tiers, so no repo is excluded by not having a test target.** Tier 1 runs the repo's own XCUITest/Espresso target when it can be seeded with test data - the most faithful record, and it is a test that already runs in CI. Tier 2 drives the flow with `agent_run_steps` and records with `ios_record_video` / `android_record_screen`, needing nothing but a launchable build. Tier 3 is the honest skip for a library or a backend, with the reason recorded. The flow itself comes from the ticket when someone wrote one there, and from the Phase 5 test scenarios otherwise: human-written beats generated, the same rule the before follows.
54
+
55
+ - **Where they land.** The artefacts are Jira attachments. The Phase 7 comment renders them _inside_ its existing sections rather than adding a fourth - the section order is fixed and the contract says so - with the before/after thumbnails under the work summary and the flow video under the scenario it demonstrates. The PR body gains a `visuals` section that names the filenames and the ticket, deliberately not the URLs: a Jira attachment URL is auth-gated and renders as a broken image for anyone reading the PR outside a Jira session.
56
+
57
+ - **Degrading quality is allowed; dropping the artefact is not.** Jira's ceiling is an instance setting, so it is a preference (`visualEvidence.maxAttachmentMb`, default 10). Over it, a video re-encodes to 720p and a screenshot to JPEG q80; still over, the video becomes a four-frame contact sheet that says why. Phase 6 Step 3 blocks when a required artefact is neither attached nor explained - the gate is against silence, not against an honest "the ticket carries no image".
58
+
59
+ - **`smoke-visual-evidence.sh`** - 32 checks, most of them about stated absence being as required as stated presence: both scripts refuse bad input rather than guessing, the size ladder never returns an empty path, all four phases act, both renderers know where pictures go, the schemas declare the fields, and no ref re-implements the attachment POST.
60
+
61
+ ### Changed
62
+
63
+ - **One Jira uploader instead of two.** `generate-issue.md` carried its own attachment `curl`; visual evidence would have been the second copy, and two copies is two places for `X-Atlassian-Token: no-check` to go missing - which fails as a 403 that reads like an auth problem. Both now call `jira-attach.sh`, which resolves host and token from prefs, keeps the token out of argv, and prints `<filename>\t<url>` per file.
64
+
19
65
  ## [16.23.0] - 2026-09-09
20
66
 
21
67
  ### Fixed
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "16.23.0",
3
+ "version": "16.25.0",
4
4
  "description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -50,7 +50,7 @@ Phase 4: Report → coverage gate + assemble + export HTML + PDF (+ Conf
50
50
 
51
51
  - **iOS**: Xcode + an available Simulator. **Android**: Android SDK + a running emulator / device.
52
52
  - **MCP**: the `multi-agent-toolkit` MCP server (>= 2.8.0, for `design_scenario_inventory`, the `design_report` coverage gate, and `design_visual_compare` region alignment). The Figma MCP (`mcp__claude_ai_Figma__*`) must be authenticated - the user supplies the Figma URL.
53
- - The selected module must support a **mock mode** (Phase 0 gate decides this). No mock support → the command halts.
53
+ - The selected module must be auditable: a **mock mode** (preferred - it can toggle variants) or a **snapshot capture** of the screen's view model. Phase 0 decides which. Neither → the command halts.
54
54
 
55
55
  ## Phase Tracker Contract (mandatory)
56
56
 
@@ -112,7 +112,7 @@ for l in sys.stdin:
112
112
  extra_keys: <design-check-config mock.keys, if any> })
113
113
  ```
114
114
  Branch on `supported`:
115
- - **`false`** → **HALT**. Show `reason` + that no runtime mock switch / launchable app / mock fixtures were found. Tell the user this module cannot be audited by design-check and stop. Do not fabricate a comparison.
115
+ - **`false`** → try the **snapshot lane** before halting: a screen renderable from its own view model (filled, flow-advanced, no backend) is auditable without a runtime mock switch. Ask via `AskUserQuestion`, persist `state.designCheck.captureLane = "snapshot"`. State the trade: snapshots reach modules mocks cannot and cannot flip an in-app variant, so the target set is whatever the view model can be built into. Declined or not renderable → **HALT** with `reason`. Never fabricate a comparison.
116
116
  - **`"debug-only"`** → surface an `AskUserQuestion` warning: mocks are compiled into the Debug build via `#if DEBUG` DI, so **variants cannot be toggled at launch** - only the default Debug state is comparable. Options (label + description in `outputLanguage`): option 1 semantically "Continue" - audit the single default Debug state only; option 2 semantically "Cancel" - stop.
117
117
  - **`true`** → continue. Persist `state.designCheck.mock = <detect result>` (mechanism, activation, variantsHint, evidence).
118
118
  5. **SCENARIO INVENTORY (this is the audit's target set)**:
@@ -170,6 +170,10 @@ Persist the variant list + spec + PNG paths to `state.designCheck.variants[]`.
170
170
 
171
171
  **This phase iterates `state.designCheck.scope.targetIds` - a finite list decided in Phase 0. It ends when every id is resolved, not when the UI stops offering new taps.** Tap-walking alone cannot reach a state that needs a different launch argument, a different scenario case, or a typed scenario code, which is exactly how a module's error / edge states go missing.
172
172
 
173
+ ### 3.0 What every capture is compared against
174
+
175
+ **Read `$HOME/.claude/multi-agent-refs/features/design-conformance.md` before the first compare.** It owns the per-component catalog (30 groups), the scope tags, and the three rules that decide whether this phase produced a result or an impression: enumerate first (a blank cell is a fail-to-verify), measure never the token, and converge per component (two consecutive clean attempts, cap 8). The screen is done when every component converged - not when the walk reached the bottom.
176
+
173
177
  ### 3.1 Drive the inventory's plan, batch by batch
174
178
 
175
179
  **Follow `inventory.plan`, filtered to the scoped ids.** Each batch is one launch: the batch's relaunch target opens a screen, and every in-app target for that same screen is then flipped while the app is already sitting there. Driving the plan is what makes full coverage affordable - target-by-target relaunching is what made earlier runs give up a third of the way in.
@@ -260,8 +264,13 @@ Before leaving this phase, compare `covered.length + skipped.length` against `sc
260
264
  truncatedInventory: <inventory.truncated>,
261
265
  floor: <config coverage.floor, optional> },
262
266
  variants: [ { name, figmaNodeId, perceptualPct, passed, compareSize, liveSize,
263
- images, findings, fixPrompt, componentRefs } ] }
267
+ images, findings, fixPrompt, componentRefs,
268
+ walk: { components: <inventoried>, converged: <count>,
269
+ didNotConverge: [ { component, attempts } ],
270
+ notVerified: [ { item, tag, reason } ] } } ] }
264
271
  ```
272
+
273
+ `walk` separates a checked screen from a viewed one. An empty `notVerified` on a run that never captured a dark or mirrored pass is a false clean - list the skip. Field definitions: `features/design-conformance.md`.
265
274
  Pass **ids**, not counts: the engine then names each unaccounted target in the report instead of printing an anonymous tally. `compareSize` and `liveSize` come straight from the `design_visual_compare` result.
266
275
  3. **Render + export**:
267
276
  ```
@@ -20,6 +20,13 @@ Every Jira comment posted by this adapter follows the same section order. Sectio
20
20
 
21
21
  **`summary`** - 2-5 sentences in `outputLanguage`. What changed, why, and the user-visible impact. No "we", no marketing tone. Past tense (the work is done at the time the comment goes up).
22
22
 
23
+ **Visual evidence inside these two sections.** When `state.visualEvidence` carries artefacts, they render INSIDE `summary` and `test_scenarios` - never as a fourth section, which the fixed section order forbids. Upload first (`jira-attach.sh <issue> <file>...`), then reference by the returned filename:
24
+
25
+ - `summary`, after its sentences: one line naming the pair in `outputLanguage` (`Düzeltme öncesi / Düzeltme sonrası`), then the thumbnails on the next line - `!<file>-before.png|thumbnail! !<file>-after.png|thumbnail!`.
26
+ - `test_scenarios`, under the scenario the recording demonstrates: `!<file>-flow.mp4!` plus one line stating the tier used.
27
+
28
+ A `gaps[]` entry prints its reason on the line where the artefact would have been (`Düzeltme öncesi: ticket'ta görsel yok`). Never an empty thumbnail, never a silent omission. Contract: `$HOME/.claude/multi-agent-refs/features/visual-evidence.md`.
29
+
23
30
  **`test_scenarios`** - Given/When/Then numbered list. One scenario per acceptance criterion. The heading and scenario text are rendered in `outputLanguage` at write-time; the template itself (this file) shows the English skeleton:
24
31
 
25
32
  ```markdown
@@ -14,9 +14,10 @@ The PR description targets code reviewers - it stays technical. Every adapter
14
14
  | 2 | `changes` | `## Değişiklikler` | `## Changes` | always |
15
15
  | 3 | `architecture` | `## Mimari Kararlar` | `## Architecture Decisions` | when a non-trivial design choice was made |
16
16
  | 4 | `verification` | `## Doğrulama` | `## Verification` | always |
17
- | 5 | `risk` | `## Risk ve Güvenlik` | `## Risk and Security` | when `state.diffRisk.signals` carries a high-stakes signal (`security_path`, `migration`, `public_api`, `no_test_change`, `test_lines_removed`) |
18
- | 6 | `dependencies` | `## Bağımlılıklar` | `## Dependencies` | when deps added/removed/bumped |
19
- | 7 | `related` | `## İlgili` | `## Related` | always (Jira/issue ref; never `Closes/Fixes`) |
17
+ | 5 | `visuals` | `## Görsel Kanıt` | `## Visual Evidence` | when `state.visualEvidence.required` |
18
+ | 6 | `risk` | `## Risk ve Güvenlik` | `## Risk and Security` | when `state.diffRisk.signals` carries a high-stakes signal (`security_path`, `migration`, `public_api`, `no_test_change`, `test_lines_removed`) |
19
+ | 7 | `dependencies` | `## Bağımlılıklar` | `## Dependencies` | when deps added/removed/bumped |
20
+ | 8 | `related` | `## İlgili` | `## Related` | always (Jira/issue ref; never `Closes/Fixes`) |
20
21
 
21
22
  ### Section content rules
22
23
 
@@ -62,6 +63,18 @@ Multi-repo PRs (one PR per repo) emit verification commands for that repo's stac
62
63
  - Rollback: feature flag <name> | git revert <sha> | none, and why
63
64
  ```
64
65
 
66
+ **`visuals`** - only when `state.visualEvidence.required`. The images live on the Jira issue as attachments; this section exists so a reviewer opening the PR knows they are there and what each one shows. Filenames, not raw URLs - a Jira attachment URL is auth-gated and renders as a broken image for anyone reading the PR outside a Jira session.
67
+
68
+ ```markdown
69
+ ## Visual Evidence
70
+
71
+ - Before: `<before-filename>` (attached to PROJ-XXXXX)
72
+ - After: `<after-filename>` (attached to PROJ-XXXXX)
73
+ - Flow video: `<flow-filename>`, tier <N> (attached to PROJ-XXXXX)
74
+ ```
75
+
76
+ Every `state.visualEvidence.gaps[]` entry becomes its own line with the reason instead of a filename (`- Before: none - the ticket carries no image attachment`). Phase 6 Step 3 blocks on a required artefact that is neither listed nor explained. Contract: `$HOME/.claude/multi-agent-refs/features/visual-evidence.md`.
77
+
65
78
  **`dependencies`** - only when `Package.swift` / `Podfile` / `build.gradle` / `package.json` changed. Each entry: `package@old → new - reason`.
66
79
 
67
80
  **`related`** - flat list, plain text. Examples:
@@ -0,0 +1,312 @@
1
+ # Design conformance - the component walk, and why a glance is not a pass
2
+
3
+ A design audit that reads a screen top to bottom and reports what looks wrong
4
+ finds about one defect per element: the wrong font on a label, and not that same
5
+ label's wrong colour and wrong inset. This file is the catalog, and the three
6
+ mechanisms that turn "I looked at it" into a countable result.
7
+
8
+ Derived generically from an upstream corporate screen-QA tool (recorded in
9
+ `derivedSkillSources`). The doctrine is portable. Its vendored design-token
10
+ catalogs and product examples are not, and were not taken - section 7 says what
11
+ replaces them.
12
+
13
+ Consumers: `/multi-agent:design-check` (the runner), Phase 4 review when a UI
14
+ diff is under review, and `features/visual-evidence.md` when a capture has to
15
+ prove a fix. Gate: `smoke-design-conformance.sh`.
16
+
17
+ ## 1. Enumerate first, then fill every cell
18
+
19
+ Do **not** walk by finding, and do not walk by headline. Build the inventory
20
+ once, before any comparison:
21
+
22
+ 1. Enumerate every visible element of the design frame - every text, icon,
23
+ image, field, **button**, container. An element nested inside a button, field
24
+ or card is **its own row**, marked as nested (`> in <Button>`), never folded
25
+ into its parent.
26
+ 2. For each row, pre-fill the design side of every rule that applies and leave
27
+ the implementation side blank.
28
+ 3. Fill every blank. **A blank cell is a fail-to-verify, not a pass**, and it is
29
+ reported as one.
30
+
31
+ The inventory is the coverage guarantee; the rule groups in section 5 are the
32
+ per-cell recipe. A truncated inventory makes the denominator wrong, so it is
33
+ reported as incomplete rather than as a clean percentage of a partial set.
34
+
35
+ ## 2. Measure, never read the token
36
+
37
+ Every `[MEASURE]` item compares the design value against **measured pixels** in
38
+ the render, or against the implementation's resolved value in code. Never
39
+ against:
40
+
41
+ - the design tool's node box, which includes invisible padding, or
42
+ - the implementation's own layout token (a spacing or size constant, a
43
+ `.frame(height:)`, a Compose `Modifier.height`).
44
+
45
+ The token is the thing under test. Measuring it against itself is how a 48pt
46
+ field ships against a 56pt design with every check green. Button and field
47
+ **height** are the two a token lies about most often, because internal padding
48
+ and safe-area insets are not in it.
49
+
50
+ ## 3. Adaptive per-component convergence
51
+
52
+ One pass over a component catches roughly one defect on it. So each component is
53
+ re-checked until **two consecutive attempts find nothing new on it**.
54
+
55
+ - Soft cap: reaching attempt 8 without two consecutive clean attempts stops that
56
+ component with a `did-not-converge` warning. There is never a ninth attempt.
57
+ - A component with a font, a margin and a colour defect needs about three
58
+ finding attempts plus two clean ones. A correct component needs the two clean
59
+ attempts and nothing more.
60
+ - The screen is done when **every** inventoried component has reached two
61
+ consecutive clean attempts or its cap with a warning. Not when the walk
62
+ reached the bottom.
63
+
64
+ On a fixing run, each attempt that found something is fixed, re-captured, and
65
+ its changes appended to one accumulating record, so the final artefact shows
66
+ what each attempt changed rather than one undifferentiated diff.
67
+
68
+ ## 4. Scope tags - how an item is verified
69
+
70
+ | Tag | Meaning |
71
+ |---|---|
72
+ | `[MEASURE]` | Verified in the primary capture: design value against measured pixels or resolved code value (section 2) |
73
+ | `[CAPTURE+]` | Needs an extra capture pass - a second appearance, a mirrored layout, another width, another state. Skipping one is allowed only if the skip is logged |
74
+ | `[A11Y]` | Accessibility lane: the platform accessibility tree (labels, element frames) plus contrast math |
75
+ | `[DYNAMIC]` | Behavioural or temporal; a static capture cannot show it. Verify by driving the device, or report `not verified (dynamic)`. **Never silently pass a `[DYNAMIC]` item** |
76
+
77
+ ## 5. The catalog
78
+
79
+ Run every applicable item for **every** component, then every gap through
80
+ group 2, then the whole-screen passes (17, 21, 22, 24, 25). A check that was not
81
+ run is a fail-to-verify.
82
+
83
+ **1. Layout** `[MEASURE]` - x relative to container (never absolute y: the status
84
+ bar offsets it), y relative to an in-content span, L/R/T/B margin to parent,
85
+ centring, baseline alignment across a row, shared leading edge for siblings,
86
+ label-left/value-right rows aligned at both ends, safe-area and system-bar
87
+ clearance.
88
+
89
+ > **Horizontal-inset lane** - the most-skipped one. For every element record
90
+ > **both** sides of the leading inset: the design value and the measured
91
+ > implementation value. One side filled is a fail-to-verify. Then two cheap
92
+ > catches that need no per-element math: (a) **leading rail** - every loose,
93
+ > non-carded text in a vertical group must share one leading x; if the
94
+ > implementation puts a subtitle at the card's outer edge while the design insets
95
+ > it, the rail is broken; (b) **sibling consistency** - when sibling screens
96
+ > exist, diff the same element's inset across them; the outlier is usually the
97
+ > bug. A different text-wrap point is a symptom of a different margin, not only
98
+ > of a different font.
99
+
100
+ **2. Spacing** `[MEASURE]` - outer margins, inner padding, screen edge margin
101
+ (L=R symmetric), text-to-icon and icon-to-button and card-to-card and
102
+ section-to-section gaps, list/grid/row spacing, the vertical gap **between**
103
+ adjacent components (`next.y - (prev.y + prev.h)` against the stack spacing), and
104
+ every gap resolving to a spacing token - an off-scale value is a deviation.
105
+
106
+ **3. Size** `[MEASURE]` - width and height against the design (measured), min and
107
+ max honoured, `[CAPTURE+]` responsive widths (group 17), `[A11Y]` interactive tap
108
+ target at least 44x44pt on iOS / 48x48dp on Android, read from the accessibility
109
+ tree rather than assumed.
110
+
111
+ **4. Typography** `[MEASURE]` - family, size, weight, style, letter spacing, line
112
+ height, paragraph spacing, alignment, text transform, text decoration, foreground
113
+ colour, and truncation: ellipsis position, wrap and line limit matching the
114
+ design.
115
+
116
+ **5. Colours** `[MEASURE]` - background colour and opacity, gradient direction and
117
+ stops, blur effects, foreground and secondary text, disabled and placeholder,
118
+ border and divider and icon tint, brand accent and the status set
119
+ (success/error/warning/info), field prefix or affix colour, and no hardcoded
120
+ value: every colour resolves to a design token.
121
+
122
+ **6. Border** `[MEASURE]` - width, corner radius (scalar and per-corner), style
123
+ (solid vs dashed) and stroke alignment, colour and opacity.
124
+
125
+ **7. Shadow / elevation** `[MEASURE]` - present or not, colour and opacity, blur
126
+ radius, spread, x/y offset, and parity between the iOS shadow and the Android
127
+ elevation for the same surface.
128
+
129
+ **8. Shape** `[MEASURE]` - the drawn shape matches: radius about half the height
130
+ means a pill; radius about half the smaller side with a square box means a
131
+ circle.
132
+
133
+ **9. Icons** `[MEASURE]` - correct icon and variant, **size measured from the
134
+ rendered bounding box in both images**, never the node box or a size token,
135
+ weight (line vs filled), colour and tint, padding and alignment and icon-to-text
136
+ spacing, rotation and mirroring, rendering mode (a multicolour icon rendered as a
137
+ single-tint template is a defect), and vector crispness.
138
+
139
+ **10. Images** `[MEASURE]` - width and height, aspect ratio preserved, crop mode
140
+ (fill vs fit), no distortion, no unintended blur and sufficient resolution,
141
+ `[CAPTURE+]` placeholder and loading state.
142
+
143
+ **11. Buttons** `[MEASURE]` - width and **measured height** (from the rendered
144
+ fill rect, never a frame token), L/R/T/B spacing with L=R symmetry, the inner
145
+ label's font/weight/colour/alignment **as its own row**, the inner icon
146
+ (presence, size, colour, side, spacing), centring, fill and border and text
147
+ colour for the current state, `[CAPTURE+]` the state set (group 21).
148
+
149
+ > A button instance matches no text, icon or field kind, so an inventory built by
150
+ > kind drops it silently - and with it the button height and its nested label.
151
+ > The button is its own row and its children are exploded into their own rows.
152
+
153
+ **12. Input fields** `[MEASURE]` - **measured height** (the rendered field box,
154
+ never a frame or size token, because internal padding is not in the token),
155
+ width, border, radius, placeholder text and its colour and alignment, background
156
+ shade (read-only vs editable), prefix or affix and its colour, cursor and
157
+ selection colour, character counter behaviour, `[CAPTURE+]` focus/error/disabled/
158
+ filled states with the error message's colour, position and icon, `[DYNAMIC]`
159
+ keyboard type.
160
+
161
+ **13. Card** `[MEASURE]` - padding, radius, background, border, shadow, and
162
+ content grouping: **sample the pixel behind every text block** - is it inside a
163
+ card or bare on the page background? A design block in a card that the
164
+ implementation renders bare (or the reverse) is a wrong-container finding
165
+ (group 27) and is invisible to the text checks alone. Diff the design's container
166
+ list one-to-one against the implementation's card wrappers; the implementation
167
+ side is code-verifiable (inside a card helper vs placed bare in a stack with
168
+ padding). Never infer this from a glance.
169
+
170
+ **14. Lists** `[MEASURE]` - item height and internal padding, divider colour and
171
+ thickness and insets, inter-item spacing, section header grouping and order,
172
+ `[DYNAMIC]` scroll behaviour and lazy loading.
173
+
174
+ **15. Navigation / header** `[MEASURE]` - bar height, title text and alignment,
175
+ back affordance present and correct and mirrored under RTL, right-side actions
176
+ present and ordered, bottom navigation items with icons, labels and selected
177
+ state, `[DYNAMIC]` transition style.
178
+
179
+ **16. Scroll** - `[DYNAMIC]` smooth scroll, bounce, overscroll, sticky header,
180
+ scroll indicator; `[MEASURE]` the pinned header or footer position, which is the
181
+ static part a capture can check.
182
+
183
+ **17. Responsive** `[CAPTURE+]` - small, medium and large phone widths, tablet,
184
+ landscape. No overflow, clipping or distortion at any of them. Re-capture per
185
+ width rather than reasoning about it.
186
+
187
+ **18. Alignment** `[MEASURE]` - horizontal, vertical, and baseline across a row.
188
+
189
+ **19. Animation and motion** `[DYNAMIC]` - duration, curve, delay,
190
+ fade/scale/slide/rotation, loading and skeleton animation, haptics. Verify by
191
+ recording the device or report `not verified (dynamic)`.
192
+
193
+ **20. Visibility** `[CAPTURE+]` - hidden vs visible renders correctly; collapse
194
+ and expand states captured separately.
195
+
196
+ **21. States** `[CAPTURE+]` per interactive component - default, pressed, focus,
197
+ selected, active, disabled (colour, opacity, and genuinely not tappable),
198
+ loading, empty, error, success.
199
+
200
+ > **State fidelity gate**: the captured implementation must be in the exact state
201
+ > the design frame depicts - the same toggle position, the same filled or empty
202
+ > content, the same validation result. A capture in the wrong state is redone,
203
+ > not compared.
204
+
205
+ **22. Accessibility** `[A11Y]` - contrast at least 4.5:1 computed from sampled
206
+ colours, tap target at least 44x44pt / 48x48dp from the accessibility tree,
207
+ screen-reader label present and meaningful plus a meaningful hint, correct
208
+ traits, an accessibility identifier on every interactive element, and a logical
209
+ focus order. Whether to also audit scaled text sizes depends on the project: a
210
+ design system with fixed, non-scaling typography makes a forced-scale render
211
+ meaningless noise, so state which of the two the project is instead of assuming.
212
+
213
+ **23. Platform** - iOS: safe area, notch and Dynamic Island clearance, home
214
+ indicator, native navigation behaviour. Android: Material compliance, status and
215
+ navigation bar treatment, ripple.
216
+
217
+ **24. Dark appearance** `[CAPTURE+]` - capture a second image in the dark
218
+ appearance and re-run groups 3 to 9 against the dark design frame. Background,
219
+ text, icon, border, shadow, divider and gradient all adapt; no colour stuck at
220
+ its light value; contrast preserved.
221
+
222
+ **25. RTL and localization** - `[CAPTURE+]` a mirrored-locale capture: layout
223
+ mirrored, leading and trailing swapped, directional icons flipped; `[MEASURE]` no
224
+ hardcoded strings, every user-visible string from a localization key; a raw or
225
+ undefined key rendered on screen (the design shows copy, the implementation shows
226
+ `Screen.SomeKey`) reported in its own localization section; `[CAPTURE+]` a
227
+ long-language pass with no clipping. Compare the string's *presence and shape*,
228
+ not a translation of the same phrase against itself.
229
+
230
+ **26. Content and data formatting** `[MEASURE]` - number grouping, currency symbol
231
+ and position, date format, pluralization, and a long dynamic value that does not
232
+ overflow. The value itself (a name, a number, a masked field) is not under test.
233
+
234
+ **27. Component presence and order - hard gate** - the group the whole walk
235
+ exists for.
236
+
237
+ > Mechanical, not a glance. Number every element of the design list - text **and**
238
+ > every icon, avatar, badge, **and every button** - and for each write its
239
+ > implementation counterpart or `MISSING`. Report the count explicitly: "N design
240
+ > components -> N implementation counterparts". "Complete" may not be concluded
241
+ > without that list. A button and the label nested inside it are two presence
242
+ > checks, not one.
243
+ >
244
+ > - **Icons are the most-missed presence class.** For every icon in the design
245
+ > list, locate it in the render by measuring its coloured bounding box. A design
246
+ > icon with roughly no matching pixels is a missing-element finding.
247
+ > - **Recurse into nested blocks.** The gate applies to rows inside cards - masked
248
+ > rows, list rows, key/value rows - not only to top-level sections. "That card
249
+ > looks right" is not a pass.
250
+ > - **Bidirectional, and code-side too.** Diff design-to-implementation
251
+ > (`MISSING`) *and* implementation-to-design (`EXTRA`). The most-missed extra is
252
+ > a neutral decoration the implementation adds - a trailing chevron, an
253
+ > underline, a strikethrough - which a colour-based, one-directional comparison
254
+ > cannot see. Diff the implementation's explicit decoration modifiers against
255
+ > the design's own decoration nodes, independently of the string.
256
+ > - **A localization gap does not short-circuit this gate.** Raw keys are one
257
+ > finding; presence, icon, decoration and structure checks still run, because
258
+ > they read code and spec rather than the rendered string.
259
+
260
+ Findings: `MISSING` · `EXTRA` · `MISSING-BETWEEN` (dropped from the middle of the
261
+ sequence) · `REORDERED` · `WRONG-STATE` · `WRONG-CONTAINER` (present but grouped
262
+ differently, loose vs carded).
263
+
264
+ **28. Keyboard and input behaviour** `[DYNAMIC]` - keyboard avoidance with the
265
+ active field not covered, dismiss behaviour, and return/next moving to the
266
+ correct field.
267
+
268
+ **29. Analytics** - out of visual scope; checked only when the spec requires it.
269
+
270
+ **30. Cross-check matrix** - per component confirm x, y, w, h, the four margins,
271
+ the four paddings, font family/size/weight, line height, letter spacing, text
272
+ colour, background colour, border width/radius/colour, shadow, opacity, icon
273
+ size/colour/position, image size/crop, divider, alignment, responsive, state,
274
+ animation, safe area, and overflow. **Mark only deviations** on the image;
275
+ matches belong in the report text.
276
+
277
+ ## 6. Output
278
+
279
+ Mark only problems on the image, with thin lines. Everything else goes in the
280
+ report, whose section keys are stable and whose prose follows `outputLanguage`:
281
+
282
+ | Key | Content |
283
+ |---|---|
284
+ | `deviations` | Numbered, each one a design value against the measured or coded implementation value |
285
+ | `localization` | Raw keys, missing keys, hardcoded strings |
286
+ | `matches` | What was checked and agreed |
287
+ | `ignored` | What was deliberately out of scope, with the reason |
288
+ | `not_verified` | **Every** `[DYNAMIC]` and `[CAPTURE+]` item that was not run, so the coverage gap is explicit rather than absent |
289
+
290
+ Each finding is `[what] + [why] + [how]` with `file:line` wherever the
291
+ implementation side is code-resolvable. A finding without a file reference is
292
+ still a finding; a finding without a "how" is a complaint.
293
+
294
+ ## 7. What a token catalog is, and why none ships here
295
+
296
+ Comparing typography and colour exactly needs the project's own catalogs - the
297
+ resolved name, size, weight and line height of each typography style, and the
298
+ resolved value of each colour token. Those are **project assets**, not pipeline
299
+ content: they carry brand colours, licensed font names and product vocabulary.
300
+
301
+ So this contract defines the *interface*, never the data. The runner resolves
302
+ catalogs in this order and states which one it used:
303
+
304
+ 1. A path in preferences (`designCheck.tokenCatalogs`), when the project exports
305
+ them.
306
+ 2. Generated from the repo's own token definitions at run time.
307
+ 3. Neither available -> typography and colour items degrade from `[MEASURE]` to
308
+ sampled-pixel comparison, and the report says so. They are not silently
309
+ passed.
310
+
311
+ A vendored catalog inside the pipeline would be one project's design system
312
+ shipped to every other project: wrong for all of them, and stale for its owner.
@@ -0,0 +1,172 @@
1
+ # Visual evidence - before/after screenshots and the UI flow video
2
+
3
+ A UI fix that reads as three changed files in a diff is not reviewable. The
4
+ reviewer cannot see what was wrong, and the tester cannot see what to look for.
5
+ This contract makes the pipeline carry the picture: the state the reporter saw,
6
+ the state the fix produces, and where possible a recording of the flow running.
7
+
8
+ The artefacts live as **Jira attachments** and are referenced from two places -
9
+ the Phase 7 Jira comment, where the picture belongs next to the work summary and
10
+ the test scenarios, and the PR body, which tells the reviewer they exist.
11
+
12
+ Consumers: Phase 3 (capture), Phase 5 (video, preferred host), Phase 6 (blocker),
13
+ `channels/jira.md` and `channels/pr.md` (render). Gate: `smoke-visual-evidence.sh`.
14
+
15
+ ## 1. When it is required
16
+
17
+ Decided mechanically from `taskType` plus the changed-file list, never from the
18
+ model's reading of the task. A file counts as UI by stack:
19
+
20
+ | Stack | UI file test |
21
+ |---|---|
22
+ | iOS | `.swift` declaring `: View`, `: ViewController`, or under a `Views/` path |
23
+ | Android | `.kt` with `@Composable`, or declaring `: Activity` / `: Fragment` |
24
+ | Web | `.tsx` / `.jsx` / `.vue` / `.svelte` |
25
+
26
+ | Case | Before | After | Video |
27
+ |---|---|---|---|
28
+ | `taskType` is `bugfix` AND a UI file changed | required | required | when a tier allows |
29
+ | Design work: `taskType` is `component`, or Figma was referenced, or a UI file was **added** | not applicable | required | when a tier allows |
30
+ | Anything else | - | - | - |
31
+
32
+ `state.visualEvidence.required` records the verdict and which rule produced it.
33
+ Nothing about this section is optional-by-omission: when it is required and an
34
+ artefact is absent, the absence is written down with its reason (section 5).
35
+
36
+ ## 2. Before - the reporter's screenshot, or nothing
37
+
38
+ **The pipeline does not build the old state to photograph it.** Reproducing a
39
+ pre-fix screen costs a second build and a second launch on every UI bug, to
40
+ recreate evidence the reporter has usually already attached.
41
+
42
+ So: Phase 0 already fetches the issue. Any image attachment on it (`.png`,
43
+ `.jpg`, `.jpeg`, `.gif`, `.heic`) is downloaded to `$WORKTREE/.pipeline/evidence/`
44
+ and recorded as `state.visualEvidence.before[]`. More than one is kept - a bug
45
+ reported on two platforms has two.
46
+
47
+ No image on the ticket means no "before". Record the gap
48
+ (`before_missing: "ticket carries no image attachment"`) and continue. The
49
+ section still renders, saying so.
50
+
51
+ ## 3. After - Phase 3, not Phase 5
52
+
53
+ Phase 5 is the natural home: the simulator is already up. It is also **dropped by
54
+ every `autopilot` and `--local` entry** (`phase-5-test.md` TLDR), so a capture
55
+ that lives only there produces nothing for unattended runs - which are exactly
56
+ the runs where nobody watched the screen.
57
+
58
+ The capture therefore happens in **Phase 3, after the build+test gate passes**,
59
+ which is in every mode's phase set. Phase 5 may add richer evidence on top; it is
60
+ never the only source.
61
+
62
+ ```bash
63
+ bash $HOME/.claude/scripts/capture-evidence.sh after \
64
+ --task "$TASK_ID" --platform "$PLATFORM" --label "<screen-slug>"
65
+ ```
66
+
67
+ The script boots the device if needed, launches the built app, cleans the status
68
+ bar (`ios_status_bar preset:clean` / the Android equivalent) so the frame carries
69
+ no clock, carrier or battery noise, captures at device resolution, downscales to
70
+ at most 1242px wide, and writes
71
+ `$WORKTREE/.pipeline/evidence/<TASK_ID>-<label>-after.png`.
72
+
73
+ A clean status bar is not cosmetic: without it two captures of the same screen
74
+ differ by the clock, which makes every "after" look like a change.
75
+
76
+ ## 4. Video - three tiers, resolved once per repo
77
+
78
+ The flow source, in order: a UI test flow written on the ticket wins; otherwise
79
+ the Phase 5 test scenarios are the flow. Human-written beats generated, the same
80
+ rule the "before" follows.
81
+
82
+ The tier is probed once and stored in `state.visualEvidence.videoTier`:
83
+
84
+ | Tier | Condition | Mechanism |
85
+ |---|---|---|
86
+ | 1 | The repo has a UI test target (XCUITest / Espresso) **and** it can be seeded with test data | Run that test, record the screen for its duration. Most faithful, and it is a test that already runs in CI |
87
+ | 2 | A runnable build exists | Drive the flow with `mcp__multi-agent-toolkit__agent_run_steps` and record with `ios_record_video` / `android_record_screen`. **No test target needed** |
88
+ | 3 | No runnable build (library / SPM package / backend) | Skip, with the reason recorded |
89
+
90
+ Tier 2 is why "can every repo do this" answers yes in practice: any app that can
91
+ be launched can be driven. A repo without a UI test target loses fidelity, not
92
+ the recording.
93
+
94
+ Preferred host is **Phase 5** - the device is already up and a human is present
95
+ to confirm the flow is the right one. In a mode without Phase 5, tier 2 runs at
96
+ the end of Phase 3 instead. Tier 1 always runs where the test runs.
97
+
98
+ Duration is capped by `visualEvidence.maxVideoSeconds` (default 60), read via
99
+ `capture-evidence.sh limits` so the number lives in one place rather than in two
100
+ documents. A flow that needs longer is not a review artefact, it is a debugging
101
+ session.
102
+
103
+ `visualEvidence.enabled` turns the whole feature off - capture, upload, both
104
+ render sections and the Phase 6 blocker with it.
105
+
106
+ ## 5. Size, and what happens when it does not fit
107
+
108
+ Jira's attachment ceiling is an instance setting, so it is a preference:
109
+ `visualEvidence.maxAttachmentMb` (default `10`).
110
+
111
+ Order of attempts, each one recorded:
112
+
113
+ 1. Upload as captured.
114
+ 2. On `413` or a local size overrun: re-encode. Video drops to 720p and a lower
115
+ bitrate; PNG converts to JPEG at quality 80. **Reducing quality is allowed;
116
+ dropping the artefact is not.**
117
+ 3. Still over: replace the video with a four-frame contact sheet (start, two
118
+ midpoints, end) as a single PNG, and say in the caption that the recording
119
+ exceeded the limit.
120
+
121
+ Never silently attach nothing.
122
+
123
+ ## 6. Rendering
124
+
125
+ ### Jira comment (`channels/jira.md`)
126
+
127
+ The comment's section order is fixed and nothing may be inserted between the
128
+ three sections. The evidence goes **inside** two of them:
129
+
130
+ - `summary` - after the summary sentences, the before/after pair as a Jira wiki
131
+ thumbnail row: `!<TASK>-<label>-before.png|thumbnail! !<TASK>-<label>-after.png|thumbnail!`
132
+ preceded by one line naming which is which in `outputLanguage`
133
+ (`Düzeltme öncesi / Düzeltme sonrası`).
134
+ - `test_scenarios` - the flow video attached under the scenario it demonstrates:
135
+ `!<TASK>-flow.mp4!`, one line stating the tier used.
136
+
137
+ Missing artefacts print their reason on the same line, never an empty frame.
138
+
139
+ ### PR body (`channels/pr.md`)
140
+
141
+ A `visuals` section whose job is to tell the reviewer the evidence exists and
142
+ where it lives - the images themselves stay on the ticket:
143
+
144
+ ```markdown
145
+ ## Görsel Kanıt
146
+
147
+ - Düzeltme öncesi: <before-filename> (PROJ-123 ekinde)
148
+ - Düzeltme sonrası: <after-filename> (PROJ-123 ekinde)
149
+ - Akış videosu: <flow-filename>, tier <N> (PROJ-123 ekinde)
150
+ ```
151
+
152
+ ## 7. Blocker
153
+
154
+ When section 1 says required and `state.visualEvidence` carries neither an
155
+ artefact nor a recorded reason for its absence, **Phase 6 Step 3 blocks** - the
156
+ same shape as the `risk` section blocker. A recorded reason is enough to pass:
157
+ the gate is against silence, not against an honest "no image on the ticket".
158
+
159
+ ## 8. State
160
+
161
+ ```json
162
+ "visualEvidence": {
163
+ "required": true,
164
+ "requiredBy": "bugfix + ui-file-changed",
165
+ "platform": "ios",
166
+ "before": [{"file": "...", "source": "ticket", "jiraFilename": "...", "url": "..."}],
167
+ "after": [{"file": "...", "capturedAt": "phase-3", "jiraFilename": "...", "url": "..."}],
168
+ "videoTier": 2,
169
+ "video": {"file": "...", "seconds": 41, "jiraFilename": "...", "url": "..."},
170
+ "gaps": [{"what": "before", "reason": "ticket carries no image attachment"}]
171
+ }
172
+ ```