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.
- package/package.json +2 -2
- package/template/.claude/agents/sd-audit.md +121 -2
- package/template/.claude/agents/sd-fix.md +11 -0
- package/template/.claude/agents/sd-research.md +49 -2
- package/template/.claude/skills/super-design/.schema-version +1 -0
- package/template/.claude/skills/super-design/SKILL.md +94 -2
- package/template/.claude/skills/super-design/audit-state.schema.json +226 -0
- package/template/.claude/skills/super-design/references/audit-methodology.md +118 -0
- package/template/.claude/skills/super-design/references/design-intelligence-rubric.md +92 -11
- package/template/.claude/skills/super-design/references/design-skills-catalog.md +31 -0
- package/template/.claude/skills/super-design/scripts/build-import-graph.sh +208 -0
- package/template/.claude/skills/super-design/scripts/detect-apps.sh +180 -0
- package/template/.claude/skills/super-design/scripts/detect-changes.sh +177 -21
- package/template/.claude/skills/super-design/scripts/discover-routes.sh +120 -6
- package/template/.claude/skills/super-design/scripts/extract-tokens.mjs +165 -4
- package/template/.claude/skills/super-design/scripts/hash-pages.sh +209 -23
- package/template/.claude/skills/super-design/scripts/setup-git-notes.sh +21 -0
- package/template/.claude/skills/super-design/scripts/validate-state.sh +74 -11
- package/template/.claude/skills/super-design/scripts/verify-audit.sh +62 -9
- package/template/.claude/skills/super-design/scripts/visual-regression.sh +275 -0
- package/template/.claude/skills/super-design/scripts/write-state.sh +53 -0
- package/template/.claude/skills/super-design/templates/audit-state.schema.json +0 -57
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Usage: detect-apps.sh [<repo_root>]
|
|
3
|
+
#
|
|
4
|
+
# Detects monorepo layout for super-design per-app audit state (artifact §11
|
|
5
|
+
# line 902). Reads workspace declarations from the known manifests:
|
|
6
|
+
# - pnpm-workspace.yaml (pnpm)
|
|
7
|
+
# - package.json#workspaces (npm / yarn / bun)
|
|
8
|
+
# - turbo.json (Turborepo — infers apps from pipeline scope)
|
|
9
|
+
# - nx.json / workspace.json (Nx)
|
|
10
|
+
# - bunfig.toml (Bun workspaces)
|
|
11
|
+
# For each matched glob we enumerate directories that also contain a
|
|
12
|
+
# package.json; those are the apps. When no workspace config is found we
|
|
13
|
+
# emit a single "single"-layout entry with path ".".
|
|
14
|
+
#
|
|
15
|
+
# Output JSON:
|
|
16
|
+
# {
|
|
17
|
+
# "layout": "monorepo" | "single",
|
|
18
|
+
# "apps": [
|
|
19
|
+
# { "name": "web",
|
|
20
|
+
# "path": "apps/web",
|
|
21
|
+
# "state_path": "apps/web/docs/super-design/.audit-state.json" }
|
|
22
|
+
# ]
|
|
23
|
+
# }
|
|
24
|
+
#
|
|
25
|
+
# POSIX-ish bash. Requires: jq. Works under git-bash on Windows (no GNU
|
|
26
|
+
# find -printf, no readarray).
|
|
27
|
+
set -euo pipefail
|
|
28
|
+
|
|
29
|
+
ROOT="${1:-.}"
|
|
30
|
+
cd "$ROOT"
|
|
31
|
+
|
|
32
|
+
log() { printf '[detect-apps] %s\n' "$*" >&2; }
|
|
33
|
+
|
|
34
|
+
# --- Collect workspace globs from whichever manifests exist ------------------
|
|
35
|
+
GLOBS=""
|
|
36
|
+
|
|
37
|
+
append_glob() {
|
|
38
|
+
# $1 = glob string; skip empty, comments, and negative (!) patterns for now.
|
|
39
|
+
local g
|
|
40
|
+
g="$(printf '%s' "$1" | tr -d '\r' | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
41
|
+
[ -z "$g" ] && return 0
|
|
42
|
+
case "$g" in
|
|
43
|
+
'#'*) return 0 ;;
|
|
44
|
+
'!'*) return 0 ;;
|
|
45
|
+
esac
|
|
46
|
+
GLOBS="${GLOBS}
|
|
47
|
+
${g}"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
# pnpm-workspace.yaml — very small YAML subset parser: lines under
|
|
51
|
+
# "packages:" that start with "- ".
|
|
52
|
+
if [ -f "pnpm-workspace.yaml" ]; then
|
|
53
|
+
in_pkgs=0
|
|
54
|
+
while IFS= read -r line; do
|
|
55
|
+
case "$line" in
|
|
56
|
+
packages:*) in_pkgs=1; continue ;;
|
|
57
|
+
esac
|
|
58
|
+
if [ "$in_pkgs" = "1" ]; then
|
|
59
|
+
case "$line" in
|
|
60
|
+
-\ *|\ *-\ *)
|
|
61
|
+
entry="$(printf '%s' "$line" | sed -e 's/^[[:space:]]*-[[:space:]]*//' -e 's/^["'\'']//' -e 's/["'\'']$//')"
|
|
62
|
+
append_glob "$entry"
|
|
63
|
+
;;
|
|
64
|
+
[A-Za-z]*:) in_pkgs=0 ;;
|
|
65
|
+
esac
|
|
66
|
+
fi
|
|
67
|
+
done < "pnpm-workspace.yaml"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# package.json#workspaces (array or { packages: [...] })
|
|
71
|
+
if [ -f "package.json" ] && command -v jq >/dev/null 2>&1; then
|
|
72
|
+
WS_JSON="$(jq -r '
|
|
73
|
+
(.workspaces // empty)
|
|
74
|
+
| if type == "array" then .[]
|
|
75
|
+
elif type == "object" then (.packages // [])[]
|
|
76
|
+
else empty end
|
|
77
|
+
' package.json 2>/dev/null || true)"
|
|
78
|
+
if [ -n "$WS_JSON" ]; then
|
|
79
|
+
while IFS= read -r g; do
|
|
80
|
+
[ -n "$g" ] && append_glob "$g"
|
|
81
|
+
done <<EOF
|
|
82
|
+
$WS_JSON
|
|
83
|
+
EOF
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# turbo.json — Turborepo does not declare apps itself but relies on the
|
|
88
|
+
# package manager's workspaces. Treat its presence as confirmation that
|
|
89
|
+
# we are in a monorepo; globs come from package.json/pnpm-workspace.yaml
|
|
90
|
+
# which we've already read. If only turbo.json exists (rare), fall back
|
|
91
|
+
# to the conventional apps/* + packages/* layout.
|
|
92
|
+
if [ -f "turbo.json" ] && [ -z "$GLOBS" ]; then
|
|
93
|
+
append_glob "apps/*"
|
|
94
|
+
append_glob "packages/*"
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# nx.json / workspace.json — Nx. Apps live under apps/, libs/ (or the
|
|
98
|
+
# workspaceLayout override).
|
|
99
|
+
if [ -f "nx.json" ]; then
|
|
100
|
+
apps_dir="apps"
|
|
101
|
+
libs_dir="libs"
|
|
102
|
+
if command -v jq >/dev/null 2>&1; then
|
|
103
|
+
override_apps="$(jq -r '.workspaceLayout.appsDir // empty' nx.json 2>/dev/null || true)"
|
|
104
|
+
override_libs="$(jq -r '.workspaceLayout.libsDir // empty' nx.json 2>/dev/null || true)"
|
|
105
|
+
[ -n "$override_apps" ] && apps_dir="$override_apps"
|
|
106
|
+
[ -n "$override_libs" ] && libs_dir="$override_libs"
|
|
107
|
+
fi
|
|
108
|
+
append_glob "${apps_dir}/*"
|
|
109
|
+
append_glob "${libs_dir}/*"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
# bunfig.toml — Bun workspaces. Bun primarily reads workspaces from
|
|
113
|
+
# package.json#workspaces (already handled); bunfig.toml presence alone
|
|
114
|
+
# is a monorepo hint only if nothing else matched.
|
|
115
|
+
if [ -f "bunfig.toml" ] && [ -z "$GLOBS" ]; then
|
|
116
|
+
append_glob "apps/*"
|
|
117
|
+
append_glob "packages/*"
|
|
118
|
+
fi
|
|
119
|
+
|
|
120
|
+
# --- Expand globs to real directories that contain a package.json ------------
|
|
121
|
+
APPS_JSON='[]'
|
|
122
|
+
seen_paths=""
|
|
123
|
+
|
|
124
|
+
expand_glob() {
|
|
125
|
+
# $1 = glob like "apps/*" or "packages/marketing-site"
|
|
126
|
+
local g="$1"
|
|
127
|
+
# shellcheck disable=SC2086
|
|
128
|
+
# We deliberately let the shell expand the glob. POSIX shells don't set
|
|
129
|
+
# nullglob, so an unmatched pattern comes back as the literal string —
|
|
130
|
+
# we filter that out below by checking directory existence.
|
|
131
|
+
for d in $g; do
|
|
132
|
+
[ -d "$d" ] || continue
|
|
133
|
+
[ -f "$d/package.json" ] || continue
|
|
134
|
+
# Normalize: strip trailing slash, leading ./
|
|
135
|
+
p="$(printf '%s' "$d" | sed -e 's#^\./##' -e 's#/$##')"
|
|
136
|
+
case "$seen_paths" in
|
|
137
|
+
*"|${p}|"*) continue ;;
|
|
138
|
+
esac
|
|
139
|
+
seen_paths="${seen_paths}|${p}|"
|
|
140
|
+
# Name = package.json "name" field (last path segment for scoped names),
|
|
141
|
+
# fallback to basename.
|
|
142
|
+
name="$(basename "$p")"
|
|
143
|
+
if command -v jq >/dev/null 2>&1; then
|
|
144
|
+
pkg_name="$(jq -r '.name // empty' "$p/package.json" 2>/dev/null || true)"
|
|
145
|
+
if [ -n "$pkg_name" ]; then
|
|
146
|
+
# Strip @scope/ prefix so "web" beats "@acme/web".
|
|
147
|
+
name="$(printf '%s' "$pkg_name" | sed -e 's#^@[^/]*/##')"
|
|
148
|
+
fi
|
|
149
|
+
fi
|
|
150
|
+
APPS_JSON="$(printf '%s' "$APPS_JSON" | jq --arg name "$name" --arg path "$p" \
|
|
151
|
+
--arg state "$p/docs/super-design/.audit-state.json" \
|
|
152
|
+
'. + [{name:$name, path:$path, state_path:$state}]')"
|
|
153
|
+
done
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if [ -n "$GLOBS" ]; then
|
|
157
|
+
while IFS= read -r g; do
|
|
158
|
+
[ -z "$g" ] && continue
|
|
159
|
+
expand_glob "$g"
|
|
160
|
+
done <<EOF
|
|
161
|
+
$GLOBS
|
|
162
|
+
EOF
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
APP_COUNT="$(printf '%s' "$APPS_JSON" | jq 'length')"
|
|
166
|
+
|
|
167
|
+
if [ "$APP_COUNT" -eq 0 ]; then
|
|
168
|
+
# No monorepo config matched OR matched but produced no apps with
|
|
169
|
+
# package.json. Emit single-app layout.
|
|
170
|
+
jq -n '{
|
|
171
|
+
layout: "single",
|
|
172
|
+
apps: [ {name: ".", path: ".", state_path: "docs/super-design/.audit-state.json"} ]
|
|
173
|
+
}'
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
jq -n --argjson apps "$APPS_JSON" '{
|
|
178
|
+
layout: "monorepo",
|
|
179
|
+
apps: $apps
|
|
180
|
+
}'
|
|
@@ -1,30 +1,184 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# Usage:
|
|
3
|
-
#
|
|
2
|
+
# Usage:
|
|
3
|
+
# detect-changes.sh <last_sha> [<last_iso>] (single app)
|
|
4
|
+
# detect-changes.sh --app <path> <last_sha> [<last_iso>] (one app, path-scoped)
|
|
5
|
+
# detect-changes.sh --all-apps (monorepo loop)
|
|
6
|
+
#
|
|
7
|
+
# Emits JSON. Single-app form:
|
|
8
|
+
# { mode, range_start, commits, files, classified }
|
|
9
|
+
#
|
|
10
|
+
# Monorepo form (--all-apps) — one wrapper with per-app results; each
|
|
11
|
+
# app reads its own `.audit-state.json` via detect-apps.sh:
|
|
12
|
+
# { "layout": "monorepo"|"single",
|
|
13
|
+
# "apps": [ { "name", "path", "state_path",
|
|
14
|
+
# "last_sha", "changes": {<single-app form>} } ] }
|
|
15
|
+
#
|
|
16
|
+
# Implements the fallback ladder from artifact §6 + §14:
|
|
17
|
+
# 1. Anchor exists → diff with rename detection (-M90%).
|
|
18
|
+
# 2. Anchor missing + shallow repo → `git fetch --unshallow --no-tags`.
|
|
19
|
+
# 3. Still missing → fetch super-design git notes, retry.
|
|
20
|
+
# 4. Still missing → `--since=<iso>` time-based fallback.
|
|
21
|
+
# 5. Empty repo / no last_sha at all → diff against empty-tree SHA
|
|
22
|
+
# 4b825dc642cb6eb9a060e54bf8d69288fbee4904 (artifact line 900).
|
|
23
|
+
# Also uses --first-parent + --cherry-pick --right-only when walking
|
|
24
|
+
# history (artifact §2.5 line 167-172).
|
|
25
|
+
#
|
|
26
|
+
# Monorepo per-app state (artifact §11 line 902): when --app is given,
|
|
27
|
+
# `git diff --name-status` is narrowed via `-- <app_path>/` pathspec so
|
|
28
|
+
# only files under that app show up. `--all-apps` discovers apps via
|
|
29
|
+
# detect-apps.sh and runs the per-app path for each.
|
|
4
30
|
set -euo pipefail
|
|
5
31
|
|
|
32
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
33
|
+
EMPTY_TREE="4b825dc642cb6eb9a060e54bf8d69288fbee4904"
|
|
34
|
+
|
|
35
|
+
log() { printf '[detect-changes] %s\n' "$*" >&2; }
|
|
36
|
+
|
|
37
|
+
# --- Arg parsing -------------------------------------------------------------
|
|
38
|
+
MODE_SELECT="single" # single | app | all
|
|
39
|
+
APP_PATH="."
|
|
40
|
+
if [ "${1:-}" = "--all-apps" ]; then
|
|
41
|
+
MODE_SELECT="all"; shift
|
|
42
|
+
elif [ "${1:-}" = "--app" ]; then
|
|
43
|
+
MODE_SELECT="app"; APP_PATH="${2:-.}"; shift 2
|
|
44
|
+
fi
|
|
6
45
|
LAST_SHA="${1:-}"
|
|
7
46
|
LAST_ISO="${2:-}"
|
|
8
47
|
|
|
9
|
-
if [[ -z "$LAST_SHA" ]]; then echo '{"error":"missing last_sha"}'; exit 2; fi
|
|
10
48
|
if ! git rev-parse --git-dir >/dev/null 2>&1; then echo '{"error":"not-a-git-repo"}'; exit 3; fi
|
|
11
49
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
50
|
+
# --- --all-apps: discover via detect-apps.sh, recurse per app ----------------
|
|
51
|
+
if [ "$MODE_SELECT" = "all" ]; then
|
|
52
|
+
APPS_DOC="$(bash "$SCRIPT_DIR/detect-apps.sh" .)"
|
|
53
|
+
LAYOUT="$(printf '%s' "$APPS_DOC" | jq -r '.layout')"
|
|
54
|
+
RESULT='[]'
|
|
55
|
+
APP_COUNT="$(printf '%s' "$APPS_DOC" | jq '.apps | length')"
|
|
56
|
+
i=0
|
|
57
|
+
while [ "$i" -lt "$APP_COUNT" ]; do
|
|
58
|
+
APP_NAME="$(printf '%s' "$APPS_DOC" | jq -r ".apps[$i].name")"
|
|
59
|
+
A_PATH="$(printf '%s' "$APPS_DOC" | jq -r ".apps[$i].path")"
|
|
60
|
+
A_STATE="$(printf '%s' "$APPS_DOC" | jq -r ".apps[$i].state_path")"
|
|
61
|
+
A_LAST_SHA=""; A_LAST_ISO=""
|
|
62
|
+
if [ -f "$A_STATE" ]; then
|
|
63
|
+
A_LAST_SHA="$(jq -r '.git_sha_at_audit // ""' "$A_STATE" 2>/dev/null || true)"
|
|
64
|
+
A_LAST_ISO="$(jq -r '.last_audit_at // ""' "$A_STATE" 2>/dev/null || true)"
|
|
65
|
+
fi
|
|
66
|
+
A_CHANGES="$(bash "$0" --app "$A_PATH" "$A_LAST_SHA" "$A_LAST_ISO")"
|
|
67
|
+
RESULT="$(printf '%s' "$RESULT" | jq \
|
|
68
|
+
--arg name "$APP_NAME" --arg path "$A_PATH" --arg state "$A_STATE" \
|
|
69
|
+
--arg lsha "$A_LAST_SHA" --argjson ch "$A_CHANGES" '
|
|
70
|
+
. + [{name:$name, path:$path, state_path:$state, last_sha:$lsha, changes:$ch}]
|
|
71
|
+
')"
|
|
72
|
+
i=$((i + 1))
|
|
73
|
+
done
|
|
74
|
+
jq -n --arg layout "$LAYOUT" --argjson apps "$RESULT" '{layout:$layout, apps:$apps}'
|
|
75
|
+
exit 0
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
# Determine HEAD availability — empty repos have no commits at all.
|
|
79
|
+
if ! git rev-parse --verify --quiet HEAD >/dev/null; then
|
|
80
|
+
log "no HEAD commit; using empty-tree SHA fallback"
|
|
81
|
+
LAST_SHA="$EMPTY_TREE"
|
|
82
|
+
fi
|
|
83
|
+
|
|
84
|
+
# If caller didn't pass a last_sha, treat it as empty-tree (first audit).
|
|
85
|
+
if [[ -z "$LAST_SHA" ]]; then
|
|
86
|
+
log "missing last_sha; defaulting to empty-tree SHA"
|
|
87
|
+
LAST_SHA="$EMPTY_TREE"
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
resolve_anchor() {
|
|
91
|
+
# Sets RANGE_START (may be empty if anchor unrecoverable).
|
|
92
|
+
if [[ "$LAST_SHA" == "$EMPTY_TREE" ]]; then
|
|
93
|
+
RANGE_START="$EMPTY_TREE"; return 0
|
|
94
|
+
fi
|
|
95
|
+
if git rev-parse --verify --quiet "${LAST_SHA}^{commit}" >/dev/null; then
|
|
96
|
+
if git merge-base --is-ancestor "$LAST_SHA" HEAD 2>/dev/null; then
|
|
97
|
+
RANGE_START="$LAST_SHA"
|
|
98
|
+
else
|
|
99
|
+
RANGE_START="$(git merge-base HEAD "$LAST_SHA" 2>/dev/null || echo "")"
|
|
100
|
+
fi
|
|
101
|
+
return 0
|
|
102
|
+
fi
|
|
103
|
+
RANGE_START=""
|
|
104
|
+
return 1
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if ! resolve_anchor; then
|
|
108
|
+
# Ladder step (a): unshallow if possible.
|
|
109
|
+
if [[ "$(git rev-parse --is-shallow-repository 2>/dev/null || echo false)" == "true" ]]; then
|
|
110
|
+
log "anchor missing and repo is shallow; attempting git fetch --unshallow --no-tags"
|
|
111
|
+
git fetch --unshallow --no-tags 2>/dev/null || log "unshallow fetch failed (continuing)"
|
|
112
|
+
resolve_anchor || true
|
|
113
|
+
fi
|
|
114
|
+
fi
|
|
115
|
+
if [[ -z "${RANGE_START:-}" ]]; then
|
|
116
|
+
# Ladder step (b): try to fetch super-design notes; they may pin a commit
|
|
117
|
+
# we don't have locally.
|
|
118
|
+
log "anchor still missing; fetching refs/notes/super-design"
|
|
119
|
+
git fetch origin '+refs/notes/super-design:refs/notes/super-design' 2>/dev/null \
|
|
120
|
+
|| log "notes fetch failed (continuing)"
|
|
121
|
+
resolve_anchor || true
|
|
122
|
+
fi
|
|
123
|
+
|
|
124
|
+
# Build the pathspec used to narrow diff/log to a single app (monorepo
|
|
125
|
+
# per-app state, artifact §11 line 902). For the default "." the scope
|
|
126
|
+
# is the repo root, preserving pre-existing single-app behavior.
|
|
127
|
+
case "$APP_PATH" in
|
|
128
|
+
.|"") SCOPE_PATH="." ;;
|
|
129
|
+
*) SCOPE_PATH="${APP_PATH%/}" ;;
|
|
130
|
+
esac
|
|
131
|
+
|
|
132
|
+
# Ladder step (c): time-based fallback.
|
|
133
|
+
if [[ -z "${RANGE_START:-}" && -n "$LAST_ISO" ]]; then
|
|
134
|
+
log "using --since=$LAST_ISO time-based fallback (scope=$SCOPE_PATH)"
|
|
135
|
+
FILES="$(git log --since="$LAST_ISO" --name-only --pretty=format: -- "$SCOPE_PATH" 2>/dev/null | sort -u | sed '/^$/d' || true)"
|
|
136
|
+
COMMITS="$(git log --first-parent --since="$LAST_ISO" --pretty=format:'%H|%s|%an|%aI' -- "$SCOPE_PATH" 2>/dev/null || true)"
|
|
137
|
+
MODE="since-time"
|
|
138
|
+
RANGE_START=""
|
|
139
|
+
elif [[ -z "${RANGE_START:-}" ]]; then
|
|
140
|
+
echo '{"error":"lost-anchor-no-fallback-time"}'; exit 4
|
|
23
141
|
else
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
142
|
+
# SHA range available. Use --name-status -M90% -z to catch renames AND
|
|
143
|
+
# filenames with spaces (NUL-terminated output).
|
|
144
|
+
RANGE="${RANGE_START}..HEAD"
|
|
145
|
+
if [[ "$RANGE_START" == "$EMPTY_TREE" ]]; then
|
|
146
|
+
# Empty-tree baseline: everything in HEAD is "new".
|
|
147
|
+
RANGE="${EMPTY_TREE} HEAD"
|
|
148
|
+
fi
|
|
149
|
+
|
|
150
|
+
# Parse NUL-terminated name-status output.
|
|
151
|
+
# Format: <STATUS>\0<path>[\0<new_path>] where STATUS can be A/M/D/Rnn/Cnn.
|
|
152
|
+
# We collapse to the post-rename path so downstream classification matches
|
|
153
|
+
# the current file layout.
|
|
154
|
+
FILES="$(
|
|
155
|
+
git diff --name-status -M90% -z \
|
|
156
|
+
-- "$SCOPE_PATH" ':!*.lock' ':!package-lock.json' ':!pnpm-lock.yaml' ':!yarn.lock' \
|
|
157
|
+
':!.github/**' ':!**/*.test.*' ':!**/*.spec.*' ':!**/*.stories.*' \
|
|
158
|
+
$RANGE 2>/dev/null |
|
|
159
|
+
awk -v RS='\0' '
|
|
160
|
+
BEGIN { status = "" }
|
|
161
|
+
{
|
|
162
|
+
if (status == "") { status = $0; next }
|
|
163
|
+
# Rename/copy has two path fields; we want the second (post-rename).
|
|
164
|
+
if (status ~ /^R/ || status ~ /^C/) {
|
|
165
|
+
if (wanted == "") { wanted = 1; next }
|
|
166
|
+
print; status = ""; wanted = ""
|
|
167
|
+
} else {
|
|
168
|
+
print; status = ""
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
' | sort -u
|
|
172
|
+
)"
|
|
173
|
+
|
|
174
|
+
# --first-parent keeps one entry per merged PR (trunk-based). Combine
|
|
175
|
+
# with --cherry-pick --right-only to dedupe back-ports / rebased copies.
|
|
176
|
+
if [[ "$RANGE_START" == "$EMPTY_TREE" ]]; then
|
|
177
|
+
COMMITS="$(git log --first-parent --pretty=format:'%H|%s|%an|%aI' HEAD 2>/dev/null || true)"
|
|
178
|
+
else
|
|
179
|
+
COMMITS="$(git log --first-parent --cherry-pick --right-only --no-merges \
|
|
180
|
+
--pretty=format:'%H|%s|%an|%aI' "${RANGE_START}...HEAD" 2>/dev/null || true)"
|
|
181
|
+
fi
|
|
28
182
|
MODE="sha-range"
|
|
29
183
|
fi
|
|
30
184
|
|
|
@@ -32,7 +186,7 @@ declare -A CLASSIFIED
|
|
|
32
186
|
while IFS= read -r p; do
|
|
33
187
|
[[ -z "$p" ]] && continue
|
|
34
188
|
case "$p" in
|
|
35
|
-
tailwind.config.*|*.tokens.json|styles/tokens.css|styles/theme.css) CLASSIFIED[tokens]+="$p,";;
|
|
189
|
+
tailwind.config.*|*.tokens.json|*.tokens|styles/tokens.css|styles/theme.css) CLASSIFIED[tokens]+="$p,";;
|
|
36
190
|
components/*|src/components/*|app/_components/*) CLASSIFIED[components]+="$p,";;
|
|
37
191
|
app/*/page.*|app/page.*|app/*/route.*|app/route.*|pages/*|src/pages/*|app/routes/*|src/routes/*) CLASSIFIED[routes]+="$p,";;
|
|
38
192
|
public/*|src/assets/*|assets/*) CLASSIFIED[imagery]+="$p,";;
|
|
@@ -43,7 +197,8 @@ while IFS= read -r p; do
|
|
|
43
197
|
esac
|
|
44
198
|
done <<< "$FILES"
|
|
45
199
|
|
|
46
|
-
jq -Rn --arg mode "$MODE" --arg range_start "$RANGE_START" --arg last_iso "$LAST_ISO" \
|
|
200
|
+
jq -Rn --arg mode "$MODE" --arg range_start "${RANGE_START:-}" --arg last_iso "$LAST_ISO" \
|
|
201
|
+
--arg app_path "$APP_PATH" \
|
|
47
202
|
--arg tokens "${CLASSIFIED[tokens]:-}" --arg components "${CLASSIFIED[components]:-}" \
|
|
48
203
|
--arg routes "${CLASSIFIED[routes]:-}" --arg imagery "${CLASSIFIED[imagery]:-}" \
|
|
49
204
|
--arg deps "${CLASSIFIED[deps]:-}" --arg theory "${CLASSIFIED[theory]:-}" \
|
|
@@ -51,7 +206,8 @@ jq -Rn --arg mode "$MODE" --arg range_start "$RANGE_START" --arg last_iso "$LAST
|
|
|
51
206
|
--argjson files "$(printf '%s\n' "$FILES" | jq -R . | jq -s .)" \
|
|
52
207
|
--argjson commits "$(printf '%s\n' "$COMMITS" | jq -R . | jq -s .)" '
|
|
53
208
|
def tolist(s): s | split(",") | map(select(length>0));
|
|
54
|
-
{
|
|
209
|
+
{app_path:$app_path,
|
|
210
|
+
mode:$mode, range_start:$range_start, since_iso:$last_iso,
|
|
55
211
|
commits:$commits, files:$files,
|
|
56
212
|
classified: {
|
|
57
213
|
tokens: tolist($tokens), components: tolist($components),
|
|
@@ -1,6 +1,26 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
+
# discover-routes.sh — discover route URLs for the detected framework.
|
|
3
|
+
#
|
|
4
|
+
# Dynamic routes (Next `[slug]` / `[[...foo]]` / `[...bar]`, Remix `$slug`,
|
|
5
|
+
# React-Router/Vue `:slug`) are emitted with a `@fixture-<id>` suffix so
|
|
6
|
+
# the route map has a stable identity across audits (artifact §2.7 + §7:
|
|
7
|
+
# "/posts/[id]@fixture-post-123"). Fixtures are resolved via:
|
|
8
|
+
#
|
|
9
|
+
# 1. Sibling file: <route-dir>/<name>.fixture.json|ts (array of IDs or
|
|
10
|
+
# objects with {id|slug|key}).
|
|
11
|
+
# 2. Nearby directory: <route-dir>/fixtures/*.json.
|
|
12
|
+
# 3. Env override: $SUPER_DESIGN_FIXTURES — a JSON object mapping the
|
|
13
|
+
# raw pattern (e.g. "/posts/[slug]") to an array of fixture IDs.
|
|
14
|
+
# 4. Placeholder fallback: `@fixture-default` (with stderr warning).
|
|
15
|
+
#
|
|
16
|
+
# Consumers (hash-pages.sh, sd-audit) MUST strip the `@fixture-<id>`
|
|
17
|
+
# suffix before navigating; the suffix is identity-only.
|
|
18
|
+
#
|
|
19
|
+
# Output: JSON array of URL strings.
|
|
2
20
|
set -euo pipefail
|
|
3
21
|
|
|
22
|
+
log() { printf '[discover-routes] %s\n' "$*" >&2; }
|
|
23
|
+
|
|
4
24
|
detect_framework() {
|
|
5
25
|
if [[ -f next.config.js || -f next.config.ts || -f next.config.mjs ]]; then echo "next"
|
|
6
26
|
elif [[ -f remix.config.js || -d app/routes ]]; then echo "remix"
|
|
@@ -13,6 +33,96 @@ detect_framework() {
|
|
|
13
33
|
else echo "unknown"; fi
|
|
14
34
|
}
|
|
15
35
|
|
|
36
|
+
# is_dynamic <url> → 0 if URL contains a dynamic segment, else 1.
|
|
37
|
+
is_dynamic() {
|
|
38
|
+
local u="$1"
|
|
39
|
+
# Next-style: [foo], [[foo]], [...foo], [[...foo]]
|
|
40
|
+
[[ "$u" == *"["*"]"* ]] && return 0
|
|
41
|
+
# Remix-style: $foo
|
|
42
|
+
[[ "$u" == *"$"* ]] && return 0
|
|
43
|
+
# RR/Vue-style: :foo
|
|
44
|
+
[[ "$u" == *":"* ]] && return 0
|
|
45
|
+
return 1
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# Look for a fixtures source for a given route URL and emit IDs one per
|
|
49
|
+
# line. Echo `__DEFAULT__` if nothing found.
|
|
50
|
+
resolve_fixture_ids() {
|
|
51
|
+
local url="$1"
|
|
52
|
+
local segment
|
|
53
|
+
# Strip leading / and use the last dynamic-bearing segment to find a file.
|
|
54
|
+
# Rough heuristic: pick the last path component that is a bare identifier
|
|
55
|
+
# (strip brackets/dollar/colon) to look up fixtures/<name>.*.
|
|
56
|
+
segment="$(printf '%s' "$url" \
|
|
57
|
+
| sed -E 's|^/||' \
|
|
58
|
+
| tr '/' '\n' \
|
|
59
|
+
| grep -E '^(\[\[?\.\.\.?.+\]\]?|\[.+\]|\$.+|:.+)$' \
|
|
60
|
+
| tail -1 \
|
|
61
|
+
| sed -E 's|^\[\[?\.?\.?\.?||; s|\]\]?$||; s|^\$||; s|^:||')"
|
|
62
|
+
[[ -z "$segment" ]] && { echo "__DEFAULT__"; return; }
|
|
63
|
+
|
|
64
|
+
# 1. Env override.
|
|
65
|
+
if [[ -n "${SUPER_DESIGN_FIXTURES:-}" ]]; then
|
|
66
|
+
local ids
|
|
67
|
+
ids="$(printf '%s' "$SUPER_DESIGN_FIXTURES" \
|
|
68
|
+
| jq -r --arg k "$url" '.[$k][]? // empty' 2>/dev/null || true)"
|
|
69
|
+
if [[ -n "$ids" ]]; then
|
|
70
|
+
printf '%s\n' "$ids"; return
|
|
71
|
+
fi
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# 2. Sibling / nearby fixture files. Search in common locations.
|
|
75
|
+
local candidate ids=""
|
|
76
|
+
for candidate in \
|
|
77
|
+
"${segment}.fixture.json" \
|
|
78
|
+
"fixtures/${segment}.json" \
|
|
79
|
+
"tests/fixtures/${segment}.json" \
|
|
80
|
+
"__fixtures__/${segment}.json" \
|
|
81
|
+
".super-design/fixtures/${segment}.json"; do
|
|
82
|
+
local found
|
|
83
|
+
found="$(find . -type f -name "$(basename "$candidate")" 2>/dev/null \
|
|
84
|
+
| grep -F "/$candidate" | head -1 || true)"
|
|
85
|
+
if [[ -n "$found" && -f "$found" ]]; then
|
|
86
|
+
ids="$(jq -r '
|
|
87
|
+
if type == "array" then
|
|
88
|
+
.[] | if type == "object" then (.id // .slug // .key // empty) else . end
|
|
89
|
+
elif type == "object" and (.fixtures|type=="array") then
|
|
90
|
+
.fixtures[] | if type=="object" then (.id // .slug // .key // empty) else . end
|
|
91
|
+
else empty end
|
|
92
|
+
' "$found" 2>/dev/null | head -5 || true)"
|
|
93
|
+
if [[ -n "$ids" ]]; then
|
|
94
|
+
printf '%s\n' "$ids"; return
|
|
95
|
+
fi
|
|
96
|
+
fi
|
|
97
|
+
done
|
|
98
|
+
|
|
99
|
+
# 3. Give up → default placeholder with warning.
|
|
100
|
+
log "no fixtures for $url; using @fixture-default"
|
|
101
|
+
echo "__DEFAULT__"
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# suffix_dynamic_routes: read raw URLs on stdin, write suffixed URLs on
|
|
105
|
+
# stdout. Static routes pass through untouched.
|
|
106
|
+
suffix_dynamic_routes() {
|
|
107
|
+
local url ids id
|
|
108
|
+
while IFS= read -r url; do
|
|
109
|
+
[[ -z "$url" ]] && continue
|
|
110
|
+
if is_dynamic "$url"; then
|
|
111
|
+
ids="$(resolve_fixture_ids "$url")"
|
|
112
|
+
if [[ "$ids" == "__DEFAULT__" ]]; then
|
|
113
|
+
printf '%s@fixture-default\n' "$url"
|
|
114
|
+
else
|
|
115
|
+
while IFS= read -r id; do
|
|
116
|
+
[[ -z "$id" ]] && continue
|
|
117
|
+
printf '%s@fixture-%s\n' "$url" "$id"
|
|
118
|
+
done <<<"$ids"
|
|
119
|
+
fi
|
|
120
|
+
else
|
|
121
|
+
printf '%s\n' "$url"
|
|
122
|
+
fi
|
|
123
|
+
done
|
|
124
|
+
}
|
|
125
|
+
|
|
16
126
|
FW="$(detect_framework)"
|
|
17
127
|
case "$FW" in
|
|
18
128
|
next)
|
|
@@ -22,24 +132,28 @@ case "$FW" in
|
|
|
22
132
|
PG="$(find pages src/pages -type f \( -name '*.tsx' -o -name '*.ts' -o -name '*.jsx' -o -name '*.js' -o -name '*.md' -o -name '*.mdx' \) 2>/dev/null \
|
|
23
133
|
| grep -vE '(^|/)(pages|src/pages)/(_app|_document|_error|404|500|api/)' \
|
|
24
134
|
| sed -E 's|(^|/)(pages|src/pages)/||; s|\.(tsx|ts|jsx|js|md|mdx)$||; s|index$||' | sort -u || true)"
|
|
25
|
-
printf '%s\n%s\n' "$APP" "$PG" | awk 'NF' | sed -E 's|^|/|; s|//|/|g' | sort -u
|
|
135
|
+
printf '%s\n%s\n' "$APP" "$PG" | awk 'NF' | sed -E 's|^|/|; s|//|/|g' | sort -u \
|
|
136
|
+
| suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
26
137
|
sveltekit)
|
|
27
138
|
find src/routes -type f -name '+page.svelte' 2>/dev/null \
|
|
28
139
|
| sed -E 's|^src/routes||; s|/\+page\.svelte$||; s|\([^)]+\)/||g' \
|
|
29
|
-
| awk 'NF==0 {print "/"; next} {print}' | sort -u
|
|
140
|
+
| awk 'NF==0 {print "/"; next} {print}' | sort -u \
|
|
141
|
+
| suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
30
142
|
astro)
|
|
31
143
|
find src/pages -type f \( -name '*.astro' -o -name '*.md' -o -name '*.mdx' \) 2>/dev/null \
|
|
32
|
-
| sed -E 's|^src/pages||; s|\.(astro|md|mdx)$||; s|/index$||' | sort -u
|
|
144
|
+
| sed -E 's|^src/pages||; s|\.(astro|md|mdx)$||; s|/index$||' | sort -u \
|
|
145
|
+
| suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
33
146
|
nuxt)
|
|
34
147
|
find pages app/pages -type f -name '*.vue' 2>/dev/null \
|
|
35
|
-
| sed -E 's|^(app/)?pages||; s|\.vue$||; s|/index$||' | sort -u
|
|
148
|
+
| sed -E 's|^(app/)?pages||; s|\.vue$||; s|/index$||' | sort -u \
|
|
149
|
+
| suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
36
150
|
remix)
|
|
37
151
|
find app/routes -type f \( -name '*.tsx' -o -name '*.ts' -o -name '*.jsx' -o -name '*.js' -o -name '*.md' -o -name '*.mdx' \) 2>/dev/null \
|
|
38
152
|
| sed -E 's|^app/routes/||; s|\.(tsx|ts|jsx|js|md|mdx)$||; s|\.|/|g; s|_index$||; s|^|/|' \
|
|
39
|
-
| sort -u | jq -Rn '[inputs]';;
|
|
153
|
+
| sort -u | suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
40
154
|
solid-start)
|
|
41
155
|
find src/routes -type f \( -name '*.tsx' -o -name '*.ts' -o -name '*.jsx' -o -name '*.js' \) 2>/dev/null \
|
|
42
156
|
| sed -E 's|^src/routes||; s|\.(tsx|ts|jsx|js)$||; s|/index$||; s|\([^)]+\)/||g' \
|
|
43
|
-
| sort -u | jq -Rn '[inputs]';;
|
|
157
|
+
| sort -u | suffix_dynamic_routes | jq -Rn '[inputs]';;
|
|
44
158
|
*) echo '[]';;
|
|
45
159
|
esac
|