td-ai-tools 1.2.0 → 1.2.2

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 (34) hide show
  1. package/package.json +1 -1
  2. package/skills/README.md +3 -0
  3. package/skills/client-overview/SKILL.md +93 -0
  4. package/skills/client-overview/agents/openai.yaml +4 -0
  5. package/skills/client-overview/scripts/branch_client_overview_context.py +317 -0
  6. package/skills/debugging-ios-webkit/SKILL.md +60 -0
  7. package/skills/debugging-ios-webkit/references/device.md +62 -0
  8. package/skills/debugging-ios-webkit/references/playwright.md +60 -0
  9. package/skills/debugging-ios-webkit/references/simulator.md +76 -0
  10. package/skills/debugging-ios-webkit/scripts/device_eval.py +62 -0
  11. package/skills/debugging-ios-webkit/scripts/device_experiment.py +99 -0
  12. package/skills/debugging-ios-webkit/scripts/device_snapshot.py +62 -0
  13. package/skills/record-changes/SKILL.md +3 -3
  14. package/skills/shopify-lint/SKILL.md +50 -0
  15. package/skills/shopify-lint/agents/openai.yaml +4 -0
  16. package/skills/shopify-lint/scripts/setup.sh +60 -0
  17. package/skills/shopify-lint/scripts/shopify_lint.py +259 -0
  18. package/skills/shopify-lint/tests/test_shopify_lint.py +137 -0
  19. package/skills/shopify-lint/theme-check-theory/.theme-check.example.yml +14 -0
  20. package/skills/shopify-lint/theme-check-theory/README.md +123 -0
  21. package/skills/shopify-lint/theme-check-theory/configs/recommended.yml +8 -0
  22. package/skills/shopify-lint/theme-check-theory/package-lock.json +1947 -0
  23. package/skills/shopify-lint/theme-check-theory/package.json +44 -0
  24. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.test.ts +198 -0
  25. package/skills/shopify-lint/theme-check-theory/src/checks/unguarded-text-setting.ts +143 -0
  26. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.test.ts +137 -0
  27. package/skills/shopify-lint/theme-check-theory/src/checks/unused-section-settings.ts +64 -0
  28. package/skills/shopify-lint/theme-check-theory/src/index.test.ts +20 -0
  29. package/skills/shopify-lint/theme-check-theory/src/index.ts +11 -0
  30. package/skills/shopify-lint/theme-check-theory/src/test-utils.ts +31 -0
  31. package/skills/shopify-lint/theme-check-theory/src/utils/ast.ts +126 -0
  32. package/skills/shopify-lint/theme-check-theory/tsconfig.build.json +10 -0
  33. package/skills/shopify-lint/theme-check-theory/tsconfig.json +15 -0
  34. package/skills/shopify-lint/theme-check-theory/vitest.config.ts +11 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "description": "Install agent skills and packs into your project",
5
5
  "type": "module",
