start-vibing 4.3.0 → 4.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (22) hide show
  1. package/package.json +2 -2
  2. package/template/.claude/agents/sd-audit.md +121 -2
  3. package/template/.claude/agents/sd-fix.md +11 -0
  4. package/template/.claude/agents/sd-research.md +49 -2
  5. package/template/.claude/skills/super-design/.schema-version +1 -0
  6. package/template/.claude/skills/super-design/SKILL.md +94 -2
  7. package/template/.claude/skills/super-design/audit-state.schema.json +226 -0
  8. package/template/.claude/skills/super-design/references/audit-methodology.md +118 -0
  9. package/template/.claude/skills/super-design/references/design-intelligence-rubric.md +92 -11
  10. package/template/.claude/skills/super-design/references/design-skills-catalog.md +31 -0
  11. package/template/.claude/skills/super-design/scripts/build-import-graph.sh +208 -0
  12. package/template/.claude/skills/super-design/scripts/detect-apps.sh +180 -0
  13. package/template/.claude/skills/super-design/scripts/detect-changes.sh +177 -21
  14. package/template/.claude/skills/super-design/scripts/discover-routes.sh +120 -6
  15. package/template/.claude/skills/super-design/scripts/extract-tokens.mjs +165 -4
  16. package/template/.claude/skills/super-design/scripts/hash-pages.sh +209 -23
  17. package/template/.claude/skills/super-design/scripts/setup-git-notes.sh +21 -0
  18. package/template/.claude/skills/super-design/scripts/validate-state.sh +74 -11
  19. package/template/.claude/skills/super-design/scripts/verify-audit.sh +62 -9
  20. package/template/.claude/skills/super-design/scripts/visual-regression.sh +275 -0
  21. package/template/.claude/skills/super-design/scripts/write-state.sh +53 -0
  22. package/template/.claude/skills/super-design/templates/audit-state.schema.json +0 -57
