td-ai-tools 1.2.0 → 1.2.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "td-ai-tools",
3
- "version": "1.2.0",
3
+ "version": "1.2.1",
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…
@@ -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.
@@ -0,0 +1,76 @@
1
+
2
+ # Debugging in the iOS Simulator
3
+
4
+ Real iOS WebKit + real Safari chrome, scriptable from the terminal. The one
5
+ hard limitation: **you cannot synthesize taps** (`simctl` has no touch API,
6
+ and macOS blocks synthetic clicks without Accessibility permission).
7
+
8
+ ## Quick Start
9
+
10
+ ```bash
11
+ xcrun simctl list devices available # find a device + UDID
12
+ open -a Simulator --args -CurrentDeviceUDID <UDID>
13
+ xcrun simctl bootstatus <UDID> -b # wait for boot
14
+ xcrun simctl openurl booted "https://site.example/page"
15
+ xcrun simctl io booted screenshot /tmp/shot.png
16
+ ```
17
+
18
+ ## Driving state without touch
19
+
20
+ - **URL parameters the page already supports** — e.g. many themes open the
21
+ cart drawer on load with `?cart=open`; check the theme's JS for what it reads.
22
+ - **Temporary debug hooks** — add a query-param-gated script to the page that
23
+ auto-runs the interaction sequence, sync it to a dev theme, load with the
24
+ param, screenshot on a timer, then remove the hook:
25
+
26
+ ```js
27
+ if (new URLSearchParams(location.search).has('debug_cycles')) {
28
+ window.addEventListener('load', async () => {
29
+ const t = (ms) => new Promise(r => setTimeout(r, ms));
30
+ // example: cycle whatever component is under test
31
+ const d = document.querySelector('cart-drawer');
32
+ await t(2000); d.open(); await t(2500); d.close(); await t(1500); d.open();
33
+ });
34
+ }
35
+ ```
36
+
37
+ - **App-switch nudge** (forces visibility/viewport recalc):
38
+ `xcrun simctl launch booted com.apple.Preferences; sleep 2; xcrun simctl launch booted com.apple.mobilesafari`
39
+
40
+ ## Measuring screenshots
41
+
42
+ Screenshots are 3x scale on Pro-class devices — divide pixels by 3 for CSS px.
43
+ Pixel-scan with PIL rather than eyeballing:
44
+
45
+ ```python
46
+ from PIL import Image
47
+ im = Image.open('/tmp/shot.png').convert('RGB')
48
+ # find rows containing the element's known background colour, measure the band
49
+ ```
50
+
51
+ Comparing the measured painted box against the element's expected border-box
52
+ identifies which box was painted: a background at exactly content-box size
53
+ means padding wasn't painted (stale layer or native form-control painter).
54
+
55
+ ## Calibrating viewport units
56
+
57
+ The simulator reaches your Mac's `localhost` directly. Serve a test page
58
+ (`python3 -m http.server 8899`) with fixed-position divs of `100svh`,
59
+ `100dvh`, `100lvh`, `100%` height and a `bottom: 0` marker, screenshot, and
60
+ measure where each ends. This settles "which viewport is this unit tracking"
61
+ arguments with data.
62
+
63
+ ## Shopify-specific gotchas
64
+
65
+ - Preview/development themes show the **Draft bar**, which overlays the
66
+ bottom ~46pt of the viewport and covers bottom-anchored CTAs — it looks
67
+ exactly like a layout bug. Append `&pb=0` to hide it before judging.
68
+ - Add items to the cart with
69
+ `openurl booted ".../cart/add?id=<variant>&quantity=1&return_to=/cart"`
70
+ (a `/cart/<variant>:1` permalink jumps straight to checkout — not useful).
71
+
72
+ ## When this level is not enough
73
+
74
+ If the bug needs real touch gestures, device GPU behavior, or you have the
75
+ broken state live on someone's phone, use references/device.md. For fast CSS iteration first, use
76
+ references/playwright.md.
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ """Evaluate JavaScript in a live Safari tab on a USB-connected iOS device.
3
+
4
+ Usage:
5
+ python device_eval.py <url-substring> ['<js-expression>']
6
+
7
+ The expression runs in the page and its return value is printed. Wrap DOM
8
+ reads in an IIFE returning JSON.stringify(...) for structured output.
9
+ Defaults to a generic element diagnostic if no expression is given.
10
+
11
+ Requires: pip install pymobiledevice3 (Python >= 3.10)
12
+ Phone: Settings -> Safari -> Advanced -> Web Inspector ON, tab foreground,
13
+ screen unlocked, and no other inspector attached (close Mac Safari Develop).
14
+ """
15
+ import asyncio
16
+ import sys
17
+ import uuid
18
+
19
+ from pymobiledevice3.lockdown import create_using_usbmux
20
+ from pymobiledevice3.services.webinspector import WebinspectorService
21
+ from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
22
+ from pymobiledevice3.services.web_protocol.session_protocol import SessionProtocol
23
+
24
+ DEFAULT_EXPR = r"""(() => {
25
+ return JSON.stringify({
26
+ href: location.href.slice(0, 120),
27
+ theme: (window.Shopify && Shopify.theme) ? Shopify.theme : undefined,
28
+ vv: { h: Math.round(visualViewport.height), ih: innerHeight },
29
+ });
30
+ })()"""
31
+
32
+
33
+ async def attach(inspector, match):
34
+ pages = await inspector.get_open_application_pages(timeout=2)
35
+ ap = next((p for p in pages if match in (getattr(p.page, 'web_url', '') or '')), None)
36
+ if ap is None:
37
+ urls = [getattr(p.page, 'web_url', None) for p in pages]
38
+ raise SystemExit(f'no tab matching {match!r}; open tabs: {urls}')
39
+ session_id = str(uuid.uuid4()).upper()
40
+ # wait_target=True is required on iOS 17+ (Target-domain wrapped protocol)
41
+ session = await asyncio.wait_for(
42
+ InspectorSession.create(
43
+ SessionProtocol(inspector, session_id, ap.application, ap.page, method_prefix=""),
44
+ wait_target=True),
45
+ timeout=10)
46
+ await session.runtime_enable()
47
+ return session
48
+
49
+
50
+ async def main():
51
+ match = sys.argv[1] if len(sys.argv) > 1 else 'http'
52
+ expr = sys.argv[2] if len(sys.argv) > 2 else DEFAULT_EXPR
53
+ lockdown = await create_using_usbmux()
54
+ inspector = WebinspectorService(lockdown=lockdown)
55
+ async with inspector:
56
+ session = await attach(inspector, match)
57
+ result = await asyncio.wait_for(session.runtime_evaluate(expr, return_by_value=True), timeout=15)
58
+ print(result)
59
+
60
+
61
+ if __name__ == '__main__':
62
+ asyncio.run(main())
@@ -0,0 +1,99 @@
1
+ #!/usr/bin/env python3
2
+ """Run stepped repair experiments on a live broken page while a human watches
3
+ the device. Each step is announced with a colored banner injected into the
4
+ page; the watcher reports which step number visually fixed the element.
5
+
6
+ Usage:
7
+ python device_experiment.py <url-substring> [target-css-selector]
8
+
9
+ Edit STEPS for the element/bug at hand. The winning step becomes the
10
+ permanent fix, wired into the code path that precedes the broken state.
11
+ """
12
+ import asyncio
13
+ import sys
14
+ import uuid
15
+
16
+ from pymobiledevice3.lockdown import create_using_usbmux
17
+ from pymobiledevice3.services.webinspector import WebinspectorService
18
+ from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
19
+ from pymobiledevice3.services.web_protocol.session_protocol import SessionProtocol
20
+
21
+ TARGET = sys.argv[2] if len(sys.argv) > 2 else 'body'
22
+
23
+ STEPS = [
24
+ ("STEP 1: banner only (sanity)", "void 0"),
25
+ ("STEP 2: opacity nudge", f"""
26
+ (() => {{ const el = document.querySelector('{TARGET}'); if (!el) return;
27
+ el.style.opacity = '0.99';
28
+ requestAnimationFrame(() => {{ el.style.opacity = ''; }}); }})()"""),
29
+ ("STEP 3: own compositing layer", f"""
30
+ (() => {{ const el = document.querySelector('{TARGET}'); if (!el) return;
31
+ el.style.transform = 'translateZ(0)'; }})()"""),
32
+ ("STEP 4: display rebuild", f"""
33
+ (() => {{ const el = document.querySelector('{TARGET}'); if (!el) return;
34
+ el.style.display = 'none'; void el.offsetHeight; el.style.display = ''; }})()"""),
35
+ ("STEP 5: reflow via class re-toggle", f"""
36
+ (() => {{ const el = document.querySelector('{TARGET}'); if (!el) return;
37
+ const p = el.parentElement; p.classList.add('td-exp-retoggle');
38
+ void p.offsetHeight; p.classList.remove('td-exp-retoggle'); }})()"""),
39
+ ("STEP 6: scroll nudge", """
40
+ (() => { const s = document.scrollingElement; s.scrollTop += 1; s.scrollTop -= 1; })()"""),
41
+ ]
42
+
43
+ BANNER = """
44
+ (() => {
45
+ let el = document.getElementById('td-exp-banner');
46
+ if (!el) {
47
+ el = document.createElement('div');
48
+ el.id = 'td-exp-banner';
49
+ el.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:2147483647;padding:18px;font:bold 24px -apple-system;text-align:center;color:#fff;';
50
+ document.body.appendChild(el);
51
+ }
52
+ el.style.background = COLOR;
53
+ el.textContent = LABEL;
54
+ })()
55
+ """
56
+
57
+ COLORS = ['#c0392b', '#d35400', '#f39c12', '#27ae60', '#2980b9', '#8e44ad']
58
+
59
+
60
+ async def main():
61
+ match = sys.argv[1] if len(sys.argv) > 1 else 'http'
62
+ lockdown = await create_using_usbmux()
63
+ inspector = WebinspectorService(lockdown=lockdown)
64
+ async with inspector:
65
+ pages = await inspector.get_open_application_pages(timeout=2)
66
+ ap = next((p for p in pages if match in (getattr(p.page, 'web_url', '') or '')), None)
67
+ if ap is None:
68
+ raise SystemExit(f'no tab matching {match!r}')
69
+ session_id = str(uuid.uuid4()).upper()
70
+ session = await asyncio.wait_for(
71
+ InspectorSession.create(
72
+ SessionProtocol(inspector, session_id, ap.application, ap.page, method_prefix=""),
73
+ wait_target=True),
74
+ timeout=10)
75
+ await session.runtime_enable()
76
+
77
+ async def banner(color, label):
78
+ await session.runtime_evaluate(BANNER.replace('COLOR', repr(color)).replace('LABEL', repr(label)))
79
+
80
+ # Lead-in so the watcher can reproduce the broken state first
81
+ for remaining in range(20, 0, -5):
82
+ await banner('#111', f'BREAK IT NOW — reproduce the bug. Steps start in {remaining}s')
83
+ print(f'lead-in {remaining}s', flush=True)
84
+ await asyncio.sleep(5)
85
+
86
+ for i, (label, code) in enumerate(STEPS):
87
+ await banner(COLORS[i % len(COLORS)], label + ' — fixed?')
88
+ await session.runtime_evaluate(code)
89
+ print('ran', label, flush=True)
90
+ await asyncio.sleep(5)
91
+
92
+ await banner('#111', 'DONE — report which step fixed it')
93
+ await asyncio.sleep(6)
94
+ await session.runtime_evaluate("document.getElementById('td-exp-banner')?.remove()")
95
+ print('done', flush=True)
96
+
97
+
98
+ if __name__ == '__main__':
99
+ asyncio.run(main())
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env python3
2
+ """Capture a Page.snapshotRect PNG from a live Safari tab on a USB device.
3
+
4
+ Usage:
5
+ python device_snapshot.py <url-substring> [out.png] [x y w h]
6
+
7
+ IMPORTANT: the snapshot repaints from the render tree. If the snapshot looks
8
+ CORRECT while the device screen looks BROKEN, the bug is a stale compositor
9
+ texture (paint bug), not a layout bug — fix with a paint invalidation (e.g.
10
+ opacity nudge), not with CSS.
11
+ """
12
+ import asyncio
13
+ import base64
14
+ import json
15
+ import sys
16
+ import uuid
17
+
18
+ from pymobiledevice3.lockdown import create_using_usbmux
19
+ from pymobiledevice3.services.webinspector import WebinspectorService
20
+ from pymobiledevice3.services.web_protocol.inspector_session import InspectorSession
21
+ from pymobiledevice3.services.web_protocol.session_protocol import SessionProtocol
22
+
23
+
24
+ async def main():
25
+ match = sys.argv[1] if len(sys.argv) > 1 else 'http'
26
+ out = sys.argv[2] if len(sys.argv) > 2 else 'snapshot.png'
27
+ x, y, w, h = (int(a) for a in sys.argv[3:7]) if len(sys.argv) > 6 else (0, 0, 430, 932)
28
+
29
+ lockdown = await create_using_usbmux()
30
+ inspector = WebinspectorService(lockdown=lockdown)
31
+ async with inspector:
32
+ pages = await inspector.get_open_application_pages(timeout=2)
33
+ ap = next((p for p in pages if match in (getattr(p.page, 'web_url', '') or '')), None)
34
+ if ap is None:
35
+ raise SystemExit(f'no tab matching {match!r}')
36
+ session_id = str(uuid.uuid4()).upper()
37
+ session = await asyncio.wait_for(
38
+ InspectorSession.create(
39
+ SessionProtocol(inspector, session_id, ap.application, ap.page, method_prefix=""),
40
+ wait_target=True),
41
+ timeout=10)
42
+
43
+ mid = await session.send_message_to_target({'method': 'Page.enable', 'params': {}})
44
+ await asyncio.wait_for(session.receive_response_by_id(mid), timeout=10)
45
+
46
+ mid = await session.send_message_to_target(
47
+ {'method': 'Page.snapshotRect',
48
+ 'params': {'x': x, 'y': y, 'width': w, 'height': h, 'coordinateSystem': 'Viewport'}})
49
+ res = await asyncio.wait_for(session.receive_response_by_id(mid), timeout=15)
50
+ # iOS 17+ wraps responses in the Target domain — unwrap
51
+ if res.get('method') == 'Target.dispatchMessageFromTarget':
52
+ res = json.loads(res['params']['message'])
53
+ data = res.get('result', res).get('dataURL', '')
54
+ if not data.startswith('data:image/png;base64,'):
55
+ raise SystemExit(f'snapshot failed: {json.dumps(res)[:200]}')
56
+ with open(out, 'wb') as f:
57
+ f.write(base64.b64decode(data.split(',', 1)[1]))
58
+ print('saved', out)
59
+
60
+
61
+ if __name__ == '__main__':
62
+ asyncio.run(main())
@@ -33,8 +33,8 @@ Use the script output to identify changed files, then inspect the relevant diffs
33
33
  Prioritize:
34
34
 
35
35
  - User-facing behavior changes
36
- - Theme setting/schema changes
37
- - CSS or markup changes that affect storefront rendering
36
+ - CMS setting/schema changes
37
+ - CSS or markup changes that affect rendering
38
38
  - Renamed files and vendor-theme hotspots
39
39
 
40
40
  Do not summarize the `docs/changes.md` edit itself as part of the branch work. If the branch contains unrelated skill or tooling files, either omit them from the changelog entry or separate them clearly when they are relevant to the project's maintenance history.
@@ -57,7 +57,7 @@ Guidelines:
57
57
  - Make the title describe the feature or fix, not the branch name.
58
58
  - Write `Purpose` in plain language with outcome-focused bullets.
59
59
  - Use the `Files changed` table to explain why each file matters.
60
- - Call out non-`td-` theme or vendor files in a separate subsection when they are upgrade-sensitive.
60
+ - Call out non-`td-` theme or vendor files in a separate subsection when in a Shopify project.
61
61
  - Mention renamed files explicitly.
62
62
  - Keep `Upgrade impact` brief and concrete.
63
63
  - Use `Notes` for implementation details, edge cases, or assumptions.