6
6
  "scripts": {
package/skills/README.md CHANGED
@@ -7,6 +7,8 @@
7
7
  - `browser-validation`: Before completing a task validate frontend or template changes in a real browser with the Playwright-CLI…
8
8
  - `cache-reset`: Clear and warm Laravel and Statamic caches (including Statamic Glide image caches) after content or template…
9
9
  - `car-ticket-generator`: Generate a ticket for the codex-auto-runner queue
10
+ - `client-overview`: Generate a client-facing markdown report that summarizes all changes on the current branch against the…
11
+ - `debugging-ios-webkit`: Debugs iOS Safari/Chrome-iOS rendering bugs — stale paints, viewport/browser-chrome clipping, mobile-only CSS…
10
12
  - `everhour-basecamp-estimates`: Bulk update Everhour task estimates from a Basecamp todo or todolist URL, then append bracketed hours to the…
11
13
  - `forge-cli`: Manage Laravel Forge servers, sites, and provisioned resources from the terminal with the Laravel Forge CLI,…
12
14
  - `horizon-component-migration`: Bundle Shopify Horizon components into a migration package for a different theme, including recursive…
@@ -16,6 +18,7 @@
16
18
  - `pull-request-statamic`: Generates GitHub pull request descriptions for Statamic and Laravel development by analyzing git diffs and…
17
19
  - `record-changes`: Update `docs/changes.md` by summarizing the current branch against the primary development branch.
18
20
  - `shopify-cli`: Shopify CLI workflows for theme development.
21
+ - `shopify-lint`: Run Shopify CLI Theme Check with Theory Digital's bundled custom checks while reporting and failing only on offenses in files modified on the current Git branch.
19
22
  - `stylesheet-migration`: Migrate Shopify Liquid `{% stylesheet %}` blocks into theme CSS assets using bundled Python scripts.
20
23
  - `td-js-vanilla-rules`: Theory Digital vanilla JavaScript standards for Shopify theme work.
21
24
  - `td-review`: Run parallel code review agents on a PR (including TD theme compliance) and produce a synthesized findings…
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: client-overview
3
+ version: 1.0.0
4
+ description: Generate a client-facing markdown report that summarizes all changes on the current branch against the primary development branch for a non-technical audience, with accessibility changes explained through end-user experience, accessibility benchmark categories, and practical impact.
5
+ ---
6
+
7
+ # Client Overview
8
+
9
+ Generate a markdown report for a client or stakeholder audience. Cover all meaningful branch changes, but refer to the work as an update (client's will not understand the term branch). Give extra attention to accessibility-related changes by explaining what people experience when using the affected feature, as well as which WCAG guidelines the change addresses.
10
+
11
+ ## Workflow
12
+
13
+ ### 1. Collect Branch Context
14
+
15
+ Run the bundled script first:
16
+
17
+ ```bash
18
+ python3 .agents/skills/client-overview/scripts/branch_client_overview_context.py
19
+ ```
20
+
21
+ If the user specifies another comparison branch, pass it explicitly:
22
+
23
+ ```bash
24
+ python3 .agents/skills/client-overview/scripts/branch_client_overview_context.py --base release/x.y
25
+ ```
26
+
27
+ Default output folder: `docs/`, if the repo does not have a docs folder use the root instead. Do not use or reference the changes.md file, as that is intended for internal technical users.
28
+
29
+ ### 2. Inspect the Actual Changes
30
+
31
+ Use the script output to identify changed files, then inspect relevant diffs and source files with `git diff` and targeted reads. Do not rely on branch names or file names alone.
32
+
33
+ Prioritize:
34
+
35
+ - User-facing behavior, layout, copy, navigation, forms, search, cart, checkout, account, and content changes
36
+ - Accessibility-relevant markup, ARIA, labels, headings, landmarks, alt text, focus handling, keyboard behavior, validation, contrast, spacing, motion, responsive behavior, and interactive controls
37
+ - CSS and JavaScript changes that affect visibility, focus, scroll behavior, modals, drawers, menus, accordions, sliders, media, filters, or dynamic content
38
+ - CMS settings or schema changes that alter what site admins can configure for visitors
39
+
40
+ Do not summarize the report file edit itself as branch work. If the branch includes tooling-only changes, include them only when they affect the non-technical reader's experience or confidence in the release.
41
+
42
+ ### 3. Translate Accessibility Changes
43
+
44
+ For each accessibility-relevant change, write in terms of:
45
+
46
+ - **Benchmark category:** Use plain category names such as keyboard access, screen reader clarity, focus visibility, form labels and errors, color contrast, reduced motion, responsive reflow, touch target usability, readable content, or predictable navigation. Add WCAG references only if the code clearly maps to them.
47
+ - **User experience:** State what someone with a disability, temporary impairment, or assistive technology setup can now do, understand, avoid, or recover from.
48
+ - **Affected journey:** Name the feature or page area in shopper terms, such as product options, search filters, cart drawer, checkout path, navigation menu, newsletter signup, or collection browsing.
49
+ - **Before/after effect:** Describe the practical improvement without blaming previous work. Example: "Keyboard shoppers can now see which control is active while moving through the menu."
50
+
51
+ Avoid implementation-heavy language in the main summary. Keep file paths, selectors, variables, and component names out of the file unless there is a clear edge case requiring them.
52
+
53
+ ### 4. Write the Report
54
+
55
+ Example entry structure:
56
+
57
+ ```markdown
58
+ ## <short user-facing title>
59
+ **Date:** YYYY-MM-DD
60
+
61
+ ### At a Glance
62
+ - <plain-language summary of the update outcome>
63
+
64
+ ### What Changed for Users
65
+ | Area | User-facing change | Why it matters |
66
+ |------|--------------------|----------------|
67
+
68
+ ### Accessibility Experience
69
+ | Benchmark category | Affected experience | User impact |
70
+ |--------------------|---------------------|-------------|
71
+ ```
72
+
73
+ Guidelines:
74
+
75
+ - Title the entry by the experience improved, not the branch name.
76
+ - Write for clients, support, QA, designers, and stakeholders who may not read code.
77
+ - In the output file, refer to the branch as "this update" or "the update"; do not call it "the branch".
78
+ - Do not refer to yourself, the agent, or the writing process in the output file. Avoid first-person process statements such as "I reviewed," "I checked," "this summary," or "this report."
79
+ - Prefer "people using keyboard navigation" over "keyboard users" when the sentence is about lived experience.
80
+ - Prefer "screen reader announces..." only when the code change truly affects accessible names, roles, states, landmarks, headings, or live regions.
81
+
82
+ ### 5. Verify Before Finishing
83
+
84
+ Before wrapping up:
85
+
86
+ - Re-read the new markdown entry in context.
87
+ - Confirm every listed file or area appears in the branch diff.
88
+ - Confirm the report describes observed code behavior, not intended behavior inferred from a branch name.
89
+ - Confirm accessibility claims are tied to a benchmark category and an affected user experience.
90
+
91
+ ## Resource
92
+
93
+ - `scripts/branch_client_overview_context.py`: Resolve the best available base branch and print branch file changes with accessibility inspection hints.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Client Overview"
3
+ short_description: "Summarize branch changes for clients"
4
+ default_prompt: "Use $client-overview to compare this branch to main and write a client-facing accessibility-focused change summary."
@@ -0,0 +1,317 @@
1
+ #!/usr/bin/env python3
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import subprocess
7
+ import sys
8
+
9
+
10
+ DEFAULT_BASE_CANDIDATES = ("main", "origin/main", "master", "origin/master")
11
+
12
+ ACCESSIBILITY_HINTS = (
13
+ (
14
+ "keyboard access and focus behavior",
15
+ (
16
+ "focus",
17
+ "tabindex",
18
+ "keydown",
19
+ "keyup",
20
+ "keypress",
21
+ "escape",
22
+ "drawer",
23
+ "modal",
24
+ "menu",
25
+ "accordion",
26
+ "slider",
27
+ "carousel",
28
+ ),
29
+ ),
30
+ (
31
+ "screen reader clarity and semantic structure",
32
+ (
33
+ "aria",
34
+ "role",
35
+ "label",
36
+ "heading",
37
+ "h1",
38
+ "h2",
39
+ "h3",
40
+ "landmark",
41
+ "sr-only",
42
+ "visually-hidden",
43
+ "alt",
44
+ ),
45
+ ),
46
+ (
47
+ "form labels, instructions, and error recovery",
48
+ (
49
+ "form",
50
+ "input",
51
+ "select",
52
+ "textarea",
53
+ "error",
54
+ "invalid",
55
+ "required",
56
+ "newsletter",
57
+ "contact",
58
+ ),
59
+ ),
60
+ (
61
+ "color contrast, readable content, and visual state",
62
+ (
63
+ "color",
64
+ "contrast",
65
+ "opacity",
66
+ "background",
67
+ "foreground",
68
+ "text",
69
+ "font",
70
+ "hover",
71
+ "active",
72
+ ),
73
+ ),
74
+ (
75
+ "responsive reflow, zoom, and touch target usability",
76
+ (
77
+ "mobile",
78
+ "responsive",
79
+ "breakpoint",
80
+ "media",
81
+ "width",
82
+ "height",
83
+ "spacing",
84
+ "padding",
85
+ "touch",
86
+ ),
87
+ ),
88
+ (
89
+ "motion, animation, and scroll comfort",
90
+ (
91
+ "motion",
92
+ "animation",
93
+ "transition",
94
+ "scroll",
95
+ "autoplay",
96
+ "video",
97
+ "prefers-reduced-motion",
98
+ ),
99
+ ),
100
+ )
101
+
102
+
103
+ def run_git(*args: str) -> str:
104
+ completed = subprocess.run(
105
+ ["git", *args],
106
+ check=True,
107
+ capture_output=True,
108
+ text=True,
109
+ )
110
+ return completed.stdout.strip()
111
+
112
+
113
+ def ref_exists(ref: str) -> bool:
114
+ completed = subprocess.run(
115
+ ["git", "rev-parse", "--verify", f"{ref}^{{commit}}"],
116
+ capture_output=True,
117
+ text=True,
118
+ )
119
+ return completed.returncode == 0
120
+
121
+
122
+ def resolve_base_ref(explicit_base: str | None) -> str:
123
+ if explicit_base:
124
+ if not ref_exists(explicit_base):
125
+ raise SystemExit(f"Base ref not found: {explicit_base}")
126
+ return explicit_base
127
+
128
+ for candidate in DEFAULT_BASE_CANDIDATES:
129
+ if ref_exists(candidate):
130
+ return candidate
131
+
132
+ searched = ", ".join(DEFAULT_BASE_CANDIDATES)
133
+ raise SystemExit(f"No base ref found. Tried: {searched}")
134
+
135
+
136
+ def parse_name_status(output: str) -> list[dict[str, str]]:
137
+ files = []
138
+ for line in output.splitlines():
139
+ if not line:
140
+ continue
141
+ parts = line.split("\t")
142
+ status = parts[0]
143
+
144
+ if status.startswith("R") and len(parts) >= 3:
145
+ files.append(
146
+ {
147
+ "status": status,
148
+ "path": parts[2],
149
+ "old_path": parts[1],
150
+ }
151
+ )
152
+ continue
153
+
154
+ path = parts[1] if len(parts) > 1 else ""
155
+ files.append({"status": status, "path": path, "old_path": ""})
156
+ return files
157
+
158
+
159
+ def parse_numstat(output: str) -> dict[str, dict[str, str]]:
160
+ stats = {}
161
+ for line in output.splitlines():
162
+ if not line:
163
+ continue
164
+ parts = line.split("\t")
165
+ if len(parts) < 3:
166
+ continue
167
+ additions, deletions, path = parts[0], parts[1], parts[2]
168
+ stats[path] = {"additions": additions, "deletions": deletions}
169
+ return stats
170
+
171
+
172
+ def resolve_stats(
173
+ numstat: dict[str, dict[str, str]], path: str, old_path: str
174
+ ) -> dict[str, str] | None:
175
+ stats = numstat.get(path)
176
+ if stats or not old_path:
177
+ return stats
178
+
179
+ path_dir = os.path.dirname(path)
180
+ old_dir = os.path.dirname(old_path)
181
+ path_name = os.path.basename(path)
182
+ old_name = os.path.basename(old_path)
183
+
184
+ for candidate_path, candidate_stats in numstat.items():
185
+ if old_path in candidate_path and path in candidate_path:
186
+ return candidate_stats
187
+ if (
188
+ path_dir == old_dir
189
+ and path_dir
190
+ and candidate_path.startswith(f"{path_dir}/")
191
+ and old_name in candidate_path
192
+ and path_name in candidate_path
193
+ ):
194
+ return candidate_stats
195
+
196
+ return None
197
+
198
+
199
+ def accessibility_hints_for(path: str) -> list[str]:
200
+ path_lower = path.lower()
201
+ _, ext = os.path.splitext(path_lower)
202
+ hints = []
203
+
204
+ for label, keywords in ACCESSIBILITY_HINTS:
205
+ if any(keyword in path_lower for keyword in keywords):
206
+ hints.append(label)
207
+
208
+ if ext in {".css", ".scss", ".sass"}:
209
+ hints.extend(
210
+ [
211
+ "color contrast, readable content, and visual state",
212
+ "responsive reflow, zoom, and touch target usability",
213
+ ]
214
+ )
215
+ elif ext in {".js", ".ts", ".jsx", ".tsx"}:
216
+ hints.extend(
217
+ [
218
+ "keyboard access and focus behavior",
219
+ "motion, animation, and scroll comfort",
220
+ ]
221
+ )
222
+ elif ext in {".liquid", ".html", ".vue", ".svelte"}:
223
+ hints.extend(
224
+ [
225
+ "screen reader clarity and semantic structure",
226
+ "form labels, instructions, and error recovery",
227
+ ]
228
+ )
229
+
230
+ return sorted(set(hints))
231
+
232
+
233
+ def build_context(base_ref: str) -> dict[str, object]:
234
+ current_branch = run_git("branch", "--show-current")
235
+ merge_base = run_git("merge-base", base_ref, "HEAD")
236
+ shortstat = run_git("diff", "--shortstat", merge_base, "HEAD")
237
+ name_status = parse_name_status(
238
+ run_git("diff", "--name-status", "--find-renames", merge_base, "HEAD")
239
+ )
240
+ numstat = parse_numstat(run_git("diff", "--numstat", "--find-renames", merge_base, "HEAD"))
241
+
242
+ for entry in name_status:
243
+ stats = resolve_stats(numstat, entry["path"], entry["old_path"])
244
+ entry["additions"] = stats["additions"] if stats else "?"
245
+ entry["deletions"] = stats["deletions"] if stats else "?"
246
+ path_for_hints = f"{entry['old_path']} {entry['path']}".strip()
247
+ entry["accessibility_hints"] = accessibility_hints_for(path_for_hints)
248
+
249
+ return {
250
+ "current_branch": current_branch,
251
+ "base_ref": base_ref,
252
+ "merge_base": merge_base,
253
+ "shortstat": shortstat,
254
+ "files": name_status,
255
+ }
256
+
257
+
258
+ def print_markdown(context: dict[str, object]) -> None:
259
+ print("# Branch Client Overview Context")
260
+ print()
261
+ print(f"- Current branch: `{context['current_branch']}`")
262
+ print(f"- Base ref: `{context['base_ref']}`")
263
+ print(f"- Merge base: `{context['merge_base']}`")
264
+ print(f"- Summary: {context['shortstat'] or 'No changes detected'}")
265
+ print()
266
+
267
+ files = context["files"]
268
+ if not files:
269
+ print("No changed files detected.")
270
+ return
271
+
272
+ print("| Status | Path | +/- | Accessibility inspection hints |")
273
+ print("|--------|------|-----|----------------------------------|")
274
+
275
+ for entry in files:
276
+ status = entry["status"]
277
+ path = entry["path"]
278
+ if entry["old_path"]:
279
+ path = f"{entry['old_path']} -> {entry['path']}"
280
+ delta = f"+{entry['additions']} / -{entry['deletions']}"
281
+ hints = ", ".join(entry["accessibility_hints"]) or "No obvious hint from file path"
282
+ print(f"| `{status}` | `{path}` | {delta} | {hints} |")
283
+
284
+
285
+ def main() -> int:
286
+ parser = argparse.ArgumentParser(
287
+ description="Resolve a base branch and print context for client-facing change reports."
288
+ )
289
+ parser.add_argument(
290
+ "--base",
291
+ help="Explicit base ref to compare against. Defaults to main/origin-main or master/origin-master fallback.",
292
+ )
293
+ parser.add_argument(
294
+ "--format",
295
+ choices=("markdown", "json"),
296
+ default="markdown",
297
+ help="Output format.",
298
+ )
299
+ args = parser.parse_args()
300
+
301
+ base_ref = resolve_base_ref(args.base)
302
+ context = build_context(base_ref)
303
+
304
+ if args.format == "json":
305
+ print(json.dumps(context, indent=2))
306
+ else:
307
+ print_markdown(context)
308
+
309
+ return 0
310
+
311
+
312
+ if __name__ == "__main__":
313
+ try:
314
+ raise SystemExit(main())
315
+ except subprocess.CalledProcessError as error:
316
+ sys.stderr.write(error.stderr or str(error))
317
+ raise SystemExit(error.returncode)
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: debugging-ios-webkit
3
+ description: Debugs iOS Safari/Chrome-iOS rendering bugs — stale paints, viewport/browser-chrome clipping, mobile-only CSS issues — in whichever environment is specified. Pass "playwright" (fast emulated WebKit), "simulator" (Xcode iOS Simulator via simctl), or "device" (USB-connected physical iPhone via pymobiledevice3 Web Inspector). Use when a bug is reported "only on iPhone", when the user wants to test on a real device or in the simulator, or when styles are correct but pixels are wrong.
4
+ ---
5
+
6
+ # Debugging iOS WebKit Rendering Issues
7
+
8
+ Every iOS browser is WebKit — "Safari and Chrome on iOS" is one engine.
9
+
10
+ ## Choose your environment
11
+
12
+ Read ONLY the reference for the environment requested (or per the guidance
13
+ below), then follow it:
14
+
15
+ - **[references/playwright.md](references/playwright.md)** — emulated WebKit,
16
+ headless, seconds per iteration. CSS cascade, layout, load-order races,
17
+ scripted touch loops. Cannot reproduce iOS compositor/GPU or browser-chrome
18
+ behavior.
19
+ - **[references/simulator.md](references/simulator.md)** — real iOS WebKit +
20
+ real Safari chrome via `simctl`. Viewport units (svh/dvh), safe-area,
21
+ pixel-accurate screenshots. No touch synthesis — drive state via URL params
22
+ or temporary page hooks.
23
+ - **[references/device.md](references/device.md)** — physical iPhone over
24
+ USB. Attach to the live (broken) page, evaluate JS, capture render
25
+ snapshots, run stepped on-screen repair experiments with the person
26
+ watching. The whole truth; required for stale-compositor bugs.
27
+
28
+ If the user didn't specify: start with playwright for speed, escalate to
29
+ simulator when the bug involves viewport/chrome behavior, and to device when
30
+ neither reproduces it or someone has the broken state on a phone right now.
31
+
32
+ ## Shared diagnostic: layout bug vs paint bug (read this regardless)
33
+
34
+ In the broken state, measure **three** things: computed styles + bounding
35
+ rects (JS), a render-tree snapshot (`Page.snapshotRect` on device, or a fresh
36
+ screenshot in playwright/simulator), and what the eyes/screen actually show.
37
+
38
+ - **Styles or rects wrong** → layout/cascade bug. Fix CSS. Iterate at the
39
+ playwright level.
40
+ - **Styles, rects, AND snapshot correct while the screen shows garbage** →
41
+ the compositor is presenting a stale GPU texture. Fix with a paint
42
+ invalidation in JS, not with CSS. The cheapest reliable one is the opacity
43
+ nudge; to avoid a visible pop, dirty the layer *before* the element becomes
44
+ visible and keep nudging every animation frame through any entrance
45
+ transition:
46
+
47
+ ```js
48
+ el.style.opacity = '0.99';
49
+ const start = performance.now();
50
+ (function tick() {
51
+ if (performance.now() - start < 350) {
52
+ el.style.opacity = el.style.opacity === '0.99' ? '1' : '0.99';
53
+ requestAnimationFrame(tick);
54
+ } else { el.style.opacity = ''; }
55
+ })();
56
+ ```
57
+
58
+ A painted background measuring exactly the element's *content-box* (instead
59
+ of border-box) is the signature of unpainted padding — stale layer or native
60
+ form-control painter (`-webkit-appearance` missing at first style pass).
@@ -0,0 +1,62 @@
1
+
2
+ # Debugging on a Real iOS Device
3
+
4
+ Attach to the live page over the cable — including a currently-broken state
5
+ someone is looking at — without reloading or losing it.
6
+
7
+ ## Setup (once)
8
+
9
+ ```bash
10
+ python3.12 -m venv venv && venv/bin/pip install pymobiledevice3
11
+ ```
12
+
13
+ Python ≥ 3.10 required (Xcode's 3.9 breaks the CLI). On the phone:
14
+ Settings → (Apps →) Safari → Advanced → **Web Inspector** ON. Plug in, trust.
15
+
16
+ ## Quick Start
17
+
18
+ ```bash
19
+ venv/bin/python -m pymobiledevice3 webinspector opened-tabs # list tabs
20
+ python scripts/device_eval.py mystore.com '<js expression>' # eval in tab
21
+ python scripts/device_snapshot.py mystore.com out.png # render snapshot
22
+ python scripts/device_experiment.py mystore.com '.my-footer' # live experiments
23
+ ```
24
+
25
+ Scripts take a URL substring to pick the tab. First sanity check: confirm
26
+ `location.href` and `Shopify.theme` — reporters are frequently on production
27
+ or the wrong theme, which explains many "still happening" reports instantly.
28
+
29
+ ## Rules that prevent silent hangs
30
+
31
+ - **One inspector client at a time.** Close Mac Safari's Develop-menu
32
+ inspector, or your attach hangs with no error.
33
+ - The target tab must be **foreground** with the **screen unlocked**.
34
+ - iOS 17+ wraps the protocol in the `Target` domain: sessions need
35
+ `wait_target=True` and responses arrive inside
36
+ `Target.dispatchMessageFromTarget` (the scripts handle both).
37
+ - `ios_webkit_debug_proxy` lists tabs on modern iOS but its eval channel is
38
+ broken — use pymobiledevice3.
39
+
40
+ ## Applying the shared diagnostic here
41
+
42
+ Run the layout-vs-paint diagnostic from SKILL.md using `device_eval.py`
43
+ (rects + computed styles) and `device_snapshot.py` (render-tree snapshot,
44
+ via `Page.snapshotRect` — it bypasses stale compositor layers, so a correct
45
+ snapshot against a broken screen is the paint-bug proof).
46
+
47
+ ## Live repair experiments (stale-paint bugs)
48
+
49
+ Don't guess which invalidation works — bisect it on the actual broken state
50
+ with the reporter watching (`device_experiment.py`):
51
+
52
+ 1. A countdown banner tells them to reproduce the bug ("break it now,
53
+ steps start in 20s") — **warn them before starting so they're watching**.
54
+ 2. Numbered colored banners announce each repair candidate ~5s apart:
55
+ banner-only sanity → opacity nudge → `translateZ(0)` layer →
56
+ `display:none` rebuild → class re-toggle → scroll nudge.
57
+ 3. They report the step number that visually healed the element. Wire exactly
58
+ that operation into the code path that precedes the broken state.
59
+
60
+ The winning invalidation (usually the opacity nudge — see the shared
61
+ diagnostic in SKILL.md for the production-safe anti-pop version) goes into
62
+ the code path that precedes the broken state.
@@ -0,0 +1,60 @@
1
+
2
+ # Debugging with Playwright WebKit
3
+
4
+ Every iOS browser is WebKit (Safari and Chrome-iOS share the engine), so
5
+ Playwright's WebKit build catches most engine-level bugs in seconds, headless.
6
+
7
+ ## Quick Start
8
+
9
+ ```js
10
+ const { webkit, devices } = require('playwright');
11
+ const browser = await webkit.launch();
12
+ const ctx = await browser.newContext({ ...devices['iPhone 14 Pro'], hasTouch: true });
13
+ const page = await ctx.newPage();
14
+ ```
15
+
16
+ One-off setup: `npm i playwright` in a scratch dir (e.g. `/tmp/repro`).
17
+
18
+ ## Instructions
19
+
20
+ 1. **Measure two ways, always.** Computed styles/rects via `page.evaluate`
21
+ AND painted pixels via `page.screenshot()` + a pixel scan. A paint bug
22
+ shows correct computed values with wrong pixels; if you only read
23
+ `getComputedStyle` you will wrongly conclude "works for me".
24
+
25
+ 2. **Real touch, not synthetic clicks.** Use `locator.tap()` (requires
26
+ `hasTouch: true`) when the bug involves touch-driven UI.
27
+
28
+ 3. **Interaction loops.** Before declaring something unreproducible, loop the
29
+ failing interaction 30x, measuring on every iteration:
30
+
31
+ ```js
32
+ for (let i = 1; i <= 30; i++) {
33
+ await page.locator('.js-open-trigger').tap(); // your theme's selector
34
+ await page.waitForTimeout(400);
35
+ const m = await page.evaluate(() => {
36
+ const el = document.querySelector('#target-element');
37
+ const r = el.getBoundingClientRect();
38
+ return { w: Math.round(r.width), h: Math.round(r.height), pad: getComputedStyle(el).padding };
39
+ });
40
+ console.log(i, JSON.stringify(m));
41
+ await page.locator('.js-close-trigger').tap();
42
+ await page.waitForTimeout(300);
43
+ }
44
+ ```
45
+
46
+ 4. **Slow-network CSS races.** Delay one stylesheet with `page.route()`.
47
+ Gotcha: a setup navigation warms the HTTP cache and the route never fires
48
+ on the test navigation — `route.abort()` the asset during setup, then
49
+ switch the handler to delay on the real run.
50
+
51
+ 5. **Test against the same origin the reporter used** (CDN preview vs local
52
+ proxy) — asset latency and compiled bundles differ per page and origin.
53
+
54
+ ## When this level is not enough
55
+
56
+ Playwright WebKit renders everything from a healthy desktop compositor. If
57
+ computed styles and screenshots are clean here but devices still show the bug,
58
+ it lives in iOS-specific territory: dynamic viewport (`dvh`) lag, browser
59
+ chrome show/hide, native form-control painters, or stale GPU textures.
60
+ Escalate to references/simulator.md.