start-vibing 4.2.0 → 4.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/template/.claude/agents/sd-audit.md +288 -6
- package/template/.claude/agents/sd-fix.md +101 -2
- package/template/.claude/agents/sd-research.md +119 -3
- package/template/.claude/skills/mobile-app-patterns/SKILL.md +237 -0
- package/template/.claude/skills/super-design/.schema-version +1 -0
- package/template/.claude/skills/super-design/SKILL.md +34 -10
- package/template/.claude/skills/super-design/references/audit-methodology.md +118 -0
- package/template/.claude/skills/super-design/references/component-flow-discovery.md +258 -0
- package/template/.claude/skills/super-design/references/design-intelligence-rubric.md +457 -0
- package/template/.claude/skills/super-design/references/design-skills-catalog.md +133 -0
- package/template/.claude/skills/super-design/scripts/detect-changes.sh +113 -18
- package/template/.claude/skills/super-design/scripts/discover-routes.sh +7 -0
- package/template/.claude/skills/super-design/scripts/extract-tokens.mjs +19 -2
- package/template/.claude/skills/super-design/scripts/hash-pages.sh +6 -0
- package/template/.claude/skills/super-design/scripts/setup-git-notes.sh +21 -0
- package/template/.claude/skills/super-design/scripts/validate-state.sh +34 -2
- package/template/.claude/skills/super-design/scripts/write-state.sh +26 -0
|
@@ -1,30 +1,122 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Usage: detect-changes.sh <last_sha> [<last_iso>]
|
|
3
3
|
# Emits JSON: { mode, range_start, commits, files, classified }
|
|
4
|
+
#
|
|
5
|
+
# Implements the fallback ladder from artifact §6 + §14:
|
|
6
|
+
# 1. Anchor exists → diff with rename detection (-M90%).
|
|
7
|
+
# 2. Anchor missing + shallow repo → `git fetch --unshallow --no-tags`.
|
|
8
|
+
# 3. Still missing → fetch super-design git notes, retry.
|
|
9
|
+
# 4. Still missing → `--since=<iso>` time-based fallback.
|
|
10
|
+
# 5. Empty repo / no last_sha at all → diff against empty-tree SHA
|
|
11
|
+
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904 (artifact line 900).
|
|
12
|
+
# Also uses --first-parent + --cherry-pick --right-only when walking
|
|
13
|
+
# history (artifact §2.5 line 167-172).
|
|
4
14
|
set -euo pipefail
|
|
5
15
|
|
|
6
16
|
LAST_SHA="${1:-}"
|
|
7
17
|
LAST_ISO="${2:-}"
|
|
18
|
+
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
19
|
+
|
|
20
|
+
log() { printf '[detect-changes] %s\n' "$*" >&2; }
|
|
8
21
|
|
|
9
|
-
if [[ -z "$LAST_SHA" ]]; then echo '{"error":"missing last_sha"}'; exit 2; fi
|
|
10
22
|
if ! git rev-parse --git-dir >/dev/null 2>&1; then echo '{"error":"not-a-git-repo"}'; exit 3; fi
|
|
11
23
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
# Determine HEAD availability — empty repos have no commits at all.
|
|
25
|
+
if ! git rev-parse --verify --quiet HEAD >/dev/null; then
|
|
26
|
+
log "no HEAD commit; using empty-tree SHA fallback"
|
|
27
|
+
LAST_SHA="$EMPTY_TREE"
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# If caller didn't pass a last_sha, treat it as empty-tree (first audit).
|
|
31
|
+
if [[ -z "$LAST_SHA" ]]; then
|
|
32
|
+
log "missing last_sha; defaulting to empty-tree SHA"
|
|
33
|
+
LAST_SHA="$EMPTY_TREE"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
resolve_anchor() {
|
|
37
|
+
# Sets RANGE_START (may be empty if anchor unrecoverable).
|
|
38
|
+
if [[ "$LAST_SHA" == "$EMPTY_TREE" ]]; then
|
|
39
|
+
RANGE_START="$EMPTY_TREE"; return 0
|
|
40
|
+
fi
|
|
41
|
+
if git rev-parse --verify --quiet "${LAST_SHA}^{commit}" >/dev/null; then
|
|
42
|
+
if git merge-base --is-ancestor "$LAST_SHA" HEAD 2>/dev/null; then
|
|
43
|
+
RANGE_START="$LAST_SHA"
|
|
44
|
+
else
|
|
45
|
+
RANGE_START="$(git merge-base HEAD "$LAST_SHA" 2>/dev/null || echo "")"
|
|
46
|
+
fi
|
|
47
|
+
return 0
|
|
48
|
+
fi
|
|
49
|
+
RANGE_START=""
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if ! resolve_anchor; then
|
|
54
|
+
# Ladder step (a): unshallow if possible.
|
|
55
|
+
if [[ "$(git rev-parse --is-shallow-repository 2>/dev/null || echo false)" == "true" ]]; then
|
|
56
|
+
log "anchor missing and repo is shallow; attempting git fetch --unshallow --no-tags"
|
|
57
|
+
git fetch --unshallow --no-tags 2>/dev/null || log "unshallow fetch failed (continuing)"
|
|
58
|
+
resolve_anchor || true
|
|
59
|
+
fi
|
|
60
|
+
fi
|
|
61
|
+
if [[ -z "${RANGE_START:-}" ]]; then
|
|
62
|
+
# Ladder step (b): try to fetch super-design notes; they may pin a commit
|
|
63
|
+
# we don't have locally.
|
|
64
|
+
log "anchor still missing; fetching refs/notes/super-design"
|
|
65
|
+
git fetch origin '+refs/notes/super-design:refs/notes/super-design' 2>/dev/null \
|
|
66
|
+
|| log "notes fetch failed (continuing)"
|
|
67
|
+
resolve_anchor || true
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Ladder step (c): time-based fallback.
|
|
71
|
+
if [[ -z "${RANGE_START:-}" && -n "$LAST_ISO" ]]; then
|
|
72
|
+
log "using --since=$LAST_ISO time-based fallback"
|
|
73
|
+
FILES="$(git log --since="$LAST_ISO" --name-only --pretty=format: 2>/dev/null | sort -u | sed '/^$/d' || true)"
|
|
74
|
+
COMMITS="$(git log --first-parent --since="$LAST_ISO" --pretty=format:'%H|%s|%an|%aI' 2>/dev/null || true)"
|
|
75
|
+
MODE="since-time"
|
|
76
|
+
RANGE_START=""
|
|
77
|
+
elif [[ -z "${RANGE_START:-}" ]]; then
|
|
78
|
+
echo '{"error":"lost-anchor-no-fallback-time"}'; exit 4
|
|
23
79
|
else
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
80
|
+
# SHA range available. Use --name-status -M90% -z to catch renames AND
|
|
81
|
+
# filenames with spaces (NUL-terminated output).
|
|
82
|
+
RANGE="${RANGE_START}..HEAD"
|
|
83
|
+
if [[ "$RANGE_START" == "$EMPTY_TREE" ]]; then
|
|
84
|
+
# Empty-tree baseline: everything in HEAD is "new".
|
|
85
|
+
RANGE="${EMPTY_TREE} HEAD"
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Parse NUL-terminated name-status output.
|
|
89
|
+
# Format: <STATUS>\0<path>[\0<new_path>] where STATUS can be A/M/D/Rnn/Cnn.
|
|
90
|
+
# We collapse to the post-rename path so downstream classification matches
|
|
91
|
+
# the current file layout.
|
|
92
|
+
FILES="$(
|
|
93
|
+
git diff --name-status -M90% -z \
|
|
94
|
+
-- . ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml' ':!yarn.lock' \
|
|
95
|
+
':!.github/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/*.stories.*' \
|
|
96
|
+
$RANGE 2>/dev/null |
|
|
97
|
+
awk -v RS='\0' '
|
|
98
|
+
BEGIN { status = "" }
|
|
99
|
+
{
|
|
100
|
+
if (status == "") { status = $0; next }
|
|
101
|
+
# Rename/copy has two path fields; we want the second (post-rename).
|
|
102
|
+
if (status ~ /^R/ || status ~ /^C/) {
|
|
103
|
+
if (wanted == "") { wanted = 1; next }
|
|
104
|
+
print; status = ""; wanted = ""
|
|
105
|
+
} else {
|
|
106
|
+
print; status = ""
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
' | sort -u
|
|
110
|
+
)"
|
|
111
|
+
|
|
112
|
+
# --first-parent keeps one entry per merged PR (trunk-based). Combine
|
|
113
|
+
# with --cherry-pick --right-only to dedupe back-ports / rebased copies.
|
|
114
|
+
if [[ "$RANGE_START" == "$EMPTY_TREE" ]]; then
|
|
115
|
+
COMMITS="$(git log --first-parent --pretty=format:'%H|%s|%an|%aI' HEAD 2>/dev/null || true)"
|
|
116
|
+
else
|
|
117
|
+
COMMITS="$(git log --first-parent --cherry-pick --right-only --no-merges \
|
|
118
|
+
--pretty=format:'%H|%s|%an|%aI' "${RANGE_START}...HEAD" 2>/dev/null || true)"
|
|
119
|
+
fi
|
|
28
120
|
MODE="sha-range"
|
|
29
121
|
fi
|
|
30
122
|
|
|
@@ -32,7 +124,7 @@ declare -A CLASSIFIED
|
|
|
32
124
|
while IFS= read -r p; do
|
|
33
125
|
[[ -z "$p" ]] && continue
|
|
34
126
|
case "$p" in
|
|
35
|
-
tailwind.config.*|*.tokens.json|styles/tokens.css|styles/theme.css) CLASSIFIED[tokens]+="$p,";;
|
|
127
|
+
tailwind.config.*|*.tokens.json|*.tokens|styles/tokens.css|styles/theme.css) CLASSIFIED[tokens]+="$p,";;
|
|
36
128
|
components/*|src/components/*|app/_components/*) CLASSIFIED[components]+="$p,";;
|
|
37
129
|
app/*/page.*|app/page.*|app/*/route.*|app/route.*|pages/*|src/pages/*|app/routes/*|src/routes/*) CLASSIFIED[routes]+="$p,";;
|
|
38
130
|
public/*|src/assets/*|assets/*) CLASSIFIED[imagery]+="$p,";;
|
|
@@ -43,7 +135,7 @@ while IFS= read -r p; do
|
|
|
43
135
|
esac
|
|
44
136
|
done <<< "$FILES"
|
|
45
137
|
|
|
46
|
-
jq -Rn --arg mode "$MODE" --arg range_start "$RANGE_START" --arg last_iso "$LAST_ISO" \
|
|
138
|
+
jq -Rn --arg mode "$MODE" --arg range_start "${RANGE_START:-}" --arg last_iso "$LAST_ISO" \
|
|
47
139
|
--arg tokens "${CLASSIFIED[tokens]:-}" --arg components "${CLASSIFIED[components]:-}" \
|
|
48
140
|
--arg routes "${CLASSIFIED[routes]:-}" --arg imagery "${CLASSIFIED[imagery]:-}" \
|
|
49
141
|
--arg deps "${CLASSIFIED[deps]:-}" --arg theory "${CLASSIFIED[theory]:-}" \
|
|
@@ -59,3 +151,6 @@ jq -Rn --arg mode "$MODE" --arg range_start "$RANGE_START" --arg last_iso "$LAST
|
|
|
59
151
|
deps: tolist($deps), theory: tolist($theory), content: tolist($content)
|
|
60
152
|
}
|
|
61
153
|
}'
|
|
154
|
+
|
|
155
|
+
# TODO(sd-audit-state §11/artifact line 902): monorepo per-app state
|
|
156
|
+
# (apps/*/docs/super-design/.audit-state.json) not yet supported.
|
|
@@ -1,4 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# TODO(sd-audit-state artifact §14): dynamic routes like `/posts/[slug]`
|
|
3
|
+
# should be expanded to `/posts/@fixture-<id>` using a fixtures manifest
|
|
4
|
+
# (discovered from tests or a user-configured JSON) before being passed
|
|
5
|
+
# to hash-pages/sd-audit. Current impl emits the raw pattern.
|
|
6
|
+
# TODO(sd-audit-state artifact §8): madge-based import-graph builder
|
|
7
|
+
# (`madge --json --ts-config tsconfig.json src`) to compute N-hop
|
|
8
|
+
# component → page blast radius. Expected alongside this script.
|
|
2
9
|
set -euo pipefail
|
|
3
10
|
|
|
4
11
|
detect_framework() {
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
// Extract + canonicalize + hash design tokens from Tailwind configs and
|
|
3
|
+
// CSS custom properties. Output is deterministic regardless of insertion
|
|
4
|
+
// order (artifact §9.2 line 722: "canonical = JSON.stringify(theme,
|
|
5
|
+
// Object.keys(theme).sort())").
|
|
2
6
|
import fs from "node:fs";
|
|
3
7
|
import path from "node:path";
|
|
8
|
+
import { createHash } from "node:crypto";
|
|
4
9
|
import { pathToFileURL } from "node:url";
|
|
5
10
|
|
|
6
11
|
const out = {};
|
|
@@ -28,12 +33,24 @@ try {
|
|
|
28
33
|
}
|
|
29
34
|
} catch (e) { out._postcss_error = String(e.message || e); }
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
// TODO(sd-audit-state §9.1 artifact line 707-709): DTCG *.tokens.json and
|
|
37
|
+
// Tokens Studio support — parse JSON, resolve `{alias}` refs per §9.2 line
|
|
38
|
+
// 747 before hashing, then merge into `out` with prefix `dtcg:`.
|
|
39
|
+
|
|
40
|
+
// Deterministic canonical form: keys sorted top-to-bottom.
|
|
41
|
+
const sorted = Object.keys(out).sort().reduce((acc, k) => { acc[k] = out[k]; return acc; }, {});
|
|
42
|
+
const canonical = JSON.stringify(sorted);
|
|
43
|
+
const tokens_hash = "sha256:" + createHash("sha256").update(canonical).digest("hex");
|
|
44
|
+
|
|
45
|
+
console.log(JSON.stringify({ tokens: sorted, tokens_hash }, null, 2));
|
|
32
46
|
|
|
33
47
|
function flatten(obj, prefix, acc) {
|
|
34
48
|
if (obj == null) return;
|
|
35
49
|
if (typeof obj !== "object") { acc[prefix] = String(obj); return; }
|
|
36
|
-
|
|
50
|
+
// Sort keys so hash is stable across JS engines / config formatters.
|
|
51
|
+
// Artifact §9.2 line 722 calls this out explicitly.
|
|
52
|
+
for (const k of Object.keys(obj).sort()) {
|
|
53
|
+
const v = obj[k];
|
|
37
54
|
const key = `${prefix}.${k}`;
|
|
38
55
|
if (v && typeof v === "object" && !Array.isArray(v)) flatten(v, key, acc);
|
|
39
56
|
else acc[key] = Array.isArray(v) ? v.join(",") : String(v);
|
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# Usage: hash-pages.sh <urls_file>
|
|
3
|
+
#
|
|
4
|
+
# TODO(sd-audit-state artifact §10 line 492, §16 line 1367-1384):
|
|
5
|
+
# Per-viewport hashes (mobile_375 / tablet_768 / desktop_1280) + pHash
|
|
6
|
+
# for perceptual similarity, plus mask_selectors passed to
|
|
7
|
+
# page.screenshot({ mask: [...] }) for deterministic diffs. Current impl
|
|
8
|
+
# hashes a single desktop viewport only.
|
|
3
9
|
set -euo pipefail
|
|
4
10
|
URLS="${1:?usage: hash-pages.sh <urls_file>}"
|
|
5
11
|
OUT_DIR="${OUT_DIR:-docs/super-design/.cache/hashes}"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: setup-git-notes.sh
|
|
3
|
+
#
|
|
4
|
+
# One-shot setup so `git notes --ref=super-design` round-trips across
|
|
5
|
+
# clones. Without the remote refspec, git fetch ignores notes by default
|
|
6
|
+
# (artifact §7 line 570-573).
|
|
7
|
+
set -euo pipefail
|
|
8
|
+
|
|
9
|
+
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
|
10
|
+
echo '{"error":"not-a-git-repo"}' >&2; exit 3
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
# Idempotent: only add if absent.
|
|
14
|
+
if git config --get-all remote.origin.fetch 2>/dev/null |
|
|
15
|
+
grep -q 'refs/notes/super-design'; then
|
|
16
|
+
echo '{"status":"already-configured"}'
|
|
17
|
+
else
|
|
18
|
+
git config --add remote.origin.fetch \
|
|
19
|
+
'+refs/notes/super-design:refs/notes/super-design'
|
|
20
|
+
echo '{"status":"added","ref":"refs/notes/super-design"}'
|
|
21
|
+
fi
|
|
@@ -1,14 +1,46 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# Usage: validate-state.sh [<state_path>]
|
|
3
|
+
#
|
|
4
|
+
# Validates the super-design audit state file. On schema/parse errors,
|
|
5
|
+
# moves the broken file aside (artifact §3 "Graceful corruption handling"
|
|
6
|
+
# line 74) and emits a JSON verdict. Also enforces schema_version major
|
|
7
|
+
# compatibility (artifact §12 line 934).
|
|
2
8
|
set -euo pipefail
|
|
3
9
|
STATE="${1:-docs/super-design/.audit-state.json}"
|
|
10
|
+
|
|
11
|
+
# Current schema major is either read from a sibling .schema-version file
|
|
12
|
+
# (so the number can be bumped without editing shell) or falls back to 1.
|
|
13
|
+
SCHEMA_VERSION_FILE="$(dirname "$0")/../.schema-version"
|
|
14
|
+
if [[ -f "$SCHEMA_VERSION_FILE" ]]; then
|
|
15
|
+
CURRENT_SCHEMA_MAJOR="$(cut -d. -f1 <"$SCHEMA_VERSION_FILE" | tr -d '[:space:]')"
|
|
16
|
+
else
|
|
17
|
+
CURRENT_SCHEMA_MAJOR=1
|
|
18
|
+
fi
|
|
19
|
+
|
|
4
20
|
if [[ ! -f "$STATE" ]]; then echo '{"status":"missing"}'; exit 2; fi
|
|
5
|
-
|
|
21
|
+
|
|
22
|
+
# Parse + shape check. On failure, rename so the user can inspect and we
|
|
23
|
+
# fall through to first-audit (SKILL.md Step 1 treats "corrupt" that way).
|
|
24
|
+
if ! jq -e '
|
|
6
25
|
(.schema_version | type == "string") and
|
|
7
26
|
(.last_audit_at | fromdateiso8601 | . > 0) and
|
|
8
27
|
(.git_sha_at_audit | test("^[0-9a-f]{7,64}$")) and
|
|
9
28
|
(.skill_version | type == "string") and
|
|
10
29
|
(.tools | type == "object")
|
|
11
|
-
' "$STATE" >/dev/null 2>&1
|
|
30
|
+
' "$STATE" >/dev/null 2>&1; then
|
|
31
|
+
mv "$STATE" "$STATE.corrupt-$(date +%s)" 2>/dev/null || true
|
|
32
|
+
echo '{"status":"corrupt"}'; exit 2
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# schema_version major-bump check — if state was written by a newer OR
|
|
36
|
+
# incompatible-older skill, force a full re-audit rather than silently
|
|
37
|
+
# trusting the shape.
|
|
38
|
+
STATE_MAJOR="$(jq -r '.schema_version' "$STATE" | cut -d. -f1)"
|
|
39
|
+
if [[ -z "$STATE_MAJOR" || "$STATE_MAJOR" != "$CURRENT_SCHEMA_MAJOR" ]]; then
|
|
40
|
+
echo "{\"status\":\"schema-incompatible\",\"action\":\"force-full\",\"state_major\":\"${STATE_MAJOR:-unknown}\",\"current_major\":\"${CURRENT_SCHEMA_MAJOR}\"}"
|
|
41
|
+
exit 1
|
|
42
|
+
fi
|
|
43
|
+
|
|
12
44
|
AGE_DAYS=$(( ( $(date -u +%s) - $(jq -r '.last_audit_at | fromdateiso8601' "$STATE") ) / 86400 ))
|
|
13
45
|
if (( AGE_DAYS > 180 )); then echo "{\"status\":\"stale-force-full\",\"age_days\":$AGE_DAYS}"; exit 1
|
|
14
46
|
elif (( AGE_DAYS > 90 )); then echo "{\"status\":\"stale-refresh-research\",\"age_days\":$AGE_DAYS}"; exit 1
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: write-state.sh [<target>] (reads JSON body from stdin)
|
|
3
|
+
#
|
|
4
|
+
# Atomic state writer: accepts JSON on stdin, writes to <target>.tmp,
|
|
5
|
+
# validates with jq, then renames in place. Referenced from
|
|
6
|
+
# docs/compass_artifact §11 ("Write-then-rename (atomic)") and SKILL.md
|
|
7
|
+
# Step 4 ("Atomic write .audit-state.json").
|
|
8
|
+
set -euo pipefail
|
|
9
|
+
|
|
10
|
+
TARGET="${1:-docs/super-design/.audit-state.json}"
|
|
11
|
+
TMP="${TARGET}.tmp"
|
|
12
|
+
|
|
13
|
+
mkdir -p "$(dirname "$TARGET")"
|
|
14
|
+
|
|
15
|
+
# Drain stdin into the tmp file.
|
|
16
|
+
cat >"$TMP"
|
|
17
|
+
|
|
18
|
+
# Validate it is parseable JSON before swapping.
|
|
19
|
+
if ! jq -e 'type == "object"' "$TMP" >/dev/null 2>&1; then
|
|
20
|
+
rm -f "$TMP"
|
|
21
|
+
echo '{"error":"invalid-json-on-stdin"}' >&2
|
|
22
|
+
exit 2
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
mv -f "$TMP" "$TARGET"
|
|
26
|
+
echo "{\"status\":\"written\",\"path\":\"$TARGET\"}"
|