[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6J3Y8C7C8S9SWXNCGJK99W2":3,"skill-same:01M6J3Y8C7C8S9SWXNCGJK99W2":118},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":31,"publishedAt":58,"linkOnly":59,"markdownContent":60,"markdownBody":61,"frontMatter":62,"refName":63,"commitSha":64,"skillDirectory":65,"forksCount":14,"repositoryHtmlUrl":66,"repositoryHomepageUrl":6,"ownerHtmlUrl":67,"ownerBlogUrl":6,"ownerDescription":68,"updatedAt":69,"install":70,"resources":83},"01M6J3Y8C7C8S9SWXNCGJK99W2","click-test",null,"Structural DOM audit of a live app — walk every interactive element and\nassert semantic-HTML contracts (duplicate IDs, orphan labels, inputs without\nnames, ARIA misuse, heading hierarchy, missing alt text, undersized touch\ntargets). Complements \u002Fpersona-test: persona-test catches narrative UX issues\na real user would hit; click-test catches structural issues that hide in\nJS-rendered surfaces and silently break assistive tech, form submissions, or\nReact reconciliation. Drives a browser via the shared driver contract; optional\n--with-modals opens each modal\u002Fdropdown and rescans; device presets and\nmatrix mode cover responsive breakpoints.\nTriggers on: \"click test\", \"click-test\", \"structural audit\", \"DOM audit\",\n\"accessibility audit\", \"duplicate ID check\", \"walk every element\",\n\"audit the DOM\", \"structural QA\", \"\u002Fclick-test\".\nFull command syntax: see the Usage section in this skill.","Lbstrydom\u002Fclaude-engineering-skills","Lbstrydom","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F77991486?v=4","01M6J3X4N2T0FTZT1RKFXDVS0K","Louis Strydom","01M6J3X4SXV0BXWDMXPW90D8BK",0,"MIT","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002FSKILL.md",[18,23,26],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000008","测试与质量","SCENARIO","使用场景",{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000009","前端开发",{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57],"click test","click","test","structural","hierarchy","labels","break","matrix","mode","narrative","element","app","live","orphan","responsive","full","shared","reconciliation","command","text","persona","ids","browser","qa","issues","react","2026-11-10 10:12:30",false,"---\nname: click-test\ndescription: |\n  Structural DOM audit of a live app — walk every interactive element and\n  assert semantic-HTML contracts (duplicate IDs, orphan labels, inputs without\n  names, ARIA misuse, heading hierarchy, missing alt text, undersized touch\n  targets). Complements \u002Fpersona-test: persona-test catches narrative UX issues\n  a real user would hit; click-test catches structural issues that hide in\n  JS-rendered surfaces and silently break assistive tech, form submissions, or\n  React reconciliation. Drives a browser via the shared driver contract; optional\n  --with-modals opens each modal\u002Fdropdown and rescans; device presets and\n  matrix mode cover responsive breakpoints.\n  Triggers on: \"click test\", \"click-test\", \"structural audit\", \"DOM audit\",\n  \"accessibility audit\", \"duplicate ID check\", \"walk every element\",\n  \"audit the DOM\", \"structural QA\", \"\u002Fclick-test\".\n  Full command syntax: see the Usage section in this skill.\n---\n\n> **Worktree preflight** — in a linked git worktree the synced tooling tree\n> `scripts\u002F.claude-skills\u002F` is absent — it is gitignored, so `git worktree add`\n> does not populate it, and every command below that uses it dies on a bare\n> `MODULE_NOT_FOUND`. Run `npm run skills:hydrate` first. If this repo defines\n> no such script, `references\u002Fworktree-preflight.md` holds the entry to add — ask\n> the user first, since it edits their `package.json`.\n\n## Usage\n\n```\nUsage:\n  \u002Fclick-test \u003Curl>                                         — scan the URL's initial render (default device: desktop)\n  \u002Fclick-test \u003Curl> --routes \"\u002Fa,\u002Fb,\u002Fc\"                     — scan multiple routes\n  \u002Fclick-test \u003Curl> --with-modals                           — also open + scan each modal\u002Fdropdown\n  \u002Fclick-test \u003Curl> --scope a11y|forms|ids|all              — focus the assertion set (default: all)\n  \u002Fclick-test \u003Curl> --device \u003Cpreset>                       — run in a specific device preset\n  \u002Fclick-test \u003Curl> --devices \"\u003Cp1>,\u003Cp2>,...\"               — matrix mode: run in each preset, merge findings\nDevice presets: desktop (default) | desktop-large | tablet | mobile | mobile-small\nExamples:\n  \u002Fclick-test http:\u002F\u002Flocalhost:3000\n  \u002Fclick-test https:\u002F\u002Fmyapp.railway.app --routes \"\u002F,\u002Fcellar,\u002Fadmin\"\n  \u002Fclick-test http:\u002F\u002Flocalhost:3000 --with-modals --scope a11y\n  \u002Fclick-test https:\u002F\u002Fmyapp.railway.app --device mobile                            — mobile only\n  \u002Fclick-test https:\u002F\u002Fmyapp.railway.app --devices \"desktop,mobile\" --with-modals   — cross-device coverage\n```\n\n# Click-Test — Structural DOM Audit\n\n> **This skill's last line is an end marker naming the skill between em-dashes, after\n> the Reference files table.** If that last line is not in your context (context\n> compaction keeps only the start of a skill), re-read\n> `.claude\u002Fskills\u002Fclick-test\u002FSKILL.md` in full before continuing — the later steps still apply.\n\nWalk every interactive element on each target route and assert the structural\nHTML contract. **Not a persona test** — the mechanical sibling of\n`\u002Fpersona-test`; run both, their findings rarely overlap.\n→ `references\u002Fscan-rules-detail.md` §click-test vs \u002Fpersona-test.\n\n---\n\n## Phase 0 — Parse Arguments\n\nParse `$ARGUMENTS`:\n\n\u003C!-- host-contract: input-acquisition; grammar=path+flags; empty=ask-and-stop -->\n\n**Where `$ARGUMENTS` comes from** — orchestrator-supplied input first, else\nthe host's verbatim invocation suffix, else the span of the user's **current**\nmessage naming this skill or its subject. Never inferred from surrounding\nconversation. This site is `path+flags`; on empty input, a URL is required — ask for it and stop. Never reuse a URL from earlier in the conversation; scanning the wrong origin is a real action against someone's site.\nFull contract: `references\u002Finput-acquisition.md`.\n\n1. **url** — first URL-shaped token (or `PERSONA_TEST_APP_URL` env). Validate\n   with `new URL(value)` — reject non-URL with usage error.\n2. **routes** — `--routes \"\u002Fa,\u002Fb,\u002Fc\"` (comma-separated). When omitted, the\n   default target is **the base URL itself** (preserves any path prefix —\n   `https:\u002F\u002Fhost\u002Fapp` is scanned, not `https:\u002F\u002Fhost\u002F`). When provided, each\n   token is resolved as `new URL(routeToken, base).href` — supports absolute\n   routes, root-relative routes (explicit `\u002F` means origin-root), and\n   path-relative routes. Empty\u002Fwhitespace tokens dropped. Duplicates after\n   normalisation dropped. Examples:\n   - `--routes` omitted, base `https:\u002F\u002Fhost\u002Fapp` → scans `https:\u002F\u002Fhost\u002Fapp`\n   - `--routes \"\u002F\"` against base `https:\u002F\u002Fhost\u002Fapp` → scans `https:\u002F\u002Fhost\u002F`\n   - `--routes \"settings\"` against base `https:\u002F\u002Fhost\u002Fapp\u002F` → scans `https:\u002F\u002Fhost\u002Fapp\u002Fsettings`\n3. **with_modals** — `--with-modals` flag (default: off)\n4. **scope** — `--scope a11y|forms|ids|all` (default: `all`). Unknown value →\n   fail with usage.\n5. **ready_selector** — `--ready-selector \"\u003Ccss>\"` (optional). Default:\n   `[data-testid=app-ready], [data-click-test-ready=true]`. If neither\n   attribute exists in the app, **pass an explicit selector** that\n   uniquely identifies the loaded state — do not fall back to generic\n   `main` \u002F `#root` (those match on skeleton DOMs and produce false\n   \"scanned\" verdicts on still-loading pages).\n6. **ready_timeout_ms** — `--ready-timeout 8000` (default 8000).\n7. **force_cache_bust** — `--force-cache-bust` flag (default: off, see Phase 2).\n8. **max_triggers_per_route** — `--max-triggers 50` (default 50, Phase 4b only).\n9. **device \u002F viewport** — three mutually-exclusive ways to pick the viewport\n   (and adjacent emulation flags). Specify at most one; supplying two → fail\n   with usage:\n   - `--device \u003Cpreset>` — one of `desktop`, `desktop-large`, `tablet`,\n     `mobile`, `mobile-small` (canonical registry:\n     [`scripts\u002Flib\u002Fdevice-presets.mjs`](https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fscripts\u002Flib\u002Fdevice-presets.mjs)).\n     Sets viewport + `isMobile` + `hasTouch` flags. Unknown name → fail.\n   - `--devices \"\u003Cp1>,\u003Cp2>,...\">` — **matrix mode**. Each preset runs the full\n     route crawl independently; findings are tagged with `device` and merged.\n     Cost is multiplicative — be explicit, this is opt-in.\n   - `--viewport \u003CW>x\u003CH>` — legacy direct viewport (W and H in `[320, 4096]`).\n     Synthesises a `custom` device record with `isMobile\u002FhasTouch` inferred\n     from width (`\u003C768 → mobile\u002Ftouch`). Kept for back-compat; prefer `--device`.\n   - None of the above → default device = `desktop` (1280×720). Identical to\n     today's behaviour for callers passing nothing.\n\n   The selected device is applied via a single `browser_resize` call before\n   the first `browser_navigate` of each route (matrix mode resizes once per\n   device-pass). UA emulation, real touch events, and DPR scaling are NOT\n   provided — MCP browser drivers don't expose context-level launch options. For\n   full emulation, use `\u002Fpersona-test --mode consistency` (code-driven Playwright).\n\nRequired: `url`. If missing, output usage and STOP. Unknown flags → fail\nwith usage. Duplicate flags → last wins, log a stderr warning.\n\n---\n\n## Phase 0b — Run Contract (recorded in report)\n\nCapture these defaults at the start of each session and include them\nverbatim in the Phase 6 report so two runs against the same commit can be\ncompared:\n\n| Field | Default | Override |\n|---|---|---|\n| Device | `desktop` (1280×720, touch=false) | `--device \u003Cpreset>` \u002F `--devices \"\u003Clist>\"` (matrix) \u002F `--viewport WxH` (legacy) |\n| Browser | whatever the selected driver provides (Playwright MCP = Chromium) | — |\n| Locale \u002F timezone | tool default (do NOT randomise) | — |\n| Auth | none by default — public surface only | pre-authenticate the shared MCP session; see \"Auth-gated routes\" below |\n| Network idle | wait for `load` + ready-selector or 8000ms | `--ready-timeout` |\n\n**Auth-gated routes**: after each navigation, BEFORE running the static\nscan, check:\n\n1. **Cross-origin redirect** (OAuth \u002F SSO): if `new URL(location.href).origin !==\n   new URL(targetUrl).origin`, classify as `coverageStatus: \"auth-required\"`\n   regardless of DOM content. Catches OAuth providers (Google, Okta, Auth0)\n   that redirect to a third-party domain — never scan accessibility of those\n   pages and attribute findings to the audited app.\n2. **Same-origin password form**: if URL is same-origin AND DOM contains\n   `[type=password]`, classify as `coverageStatus: \"auth-required\"`. Catches\n   first-party login walls.\n\nclick-test does not log in itself. If primary surfaces need a login,\npre-authenticate the shared driver connection via `--storage-state` before\nrunning — see [`references\u002Fauth-bootstrap.md`](references\u002Fauth-bootstrap.md).\nThe OVERALL verdict\nbecomes at most `Incomplete` (never `Clean`) when any route is\n`auth-required`.\n→ `references\u002Fscan-rules-detail.md` §Auth-gated routes — read when a target is login-gated.\n\n---\n\n## Phase 1 — Detect Browser Tool\n\nResolve the driver through the **one detection oracle** —\n[`references\u002Fbrowser-tool-detection.md`](references\u002Fbrowser-tool-detection.md).\nDo not restate its ladder here; click-test declares only what it needs.\n\n**Minimum capability set**: `navigate`, `evaluate`, `click`, `keyboard`\n(Escape press), `wait`, `currentUrl`. Verify each before the first scan and log\nthe chosen driver plus the capabilities matched.\n\n**`evaluate` is non-negotiable and it is what excludes static drivers.** The DOM\nscan *is* a `page.evaluate` call, so a fetch-and-parse driver can never serve\nclick-test however well it parses HTML. There is no degraded mode here: an unmet\nset means abort before scanning with\n`[BLOCKED] \u003Cdriver> missing capability: \u003Ccap>` — never a clean empty pass, which\nwould report \"no findings\" from a page nobody scanned.\n\n---\n\n## Phase 2 — Pre-flight Cache-bust (own-app only by default)\n\nService workers silently serve stale bundles, masking real deploys. The\ncache-bust is **mandatory for own-app hostnames**, where clearing user state\ncosts nothing. **For external URLs**: do NOT cache-bust by default — clearing\n`serviceWorker` + `caches` on a third-party origin can log out the operator,\nremove offline data, or alter the very state being measured. Require the\nexplicit `--force-cache-bust` flag — and even then, warn loudly on stderr.\n\n| Hostname class | Default behaviour |\n|---|---|\n| Own-app (localhost \u002F `*.railway.app` \u002F `*.vercel.app` \u002F `*.netlify.app` \u002F `*.local`) | Cache-bust runs unconditionally |\n| External | Skip cache-bust. `--force-cache-bust` overrides with stderr warning |\n| Any driver without `evaluate` | Unreachable — click-test aborts `blocked` before this point (Phase 1) |\n\nCache-bust script (when it runs). Both APIs (`serviceWorker`, `caches`) can\nbe undefined globally in non-secure contexts (HTTP-only test URLs) —\nreferencing them throws `ReferenceError` BEFORE optional chaining is\nevaluated. Always check `typeof` first:\n\n```js\n\u002F\u002F browser_evaluate\n(async () => {\n  let unregistered = 0, cachesDeleted = 0;\n  if (typeof navigator !== 'undefined' && navigator.serviceWorker) {\n    const regs = await navigator.serviceWorker.getRegistrations().catch(() => []);\n    await Promise.all(regs.map(r => r.unregister().catch(() => {})));\n    unregistered = regs.length;\n  }\n  if (typeof caches !== 'undefined') {\n    const keys = await caches.keys().catch(() => []);\n    await Promise.all(keys.map(k => caches.delete(k).catch(() => {})));\n    cachesDeleted = keys.length;\n  }\n  return { unregistered, cachesDeleted };\n})()\n```\n\nThen `browser_navigate(url)` again to force a fresh fetch. Log result:\n`[cache-bust] unregistered \u003Cn> SW, cleared \u003Cn> caches (own-app)` or\n`[cache-bust] skipped (external host; pass --force-cache-bust to override)`.\n\nRecord `cacheBustMode: \"own-app\" | \"forced\" | \"skipped-external\" | \"n\u002Fa\"`\nin the run contract for the Phase 6 report.\n\n---\n\n## Phase 3 — Crawl Routes\n\n### Get the device-pass contract (required; do not skip)\n\nBefore any browser work, call:\n\n```bash\nnode scripts\u002Flib\u002Fdevice-presets.mjs prep-matrix\n```\n\nOptional flags — add at most one, matching what your `$ARGUMENTS` contained (none if neither flag was passed): `--device PRESET`, `--devices \"LIST\"`, `--viewport WxH`.\n\n\u003C!-- host-contract: input-acquisition; grammar=path+flags; empty=default -->\n_This site: `path+flags` — neither device flag present means a single `desktop` pass — do not synthesise a matrix._\n\nThe CLI returns `{kind: \"click-test-prep\", matrixMode, totalPasses, passes[]}`,\neach pass carrying `device`, `expectedFirstMcpCall` and `logLine`\n(→ `references\u002Fscan-rules-detail.md` §Device prep contract for the full shape).\n\nThe LLM does NOT pick device order, dimensions, or pass count — it walks\n`passes` in array order, executing each `expectedFirstMcpCall` verbatim.\nMutual-exclusion conflicts (e.g. `--device` + `--devices`) cause the CLI\nto exit non-zero with the error on stderr; surface that, do not proceed.\n\n### Device pass loop\n\nFor each `pass` in `contract.passes` (in array order):\n\n1. Echo `pass.logLine` to stderr.\n2. Call `browser_resize` with `pass.expectedFirstMcpCall.args` verbatim\n   — once per device-pass, BEFORE the first navigate.\n3. Tag every finding from this pass with `device: pass.device.name`.\n4. Run the per-route loop below.\n\nCache-bust (Phase 2) runs **once at session start**, not per device-pass —\nservice-worker state is global to the browser context.\n\n### Per-route loop (within a device pass)\n\nFor each route in `routes`:\n\n1. `browser_navigate({url: new URL(route, base).href})` — never string-concat\n2. **Readiness protocol** (replaces vague \"stable string\" wait):\n   a. Wait for `load` event (most MCPs do this implicitly on navigate).\n   b. Poll: ready-selector matches AND `document.querySelector('[aria-busy=\"true\"], [role=\"progressbar\"]')` returns null.\n   c. Timeout after `--ready-timeout` ms (default 8000).\n   d. If timeout fires, classify the route as `coverageStatus: \"readiness-timeout\"`\n      and skip its scan — do NOT scan a skeleton DOM.\n3. **Route execution status** — independent of finding count. Record one of:\n   - `scanned` — readiness met AND scanner output validated. (May still have findings.)\n   - `auth-required` — landed on login (URL changed AND `[type=password]` present)\n   - `navigation-error` — `browser_navigate` threw (rendered app-level 404\u002F500 pages\n     count as `scanned` unless the tool exposes a real response status and it's 4xx\u002F5xx)\n   - `readiness-timeout` — readiness selector never matched within timeout\n   - `scanner-error` — `browser_evaluate` threw or returned schema-invalid shape\n   - `skipped-external-anchor` — route resolved to a different origin\n4. If state is `scanned`, run **Phase 4 — Static scan** (one pass per settled DOM state — see Reminders)\n5. If `with_modals` AND state is `scanned`, run **Phase 4b — Dynamic-surface scan**\n6. Record findings tagged with `{device, route, coverageStatus, via}`; record per-route\n   element counts and dynamic-surface counts in the run metrics (Phase 4 schema).\n\n---\n\n## Phase 4 — Static Scan\n\n`browser_evaluate` with the scanner from\n[`references\u002Fdom-scanner.md`](references\u002Fdom-scanner.md). The scanner returns\na single result object — **validate before use** (a hostile or malformed\npage could return anything from `browser_evaluate`):\n\n```js\n\u002F\u002F pseudo-Zod (canonical contract — runner enforces, report consumes)\nClickTestFinding = {\n  kind: enum(\"duplicate-id\", \"orphan-label\", \"input-no-name\",\n             \"button-no-name\", \"link-no-name\", \"form-field-no-name\",\n             \"duplicate-aria-label\", \"aria-hidden-focusable\",\n             \"empty-link\", \"heading-skip\", \"img-no-alt\",\n             \"small-touch-target\", \"positive-tabindex\"),  \u002F\u002F 13 kinds\n  severity: enum(\"P0\",\"P1\",\"P2\",\"P3\"),   \u002F\u002F EFFECTIVE severity — capped to P3 when !perceivable\n  declaredSeverity: enum(\"P0\",\"P1\",\"P2\",\"P3\"),  \u002F\u002F the kind's intrinsic severity, pre-cap\n  perceivable: boolean,      \u002F\u002F was the element RENDERED in the captured state?\n  selector: string.max(500),\n  snippet: string.max(200),  \u002F\u002F outerHTML truncated; redact before persist\n  detail: string.max(500),\n  device: string,            \u002F\u002F preset name (\"desktop\", \"mobile\", …) or \"custom\"\n}\nClickTestScanResult = {\n  schemaVersion: literal(1),\n  routeUrl: string,                  \u002F\u002F the final navigated URL\n  device: string,                    \u002F\u002F matches each finding's device tag\n  elementsScanned: number,           \u002F\u002F document.querySelectorAll('*').length at scan time\n  interactiveElementsScanned: number, \u002F\u002F count of button\u002Fa\u002Finput\u002Fselect\u002Ftextarea\u002F[role=button|link]\n  findings: ClickTestFinding[].max(999),  \u002F\u002F reject >999 — flags scanner-error\n  shadowGapCount: number,            \u002F\u002F open shadow roots not traversed\n  iframeGapCount: number,            \u002F\u002F iframes not traversed\n}\n```\n\n**Device-pass aggregation** (when `--devices` produces multiple device passes):\neach finding's `device` field is set by the runner from the active pass — the\nscanner does not need to know. Findings dedupe by `{device, route, via, kind, selector}` —\nthe same duplicate-id on `mobile` AND `desktop` is reported twice (correctly:\nit's two regressions that may have different fix sites if responsive CSS\nhides one). The Phase 6 report's PER-DEVICE COVERAGE table surfaces which\ndevice flagged which.\n\n`small-touch-target` is **device-sensitive**: when both desktop and mobile\npasses ran, downgrade desktop-pass `small-touch-target` findings by one\nseverity (P2 → P3) — the mobile pass is the authoritative read.\n\nIf validation fails, classify the route as `coverageStatus: \"scanner-error\"`\nand skip its findings — never persist or report unvalidated output.\n\n**Per-route aggregation** when `--with-modals` produces multiple scan results\n(one static + one per opened modal): the per-route record stores the static\nscan's `elementsScanned` \u002F `interactiveElementsScanned` as the route totals\n(modals are subsets, not additions). For `shadowGapCount` \u002F `iframeGapCount`,\ntake the **max** observed across passes (NOT sum — each modal pass re-scans\nthe whole DOM, so the static gaps would be double-counted). `findings` are\nunioned and deduped by `{device, route, via, kind, selector}`.\nEach finding's `via` field records its origin (`static` or `modal:\u003Caccessible-name>`).\n\n**Redaction**: before printing or persisting any `snippet`, run it through\nthe existing redactor (`scripts\u002Flib\u002Fredact.mjs::redact` for string-typed\ncontent; use `redactObject` if the runner ever boxes the snippet inside a\nlarger payload). DOM `outerHTML` can contain emails, tokens in `data-*`\nattributes, hidden form values, customer content. The default scanner\nalready truncates to 200 chars; redaction is the second layer.\n\n### Finding taxonomy (canonical rule registry)\n\n13 rules, 1:1 with the `kind` enum above:\n\n| Kind | Default severity | In `ids` | In `forms` | In `a11y` |\n|---|---|---|---|---|\n| `duplicate-id` | P0 | ✓ | ✓ | ✓ |\n| `orphan-label` | P0 | — | ✓ | ✓ |\n| `input-no-name` | P0 | — | ✓ | ✓ |\n| `button-no-name` | P0 | — | — | ✓ |\n| `link-no-name` | P1 | — | — | ✓ |\n| `form-field-no-name` | P1 \u002F **P3 when the form has no `action`** | ✓ | ✓ | — |\n| `duplicate-aria-label` | P2 *(see note)* | — | — | ✓ |\n| `aria-hidden-focusable` | P1 | — | — | ✓ |\n| `empty-link` | P1 | — | — | ✓ |\n| `heading-skip` | P2 | — | — | ✓ |\n| `img-no-alt` | P2 | — | — | ✓ |\n| `small-touch-target` | P2 | — | — | ✓ |\n| `positive-tabindex` | P2 | — | — | ✓ |\n\n`button-no-name` and `link-no-name` are separate kinds (one rule per kind)\nso the severity is unambiguous in the enum. The scanner emits one or the\nother based on element type — never both for the same node.\n\n`--scope all` → keep everything. `--scope \u003Cname>` → keep rows with `✓` in\nthat column. Invalid scope → fail with usage.\n\n`duplicate-aria-label` note: high false-positive rate on grid\u002Fcard layouts,\nso the scanner only fires it within the same `role` + parent region.\n→ `references\u002Fscan-rules-detail.md` §duplicate-aria-label FP note.\n\n---\n\n## Phase 4b — Dynamic-surface Scan (`--with-modals` only)\n\nModals mount duplicate IDs only while open, which the static scan cannot see\n(→ `references\u002Fscan-rules-detail.md` §Why `--with-modals` exists).\n\n### Trigger discovery (one evaluate per route, before clicking)\n\n```js\n\u002F\u002F browser_evaluate — returns DiscoveredTrigger[]\n\u002F\u002F DiscoveredTrigger = { idx, selector, role, accessibleName, regionKey,\n\u002F\u002F                       destructiveReason: string|null, disabled: bool,\n\u002F\u002F                       href: string|null, formAction: string|null }\n```\n\nThe runner stores this array, then iterates by `idx`. Before each click:\n\n1. `browser_evaluate` re-queries by the stored `selector` — if it returns\n   null or a different element (compared by `idx → selector → accessibleName`\n   triple), skip with `coverageStatus: \"trigger-stale\"`.\n2. If `destructiveReason` is non-null → skip with that reason.\n3. If `disabled === true` → skip with `\"disabled\"`.\n4. Otherwise proceed to click.\n\nThe trigger candidate set: `[aria-haspopup=\"true\"], [data-modal-trigger],\nbutton[aria-controls], [role=\"combobox\"]`.\n\n### Per-trigger flow\n\n1. **Safety classifier** — skip if the trigger is destructive (see below)\n2. **URL snapshot** — record `currentUrl` before click\n3. `browser_click(\u003Ctrigger>)`\n4. Wait for modal mount: poll for `[role=\"dialog\"]`, `\u003Cdialog[open]>`, or\n   `aria-expanded=\"true\"` on the trigger, up to 1000ms. Timeout → log\n   \"trigger opened nothing\" and skip.\n5. Re-run the scanner — tag findings with `via: \"modal:\u003Caccessible-name>\"`\n6. Close: `Escape` key → if dialog still mounted, click `[aria-label=\"Close\"]`\n   inside it → if still mounted, click outside the dialog at `(8, 8)`.\n7. **Verify closed AND URL unchanged** — if URL changed, treat as\n   navigation side-effect: re-navigate to the route's URL **then re-run\n   the Phase 3 readiness protocol** (don't assume the page is ready\n   immediately after navigation). Resume the trigger loop only once\n   readiness is met. If dialog still mounted after all 3 close attempts,\n   log `coverageStatus: \"modal-stuck\"` for this route and stop the\n   dynamic scan for that route.\n8. Continue to next trigger until **`--max-triggers`** (default 50) reached.\n   Excess triggers logged as `skipped: budget-exceeded`.\n\n### Safety classifier — destructive-trigger filter\n\nSkip a trigger if **any** of these match (case-insensitive, accent-folded):\n\n- Accessible name (computed: `aria-label`, `aria-labelledby`, visible text,\n  `title` — in that order) matches `\u002F^(delete|destroy|remove|purge|reset|sign\\s*out|log\\s*out|cancel\\s*subscription|deactivate|close\\s*account|drop|truncate|wipe)\u002Fi`\n- `data-destructive=\"true\"` or `data-action` value matches the regex above\n- `role=\"link\"` with `href` pointing to a different origin or matching\n  `\u002Flogout|signout|delete\u002Fi`\n- Inside a form whose `action` matches the destructive regex\n- `aria-disabled=\"true\"` or `[disabled]` (don't click disabled controls)\n\nLog each skipped trigger with reason: `skipped-destructive: \u003Caccessible-name> (matched: \u003Cpattern>)`.\n\n**Deduplication**: two triggers are dupes when they share `(accessibleName, role, closest [role=\"dialog\"]\u002Fsection ancestor)`. Skip dupes after the first.\n\n---\n\n## Phase 5 — Severity Model\n\nThe authoritative severity-per-kind mapping is the Phase 4 taxonomy table.\n→ `references\u002Fscan-rules-detail.md` §Severity intuition — read when a kind's\nseverity is disputed (incl. the `form-field-no-name` P1-vs-P3 action rule).\n\n### Perceivability cap (2026-07-30)\n\n`push()` runs every finding through `__isPerceivable(el)` — one call site, so all\n13 kinds get it. A finding on an element that is not **rendered** in the captured\nstate (`display:none`, `visibility:hidden`, `opacity:0`,\n`content-visibility:hidden`, `[inert]`, detached, or zero-size) is **demoted to\nP3** and tagged `perceivable:false`; `declaredSeverity` preserves what the kind\nwould otherwise have been.\n\n**Demote, do not drop.** A hidden element may become visible — that is exactly\nwhat `--with-modals` re-scans for — so dropping the finding destroys signal. The\ntag is **state-relative**: it says \"not perceivable in *this* capture\", never\n\"never perceivable\".\n\n**Filter the headline on `perceivable`,** and report capped findings separately\nso the count stays honest — 704 findings with 6 perceivable is a very different\nreport from 704 undifferentiated ones.\n\n→ `references\u002Fscan-rules-detail.md` §Perceivability — the field incident,\nthe release-scope note, and the canonical predicate source.\n\n### Touch targets are measured over activating regions\n\n`small-touch-target` measures **every region that activates the control**, not\njust the control's own border box. A wrapping `\u003Clabel>` — or one associated via\n`label[for=…]`, both covered by the spec's `el.labels` — is part of the tap\ntarget, because clicking it activates the control. Regions are contiguous (no\nbounding-box union); the rule passes on the first region ≥24×24 and reports the\nlargest; a control whose every region is undersized still fires.\n→ `references\u002Fscan-rules-detail.md` §Touch targets — field data and why.\n\nConfidence is not a click-test concept — assertions are deterministic. Either\nthe DOM violates the contract or it doesn't. If runs vary across same\nURL + commit, something non-deterministic (A\u002FB test, random ID generator,\nload-order race) is involved; flag that meta-finding itself.\n\n---\n\n## Phase 6 — Structured Report\n\nHeader (the Phase 0b run contract, verbatim):\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n  CLICK-TEST REPORT\n  URL: \u003Cbase>\n  Routes: \u003Clist>\n  Devices: \u003Cname(s)>   (e.g. \"mobile (390x844)\" or \"desktop, mobile\" for matrix)\n  Scope: \u003Cscope>   Modals: \u003Con|off>   Cache-bust: \u003Cown-app|forced|skipped-external|n\u002Fa>\n  Ready-selector: \u003Ccss>   Ready-timeout: \u003Cms>\n  Tool: \u003Cbrowser_tool> — \u003CN> elements scanned (across all device passes) — \u003Cduration>\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n```\n\nThen, in order:\n- **FINDINGS (\u003Ctotal>)** — per finding `[P\u003Cn>] \u003Ckind> \u003Csubject>` then Device,\n  Route, Via, Selector, Detail, Snippet (redacted).\n- **ROUTE COVERAGE** — one line per route: `\u003Croute> — \u003CcoverageStatus> —\n  \u003Cn> elements — \u003Cn> findings (P0:n, …)` or `skipped (\u003Creason>)`; under\n  `--devices`, grouped per `device=\u003Cname> (\u003CWxH>)` plus a **CROSS-DEVICE**\n  section (shared \u002F desktop-only \u002F mobile-only counts + one interpretation line).\n- **DYNAMIC SURFACES** (`--with-modals`) — per device+route: triggers total,\n  scanned, skipped-destructive, modal-stuck.\n- **OVERALL** — `\u003CBroken | Broken+Incomplete | Has issues | Incomplete | Clean>`\n  plus a one-sentence Reason.\n\n→ `references\u002Fphase6-output-sample.md` — read before rendering, for the full sample.\n\n```\nOVERALL VERDICT RULES (deterministic precedence — first match wins):\n  Coverage condition (referenced below):\n    \"covered\"     = every route coverageStatus=\"scanned\" AND\n                    shadowGapCount == 0 AND iframeGapCount == 0 AND\n                    no dynamic-scan failure\n                    (failure = modal-stuck\u002Fbudget-exceeded OR trigger-stale > 25% of triggers)\n    \"gaps\"        = NOT covered\n\n  1. Broken+Incomplete → any P0 finding AND gaps\n  2. Has issues+Incomplete → any P1\u002FP2 finding (no P0) AND gaps\n  3. Broken             → any P0 finding AND covered\n  4. Has issues         → any P1\u002FP2 finding (no P0) AND covered\n  5. Incomplete         → zero findings AND gaps\n  6. Clean              → zero findings AND covered\n\n  Issues are NEVER masked by coverage gaps — verdict 2 explicitly surfaces\n  the P1\u002FP2 + gap combination rather than letting it fall through to\n  \"Incomplete\" (which would hide the discovered defects).\n\n  Notes:\n    - \"Has issues\" applies regardless of P1 vs P2; the count distinction lives\n      in the FINDINGS block, not the verdict.\n    - The verdict precedence is independent of coverage — a Broken+Incomplete\n      run with one P0 and one auth-required route is more severe than either\n      alone and gets its own label so \u002Fship can gate appropriately.\n```\n\nSort findings P0 first, then by `kind` alphabetical so similar issues group.\n\n---\n\n## Phase 7 — Persistence (Out of Scope for v1)\n\nv1 does not persist findings: no `record-click-test` subcommand exists, so do\nnot invent one or claim a store write. The local Phase 6 report is the whole\noutput — print, read, fix, re-run.\n→ `references\u002Fv1-scope-and-deployment.md` — read when asked why, what v2 adds,\nwhich R3 audit concerns were deferred, or how this skill is deployed.\n\n---\n\n## Reminders\n\n- **One scanner pass per settled DOM state** — one static pass per route per\n  device, plus one pass per successfully opened dynamic surface in\n  `--with-modals` mode. Aggregate findings in JS using\n  `{device, route, via, kind, selector}` as the dedup key so a finding\n  present in both static + modal states (on the same device) is reported\n  once, but the same finding on two different devices is reported twice\n  (responsive CSS can cause a duplicate-id to surface on only one viewport).\n\n---\n\n## Reference files\n\n| File | Summary | Read when |\n|---|---|---|\n| `references\u002Finput-acquisition.md` | Where a skill's arguments come from on any host, and what to do when there are none. | Reading $ARGUMENTS on a host that does not substitute it, or deciding what empty input means at a site. |\n| `references\u002Fbrowser-tool-detection.md` | The browser-driver contract — capabilities, driver table, selection order, minimum sets, degraded\u002Fblocked evidence. | Phase 1 driver selection — resolving which driver to use, or diagnosing a `blocked` result. |\n| `references\u002Fauth-bootstrap.md` | Sanctioned auth-gated exploratory-testing pattern via MCP-server storageState, plus its connect-time-race escape hatch. | The target's primary surfaces need a login — pre-authenticate the shared driver connection before scanning, so `auth-required` routes are not coverage gaps. |\n| `references\u002Fdom-scanner.md` | The full browser_evaluate scanner JS — every assertion's implementation, selector-stringifier helpers, severity mapping. | About to run Phase 4 or Phase 4b. |\n| `references\u002Fscan-rules-detail.md` | Why the scan rules are what they are — perceivability, touch-target regions, severity intuition, FP notes, prep JSON. | A finding's severity or perceivability is disputed, a touch-target finding looks false, OR the prep contract shape is unclear. |\n| `references\u002Fphase6-output-sample.md` | Full Phase 6 output sample — finding entries, single-device and matrix ROUTE COVERAGE, CROSS-DEVICE, DYNAMIC SURFACES. | About to render the Phase 6 report. |\n| `references\u002Fv1-scope-and-deployment.md` | Why v1 has no persistence, the R3-deferred v2 concerns, and the maintainer deployment recipe + acceptance checks. | Asked about persistence, \u002Fship integration or v2 scope, OR deploying\u002Fsyncing this skill. |\n| `references\u002Fworktree-preflight.md` | Worktree preflight remedy — the skills:hydrate npm script to add when a linked worktree lacks the tooling tree. | A command dies with `MODULE_NOT_FOUND` in a linked worktree AND this repo has no `skills:hydrate` script. |\n\n— end of \u002Fclick-test —\n","> **Worktree preflight** — in a linked git worktree the synced tooling tree\n> `scripts\u002F.claude-skills\u002F` is absent — it is gitignored, so `git worktree add`\n> does not populate it, and every command below that uses it dies on a bare\n> `MODULE_NOT_FOUND`. Run `npm run skills:hydrate` first. If this repo defines\n> no such script, `references\u002Fworktree-preflight.md` holds the entry to add — ask\n> the user first, since it edits their `package.json`.\n\n## Usage\n\n```\nUsage:\n  \u002Fclick-test \u003Curl>                                         — scan the URL's initial render (default device: desktop)\n  \u002Fclick-test \u003Curl> --routes \"\u002Fa,\u002Fb,\u002Fc\"                     — scan multiple routes\n  \u002Fclick-test \u003Curl> --with-modals                           — also open + scan each modal\u002Fdropdown\n  \u002Fclick-test \u003Curl> --scope a11y|forms|ids|all              — focus the assertion set (default: all)\n  \u002Fclick-test \u003Curl> --device \u003Cpreset>                       — run in a specific device preset\n  \u002Fclick-test \u003Curl> --devices \"\u003Cp1>,\u003Cp2>,...\"               — matrix mode: run in each preset, merge findings\nDevice presets: desktop (default) | desktop-large | tablet | mobile | mobile-small\nExamples:\n  \u002Fclick-test http:\u002F\u002Flocalhost:3000\n  \u002Fclick-test https:\u002F\u002Fmyapp.railway.app --routes \"\u002F,\u002Fcellar,\u002Fadmin\"\n  \u002Fclick-test http:\u002F\u002Flocalhost:3000 --with-modals --scope a11y\n  \u002Fclick-test https:\u002F\u002Fmyapp.railway.app --device mobile                            — mobile only\n  \u002Fclick-test https:\u002F\u002Fmyapp.railway.app --devices \"desktop,mobile\" --with-modals   — cross-device coverage\n```\n\n# Click-Test — Structural DOM Audit\n\n> **This skill's last line is an end marker naming the skill between em-dashes, after\n> the Reference files table.** If that last line is not in your context (context\n> compaction keeps only the start of a skill), re-read\n> `.claude\u002Fskills\u002Fclick-test\u002FSKILL.md` in full before continuing — the later steps still apply.\n\nWalk every interactive element on each target route and assert the structural\nHTML contract. **Not a persona test** — the mechanical sibling of\n`\u002Fpersona-test`; run both, their findings rarely overlap.\n→ `references\u002Fscan-rules-detail.md` §click-test vs \u002Fpersona-test.\n\n---\n\n## Phase 0 — Parse Arguments\n\nParse `$ARGUMENTS`:\n\n\u003C!-- host-contract: input-acquisition; grammar=path+flags; empty=ask-and-stop -->\n\n**Where `$ARGUMENTS` comes from** — orchestrator-supplied input first, else\nthe host's verbatim invocation suffix, else the span of the user's **current**\nmessage naming this skill or its subject. Never inferred from surrounding\nconversation. This site is `path+flags`; on empty input, a URL is required — ask for it and stop. Never reuse a URL from earlier in the conversation; scanning the wrong origin is a real action against someone's site.\nFull contract: `references\u002Finput-acquisition.md`.\n\n1. **url** — first URL-shaped token (or `PERSONA_TEST_APP_URL` env). Validate\n   with `new URL(value)` — reject non-URL with usage error.\n2. **routes** — `--routes \"\u002Fa,\u002Fb,\u002Fc\"` (comma-separated). When omitted, the\n   default target is **the base URL itself** (preserves any path prefix —\n   `https:\u002F\u002Fhost\u002Fapp` is scanned, not `https:\u002F\u002Fhost\u002F`). When provided, each\n   token is resolved as `new URL(routeToken, base).href` — supports absolute\n   routes, root-relative routes (explicit `\u002F` means origin-root), and\n   path-relative routes. Empty\u002Fwhitespace tokens dropped. Duplicates after\n   normalisation dropped. Examples:\n   - `--routes` omitted, base `https:\u002F\u002Fhost\u002Fapp` → scans `https:\u002F\u002Fhost\u002Fapp`\n   - `--routes \"\u002F\"` against base `https:\u002F\u002Fhost\u002Fapp` → scans `https:\u002F\u002Fhost\u002F`\n   - `--routes \"settings\"` against base `https:\u002F\u002Fhost\u002Fapp\u002F` → scans `https:\u002F\u002Fhost\u002Fapp\u002Fsettings`\n3. **with_modals** — `--with-modals` flag (default: off)\n4. **scope** — `--scope a11y|forms|ids|all` (default: `all`). Unknown value →\n   fail with usage.\n5. **ready_selector** — `--ready-selector \"\u003Ccss>\"` (optional). Default:\n   `[data-testid=app-ready], [data-click-test-ready=true]`. If neither\n   attribute exists in the app, **pass an explicit selector** that\n   uniquely identifies the loaded state — do not fall back to generic\n   `main` \u002F `#root` (those match on skeleton DOMs and produce false\n   \"scanned\" verdicts on still-loading pages).\n6. **ready_timeout_ms** — `--ready-timeout 8000` (default 8000).\n7. **force_cache_bust** — `--force-cache-bust` flag (default: off, see Phase 2).\n8. **max_triggers_per_route** — `--max-triggers 50` (default 50, Phase 4b only).\n9. **device \u002F viewport** — three mutually-exclusive ways to pick the viewport\n   (and adjacent emulation flags). Specify at most one; supplying two → fail\n   with usage:\n   - `--device \u003Cpreset>` — one of `desktop`, `desktop-large`, `tablet`,\n     `mobile`, `mobile-small` (canonical registry:\n     [`scripts\u002Flib\u002Fdevice-presets.mjs`](https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fscripts\u002Flib\u002Fdevice-presets.mjs)).\n     Sets viewport + `isMobile` + `hasTouch` flags. Unknown name → fail.\n   - `--devices \"\u003Cp1>,\u003Cp2>,...\">` — **matrix mode**. Each preset runs the full\n     route crawl independently; findings are tagged with `device` and merged.\n     Cost is multiplicative — be explicit, this is opt-in.\n   - `--viewport \u003CW>x\u003CH>` — legacy direct viewport (W and H in `[320, 4096]`).\n     Synthesises a `custom` device record with `isMobile\u002FhasTouch` inferred\n     from width (`\u003C768 → mobile\u002Ftouch`). Kept for back-compat; prefer `--device`.\n   - None of the above → default device = `desktop` (1280×720). Identical to\n     today's behaviour for callers passing nothing.\n\n   The selected device is applied via a single `browser_resize` call before\n   the first `browser_navigate` of each route (matrix mode resizes once per\n   device-pass). UA emulation, real touch events, and DPR scaling are NOT\n   provided — MCP browser drivers don't expose context-level launch options. For\n   full emulation, use `\u002Fpersona-test --mode consistency` (code-driven Playwright).\n\nRequired: `url`. If missing, output usage and STOP. Unknown flags → fail\nwith usage. Duplicate flags → last wins, log a stderr warning.\n\n---\n\n## Phase 0b — Run Contract (recorded in report)\n\nCapture these defaults at the start of each session and include them\nverbatim in the Phase 6 report so two runs against the same commit can be\ncompared:\n\n| Field | Default | Override |\n|---|---|---|\n| Device | `desktop` (1280×720, touch=false) | `--device \u003Cpreset>` \u002F `--devices \"\u003Clist>\"` (matrix) \u002F `--viewport WxH` (legacy) |\n| Browser | whatever the selected driver provides (Playwright MCP = Chromium) | — |\n| Locale \u002F timezone | tool default (do NOT randomise) | — |\n| Auth | none by default — public surface only | pre-authenticate the shared MCP session; see \"Auth-gated routes\" below |\n| Network idle | wait for `load` + ready-selector or 8000ms | `--ready-timeout` |\n\n**Auth-gated routes**: after each navigation, BEFORE running the static\nscan, check:\n\n1. **Cross-origin redirect** (OAuth \u002F SSO): if `new URL(location.href).origin !==\n   new URL(targetUrl).origin`, classify as `coverageStatus: \"auth-required\"`\n   regardless of DOM content. Catches OAuth providers (Google, Okta, Auth0)\n   that redirect to a third-party domain — never scan accessibility of those\n   pages and attribute findings to the audited app.\n2. **Same-origin password form**: if URL is same-origin AND DOM contains\n   `[type=password]`, classify as `coverageStatus: \"auth-required\"`. Catches\n   first-party login walls.\n\nclick-test does not log in itself. If primary surfaces need a login,\npre-authenticate the shared driver connection via `--storage-state` before\nrunning — see [`references\u002Fauth-bootstrap.md`](references\u002Fauth-bootstrap.md).\nThe OVERALL verdict\nbecomes at most `Incomplete` (never `Clean`) when any route is\n`auth-required`.\n→ `references\u002Fscan-rules-detail.md` §Auth-gated routes — read when a target is login-gated.\n\n---\n\n## Phase 1 — Detect Browser Tool\n\nResolve the driver through the **one detection oracle** —\n[`references\u002Fbrowser-tool-detection.md`](references\u002Fbrowser-tool-detection.md).\nDo not restate its ladder here; click-test declares only what it needs.\n\n**Minimum capability set**: `navigate`, `evaluate`, `click`, `keyboard`\n(Escape press), `wait`, `currentUrl`. Verify each before the first scan and log\nthe chosen driver plus the capabilities matched.\n\n**`evaluate` is non-negotiable and it is what excludes static drivers.** The DOM\nscan *is* a `page.evaluate` call, so a fetch-and-parse driver can never serve\nclick-test however well it parses HTML. There is no degraded mode here: an unmet\nset means abort before scanning with\n`[BLOCKED] \u003Cdriver> missing capability: \u003Ccap>` — never a clean empty pass, which\nwould report \"no findings\" from a page nobody scanned.\n\n---\n\n## Phase 2 — Pre-flight Cache-bust (own-app only by default)\n\nService workers silently serve stale bundles, masking real deploys. The\ncache-bust is **mandatory for own-app hostnames**, where clearing user state\ncosts nothing. **For external URLs**: do NOT cache-bust by default — clearing\n`serviceWorker` + `caches` on a third-party origin can log out the operator,\nremove offline data, or alter the very state being measured. Require the\nexplicit `--force-cache-bust` flag — and even then, warn loudly on stderr.\n\n| Hostname class | Default behaviour |\n|---|---|\n| Own-app (localhost \u002F `*.railway.app` \u002F `*.vercel.app` \u002F `*.netlify.app` \u002F `*.local`) | Cache-bust runs unconditionally |\n| External | Skip cache-bust. `--force-cache-bust` overrides with stderr warning |\n| Any driver without `evaluate` | Unreachable — click-test aborts `blocked` before this point (Phase 1) |\n\nCache-bust script (when it runs). Both APIs (`serviceWorker`, `caches`) can\nbe undefined globally in non-secure contexts (HTTP-only test URLs) —\nreferencing them throws `ReferenceError` BEFORE optional chaining is\nevaluated. Always check `typeof` first:\n\n```js\n\u002F\u002F browser_evaluate\n(async () => {\n  let unregistered = 0, cachesDeleted = 0;\n  if (typeof navigator !== 'undefined' && navigator.serviceWorker) {\n    const regs = await navigator.serviceWorker.getRegistrations().catch(() => []);\n    await Promise.all(regs.map(r => r.unregister().catch(() => {})));\n    unregistered = regs.length;\n  }\n  if (typeof caches !== 'undefined') {\n    const keys = await caches.keys().catch(() => []);\n    await Promise.all(keys.map(k => caches.delete(k).catch(() => {})));\n    cachesDeleted = keys.length;\n  }\n  return { unregistered, cachesDeleted };\n})()\n```\n\nThen `browser_navigate(url)` again to force a fresh fetch. Log result:\n`[cache-bust] unregistered \u003Cn> SW, cleared \u003Cn> caches (own-app)` or\n`[cache-bust] skipped (external host; pass --force-cache-bust to override)`.\n\nRecord `cacheBustMode: \"own-app\" | \"forced\" | \"skipped-external\" | \"n\u002Fa\"`\nin the run contract for the Phase 6 report.\n\n---\n\n## Phase 3 — Crawl Routes\n\n### Get the device-pass contract (required; do not skip)\n\nBefore any browser work, call:\n\n```bash\nnode scripts\u002Flib\u002Fdevice-presets.mjs prep-matrix\n```\n\nOptional flags — add at most one, matching what your `$ARGUMENTS` contained (none if neither flag was passed): `--device PRESET`, `--devices \"LIST\"`, `--viewport WxH`.\n\n\u003C!-- host-contract: input-acquisition; grammar=path+flags; empty=default -->\n_This site: `path+flags` — neither device flag present means a single `desktop` pass — do not synthesise a matrix._\n\nThe CLI returns `{kind: \"click-test-prep\", matrixMode, totalPasses, passes[]}`,\neach pass carrying `device`, `expectedFirstMcpCall` and `logLine`\n(→ `references\u002Fscan-rules-detail.md` §Device prep contract for the full shape).\n\nThe LLM does NOT pick device order, dimensions, or pass count — it walks\n`passes` in array order, executing each `expectedFirstMcpCall` verbatim.\nMutual-exclusion conflicts (e.g. `--device` + `--devices`) cause the CLI\nto exit non-zero with the error on stderr; surface that, do not proceed.\n\n### Device pass loop\n\nFor each `pass` in `contract.passes` (in array order):\n\n1. Echo `pass.logLine` to stderr.\n2. Call `browser_resize` with `pass.expectedFirstMcpCall.args` verbatim\n   — once per device-pass, BEFORE the first navigate.\n3. Tag every finding from this pass with `device: pass.device.name`.\n4. Run the per-route loop below.\n\nCache-bust (Phase 2) runs **once at session start**, not per device-pass —\nservice-worker state is global to the browser context.\n\n### Per-route loop (within a device pass)\n\nFor each route in `routes`:\n\n1. `browser_navigate({url: new URL(route, base).href})` — never string-concat\n2. **Readiness protocol** (replaces vague \"stable string\" wait):\n   a. Wait for `load` event (most MCPs do this implicitly on navigate).\n   b. Poll: ready-selector matches AND `document.querySelector('[aria-busy=\"true\"], [role=\"progressbar\"]')` returns null.\n   c. Timeout after `--ready-timeout` ms (default 8000).\n   d. If timeout fires, classify the route as `coverageStatus: \"readiness-timeout\"`\n      and skip its scan — do NOT scan a skeleton DOM.\n3. **Route execution status** — independent of finding count. Record one of:\n   - `scanned` — readiness met AND scanner output validated. (May still have findings.)\n   - `auth-required` — landed on login (URL changed AND `[type=password]` present)\n   - `navigation-error` — `browser_navigate` threw (rendered app-level 404\u002F500 pages\n     count as `scanned` unless the tool exposes a real response status and it's 4xx\u002F5xx)\n   - `readiness-timeout` — readiness selector never matched within timeout\n   - `scanner-error` — `browser_evaluate` threw or returned schema-invalid shape\n   - `skipped-external-anchor` — route resolved to a different origin\n4. If state is `scanned`, run **Phase 4 — Static scan** (one pass per settled DOM state — see Reminders)\n5. If `with_modals` AND state is `scanned`, run **Phase 4b — Dynamic-surface scan**\n6. Record findings tagged with `{device, route, coverageStatus, via}`; record per-route\n   element counts and dynamic-surface counts in the run metrics (Phase 4 schema).\n\n---\n\n## Phase 4 — Static Scan\n\n`browser_evaluate` with the scanner from\n[`references\u002Fdom-scanner.md`](references\u002Fdom-scanner.md). The scanner returns\na single result object — **validate before use** (a hostile or malformed\npage could return anything from `browser_evaluate`):\n\n```js\n\u002F\u002F pseudo-Zod (canonical contract — runner enforces, report consumes)\nClickTestFinding = {\n  kind: enum(\"duplicate-id\", \"orphan-label\", \"input-no-name\",\n             \"button-no-name\", \"link-no-name\", \"form-field-no-name\",\n             \"duplicate-aria-label\", \"aria-hidden-focusable\",\n             \"empty-link\", \"heading-skip\", \"img-no-alt\",\n             \"small-touch-target\", \"positive-tabindex\"),  \u002F\u002F 13 kinds\n  severity: enum(\"P0\",\"P1\",\"P2\",\"P3\"),   \u002F\u002F EFFECTIVE severity — capped to P3 when !perceivable\n  declaredSeverity: enum(\"P0\",\"P1\",\"P2\",\"P3\"),  \u002F\u002F the kind's intrinsic severity, pre-cap\n  perceivable: boolean,      \u002F\u002F was the element RENDERED in the captured state?\n  selector: string.max(500),\n  snippet: string.max(200),  \u002F\u002F outerHTML truncated; redact before persist\n  detail: string.max(500),\n  device: string,            \u002F\u002F preset name (\"desktop\", \"mobile\", …) or \"custom\"\n}\nClickTestScanResult = {\n  schemaVersion: literal(1),\n  routeUrl: string,                  \u002F\u002F the final navigated URL\n  device: string,                    \u002F\u002F matches each finding's device tag\n  elementsScanned: number,           \u002F\u002F document.querySelectorAll('*').length at scan time\n  interactiveElementsScanned: number, \u002F\u002F count of button\u002Fa\u002Finput\u002Fselect\u002Ftextarea\u002F[role=button|link]\n  findings: ClickTestFinding[].max(999),  \u002F\u002F reject >999 — flags scanner-error\n  shadowGapCount: number,            \u002F\u002F open shadow roots not traversed\n  iframeGapCount: number,            \u002F\u002F iframes not traversed\n}\n```\n\n**Device-pass aggregation** (when `--devices` produces multiple device passes):\neach finding's `device` field is set by the runner from the active pass — the\nscanner does not need to know. Findings dedupe by `{device, route, via, kind, selector}` —\nthe same duplicate-id on `mobile` AND `desktop` is reported twice (correctly:\nit's two regressions that may have different fix sites if responsive CSS\nhides one). The Phase 6 report's PER-DEVICE COVERAGE table surfaces which\ndevice flagged which.\n\n`small-touch-target` is **device-sensitive**: when both desktop and mobile\npasses ran, downgrade desktop-pass `small-touch-target` findings by one\nseverity (P2 → P3) — the mobile pass is the authoritative read.\n\nIf validation fails, classify the route as `coverageStatus: \"scanner-error\"`\nand skip its findings — never persist or report unvalidated output.\n\n**Per-route aggregation** when `--with-modals` produces multiple scan results\n(one static + one per opened modal): the per-route record stores the static\nscan's `elementsScanned` \u002F `interactiveElementsScanned` as the route totals\n(modals are subsets, not additions). For `shadowGapCount` \u002F `iframeGapCount`,\ntake the **max** observed across passes (NOT sum — each modal pass re-scans\nthe whole DOM, so the static gaps would be double-counted). `findings` are\nunioned and deduped by `{device, route, via, kind, selector}`.\nEach finding's `via` field records its origin (`static` or `modal:\u003Caccessible-name>`).\n\n**Redaction**: before printing or persisting any `snippet`, run it through\nthe existing redactor (`scripts\u002Flib\u002Fredact.mjs::redact` for string-typed\ncontent; use `redactObject` if the runner ever boxes the snippet inside a\nlarger payload). DOM `outerHTML` can contain emails, tokens in `data-*`\nattributes, hidden form values, customer content. The default scanner\nalready truncates to 200 chars; redaction is the second layer.\n\n### Finding taxonomy (canonical rule registry)\n\n13 rules, 1:1 with the `kind` enum above:\n\n| Kind | Default severity | In `ids` | In `forms` | In `a11y` |\n|---|---|---|---|---|\n| `duplicate-id` | P0 | ✓ | ✓ | ✓ |\n| `orphan-label` | P0 | — | ✓ | ✓ |\n| `input-no-name` | P0 | — | ✓ | ✓ |\n| `button-no-name` | P0 | — | — | ✓ |\n| `link-no-name` | P1 | — | — | ✓ |\n| `form-field-no-name` | P1 \u002F **P3 when the form has no `action`** | ✓ | ✓ | — |\n| `duplicate-aria-label` | P2 *(see note)* | — | — | ✓ |\n| `aria-hidden-focusable` | P1 | — | — | ✓ |\n| `empty-link` | P1 | — | — | ✓ |\n| `heading-skip` | P2 | — | — | ✓ |\n| `img-no-alt` | P2 | — | — | ✓ |\n| `small-touch-target` | P2 | — | — | ✓ |\n| `positive-tabindex` | P2 | — | — | ✓ |\n\n`button-no-name` and `link-no-name` are separate kinds (one rule per kind)\nso the severity is unambiguous in the enum. The scanner emits one or the\nother based on element type — never both for the same node.\n\n`--scope all` → keep everything. `--scope \u003Cname>` → keep rows with `✓` in\nthat column. Invalid scope → fail with usage.\n\n`duplicate-aria-label` note: high false-positive rate on grid\u002Fcard layouts,\nso the scanner only fires it within the same `role` + parent region.\n→ `references\u002Fscan-rules-detail.md` §duplicate-aria-label FP note.\n\n---\n\n## Phase 4b — Dynamic-surface Scan (`--with-modals` only)\n\nModals mount duplicate IDs only while open, which the static scan cannot see\n(→ `references\u002Fscan-rules-detail.md` §Why `--with-modals` exists).\n\n### Trigger discovery (one evaluate per route, before clicking)\n\n```js\n\u002F\u002F browser_evaluate — returns DiscoveredTrigger[]\n\u002F\u002F DiscoveredTrigger = { idx, selector, role, accessibleName, regionKey,\n\u002F\u002F                       destructiveReason: string|null, disabled: bool,\n\u002F\u002F                       href: string|null, formAction: string|null }\n```\n\nThe runner stores this array, then iterates by `idx`. Before each click:\n\n1. `browser_evaluate` re-queries by the stored `selector` — if it returns\n   null or a different element (compared by `idx → selector → accessibleName`\n   triple), skip with `coverageStatus: \"trigger-stale\"`.\n2. If `destructiveReason` is non-null → skip with that reason.\n3. If `disabled === true` → skip with `\"disabled\"`.\n4. Otherwise proceed to click.\n\nThe trigger candidate set: `[aria-haspopup=\"true\"], [data-modal-trigger],\nbutton[aria-controls], [role=\"combobox\"]`.\n\n### Per-trigger flow\n\n1. **Safety classifier** — skip if the trigger is destructive (see below)\n2. **URL snapshot** — record `currentUrl` before click\n3. `browser_click(\u003Ctrigger>)`\n4. Wait for modal mount: poll for `[role=\"dialog\"]`, `\u003Cdialog[open]>`, or\n   `aria-expanded=\"true\"` on the trigger, up to 1000ms. Timeout → log\n   \"trigger opened nothing\" and skip.\n5. Re-run the scanner — tag findings with `via: \"modal:\u003Caccessible-name>\"`\n6. Close: `Escape` key → if dialog still mounted, click `[aria-label=\"Close\"]`\n   inside it → if still mounted, click outside the dialog at `(8, 8)`.\n7. **Verify closed AND URL unchanged** — if URL changed, treat as\n   navigation side-effect: re-navigate to the route's URL **then re-run\n   the Phase 3 readiness protocol** (don't assume the page is ready\n   immediately after navigation). Resume the trigger loop only once\n   readiness is met. If dialog still mounted after all 3 close attempts,\n   log `coverageStatus: \"modal-stuck\"` for this route and stop the\n   dynamic scan for that route.\n8. Continue to next trigger until **`--max-triggers`** (default 50) reached.\n   Excess triggers logged as `skipped: budget-exceeded`.\n\n### Safety classifier — destructive-trigger filter\n\nSkip a trigger if **any** of these match (case-insensitive, accent-folded):\n\n- Accessible name (computed: `aria-label`, `aria-labelledby`, visible text,\n  `title` — in that order) matches `\u002F^(delete|destroy|remove|purge|reset|sign\\s*out|log\\s*out|cancel\\s*subscription|deactivate|close\\s*account|drop|truncate|wipe)\u002Fi`\n- `data-destructive=\"true\"` or `data-action` value matches the regex above\n- `role=\"link\"` with `href` pointing to a different origin or matching\n  `\u002Flogout|signout|delete\u002Fi`\n- Inside a form whose `action` matches the destructive regex\n- `aria-disabled=\"true\"` or `[disabled]` (don't click disabled controls)\n\nLog each skipped trigger with reason: `skipped-destructive: \u003Caccessible-name> (matched: \u003Cpattern>)`.\n\n**Deduplication**: two triggers are dupes when they share `(accessibleName, role, closest [role=\"dialog\"]\u002Fsection ancestor)`. Skip dupes after the first.\n\n---\n\n## Phase 5 — Severity Model\n\nThe authoritative severity-per-kind mapping is the Phase 4 taxonomy table.\n→ `references\u002Fscan-rules-detail.md` §Severity intuition — read when a kind's\nseverity is disputed (incl. the `form-field-no-name` P1-vs-P3 action rule).\n\n### Perceivability cap (2026-07-30)\n\n`push()` runs every finding through `__isPerceivable(el)` — one call site, so all\n13 kinds get it. A finding on an element that is not **rendered** in the captured\nstate (`display:none`, `visibility:hidden`, `opacity:0`,\n`content-visibility:hidden`, `[inert]`, detached, or zero-size) is **demoted to\nP3** and tagged `perceivable:false`; `declaredSeverity` preserves what the kind\nwould otherwise have been.\n\n**Demote, do not drop.** A hidden element may become visible — that is exactly\nwhat `--with-modals` re-scans for — so dropping the finding destroys signal. The\ntag is **state-relative**: it says \"not perceivable in *this* capture\", never\n\"never perceivable\".\n\n**Filter the headline on `perceivable`,** and report capped findings separately\nso the count stays honest — 704 findings with 6 perceivable is a very different\nreport from 704 undifferentiated ones.\n\n→ `references\u002Fscan-rules-detail.md` §Perceivability — the field incident,\nthe release-scope note, and the canonical predicate source.\n\n### Touch targets are measured over activating regions\n\n`small-touch-target` measures **every region that activates the control**, not\njust the control's own border box. A wrapping `\u003Clabel>` — or one associated via\n`label[for=…]`, both covered by the spec's `el.labels` — is part of the tap\ntarget, because clicking it activates the control. Regions are contiguous (no\nbounding-box union); the rule passes on the first region ≥24×24 and reports the\nlargest; a control whose every region is undersized still fires.\n→ `references\u002Fscan-rules-detail.md` §Touch targets — field data and why.\n\nConfidence is not a click-test concept — assertions are deterministic. Either\nthe DOM violates the contract or it doesn't. If runs vary across same\nURL + commit, something non-deterministic (A\u002FB test, random ID generator,\nload-order race) is involved; flag that meta-finding itself.\n\n---\n\n## Phase 6 — Structured Report\n\nHeader (the Phase 0b run contract, verbatim):\n\n```\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n  CLICK-TEST REPORT\n  URL: \u003Cbase>\n  Routes: \u003Clist>\n  Devices: \u003Cname(s)>   (e.g. \"mobile (390x844)\" or \"desktop, mobile\" for matrix)\n  Scope: \u003Cscope>   Modals: \u003Con|off>   Cache-bust: \u003Cown-app|forced|skipped-external|n\u002Fa>\n  Ready-selector: \u003Ccss>   Ready-timeout: \u003Cms>\n  Tool: \u003Cbrowser_tool> — \u003CN> elements scanned (across all device passes) — \u003Cduration>\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n```\n\nThen, in order:\n- **FINDINGS (\u003Ctotal>)** — per finding `[P\u003Cn>] \u003Ckind> \u003Csubject>` then Device,\n  Route, Via, Selector, Detail, Snippet (redacted).\n- **ROUTE COVERAGE** — one line per route: `\u003Croute> — \u003CcoverageStatus> —\n  \u003Cn> elements — \u003Cn> findings (P0:n, …)` or `skipped (\u003Creason>)`; under\n  `--devices`, grouped per `device=\u003Cname> (\u003CWxH>)` plus a **CROSS-DEVICE**\n  section (shared \u002F desktop-only \u002F mobile-only counts + one interpretation line).\n- **DYNAMIC SURFACES** (`--with-modals`) — per device+route: triggers total,\n  scanned, skipped-destructive, modal-stuck.\n- **OVERALL** — `\u003CBroken | Broken+Incomplete | Has issues | Incomplete | Clean>`\n  plus a one-sentence Reason.\n\n→ `references\u002Fphase6-output-sample.md` — read before rendering, for the full sample.\n\n```\nOVERALL VERDICT RULES (deterministic precedence — first match wins):\n  Coverage condition (referenced below):\n    \"covered\"     = every route coverageStatus=\"scanned\" AND\n                    shadowGapCount == 0 AND iframeGapCount == 0 AND\n                    no dynamic-scan failure\n                    (failure = modal-stuck\u002Fbudget-exceeded OR trigger-stale > 25% of triggers)\n    \"gaps\"        = NOT covered\n\n  1. Broken+Incomplete → any P0 finding AND gaps\n  2. Has issues+Incomplete → any P1\u002FP2 finding (no P0) AND gaps\n  3. Broken             → any P0 finding AND covered\n  4. Has issues         → any P1\u002FP2 finding (no P0) AND covered\n  5. Incomplete         → zero findings AND gaps\n  6. Clean              → zero findings AND covered\n\n  Issues are NEVER masked by coverage gaps — verdict 2 explicitly surfaces\n  the P1\u002FP2 + gap combination rather than letting it fall through to\n  \"Incomplete\" (which would hide the discovered defects).\n\n  Notes:\n    - \"Has issues\" applies regardless of P1 vs P2; the count distinction lives\n      in the FINDINGS block, not the verdict.\n    - The verdict precedence is independent of coverage — a Broken+Incomplete\n      run with one P0 and one auth-required route is more severe than either\n      alone and gets its own label so \u002Fship can gate appropriately.\n```\n\nSort findings P0 first, then by `kind` alphabetical so similar issues group.\n\n---\n\n## Phase 7 — Persistence (Out of Scope for v1)\n\nv1 does not persist findings: no `record-click-test` subcommand exists, so do\nnot invent one or claim a store write. The local Phase 6 report is the whole\noutput — print, read, fix, re-run.\n→ `references\u002Fv1-scope-and-deployment.md` — read when asked why, what v2 adds,\nwhich R3 audit concerns were deferred, or how this skill is deployed.\n\n---\n\n## Reminders\n\n- **One scanner pass per settled DOM state** — one static pass per route per\n  device, plus one pass per successfully opened dynamic surface in\n  `--with-modals` mode. Aggregate findings in JS using\n  `{device, route, via, kind, selector}` as the dedup key so a finding\n  present in both static + modal states (on the same device) is reported\n  once, but the same finding on two different devices is reported twice\n  (responsive CSS can cause a duplicate-id to surface on only one viewport).\n\n---\n\n## Reference files\n\n| File | Summary | Read when |\n|---|---|---|\n| `references\u002Finput-acquisition.md` | Where a skill's arguments come from on any host, and what to do when there are none. | Reading $ARGUMENTS on a host that does not substitute it, or deciding what empty input means at a site. |\n| `references\u002Fbrowser-tool-detection.md` | The browser-driver contract — capabilities, driver table, selection order, minimum sets, degraded\u002Fblocked evidence. | Phase 1 driver selection — resolving which driver to use, or diagnosing a `blocked` result. |\n| `references\u002Fauth-bootstrap.md` | Sanctioned auth-gated exploratory-testing pattern via MCP-server storageState, plus its connect-time-race escape hatch. | The target's primary surfaces need a login — pre-authenticate the shared driver connection before scanning, so `auth-required` routes are not coverage gaps. |\n| `references\u002Fdom-scanner.md` | The full browser_evaluate scanner JS — every assertion's implementation, selector-stringifier helpers, severity mapping. | About to run Phase 4 or Phase 4b. |\n| `references\u002Fscan-rules-detail.md` | Why the scan rules are what they are — perceivability, touch-target regions, severity intuition, FP notes, prep JSON. | A finding's severity or perceivability is disputed, a touch-target finding looks false, OR the prep contract shape is unclear. |\n| `references\u002Fphase6-output-sample.md` | Full Phase 6 output sample — finding entries, single-device and matrix ROUTE COVERAGE, CROSS-DEVICE, DYNAMIC SURFACES. | About to render the Phase 6 report. |\n| `references\u002Fv1-scope-and-deployment.md` | Why v1 has no persistence, the R3-deferred v2 concerns, and the maintainer deployment recipe + acceptance checks. | Asked about persistence, \u002Fship integration or v2 scope, OR deploying\u002Fsyncing this skill. |\n| `references\u002Fworktree-preflight.md` | Worktree preflight remedy — the skills:hydrate npm script to add when a linked worktree lacks the tooling tree. | A command dies with `MODULE_NOT_FOUND` in a linked worktree AND this repo has no `skills:hydrate` script. |\n\n— end of \u002Fclick-test —\n",{"name":5,"description":7},"main","938efbef34b7c76f0d112f336d1ac4018224d186",".claude\u002Fskills\u002Fclick-test","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills","https:\u002F\u002Fgithub.com\u002FLbstrydom","Studying M.Sc Computer Science at Heriot Watt","2026-11-12 02:12:09",{"source":8,"skillName":5,"commands":71,"prompt":81,"zipAvailable":82},[72,75,78],{"tool":73,"command":74},"npx","npx skills add Lbstrydom\u002Fclaude-engineering-skills --skill click-test",{"tool":76,"command":77},"bunx","bunx skills add Lbstrydom\u002Fclaude-engineering-skills --skill click-test",{"tool":79,"command":80},"pnpm","pnpm dlx skills add Lbstrydom\u002Fclaude-engineering-skills --skill click-test","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002FSKILL.md",true,[84,90,94,98,102,106,110,114],{"path":85,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":88,"sourceHtmlUrl":89},"references\u002Fauth-bootstrap.md","REFERENCE","参考文档",8931,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002Freferences\u002Fauth-bootstrap.md",{"path":91,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":92,"sourceHtmlUrl":93},"references\u002Fbrowser-tool-detection.md",20710,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002Freferences\u002Fbrowser-tool-detection.md",{"path":95,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":96,"sourceHtmlUrl":97},"references\u002Fdom-scanner.md",24975,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002Freferences\u002Fdom-scanner.md",{"path":99,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":100,"sourceHtmlUrl":101},"references\u002Finput-acquisition.md",4511,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002Freferences\u002Finput-acquisition.md",{"path":103,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":104,"sourceHtmlUrl":105},"references\u002Fphase6-output-sample.md",3671,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002Freferences\u002Fphase6-output-sample.md",{"path":107,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":108,"sourceHtmlUrl":109},"references\u002Fscan-rules-detail.md",7389,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002Freferences\u002Fscan-rules-detail.md",{"path":111,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":112,"sourceHtmlUrl":113},"references\u002Fv1-scope-and-deployment.md",6724,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002Freferences\u002Fv1-scope-and-deployment.md",{"path":115,"resourceType":86,"resourceTypeDesc":87,"sizeBytes":116,"sourceHtmlUrl":117},"references\u002Fworktree-preflight.md",3413,"https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002F.claude\u002Fskills\u002Fclick-test\u002Freferences\u002Fworktree-preflight.md",[119,133,144,162,176,206],{"skillNo":120,"name":121,"nameZh":6,"description":122,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":123,"categories":124,"keywords":126,"publishedAt":58},"01M6J45X4FDB2AD5BGS4R4QY8X","stated-absent","Negative fixture — the contract's stated string never appears in this file.","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fgate-honesty\u002Fnegative\u002Fstated-absent\u002Fskills\u002Fstated-absent\u002FSKILL.md",[125],{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},[127,128,129,130,131,132],"stated absent","STRING","contract","fixture","never","file",{"skillNo":134,"name":135,"nameZh":6,"description":136,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":137,"categories":138,"keywords":140,"publishedAt":58},"01M6J45TAX2PPHETZ5TJZG20QQ","nonexistent-implementation","Negative fixture — contract references an implementation file that does not exist.","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fgate-honesty\u002Fnegative\u002Fnonexistent-implementation\u002Fskills\u002Fnonexistent-implementation\u002FSKILL.md",[139],{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},[141,142,143,129,130,132],"implementation","nonexistent implementation","references",{"skillNo":145,"name":146,"nameZh":6,"description":147,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":148,"categories":149,"keywords":153,"publishedAt":58},"01M6J45P5GN52YFMZ81T7NN1VF","document-only-with-params","Negative fixture — a document-only gate illegally carries params (schema must refuse this fake-check pattern).","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fgate-honesty\u002Fnegative\u002Fdocument-only-with-params\u002Fskills\u002Fdocument-only-with-params\u002FSKILL.md",[150],{"categoryNo":151,"categoryName":152,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000002","文档处理",[154,155,156,157,158,159,160,161,130],"only","document","with","document only with params","check","schema","gate","pattern",{"skillNo":163,"name":164,"nameZh":6,"description":165,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":166,"categories":167,"keywords":171,"publishedAt":58},"01M6J45J1CCZ7AXDGQKBA9VC8M","lying-skill","Fixture skill for the gate-honesty self-check — every gate here is deliberately dishonest.","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Ftests\u002Ffixtures\u002Fgate-honesty\u002Flying-skill\u002Fskills\u002Flying-skill\u002FSKILL.md",[168],{"categoryNo":169,"categoryName":170,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000023","Skill与智能体开发",[172,158,160,173,174,130,175],"lying skill","fixture skill","self","for",{"skillNo":177,"name":178,"nameZh":6,"description":179,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":180,"categories":181,"keywords":189,"publishedAt":58},"01M6J45CW0DNN7K2W39E3PQHEY","visual-audit","Math-first, deterministic visual\u002Fpaint inspection — the 4th UX lens, complementing\npersona-test (journey), click-test (page), and nav-audit (system). Drives Playwright +\ncomputed styles + bounding boxes + CDP forced-pseudo-state to audit declared-token\nconformance, light\u002Fdark theme parity, layout physics, and affordance signifiers. A VLM\nis advisory-only and never gates. CI gate is drift-only on the changed contracted surface.\nTriggers on: \"visual audit\", \"check styling consistency\", \"theme parity\", \"dark mode parity\",\n\"design token drift\", \"paint audit\", \"button\u002Fcard consistency\", \"focus ring audit\",\n\"\u002Fvisual-audit\".","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fvisual-audit\u002FSKILL.md",[182,183,186],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":184,"categoryName":185,"taxonomyType":21,"taxonomyTypeDesc":22},"01JBSEEDCAT000000000000015","视觉设计",{"categoryNo":187,"categoryName":188,"taxonomyType":29,"taxonomyTypeDesc":30},"01JBSEEDCAT000000000000032","设计师",[190,191,192,40,193,33,194,195,196,197,52,198,131,154,199,200,201,202,203,204,205],"visual audit","visual","audit","design token","inspection","lens","design","ci","math","cdp","journey","drift","focus","physics","layout","card",{"skillNo":207,"name":208,"nameZh":6,"description":209,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":210,"categories":211,"keywords":216,"publishedAt":58},"01M6J4569S40ZASFWW698CHBKP","ux-lock","Generate Playwright e2e specs. Two modes:\n  1. LOCK mode — pin a fix's DOM contract so it doesn't regress (default).\n  2. VERIFY mode — check that a \u002Fplan plan was actually implemented\n     by parsing its Acceptance Criteria section and driving the live URL.\nVERIFY mode grades a plan against a RUNNING app. To judge the plan document\nitself, before anything is built, that is \u002Faudit-plan.\nTriggers on: \"ux lock\", \"lock ux\", \"lock in the fix\", \"write regression spec\",\n\"generate e2e test for\", \"regression test for commit\", \"lock this fix\",\n\"verify the plan\", \"verify plan implementation\", \"check the plan was built\",\n\"audit frontend implementation\", \"did we ship the plan\".\nFull command syntax: see the Usage section in this skill.","https:\u002F\u002Fgithub.com\u002FLbstrydom\u002Fclaude-engineering-skills\u002Fblob\u002Fmain\u002Fskills\u002Fux-lock\u002FSKILL.md",[212,213,214,215],{"categoryNo":19,"categoryName":20,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":24,"categoryName":25,"taxonomyType":21,"taxonomyTypeDesc":22},{"categoryNo":187,"categoryName":188,"taxonomyType":29,"taxonomyTypeDesc":30},{"categoryNo":27,"categoryName":28,"taxonomyType":29,"taxonomyTypeDesc":30},[217,218,219,40,220,221,222,192,223,224,225,50,226,227,228,229,43,230,231,232,233],"ux lock","lock","ux","commit","generate","acceptance","plan","regression","syntax","write","fix","e2e test","default","judge","URL","verify plan","frontend"]