@terreno/agent-plugin 0.0.0-stage → 57.21.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 (98) hide show
  1. package/.claude-plugin/plugin.json +32 -0
  2. package/CHANGELOG.md +2124 -0
  3. package/LICENSE +21 -0
  4. package/README.md +57 -2
  5. package/agents/pre-commit.md +65 -0
  6. package/agents/ui-verifier.md +93 -0
  7. package/package.json +32 -4
  8. package/references/async-review-bots.md +76 -0
  9. package/references/documentation-contract.md +61 -0
  10. package/references/execution-state.schema.json +77 -0
  11. package/references/github-attention-contract.md +165 -0
  12. package/references/independent-review.md +31 -0
  13. package/references/lifecycle-contract.md +211 -0
  14. package/references/loop-engineering.md +125 -0
  15. package/references/pick-roast-loop.md +112 -0
  16. package/references/pr-deployments.md +69 -0
  17. package/references/product-ci.md +164 -0
  18. package/references/push-ownership.md +91 -0
  19. package/references/stage-result.schema.json +82 -0
  20. package/references/subagent-briefing.md +43 -0
  21. package/references/testing-plan.md +163 -0
  22. package/references/user-guide.md +171 -0
  23. package/skills/1-grow/SKILL.md +145 -0
  24. package/skills/1-grow/references/grilling.md +243 -0
  25. package/skills/2-pick/SKILL.md +170 -0
  26. package/skills/2-pick/references/mocking.md +34 -0
  27. package/skills/2-pick/references/testing.md +43 -0
  28. package/skills/3-roast/SKILL.md +168 -0
  29. package/skills/4-brew/SKILL.md +201 -0
  30. package/skills/5-taste/SKILL.md +311 -0
  31. package/skills/ai-prompt-governance/SKILL.md +100 -0
  32. package/skills/attach-pr-media/SKILL.md +89 -0
  33. package/skills/attach-pr-media/scripts/upload-pr-media.sh +151 -0
  34. package/skills/backend-test-env/SKILL.md +64 -0
  35. package/skills/backfill-regression/SKILL.md +101 -0
  36. package/skills/backfill-user-guide/SKILL.md +96 -0
  37. package/skills/bootstrap-mcp-server/SKILL.md +244 -0
  38. package/skills/building-admin-interfaces/SKILL.md +83 -0
  39. package/skills/building-admin-interfaces/agents/openai.yaml +4 -0
  40. package/skills/building-admin-interfaces/references/nav-and-shell.md +42 -0
  41. package/skills/building-admin-interfaces/references/patterns-and-tools.md +73 -0
  42. package/skills/building-admin-interfaces/references/screens-and-routes.md +50 -0
  43. package/skills/building-terreno-apps/SKILL.md +280 -0
  44. package/skills/building-terreno-apps/agents/openai.yaml +4 -0
  45. package/skills/building-terreno-apps/references/navigation.md +102 -0
  46. package/skills/building-terreno-apps/references/route-structure.md +91 -0
  47. package/skills/building-terreno-apps/references/screen-patterns.md +191 -0
  48. package/skills/debug-syncdb-with-mcp/SKILL.md +75 -0
  49. package/skills/deploy-gcp/SKILL.md +73 -0
  50. package/skills/deploy-gcp/references/troubleshooting.md +29 -0
  51. package/skills/generate-release-announcements/SKILL.md +145 -0
  52. package/skills/generate-sdk/SKILL.md +156 -0
  53. package/skills/mongoose-schema-safety/SKILL.md +164 -0
  54. package/skills/pick-roast-loop/SKILL.md +152 -0
  55. package/skills/planning-loop/SKILL.md +161 -0
  56. package/skills/prd/SKILL.md +138 -0
  57. package/skills/set-up-social-login/SKILL.md +76 -0
  58. package/skills/store-assets/SKILL.md +221 -0
  59. package/skills/store-assets/examples/screenshots.yaml +31 -0
  60. package/skills/store-assets/examples/storeAssets.config.json +44 -0
  61. package/skills/store-assets/scripts/banner.ts +165 -0
  62. package/skills/store-assets/scripts/config.ts +255 -0
  63. package/skills/store-assets/scripts/devClient.ts +635 -0
  64. package/skills/store-assets/scripts/devices.ts +286 -0
  65. package/skills/store-assets/scripts/icon.ts +81 -0
  66. package/skills/store-assets/scripts/maestro/goHome.yaml +21 -0
  67. package/skills/store-assets/scripts/maestro/login.yaml +27 -0
  68. package/skills/store-assets/scripts/maestro/openApp.yaml +26 -0
  69. package/skills/store-assets/scripts/screenshots.ts +848 -0
  70. package/skills/taste-sweep/SKILL.md +172 -0
  71. package/skills/terreno-backend-api/SKILL.md +355 -0
  72. package/skills/terreno-backend-api/agents/openai.yaml +4 -0
  73. package/skills/terreno-backend-api/references/custom-routes.md +132 -0
  74. package/skills/terreno-backend-api/references/model-router.md +116 -0
  75. package/skills/terreno-backend-api/references/server-setup.md +116 -0
  76. package/skills/terreno-backend-tests/SKILL.md +176 -0
  77. package/skills/terreno-data-fetching/SKILL.md +276 -0
  78. package/skills/terreno-data-fetching/agents/openai.yaml +4 -0
  79. package/skills/terreno-data-fetching/references/auth-and-tokens.md +93 -0
  80. package/skills/terreno-data-fetching/references/realtime.md +45 -0
  81. package/skills/terreno-data-fetching/references/sdk-customization.md +84 -0
  82. package/skills/terreno-frontend-tests/SKILL.md +190 -0
  83. package/skills/terreno-mobile-tests/SKILL.md +127 -0
  84. package/skills/terreno-test-setup/SKILL.md +81 -0
  85. package/skills/terreno-test-setup/references/backend.md +131 -0
  86. package/skills/terreno-test-setup/references/frontend.md +169 -0
  87. package/skills/terreno-test-setup/references/mobile.md +134 -0
  88. package/skills/terreno-ui/SKILL.md +252 -0
  89. package/skills/terreno-ui/agents/openai.yaml +4 -0
  90. package/skills/terreno-ui/references/data-display.md +85 -0
  91. package/skills/terreno-ui/references/feedback.md +122 -0
  92. package/skills/terreno-ui/references/forms.md +119 -0
  93. package/skills/terreno-ui/references/layout.md +124 -0
  94. package/skills/terreno-ui/references/theming.md +87 -0
  95. package/skills/update-docs/SKILL.md +82 -0
  96. package/skills/upgrading-terreno/SKILL.md +86 -0
  97. package/skills/upgrading-terreno/references/ordering.md +40 -0
  98. package/skills/verify-ui-changes/SKILL.md +605 -0