@@ -0,0 +1,275 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: visual-regression.sh [--update-baselines] [<state_file>]
3
+ #
4
+ # Expected `.audit-state.json` `visual_regression` block (artifact §16
5
+ # lines 1367-1384). Kept inline here so the script self-documents the
6
+ # schema shape when audit-state.schema.json is not yet present.
7
+ #
8
+ # "visual_regression": {
9
+ # "enabled": true,
10
+ # "engine": "pixelmatch", // pixelmatch | odiff | sha256-fallback
11
+ # "threshold": 0.1, // per-pixel YIQ (pixelmatch/odiff)
12
+ # "max_diff_pixel_ratio": 0.01, // 1% pixels allowed
13
+ # "antialiasing": true,
14
+ # "viewports": [
15
+ # { "label": "mobile_375", "width": 375, "height": 667 },
16
+ # { "label": "tablet_768", "width": 768, "height": 1024 },
17
+ # { "label": "desktop_1280","width": 1280, "height": 800 }
18
+ # ],
19
+ # "mask_selectors": [
20
+ # "[data-timestamp]", ".relative-time",
21
+ # "[data-react-hydration]", "video", "canvas"
22
+ # ],
23
+ # "baseline_dir": ".super-design/baselines",
24
+ # "current_dir": "docs/super-design/.cache/hashes/screenshots",
25
+ # "diff_dir": "docs/super-design/.cache/hashes/diffs"
26
+ # }
27
+ #
28
+ # Runner behaviour:
29
+ # - Reads the block from .audit-state.json (default path overridable via
30
+ # positional arg).
31
+ # - For every (page, viewport) it finds a current screenshot for, locates
32
+ # the matching baseline PNG, runs the configured diff engine, emits
33
+ # {page, viewport, diff_ratio, threshold, pass, diff_image_path} into
34
+ # <diff_dir>/results.json.
35
+ # - Engine fallback chain: pixelmatch (npx) → odiff (npx) → sha256
36
+ # equality (logs warning on stderr).
37
+ # - --update-baselines replaces every baseline with the current capture
38
+ # and exits without diffing.
39
+ #
40
+ # POSIX sh + node; runs under Windows git-bash.
41
+ set -euo pipefail
42
+
43
+ UPDATE_BASELINES=0
44
+ STATE="docs/super-design/.audit-state.json"
45
+ while (( $# )); do
46
+ case "$1" in
47
+ --update-baselines) UPDATE_BASELINES=1 ;;
48
+ -h|--help)
49
+ grep '^#' "$0" | sed 's/^# \{0,1\}//'
50
+ exit 0
51
+ ;;
52
+ *) STATE="$1" ;;
53
+ esac
54
+ shift
55
+ done
56
+
57
+ if [[ ! -f "$STATE" ]]; then
58
+ echo '{"status":"missing-state","hint":"run audit first"}' >&2
59
+ exit 2
60
+ fi
61
+
62
+ # Defaults match artifact §16. jq pulls the override from state, if any.
63
+ CFG="$(jq -c '.visual_regression // {}' "$STATE")"
64
+ ENABLED="$(jq -r '.enabled // false' <<<"$CFG")"
65
+ if [[ "$ENABLED" != "true" && "$UPDATE_BASELINES" -ne 1 ]]; then
66
+ echo '{"status":"disabled","hint":"set visual_regression.enabled=true"}'
67
+ exit 0
68
+ fi
69
+
70
+ BASELINE_DIR="$(jq -r '.baseline_dir // ".super-design/baselines"' <<<"$CFG")"
71
+ CURRENT_DIR="$(jq -r '.current_dir // "docs/super-design/.cache/hashes/screenshots"' <<<"$CFG")"
72
+ DIFF_DIR="$(jq -r '.diff_dir // "docs/super-design/.cache/hashes/diffs"' <<<"$CFG")"
73
+ ENGINE="$(jq -r '.engine // "pixelmatch"' <<<"$CFG")"
74
+ THRESHOLD="$(jq -r '.threshold // 0.1' <<<"$CFG")"
75
+ MAX_RATIO="$(jq -r '.max_diff_pixel_ratio // 0.01' <<<"$CFG")"
76
+ ANTIALIAS="$(jq -r '.antialiasing // true' <<<"$CFG")"
77
+
78
+ mkdir -p "$BASELINE_DIR" "$DIFF_DIR"
79
+
80
+ # Enumerate current screenshots the hash-pages.sh pass wrote under
81
+ # <current_dir>/<url-encoded-url>/<viewport>.png.
82
+ if [[ ! -d "$CURRENT_DIR" ]]; then
83
+ echo "{\"status\":\"no-screenshots\",\"hint\":\"run hash-pages.sh first\",\"looked_in\":\"$CURRENT_DIR\"}" >&2
84
+ exit 2
85
+ fi
86
+
87
+ # --update-baselines: mirror current -> baseline and exit.
88
+ if [[ "$UPDATE_BASELINES" -eq 1 ]]; then
89
+ copied=0
90
+ while IFS= read -r -d '' png; do
91
+ rel="${png#$CURRENT_DIR/}"
92
+ dst="$BASELINE_DIR/$rel"
93
+ mkdir -p "$(dirname "$dst")"
94
+ cp -f "$png" "$dst"
95
+ copied=$((copied + 1))
96
+ done < <(find "$CURRENT_DIR" -type f -name '*.png' -print0)
97
+ echo "{\"status\":\"baselines-updated\",\"count\":$copied,\"baseline_dir\":\"$BASELINE_DIR\"}"
98
+ exit 0
99
+ fi
100
+
101
+ # Resolve engine availability up-front so we warn once rather than per page.
102
+ resolve_engine() {
103
+ local want="$1"
104
+ case "$want" in
105
+ pixelmatch)
106
+ if command -v npx >/dev/null 2>&1; then
107
+ if npx --yes pixelmatch --help >/dev/null 2>&1; then echo pixelmatch; return; fi
108
+ fi
109
+ ;;
110
+ odiff)
111
+ if command -v npx >/dev/null 2>&1; then
112
+ if npx --yes odiff-bin --help >/dev/null 2>&1; then echo odiff; return; fi
113
+ fi
114
+ ;;
115
+ esac
116
+ # Chain: pixelmatch > odiff > sha256.
117
+ if command -v npx >/dev/null 2>&1; then
118
+ if npx --yes pixelmatch --help >/dev/null 2>&1; then echo pixelmatch; return; fi
119
+ if npx --yes odiff-bin --help >/dev/null 2>&1; then echo odiff; return; fi
120
+ fi
121
+ echo sha256-fallback
122
+ }
123
+
124
+ RESOLVED_ENGINE="$(resolve_engine "$ENGINE")"
125
+ if [[ "$RESOLVED_ENGINE" == "sha256-fallback" ]]; then
126
+ echo "warning: pixelmatch/odiff unavailable; falling back to sha256 equality (binary pass/fail, diff_ratio will be 0 or 1)" >&2
127
+ fi
128
+
129
+ # Per-comparison worker. Runs in a node one-shot because pixelmatch /
130
+ # odiff are both invoked there; sha256 fallback is pure node too.
131
+ compare_pair() {
132
+ local baseline="$1"
133
+ local current="$2"
134
+ local diff_out="$3"
135
+
136
+ BASE="$baseline" CURR="$current" DIFF="$diff_out" \
137
+ ENGINE="$RESOLVED_ENGINE" THRESHOLD="$THRESHOLD" \
138
+ ANTIALIAS="$ANTIALIAS" \
139
+ node --experimental-vm-modules <<'JS'
140
+ import { createHash } from "node:crypto";
141
+ import { readFileSync, existsSync, writeFileSync, mkdirSync } from "node:fs";
142
+ import { dirname } from "node:path";
143
+ import { spawnSync } from "node:child_process";
144
+
145
+ const base = process.env.BASE;
146
+ const curr = process.env.CURR;
147
+ const diffPath = process.env.DIFF;
148
+ const engine = process.env.ENGINE;
149
+ const threshold = Number(process.env.THRESHOLD);
150
+ const antialias = process.env.ANTIALIAS === "true";
151
+
152
+ mkdirSync(dirname(diffPath), { recursive: true });
153
+
154
+ function sha(buf) { return createHash("sha256").update(buf).digest("hex"); }
155
+
156
+ if (!existsSync(base)) {
157
+ console.log(JSON.stringify({
158
+ pass: false, diff_ratio: 1, reason: "baseline-missing",
159
+ engine, diff_image_path: null,
160
+ }));
161
+ process.exit(0);
162
+ }
163
+
164
+ if (engine === "sha256-fallback") {
165
+ const bHash = sha(readFileSync(base));
166
+ const cHash = sha(readFileSync(curr));
167
+ const ratio = bHash === cHash ? 0 : 1;
168
+ console.log(JSON.stringify({
169
+ pass: ratio === 0, diff_ratio: ratio, engine,
170
+ diff_image_path: null,
171
+ }));
172
+ process.exit(0);
173
+ }
174
+
175
+ if (engine === "pixelmatch") {
176
+ // npx pixelmatch <baseline> <current> <diff> [threshold]
177
+ const r = spawnSync("npx", [
178
+ "--yes", "pixelmatch", base, curr, diffPath, String(threshold),
179
+ ], { encoding: "utf8", shell: process.platform === "win32" });
180
+ // pixelmatch prints e.g. "error: 123 different pixels\n" to stdout.
181
+ // total-pixels isn't emitted, so we read PNG dimensions from the diff
182
+ // header (the diff PNG is always produced even on zero-diff).
183
+ const diffPixels = Number((r.stdout.match(/(\d+)\s+different/) || [0, 0])[1]);
184
+ let total = 1;
185
+ if (existsSync(diffPath)) {
186
+ const buf = readFileSync(diffPath);
187
+ // PNG width/height live at bytes 16..23 big-endian.
188
+ if (buf.length >= 24 && buf[0] === 0x89 && buf[1] === 0x50) {
189
+ const w = buf.readUInt32BE(16);
190
+ const h = buf.readUInt32BE(20);
191
+ total = Math.max(1, w * h);
192
+ }
193
+ }
194
+ const ratio = diffPixels / total;
195
+ console.log(JSON.stringify({
196
+ pass: null, // decided by caller against max_diff_pixel_ratio
197
+ diff_ratio: ratio, diff_pixels: diffPixels, total_pixels: total,
198
+ engine, diff_image_path: diffPath,
199
+ exit: r.status, stderr: (r.stderr || "").trim().slice(0, 500),
200
+ }));
201
+ process.exit(0);
202
+ }
203
+
204
+ if (engine === "odiff") {
205
+ // odiff-bin <baseline> <current> <diff> --threshold=<t>
206
+ const args = [
207
+ "--yes", "odiff-bin", base, curr, diffPath,
208
+ `--threshold=${threshold}`,
209
+ ];
210
+ if (!antialias) args.push("--antialiasing");
211
+ const r = spawnSync("npx", args, {
212
+ encoding: "utf8", shell: process.platform === "win32",
213
+ });
214
+ const matchRatio = (r.stdout + r.stderr).match(/(\d+(?:\.\d+)?)\s*%/);
215
+ const pctDiff = matchRatio ? Number(matchRatio[1]) / 100 : (r.status === 0 ? 0 : 1);
216
+ console.log(JSON.stringify({
217
+ pass: null,
218
+ diff_ratio: pctDiff,
219
+ engine, diff_image_path: diffPath,
220
+ exit: r.status, stderr: (r.stderr || "").trim().slice(0, 500),
221
+ }));
222
+ process.exit(0);
223
+ }
224
+
225
+ // Unknown engine — surface as a hard failure rather than silent pass.
226
+ console.log(JSON.stringify({
227
+ pass: false, diff_ratio: 1,
228
+ reason: `unknown-engine:${engine}`, engine, diff_image_path: null,
229
+ }));
230
+ JS
231
+ }
232
+
233
+ results="[]"
234
+ any_fail=0
235
+ while IFS= read -r -d '' curr_png; do
236
+ rel="${curr_png#$CURRENT_DIR/}" # e.g. https%3A%2F%2F.../mobile_375.png
237
+ page_dir="$(dirname "$rel")"
238
+ viewport="$(basename "$rel" .png)"
239
+ page_url="$(printf '%s' "$page_dir" | node -e 'process.stdin.on("data",d=>process.stdout.write(decodeURIComponent(d.toString())))')"
240
+ base_png="$BASELINE_DIR/$rel"
241
+ diff_png="$DIFF_DIR/$rel"
242
+
243
+ raw="$(compare_pair "$base_png" "$curr_png" "$diff_png" || echo '{"pass":false,"diff_ratio":1,"reason":"compare-error"}')"
244
+
245
+ # Decide pass/fail against max_diff_pixel_ratio when engine left it null.
246
+ merged="$(jq -c \
247
+ --arg page "$page_url" \
248
+ --arg viewport "$viewport" \
249
+ --argjson threshold "$THRESHOLD" \
250
+ --argjson max_ratio "$MAX_RATIO" \
251
+ '. as $r
252
+ | .pass = (if .pass == null then (.diff_ratio <= $max_ratio) else .pass end)
253
+ | .page = $page
254
+ | .viewport = $viewport
255
+ | .threshold = $threshold
256
+ | .max_diff_pixel_ratio = $max_ratio' <<<"$raw")"
257
+
258
+ if [[ "$(jq -r '.pass' <<<"$merged")" != "true" ]]; then any_fail=1; fi
259
+ results="$(jq -c --argjson m "$merged" '. + [$m]' <<<"$results")"
260
+ done < <(find "$CURRENT_DIR" -type f -name '*.png' -print0)
261
+
262
+ out_file="$DIFF_DIR/results.json"
263
+ mkdir -p "$DIFF_DIR"
264
+ jq -n --argjson r "$results" \
265
+ --arg engine "$RESOLVED_ENGINE" \
266
+ --argjson threshold "$THRESHOLD" \
267
+ --argjson max_ratio "$MAX_RATIO" \
268
+ '{engine:$engine, threshold:$threshold, max_diff_pixel_ratio:$max_ratio,
269
+ results:$r, count: ($r|length),
270
+ passed: ([$r[] | select(.pass==true)] | length),
271
+ failed: ([$r[] | select(.pass!=true)] | length)}' \
272
+ >"$out_file"
273
+
274
+ cat "$out_file"
275
+ exit $any_fail
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env bash
2
+ # Usage: write-state.sh [<app_path>] [<explicit_target>]
3
+ # (reads JSON body from stdin)
4
+ #
5
+ # Atomic state writer: accepts JSON on stdin, writes to <target>.tmp,
6
+ # validates with jq, then renames in place. Referenced from
7
+ # docs/compass_artifact §11 ("Write-then-rename (atomic)") and SKILL.md
8
+ # Step 4 ("Atomic write .audit-state.json").
9
+ #
10
+ # Monorepo support (artifact §11 line 902): first positional arg is the
11
+ # app root (e.g. `apps/web`). Target state path is derived as
12
+ # `<app_path>/docs/super-design/.audit-state.json`. For single-app
13
+ # repos pass "." or omit (default behavior preserved).
14
+ # Back-compat: if the first arg already ends in `.audit-state.json`, it
15
+ # is treated as an explicit target path (legacy one-arg call sites).
16
+ set -euo pipefail
17
+
18
+ APP_PATH="${1:-.}"
19
+ EXPLICIT="${2:-}"
20
+
21
+ if [ -n "$EXPLICIT" ]; then
22
+ TARGET="$EXPLICIT"
23
+ else
24
+ case "$APP_PATH" in
25
+ *.audit-state.json)
26
+ # Legacy single-arg call: treat $APP_PATH as the full target path.
27
+ TARGET="$APP_PATH"
28
+ ;;
29
+ .|"")
30
+ TARGET="docs/super-design/.audit-state.json"
31
+ ;;
32
+ *)
33
+ TARGET="${APP_PATH%/}/docs/super-design/.audit-state.json"
34
+ ;;
35
+ esac
36
+ fi
37
+
38
+ TMP="${TARGET}.tmp"
39
+
40
+ mkdir -p "$(dirname "$TARGET")"
41
+
42
+ # Drain stdin into the tmp file.
43
+ cat >"$TMP"
44
+
45
+ # Validate it is parseable JSON before swapping.
46
+ if ! jq -e 'type == "object"' "$TMP" >/dev/null 2>&1; then
47
+ rm -f "$TMP"
48
+ echo '{"error":"invalid-json-on-stdin"}' >&2
49
+ exit 2
50
+ fi
51
+
52
+ mv -f "$TMP" "$TARGET"
53
+ echo "{\"status\":\"written\",\"path\":\"$TARGET\"}"
@@ -1,57 +0,0 @@
1
- {
2
- "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "$id": "super-design/audit-state.schema.json",
4
- "type": "object",
5
- "required": ["schema_version","skill_version","last_audit_at","git_sha_at_audit","theory_doc_sha","tools","pages_audited","findings_counts"],
6
- "properties": {
7
- "schema_version": { "type": "string", "pattern": "^\\d+\\.\\d+\\.\\d+$" },
8
- "skill_version": { "type": "string" },
9
- "last_audit_at": { "type": "string", "format": "date-time" },
10
- "git_sha_at_audit": { "type": "string", "pattern": "^[0-9a-f]{7,64}$" },
11
- "git_branch": { "type": "string" },
12
- "is_shallow_clone": { "type": "boolean" },
13
- "theory_doc_sha": { "type": "string" },
14
- "market_analysis_sha": { "type": "string" },
15
- "tools": { "type": "object", "additionalProperties": { "type": "string" } },
16
- "framework": {
17
- "type": "object",
18
- "properties": {
19
- "name": { "type": "string" },
20
- "router": { "type": "string" },
21
- "version": { "type": "string" }
22
- }
23
- },
24
- "route_map": { "type": "array", "items": { "type": "string" } },
25
- "pages_audited": {
26
- "type": "array",
27
- "items": {
28
- "type": "object",
29
- "required": ["url","last_audited"],
30
- "properties": {
31
- "url": { "type": "string" },
32
- "route_file": { "type": "string" },
33
- "html_hash": { "type": "string" },
34
- "dom_structure_hash": { "type": "string" },
35
- "viewport_hashes": { "type": "object" },
36
- "last_audited": { "type": "string", "format": "date-time" },
37
- "findings_ids": { "type": "array", "items": { "type": "string" } }
38
- }
39
- }
40
- },
41
- "components": { "type": "object", "additionalProperties": { "type": "string" } },
42
- "token_hash": { "type": "string" },
43
- "import_graph_sha": { "type": "string" },
44
- "findings_counts": {
45
- "type": "object",
46
- "required": ["blockers","high","medium","nitpicks"],
47
- "properties": {
48
- "blockers": { "type": "integer" },
49
- "high": { "type": "integer" },
50
- "medium": { "type": "integer" },
51
- "nitpicks": { "type": "integer" }
52
- }
53
- },
54
- "research_at": { "type": "string", "format": "date-time" },
55
- "ignored_paths": { "type": "array", "items": { "type": "string" } }
56
- }
57
- }