start-vibing 4.3.2 → 4.3.3
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "start-vibing",
|
|
3
|
-
"version": "4.3.
|
|
4
|
-
"description": "Setup Claude Code with 9 plugins, 6 community skills, and 8 MCP servers. Parallel install, auto-accept, superpowers + ralph-loop. super-design 0.6.
|
|
3
|
+
"version": "4.3.3",
|
|
4
|
+
"description": "Setup Claude Code with 9 plugins, 6 community skills, and 8 MCP servers. Parallel install, auto-accept, superpowers + ralph-loop. super-design 0.6.3: DSC-choice active selection — score-typeui.mjs ranks 12 typeui-* skills by 7-axis fingerprint (density/contrast/geometry/color/typography/motion/audience) from findings+DIS, top-3 written to typeui-proposal.json with copy-paste CLI; harvest-typeui.sh pulls typeui.sh catalog + vercel-labs/anthropic fallbacks; sd-fix --typeui <name> rebrands V1-V8 fixes to snap spacing/color/radius to chosen direction's tokens.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"start-vibing": "./dist/cli.js"
|
|
@@ -8,7 +8,7 @@ description: >
|
|
|
8
8
|
UX audit (WCAG 2.2 AA, Nielsen heuristics, Baymard, CWV), and synthesized
|
|
9
9
|
overview. Re-audits only what changed since last run. On explicit user request,
|
|
10
10
|
applies surgical fixes with full rollback.
|
|
11
|
-
version: 0.6.
|
|
11
|
+
version: 0.6.3
|
|
12
12
|
---
|
|
13
13
|
|
|
14
14
|
# super-design
|
|
@@ -33,17 +33,25 @@ Four-phase pipeline with 6 specialist agents:
|
|
|
33
33
|
low density, weak CTA hierarchy, vibecode smell).
|
|
34
34
|
- **Step 3h mobile-native audit** (21-item Duolingo/Linear/Arc/Cash-App
|
|
35
35
|
checklist) — replaces "responsive-web-on-a-phone" thinking.
|
|
36
|
-
- C16 ≤ 4 →
|
|
36
|
+
- C16 ≤ 4 → **DSC-choice** proposal: sd-synthesis runs
|
|
37
|
+
`scripts/score-typeui.mjs --from-audit <dir>` to derive a 7-axis site
|
|
38
|
+
fingerprint (density/contrast/geometry/color/typography/motion/audience)
|
|
39
|
+
and rank all installed typeui-* skills by fit 0-1. Top-3 are written to
|
|
40
|
+
`typeui-proposal.json` + shown in overview.md with a copy-paste CLI.
|
|
37
41
|
Produces findings.json + design-intelligence.json with SHOT+QUOTE+SEL+VAL.
|
|
38
42
|
3. **Synthesis** (sd-synthesis) — unifies research + audit + design-intelligence
|
|
39
|
-
into overview.md (per-page DIS table + executive summary
|
|
43
|
+
into overview.md (per-page DIS table + executive summary + typeui proposal
|
|
44
|
+
when craft ≤ 4).
|
|
40
45
|
4. **Fix** (sd-fix + two-stage verify) — optional. Applies safe fixes with
|
|
41
46
|
technical gates (types/lint/tests) AND semantic verification ("does this
|
|
42
47
|
fix actually resolve the finding, or just mask it?"). Template families:
|
|
43
48
|
A1-A15 a11y · V1-V8 design · U1-U10 ux · P1-P10 perf · **M1-M15 mobile**
|
|
44
49
|
(cards-in-flex-col → compact list, table-on-mobile → card-per-row,
|
|
45
50
|
centered-modal → bottom-sheet, etc.) · **DSC-1 design-skill advisory**
|
|
46
|
-
(proposes typeui-* direction
|
|
51
|
+
(proposes typeui-* direction). With `--typeui <name>` flag, sd-fix loads
|
|
52
|
+
the chosen typeui skill (tokens: primary, radius, spacing scale,
|
|
53
|
+
typography) and rebrands V1-V8 fixes so every spacing/color/radius target
|
|
54
|
+
snaps to that direction's scale — turning advisory into applied. After each
|
|
47
55
|
successful fix, re-drives Playwright to capture an after-screenshot (full
|
|
48
56
|
page + element crop) and emits `docs/super-design/sessions/<id>/fix-report.md`:
|
|
49
57
|
a self-contained visual diff with before/after images, file diffs,
|
|
@@ -109,6 +117,15 @@ Do NOT paste overview into chat.
|
|
|
109
117
|
`name` entry from `scripts/detect-apps.sh`). Required when `--scope <url>`
|
|
110
118
|
is ambiguous between multiple apps.
|
|
111
119
|
- `--fix` — run sd-fix after audit
|
|
120
|
+
- `--typeui <name>` — combine with `--fix` to apply fixes aligned to a
|
|
121
|
+
chosen typeui direction (e.g. `--fix --typeui application`). Loads tokens
|
|
122
|
+
from `~/.claude/skills/typeui-<name>/SKILL.md` and rebrands V1-V8 targets
|
|
123
|
+
to that scale. Picked from the top-3 proposed in overview.md typeui
|
|
124
|
+
block. Without this flag, DSC-1 stays advisory. Run
|
|
125
|
+
`bash scripts/score-typeui.mjs --list` to see all installed directions.
|
|
126
|
+
- `--harvest-typeui` — run `scripts/harvest-typeui.sh` first to pull
|
|
127
|
+
missing typeui-* and related design skills (typeui.sh registry with
|
|
128
|
+
built-in catalog fallback). Idempotent; add `--refresh` to re-download.
|
|
112
129
|
- `--dry-run` — artifacts without committing state
|
|
113
130
|
- `--ci` — non-interactive, create PR, exit non-zero on blockers
|
|
114
131
|
- `--update-baselines` — Re-hash pages and tokens without re-auditing (use after accepted cosmetic drift). Also accepted by `scripts/visual-regression.sh` to overwrite `.super-design/baselines/*.png` with the current capture.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# harvest-typeui.sh — discover + install typeui-* and related design skills.
|
|
3
|
+
#
|
|
4
|
+
# Sources, in order:
|
|
5
|
+
# 1. typeui.sh registry (if reachable) — fetch catalog.json, install missing
|
|
6
|
+
# 2. vercel-labs/agent-skills design skills (composition, web-design-guidelines)
|
|
7
|
+
# 3. anthropics/skills webapp-testing + mcp-builder (already covered, idempotent)
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# harvest-typeui.sh # install everything missing (user-scope ~/.claude/skills/)
|
|
11
|
+
# harvest-typeui.sh --list # print catalog without installing
|
|
12
|
+
# harvest-typeui.sh --refresh # re-download even if already installed
|
|
13
|
+
# harvest-typeui.sh --dest <dir> # install to custom dir (default: ~/.claude/skills)
|
|
14
|
+
|
|
15
|
+
set -euo pipefail
|
|
16
|
+
|
|
17
|
+
DEST="${HOME}/.claude/skills"
|
|
18
|
+
LIST_ONLY=0
|
|
19
|
+
REFRESH=0
|
|
20
|
+
|
|
21
|
+
while (( $# )); do
|
|
22
|
+
case "$1" in
|
|
23
|
+
--list) LIST_ONLY=1 ;;
|
|
24
|
+
--refresh) REFRESH=1 ;;
|
|
25
|
+
--dest) DEST="$2"; shift ;;
|
|
26
|
+
-h|--help)
|
|
27
|
+
sed -n '2,18p' "$0"
|
|
28
|
+
exit 0 ;;
|
|
29
|
+
*) echo "unknown flag: $1" >&2; exit 2 ;;
|
|
30
|
+
esac
|
|
31
|
+
shift
|
|
32
|
+
done
|
|
33
|
+
|
|
34
|
+
mkdir -p "$DEST"
|
|
35
|
+
|
|
36
|
+
TYPEUI_CATALOG_URL="${TYPEUI_CATALOG_URL:-https://typeui.sh/catalog.json}"
|
|
37
|
+
TMP=$(mktemp)
|
|
38
|
+
trap 'rm -f "$TMP"' EXIT
|
|
39
|
+
|
|
40
|
+
log() { echo "[harvest-typeui] $*"; }
|
|
41
|
+
|
|
42
|
+
fetch_catalog() {
|
|
43
|
+
if command -v curl >/dev/null 2>&1; then
|
|
44
|
+
curl -sSfL --max-time 10 "$TYPEUI_CATALOG_URL" -o "$TMP" 2>/dev/null && return 0
|
|
45
|
+
fi
|
|
46
|
+
if command -v wget >/dev/null 2>&1; then
|
|
47
|
+
wget -q --timeout=10 -O "$TMP" "$TYPEUI_CATALOG_URL" 2>/dev/null && return 0
|
|
48
|
+
fi
|
|
49
|
+
return 1
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Fallback catalog — built-in list of known typeui skills + companions.
|
|
53
|
+
# Format: name|base_url (raw SKILL.md location)
|
|
54
|
+
DEFAULT_CATALOG=$(cat <<'EOF'
|
|
55
|
+
typeui-ant|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-ant/SKILL.md
|
|
56
|
+
typeui-application|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-application/SKILL.md
|
|
57
|
+
typeui-artistic|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-artistic/SKILL.md
|
|
58
|
+
typeui-bento|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-bento/SKILL.md
|
|
59
|
+
typeui-bold|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-bold/SKILL.md
|
|
60
|
+
typeui-clean|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-clean/SKILL.md
|
|
61
|
+
typeui-dashboard|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-dashboard/SKILL.md
|
|
62
|
+
typeui-doodle|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-doodle/SKILL.md
|
|
63
|
+
typeui-dramatic|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-dramatic/SKILL.md
|
|
64
|
+
typeui-enterprise|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-enterprise/SKILL.md
|
|
65
|
+
typeui-neobrutalism|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-neobrutalism/SKILL.md
|
|
66
|
+
typeui-paper|https://raw.githubusercontent.com/typeui-sh/skills/main/typeui-paper/SKILL.md
|
|
67
|
+
composition-patterns|https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/composition-patterns/SKILL.md
|
|
68
|
+
web-design-guidelines|https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/web-design-guidelines/SKILL.md
|
|
69
|
+
react-best-practices|https://raw.githubusercontent.com/vercel-labs/agent-skills/main/skills/react-best-practices/SKILL.md
|
|
70
|
+
webapp-testing|https://raw.githubusercontent.com/anthropics/skills/main/skills/webapp-testing/SKILL.md
|
|
71
|
+
mcp-builder|https://raw.githubusercontent.com/anthropics/skills/main/skills/mcp-builder/SKILL.md
|
|
72
|
+
EOF
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
CATALOG=""
|
|
76
|
+
if fetch_catalog; then
|
|
77
|
+
# typeui.sh returned JSON — extract {name,url} tuples via node
|
|
78
|
+
if command -v node >/dev/null 2>&1; then
|
|
79
|
+
CATALOG=$(node -e "
|
|
80
|
+
const c = JSON.parse(require('fs').readFileSync('$TMP','utf8'));
|
|
81
|
+
const items = c.skills || c.items || c;
|
|
82
|
+
for (const it of items) {
|
|
83
|
+
const n = it.name || it.slug;
|
|
84
|
+
const u = it.skill_url || it.url || it.raw || it.source;
|
|
85
|
+
if (n && u) console.log(n + '|' + u);
|
|
86
|
+
}
|
|
87
|
+
" 2>/dev/null || echo "")
|
|
88
|
+
fi
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
if [[ -z "$CATALOG" ]]; then
|
|
92
|
+
log "registry unreachable or empty — using built-in catalog"
|
|
93
|
+
CATALOG="$DEFAULT_CATALOG"
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
if (( LIST_ONLY )); then
|
|
97
|
+
echo "$CATALOG"
|
|
98
|
+
exit 0
|
|
99
|
+
fi
|
|
100
|
+
|
|
101
|
+
INSTALLED=0
|
|
102
|
+
SKIPPED=0
|
|
103
|
+
FAILED=0
|
|
104
|
+
|
|
105
|
+
while IFS='|' read -r name url; do
|
|
106
|
+
[[ -z "$name" ]] && continue
|
|
107
|
+
target_dir="$DEST/$name"
|
|
108
|
+
target_file="$target_dir/SKILL.md"
|
|
109
|
+
|
|
110
|
+
if [[ -f "$target_file" && $REFRESH -eq 0 ]]; then
|
|
111
|
+
SKIPPED=$((SKIPPED + 1))
|
|
112
|
+
continue
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
mkdir -p "$target_dir"
|
|
116
|
+
if command -v curl >/dev/null 2>&1; then
|
|
117
|
+
if curl -sSfL --max-time 15 "$url" -o "$target_file" 2>/dev/null; then
|
|
118
|
+
log "installed $name"
|
|
119
|
+
INSTALLED=$((INSTALLED + 1))
|
|
120
|
+
else
|
|
121
|
+
log "failed $name ($url)"
|
|
122
|
+
FAILED=$((FAILED + 1))
|
|
123
|
+
rmdir "$target_dir" 2>/dev/null || true
|
|
124
|
+
fi
|
|
125
|
+
else
|
|
126
|
+
log "curl missing — cannot fetch $name"
|
|
127
|
+
FAILED=$((FAILED + 1))
|
|
128
|
+
fi
|
|
129
|
+
done <<< "$CATALOG"
|
|
130
|
+
|
|
131
|
+
log "done: installed=$INSTALLED skipped=$SKIPPED failed=$FAILED dest=$DEST"
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// score-typeui.mjs — rank typeui-* skills by fit against a site fingerprint.
|
|
3
|
+
//
|
|
4
|
+
// Usage:
|
|
5
|
+
// node score-typeui.mjs <fingerprint.json> [--top 3]
|
|
6
|
+
// node score-typeui.mjs --from-audit <audit-dir> # derive fingerprint from sd-audit findings + DIS
|
|
7
|
+
// node score-typeui.mjs --list # list available typeui skills with axes
|
|
8
|
+
//
|
|
9
|
+
// Fingerprint input (JSON):
|
|
10
|
+
// { density: "low|medium|high", contrast: "low|medium|high",
|
|
11
|
+
// geometry: "round|square|mixed", color: "muted|vibrant|bold",
|
|
12
|
+
// typography: "neutral|expressive|display", motion: "static|subtle|dynamic",
|
|
13
|
+
// audience: "enterprise|developer|consumer|creative" }
|
|
14
|
+
//
|
|
15
|
+
// Output (stdout): JSON { ranked: [{ name, fit, reasons, applies_tokens }] }
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import os from 'node:os';
|
|
20
|
+
|
|
21
|
+
const AXES = ['density', 'contrast', 'geometry', 'color', 'typography', 'motion', 'audience'];
|
|
22
|
+
|
|
23
|
+
// Static fingerprint matrix — derived from each typeui SKILL.md style foundations.
|
|
24
|
+
// Keep in sync with skills at ~/.claude/skills/typeui-*/SKILL.md. Expand via harvest-typeui.sh.
|
|
25
|
+
const TYPEUI = {
|
|
26
|
+
ant: {
|
|
27
|
+
density: 'high', contrast: 'medium', geometry: 'square',
|
|
28
|
+
color: 'muted', typography: 'neutral', motion: 'static', audience: 'enterprise',
|
|
29
|
+
tokens: { primary: '#1677ff', radius: 6, spacing: [4, 8, 12, 16, 24, 32] },
|
|
30
|
+
summary: 'Structured, enterprise-focused design. Data-dense tables, forms.',
|
|
31
|
+
},
|
|
32
|
+
application: {
|
|
33
|
+
density: 'medium', contrast: 'high', geometry: 'round',
|
|
34
|
+
color: 'vibrant', typography: 'neutral', motion: 'subtle', audience: 'developer',
|
|
35
|
+
tokens: { primary: '#9333ea', radius: 8, spacing: [4, 8, 12, 16, 24, 32] },
|
|
36
|
+
summary: 'Vercel/GitHub purple aesthetic. Top-bar nav, card layouts.',
|
|
37
|
+
},
|
|
38
|
+
artistic: {
|
|
39
|
+
density: 'low', contrast: 'high', geometry: 'mixed',
|
|
40
|
+
color: 'bold', typography: 'expressive', motion: 'dynamic', audience: 'creative',
|
|
41
|
+
tokens: { primary: '#000000', radius: 0, spacing: [8, 16, 24, 40, 64, 96] },
|
|
42
|
+
summary: 'High-contrast, expressive. Creative typography, bold color.',
|
|
43
|
+
},
|
|
44
|
+
bento: {
|
|
45
|
+
density: 'medium', contrast: 'medium', geometry: 'round',
|
|
46
|
+
color: 'muted', typography: 'neutral', motion: 'subtle', audience: 'consumer',
|
|
47
|
+
tokens: { primary: '#0ea5e9', radius: 16, spacing: [8, 16, 24, 32, 48, 64] },
|
|
48
|
+
summary: 'Modular grid of card-blocks. Clear hierarchy, soft spacing.',
|
|
49
|
+
},
|
|
50
|
+
bold: {
|
|
51
|
+
density: 'low', contrast: 'high', geometry: 'square',
|
|
52
|
+
color: 'bold', typography: 'display', motion: 'dynamic', audience: 'consumer',
|
|
53
|
+
tokens: { primary: '#111111', radius: 4, spacing: [8, 16, 24, 48, 80, 128] },
|
|
54
|
+
summary: 'Strong visual presence. Heavyweight type, high-contrast.',
|
|
55
|
+
},
|
|
56
|
+
clean: {
|
|
57
|
+
density: 'low', contrast: 'medium', geometry: 'round',
|
|
58
|
+
color: 'muted', typography: 'neutral', motion: 'static', audience: 'consumer',
|
|
59
|
+
tokens: { primary: '#18181b', radius: 8, spacing: [8, 16, 24, 32, 48, 72] },
|
|
60
|
+
summary: 'Simplicity-focused. Whitespace, legible type, limited palette.',
|
|
61
|
+
},
|
|
62
|
+
dashboard: {
|
|
63
|
+
density: 'high', contrast: 'high', geometry: 'round',
|
|
64
|
+
color: 'vibrant', typography: 'neutral', motion: 'subtle', audience: 'developer',
|
|
65
|
+
tokens: { primary: '#3b82f6', radius: 6, spacing: [4, 8, 12, 16, 24, 32] },
|
|
66
|
+
summary: 'Dark cloud-platform. Modular grids, glass panels, data hierarchy.',
|
|
67
|
+
},
|
|
68
|
+
doodle: {
|
|
69
|
+
density: 'low', contrast: 'medium', geometry: 'mixed',
|
|
70
|
+
color: 'vibrant', typography: 'expressive', motion: 'dynamic', audience: 'creative',
|
|
71
|
+
tokens: { primary: '#f59e0b', radius: 12, spacing: [8, 16, 24, 40, 64, 96] },
|
|
72
|
+
summary: 'Hand-drawn, sketch-like. Doodles, handwritten fonts.',
|
|
73
|
+
},
|
|
74
|
+
dramatic: {
|
|
75
|
+
density: 'low', contrast: 'high', geometry: 'mixed',
|
|
76
|
+
color: 'bold', typography: 'display', motion: 'dynamic', audience: 'creative',
|
|
77
|
+
tokens: { primary: '#dc2626', radius: 0, spacing: [16, 32, 48, 80, 128, 200] },
|
|
78
|
+
summary: 'Theatrical. Bold layouts, immersive visuals, unconventional.',
|
|
79
|
+
},
|
|
80
|
+
enterprise: {
|
|
81
|
+
density: 'high', contrast: 'high', geometry: 'square',
|
|
82
|
+
color: 'muted', typography: 'neutral', motion: 'static', audience: 'enterprise',
|
|
83
|
+
tokens: { primary: '#0f172a', radius: 4, spacing: [4, 8, 12, 16, 24, 32] },
|
|
84
|
+
summary: 'Clean, high-contrast enterprise. Drag-drop, structured layouts.',
|
|
85
|
+
},
|
|
86
|
+
neobrutalism: {
|
|
87
|
+
density: 'medium', contrast: 'high', geometry: 'square',
|
|
88
|
+
color: 'bold', typography: 'display', motion: 'subtle', audience: 'creative',
|
|
89
|
+
tokens: { primary: '#ffd700', radius: 0, spacing: [8, 16, 24, 32, 48, 64] },
|
|
90
|
+
summary: 'Bold borders, vivid accents, raw high-contrast on warm surfaces.',
|
|
91
|
+
},
|
|
92
|
+
paper: {
|
|
93
|
+
density: 'medium', contrast: 'medium', geometry: 'round',
|
|
94
|
+
color: 'muted', typography: 'expressive', motion: 'static', audience: 'consumer',
|
|
95
|
+
tokens: { primary: '#78350f', radius: 4, spacing: [8, 16, 24, 32, 48, 64] },
|
|
96
|
+
summary: 'Paper-textured, print-inspired. Serif/sans, tactile.',
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Axis weights — density/contrast/geometry dominate; audience is secondary.
|
|
101
|
+
const WEIGHTS = {
|
|
102
|
+
density: 0.22, contrast: 0.18, geometry: 0.16, color: 0.14,
|
|
103
|
+
typography: 0.12, motion: 0.10, audience: 0.08,
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
function matchScore(a, b) {
|
|
107
|
+
if (a === b) return 1.0;
|
|
108
|
+
const pairs = {
|
|
109
|
+
'low|medium': 0.5, 'medium|high': 0.5,
|
|
110
|
+
'muted|vibrant': 0.5, 'vibrant|bold': 0.5,
|
|
111
|
+
'neutral|expressive': 0.5, 'expressive|display': 0.5,
|
|
112
|
+
'static|subtle': 0.5, 'subtle|dynamic': 0.5,
|
|
113
|
+
'round|mixed': 0.6, 'square|mixed': 0.6,
|
|
114
|
+
};
|
|
115
|
+
const key = [a, b].sort().join('|');
|
|
116
|
+
return pairs[key] ?? 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function scoreFor(fp, typeui) {
|
|
120
|
+
let total = 0;
|
|
121
|
+
const reasons = [];
|
|
122
|
+
for (const axis of AXES) {
|
|
123
|
+
const fpv = fp[axis];
|
|
124
|
+
const tuv = typeui[axis];
|
|
125
|
+
if (!fpv || !tuv) continue;
|
|
126
|
+
const s = matchScore(fpv, tuv);
|
|
127
|
+
total += s * WEIGHTS[axis];
|
|
128
|
+
if (s >= 0.5) reasons.push(`${axis}:${tuv}`);
|
|
129
|
+
}
|
|
130
|
+
return { fit: Math.round(total * 100) / 100, reasons };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function rank(fp, topN = 3) {
|
|
134
|
+
const scored = Object.entries(TYPEUI).map(([name, tu]) => ({
|
|
135
|
+
name: `typeui-${name}`,
|
|
136
|
+
...scoreFor(fp, tu),
|
|
137
|
+
applies_tokens: tu.tokens,
|
|
138
|
+
summary: tu.summary,
|
|
139
|
+
}));
|
|
140
|
+
scored.sort((a, b) => b.fit - a.fit);
|
|
141
|
+
return scored.slice(0, topN);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Derive fingerprint from sd-audit findings.json + design-intelligence.json.
|
|
145
|
+
function fingerprintFromAudit(auditDir) {
|
|
146
|
+
const fp = {};
|
|
147
|
+
const diPath = path.join(auditDir, 'design-intelligence.json');
|
|
148
|
+
const fPath = path.join(auditDir, 'findings.json');
|
|
149
|
+
if (!fs.existsSync(diPath)) {
|
|
150
|
+
throw new Error(`design-intelligence.json not found in ${auditDir}`);
|
|
151
|
+
}
|
|
152
|
+
const di = JSON.parse(fs.readFileSync(diPath, 'utf8'));
|
|
153
|
+
const findings = fs.existsSync(fPath) ? JSON.parse(fs.readFileSync(fPath, 'utf8')) : [];
|
|
154
|
+
|
|
155
|
+
const scores = di.per_page?.[0]?.categories || di.categories || {};
|
|
156
|
+
const s = (k) => scores[k]?.score ?? scores[k] ?? 5;
|
|
157
|
+
|
|
158
|
+
// Density: C1 density + C11 information scent
|
|
159
|
+
const densityScore = (s('C1') + s('C11')) / 2;
|
|
160
|
+
fp.density = densityScore >= 7 ? 'high' : densityScore >= 4 ? 'medium' : 'low';
|
|
161
|
+
|
|
162
|
+
// Contrast: C6 contrast (inverse — low craft = low contrast detected)
|
|
163
|
+
fp.contrast = s('C6') >= 7 ? 'high' : s('C6') >= 4 ? 'medium' : 'low';
|
|
164
|
+
|
|
165
|
+
// Geometry: C14 border-radius variance (heuristic)
|
|
166
|
+
const radiusVar = di.summary?.radius_variance ?? 0;
|
|
167
|
+
fp.geometry = radiusVar > 8 ? 'mixed' : radiusVar > 2 ? 'round' : 'square';
|
|
168
|
+
|
|
169
|
+
// Color: C5 palette saturation
|
|
170
|
+
const satScore = di.summary?.color_saturation ?? 0.5;
|
|
171
|
+
fp.color = satScore > 0.7 ? 'bold' : satScore > 0.4 ? 'vibrant' : 'muted';
|
|
172
|
+
|
|
173
|
+
// Typography: C7 type scale variance
|
|
174
|
+
const typeVariance = di.summary?.type_variance ?? 0.5;
|
|
175
|
+
fp.typography = typeVariance > 0.7 ? 'display' : typeVariance > 0.4 ? 'expressive' : 'neutral';
|
|
176
|
+
|
|
177
|
+
// Motion: C13 motion/transitions
|
|
178
|
+
fp.motion = s('C13') >= 7 ? 'dynamic' : s('C13') >= 4 ? 'subtle' : 'static';
|
|
179
|
+
|
|
180
|
+
// Audience: inferred from findings categories — best-effort
|
|
181
|
+
const mobileCount = findings.filter((f) => f.id?.startsWith('M')).length;
|
|
182
|
+
const dataCount = findings.filter((f) => /table|dense|data/i.test(f.quote || '')).length;
|
|
183
|
+
fp.audience = dataCount > mobileCount ? 'enterprise' : 'consumer';
|
|
184
|
+
|
|
185
|
+
return fp;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function listSkills() {
|
|
189
|
+
return Object.entries(TYPEUI).map(([name, tu]) => ({
|
|
190
|
+
name: `typeui-${name}`,
|
|
191
|
+
axes: AXES.reduce((o, a) => ({ ...o, [a]: tu[a] }), {}),
|
|
192
|
+
summary: tu.summary,
|
|
193
|
+
}));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// CLI
|
|
197
|
+
const args = process.argv.slice(2);
|
|
198
|
+
if (args.includes('--list')) {
|
|
199
|
+
console.log(JSON.stringify(listSkills(), null, 2));
|
|
200
|
+
process.exit(0);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
let fp;
|
|
204
|
+
let topN = 3;
|
|
205
|
+
const topIdx = args.indexOf('--top');
|
|
206
|
+
if (topIdx >= 0) topN = parseInt(args[topIdx + 1], 10) || 3;
|
|
207
|
+
|
|
208
|
+
const fromAuditIdx = args.indexOf('--from-audit');
|
|
209
|
+
if (fromAuditIdx >= 0) {
|
|
210
|
+
const dir = args[fromAuditIdx + 1];
|
|
211
|
+
if (!dir) {
|
|
212
|
+
console.error('--from-audit requires a directory path');
|
|
213
|
+
process.exit(2);
|
|
214
|
+
}
|
|
215
|
+
fp = fingerprintFromAudit(dir);
|
|
216
|
+
} else if (args[0] && fs.existsSync(args[0])) {
|
|
217
|
+
fp = JSON.parse(fs.readFileSync(args[0], 'utf8'));
|
|
218
|
+
} else {
|
|
219
|
+
console.error('Usage: score-typeui.mjs <fingerprint.json> | --from-audit <dir> | --list');
|
|
220
|
+
process.exit(2);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const ranked = rank(fp, topN);
|
|
224
|
+
console.log(JSON.stringify({ fingerprint: fp, ranked }, null, 2));
|