@@ -0,0 +1,40 @@
1
+ # Upgrade ordering
2
+
3
+ Why the `upgrading-terreno` skill bumps packages in a fixed order. Packages below are every published job in `.github/workflows/publish-on-tag.yml`.
4
+
5
+ ## Package classes
6
+
7
+ | Class | Packages (`publish-on-tag.yml` job) |
8
+ | --- | --- |
9
+ | Backend | `api`, `test`, `admin-backend`, `blocks`, `ai`, `api-health`, `comms`, `feature-flags`, `mcp` |
10
+ | Frontend | `ui`, `rtk`, `syncdb`, `admin-frontend`, `admin-spa` |
11
+
12
+ `blocks` holds contracts that both `ai` and `ui` depend on, so it moves with the backend step.
13
+
14
+ `demo` and the example apps are not published.
15
+
16
+ ## Backend before frontend
17
+
18
+ The OpenAPI document from the running backend is the contract `bun run sdk` consumes. Regenerating the client against an **un-upgraded** backend produces hooks for the old surface: new routes and fields are missing, and consumer screens fail at **compile** (`Property X does not exist on type …` or a missing generated hook).
19
+
20
+ Bump and compile backend packages first, then start that backend, then regenerate.
21
+
22
+ ## Typed client only after the new backend is up
23
+
24
+ `example-frontend` codegen reads `http://localhost:4000/openapi.json` (or `OPENAPI_URL`). If the process is still the old binary, the file `store/openApiSdk.ts` is silently stale. Symptom: runtime 404 on new paths or TypeScript errors after you edit screens to call new endpoints that the SDK never emitted.
25
+
26
+ ## Expo before `@terreno/ui`
27
+
28
+ `ui/package.json` `peerDependencies` pin `react-native` to the Expo line (for example `~0.86.0` on Terreno 57). Installing `@terreno/ui@57` while the app is still on Expo 54 / RN 0.81 fails peer resolution or breaks at Metro/native build. Symptom: `ERESOLVE` / unmet peer, or a native module ABI mismatch after `expo start`.
29
+
30
+ Run `upgrading-expo` (skill name) before bumping Terreno frontend packages when the Terreno major tracks a new Expo SDK.
31
+
32
+ ## Violation symptoms
33
+
34
+ | If you… | What breaks |
35
+ | --- | --- |
36
+ | Bump `@terreno/ui` before Expo | Unmet `react-native` peers; native rebuild required anyway |
37
+ | Run `bun run sdk` before upgrading/starting the backend | Generated client missing new operations; compile errors in screens |
38
+ | Bump frontend `@terreno/*` before backend compile | Client types and server handlers disagree; 4xx/5xx on new fields |
39
+ | Mix `@terreno/api@57` with `@terreno/ui@0.31` | Unsupported; lockstep publish tags one version for every package |
40
+ | Continue after a failed `compile` | Half-upgraded lockfile; rollback is unclear |
@@ -0,0 +1,605 @@
1
+ ---
2
+ name: verify-ui-changes
3
+ description: >-
4
+ Mandatory when any feature touches the frontend. Bring up a stack you own, log in,
5
+ drive the change under the required checks (right page, not blank, no error overlay,
6
+ console clean, DOM assertion, interaction proof, screenshot), run the repository's
7
+ Maestro flows on the simulator for mobile-focused work (direct simulator commands when
8
+ the app has no Maestro), and post screenshots and
9
+ video to the PR or a secret gist in the report format. Lifecycle composition: Pick
10
+ for implementation runtime checks, Roast for independent proof, and Taste after a
11
+ UI-affecting review/CI fix.
12
+ claudecode:
13
+ model: haiku
14
+ ---
15
+
16
+ # Verify UI Changes
17
+
18
+ Use this skill automatically whenever a task changes UI behavior, UI layout, visual styling, component stories, navigation screens, user-visible copy, or any full-stack feature with a frontend surface.
19
+
20
+ If you made a user-facing change, you must run it, check that it actually works, and
21
+ show evidence. No evidence, no completion claim. A passing build, type check, or unit
22
+ test is necessary, not sufficient. **User-facing** is anything a user could notice:
23
+ components, screens, routes, navigation, styles, layout, responsive behavior, theming,
24
+ forms, validation, error / empty / loading states, copy, translations, API changes whose
25
+ output a client renders, emails, push, SMS, exported files. Pure refactors, internal
26
+ tooling, and backend-only changes with no rendered consumer are exempt, but say so:
27
+ "No user-facing change: <reason>".
28
+
29
+ ## Trigger Files
30
+
31
+ Load this skill before validating changes to:
32
+
33
+ - `*.tsx`
34
+ - `*.jsx`
35
+ - `*.html`
36
+ - `*.css`
37
+ - `*.scss`
38
+ - `*.less`
39
+ - `*.styl`
40
+ - `*.vue`
41
+ - Story/demo files that render UI states
42
+ - Theme, layout, navigation, or component configuration that changes rendered UI
43
+ - Full-stack PRs that include any of the above, even when backend files dominate the diff
44
+
45
+ ## The loop (every frontend-touching feature)
46
+
47
+ Do not open or update a PR for frontend work until this loop is complete.
48
+
49
+ 1. **State the flow in one sentence** before testing:
50
+ `<entry point> -> <user action or state> -> <expected visible result>`. For a general
51
+ smoke: app loads -> first meaningful screen renders -> primary controls respond
52
+ without errors.
53
+ 2. **Get the app running on a stack you own** (package-specific sections below). If the
54
+ backend or an app port is already taken by something you did not start, **stop and
55
+ say which ports are taken**. Do not test through them and do not kill them: on a
56
+ developer machine they are usually the simulators' Metros and a backend pointed at
57
+ staging, and the checks could change staging data. When the repository ships a
58
+ `verify-ui` stack script, use it; it refuses foreign ports for you.
59
+ 3. **Write adversarial instructions, not happy-path ones.** Bad: "check that the form
60
+ renders". Good: "submit empty, with an invalid email, with a wrong password, with
61
+ valid credentials; assert each error message, the redirect, and the console". Cover
62
+ the error, empty, disabled, and loading states the change touches. Drive the UI
63
+ through `testID`s; if an element has none, add one to the component first
64
+ (`{screen}-{element}-{qualifier}`), never a class or structural selector. As Roast's
65
+ independent verifier, do not add it: report the missing `testID` (see "Who edits").
66
+ 4. **Log in and exercise the changed feature** with the seeded accounts documented
67
+ below: navigate to the affected screen and run the flow end to end, not just confirm
68
+ the app or page loads. Visual or layout work gets a desktop and a phone viewport.
69
+ 5. **Run the required checks** on every app × viewport run:
70
+
71
+ | Check | Passing looks like |
72
+ |---|---|
73
+ | Right page | Final pathname is the expected one and is not the login page |
74
+ | Not blank | ≥ 40 characters of visible text, not an empty shell or a stuck spinner |
75
+ | No error overlay | No framework red box or compile-error text in the document |
76
+ | Console clean | Zero console or page errors; warnings are listed for you to explain |
77
+ | DOM assertion | ≥ 1 assertion on a visible element, text, or URL, all passed |
78
+ | Interaction proof | ≥ 1 tap / type / key press, followed by an assertion of the new state |
79
+ | Screenshot | ≥ 1 screenshot; take one of the failure state when a step fails |
80
+
81
+ For visual or layout work also look at the phone run for clipping, overlap,
82
+ unreadable text, bad wrapping, layout shift, missing assets, and z-index problems.
83
+ 6. **Look at every screenshot.** Check the changed state actually rendered: not a
84
+ spinner, not an empty state caused by seed data, not the login page. DOM assertions
85
+ are primary; screenshots support visual claims, they do not replace them. A console
86
+ error in the changed code path is a failure even when the shot looks right: explain
87
+ it or fix it (see "Baselining console warnings" for the demo app).
88
+ 7. **Mobile-focused change? Verify it on the simulator** (section below): the matching
89
+ Maestro flow when the app has Maestro configured, direct simulator commands when it
90
+ does not. Web alone is not enough.
91
+ 8. **If anything fails: fix it and re-verify immediately.** Do not stop to report a
92
+ half-working state. Retry a failing tool or UI action once before calling it a
93
+ blocker. Repeat until zero failures. As Roast's independent verifier, do not fix:
94
+ report FAIL with the evidence (see "Who edits").
95
+ 9. **Publish and report** in the report format below: to the PR `Verification` table
96
+ (see "GitHub Reviewer Evidence"), or to a secret gist when the repository's
97
+ `verify-ui` publish script exists or there is no PR yet. Give the user the link plus
98
+ the Result line.
99
+
100
+ ## Simulator leg (mobile-focused changes)
101
+
102
+ Required when the change is native screens, gestures, navigation stacks, keyboard
103
+ behavior, safe areas, push, camera / permissions, auth storage, `*.ios.tsx`,
104
+ `*.android.tsx`, `*.native.tsx`, `Platform.OS` branches, or the ticket says "mobile" or
105
+ "app".
106
+
107
+ **Maestro configured** (the app has a `<app>/maestro/` directory with flows): verify with
108
+ those **Maestro** flows on the app's dedicated simulator, not with an ad-hoc tap script.
109
+ Run the flow that matches the change. No matching flow? Add one next to the existing
110
+ flows, reusing their launch and login subflows, with `testID` selectors and
111
+ `extendedWaitUntil` instead of sleeps; it is part of the change and stays in the
112
+ repository. Maestro configured but the CLI or Java missing: install them with the
113
+ repository's documented command, or report UNVERIFIED.
114
+
115
+ **Maestro not configured** (no `<app>/maestro/`): do not set Maestro up as part of the
116
+ change. Drive the simulator with direct commands instead, from a temporary script kept
117
+ outside the tree:
118
+
119
+ | Step | iOS simulator | Android emulator |
120
+ |---|---|---|
121
+ | Boot and install | `xcrun simctl boot <device>`, `xcrun simctl install booted <app>` | `emulator -avd <avd>`, `adb install <apk>` |
122
+ | Open the screen | `xcrun simctl openurl booted <scheme>://<path>` | `adb shell am start -a android.intent.action.VIEW -d <scheme>://<path>` |
123
+ | Tap and type | `idb ui tap <x> <y>`, `idb ui text "<text>"` | `adb shell input tap <x> <y>`, `adb shell input text "<text>"` |
124
+ | Assert | `idb ui describe-all` (find the `testID` and expected text) | `adb exec-out uiautomator dump /dev/tty` (find the `resource-id` and expected text) |
125
+ | Evidence | `xcrun simctl io booted screenshot <file>`, `xcrun simctl io booted recordVideo <file>` | `adb exec-out screencap -p > <file>`, `adb shell screenrecord <file>` |
126
+
127
+ Take tap coordinates from the accessibility tree, not from guesses. Assert on the tree
128
+ after each interaction; a screenshot alone is not an assertion. The same required checks
129
+ and outcomes apply. Without `idb`, iOS can only open screens and capture evidence, so an
130
+ iOS flow that needs taps is UNVERIFIED (`idb` missing) unless you install it.
131
+
132
+ A missing or stale dev client
133
+ (`Cannot find native module`) is rebuilt with the repository's documented command, then
134
+ the flow reruns. The flow deep-links the dev client to Metro, so Metro and the backend
135
+ must be the stack you started: "started by you" means the process you recorded is still
136
+ alive and holds the port, not that a pid file exists. A stack that crashed and was
137
+ followed by a developer's Metro or a backend on staging is not yours. If the simulator
138
+ or dev client is unavailable, the report says **UNVERIFIED (simulator)**; a passing web
139
+ check never stands in for the mobile one. Keep the three outcomes apart: a flow that ran
140
+ and failed is FAIL, a flow that never ran (stack not yours, no simulator or dev client,
141
+ simulator would not boot, Maestro, Java, or `idb` missing, no report written) is UNVERIFIED
142
+ with the blocker, and neither is ever reported as the other. A repository runner should
143
+ encode that in its exit codes (for example 2 for failed, 3 for UNVERIFIED). Also check
144
+ what web cannot: the keyboard does not cover inputs, safe areas, and the tablet vs
145
+ phone layout fork.
146
+
147
+ ## Who edits
148
+
149
+ Only the implementer (Pick, or a Taste fix) changes the tree: adds `testID`s, adds
150
+ Maestro flows, and fixes failures. Roast's independent verifier (the `ui-verifier`
151
+ agent) runs the same loop report-only. A missing `testID` or Maestro flow, or a failing
152
+ check, is FAIL with the evidence; a check it could not run is UNVERIFIED with the
153
+ blocker. It never edits the tree to make a run pass.
154
+
155
+ ## Evidence rules
156
+
157
+ - Seed or QA data only. Never screenshot, log, or paste real user data or PHI into
158
+ evidence. Secret gists are unlisted, not private. If the only way to verify is against
159
+ real data, stop and ask.
160
+ - Store screenshots, recordings, and temporary test scripts outside the committed source
161
+ tree (`/opt/cursor/artifacts/` in Cursor Cloud, your scratchpad otherwise) unless the
162
+ project specifies an evidence directory or the user asks for committed artifacts.
163
+ Playwright scenarios and Maestro flows that document a feature may stay in the
164
+ repository.
165
+ - If credentials are missing, ask for them. Never guess.
166
+
167
+ ## When you can't verify
168
+
169
+ No browser tooling, app won't start, missing credentials, ports taken by a stack that is
170
+ not yours, needs a device: do not claim it works. Mark it **UNVERIFIED** with the exact
171
+ blocker and the commands or setup steps attempted, say what you did verify (build,
172
+ types, unit tests), give the reviewer the exact steps to verify themselves, and ask for
173
+ missing credentials rather than guessing. In the PR, that is one `⚠️ Not tested` row
174
+ naming the blocked behavior and the reviewer action, with decisive error detail in an
175
+ optional `<details>` block. Do not present compile-only or app-start-only checks as
176
+ complete verification.
177
+
178
+ ## Package-Specific Manual Verification
179
+
180
+ ### `@terreno/ui` component changes
181
+
182
+ Test UI package component changes only in the demo app.
183
+
184
+ 1. Start the demo web app from the repo root:
185
+
186
+ ```bash
187
+ bun run demo:start
188
+ ```
189
+
190
+ Or from `demo/`:
191
+
192
+ ```bash
193
+ bun run web
194
+ ```
195
+
196
+ The demo runs on `http://localhost:8085`.
197
+
198
+ 2. Open the developer-mode screen for the changed component:
199
+
200
+ ```text
201
+ http://localhost:8085/dev
202
+ ```
203
+
204
+ 3. Select the component and the most relevant story.
205
+ - Direct URLs use the component name from `demo/demoConfig.tsx` and a story query param.
206
+ - **The `?story=` value must exactly match a key of the `stories` object in
207
+ `demo/story-config/<Component>.config.tsx`, URL-encoded.** These keys are human-readable
208
+ display names, not slugs — e.g. `http://localhost:8085/dev/Button?story=Variants`, and they can
209
+ contain spaces, which must be URL-encoded (`?story=Inline%20Feedback%20Prompt`).
210
+ `demo/app/dev/[component].tsx` calls `router.replace("/dev")` only when `story` is absent or the
211
+ component isn't in `demoConfig.tsx`; a *valid component with an unknown story* renders a **blank
212
+ page** instead. Either way you get no error, so confirm the story actually rendered.
213
+ - Use `/dev/<Component>?story=<Story>` to see one raw story full-screen.
214
+ - Use `/demo/<Component>` when you need the **interactive prop controls**
215
+ (`demoOptions.controls` — selects, booleans). Those controls render *only* on the `/demo`
216
+ page, not on `/dev`. The `/demo` page also renders every story at the bottom, so it is the
217
+ fastest way to see all states plus drive props from one screen.
218
+
219
+ 4. Verify the changed state and at least one adjacent state that could regress.
220
+ - For visual primitives: compare default, disabled, loading, icon, and full-width states when relevant.
221
+ - For interactive components: click, type, open, close, hover, and keyboard navigate as appropriate.
222
+
223
+ 5. When shipping a **new** `@terreno/ui` component or changing public props, also verify the generated docs page:
224
+ - Run `cd ui && bun run types && bun run website:generate`
225
+ - Check the docs deploy preview (or `bun run website:build` locally) for the component page and embedded demo iframe.
226
+
227
+ When the change is `LineChart`, `BarChart`, `AreaChart`, `DonutChart`, `DashboardGrid`, or
228
+ chart layout/paint helpers:
229
+
230
+ 1. Run `bun run ui:charts:compare` (starts demo web on port 8085 unless it is already up).
231
+ 2. If any fixture fails, follow the `review-chart-visuals` skill: open
232
+ `demo/rendered-snapshots/<id>.png`, `demo/chart-visual-output/actual/<id>.png`, and
233
+ `demo/chart-visual-output/diff/<id>.png` and describe the visual delta. Do not treat
234
+ React test-renderer JSON snapshots as proof of chart paint.
235
+ Operator steps: [Compare rendered chart snapshots](../../../../docs/how-to/compare-chart-rendered-snapshots.md).
236
+
237
+ Do not launch the example app to validate isolated `@terreno/ui` component changes unless the change also affects an app-level integration.
238
+
239
+ #### Driving the `/demo` prop controls
240
+
241
+ The controls are `@terreno/ui` `Field`s, **not native HTML inputs**:
242
+
243
+ - A `type: "select"` control renders a custom dropdown — there is **no `<select>` element**, so
244
+ Playwright/CDP `selectOption`-style helpers do nothing. Click the field to open the option list,
245
+ then click the option row. After opening, the field's `<input>` swaps its value for a
246
+ `placeholder="Search..."`, which is a reliable signal that the list is open.
247
+ - A `type: "boolean"` control renders a toggle `<div>`; click it directly.
248
+
249
+ #### Objectively verifying icon fill, color, and icon family
250
+
251
+ Screenshots alone make solid-vs-outline and two similar greys hard to judge. Read computed styles
252
+ instead — both the Terreno `Icon` and `@expo/vector-icons` render **font glyphs**, so:
253
+
254
+ - **Which icon set is actually rendering** → computed `font-family` on the glyph element. This is
255
+ the definitive check when a component migrates icon libraries: Terreno's `Icon` falls back to
256
+ FontAwesome6 (`FontAwesome6Free-Solid` / `FontAwesome6Free-Regular`), whereas a direct
257
+ `import Feather from "@expo/vector-icons/Feather"` reports `feather` and
258
+ `import MaterialIcons from "@expo/vector-icons/MaterialIcons"` reports `material` (from
259
+ `createIconSet(glyphMap, 'Material Icons', ...)` — the computed value is normalised, so don't
260
+ expect the literal string `MaterialIcons`). Don't try to judge this from a screenshot — the
261
+ glyphs look similar at small sizes. This whole procedure only applies to **font-backed** icons: a
262
+ custom icon registered through `TerrenoProvider`'s icon registry takes precedence over the
263
+ FontAwesome glyph set (`ui/src/Icon.tsx`) and may render SVG or any component, so probe its own
264
+ markup rather than `font-family`.
265
+ - **Fill / `type` prop** (FontAwesome `Icon` only) → computed `font-family`:
266
+ `FontAwesome6Free-Solid` for `type="solid"` vs `FontAwesome6Free-Regular` for `type="regular"`.
267
+ Note that most `@expo/vector-icons` sets (Feather included) have **no solid variant**, so a
268
+ component using them must signal selection some other way — usually a filled surface behind
269
+ the glyph. When that is the design, verify the `backgroundColor` and `borderRadius` of the
270
+ wrapping `Pressable` rather than the glyph's fill (see "Verifying a filled-surface selection
271
+ cue" below).
272
+ - **Fill via a different icon *name*** (the strongest check, and the one to reach for with sets
273
+ that ship paired outline/filled glyphs such as MaterialIcons' `thumb-up-off-alt` vs
274
+ `thumb-up-alt`) → read the glyph element's **`textContent` codepoint**. `@expo/vector-icons`
275
+ renders the icon as a single character from the icon font, so `el.textContent.codePointAt(0)`
276
+ identifies exactly which icon name is rendering. Resolve the expected numbers from the shipped
277
+ glyphmap so you are asserting against ground truth rather than a guess:
278
+
279
+ ```bash
280
+ # glyphmaps live under node_modules/**/@expo/vector-icons/build/vendor/.../glyphmaps/<Set>.json
281
+ node -e 'const m=require("<path>/MaterialIcons.json");
282
+ for (const n of ["thumb-up-off-alt","thumb-up-alt"]) console.log(n, m[n], "U+"+m[n].toString(16));'
283
+ ```
284
+
285
+ This matters because a broken name mapping still recolours correctly, so a colour-only check
286
+ passes on a component whose fill never changes. Colour is not proof of fill; the codepoint is.
287
+ - **The icon font actually loaded** → a failed font load renders tofu boxes while the codepoint
288
+ check still passes, so also assert `document.fonts.check('20px <family>')` and confirm the family
289
+ appears with status `loaded` in `document.fonts`. To rule out fallback rendering, measure the
290
+ glyph's advance width in the icon family versus a generic fallback family and check that they
291
+ *differ* — icon glyphs are not guaranteed to be square, so treat a specific width (e.g. exactly
292
+ 20px) as a smell rather than a pass/fail rule, and prefer font-loading or network evidence. After a
293
+ migration, assert the **probed glyph's** computed family is the new one; do not require the old
294
+ family to be absent from `document.fonts`, since another element or an earlier mount in this SPA
295
+ session can keep it registered.
296
+ - **Size** → computed `font-size` in px. `iconSizeToNumber` in `ui/src/Common.ts` maps
297
+ `xs/sm/md/lg/xl/2xl` → `8/12/16/20/24/28`.
298
+ - **Color** → computed `color`, compared against `theme.text[...]` in `ui/src/Theme.tsx`.
299
+ Watch out: the `text` color map and the surface/other maps use the **same token names for
300
+ different primitives**. In the light theme `text.secondaryDark` is `secondary800` (`#092E3A`),
301
+ **not** `secondary500` — always trace the map that the component actually reads
302
+ (`Icon` uses `theme.text[color]`).
303
+
304
+ Read these from the icon element (the `Pressable`'s first element child), e.g. a one-shot probe
305
+ returning `{fontFamily, fontSize, color, disabled}` per button, and snapshot it before *and* after
306
+ each interaction. For a disabled no-op, assert the after-state is identical to the before-state,
307
+ then re-enable and repeat the same click to prove the hit target was live all along.
308
+
309
+ Measure geometry with the page at its default scale. Browser page zoom does *not* skew these
310
+ comparisons — `getBoundingClientRect()` and computed `font-size` are both CSS pixels — but a CDP
311
+ `Emulation.setPageScaleFactor`/`setDeviceMetricsOverride` scale factor or a pinch-zoomed
312
+ `visualViewport.scale` does change the mapping to device pixels. Record `window.devicePixelRatio`
313
+ and `visualViewport.scale` alongside the numbers you report so a mismatch is attributable.
314
+
315
+ #### Verifying a filled-surface selection cue (and its contrast)
316
+
317
+ When selection is shown by a background circle/pill instead of a glyph change, probe the
318
+ **`Pressable`** (not the glyph) for `backgroundColor` and `borderRadius`, and assert:
319
+
320
+ - selected → a concrete `rgb(...)` matching the intended `theme.surface[...]` token;
321
+ - unselected → `rgba(0, 0, 0, 0)` (transparent);
322
+ - `borderRadius` is exactly half the tap target at every size, otherwise it is a rounded square
323
+ rather than a circle.
324
+
325
+ Also **compute contrast ratios from the rendered colors**, since a surface-only cue can be too
326
+ subtle where a solid-vs-outline cue never was. Walk up the DOM to find the first ancestor with a
327
+ non-transparent `backgroundColor` to get the real page background (the demo page surface is
328
+ `#F2F2F2`, not white), then apply the WCAG relative-luminance formula. Check both
329
+ glyph-on-surface (aim ≥4.5:1) and **surface-vs-page-background**, which WCAG 1.4.11 wants at ≥3:1
330
+ for a non-text state indicator — pale tokens like `secondary100` (`#B6CDD5`) land around 1.48:1 on
331
+ `#F2F2F2` and are worth flagging to design as a non-blocking observation. Muted-icon-on-`surface.disabled`
332
+ combinations tend to be ~2:1 grey-on-grey; that is cosmetic (disabled controls are exempt from
333
+ WCAG contrast) but still worth a screenshot and a note.
334
+
335
+ #### Verifying pixel geometry against a Figma spec
336
+
337
+ When a PR claims a component now matches a Figma frame, verify the numbers at **browser zoom 1**
338
+ and report what you actually measured rather than rounding to "matches":
339
+
340
+ - Glyph box → `getBoundingClientRect()` on the icon element; also cross-check computed `font-size`.
341
+ - Distance between two icons → measure **both** the tap-target centers and the glyph centers. If
342
+ they disagree, the glyph is not centered in its pressable.
343
+ - Don't take a `gap` prop on trust: measure `next.left - prev.right` in the DOM. Terreno's spacing
344
+ scale means `gap={2}` is 8px, so `24px target + 8px gap = 32px` between centers — verify that
345
+ arithmetic instead of assuming it.
346
+ - Figma often specifies the gap between *icon boxes* while the DOM gap is between *tap targets*.
347
+ With a 16px glyph in a 24px target these differ by 8px (16 vs 8) — both can be "correct" at once,
348
+ so state which one you measured.
349
+
350
+ #### Painted ink vs the em box
351
+
352
+ `getBoundingClientRect()` on a glyph element returns the **em box** (equal to `font-size`), which is
353
+ not what a designer means by "the icon is 16px" — icon fonts pad their artwork inside the em box, so
354
+ a 20px MaterialIcons thumb paints about 16.5 x 14.9 px. When the spec is about the *visible* icon,
355
+ measure the ink:
356
+
357
+ 1. **Screenshot pixel analysis (preferred).** Set `document.body.style.zoom = 8`, screenshot, compute
358
+ the bounding box of non-background pixels, and divide by the zoom factor — validating that factor
359
+ independently first (a known 32px centre distance should measure 256px).
360
+ 2. **Canvas `TextMetrics`** — `measureText(glyph)` with `actualBoundingBoxLeft/Right/Ascent/Descent`
361
+ in the normalized family at the same px size. Faster, but it rounds outward from the rasterized
362
+ ink and can read 1-2px large. Use a **synchronous** IIFE; an async one returns an unserialized
363
+ Promise (`{}`).
364
+
365
+ Report width and height separately. Icon glyphs are rarely square, so "make the ink exactly NxN" is
366
+ usually impossible with one font-size — give the font-size that satisfies each dimension and let the
367
+ author pick which one the Figma box is keyed to.
368
+
369
+ #### Flush (zero-gap) tap targets
370
+
371
+ When adjacent targets have no gap, assert `positive.right === negative.left` and a non-overlap check
372
+ rather than only the centre distance, and hit-test a few px either side of the shared edge with
373
+ `document.elementFromPoint` to prove neither target steals the other's clicks.
374
+
375
+ #### Prop tables come from generated TypeDoc, not the source
376
+
377
+ The demo Props table is rendered from `demo/ui-types-documentation.json` (mirrored into
378
+ `mcp-server/{src,dist}/docs/`). Those files are **git-ignored build output**, so a PR that removes or
379
+ renames a public prop in `ui/src/Common.ts` will still show the dead prop in a demo app started from
380
+ a stale copy. Regenerate with `bun run generate-types` in `demo/` before reading the Props table, and
381
+ report drift as a local staleness issue, not a missing commit.
382
+
383
+ Note `demoOptions.size` in `demo/story-config/*.config.tsx` is the demo *panel* sizing key used by
384
+ many configs — unrelated to a component's own `size` prop, and not stale metadata.
385
+
386
+ #### Traps when clicking small tap targets
387
+
388
+ Two things will silently produce bogus click results:
389
+
390
+ 1. **Screenshot coordinates are not CSS coordinates.** The browser screenshot may be 1024px wide
391
+ while `window.innerWidth` is 1600 (scale 0.64). If your click tool takes *screenshot-image*
392
+ coordinates, multiply CSS coordinates by `screenshotWidth / innerWidth` first; Playwright/CDP
393
+ `mouse.click` takes **viewport CSS pixels**, so pass `getBoundingClientRect()` values straight
394
+ through and do NOT rescale. Know which space your tool uses — guessing wrong makes clicks land on
395
+ nothing and look like a dead hit target.
396
+ 2. **A fully-rounded pressable has a circular hit area.** With `borderRadius: tapTarget / 2` the
397
+ bounding-box corners are not clickable — sweeping `document.elementFromPoint` over the box shows
398
+ only ~84% of it hits the button. Always click the **center**, and if you need to characterise the
399
+ real target, do the `elementFromPoint` pixel sweep rather than guessing.
400
+
401
+ Also probe the midpoint of the gap between two adjacent targets with `elementFromPoint`: if it
402
+ returns neither button, the hit areas don't touch and cannot steal each other's clicks. Compare the
403
+ tap-target size against WCAG 2.5.8 (24×24 CSS px); anything smaller may still pass via the spacing
404
+ exception if a 24px circle on each target wouldn't intersect the neighbour's, so report the number
405
+ rather than declaring a violation.
406
+
407
+ #### `accessibilityState.selected` does not reach the web DOM
408
+
409
+ react-native-web maps the `disabled` half of `accessibilityState` (you get `disabled` +
410
+ `tabindex="-1"`) but for `accessibilityRole="button"` it drops `selected` — no `aria-selected`,
411
+ `aria-pressed` or `aria-checked` is emitted. So a `Pressable` with
412
+ `accessibilityState={{selected: isSelected}}` renders a selected DOM node **identical** to an
413
+ unselected one apart from its content. Always verify this by dumping every attribute of both nodes
414
+ rather than trusting the source.
415
+
416
+ This matters most when a design unifies selected/unselected colors so glyph shape is the only visual
417
+ cue: there is then no non-visual cue at all, and docs claiming "selection is announced via
418
+ accessibility state" are false on web. The fix must be a **role-compatible** state: `aria-selected`
419
+ is only valid on selectable composite descendants (tab, option, row, gridcell), so it is wrong on a
420
+ button. Use `accessibilityRole="checkbox"` + `aria-checked` for a two-state control (see the
421
+ keyboard caveat below), or `aria-pressed` if a toggle-button role is available.
422
+
423
+ Confirmed root cause: `react-native-web` (checked at 0.21.2) only consumes **flat** props in
424
+ `dist/modules/createDOMProps/index.js` (`aria-checked`, `aria-selected`, `aria-disabled`, …) and
425
+ never reads `accessibilityState` at all. The `disabled` half only appears to work because
426
+ `Pressable`'s own `disabled` prop sets it through a separate path. Note RN 0.81 has no
427
+ `aria-pressed` prop and RNW has no mapping for `accessibilityRole="togglebutton"`, so
428
+ `accessibilityRole="checkbox"` + `aria-checked` is usually the only two-state option that is both
429
+ typed by RN and forwarded by RNW.
430
+
431
+ #### Changing `accessibilityRole` changes the rendered ELEMENT — always re-test the keyboard
432
+
433
+ This is the trap that a role fix is most likely to introduce, and no amount of attribute dumping
434
+ will catch it. RNW renders `accessibilityRole="button"` as a **real `<button type="button">`**
435
+ (verify on any Terreno `Button`: `/dev/Button?story=Variants` shows
436
+ `<button aria-label="…" type="button">`), so both **Space** and **Enter** activate it via browser
437
+ default with no JS. Any other role — `checkbox`, `switch`, `radio`, `tab` — renders a plain
438
+ `<div role="…" tabindex="0">`, where there is no browser default activation and RNW's Pressable
439
+ keyboard path wires **Enter only**. Switching `button` → `checkbox` therefore silently drops Space,
440
+ which for `role="checkbox"` is the canonical ARIA APG activation key (Enter isn't even required).
441
+
442
+ So after any `accessibilityRole` change, test with the real keyboard, not just clicks:
443
+
444
+ 1. `Tab` to the control and confirm a visible focus ring.
445
+ 2. Press **Space**; dump `aria-checked`/`aria-pressed` and the visible state.
446
+ 3. Press **Enter**; dump again.
447
+ 4. Report which keys actually toggle. If Space is dead on a `checkbox`/`switch`/`radio` role, that is
448
+ a real keyboard-accessibility defect — suggest an explicit `onKeyDown` handling `" "` / `"Space"`
449
+ with `preventDefault()` (to suppress page scroll), or backing the control with a real
450
+ `<input type="checkbox">`.
451
+
452
+ When a component does add its own `onKeyDown`, check two things a native button gives you for free:
453
+ that Space does not scroll (test on a page with **real** scroll room and assert `scrollY`/`scrollTop`
454
+ is unchanged), and that a **held** key does not rapid-fire — `keydown` repeats while the key is down,
455
+ so the handler needs `if (event.repeat) return;`. A discrete-key-press harness cannot observe
456
+ auto-repeat, so read the source for the `event.repeat` guard rather than claiming it verified.
457
+
458
+ To decide whether it is a *regression* rather than pre-existing, read the previous commit's source
459
+ (`git show <prev>:path/to/Component.tsx | grep accessibility`) — if it used `accessibilityRole="button"`
460
+ it was a native button and Space used to work.
461
+
462
+ #### Baselining console warnings
463
+
464
+ The demo app emits pre-existing React-Native-Web deprecations on essentially every page
465
+ (`props.pointerEvents is deprecated`). Before blaming your component, load an unrelated component
466
+ page (e.g. `/demo/IconButton`) and diff the console output. Only warnings that appear on your page
467
+ and not the baseline are attributable to your change.
468
+
469
+ The `"shadow*" style props are deprecated. Use "boxShadow".` warning is fixed — `@terreno/ui` uses
470
+ `boxShadow` (see `createBoxShadow` in `ui/src/Utilities.tsx`) and `react-native-modalize` /
471
+ `react-native-actions-sheet` are patched in `patches/`. If it reappears, a new `shadow*` style prop
472
+ was introduced; do not re-add it to the baseline.
473
+
474
+ ### `admin-frontend` and example app UI changes
475
+
476
+ Test `admin-frontend` changes in the example full-stack app.
477
+
478
+ 1. Start the backend from the repo root:
479
+
480
+ ```bash
481
+ bun run backend:dev
482
+ ```
483
+
484
+ The backend runs on `http://localhost:4000`.
485
+
486
+ 2. Seed login data in a separate terminal:
487
+
488
+ ```bash
489
+ cd example-backend && bun run seed
490
+ ```
491
+
492
+ 3. Start the frontend from the repo root:
493
+
494
+ ```bash
495
+ bun run frontend:web
496
+ ```
497
+
498
+ The frontend runs on `http://localhost:8082`.
499
+
500
+ 4. Log in with the seeded admin account:
501
+ - Email: `admin@example.com`
502
+ - Password: `testpassword123`
503
+
504
+ 5. Navigate to the changed admin screen.
505
+ - Admin home: `http://localhost:8082/admin`
506
+ - Model table: `http://localhost:8082/admin/<modelName>`
507
+ - Configuration: `http://localhost:8082/admin/configuration`
508
+ - Consent forms: `http://localhost:8082/admin/consent-forms`
509
+ - Consent responses: `http://localhost:8082/admin/consent-responses`
510
+
511
+ 6. Verify both the changed screen and the path that gets the user there.
512
+ - For list/table changes: verify loading, populated, empty, pagination, sorting, and row navigation when applicable.
513
+ - For form changes: verify create/edit validation, save loading, cancel/back navigation, and error handling.
514
+ - For admin cards/navigation: verify the card appears, the label/copy is correct, and click navigation still works.
515
+
516
+ ### Other frontend package changes
517
+
518
+ - `example-frontend` user-facing screens: use `bun run backend:dev`, `cd example-backend && bun run seed`, and `bun run frontend:web`, then log in with `test@example.com` / `testpassword123`, navigate to the changed feature, and exercise it.
519
+ - Demo-only story changes: use the demo app `/dev` route and the story being changed.
520
+ - Generated SDK or API-surface-only frontend changes: combine targeted automated checks with an example app login + feature smoke test when a rendered UI path is affected.
521
+
522
+ ## Report format
523
+
524
+ Findings first when anything broke (what the user sees, repro, evidence, fix made or
525
+ open). Then **Result** (PASS / FAIL / UNVERIFIED, one line), **Flow under test**,
526
+ **Environment** (URLs, viewports, tool, database, commit), **Remaining risk**, and per
527
+ app × viewport: the checks table, the interaction steps, the DOM assertions, every
528
+ screenshot labelled, the recording, and the console log; then per simulator flow:
529
+ result, device, failure text, final screen, recording, log tail. Concise, but concrete
530
+ enough that a PR reviewer trusts it without re-running. **Result** is the worst outcome
531
+ across every required leg: any FAIL makes it FAIL, otherwise any UNVERIFIED leg makes it
532
+ UNVERIFIED. A required leg that did not run, such as the simulator for a mobile-focused
533
+ change, is UNVERIFIED with its blocker, never remaining risk. Only platforms the change
534
+ does not require go under remaining risk.
535
+
536
+ ## GitHub Reviewer Evidence
537
+
538
+ Post UI verification evidence to GitHub through the PR body so reviewers can see it without local setup.
539
+
540
+ - Follow [`.github/PULL_REQUEST_TEMPLATE.md`](../../../../.github/PULL_REQUEST_TEMPLATE.md):
541
+ one concise `Verification` row per exercised flow, naming the tested URL, the flow and
542
+ state proved, and the Result line. Never include credentials.
543
+ - Save screenshots and videos under `/opt/cursor/artifacts` (or your scratchpad) and
544
+ reference only decisive artifacts with HTML tags, e.g.
545
+ `<video src="/opt/cursor/artifacts/admin_model_list_demo.mp4" controls></video>`.
546
+ Use `RecordScreen` in Cursor Cloud for video walkthroughs.
547
+ - Use the PR management tool to create or update the PR body after artifacts exist.
548
+ Without a tool that uploads files, or when `/opt/cursor/artifacts/` is missing or
549
+ read-only, upload with the `attach-pr-media` skill and use the URLs it returns, or
550
+ link the secret gist the repository's publish script created.
551
+ - Keep evidence minimal: one short video is preferred for interaction flows, plus one screenshot when it shows a static visual state more clearly.
552
+
553
+ ## Rationalizations to reject
554
+
555
+ - "The build passes, so it works." Render it.
556
+ - "It's a one-line CSS change." Screenshot it, at phone width too.
557
+ - "I checked the happy path." Check error, empty, and loading states.
558
+ - "The screenshot looks right." Assert on the DOM as well.
559
+ - "There's a console error but it's probably unrelated." Explain it or fix it.
560
+ - "I'll mention it might need testing." Verify it or mark it UNVERIFIED.
561
+ - "It works on web, and it's React Native, so it works on the phone." Not for
562
+ mobile-focused features. Run the Maestro flow, or direct simulator commands when the
563
+ app has no Maestro, on the simulator.
564
+ - "Something is already on the port, I'll just use it." It may be staging. Stop and say
565
+ the ports are taken.
566
+
567
+ ## Cursor Cloud Notes
568
+
569
+ - Delegate GUI-driven verification to the `ui-verifier` subagent when your harness supports subagents; in Cursor Cloud, fall back to its built-in browser/GUI capability.
570
+ - For web apps on Bun 1.4+, use the documented [`terreno web`](../../docs/reference/cli.md#web-app-automation) flow to collect an agent-readable snapshot and final screenshot. This can prove static rendered state and complement the required GUI video for interaction flows.
571
+ - Use `RecordScreen` for user-facing video walkthroughs of interactive UI changes.
572
+ - Leave test servers running after verification so the user can continue testing.
573
+
574
+ ## Final Response Checklist
575
+
576
+ - Lead with the Result line and the evidence link; include the decisive screenshot or
577
+ video for UI changes.
578
+ - Prefix every command in the testing section with pass, warning, or fail status.
579
+ - Explain why each test or check was run, and which required checks each run passed.
580
+ - Mention any environment limitation only when it prevented expected UI verification,
581
+ and then mark the affected flow UNVERIFIED with the blocker.
582
+ - Confirm PR evidence was posted or will be posted before the PR is opened/updated.
583
+
584
+ ## Pixel Measurement: Derive the Screenshot Scale Empirically
585
+
586
+ When measuring painted ink (or any exact pixel geometry) from a zoomed screenshot, never assume
587
+ `saved screenshot px == zoom factor × CSS px`. The screenshot the harness writes to disk can be a
588
+ different size than the image it shows inline, and the browser window may have been resized between
589
+ runs.
590
+
591
+ Do this before dividing by the zoom factor:
592
+
593
+ 1. Read `window.innerWidth` and compare it to the width of the saved PNG. If they are equal, one
594
+ screenshot pixel is one CSS layout pixel and the zoom factor is the only divisor.
595
+ 2. Cross-validate against a known layout pitch (e.g. two flush 32px targets must sit exactly
596
+ `32 × zoom` screenshot px apart). If the pitch does not match, the scale assumption is wrong and
597
+ every derived number is wrong.
598
+ 3. Measure each glyph inside **its own element rect** (from `getBoundingClientRect()` at the same
599
+ zoom), not across the whole image — otherwise adjacent labels and section bands leak into the
600
+ bounding box.
601
+ 4. Use the **modal colour of that sub-region** as the background reference rather than a hardcoded
602
+ page background, and re-run the bbox at 2–3 thresholds (e.g. background delta > 10 / 30 / 60).
603
+ A measurement that shifts with the threshold is an antialiasing artefact, not a result.
604
+ 5. Report ink width and height separately per variant, plus the non-background pixel count — the
605
+ count is what distinguishes a filled glyph from an outline glyph that shares the same bbox.