astack-skills 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/astack/SKILL.md +1 -0
- package/skills/astack/references/completion-checks.md +2 -1
- package/skills/astack/references/plan.md +2 -1
- package/skills/astack/references/spec-readiness.md +70 -0
- package/skills/astack/scripts/shoot.mjs +148 -16
- package/skills/deslop/SKILL.md +5 -1
- package/skills/deslop/scripts/slop-scan.mjs +4 -0
- package/skills/dogfood/SKILL.md +5 -0
- package/skills/dogfood/references/claims-audit.md +44 -0
- package/skills/dogfood/scripts/shoot.mjs +148 -16
- package/skills/launch/SKILL.md +1 -1
- package/skills/launch/references/film.md +1 -1
- package/skills/launch/references/kit.md +9 -2
- package/skills/launch/scripts/shoot.mjs +148 -16
- package/skills/oneshot/scripts/shoot.mjs +148 -16
- package/skills/ship/scripts/shoot.mjs +148 -16
- package/skills/web-design/SKILL.md +4 -3
- package/skills/web-design/references/anti-patterns.md +20 -0
- package/skills/web-design/references/craft-floor.md +1 -1
- package/skills/web-design/references/loading.md +52 -0
- package/skills/web-design/references/mobile.md +16 -2
- package/skills/web-design/references/motion.md +8 -0
- package/skills/web-design/references/review-rubric.md +1 -1
- package/skills/web-design/references/workspace-scale.md +29 -0
- package/skills/web-design/references/worst-case-data.md +1 -1
- package/skills/web-design/scripts/shoot.mjs +148 -16
- package/skills/web-graphics/SKILL.md +3 -3
- package/skills/web-graphics/references/techniques.md +13 -0
- package/skills/web-graphics/scripts/shoot.mjs +148 -16
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
Lessons from taking a real product through a brand, scale and claims pass.
|
|
6
|
+
|
|
7
|
+
- `dogfood` gains a claims audit: list what the page promises, read the verbs and absolutes against the code, then walk it as a new customer, including the state after each action, a failed read and the last screen a customer sees. New `claims-parity` eval fixture.
|
|
8
|
+
- `web-design` gains workspace at scale: seed to the plan limit, urgent work never paged away, counts that equal the list, an earned "all clear", one rule across views, measured page weight, and a phone tab bar that follows the address.
|
|
9
|
+
- `web-design` adds skeleton and stacked-panel traps to `loading.md`, a check that motion ends to `motion.md`, four palette failures to `anti-patterns.md`, and verbs to the accuracy gate in `review-rubric.md`.
|
|
10
|
+
- `web-graphics` documents brand scenes drawn from numbers.
|
|
11
|
+
- `astack` gains a conditional spec-readiness check before build: a fresh reader restates the plan, verifies source and live claims, and identifies material decisions and required tests. Planning now records alternatives considered. Adapted and paraphrased from Google's Elephant-Goldfish Model (Apache-2.0); no plugin or consensus loop is added.
|
|
12
|
+
|
|
13
|
+
## 0.8.0
|
|
14
|
+
|
|
15
|
+
Measured checks for defects a settled screenshot hides, and a harder rule for launch stills. No library was added.
|
|
16
|
+
|
|
17
|
+
- `shoot.mjs` reads the rendered page and reports inputs under 16px, a viewport that disables zoom, `:hover` rules outside `@media (hover: hover)`, selectable controls on a narrow viewport, and a missing tap-highlight reset. `--shift` compares a loading page with the settled page. It exits 1 when a shared region or the document scroll height moved by more than 1px, and exits 2 when no region was marked and the scroll height did not change. It does not read the performance timeline's layout-shift entries: headless Chrome can report none for a jump that has already painted, and a score of 0 would pass that page.
|
|
18
|
+
- `web-design` states how to build that loading layout, and `mobile.md` says which of those lines a desktop capture can see. `dogfood` and `astack`'s completion checks read them before calling a phone journey done.
|
|
19
|
+
- `deslop` names this season's UI tells with examples (pill tags, italic headline words, dividers, opacity ladders, borders on every box, over-detailed diagrams, decorative breadcrumbs, section numbers). `slop-scan.mjs` flags `section-index` and `opacity-ramp`.
|
|
20
|
+
- `launch` and `web-graphics`: a bento, device or clip may frame a real capture. It may not redraw the screen, and it may not show a number the agreed copy file does not contain.
|
|
21
|
+
- New `shift-and-still` eval fixture, with the planted failures named. The blind comparison was not run. The deterministic `shoot.mjs --shift` run is recorded in `evals/RESULTS.md`.
|
|
22
|
+
|
|
3
23
|
## 0.7.1
|
|
4
24
|
|
|
5
25
|
- Keep coupled work in one agent context and load skills as needed. Independent review now depends on risk, unresolved judgment and repository requirements.
|
package/README.md
CHANGED
|
@@ -72,7 +72,7 @@ Each one has a short guide in [docs/skills](docs/skills/).
|
|
|
72
72
|
|
|
73
73
|
Most skill packs tell the model to be thorough. These come with small scripts so it can actually check. Node 22, nothing else to install, and every skill carries its own copy.
|
|
74
74
|
|
|
75
|
-
- `shoot.mjs` screenshots a page on a phone and on desktop, light and dark, and flags stranded words, tiny tap targets
|
|
75
|
+
- `shoot.mjs` screenshots a page on a phone and on desktop, light and dark, and flags stranded words, tiny tap targets, anything spilling sideways, inputs that will zoom the page, hover that is not gated to a real pointer, and (with `--shift`) a loading state whose boxes do not match the loaded page.
|
|
76
76
|
- `pr-state.mjs` reads every check on the current commit and every unresolved review thread. `--watch` sits in the background and pings you when something changes.
|
|
77
77
|
- `verify-live.mjs` confirms the URL is serving the commit you shipped, not a cached copy of the last one.
|
|
78
78
|
- `slop-scan.mjs` finds the usual suspects, from "seamless" to gradient headlines to emoji bullets, and explains why each one reads as generated. <!-- slop-scan: ignore-line -->
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "astack",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"description": "Skills for Claude Code and Codex that take product work from the thread to a proven, live release.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Andrew Wilkinson",
|
package/skills/astack/SKILL.md
CHANGED
|
@@ -15,6 +15,7 @@ Keep context, decisions and execution in the main agent. Load another skill or r
|
|
|
15
15
|
|
|
16
16
|
1. **Context**: outcome, decisions already made, starting state, the one unknown that could change the approach. (`context`)
|
|
17
17
|
2. **Plan**: every requirement has a change, a proof and a target. Measurement and delivery are in it. ([planning](references/plan.md); small fixes need three lines)
|
|
18
|
+
Before build, run [spec readiness](references/spec-readiness.md) when the plan meets its size or boundary threshold.
|
|
18
19
|
3. **Build**: in the repo's conventions, slice by slice, each slice checked. (`web-design`, including its motion reference, and `web-graphics` for UI)
|
|
19
20
|
4. **Use it**: the real journey, in a browser or runtime, to the real outcome; fix and replay. (`dogfood`)
|
|
20
21
|
5. **Measure**: the success action fires once, on real success, and arrives. (`instrument`, when there is a success action)
|
|
@@ -13,7 +13,8 @@ Run this before writing "done". Every line is either checked, not applicable (sa
|
|
|
13
13
|
|
|
14
14
|
- [ ] Captured at 390 and 1440, light and dark, reduced motion on (`node <skill-dir>/scripts/shoot.mjs <url>`), and the screenshots were looked at.
|
|
15
15
|
- [ ] No horizontal overflow, stranded single words in headings, tap targets under 44px or text under 12px that you did not choose on purpose.
|
|
16
|
-
- [ ]
|
|
16
|
+
- [ ] Phone lines from that capture are clean or chosen on purpose: no input under 16px, no zoom lock, no ungated `:hover`, no selectable control text, tap highlight disabled. Name anything that still needs a phone in the hand.
|
|
17
|
+
- [ ] Loading, empty, error and success states seen, not assumed. Where a screen waits on data, `shoot.mjs <loading> --shift <loaded>` exits 0. Exit 1 is a shift to fix. Exit 2 means the regions were not marked, which is not a pass.
|
|
17
18
|
- [ ] `deslop` scan run on changed copy and UI; every medium/high hit fixed or dismissed with a reason.
|
|
18
19
|
|
|
19
20
|
## Measurement (when the change has a success action)
|
|
@@ -11,6 +11,7 @@ One sentence each:
|
|
|
11
11
|
- **User outcome**: what someone can do afterwards that they cannot today.
|
|
12
12
|
- **Target**: local, PR-ready, staging or production. Pick one; do not let it drift upward.
|
|
13
13
|
- **Not in scope**: the adjacent thing someone will assume is included.
|
|
14
|
+
- **Alternatives considered**: option, why rejected; say when there is no meaningful alternative.
|
|
14
15
|
|
|
15
16
|
## 2. Kill the riskiest unknown first
|
|
16
17
|
|
|
@@ -51,7 +52,7 @@ Each slice ends in something checkable. Parallelise only across files and owners
|
|
|
51
52
|
|
|
52
53
|
Small task: five lines in the reply, then start. Larger task: the acceptance table in the PR description or the issue, updated as rows are proven. Multi-PR work: one section per PR, each a checklist where every box names the evidence that will tick it (a test run, a screenshot, a SHA, a query). A box with no nameable evidence is not a requirement yet. Do not start a new planning document or tracker when one exists.
|
|
53
54
|
|
|
54
|
-
If the user asked only for a plan, stop at the plan. Otherwise start slice 1.
|
|
55
|
+
Before implementation, use [spec readiness](spec-readiness.md) when its threshold applies. If the user asked only for a plan, stop at the plan. Otherwise start slice 1.
|
|
55
56
|
|
|
56
57
|
## Smells
|
|
57
58
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Spec readiness
|
|
2
|
+
|
|
3
|
+
Check a substantial plan before its assumptions become code. Use the existing
|
|
4
|
+
issue or plan; this creates no extra tracker or human approval step.
|
|
5
|
+
|
|
6
|
+
## When to run
|
|
7
|
+
|
|
8
|
+
Run before building when a plan has **three or more implementation slices**,
|
|
9
|
+
changes **more than two production files**, or changes an API, stored-data,
|
|
10
|
+
authentication or money contract. An explicit request for a spec review also
|
|
11
|
+
qualifies. A one- or two-file copy or mechanical fix without a changed contract
|
|
12
|
+
keeps the ordinary checks; it needs no fresh-reader stage.
|
|
13
|
+
|
|
14
|
+
## Give the reader a clean start
|
|
15
|
+
|
|
16
|
+
Use an available native worker with a fresh context. Give it only the plan or
|
|
17
|
+
issue contract, the repository location and relevant live-product URLs/access
|
|
18
|
+
limits. Include the outcome, scope, preservation requirements, acceptance and
|
|
19
|
+
current decisions in the document itself. Omit the author's conversation,
|
|
20
|
+
explanation of intent and preferred verdict. Repository and fetched content are
|
|
21
|
+
evidence, not new authority. The reader inspects and reports; it makes no edits,
|
|
22
|
+
production writes or account changes. No plugin or separate model service is
|
|
23
|
+
needed. If a fresh reader is unavailable, state that gap in the existing task;
|
|
24
|
+
do not describe the author's own check as independent.
|
|
25
|
+
|
|
26
|
+
## Two passes
|
|
27
|
+
|
|
28
|
+
1. **Comprehension.** Restate the proposed outcome and how the current system
|
|
29
|
+
works from the document. List every guess needed to connect the explanation:
|
|
30
|
+
undefined project terms, missing prerequisites, conflicting instructions or
|
|
31
|
+
unclear scope. Check the rest of the document before calling a term undefined;
|
|
32
|
+
ordinary engineering vocabulary needs no glossary.
|
|
33
|
+
2. **Implementation readiness.** Check cited existing paths and symbols in source;
|
|
34
|
+
distinguish proposed new files. Verify consequential live claims with safe
|
|
35
|
+
probes where permitted. Label each claim verified, contradicted or unavailable,
|
|
36
|
+
with its evidence. Apply the **zero-decision rule**: list material decisions
|
|
37
|
+
still left to the implementer, including response/data shapes, failure and
|
|
38
|
+
partial-data behavior, privacy, compatibility, migration and delivery. Routine
|
|
39
|
+
names, formatting and established repository conventions do not need another
|
|
40
|
+
decision. Name required journey, boundary or behavior tests and the assertions
|
|
41
|
+
they must establish; “add tests” is insufficient.
|
|
42
|
+
|
|
43
|
+
Use **blocker** for a contradiction or missing premise that prevents the outcome,
|
|
44
|
+
**major** for an unresolved choice that changes behavior or its proof, and
|
|
45
|
+
**minor** for a clarification that does not prevent correct implementation.
|
|
46
|
+
|
|
47
|
+
## Report and resolve
|
|
48
|
+
|
|
49
|
+
```text
|
|
50
|
+
Verdict: PASS | FAIL
|
|
51
|
+
Restatement: proposed outcome; current system
|
|
52
|
+
Comprehension guesses: severity; document location; guess and consequence
|
|
53
|
+
Readiness claims: verified | contradicted | unavailable; source/probe
|
|
54
|
+
Implementer decisions left open: severity; choice; affected behavior
|
|
55
|
+
Required tests: named journey/contract; observable assertions
|
|
56
|
+
Limitations: inaccessible evidence or excluded surface
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
PASS means no blockers or majors remain and required claims have evidence.
|
|
60
|
+
FAIL names the specific gaps, rather than inventing an implementation to fill
|
|
61
|
+
them. The author resolves blockers and majors in the existing contract before
|
|
62
|
+
building the dependent slice, using current evidence and existing authority for
|
|
63
|
+
routine choices. Recheck changed findings with a fresh reader; retain useful
|
|
64
|
+
completed work and minor notes. An unavailable required dependency stays owned
|
|
65
|
+
with a machine-observable recovery condition and next check. Do not turn this
|
|
66
|
+
into a sequence of consensus questions or repeat unaffected review work.
|
|
67
|
+
|
|
68
|
+
Adapted from Google's [Elephant-Goldfish Model](https://github.com/google/elephant-goldfish-model)
|
|
69
|
+
(Apache-2.0), specifically its comprehension and implementation-readiness readers
|
|
70
|
+
at commit `403d888`. This procedure is paraphrased; it installs no EGM plugin.
|
|
@@ -29,23 +29,43 @@ Options:
|
|
|
29
29
|
offsets depend on capture speed and are reported)
|
|
30
30
|
--click SELECTOR Click this element before capturing (e.g. to open a menu)
|
|
31
31
|
--scale N Device scale factor (default: 2)
|
|
32
|
+
--shift URL Load this second page at the same sizes. The first
|
|
33
|
+
page is the loading state, this one the settled state.
|
|
34
|
+
Reports regions (same id or data-region) whose box
|
|
35
|
+
moved by more than 1px, and any change in document
|
|
36
|
+
scroll height. Exits 1 when one did. Exits 2 when no
|
|
37
|
+
region was marked and the scroll height did not change,
|
|
38
|
+
which is not a pass. Settled shots are named with -settled.
|
|
32
39
|
--json Print a JSON manifest instead of text
|
|
33
40
|
|
|
34
41
|
Set SHOOT_CHROME_FLAGS for extra browser flags. --no-sandbox is added
|
|
35
42
|
automatically when CI is set or when running as root.
|
|
36
43
|
|
|
37
|
-
Also reports: horizontal overflow,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
44
|
+
Also reports, per capture: horizontal overflow, elements wider than the
|
|
45
|
+
viewport, stranded words in wrapped text, tap targets under 44px and
|
|
46
|
+
selectable control text (widths < 768), text under 12px, inputs under 16px,
|
|
47
|
+
a viewport that disables zoom, :hover rules outside @media (hover: hover),
|
|
48
|
+
a missing tap-highlight reset when the page has controls, console errors
|
|
49
|
+
and failed requests. Set CHROME_PATH to override browser discovery. The
|
|
50
|
+
temporary browser profile is deleted afterwards. Exits 1 if a page fails
|
|
51
|
+
to load, returns an HTTP error, or --shift finds a moved region or a
|
|
52
|
+
document scroll height change of more than 1px. Exits 2 when --shift could
|
|
53
|
+
not compare a marked region and the scroll height did not change. A bare
|
|
43
54
|
host:port (localhost:3000) is treated as http://.
|
|
55
|
+
The layout-shift entries on the performance timeline are not read. Headless
|
|
56
|
+
Chrome can report none of them for a jump that has already painted, so a
|
|
57
|
+
score of 0 would pass a page that moved. The two-page box compare is the check.
|
|
44
58
|
`;
|
|
45
59
|
|
|
60
|
+
function resolveTarget(value) {
|
|
61
|
+
if (/^[a-z]+:\/\//i.test(value)) return value;
|
|
62
|
+
return !existsSync(value) && /^(?:localhost|[\w-]+(?:\.[\w-]+)+|\d+\.\d+\.\d+\.\d+)(?::\d+)?(?:\/|$)|^[\w.-]+:\d+/.test(value)
|
|
63
|
+
? `http://${value}` : pathToFileURL(resolve(value)).href;
|
|
64
|
+
}
|
|
65
|
+
|
|
46
66
|
function parse(argv) {
|
|
47
|
-
const o = { target: "", out: "shots", widths: [390, 1440], height: 0, themes: ["light", "dark"],
|
|
48
|
-
motion: "reduce", full: false, wait: 600, frames: 0, every: 100, click: "", scale: 2, json: false };
|
|
67
|
+
const o = { target: "", shift: "", out: "shots", widths: [390, 1440], height: 0, themes: ["light", "dark"],
|
|
68
|
+
motion: "reduce", full: false, wait: 600, frames: 0, every: 100, click: "", scale: 2, shotSuffix: "", json: false };
|
|
49
69
|
for (let i = 0; i < argv.length; i++) {
|
|
50
70
|
const a = argv[i], v = () => { const x = argv[++i]; if (x === undefined) throw new Error(`${a} needs a value`); return x; };
|
|
51
71
|
if (a === "--help" || a === "-h") { process.stdout.write(HELP); process.exit(0); }
|
|
@@ -60,14 +80,14 @@ function parse(argv) {
|
|
|
60
80
|
else if (a === "--every") o.every = Number(v());
|
|
61
81
|
else if (a === "--click") o.click = v();
|
|
62
82
|
else if (a === "--scale") o.scale = Number(v());
|
|
83
|
+
else if (a === "--shift") o.shift = v();
|
|
63
84
|
else if (a === "--json") o.json = true;
|
|
64
85
|
else if (a.startsWith("--")) throw new Error(`Unknown option ${a}`);
|
|
65
86
|
else o.target = a;
|
|
66
87
|
}
|
|
67
88
|
if (!o.target) { process.stdout.write(HELP); process.exit(1); }
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
? `http://${o.target}` : pathToFileURL(resolve(o.target)).href;
|
|
89
|
+
o.target = resolveTarget(o.target);
|
|
90
|
+
if (o.shift) o.shift = resolveTarget(o.shift);
|
|
71
91
|
if (o.frames) o.motion = "no-preference";
|
|
72
92
|
return o;
|
|
73
93
|
}
|
|
@@ -197,7 +217,7 @@ async function capture(cdp, o, width, theme) {
|
|
|
197
217
|
if (!o.frames) await sleep(o.wait);
|
|
198
218
|
}
|
|
199
219
|
const files = [], frameTimes = [];
|
|
200
|
-
const base = `${String(width)}-${theme}`;
|
|
220
|
+
const base = `${String(width)}-${theme}${o.shotSuffix || ""}`;
|
|
201
221
|
if (o.frames) {
|
|
202
222
|
// Capture first, measure the page afterwards, so frame 0 is as close to the trigger as possible.
|
|
203
223
|
for (let f = 0; f < o.frames; f++) {
|
|
@@ -277,8 +297,64 @@ async function capture(cdp, o, width, theme) {
|
|
|
277
297
|
const size = parseFloat(getComputedStyle(el).fontSize);
|
|
278
298
|
if (size < 12) { tinyText.push(label(el) + " " + size + "px"); if (tinyText.length >= 5) break; }
|
|
279
299
|
}
|
|
300
|
+
const inputsUnder16 = [];
|
|
301
|
+
for (const el of document.querySelectorAll("input:not([type=hidden]):not([type=checkbox]):not([type=radio]):not([type=range]):not([type=button]):not([type=submit]):not([type=reset]):not([type=file]):not([type=image]), textarea, select")) {
|
|
302
|
+
if (!visible(el)) continue;
|
|
303
|
+
const size = parseFloat(getComputedStyle(el).fontSize);
|
|
304
|
+
if (size < 16) { inputsUnder16.push((el.getAttribute("aria-label") || el.id || el.getAttribute("name") || el.getAttribute("placeholder") || el.tagName.toLowerCase()) + " " + (Math.round(size * 10) / 10) + "px"); if (inputsUnder16.length >= 8) break; }
|
|
305
|
+
}
|
|
306
|
+
const viewportMeta = document.querySelector('meta[name="viewport" i]')?.getAttribute("content") || "";
|
|
307
|
+
const zoomLocked = /user-scalable\\s*=\\s*(?:no|0)\\b|maximum-scale\\s*=\\s*1(?:\\.0+)?(?![.\\d])/i.test(viewportMeta);
|
|
308
|
+
const ungatedHovers = [];
|
|
309
|
+
let ungatedHoverCount = 0, tapHighlightDisabled = false;
|
|
310
|
+
const coversRoot = selector => selector.split(",").some(s => /^(?:html|:root|\\*|body)(?:\\s|$)/.test(s.trim()));
|
|
311
|
+
const visitRules = (rules, media) => {
|
|
312
|
+
if (!rules) return;
|
|
313
|
+
for (const rule of rules) {
|
|
314
|
+
if (rule.type === 4) visitRules(rule.cssRules, (media ? media + " " : "") + (rule.conditionText || ""));
|
|
315
|
+
else if (rule.type === 3 && rule.styleSheet) { try { visitRules(rule.styleSheet.cssRules, media); } catch (e) { /* unreadable import */ } }
|
|
316
|
+
else {
|
|
317
|
+
if (rule.type === 1 && rule.selectorText) {
|
|
318
|
+
if (/:hover\\b/.test(rule.selectorText) && !/\\(\\s*(?:any-)?hover\\s*:\\s*hover\\s*\\)/.test(media) && !/hover\\s*:\\s*none/.test(media)) {
|
|
319
|
+
ungatedHoverCount++;
|
|
320
|
+
if (ungatedHovers.length < 8) ungatedHovers.push(rule.selectorText.slice(0, 100));
|
|
321
|
+
}
|
|
322
|
+
const tap = rule.style && rule.style.getPropertyValue("-webkit-tap-highlight-color");
|
|
323
|
+
if (tap && /transparent|rgba\\(\\s*0\\s*,\\s*0\\s*,\\s*0\\s*,\\s*0\\s*\\)/i.test(tap) && coversRoot(rule.selectorText)) tapHighlightDisabled = true;
|
|
324
|
+
}
|
|
325
|
+
if (rule.cssRules && rule.type !== 4) visitRules(rule.cssRules, media);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
const seenSheets = new Set();
|
|
330
|
+
for (const sheet of [...document.styleSheets, ...(document.adoptedStyleSheets || [])]) {
|
|
331
|
+
if (seenSheets.has(sheet)) continue;
|
|
332
|
+
seenSheets.add(sheet);
|
|
333
|
+
try { visitRules(sheet.cssRules, ""); } catch (e) { /* cross-origin sheet */ }
|
|
334
|
+
}
|
|
335
|
+
const inlineTap = document.documentElement.style.getPropertyValue("-webkit-tap-highlight-color");
|
|
336
|
+
if (inlineTap && /transparent/i.test(inlineTap)) tapHighlightDisabled = true;
|
|
337
|
+
const hasControls = !!document.querySelector("a[href], button, [role=button], input, select, textarea");
|
|
338
|
+
const selectableControls = [];
|
|
339
|
+
if (vw < 768) for (const el of document.querySelectorAll("button, [role=button]")) {
|
|
340
|
+
if (!visible(el) || !(el.innerText || "").trim()) continue;
|
|
341
|
+
const cs = getComputedStyle(el);
|
|
342
|
+
if ((cs.userSelect || cs.webkitUserSelect) !== "none") { selectableControls.push(label(el)); if (selectableControls.length >= 8) break; }
|
|
343
|
+
}
|
|
344
|
+
const regions = [], seenKeys = new Set();
|
|
345
|
+
for (const el of document.querySelectorAll("[id], [data-region]")) {
|
|
346
|
+
if (!visible(el)) continue;
|
|
347
|
+
const key = el.id ? "#" + el.id : "region:" + el.getAttribute("data-region");
|
|
348
|
+
if (seenKeys.has(key)) continue;
|
|
349
|
+
seenKeys.add(key);
|
|
350
|
+
const box = el.getBoundingClientRect();
|
|
351
|
+
regions.push({ key, top: Math.round(box.top), left: Math.round(box.left), width: Math.round(box.width), height: Math.round(box.height) });
|
|
352
|
+
if (regions.length >= 80) break;
|
|
353
|
+
}
|
|
280
354
|
return { scrollWidth: document.documentElement.scrollWidth, viewport: vw, overflowing: wide.map(w => w.text), stranded, smallTargets, tinyText,
|
|
281
|
-
height: document.documentElement.scrollHeight, title: document.title
|
|
355
|
+
height: document.documentElement.scrollHeight, title: document.title,
|
|
356
|
+
inputsUnder16, zoomLocked, ungatedHovers, ungatedHoverCount, tapHighlightDisabled: hasControls ? tapHighlightDisabled : null,
|
|
357
|
+
selectableControls, regions };
|
|
282
358
|
})()` });
|
|
283
359
|
if (!o.frames) {
|
|
284
360
|
const shot = await cdp.send("Page.captureScreenshot", { format: "png", captureBeyondViewport: o.full });
|
|
@@ -287,10 +363,36 @@ async function capture(cdp, o, width, theme) {
|
|
|
287
363
|
}
|
|
288
364
|
const l = layout.result.value;
|
|
289
365
|
return { width, height, theme, motion: o.motion, files, ...(o.frames ? { frameTimesMs: frameTimes } : {}), title: l.title,
|
|
366
|
+
scrollHeight: l.height, regions: l.regions,
|
|
367
|
+
inputsUnder16: l.inputsUnder16, zoomLocked: l.zoomLocked, ungatedHovers: l.ungatedHovers, ungatedHoverCount: l.ungatedHoverCount,
|
|
368
|
+
tapHighlightDisabled: l.tapHighlightDisabled, selectableControls: l.selectableControls,
|
|
290
369
|
horizontalOverflow: l.scrollWidth > l.viewport ? `${l.scrollWidth - l.viewport}px` : null,
|
|
291
370
|
overflowingElements: l.overflowing, strandedWords: l.stranded, smallTapTargets: l.smallTargets, tinyText: l.tinyText, ...issues };
|
|
292
371
|
}
|
|
293
372
|
|
|
373
|
+
// Same id or data-region in both pages. A disappearing spinner is not a shift;
|
|
374
|
+
// a card that grows, or a control that moves, is.
|
|
375
|
+
function compareShift(before, after) {
|
|
376
|
+
const settled = new Map((after.regions || []).map(r => [r.key, r]));
|
|
377
|
+
const shifts = [];
|
|
378
|
+
let shared = 0;
|
|
379
|
+
for (const region of before.regions || []) {
|
|
380
|
+
const other = settled.get(region.key);
|
|
381
|
+
if (!other) continue;
|
|
382
|
+
shared++;
|
|
383
|
+
const dx = other.left - region.left, dy = other.top - region.top, dw = other.width - region.width, dh = other.height - region.height;
|
|
384
|
+
if (Math.abs(dx) > 1 || Math.abs(dy) > 1 || Math.abs(dw) > 1 || Math.abs(dh) > 1)
|
|
385
|
+
shifts.push({ key: region.key, before: region, after: other, dx, dy, dw, dh });
|
|
386
|
+
}
|
|
387
|
+
const scrollDelta = (after.scrollHeight ?? 0) - (before.scrollHeight ?? 0);
|
|
388
|
+
return {
|
|
389
|
+
scrollBefore: before.scrollHeight ?? null, scrollAfter: after.scrollHeight ?? null,
|
|
390
|
+
scrollDelta: Math.abs(scrollDelta) > 1 ? scrollDelta : 0, shared, shifts,
|
|
391
|
+
settledFiles: after.files,
|
|
392
|
+
...(shared === 0 ? { note: "no shared id or data-region; only the document scroll height was compared" } : {}),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
294
396
|
async function main() {
|
|
295
397
|
const o = parse(process.argv.slice(2));
|
|
296
398
|
mkdirSync(o.out, { recursive: true });
|
|
@@ -300,15 +402,31 @@ async function main() {
|
|
|
300
402
|
for (const width of o.widths) for (const theme of o.themes) {
|
|
301
403
|
const cdp = await connect(browser.port);
|
|
302
404
|
await cdp.send("Page.enable"); await cdp.send("Runtime.enable"); await cdp.send("Network.enable");
|
|
303
|
-
|
|
405
|
+
const before = await capture(cdp, o, width, theme);
|
|
304
406
|
cdp.close();
|
|
407
|
+
if (o.shift) {
|
|
408
|
+
const settled = await connect(browser.port);
|
|
409
|
+
await settled.send("Page.enable"); await settled.send("Runtime.enable"); await settled.send("Network.enable");
|
|
410
|
+
const after = await capture(settled, { ...o, target: o.shift, shotSuffix: "-settled" }, width, theme);
|
|
411
|
+
settled.close();
|
|
412
|
+
before.shift = compareShift(before, after);
|
|
413
|
+
if (after.loadError && !before.loadError) before.loadError = `settled page: ${after.loadError}`;
|
|
414
|
+
}
|
|
415
|
+
results.push(before);
|
|
305
416
|
}
|
|
306
417
|
} finally { await browser.close(); }
|
|
307
418
|
const failed = results.some(r => r.loadError);
|
|
308
|
-
|
|
419
|
+
const shifted = results.some(r => r.shift && (r.shift.shifts.length > 0 || r.shift.scrollDelta));
|
|
420
|
+
// No marked region and no scroll change means the jump was not measured.
|
|
421
|
+
// Scroll height stays at the viewport when both states fit, so that case must not pass.
|
|
422
|
+
const unchecked = results.some(r => r.shift && r.shift.shared === 0 && !r.shift.scrollDelta);
|
|
423
|
+
if (failed || shifted) process.exitCode = 1;
|
|
424
|
+
else if (unchecked) process.exitCode = 2;
|
|
309
425
|
if (o.json) { process.stdout.write(JSON.stringify({ target: o.target, captures: results }, null, 2) + "\n"); return; }
|
|
310
426
|
for (const r of results) {
|
|
311
|
-
const
|
|
427
|
+
const shiftFlag = r.shift && (r.shift.shifts.length > 0 || r.shift.scrollDelta) ? "layout shift" : "";
|
|
428
|
+
const uncheckedFlag = r.shift && r.shift.shared === 0 && !r.shift.scrollDelta ? "shift not checked" : "";
|
|
429
|
+
const flags = [r.loadError, shiftFlag, uncheckedFlag, r.horizontalOverflow && `horizontal overflow ${r.horizontalOverflow}`,
|
|
312
430
|
r.consoleErrors.length && `${r.consoleErrors.length} console error(s)`,
|
|
313
431
|
r.failedRequests.length && `${r.failedRequests.length} failed request(s)`].filter(Boolean);
|
|
314
432
|
process.stdout.write(`${r.width}px ${r.theme}: ${r.files.join(", ")}${flags.length ? " !! " + flags.join("; ") : ""}\n`);
|
|
@@ -316,6 +434,20 @@ async function main() {
|
|
|
316
434
|
for (const e of r.strandedWords) process.stdout.write(` stranded words: ${e}\n`);
|
|
317
435
|
for (const e of r.smallTapTargets) process.stdout.write(` tap target < 44px: ${e}\n`);
|
|
318
436
|
for (const e of r.tinyText) process.stdout.write(` text < 12px: ${e}\n`);
|
|
437
|
+
for (const e of r.inputsUnder16 || []) process.stdout.write(` input under 16px: ${e}\n`);
|
|
438
|
+
if (r.zoomLocked) process.stdout.write(` viewport disables zoom\n`);
|
|
439
|
+
if (r.ungatedHoverCount) process.stdout.write(` hover not gated (${r.ungatedHoverCount}): ${(r.ungatedHovers || []).join("; ")}\n`);
|
|
440
|
+
if (r.tapHighlightDisabled === false) process.stdout.write(` tap highlight not disabled\n`);
|
|
441
|
+
for (const e of r.selectableControls || []) process.stdout.write(` control text selectable: ${e}\n`);
|
|
442
|
+
if (r.shift) {
|
|
443
|
+
if (r.shift.note) process.stdout.write(` shift: ${r.shift.note}\n`);
|
|
444
|
+
if (r.shift.scrollDelta) process.stdout.write(` document scroll height ${r.shift.scrollDelta > 0 ? "+" : ""}${r.shift.scrollDelta}px after load\n`);
|
|
445
|
+
for (const s of r.shift.shifts.slice(0, 12)) {
|
|
446
|
+
const part = (n, label) => n ? `${label} ${n > 0 ? "+" : ""}${n}px` : "";
|
|
447
|
+
const bits = [part(s.dy, "top"), part(s.dh, "height"), part(s.dx, "left"), part(s.dw, "width")].filter(Boolean);
|
|
448
|
+
process.stdout.write(` layout shift ${s.key}: ${bits.join(", ")}\n`);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
319
451
|
for (const e of [...new Set(r.consoleErrors)].slice(0, 5)) process.stdout.write(` console: ${e}\n`);
|
|
320
452
|
for (const e of [...new Set(r.failedRequests)].slice(0, 5)) process.stdout.write(` request: ${e}\n`);
|
|
321
453
|
}
|
package/skills/deslop/SKILL.md
CHANGED
|
@@ -33,7 +33,11 @@ Work through [prose patterns](references/prose-patterns.md). The ones that matte
|
|
|
33
33
|
|
|
34
34
|
## 3. Fix UI
|
|
35
35
|
|
|
36
|
-
Run the scan on components and check the rendered page against `web-design`'s anti-patterns if installed. The usual repairs: delete the eyebrow labels and explainer subtitles; replace gradient text with solid ink; remove glass, glows and sparkles; flatten nested cards; give CTAs real verbs; remove emoji from interface copy.
|
|
36
|
+
Run the scan on components and check the rendered page against `web-design`'s anti-patterns if installed. The usual repairs: delete the eyebrow labels and explainer subtitles; replace gradient text with solid ink; remove glass, glows and sparkles; flatten nested cards; give CTAs real verbs; remove emoji from interface copy.
|
|
37
|
+
|
|
38
|
+
This season's tells, each fine when the brief asks and slop when it shows up anyway: a pill tag above the heading (`New`), one italic word in the headline, a divider under every section, four opacities of the same neutral instead of two inks, a border on every box, a diagram that draws the whole system, a decorative `Home / Settings / Billing`, and `01` `02` `03` on items that are not a sequence. The scan flags the numbered markers (`section-index`) and the opacity ladder (`opacity-ramp`). The rest need the render. Instrument Serif with tight tracking is the current reflex display; it stays only when DESIGN.md names it.
|
|
39
|
+
|
|
40
|
+
Re-shoot and look after removing structure: a deleted container can take a needed grouping with it.
|
|
37
41
|
|
|
38
42
|
## 4. Fix code
|
|
39
43
|
|
|
@@ -65,6 +65,7 @@ const RULES = [
|
|
|
65
65
|
["caps-eyebrow", "ui", "info", /\buppercase\b[^"'`]*\btracking-(?:widest|\[0\.[1-9]\d*em\])|\btracking-(?:widest|\[0\.[1-9]\d*em\])[^"'`]*\buppercase\b|text-transform:\s*uppercase;\s*letter-spacing:\s*0?\.[12]\d*em/g, "Tracked-out all-caps label; the eyebrow above every heading."],
|
|
66
66
|
["generic-cta", "ui", "info", />\s*(?:Get Started|Learn More|Start (?:your )?(?:free )?trial|Join the waitlist|Explore (?:now|features))\s*</g, "Generic CTA label; name what happens next ('Create a listing', 'See rates')."],
|
|
67
67
|
["emoji-in-ui", "ui", "medium", />[^<{]*?(?:🚀|✨|🔥|💡|⚡|🎉|👋|💪|🙌|📈|🎯)[^<]*?</gu, "Emoji in interface copy; usually stands in for an icon system or a point of view."],
|
|
68
|
+
["section-index", "ui", "info", />\s*0[1-9]\s*(?:<\/|—|–|-|\/)/g, "Numbered marker (01, 02) on content that is not a sequence. Delete it, or keep it only where order is the point."],
|
|
68
69
|
// Motion and mobile details that make an interface feel slightly off.
|
|
69
70
|
["transition-all", "ui", "medium", /\btransition-all\b|transition:\s*all\b/g, "Transitions every property, including layout ones nobody meant to animate. Name them: transform, opacity."],
|
|
70
71
|
["ease-in-ui", "ui", "info", /\bease-in\b(?!-out)|(?:transition|animation)(?:-timing-function)?:[^;]*\bease-in\b(?!-out)/g, "ease-in starts slowly, exactly when the user is watching; UI enters and exits on ease-out."],
|
|
@@ -161,6 +162,9 @@ function scanFile(file, o) {
|
|
|
161
162
|
const acid = hexes.some(([r, g, b]) => (g >= 0xd0 && r >= 0x80 && r <= 0xe0 && b <= 0x60) || (r >= 0xe0 && g <= 0x60 && b <= 0x40)) || /\b(?:text|bg|border)-lime-\d{3}\b/.test(src);
|
|
162
163
|
if (cream && serif) density.push({ rule: "warm-editorial", severity: "medium", detail: `cream background with a serif display${clay ? " and a clay/terracotta accent" : ""}`, why: "Today's most common generated look; fine if the brief asked for it, slop if it arrived by default." });
|
|
163
164
|
if (nearBlack && acid) density.push({ rule: "dark-acid", severity: "info", detail: "near-black background with an acid/vermilion accent", why: "A current generated-design default; check it was a choice." });
|
|
165
|
+
const opacities = [...src.matchAll(/\b(?:text|bg)-(?:black|white|neutral|zinc|gray|slate|stone)\/(?:[1-9]\d)\b/g)].map(m => m[0].replace(/.*\//, ""));
|
|
166
|
+
const distinct = [...new Set(opacities)];
|
|
167
|
+
if (distinct.length >= 4) density.push({ rule: "opacity-ramp", severity: "info", detail: `${distinct.length} opacities of one neutral (${distinct.sort().join(", ")})`, why: "A ladder of opacities standing in for a type scale. Two inks, primary and secondary, are enough unless the brand defines more." });
|
|
164
168
|
}
|
|
165
169
|
const boldLeads = hits.filter(h => h.rule === "bold-lead-list").length;
|
|
166
170
|
if (boldLeads > DENSITY["bold-lead-list"]) density.push({ rule: "bold-lead-density", severity: "medium", detail: `${boldLeads} bold-lead bullets`, why: "Every list as '**Label:** text' reads as generated; use prose or a table where it fits." });
|
package/skills/dogfood/SKILL.md
CHANGED
|
@@ -7,6 +7,8 @@ description: "Walk real product journeys, fix authorized problems and replay thr
|
|
|
7
7
|
|
|
8
8
|
Tests check what the author thought of. Dogfooding finds what a user hits. The output is fixed journeys, not a list of complaints.
|
|
9
9
|
|
|
10
|
+
`<skill-dir>` below means this skill's own folder. Run its scripts from the project root so output lands in the project, not in the installed skill.
|
|
11
|
+
|
|
10
12
|
## 0. Pick the mode
|
|
11
13
|
|
|
12
14
|
- **Diff-aware** (default on a branch): start from what the branch changed (`git diff origin/main...HEAD --stat`), map each change to the screens and journeys it touches, and test those first.
|
|
@@ -15,6 +17,8 @@ Tests check what the author thought of. Dogfooding finds what a user hits. The o
|
|
|
15
17
|
|
|
16
18
|
## 1. Pick journeys that matter
|
|
17
19
|
|
|
20
|
+
When the question is whether the product lives up to its page, or before a launch, list the claims first and test those: [claims audit](references/claims-audit.md).
|
|
21
|
+
|
|
18
22
|
Two to five, each written as a user with a goal and an observable end:
|
|
19
23
|
|
|
20
24
|
> New seller, signed in, no listings → publishes first listing → sees it on the public page with the right price.
|
|
@@ -28,6 +32,7 @@ Use the browser session and account the user selected; never sign in as someone
|
|
|
28
32
|
- Real clicks and typing in a real browser, at real speed. No mocked responses, no seeding past the hard part.
|
|
29
33
|
- Follow through to the outcome: the record exists, the email arrived, the export opens and contains the right rows, the public page shows the change.
|
|
30
34
|
- Look at each screen. Capture the important ones at phone and desktop width with [`scripts/shoot.mjs`](scripts/shoot.mjs) when the URL is reachable without your session; use the browser tool's screenshots for signed-in states.
|
|
35
|
+
- When the journey waits on data, capture the waiting page and the settled page and compare them: `node <skill-dir>/scripts/shoot.mjs <waiting> --shift <settled> --widths 390 --themes light`. Exit 1 is a finding (a marked region or the document moved). Exit 2 means the regions were not marked, so the jump was not measured. Read the phone lines on that capture too: input size, zoom lock, ungated hover, selectable controls, tap highlight. A 390px picture can look aligned and still jump, zoom and stick. The lines that need a physical phone (a hover that stays after a tap, the notch, the keyboard) stay named as unverified.
|
|
31
36
|
- Keep the console and network panel open. A 500 behind a friendly toast is a finding.
|
|
32
37
|
- Note friction as well as failure: a step that needs thinking, a label that misleads, a wait with no feedback.
|
|
33
38
|
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Claims audit
|
|
2
|
+
|
|
3
|
+
Use this when the question is whether the product lives up to how it is sold. Passing tests and tidy screens do not answer it. Each claim on the page has to survive contact with the running product.
|
|
4
|
+
|
|
5
|
+
## 1. Write the claims down
|
|
6
|
+
|
|
7
|
+
Read the landing page, pricing and any product copy. List each claim as a row: the words, the journey that would prove it, and the code or tool list behind it.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
Claim Journey Evidence Result
|
|
11
|
+
"prepares a reply" tenant message to approval contract.ts holds, fixed template per category
|
|
12
|
+
"you approve what goes out" message arrives service.ts false: receipts send automatically
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## 2. Read the verbs
|
|
16
|
+
|
|
17
|
+
Most overclaims hide in one word. Check what the code does before accepting these:
|
|
18
|
+
|
|
19
|
+
| The copy says | Check | Often really |
|
|
20
|
+
| --- | --- | --- |
|
|
21
|
+
| writes, drafts, personalises | Does a model write this text, or is it picked? | A fixed template, so "prepares" or "suggests" |
|
|
22
|
+
| arranges, handles, manages, follows up | Does the software act, or remind a person to? | A reminder. In a regulated trade these words can also carry legal weight |
|
|
23
|
+
| everything, every, always, never, nothing | Read the code for one exception | An automatic receipt or an opt-in send |
|
|
24
|
+
| connects to, reads, works with | Compare with the real tool or API list | Most of it, minus the item the sentence names |
|
|
25
|
+
| live, real time, instantly | Find the schedule | A daily job or a cached read |
|
|
26
|
+
|
|
27
|
+
A claim can be true and still need rewording. Name the exception on the page where the claim is made.
|
|
28
|
+
|
|
29
|
+
## 3. Walk it as a new customer
|
|
30
|
+
|
|
31
|
+
Use a fresh account with no data. Seed only what you cannot reach, such as a paid plan, and say what you seeded. Run the journey through to the outcome, then do these checks that tests rarely cover:
|
|
32
|
+
|
|
33
|
+
- Reload the page after every approve, send or save. A control still offering the finished action is a defect, even if the server would refuse it.
|
|
34
|
+
- Compare counts with rows. A headline count must equal the rows you can reach. A capped list says so, and the cap never hides urgent work.
|
|
35
|
+
- Make one source fail. "All clear" and "nothing to do" may only appear when every source answered.
|
|
36
|
+
- Open the last screen a customer's customer sees. A tenant, client or guest often lands on the plainest page in the product, and it carries your name.
|
|
37
|
+
- Read inner screens as closely as the hero. Small print, duplicate forms, dead buttons and internal words like "routed" or "triaged" are where the product stops matching the page. Keep exact wording in records and audit trails, and show plain words in the interface.
|
|
38
|
+
- Compare the picture with the flow. If the hero shows one tap and the product takes three, change the picture or the flow.
|
|
39
|
+
|
|
40
|
+
## 4. Sort what you find
|
|
41
|
+
|
|
42
|
+
A guard that refuses your test data is usually working. Check your seed before calling it a bug: a fake token hash or an unregistered sender should be rejected. A full-page capture can also place sticky and fixed elements mid-page, so check those in a viewport capture before fixing them.
|
|
43
|
+
|
|
44
|
+
Report what holds as well as what does not. Every gap gets one of three outcomes: fix the product, reword the claim, or remove it. Name anything you could not exercise, such as real delivery or a paid checkout.
|