@ericrisco/rsc 3.1.3 → 3.3.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/README.md +8 -6
- package/manifest.json +43 -69
- package/package.json +6 -1
- package/scripts/design-upstream.js +92 -0
- package/scripts/doctor.js +0 -24
- package/scripts/lib/default-skill-floor.js +5 -1
- package/scripts/lib/domains.js +2 -2
- package/scripts/lib/recommend.js +1 -3
- package/scripts/lib/retired-skills.js +7 -1
- package/skills/ai-media/SKILL.md +2 -2
- package/skills/design/IMPECCABLE-LICENSE.txt +191 -0
- package/skills/design/SKILL.md +79 -328
- package/skills/design/THIRD_PARTY.md +26 -0
- package/skills/design/engine/LICENSE +21 -0
- package/skills/design/engine/RUN.md +515 -0
- package/skills/design/engine/UPSTREAM.json +4 -0
- package/skills/design/engine/assets/app.css +216 -0
- package/skills/design/engine/assets/base.css +105 -0
- package/skills/design/engine/assets/base.js +37 -0
- package/skills/design/engine/assets/charts.js +617 -0
- package/skills/design/engine/assets/scenes/stage.css +28 -0
- package/skills/design/engine/assets/scenes/stage.js +133 -0
- package/skills/design/engine/library/app.json +1327 -0
- package/skills/design/engine/library/directions.json +1078 -0
- package/skills/design/engine/library/heroes.json +1086 -0
- package/skills/design/engine/library/motions.json +92 -0
- package/skills/design/engine/library/palettes.json +1485 -0
- package/skills/design/engine/library/photos.json +7240 -0
- package/skills/design/engine/library/sections.json +4429 -0
- package/skills/design/engine/library/sites.json +8350 -0
- package/skills/design/engine/library/type.json +2024 -0
- package/skills/design/engine/references/app-screens.md +1199 -0
- package/skills/design/engine/references/colour.md +141 -0
- package/skills/design/engine/references/copy.md +177 -0
- package/skills/design/engine/references/critique.md +182 -0
- package/skills/design/engine/references/imagery.md +791 -0
- package/skills/design/engine/references/layout.md +292 -0
- package/skills/design/engine/references/motion.md +980 -0
- package/skills/design/engine/references/research.md +356 -0
- package/skills/design/engine/references/scenes.md +174 -0
- package/skills/design/engine/references/tells.md +115 -0
- package/skills/design/engine/references/type.md +170 -0
- package/skills/design/engine/scripts/assets.mjs +865 -0
- package/skills/design/engine/scripts/draw.mjs +584 -0
- package/skills/design/engine/scripts/inspect.mjs +303 -0
- package/skills/design/engine/scripts/lib/app-draw.mjs +203 -0
- package/skills/design/engine/scripts/lib/cdp.mjs +248 -0
- package/skills/design/engine/scripts/lib/probe.mjs +909 -0
- package/skills/design/engine/scripts/lib/rules.mjs +243 -0
- package/skills/design/engine/scripts/lint.mjs +119 -0
- package/skills/design/engine/scripts/scenes.mjs +124 -0
- package/skills/design/evals/cases.yaml +35 -46
- package/skills/design/references/design-md.md +49 -0
- package/skills/design/references/finish-review.md +46 -0
- package/skills/design/references/modes.md +51 -0
- package/skills/expo/SKILL.md +3 -4
- package/skills/marketing/references/landing-copy.md +2 -2
- package/skills/react/SKILL.md +2 -2
- package/skills/react-native/SKILL.md +3 -3
- package/skills/scroll-film/SKILL.md +220 -0
- package/skills/scroll-film/assets/page-template.html +128 -0
- package/skills/scroll-film/assets/scrub-engine.js +352 -0
- package/skills/scroll-film/evals/README.md +26 -0
- package/skills/scroll-film/evals/cases.yaml +81 -0
- package/skills/scroll-film/references/chain.md +218 -0
- package/skills/scroll-film/references/engine.md +132 -0
- package/skills/scroll-film/scripts/verify.sh +158 -0
- package/skills/swift-ios/SKILL.md +1 -1
- package/skills/ui-engineering/SKILL.md +3 -4
- package/skills/ui-engineering/evals/cases.yaml +2 -2
- package/skills/variant-explorer/SKILL.md +4 -4
- package/skills/variant-explorer/evals/cases.yaml +1 -1
- package/targets/clone-bootstrap.mjs +1 -1
- package/scripts/lib/design-identity.js +0 -117
- package/scripts/lib/starting-point.js +0 -314
- package/skills/design/evals/README.md +0 -53
- package/skills/design/references/ai-tells.md +0 -75
- package/skills/design/references/brand-grounding.md +0 -197
- package/skills/design/references/copywriting-frameworks.md +0 -138
- package/skills/design/references/design-systems.md +0 -49
- package/skills/design/references/inspiration-sources.md +0 -96
- package/skills/design/references/landing-anatomy-and-cro.md +0 -202
- package/skills/design/references/motion-and-interaction.md +0 -228
- package/skills/design/references/research-method.md +0 -154
- package/skills/design/references/signature-and-craft.md +0 -148
- package/skills/design/references/starting-point.md +0 -75
- package/skills/design/references/trends-2026.md +0 -80
- package/skills/design/references/visual-system.md +0 -237
- package/skills/design/scripts/verify.sh +0 -357
- package/skills/design-dna/PROMPT.md +0 -211
- package/skills/design-dna/SKILL.md +0 -162
- package/skills/design-dna/design-dna.schema.json +0 -230
- package/skills/design-dna/evals/README.md +0 -64
- package/skills/design-dna/evals/cases.yaml +0 -68
- package/skills/design-dna/references/method.md +0 -436
- package/skills/design-dna/scripts/emit.py +0 -164
- package/skills/design-dna/templates/PROMPT.md.tmpl +0 -42
- package/skills/design-dna/templates/SKILL.md.tmpl +0 -42
- package/skills/design-dna/templates/check.py.tmpl +0 -143
- package/skills/design-loop/SKILL.md +0 -160
- package/skills/design-loop/evals/README.md +0 -56
- package/skills/design-loop/evals/cases.yaml +0 -60
- package/skills/motion-craft/SKILL.md +0 -125
- package/skills/motion-craft/evals/cases.yaml +0 -38
- package/skills/motion-craft/scripts/verify.sh +0 -75
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
// Does this harness have a design identity, and can we tell?
|
|
2
|
-
//
|
|
3
|
-
// `design` has declared for a long time that without a brand study it STOPS and does not design.
|
|
4
|
-
// Nothing checked it: `grep -rln "wiki/brand" scripts/` returned nothing before this file existed.
|
|
5
|
-
// That is P2 — a rule declared binding with no mechanism is a decorative gate. Spec:
|
|
6
|
-
// 02-DOCS/wiki/sdd/specs/design-area-cleanup.md
|
|
7
|
-
//
|
|
8
|
-
// Three states, and the third is the whole point. A checker that cannot look must never report
|
|
9
|
-
// green: that exact defect was found and fixed in the design-dna style checker two days earlier,
|
|
10
|
-
// where regex probes ran against empty text and returned vacuous passes. Absence of evidence is
|
|
11
|
-
// reported as absence of evidence.
|
|
12
|
-
//
|
|
13
|
-
// Read-only. It never writes: 02-DOCS is untracked (P9) and there is no undo there.
|
|
14
|
-
import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
15
|
-
import { join } from 'node:path';
|
|
16
|
-
|
|
17
|
-
// Where the harness keeps durable design knowledge, and the section in the root instructions file
|
|
18
|
-
// that points at it. Both are the `design`/`harness` convention, not invented here.
|
|
19
|
-
export const BRAND_DIR = join('02-DOCS', 'wiki', 'brand');
|
|
20
|
-
export const BRAND_SECTION = '## Brand & voice';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* @returns {{state: 'present'|'missing'|'inconclusive', linked: boolean, articles: string[],
|
|
24
|
-
* reason: string, fix?: string}}
|
|
25
|
-
* `present` — articles exist under the brand dir. `linked` says whether the root
|
|
26
|
-
* instructions file points at them, which is how other skills find it.
|
|
27
|
-
* `missing` — looked, found nothing. Carries the fix (P6: a denial carries its way out).
|
|
28
|
-
* `inconclusive` — could not look. Never conflated with `missing`, and never with green.
|
|
29
|
-
*/
|
|
30
|
-
export function designIdentity(root = process.cwd()) {
|
|
31
|
-
const dir = join(root, BRAND_DIR);
|
|
32
|
-
|
|
33
|
-
let articles;
|
|
34
|
-
try {
|
|
35
|
-
if (!existsSync(dir)) {
|
|
36
|
-
// existsSync FOLLOWS symlinks, so a dangling one reports false and would be filed as "no
|
|
37
|
-
// identity here". It is not absence, it is unreadability — the same conflation this whole
|
|
38
|
-
// area was cleaning up. lstat sees the link itself and tells the two apart.
|
|
39
|
-
let danglingLink = false;
|
|
40
|
-
try { danglingLink = lstatSync(dir).isSymbolicLink(); } catch { /* genuinely absent */ }
|
|
41
|
-
if (danglingLink) {
|
|
42
|
-
return {
|
|
43
|
-
state: 'inconclusive',
|
|
44
|
-
linked: false,
|
|
45
|
-
articles: [],
|
|
46
|
-
reason: `${BRAND_DIR} is a symlink that does not resolve`,
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
state: 'missing',
|
|
51
|
-
linked: false,
|
|
52
|
-
articles: [],
|
|
53
|
-
reason: `no ${BRAND_DIR}/ in this harness`,
|
|
54
|
-
fix: 'run `design-loop` to converge on a winner, then `design-dna` to write its record here',
|
|
55
|
-
};
|
|
56
|
-
}
|
|
57
|
-
// Not statSync: it follows symlinks, so a symlink here could report on a directory outside the
|
|
58
|
-
// declared zone. Same reason drift-check.js:167 uses dirents.
|
|
59
|
-
if (!statSync(dir).isDirectory()) {
|
|
60
|
-
return {
|
|
61
|
-
state: 'inconclusive',
|
|
62
|
-
linked: false,
|
|
63
|
-
articles: [],
|
|
64
|
-
reason: `${BRAND_DIR} exists but is not a directory`,
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
articles = readdirSync(dir).filter((f) => f.endsWith('.md') && f !== 'index.md');
|
|
68
|
-
} catch (e) {
|
|
69
|
-
// Unreadable for any reason (permissions, dangling symlink, ELOOP). We did not look, so we
|
|
70
|
-
// cannot say. Reporting `missing` here would invent a finding; reporting green would hide one.
|
|
71
|
-
return {
|
|
72
|
-
state: 'inconclusive',
|
|
73
|
-
linked: false,
|
|
74
|
-
articles: [],
|
|
75
|
-
reason: `cannot read ${BRAND_DIR}: ${e.code || e.message}`,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
// The link matters on its own: five skills reach the identity through the root instructions file,
|
|
80
|
-
// so an unlinked identity is one they will not find.
|
|
81
|
-
let linked = false;
|
|
82
|
-
const claudeMd = join(root, 'CLAUDE.md');
|
|
83
|
-
if (existsSync(claudeMd)) {
|
|
84
|
-
try {
|
|
85
|
-
linked = readFileSync(claudeMd, 'utf8').includes(BRAND_SECTION);
|
|
86
|
-
} catch {
|
|
87
|
-
// The articles are what we were asked about and we did read them. A CLAUDE.md we could not
|
|
88
|
-
// read makes `linked` unknown, not the whole answer unknown — report it in the reason.
|
|
89
|
-
return {
|
|
90
|
-
state: articles.length ? 'present' : 'missing',
|
|
91
|
-
linked: false,
|
|
92
|
-
articles,
|
|
93
|
-
reason: `${articles.length} article(s), but CLAUDE.md could not be read to confirm the link`,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
if (!articles.length) {
|
|
99
|
-
return {
|
|
100
|
-
state: 'missing',
|
|
101
|
-
linked,
|
|
102
|
-
articles: [],
|
|
103
|
-
reason: `${BRAND_DIR}/ exists but holds no identity article`,
|
|
104
|
-
fix: 'run `design-loop` to converge on a winner, then `design-dna` to write its record here',
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return {
|
|
109
|
-
state: 'present',
|
|
110
|
-
linked,
|
|
111
|
-
articles,
|
|
112
|
-
reason: linked
|
|
113
|
-
? `${articles.length} article(s), linked from CLAUDE.md`
|
|
114
|
-
: `${articles.length} article(s), but no "${BRAND_SECTION}" section in CLAUDE.md points at them`,
|
|
115
|
-
...(linked ? {} : { fix: `add a "${BRAND_SECTION}" section to CLAUDE.md linking ${BRAND_DIR}/` }),
|
|
116
|
-
};
|
|
117
|
-
}
|
|
@@ -1,314 +0,0 @@
|
|
|
1
|
-
// What does this project already own to start from, and does its colour hold up?
|
|
2
|
-
//
|
|
3
|
-
// The design area declares three times that it will propose a starting point when the user has
|
|
4
|
-
// none — `design-loop` phase 1 on `skip`, the closing line of `design`'s brand grounding, and
|
|
5
|
-
// `design-dna`'s REUSE mode — and none of the three had anything behind it. An unbacked promise to
|
|
6
|
-
// propose falls back to the model's prior, which is the AI-template median the area exists to
|
|
7
|
-
// escape. Spec: 02-DOCS/wiki/sdd/specs/design-starting-point.md
|
|
8
|
-
//
|
|
9
|
-
// This file is the half that can be an algorithm (P1): listing what is installed, and refusing to
|
|
10
|
-
// propose a colour that fails contrast. The half that needs judgement — which reference is the right
|
|
11
|
-
// bar for what you are building — is prose, in design/references/starting-point.md.
|
|
12
|
-
//
|
|
13
|
-
// Read-only, always. `02-DOCS` is untracked and has no undo (P9), and a discovery pass has no
|
|
14
|
-
// business writing anywhere.
|
|
15
|
-
import { existsSync, lstatSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
16
|
-
import { join } from 'node:path';
|
|
17
|
-
import { homedir } from 'node:os';
|
|
18
|
-
import { designIdentity, BRAND_DIR } from './design-identity.js';
|
|
19
|
-
|
|
20
|
-
// WCAG 2.2 AA. Body text, and the relaxed floor for large text and UI components.
|
|
21
|
-
export const TEXT_MIN = 4.5;
|
|
22
|
-
export const UI_MIN = 3.0;
|
|
23
|
-
|
|
24
|
-
// Which palette roles are held to which floor. `design-dna`'s schema fixes the role vocabulary
|
|
25
|
-
// (ground, ink, accent, secondary, hairline); anything outside it is skipped rather than guessed at,
|
|
26
|
-
// because inventing a threshold for an unknown role is how a gate starts approving things.
|
|
27
|
-
const TEXT_ROLES = new Set(['ink', 'secondary']);
|
|
28
|
-
const UI_ROLES = new Set(['accent']);
|
|
29
|
-
const GROUND_ROLE = 'ground';
|
|
30
|
-
|
|
31
|
-
// A record is only reusable if it carries the fields a style is actually rebuilt from. Half a record
|
|
32
|
-
// cannot be reused, so it is reported as unreadable instead of offered as a candidate.
|
|
33
|
-
const REQUIRED = ['palette'];
|
|
34
|
-
const DIMENSIONS = ['palette', 'type', 'space', 'surface', 'motion', 'signatures', 'voice'];
|
|
35
|
-
|
|
36
|
-
/** Where `design-dna` emits, and where rsc installs, in both scopes. */
|
|
37
|
-
export function defaultRoots({ home = homedir(), cwd = process.cwd() } = {}) {
|
|
38
|
-
const roots = [];
|
|
39
|
-
for (const base of home === cwd ? [cwd] : [cwd, home]) {
|
|
40
|
-
roots.push(join(base, '.claude', 'skills'));
|
|
41
|
-
roots.push(join(base, '.rsc', 'skills'));
|
|
42
|
-
}
|
|
43
|
-
return roots;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function parseHex(hex) {
|
|
47
|
-
if (typeof hex !== 'string') return null;
|
|
48
|
-
const m = /^#([0-9a-fA-F]{6})$/.exec(hex.trim());
|
|
49
|
-
if (!m) return null;
|
|
50
|
-
const n = parseInt(m[1], 16);
|
|
51
|
-
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
// WCAG relative luminance. The sRGB transfer function, not a shortcut: an approximation here is a
|
|
55
|
-
// wrong ratio everywhere downstream, and it would pass a test written against this file's own output.
|
|
56
|
-
function luminance([r, g, b]) {
|
|
57
|
-
const lin = [r, g, b].map((c) => {
|
|
58
|
-
const s = c / 255;
|
|
59
|
-
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
|
|
60
|
-
});
|
|
61
|
-
return 0.2126 * lin[0] + 0.7152 * lin[1] + 0.0722 * lin[2];
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* WCAG 2.x contrast ratio between two colours, 1.0 … 21.0. Symmetric.
|
|
66
|
-
* @param {string} a sRGB hex, `#rrggbb`
|
|
67
|
-
* @param {string} b sRGB hex, `#rrggbb`
|
|
68
|
-
*/
|
|
69
|
-
export function contrastRatio(a, b) {
|
|
70
|
-
const ca = parseHex(a);
|
|
71
|
-
const cb = parseHex(b);
|
|
72
|
-
if (!ca || !cb) throw new TypeError(`not an sRGB hex colour: ${!ca ? a : b}`);
|
|
73
|
-
const la = luminance(ca);
|
|
74
|
-
const lb = luminance(cb);
|
|
75
|
-
const [hi, lo] = la >= lb ? [la, lb] : [lb, la];
|
|
76
|
-
return (hi + 0.05) / (lo + 0.05);
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Does this palette's text hold up against its own ground?
|
|
81
|
-
*
|
|
82
|
-
* Runs before a measured colour is written as a proposed dimension: a value that fails is not
|
|
83
|
-
* proposed at all, rather than proposed and flagged. Three outcomes are kept apart on purpose —
|
|
84
|
-
* measured-and-passing, measured-and-failing, and **not measured**. Collapsing the third into the
|
|
85
|
-
* first is the vacuous pass this area has already paid for twice.
|
|
86
|
-
*
|
|
87
|
-
* @param {Array<{role: string, hex: string, name?: string}>} colors — `palette.colors` from a dna.json
|
|
88
|
-
* @returns {{ok: boolean, pairs: Array<object>, failures: Array<object>, skipped: Array<object>,
|
|
89
|
-
* unparsed: string[], reason: string}}
|
|
90
|
-
*/
|
|
91
|
-
export function checkPairings(colors) {
|
|
92
|
-
const list = Array.isArray(colors) ? colors : [];
|
|
93
|
-
const pairs = [];
|
|
94
|
-
const skipped = [];
|
|
95
|
-
const unparsed = [];
|
|
96
|
-
|
|
97
|
-
const ground = list.find((c) => c && c.role === GROUND_ROLE);
|
|
98
|
-
const groundHex = ground ? parseHex(ground.hex) : null;
|
|
99
|
-
if (!ground) {
|
|
100
|
-
return { ok: false, pairs, failures: [], skipped, unparsed, reason: 'no ground role in the palette — nothing to measure against' };
|
|
101
|
-
}
|
|
102
|
-
if (!groundHex) {
|
|
103
|
-
unparsed.push(String(ground.hex));
|
|
104
|
-
return { ok: false, pairs, failures: [], skipped, unparsed, reason: `ground colour is not an sRGB hex: ${ground.hex}` };
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
for (const c of list) {
|
|
108
|
-
if (!c || c.role === GROUND_ROLE) continue;
|
|
109
|
-
const isText = TEXT_ROLES.has(c.role);
|
|
110
|
-
const isUi = UI_ROLES.has(c.role);
|
|
111
|
-
if (!isText && !isUi) {
|
|
112
|
-
// A hairline is not text. Saying so is not the same as saying it passed.
|
|
113
|
-
skipped.push({ role: c.role, name: c.name, reason: 'role carries no text, so no text floor applies' });
|
|
114
|
-
continue;
|
|
115
|
-
}
|
|
116
|
-
if (!parseHex(c.hex)) {
|
|
117
|
-
unparsed.push(String(c.hex));
|
|
118
|
-
continue;
|
|
119
|
-
}
|
|
120
|
-
const min = isText ? TEXT_MIN : UI_MIN;
|
|
121
|
-
const ratio = Math.round(contrastRatio(c.hex, ground.hex) * 100) / 100;
|
|
122
|
-
pairs.push({ role: c.role, name: c.name, fg: c.hex, bg: ground.hex, ratio, min, ok: ratio >= min });
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const failures = pairs.filter((p) => !p.ok);
|
|
126
|
-
// Not measuring anything is not a pass. An empty failure list only means green when at least one
|
|
127
|
-
// pair was actually evaluated and nothing was left unparsed.
|
|
128
|
-
const ok = failures.length === 0 && unparsed.length === 0 && pairs.length > 0;
|
|
129
|
-
const reason = unparsed.length
|
|
130
|
-
? `${unparsed.length} colour(s) could not be parsed, so they were not measured`
|
|
131
|
-
: pairs.length === 0
|
|
132
|
-
? 'no text or UI role to measure against the ground'
|
|
133
|
-
: failures.length
|
|
134
|
-
? `${failures.length} pair(s) below their floor`
|
|
135
|
-
: `${pairs.length} pair(s) measured, all above their floor`;
|
|
136
|
-
return { ok, pairs, failures, skipped, unparsed, reason };
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function readRecord(dir, slug) {
|
|
140
|
-
const file = join(dir, 'dna.json');
|
|
141
|
-
let raw;
|
|
142
|
-
try {
|
|
143
|
-
raw = readFileSync(file, 'utf8');
|
|
144
|
-
} catch (e) {
|
|
145
|
-
return { error: `cannot read dna.json: ${e.code || e.message}` };
|
|
146
|
-
}
|
|
147
|
-
let dna;
|
|
148
|
-
try {
|
|
149
|
-
dna = JSON.parse(raw);
|
|
150
|
-
} catch (e) {
|
|
151
|
-
return { error: `dna.json is not valid JSON: ${e.message}` };
|
|
152
|
-
}
|
|
153
|
-
const missing = REQUIRED.filter((k) => !dna || !dna[k]);
|
|
154
|
-
if (missing.length) return { error: `record is missing ${missing.join(', ')} — half a record cannot be reused` };
|
|
155
|
-
|
|
156
|
-
return {
|
|
157
|
-
record: {
|
|
158
|
-
slug: (dna.meta && dna.meta.slug) || slug,
|
|
159
|
-
name: (dna.meta && dna.meta.name) || slug,
|
|
160
|
-
path: file,
|
|
161
|
-
covers: DIMENSIONS.filter((d) => dna[d]),
|
|
162
|
-
palette: (dna.palette && dna.palette.colors) || [],
|
|
163
|
-
},
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// What the harness's own identity articles mention. A record the project actually used outranks one
|
|
168
|
-
// that merely happens to be installed — but the others stay on the list, because choosing for the
|
|
169
|
-
// user is exactly what the spec forbids.
|
|
170
|
-
function citedSlugs(harnessRoot) {
|
|
171
|
-
const dir = join(harnessRoot, BRAND_DIR);
|
|
172
|
-
let text = '';
|
|
173
|
-
try {
|
|
174
|
-
for (const f of readdirSync(dir)) {
|
|
175
|
-
if (!f.endsWith('.md')) continue;
|
|
176
|
-
try { text += readFileSync(join(dir, f), 'utf8').toLowerCase(); } catch { /* one unreadable article is not the answer */ }
|
|
177
|
-
}
|
|
178
|
-
} catch { /* no identity here; designIdentity reports that half */ }
|
|
179
|
-
return text;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* What this project already owns to start a design from.
|
|
184
|
-
*
|
|
185
|
-
* @param {string[]} roots — skill directories to scan. Defaults to both scopes, both layouts.
|
|
186
|
-
* @param {string} harnessRoot — where `02-DOCS/` lives, for the identity half and the ordering.
|
|
187
|
-
* @returns {{state: 'owned'|'none'|'inconclusive', identity: object, records: object[],
|
|
188
|
-
* unreadable: Array<{path: string, reason: string}>, reason: string, fix?: string}}
|
|
189
|
-
* `owned` — an identity or at least one legible record exists.
|
|
190
|
-
* `none` — looked everywhere, found nothing. Carries the way out (P6).
|
|
191
|
-
* `inconclusive` — something could not be read. Never reported as `none`, never as green.
|
|
192
|
-
*/
|
|
193
|
-
export function ownedStartingPoints(roots = defaultRoots(), harnessRoot = process.cwd()) {
|
|
194
|
-
const identity = designIdentity(harnessRoot);
|
|
195
|
-
const records = [];
|
|
196
|
-
const unreadable = [];
|
|
197
|
-
let blind = 0;
|
|
198
|
-
|
|
199
|
-
for (const root of roots) {
|
|
200
|
-
// existsSync follows symlinks, so a dangling one answers false and would be filed as "nothing
|
|
201
|
-
// installed here". That is unreadability, not absence — the exact conflation designIdentity was
|
|
202
|
-
// fixed for. lstat sees the link itself and tells them apart.
|
|
203
|
-
if (!existsSync(root)) {
|
|
204
|
-
let dangling = false;
|
|
205
|
-
try { dangling = lstatSync(root).isSymbolicLink(); } catch { /* genuinely absent */ }
|
|
206
|
-
if (dangling) {
|
|
207
|
-
blind += 1;
|
|
208
|
-
unreadable.push({ path: root, reason: 'skills root is a symlink that does not resolve' });
|
|
209
|
-
}
|
|
210
|
-
continue;
|
|
211
|
-
}
|
|
212
|
-
let entries;
|
|
213
|
-
try {
|
|
214
|
-
if (!statSync(root).isDirectory()) {
|
|
215
|
-
blind += 1;
|
|
216
|
-
unreadable.push({ path: root, reason: 'skills root exists but is not a directory' });
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
entries = readdirSync(root, { withFileTypes: true });
|
|
220
|
-
} catch (e) {
|
|
221
|
-
blind += 1;
|
|
222
|
-
unreadable.push({ path: root, reason: `cannot read skills root: ${e.code || e.message}` });
|
|
223
|
-
continue;
|
|
224
|
-
}
|
|
225
|
-
for (const entry of entries) {
|
|
226
|
-
// Dirents, not statSync: a symlinked entry could otherwise report on a directory outside the
|
|
227
|
-
// root we were asked to scan.
|
|
228
|
-
if (!entry.isDirectory()) continue;
|
|
229
|
-
const dir = join(root, entry.name);
|
|
230
|
-
if (!existsSync(join(dir, 'dna.json'))) continue;
|
|
231
|
-
const { record, error } = readRecord(dir, entry.name);
|
|
232
|
-
if (error) { unreadable.push({ path: join(dir, 'dna.json'), reason: error }); continue; }
|
|
233
|
-
if (!records.some((r) => r.slug === record.slug)) records.push(record);
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const cited = citedSlugs(harnessRoot);
|
|
238
|
-
records.sort((a, b) => {
|
|
239
|
-
const ac = cited.includes(a.slug.toLowerCase()) ? 0 : 1;
|
|
240
|
-
const bc = cited.includes(b.slug.toLowerCase()) ? 0 : 1;
|
|
241
|
-
return ac - bc || a.slug.localeCompare(b.slug);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
if (records.length || identity.state === 'present') {
|
|
245
|
-
return {
|
|
246
|
-
state: 'owned',
|
|
247
|
-
identity,
|
|
248
|
-
records,
|
|
249
|
-
unreadable,
|
|
250
|
-
reason: [
|
|
251
|
-
identity.state === 'present' ? `identity present (${identity.articles.length} article(s))` : 'no harness identity',
|
|
252
|
-
`${records.length} style record(s)`,
|
|
253
|
-
unreadable.length ? `${unreadable.length} unreadable` : null,
|
|
254
|
-
].filter(Boolean).join(', '),
|
|
255
|
-
};
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
// Something was there and we could not use it. Saying "nothing to propose" would be a finding we
|
|
259
|
-
// did not earn, and saying nothing at all would hide it.
|
|
260
|
-
if (unreadable.length || blind || identity.state === 'inconclusive') {
|
|
261
|
-
return {
|
|
262
|
-
state: 'inconclusive',
|
|
263
|
-
identity,
|
|
264
|
-
records,
|
|
265
|
-
unreadable,
|
|
266
|
-
reason: unreadable.length
|
|
267
|
-
? `could not read ${unreadable.length} candidate(s): ${unreadable[0].reason}`
|
|
268
|
-
: `identity could not be read: ${identity.reason}`,
|
|
269
|
-
fix: 'name what could not be read, and ask for another route to it — never propose past a blind spot',
|
|
270
|
-
};
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
return {
|
|
274
|
-
state: 'none',
|
|
275
|
-
identity,
|
|
276
|
-
records: [],
|
|
277
|
-
unreadable,
|
|
278
|
-
reason: 'no harness identity and no style record installed in any scope',
|
|
279
|
-
fix: 'propose at most three openable references as candidate bars (design/references/starting-point.md), then run `design-loop`',
|
|
280
|
-
};
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* The compact projection `doctor` reports: one object, not a section (P5 — every byte of a report is
|
|
285
|
-
* paid by every user who runs it). Contrast is folded in per record because a record whose own
|
|
286
|
-
* palette fails is not a starting point anyone should be offered.
|
|
287
|
-
*
|
|
288
|
-
* @returns {{state: string, records: string[], contrast: string, unreadable: number, reason: string}}
|
|
289
|
-
*/
|
|
290
|
-
export function startingPointSummary(root = process.cwd(), roots = defaultRoots({ cwd: root })) {
|
|
291
|
-
const r = ownedStartingPoints(roots, root);
|
|
292
|
-
let failing = 0;
|
|
293
|
-
let unmeasured = 0;
|
|
294
|
-
for (const rec of r.records) {
|
|
295
|
-
const c = checkPairings(rec.palette);
|
|
296
|
-
if (c.failures.length) failing += 1;
|
|
297
|
-
else if (!c.ok) unmeasured += 1;
|
|
298
|
-
}
|
|
299
|
-
const contrast = r.records.length === 0
|
|
300
|
-
? 'no record to measure'
|
|
301
|
-
: failing
|
|
302
|
-
? `${failing} record(s) with a pair below its floor`
|
|
303
|
-
: unmeasured
|
|
304
|
-
? `${unmeasured} record(s) could not be measured`
|
|
305
|
-
: 'all records above their floors';
|
|
306
|
-
return {
|
|
307
|
-
state: r.state,
|
|
308
|
-
records: r.records.map((x) => x.slug),
|
|
309
|
-
contrast,
|
|
310
|
-
unreadable: r.unreadable.length,
|
|
311
|
-
reason: r.reason,
|
|
312
|
-
...(r.fix ? { fix: r.fix } : {}),
|
|
313
|
-
};
|
|
314
|
-
}
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
# Eval harness — `design` skill
|
|
2
|
-
|
|
3
|
-
This is an **agent-run** eval, not a shell script. The cases in `cases.yaml` are fed to a
|
|
4
|
-
Claude Code agent and graded by inspection. There is no `pytest` to run; the "harness" is the
|
|
5
|
-
procedure below plus a human (or a judge agent) checking outputs against the rubrics.
|
|
6
|
-
|
|
7
|
-
## What's under test
|
|
8
|
-
|
|
9
|
-
1. **Triggering** — does the `design` skill activate on the right prompts and stay quiet on
|
|
10
|
-
near-misses that belong to a sibling (`marketing`, `nextjs`, `flutter`) or to nothing in-repo?
|
|
11
|
-
2. **Capability** — when it does fire, does the guidance measurably beat an unguided answer?
|
|
12
|
-
|
|
13
|
-
## A. Triggering accuracy
|
|
14
|
-
|
|
15
|
-
For each prompt in `should_trigger` and `should_not_trigger`:
|
|
16
|
-
|
|
17
|
-
1. Start a fresh agent session with **only the `design` skill discoverable** (its `SKILL.md`
|
|
18
|
-
description in the system prompt / skill index). Do not pre-load sibling skills.
|
|
19
|
-
2. Paste the prompt verbatim as the user's first message.
|
|
20
|
-
3. Observe whether the agent **invokes/loads the `design` skill** (announces it, reads
|
|
21
|
-
`SKILL.md`, or applies its protocol) before answering.
|
|
22
|
-
4. Run **3–5 independent trials per prompt** (fresh session each) to average out sampling noise.
|
|
23
|
-
|
|
24
|
-
Scoring per prompt:
|
|
25
|
-
- `should_trigger` → PASS if the skill fires in the **majority** of trials.
|
|
26
|
-
- `should_not_trigger` → PASS if the skill **does not** fire in the majority of trials. Bonus:
|
|
27
|
-
the agent routes to the `route_to` target (or correctly declines when `route_to: none`).
|
|
28
|
-
|
|
29
|
-
**Pass bar:** >= 90% trigger accuracy across all prompts (true-positives + true-negatives over
|
|
30
|
-
total). Any `should_not_trigger` that fires in a majority of trials is a hard fail to fix in the
|
|
31
|
-
`SKILL.md` description/When-NOT section, even if the aggregate clears 90%.
|
|
32
|
-
|
|
33
|
-
## B. Capability uplift (with vs without)
|
|
34
|
-
|
|
35
|
-
For each `capability` scenario:
|
|
36
|
-
|
|
37
|
-
1. **Without:** fresh session, skill NOT available, give the scenario prompt. Save the answer.
|
|
38
|
-
2. **With:** fresh session, `design` skill available and applied, same prompt. Save the answer.
|
|
39
|
-
3. Grade both against the scenario's `must_include` points — one point per item, mark
|
|
40
|
-
covered / not covered. Use a second agent as judge or grade by hand.
|
|
41
|
-
4. Compute coverage = covered / total `must_include`, for each condition.
|
|
42
|
-
|
|
43
|
-
**Pass bar:** WITH the skill covers **>= 80%** of `must_include`; WITHOUT it covers materially
|
|
44
|
-
less (target a >= 30-point absolute gap). If the gap is small, the skill isn't earning its place —
|
|
45
|
-
fix the skill, not the rubric. The brand-grounding STOP and the research-first/CWV/WCAG points are
|
|
46
|
-
the high-signal items most likely to be missing without the skill; weight attention there.
|
|
47
|
-
|
|
48
|
-
## Notes / honesty
|
|
49
|
-
|
|
50
|
-
- Triggering is inherently stochastic; that's why we average trials and report a percentage, not a
|
|
51
|
-
binary. Re-run if results straddle the bar.
|
|
52
|
-
- Keep grading faithful to `SKILL.md` — if a case drifts from the actual description, fix the case.
|
|
53
|
-
- These files document intent and a repeatable procedure; they are **not** wired into CI.
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
# AI tells — the ones only a reader can catch
|
|
2
|
-
|
|
3
|
-
The mechanical tells are **not here**. `scripts/verify.sh` carries a registry of them and
|
|
4
|
-
flags them with file and line, so re-teaching them as prose would be paying twice for the
|
|
5
|
-
same rule. This file holds only what a `grep` cannot decide: presences you have to *look* at
|
|
6
|
-
the page to see.
|
|
7
|
-
|
|
8
|
-
Part of this corpus is adapted from [Leonxlnx/taste-skill](https://github.com/Leonxlnx/taste-skill)
|
|
9
|
-
(MIT), whose lists came out of real production tests. The split into checkable-vs-judged, the
|
|
10
|
-
thresholds, and the wording are ours.
|
|
11
|
-
|
|
12
|
-
Every item below is a **default**, overridable by a brief that explicitly asks for it. What
|
|
13
|
-
makes them tells is that they show up *unasked*, as the model's idea of "looking designed".
|
|
14
|
-
|
|
15
|
-
## Fabricated substance
|
|
16
|
-
|
|
17
|
-
- **A product UI built out of divs.** A fake task list, fake terminal or fake dashboard
|
|
18
|
-
assembled from styled rectangles is the single most reliable signature of generated work.
|
|
19
|
-
Use a real screenshot, a real component, a generated image, or show nothing.
|
|
20
|
-
- **Hand-rolled decorative SVG.** Abstract squiggles and blobs authored inline to fill space.
|
|
21
|
-
Icons come from a maintained set; decoration comes from a real asset or not at all.
|
|
22
|
-
- **The Jane Doe cast.** "John Doe", "Sarah Chan", egg avatars, `99.99%`, `50%`, round
|
|
23
|
-
thousands, and startup-slop brand names ("Acme", "Nexus", "SmartFlow"). Real work has
|
|
24
|
-
specific, slightly awkward names and messy numbers (`47.2%`, not `50%`).
|
|
25
|
-
- **Testimonials nobody said.** A quote with no attributable person, or five quotes with the
|
|
26
|
-
same cadence, reads as filler. One real quote beats three invented ones.
|
|
27
|
-
|
|
28
|
-
## Decoration wearing the costume of information
|
|
29
|
-
|
|
30
|
-
- **Atmospheric locale strips.** A city, a timezone, a temperature in the nav or footer. Only
|
|
31
|
-
earns its place for a genuinely distributed studio, a travel brand, or a physical venue.
|
|
32
|
-
- **Status dots with no status.** A coloured dot before nav items, list rows or badges. Keep
|
|
33
|
-
it only where it reports real state, and then only once per section.
|
|
34
|
-
- **Invented photo credits.** `Field study no. 12 · Ines Caetano` under a stock image.
|
|
35
|
-
Credit a real photographer or write a functional caption.
|
|
36
|
-
- **Pills laid over images.** Tags floated on top of a photo. Either the image speaks alone,
|
|
37
|
-
or the caption sits below it, outside the frame.
|
|
38
|
-
- **Decoration strips under the hero.** `BRAND. MOTION. SPATIAL.` in small mono caps. Only
|
|
39
|
-
legitimate when those words are real navigation or real status.
|
|
40
|
-
|
|
41
|
-
## Labels that say nothing
|
|
42
|
-
|
|
43
|
-
- **Poetic section labels.** "Field notes", "From the field", "On our desks", "Currently on
|
|
44
|
-
the bench". Use the functional name ("Testimonials", "Latest writing") or no label.
|
|
45
|
-
- **Generic step labels.** "Stage 1 / Stage 2", "Phase 01 / 02". The step content *is* the
|
|
46
|
-
label: "Install", "Configure", "Ship".
|
|
47
|
-
- **Mock-humble asides** and micro-meta sentences under a heading ("The list will stay short
|
|
48
|
-
on purpose"). Eyebrow, headline, body. Nothing else.
|
|
49
|
-
- **"Quietly trusted by".** Say "Used at", "Customers include", or let the logos speak.
|
|
50
|
-
|
|
51
|
-
## Composition habits
|
|
52
|
-
|
|
53
|
-
- **Three identical feature cards.** The default feature row. Reach for a two-column zig-zag,
|
|
54
|
-
an asymmetric grid, or a horizontal rail instead — and do not run the same split three
|
|
55
|
-
sections in a row.
|
|
56
|
-
- **The corner floater.** A giant left-aligned headline with a small explainer paragraph
|
|
57
|
-
floating in the section's top-right, aligned to nothing. Stack it, or build a real
|
|
58
|
-
two-column header.
|
|
59
|
-
- **Rotated vertical text** and crosshair grid lines drawn only to look designed. Both are
|
|
60
|
-
agency-portfolio clichés unless they organise real content.
|
|
61
|
-
- **A bento with empty cells.** N items means N cells, and at least two cells need real
|
|
62
|
-
visual variation — otherwise it is a white card grid with extra steps.
|
|
63
|
-
|
|
64
|
-
## The second default (repetition)
|
|
65
|
-
|
|
66
|
-
Obeying every rule produces correct pages that all look the same. Before fixing type and
|
|
67
|
-
palette, read the project's recorded design decisions and either reuse them deliberately or
|
|
68
|
-
say why you are departing. Two specific defaults to distrust: reaching for the same display
|
|
69
|
-
serif again, and answering every premium-consumer brief with the beige/brass/oxblood family.
|
|
70
|
-
|
|
71
|
-
## Surface treatments to distrust by default
|
|
72
|
-
|
|
73
|
-
Outer glows and neon; pure `#000000` (use an off-black); oversaturated accents that refuse to
|
|
74
|
-
sit with the neutrals; gradient fills on large headings; custom cursors (outdated, hostile to
|
|
75
|
-
accessibility and to performance); glass on every surface rather than on floating ones.
|