@bongos/core 1.20.39 → 1.20.41
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/.bongos-core.json +260 -240
- package/.claude/skills/design/SKILL.md +6 -5
- package/docs/adr/0081-tool-agnostic-design-layer.md +1 -1
- package/docs/adr/0198-third-party-skill-vendoring-policy.md +2 -2
- package/docs/adr/0349-a-version-preview-is-a-sandboxed-child-the-web-tier-launches.md +1 -0
- package/docs/file-map.md +51 -54
- package/docs/module-api-changelog.md +4 -0
- package/docs/modules-contract.md +7 -5
- package/docs/onboarding/slash-commands.md +1 -1
- package/modules/design-styles/CLAUDE.md +15 -0
- package/modules/design-styles/module.json +12 -0
- package/modules/{ui-design → design-styles}/skills/PREAMBLE.md +2 -2
- package/modules/{ui-design → design-styles}/skills/README.md +6 -6
- package/modules/{ui-design → design-styles}/skills/brandkit/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/design-taste-frontend/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/gpt-taste/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/high-end-visual-design/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/image-to-code/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/imagegen-frontend-mobile/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/imagegen-frontend-web/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/impeccable/SKILL.md +3 -3
- package/modules/{ui-design → design-styles}/skills/industrial-brutalist-ui/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/minimalist-ui/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/policy.json +1 -1
- package/modules/{ui-design → design-styles}/skills/redesign-existing-projects/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/stitch-design-taste/SKILL.md +2 -2
- package/modules/{ui-design → design-styles}/skills/style/SKILL.md +2 -2
- package/modules/npm-release/preview/divert.js +16 -6
- package/modules/npm-release/preview/proxy.js +4 -1
- package/modules/npm-release/routes/preview.js +6 -5
- package/modules/pixel-art/CLAUDE.md +15 -0
- package/modules/pixel-art/module.json +12 -0
- package/modules/provisioning/starter-bundles.js +15 -0
- package/modules/ui-design/kit/serve.js +2 -0
- package/modules/ui-design/module.json +3 -3
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/release-notes.json +12 -0
- package/scripts/gds/doc-cli-guard.js +1 -1
- package/scripts/gds/fitness-ratchets.js +4 -1
- package/scripts/gds/fitness.js +2 -1
- package/scripts/gds/publish-manifest.js +2 -0
- package/scripts/gds/skill-lint.js +30 -8
- package/src/bongos/module-scope-map.js +8 -2
- package/src/module-api.js +1 -1
- package/tests/claude_materialize.mjs +9 -8
- package/tests/module_loader.mjs +1 -1
- package/tests/npm_release_preview_divert.mjs +10 -0
- package/tests/npm_release_preview_proxy.mjs +26 -0
- package/tests/npm_release_preview_routes.mjs +15 -2
- package/tests/opt_in_skill_modules.mjs +165 -0
- package/tests/skill_lint.mjs +26 -2
- package/tests/ui_design_skills.mjs +29 -17
- package/modules/ui-design/skills/design-taste-frontend-v1/SKILL.md +0 -135
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/adapt.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/animate.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/audit.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/bolder.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/clarify.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/colorize.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/critique.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/distill.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/document.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/extract.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/harden.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/layout.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/onboard.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/optimize.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/polish.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/quieter.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/shape.md +0 -0
- /package/modules/{ui-design → design-styles}/skills/impeccable/reference/typeset.md +0 -0
- /package/{.claude → modules/pixel-art}/skills/otb-character-review/SKILL.md +0 -0
- /package/{.claude → modules/pixel-art}/skills/otb-design-review/SKILL.md +0 -0
- /package/{.claude → modules/pixel-art}/skills/otb-feedback-capture/SKILL.md +0 -0
- /package/{.claude → modules/pixel-art}/skills/otb-figma-sync/SKILL.md +0 -0
- /package/{.claude → modules/pixel-art}/skills/otb-tile-generate/SKILL.md +0 -0
|
@@ -137,6 +137,32 @@ test('JSON and other non-page answers pass through untouched, headers included',
|
|
|
137
137
|
assert.doesNotMatch(res.text, /banner/);
|
|
138
138
|
});
|
|
139
139
|
|
|
140
|
+
test('a Set-Cookie from the preview never reaches the browser (it would overwrite the live hall cookies), task 1004466', async () => {
|
|
141
|
+
const cookies = ['cb_stealth=child; Path=/', '__Host-sid=child; Path=/; Secure', 'bongos_preview=; Max-Age=0'];
|
|
142
|
+
for (const [type, body] of [['application/json', '{}'], ['text/html; charset=utf-8', '<html><body>x</body></html>']]) {
|
|
143
|
+
const http = fakeHttp(() => ({ headers: { 'content-type': type, 'set-cookie': cookies, 'x-keep': 'yes' }, body }));
|
|
144
|
+
const res = new FakeRes();
|
|
145
|
+
make(http)(request(), res);
|
|
146
|
+
await finish(res);
|
|
147
|
+
assert.equal(Object.keys(res.headers).filter((k) => k.toLowerCase() === 'set-cookie').length, 0, type);
|
|
148
|
+
assert.equal(res.headers['x-keep'], 'yes', 'other headers still pass');
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test('a streamed answer with a Set-Cookie is still streamed, cookie dropped', async () => {
|
|
153
|
+
const stream = new PassThrough();
|
|
154
|
+
const http = fakeHttp(() => ({ headers: { 'content-type': 'text/event-stream', 'set-cookie': 'cb_stealth=child' }, stream }));
|
|
155
|
+
const res = new FakeRes();
|
|
156
|
+
make(http)(request(), res);
|
|
157
|
+
await tick();
|
|
158
|
+
stream.write('data: one\n\n');
|
|
159
|
+
await tick();
|
|
160
|
+
assert.equal(res.headers['set-cookie'], undefined);
|
|
161
|
+
assert.equal(res.text, 'data: one\n\n');
|
|
162
|
+
stream.end();
|
|
163
|
+
await finish(res);
|
|
164
|
+
});
|
|
165
|
+
|
|
140
166
|
test('an event stream is streamed, not held: the first chunk arrives before the stream ends', async () => {
|
|
141
167
|
const stream = new PassThrough();
|
|
142
168
|
const http = fakeHttp(() => ({ headers: { 'content-type': 'text/event-stream' }, stream }));
|
|
@@ -52,6 +52,7 @@ let base;
|
|
|
52
52
|
before(async () => {
|
|
53
53
|
state = { state: 'idle', version: null };
|
|
54
54
|
const app = express();
|
|
55
|
+
app.set('trust proxy', true);
|
|
55
56
|
app.use(express.json());
|
|
56
57
|
app.use('/api/bongos', buildRouter({ supervisor, listPublished: async () => registry, runningVersion: () => '1.19.1040' }));
|
|
57
58
|
server = http.createServer(app);
|
|
@@ -60,10 +61,10 @@ before(async () => {
|
|
|
60
61
|
});
|
|
61
62
|
after(() => new Promise((r) => server.close(r)));
|
|
62
63
|
|
|
63
|
-
function call(method, url, { cookie = 'sid=good', body } = {}) {
|
|
64
|
+
function call(method, url, { cookie = 'sid=good', body, extra } = {}) {
|
|
64
65
|
return new Promise((resolve, reject) => {
|
|
65
66
|
const data = body === undefined ? null : JSON.stringify(body);
|
|
66
|
-
const headers = { host: 'builders.example.test' };
|
|
67
|
+
const headers = { host: 'builders.example.test', ...(extra || {}) };
|
|
67
68
|
if (cookie) headers.cookie = cookie;
|
|
68
69
|
if (data) { headers['content-type'] = 'application/json'; headers['content-length'] = Buffer.byteLength(data); }
|
|
69
70
|
const r = http.request({ host: '127.0.0.1', port: base, path: '/api/bongos' + url, method, headers }, (res) => {
|
|
@@ -178,6 +179,18 @@ test('enter sets the steering cookie for the running version and goes to the hal
|
|
|
178
179
|
assert.match(c, /^bongos_preview=1\.19\.1030; Path=\/; HttpOnly; SameSite=Lax/);
|
|
179
180
|
});
|
|
180
181
|
|
|
182
|
+
test('exit clears with the same attributes enter set, Secure included, and is not cached (task 1004466)', async () => {
|
|
183
|
+
state = { state: 'running', version: '1.19.1030' };
|
|
184
|
+
for (const extra of [undefined, { 'x-forwarded-proto': 'https' }]) {
|
|
185
|
+
const e = await call('GET', '/npm-release/preview/enter?v=1.19.1030', { extra });
|
|
186
|
+
const x = await call('GET', '/npm-release/preview/exit', { extra });
|
|
187
|
+
const attrs = (r) => [].concat(r.headers['set-cookie'])[0].split('; ').slice(1).filter((a) => !/^Max-Age=/.test(a)).join('; ');
|
|
188
|
+
assert.equal(attrs(x), attrs(e));
|
|
189
|
+
assert.equal(/Secure/.test(attrs(x)), Boolean(extra));
|
|
190
|
+
assert.equal(x.headers['cache-control'], 'no-store');
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
|
|
181
194
|
test('enter refuses a preview that is not the one running, and sets no cookie', async () => {
|
|
182
195
|
for (const [st, v] of [[{ state: 'idle', version: null }, '1.19.1030'], [{ state: 'starting', version: '1.19.1030' }, '1.19.1030'], [{ state: 'running', version: '1.19.1031' }, '1.19.1030'], [{ state: 'running', version: '1.19.1030' }, 'zzz']]) {
|
|
183
196
|
state = st;
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
// tests/opt_in_skill_modules.mjs — the two opt-in skill modules of task 1004470 (owner
|
|
2
|
+
// ruling on blocker 1000139), pinned on the REAL tree rather than a fixture core:
|
|
3
|
+
//
|
|
4
|
+
// pixel-art (default: false) — the five otb-* skills, orphans in .claude/skills/
|
|
5
|
+
// until now, carrying skill text only and never a copyrighted asset;
|
|
6
|
+
// design-styles (default: false) — the thirteen design-style skills split out of
|
|
7
|
+
// ui-design, which keeps /design + the sync skills and stays default-on.
|
|
8
|
+
//
|
|
9
|
+
// Both shapes the task names: a default-config instance materialises NEITHER module's
|
|
10
|
+
// skills, and an instance that turns both on materialises all eighteen with nothing
|
|
11
|
+
// refused by the vendoring gate. Plus the reason the split exists — skill-lint's resident
|
|
12
|
+
// listing for the default shape is ~6,800 chars smaller — and the publish allowlist that
|
|
13
|
+
// lets a released core carry the modules at all. DB-free.
|
|
14
|
+
import assert from 'node:assert/strict';
|
|
15
|
+
import { test } from 'node:test';
|
|
16
|
+
import { createRequire } from 'node:module';
|
|
17
|
+
import fs from 'node:fs';
|
|
18
|
+
import os from 'node:os';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const require = createRequire(import.meta.url);
|
|
23
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
24
|
+
const materialize = require(path.join(ROOT, 'scripts', 'gds', 'claude-materialize.js'));
|
|
25
|
+
const lint = require(path.join(ROOT, 'scripts', 'gds', 'skill-lint.js'));
|
|
26
|
+
const publish = require(path.join(ROOT, 'scripts', 'gds', 'publish-manifest.js'));
|
|
27
|
+
const readJson = (p) => JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
28
|
+
|
|
29
|
+
const PIXEL_ART = ['otb-tile-generate', 'otb-design-review', 'otb-character-review', 'otb-feedback-capture', 'otb-figma-sync'];
|
|
30
|
+
const DESIGN_STYLES = ['design-taste-frontend', 'high-end-visual-design', 'redesign-existing-projects', 'impeccable', 'style', 'minimalist-ui', 'industrial-brutalist-ui', 'gpt-taste', 'stitch-design-taste', 'image-to-code', 'imagegen-frontend-web', 'imagegen-frontend-mobile', 'brandkit'];
|
|
31
|
+
const ALL = [...PIXEL_ART, ...DESIGN_STYLES];
|
|
32
|
+
const UI_DESIGN_KEEPS = ['design', 'design-sync', 'figma-design-sync'];
|
|
33
|
+
|
|
34
|
+
function instance(modules) {
|
|
35
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'opt-in-skills-'));
|
|
36
|
+
if (modules) {
|
|
37
|
+
fs.mkdirSync(path.join(dir, 'config'), { recursive: true });
|
|
38
|
+
fs.writeFileSync(path.join(dir, 'config', 'modules.json'), JSON.stringify({ modules }));
|
|
39
|
+
}
|
|
40
|
+
return dir;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
test('the two manifests: default off, each declares exactly its skills, and ui-design keeps only its three', () => {
|
|
44
|
+
const pa = readJson(path.join(ROOT, 'modules', 'pixel-art', 'module.json'));
|
|
45
|
+
const ds = readJson(path.join(ROOT, 'modules', 'design-styles', 'module.json'));
|
|
46
|
+
const ui = readJson(path.join(ROOT, 'modules', 'ui-design', 'module.json'));
|
|
47
|
+
assert.equal(pa.key, 'pixel-art');
|
|
48
|
+
assert.equal(pa.default, false, 'pixel-art is opt-in');
|
|
49
|
+
assert.deepEqual([...pa.contributes.skills].sort(), [...PIXEL_ART].sort());
|
|
50
|
+
assert.equal(ds.key, 'design-styles');
|
|
51
|
+
assert.equal(ds.default, false, 'design-styles is opt-in');
|
|
52
|
+
assert.deepEqual([...ds.contributes.skills].sort(), [...DESIGN_STYLES].sort());
|
|
53
|
+
assert.equal(ui.default, true, 'ui-design stays default-on');
|
|
54
|
+
assert.deepEqual([...ui.contributes.skills].sort(), [...UI_DESIGN_KEEPS].sort(), 'ui-design keeps /design and the two sync skills, nothing else');
|
|
55
|
+
for (const m of [pa, ds]) {
|
|
56
|
+
for (const k of ['routes', 'pollers', 'migrations', 'uiSections']) assert.ok(!(k in m.contributes), `${m.key}: declaration-only, contributes no ${k}`);
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('design-taste-frontend-v1 is gone everywhere a skill can live', () => {
|
|
61
|
+
for (const dir of [path.join(ROOT, '.claude', 'skills'), ...fs.readdirSync(path.join(ROOT, 'modules')).map((m) => path.join(ROOT, 'modules', m, 'skills'))]) {
|
|
62
|
+
assert.ok(!fs.existsSync(path.join(dir, 'design-taste-frontend-v1')), `${path.relative(ROOT, dir)}: no design-taste-frontend-v1`);
|
|
63
|
+
}
|
|
64
|
+
const declared = fs.readdirSync(path.join(ROOT, 'modules')).flatMap((m) => {
|
|
65
|
+
const p = path.join(ROOT, 'modules', m, 'module.json');
|
|
66
|
+
return fs.existsSync(p) ? ((readJson(p).contributes || {}).skills || []) : [];
|
|
67
|
+
});
|
|
68
|
+
assert.ok(!declared.includes('design-taste-frontend-v1'), 'no module declares it');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test('the eighteen skills left the core .claude/skills/ and live only in their module', () => {
|
|
72
|
+
for (const name of PIXEL_ART) {
|
|
73
|
+
assert.ok(!execTracked(name), `${name}: not a tracked core skill any more`);
|
|
74
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'pixel-art', 'skills', name, 'SKILL.md')), `${name}: modules/pixel-art/skills/${name}/SKILL.md`);
|
|
75
|
+
}
|
|
76
|
+
for (const name of DESIGN_STYLES) {
|
|
77
|
+
assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'design-styles', 'skills', name, 'SKILL.md')), `${name}: modules/design-styles/skills/${name}/SKILL.md`);
|
|
78
|
+
assert.ok(!fs.existsSync(path.join(ROOT, 'modules', 'ui-design', 'skills', name)), `${name}: no copy left under ui-design`);
|
|
79
|
+
}
|
|
80
|
+
for (const f of ['policy.json', 'PREAMBLE.md', 'README.md']) assert.ok(fs.existsSync(path.join(ROOT, 'modules', 'design-styles', 'skills', f)), `design-styles carries skills/${f}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// A copy materialised into the core checkout (--module-skills-only) is untracked and
|
|
84
|
+
// self-ignored; only a TRACKED .claude/skills/<name> would mean the move did not happen.
|
|
85
|
+
function execTracked(name) {
|
|
86
|
+
const { spawnSync } = require('node:child_process');
|
|
87
|
+
return spawnSync('git', ['-C', ROOT, 'ls-files', '--error-unmatch', '--', `.claude/skills/${name}/SKILL.md`], { encoding: 'utf8' }).status === 0;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
test('pixel-art carries skill text only — never a reference map, a palette, a rubric or generated art', () => {
|
|
91
|
+
const files = [];
|
|
92
|
+
const walk = (d) => { for (const e of fs.readdirSync(d, { withFileTypes: true })) { const p = path.join(d, e.name); if (e.isDirectory()) walk(p); else files.push(path.relative(path.join(ROOT, 'modules', 'pixel-art'), p).split(path.sep).join('/')); } };
|
|
93
|
+
walk(path.join(ROOT, 'modules', 'pixel-art'));
|
|
94
|
+
const allowed = new Set(['module.json', 'CLAUDE.md', ...PIXEL_ART.map((n) => `skills/${n}/SKILL.md`)]);
|
|
95
|
+
assert.deepEqual(files.filter((f) => !allowed.has(f)), [], 'nothing but the manifest, its CLAUDE.md and the five SKILL.md files (ADR 0098: the art assets stay in the never-published art-pipeline)');
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test('DEFAULT config: a fresh instance materialises neither module\'s skills, and still gets /design', () => {
|
|
99
|
+
const dir = instance(null);
|
|
100
|
+
const res = materialize.materializeClaude({ coreRoot: ROOT, instanceDir: dir, dryRun: false });
|
|
101
|
+
for (const name of ALL) {
|
|
102
|
+
assert.ok(!fs.existsSync(path.join(dir, '.claude', 'skills', name)), `${name}: not materialised by default`);
|
|
103
|
+
assert.ok(res.excludedSkills.includes(name), `${name}: reported as excluded (module off)`);
|
|
104
|
+
}
|
|
105
|
+
for (const name of UI_DESIGN_KEEPS) assert.ok(fs.existsSync(path.join(dir, '.claude', 'skills', name, 'SKILL.md')), `${name}: ui-design's own skill still lands`);
|
|
106
|
+
assert.ok(fs.existsSync(path.join(dir, '.claude', 'skills', 'builder-ship', 'SKILL.md')), 'a core skill still lands');
|
|
107
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test('ENABLED config: pixel-art + design-styles on → all eighteen materialise, none refused by the vendoring gate', () => {
|
|
111
|
+
const dir = instance({ 'pixel-art': true, 'design-styles': true });
|
|
112
|
+
const res = materialize.materializeClaude({ coreRoot: ROOT, instanceDir: dir, dryRun: false });
|
|
113
|
+
assert.deepEqual(res.notes.filter((n) => n.includes('refused')), [], 'the vendoring gate refused nothing');
|
|
114
|
+
for (const name of ALL) {
|
|
115
|
+
assert.ok(fs.existsSync(path.join(dir, '.claude', 'skills', name, 'SKILL.md')), `${name}: materialised`);
|
|
116
|
+
assert.ok(!res.excludedSkills.includes(name), `${name}: not excluded`);
|
|
117
|
+
}
|
|
118
|
+
assert.ok(fs.existsSync(path.join(dir, '.claude', 'skills', 'impeccable', 'reference', 'audit.md')), 'impeccable lands as a tree, its playbooks included');
|
|
119
|
+
// the gate itself, applied to each source the materialiser resolved for the two modules
|
|
120
|
+
const roster = materialize.moduleSkillRoster({ coreRoot: ROOT, instanceDir: dir });
|
|
121
|
+
const sources = materialize.moduleSkillSources({ roster });
|
|
122
|
+
assert.deepEqual(sources.refused.filter((r) => ['pixel-art', 'design-styles'].includes(r.module)), []);
|
|
123
|
+
const mine = sources.filter((s) => ['pixel-art', 'design-styles'].includes(s.module));
|
|
124
|
+
assert.deepEqual(mine.map((s) => s.name).sort(), [...ALL].sort(), 'the materialiser resolves exactly the eighteen');
|
|
125
|
+
const policy = readJson(path.join(ROOT, 'modules', 'design-styles', 'skills', 'policy.json'));
|
|
126
|
+
for (const s of mine) if (s.vendored) assert.ok(materialize.vendoredSkillGate(s.dir, policy).ok, `${s.name}: a vendored copy passes the gate`);
|
|
127
|
+
// and the instance's provenance record names each with its module, so switching one off later withdraws it
|
|
128
|
+
const record = readJson(path.join(dir, '.claude', 'skills', materialize.INSTANCE_SKILLS_MANIFEST)).skills;
|
|
129
|
+
for (const name of PIXEL_ART) assert.equal(record[name].module, 'pixel-art', `${name}: recorded under pixel-art`);
|
|
130
|
+
for (const name of DESIGN_STYLES) assert.equal(record[name].module, 'design-styles', `${name}: recorded under design-styles`);
|
|
131
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test('switching the modules back off withdraws what an earlier run landed', () => {
|
|
135
|
+
const dir = instance({ 'pixel-art': true, 'design-styles': true });
|
|
136
|
+
materialize.materializeClaude({ coreRoot: ROOT, instanceDir: dir, dryRun: false });
|
|
137
|
+
fs.writeFileSync(path.join(dir, 'config', 'modules.json'), JSON.stringify({ modules: { 'pixel-art': false, 'design-styles': false } }));
|
|
138
|
+
const res = materialize.materializeClaude({ coreRoot: ROOT, instanceDir: dir, dryRun: false, overwrite: true });
|
|
139
|
+
for (const name of ALL) assert.ok(!fs.existsSync(path.join(dir, '.claude', 'skills', name)), `${name}: withdrawn`);
|
|
140
|
+
assert.ok(res.withdrawnSkills.length >= ALL.length, 'every one is reported withdrawn');
|
|
141
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test('skill-lint lints all eighteen but counts only the resident ones: the default shape is ~6,800 chars smaller', () => {
|
|
145
|
+
const files = lint.listSkillFiles(ROOT);
|
|
146
|
+
const names = files.map((f) => path.basename(path.dirname(f)));
|
|
147
|
+
for (const n of ALL) assert.ok(names.includes(n), `${n}: still linted though its module is off`);
|
|
148
|
+
const off = instance(null);
|
|
149
|
+
const on = instance({ 'pixel-art': true, 'design-styles': true });
|
|
150
|
+
const def = lint.lintSkills(files, { resident: lint.residentSkillFiles(ROOT, { instanceDir: off, files }) });
|
|
151
|
+
const all = lint.lintSkills(files, { resident: lint.residentSkillFiles(ROOT, { instanceDir: on, files }) });
|
|
152
|
+
const delta = all.listingChars - def.listingChars;
|
|
153
|
+
assert.ok(delta >= 6000, `the two modules take ~6,800 chars out of a default listing (measured ${delta})`);
|
|
154
|
+
assert.equal(all.listedSkills - def.listedSkills, ALL.length, 'exactly the eighteen leave the default listing');
|
|
155
|
+
assert.equal(lint.lintSkills(files).listingChars, all.listingChars, 'with no resident list, every linted file is counted (the old behaviour)');
|
|
156
|
+
for (const d of [off, on]) fs.rmSync(d, { recursive: true, force: true });
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test('a released core carries both modules (shipped is not enabled), and art-pipeline stays excluded', () => {
|
|
160
|
+
assert.ok(publish.isPublishable('modules/pixel-art/module.json'), 'pixel-art publishes');
|
|
161
|
+
assert.ok(publish.isPublishable('modules/pixel-art/skills/otb-tile-generate/SKILL.md'), 'its skills publish');
|
|
162
|
+
assert.ok(publish.isPublishable('modules/design-styles/module.json'), 'design-styles publishes');
|
|
163
|
+
assert.ok(publish.isPublishable('modules/design-styles/skills/impeccable/reference/audit.md'), 'its skill trees publish');
|
|
164
|
+
assert.ok(!publish.isPublishable('modules/art-pipeline/references/x.png'), 'the copyrighted reference maps never publish (ADR 0098)');
|
|
165
|
+
});
|
package/tests/skill_lint.mjs
CHANGED
|
@@ -106,14 +106,38 @@ test('the lister reaches module-owned skills, and a core copy of the same name w
|
|
|
106
106
|
});
|
|
107
107
|
|
|
108
108
|
test('every module-owned skill this repo ships parses too — the 14 that did not on 2026-09-04 (task 1003587)', () => {
|
|
109
|
-
|
|
109
|
+
// Those skills moved to the opt-in design-styles module (task 1004470, which also deleted
|
|
110
|
+
// design-taste-frontend-v1, leaving 13); an OFF module's skills are still linted.
|
|
111
|
+
const dir = path.join(ROOT, 'modules', 'design-styles', 'skills');
|
|
110
112
|
const files = lint.listSkillFiles(ROOT).filter((f) => f.startsWith(dir) || fs.existsSync(path.join(dir, path.basename(path.dirname(f)), 'SKILL.md')));
|
|
111
|
-
assert.ok(files.length >=
|
|
113
|
+
assert.ok(files.length >= 13, `the design-styles module ships 13+ skills; found ${files.length}`);
|
|
112
114
|
const report = lint.lintSkills(files);
|
|
113
115
|
const broken = report.results.filter((r) => r.errors.length).map((r) => path.relative(ROOT, r.file) + ': ' + r.errors.join('; '));
|
|
114
116
|
assert.deepEqual(broken, [], broken.join('\n'));
|
|
115
117
|
});
|
|
116
118
|
|
|
119
|
+
test('the listing counts only RESIDENT skills: an off module\'s skill is linted but not counted, an on one is both (task 1004470)', () => {
|
|
120
|
+
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'skill-lint-resident-'));
|
|
121
|
+
const skill = (n) => `---\nname: ${n}\ndescription: ${'x'.repeat(100)}\n---\nbody\n`;
|
|
122
|
+
const put = (rel, text) => { const p = path.join(root, rel); fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, text); };
|
|
123
|
+
put('.claude/skills/core-one/SKILL.md', skill('core-one'));
|
|
124
|
+
put('modules/offmod/module.json', JSON.stringify({ key: 'offmod', default: false, contributes: { skills: ['off-one'] } }));
|
|
125
|
+
put('modules/offmod/skills/off-one/SKILL.md', skill('off-one'));
|
|
126
|
+
put('modules/onmod/module.json', JSON.stringify({ key: 'onmod', default: true, contributes: { skills: ['on-one'] } }));
|
|
127
|
+
put('modules/onmod/skills/on-one/SKILL.md', skill('on-one'));
|
|
128
|
+
const files = lint.listSkillFiles(root);
|
|
129
|
+
assert.equal(files.length, 3, 'all three are linted');
|
|
130
|
+
const resident = lint.residentSkillFiles(root, { files }).map((f) => path.basename(path.dirname(f))).sort();
|
|
131
|
+
assert.deepEqual(resident, ['core-one', 'on-one'], 'the default-off module\'s skill is not resident');
|
|
132
|
+
const report = lint.lintSkills(files, { resident: lint.residentSkillFiles(root, { files }) });
|
|
133
|
+
assert.equal(report.results.length, 3);
|
|
134
|
+
assert.equal(report.listedSkills, 2);
|
|
135
|
+
assert.equal(report.listingChars, ('core-one'.length + 100) + ('on-one'.length + 100), 'the listing is the two resident entries');
|
|
136
|
+
put('config/modules.json', JSON.stringify({ modules: { offmod: true, onmod: false } }));
|
|
137
|
+
assert.deepEqual(lint.residentSkillFiles(root, { files }).map((f) => path.basename(path.dirname(f))).sort(), ['core-one', 'off-one'], 'the instance\'s config/modules.json wins over each module\'s default');
|
|
138
|
+
fs.rmSync(root, { recursive: true, force: true });
|
|
139
|
+
});
|
|
140
|
+
|
|
117
141
|
test('every committed skill parses (the ten that did not on 2026-09-03 must stay fixed)', () => {
|
|
118
142
|
const report = lint.lintSkills(lint.listSkillFiles(ROOT));
|
|
119
143
|
const broken = report.results.filter((r) => r.errors.length).map((r) => path.relative(ROOT, r.file) + ': ' + r.errors.join('; '));
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
// face, and keeps the translations that ADR fixed (the archetype that still ships both modes,
|
|
15
15
|
// the deterministic selection, the transcribed colour contract). Since task 1003475 (ADR
|
|
16
16
|
// 0231) a module skill declares one of TWO origins - a rebuild from a spec, or first party -
|
|
17
|
-
// and the origin pins branch on which; the /style session is the first original.
|
|
17
|
+
// and the origin pins branch on which; the /style session is the first original. Since
|
|
18
|
+
// task 1004470 those skills are the opt-in `design-styles` module (default: false), split
|
|
19
|
+
// out of ui-design, which keeps /design, the sync skills, the kit and the style library
|
|
20
|
+
// (modules/ui-design/styles/) the skills still name as their palette source. The file
|
|
21
|
+
// keeps its name because the preamble's own marker line points here. DB-free.
|
|
18
22
|
import assert from 'node:assert/strict';
|
|
19
23
|
import { test } from 'node:test';
|
|
20
24
|
import { createRequire } from 'node:module';
|
|
@@ -25,7 +29,8 @@ import { fileURLToPath } from 'node:url';
|
|
|
25
29
|
|
|
26
30
|
const require = createRequire(import.meta.url);
|
|
27
31
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
28
|
-
const MODULE = path.join(ROOT, 'modules', '
|
|
32
|
+
const MODULE = path.join(ROOT, 'modules', 'design-styles');
|
|
33
|
+
const UI_DESIGN = path.join(ROOT, 'modules', 'ui-design'); // keeps the style library + /design
|
|
29
34
|
const SKILLS = path.join(MODULE, 'skills');
|
|
30
35
|
const VENDOR = path.join(SKILLS, 'vendor');
|
|
31
36
|
const read = (p) => fs.readFileSync(p, 'utf8').replace(/\r\n/g, '\n');
|
|
@@ -114,9 +119,11 @@ test('every vendored SKILL.md opens with the PLATFORM PREAMBLE, byte-identical t
|
|
|
114
119
|
});
|
|
115
120
|
|
|
116
121
|
// ── every in-house rebuild: the preamble, the Rebuilt-from section, no sidecar (ADR 0220) ──
|
|
117
|
-
|
|
122
|
+
// design-taste-frontend-v1 was the fourth until task 1004470 deleted it (owner: "v1 can go").
|
|
123
|
+
const TASTE_CORE = ['design-taste-frontend', 'high-end-visual-design', 'redesign-existing-projects'];
|
|
118
124
|
|
|
119
|
-
test('the taste core of task 1003325 exists as
|
|
125
|
+
test('the taste core of task 1003325 exists as three in-house skills, every one declared, and the deleted v1 stays gone', () => {
|
|
126
|
+
assert.ok(!ownDirs.includes('design-taste-frontend-v1') && !manifest.contributes.skills.includes('design-taste-frontend-v1'), 'design-taste-frontend-v1 is neither on disk nor declared (task 1004470)');
|
|
120
127
|
for (const name of TASTE_CORE) {
|
|
121
128
|
assert.ok(ownDirs.includes(name), `${name}: skills/${name}/SKILL.md exists`);
|
|
122
129
|
assert.ok(manifest.contributes.skills.includes(name), `${name}: declared in contributes.skills`);
|
|
@@ -208,7 +215,7 @@ test('no image-family skill carries the directives the panel flagged: no eagerne
|
|
|
208
215
|
});
|
|
209
216
|
|
|
210
217
|
test('the worked example of the image family is the grove specimen: the pair with a sidecar beside every raster, referenced as tokens on the mock body', () => {
|
|
211
|
-
const grove = path.join(
|
|
218
|
+
const grove = path.join(UI_DESIGN, 'styles', 'grove');
|
|
212
219
|
for (const f of ['grove-pod.jpg', 'grove-pod-cut.webp']) {
|
|
213
220
|
assert.ok(fs.existsSync(path.join(grove, 'assets', f)), `styles/grove/assets/${f} exists`);
|
|
214
221
|
const side = path.join(grove, 'assets', `${f}.json`);
|
|
@@ -360,7 +367,7 @@ test('every in-house skill works inside the world: the style library is the pale
|
|
|
360
367
|
|
|
361
368
|
test('the preamble copies are policy, not doc-entropy debt: the scanner skips the block and still flags a plain duplicate', () => {
|
|
362
369
|
const de = require(path.join(ROOT, 'scripts', 'gds', 'docs-entropy.js'));
|
|
363
|
-
const carriers = ['modules/
|
|
370
|
+
const carriers = ['modules/design-styles/skills/PREAMBLE.md', ...ownDirs.map((n) => `modules/design-styles/skills/${n}/SKILL.md`)];
|
|
364
371
|
assert.ok(carriers.length >= 2, 'at least one own skill carries the block beside PREAMBLE.md');
|
|
365
372
|
for (const rel of carriers) assert.ok(read(path.join(ROOT, rel)).includes(policy.preamble.begin), `${rel} carries the block`);
|
|
366
373
|
const dupes = de.scanDuplicateProse(carriers);
|
|
@@ -388,41 +395,42 @@ test('every skill the manifest contributes resolves to a skill dir (core .claude
|
|
|
388
395
|
assert.ok(homes.some((h) => fs.existsSync(path.join(h, 'SKILL.md'))), `${name}: declared but no SKILL.md in any home`);
|
|
389
396
|
}
|
|
390
397
|
for (const name of [...ownDirs, ...vendoredDirs]) assert.ok(declared.includes(name), `${name}: on disk under the module but not declared in contributes.skills`);
|
|
391
|
-
const sources = materialize.moduleSkillSources({ coreRoot: ROOT, instanceDir: ROOT });
|
|
398
|
+
const sources = materialize.moduleSkillSources({ coreRoot: ROOT, instanceDir: ROOT }).filter((s) => s.module === 'design-styles');
|
|
392
399
|
assert.deepEqual(sources.map((s) => s.name).sort(), [...ownDirs, ...vendoredDirs].sort(), 'the materialiser resolves exactly the module skill dirs on disk');
|
|
393
400
|
});
|
|
394
401
|
|
|
395
402
|
// ── materialisation: the builder's .claude/skills carries the set, sidecar included ─
|
|
396
|
-
|
|
403
|
+
// The module ships default: false (task 1004470), so the shape under test is the real one:
|
|
404
|
+
// ON only when the instance's config/modules.json says so, OFF with no config at all.
|
|
405
|
+
test('a module-owned skill lands in a materialised .claude/skills with its sidecar when the instance turns the module on, and not by default', () => {
|
|
397
406
|
const core = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-skills-core-'));
|
|
398
|
-
const mod = path.join(core, 'modules', '
|
|
407
|
+
const mod = path.join(core, 'modules', 'design-styles');
|
|
399
408
|
fs.mkdirSync(path.join(mod, 'skills', 'vendor', 'sample'), { recursive: true });
|
|
400
409
|
fs.mkdirSync(path.join(core, '.claude', 'skills'), { recursive: true });
|
|
401
|
-
fs.writeFileSync(path.join(mod, 'module.json'), JSON.stringify({ key: '
|
|
410
|
+
fs.writeFileSync(path.join(mod, 'module.json'), JSON.stringify({ key: 'design-styles', default: false, contributes: { skills: ['sample'] } }));
|
|
411
|
+
const enable = (dir) => { fs.mkdirSync(path.join(dir, 'config'), { recursive: true }); fs.writeFileSync(path.join(dir, 'config', 'modules.json'), JSON.stringify({ modules: { 'design-styles': true } })); return dir; };
|
|
402
412
|
const pre = read(path.join(SKILLS, 'PREAMBLE.md'));
|
|
403
413
|
fs.writeFileSync(path.join(mod, 'skills', 'vendor', 'sample', 'SKILL.md'), `---\nname: sample\n---\n\n${pre}\nupstream body\n`);
|
|
404
414
|
fs.writeFileSync(path.join(mod, 'skills', 'vendor', 'sample', 'PROVENANCE.md'), '---\nname: sample\nlicence: MIT\nscanVerdict: clean\n---\n');
|
|
405
415
|
fs.writeFileSync(path.join(mod, 'skills', 'vendor', 'sample', 'LICENSE'), 'MIT\n');
|
|
406
416
|
|
|
407
|
-
const on = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-skills-inst-'));
|
|
417
|
+
const on = enable(fs.mkdtempSync(path.join(os.tmpdir(), 'ui-skills-inst-')));
|
|
408
418
|
const resOn = materialize.materializeClaude({ coreRoot: core, instanceDir: on, dryRun: false });
|
|
409
419
|
assert.equal(resOn.moduleSkills, 3);
|
|
410
420
|
for (const f of ['SKILL.md', 'PROVENANCE.md', 'LICENSE']) assert.ok(fs.existsSync(path.join(on, '.claude', 'skills', 'sample', f)), `materialised ${f}`);
|
|
411
421
|
assert.ok(read(path.join(on, '.claude', 'skills', 'sample', 'SKILL.md')).includes(policy.preamble.begin), 'the preamble survives materialisation');
|
|
412
422
|
|
|
413
|
-
const off = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-skills-inst-'));
|
|
414
|
-
fs.mkdirSync(path.join(off, 'config'), { recursive: true });
|
|
415
|
-
fs.writeFileSync(path.join(off, 'config', 'modules.json'), JSON.stringify({ modules: { 'ui-design': false } }));
|
|
423
|
+
const off = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-skills-inst-')); // no config/modules.json: the module's own default, off
|
|
416
424
|
const resOff = materialize.materializeClaude({ coreRoot: core, instanceDir: off, dryRun: false });
|
|
417
425
|
assert.equal(resOff.moduleSkills, 0);
|
|
418
|
-
assert.ok(!fs.existsSync(path.join(off, '.claude', 'skills', 'sample')), '
|
|
426
|
+
assert.ok(!fs.existsSync(path.join(off, '.claude', 'skills', 'sample')), 'a default-config instance never receives the skill');
|
|
419
427
|
assert.deepEqual(resOff.excludedSkills, ['sample']);
|
|
420
428
|
|
|
421
429
|
// the policy holds at COPY time too, read from this module's own policy.json: a
|
|
422
430
|
// dangerous verdict never lands, whatever the CI test would have said
|
|
423
431
|
fs.copyFileSync(path.join(SKILLS, 'policy.json'), path.join(mod, 'skills', 'policy.json'));
|
|
424
432
|
fs.writeFileSync(path.join(mod, 'skills', 'vendor', 'sample', 'PROVENANCE.md'), '---\nname: sample\nlicence: MIT\nscanVerdict: dangerous\n---\n');
|
|
425
|
-
const refusedInst = fs.mkdtempSync(path.join(os.tmpdir(), 'ui-skills-inst-'));
|
|
433
|
+
const refusedInst = enable(fs.mkdtempSync(path.join(os.tmpdir(), 'ui-skills-inst-')));
|
|
426
434
|
const resRefused = materialize.materializeClaude({ coreRoot: core, instanceDir: refusedInst, dryRun: false });
|
|
427
435
|
assert.equal(resRefused.moduleSkills, 0);
|
|
428
436
|
assert.ok(!fs.existsSync(path.join(refusedInst, '.claude', 'skills', 'sample')), 'a dangerous verdict is refused at copy time');
|
|
@@ -438,6 +446,10 @@ test('/design lists every module skill (vendored or in-house) by name, one line
|
|
|
438
446
|
assert.ok(line, `/design names ${name} on a line of its own`);
|
|
439
447
|
}
|
|
440
448
|
assert.match(design, /playwright/i, '/design names the playwright plugin');
|
|
449
|
+
// task 1004470: the styles are opt-in, so /design must say so plainly when they are absent
|
|
450
|
+
assert.match(design, /`design-styles` module/, '/design names the module the styles come from');
|
|
451
|
+
assert.match(design, /"design-styles": true/, '/design gives the one config line that turns them on');
|
|
452
|
+
assert.match(design, /If `\.claude\/skills\/` has no `impeccable`[^\n]*design-styles[^\n]*off/i, '/design says in plain words that a missing style skill means the module is off');
|
|
441
453
|
});
|
|
442
454
|
|
|
443
455
|
test('the skills README and the module CLAUDE.md roster every module skill by name', () => {
|
|
@@ -445,7 +457,7 @@ test('the skills README and the module CLAUDE.md roster every module skill by na
|
|
|
445
457
|
const claude = read(path.join(MODULE, 'CLAUDE.md'));
|
|
446
458
|
for (const name of [...vendoredDirs, ...ownDirs]) {
|
|
447
459
|
assert.ok(readme.includes(`\`${name}\``), `README rosters ${name}`);
|
|
448
|
-
assert.ok(claude.includes(`\`${name}\``), `modules/
|
|
460
|
+
assert.ok(claude.includes(`\`${name}\``), `modules/design-styles/CLAUDE.md names ${name}`);
|
|
449
461
|
}
|
|
450
462
|
assert.ok(readme.includes('policy.json') && readme.includes('PREAMBLE.md') && readme.includes('PROVENANCE.md'));
|
|
451
463
|
assert.ok(readme.includes('Rebuilt from'), 'the README says what a rebuild carries instead of a sidecar');
|
|
@@ -1,135 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
name: design-taste-frontend-v1
|
|
3
|
-
description: >-
|
|
4
|
-
The earlier generation of the taste skill, kept under its own name so a task or builder that asks for it
|
|
5
|
-
gets exactly this behaviour while the default (design-taste-frontend) evolves separately. A fixed triple of
|
|
6
|
-
dials, the layout bans, full interaction cycles, and a named vocabulary of premium patterns. Triggers on
|
|
7
|
-
"design-taste-frontend-v1", "the fixed dials", "the pattern vocabulary".
|
|
8
|
-
plain: >-
|
|
9
|
-
The earlier version of the landing-page style guide, kept so work that asked for it gets exactly the same results.
|
|
10
|
-
reach-for: >-
|
|
11
|
-
Only when a task asks for this older version by name.
|
|
12
|
-
cost: >-
|
|
13
|
-
Uses your session. It changes the page, and you check it on a preview.
|
|
14
|
-
---
|
|
15
|
-
|
|
16
|
-
<!-- BEGIN PLATFORM PREAMBLE · ui-design module · one block, byte-identical in every skill the module ships (vendored or rebuilt in-house) so an upstream refresh is a three-way merge; the source is modules/ui-design/skills/PREAMBLE.md and tests/ui_design_skills.mjs fails on drift -->
|
|
17
|
-
> **Platform preamble — read before any rule below.** This skill ships with the `ui-design` module (vendored from a scanned origin, or rebuilt in-house from a spec) and runs INSIDE an instance's world, which it reads first and never overrides.
|
|
18
|
-
>
|
|
19
|
-
> 1. **Load the world before any taste rule.** `config/branding.json` over `config/branding.neutral.json` (`theme.ui` is the fifteen tokens: <redacted> colours and two font stacks), `config/design-tokens.json` over `config/design-tokens.neutral.json`, and `DESIGN.md` at the repo root: the instance's palette and derived tiers, faces, materials, motion grammar, components, and its Do's and Don'ts. Where an instance has no `DESIGN.md`, the neutral pack is the world and the platform floors in item 6 are the whole rulebook; say so and build to them. Never invent a palette.
|
|
20
|
-
> 2. **The fifteen-token contract is the only colour source.** Every colour is a `var()` or a `color-mix()` of the fifteen; the only legal literal is pure black or white with alpha as a scrim, mask or halo. No page `:root` (page tokens live on `body`), and every dark rule is written twice: `:root[data-mode="dark"]` and its `prefers-color-scheme` twin.
|
|
21
|
-
> 3. **A style skill produces variants INSIDE that world, never a second world.** The look this skill carries (its palette, faces, materials, mood boards) is a reference for composition and craft; the instance's tokens and faces replace it. Changing the world is the owner's decision and an ADR, not a session's taste.
|
|
22
|
-
> 4. **Images are for hero plates and hero objects only.** UI is built from templates and the instance's world, not painted; any image-generation step below applies to text-free plates and hero objects, never to controls, cards, text, or a screenshot of a screen.
|
|
23
|
-
> 5. **The instance's taste bans apply and win.** Read them from its `DESIGN.md`. Cloud Bongos's own (no em dash or en dash in visible copy, no emoji, no gradient text, no three-equal-card row, no fake screenshot built from divs) are that instance's, not the module's; where this skill's rules conflict with an instance's bans, the instance wins.
|
|
24
|
-
> 6. **The platform floors hold on every instance** and a `DESIGN.md` may tighten them, never loosen them: every control clears 24px on both sides (primary pills 46–48px); AA in both modes (4.5:1 body text, 3:1 large text and non-text) measured from the real stylesheet; and the Kill Switch, `@media (prefers-reduced-motion: reduce)` at the foot of the token layer killing every animation and transition, no motion authored in JavaScript, and a rest frame that is a complete composition on its own.
|
|
25
|
-
> 7. **Look before you ship.** Render the real surface through the module's kit before and after (`/design` Step 2 and `docs/recipes/ui-look-before-you-ship.md`: every state at 1440 / 390 / 320 in dark and light, audited against the floors); a page with no states file gets one first, and new interactive code ships with a pin in the page's own test file.
|
|
26
|
-
>
|
|
27
|
-
> The rules below are the skill's own. A vendored copy carries a `PROVENANCE.md` beside this file naming the origin, the pinned commit, the licence and the scan verdict it was vendored under; an in-house rebuild names its rebuild spec and its ADR in the section right after this block.
|
|
28
|
-
<!-- END PLATFORM PREAMBLE -->
|
|
29
|
-
|
|
30
|
-
## Rebuilt from
|
|
31
|
-
|
|
32
|
-
This skill is an **in-house rebuild**, not a vendored copy. Its documented functionality comes from `Leonxlnx/taste-skill` @ `<redacted>` (MIT; the `taste-skill-v1` entry), which `/scan-before-install` reduced to `dangerous` at the default tier on 2026-08-28, so under [ADR 0198](../../../../docs/adr/0198-third-party-skill-vendoring-policy.md) it could land only as a REBUILD-SPEC. The spec is the body of [task 1003325](https://cloudbongos.com/builders#/task/1003325); this file implements its requirements from scratch, **no upstream bytes consulted or copied**; the landing shape is [ADR 0220](../../../../docs/adr/0220-an-in-house-rebuilt-skill-is-a-first-party-skill.md).
|
|
33
|
-
|
|
34
|
-
What changed in translation, on purpose: the upstream generation was written for a component framework with a motion library; a platform surface here is static markup, one sheet and the instance's transforms, so the stack conventions are restated for that and **every motion rule lives inside the Kill Switch** (the spring, the magnetic hover and the shared-element transition, which need script, are named as not available rather than quietly kept). The typography and colour rules are read **against the instance's pack**: the pack's faces are the faces and the pack's accent is the accent, whatever this skill would have preferred. Imagery follows the owner's rule (hero plates and hero objects only).
|
|
35
|
-
|
|
36
|
-
## Why v1 is kept
|
|
37
|
-
|
|
38
|
-
The default skill (`design-taste-frontend`) reads the brief, states a design read and sets its dials from presets. This generation does something narrower and more predictable: it starts from a **fixed baseline** and adapts it in conversation, and it carries a **vocabulary** the default folded into its steps. A task that says "v1" means this file, and this file does not change when the default does. If both are in front of you and the task names neither, use the default.
|
|
39
|
-
|
|
40
|
-
## The baseline triple
|
|
41
|
-
|
|
42
|
-
Variance **6**, motion **4**, density **5**. Fixed. Adapt them **conversationally**: when the person you are working with says "calmer", motion drops one; "busier" raises density one; "more editorial" raises variance one. Say the new triple each time it moves. Do not ask anyone to edit this file to change a dial, and do not edit it yourself for a session: the triple is the skill's baseline, the conversation is the override.
|
|
43
|
-
|
|
44
|
-
The motion value is clamped by the world before the dial applies (the chrome world: ambient loops at 20s or slower, one authored arrival, everything dead under reduced motion), and motion 0 is what every page becomes under `prefers-reduced-motion`, so the rest frame is designed first.
|
|
45
|
-
|
|
46
|
-
## Stack conventions for a served surface
|
|
47
|
-
|
|
48
|
-
- **Check the surface's stack before importing anything**: the instance's `package.json`, the directory's nested `CLAUDE.md`, the sheet the page already loads. A platform surface is static HTML served through the instance's transforms, one sheet, and script only for live data (`createElement` / `textContent`, never `innerHTML`). This skill introduces no framework and no library.
|
|
49
|
-
- **If a package is missing, print the install command and stop.** Adding a dependency is a task decision, not a taste decision; the ship notes carry the command, the claim's owner takes it.
|
|
50
|
-
- **Static markup by default; script isolated to leaf behaviour** (a disclosure, a live refresh) and never to motion, because script motion escapes the Kill Switch.
|
|
51
|
-
- **Utility CSS is not assumed.** If a surface already uses one, read its version from the manifest before writing a class; if it does not, write the world's tokens straight.
|
|
52
|
-
- **Breakpoints are the world's** (one, at 900px in the chrome world) and the kit's three widths (1440 / 390 / 320) are the proof; the container is `spacing.container`; a full-height section is the world's floored stage or `100dvh`, never `100vh`.
|
|
53
|
-
|
|
54
|
-
## Typography rules
|
|
55
|
-
|
|
56
|
-
- **Display type is large and tight-tracked**: the pack's display face at the world's display weight and tracking (the chrome world: Manrope 300, `-0.042em`, lowercase). A display line is drawing, not a slogan.
|
|
57
|
-
- **Body measure is capped near 65 characters** (`max-width: 62ch` on prose; the chrome world's bands cap at 56 to 62ch).
|
|
58
|
-
- **The pack's body face is the UI face.** A default UI sans is discouraged for premium work only where the pack does not name it; where the pack does, the pack wins.
|
|
59
|
-
- **Serif is not a UI face** on a working surface (a ledger, a settings page, a form). A look from `modules/ui-design/styles/` that carries a serif carries it for display, and the body stack decides the rest.
|
|
60
|
-
- Every real number is `font-variant-numeric: tabular-nums`; a figure that refreshes must not reflow the composition.
|
|
61
|
-
|
|
62
|
-
## Colour rules
|
|
63
|
-
|
|
64
|
-
- **One accent, the pack's,** spent only on the pressable and the live (the chrome world's One Warm Thing Rule). Under 80% saturation is the pack author's business, not this page's: the accent is not adjusted per page.
|
|
65
|
-
- **No purple-to-blue glow aesthetic**, no neon-on-near-black HUD, no gradient text. These are bans whichever pack is on.
|
|
66
|
-
- **One palette per project** means the pack, and the pack's neutral ramp is the only ramp: no warm grey on one band and cool grey on the next, because there is only `bg`, `bg-card`, `bg-deep`, `bg-frame` and the inks.
|
|
67
|
-
- Every colour is a `var()` or a `color-mix()` of the fifteen; the dark twin is written twice; a literal is pure black or white with alpha as a scrim, mask or halo, and nothing else.
|
|
68
|
-
|
|
69
|
-
## Emoji and icons
|
|
70
|
-
|
|
71
|
-
- **No emoji** in code, markup, copy or alt text. Not as a bullet, not as a status glyph, not in a button label. Cloud Bongos's `DESIGN.md` bans it by name; the default holds on every instance.
|
|
72
|
-
- **Icons come from one small allowed set of families, at one standardised stroke width.** The allowed set on a platform surface is: the world's inline mark (the chrome world's bongo-pair SVG), and one hairline glyph family drawn at a single stroke (`1.5px`, `stroke-linecap: round`) for the few glyphs a page needs (a chevron, an arrow, a magnifier, a close mark). Never two families on one page, never a thick default icon set, never a hand-drawn path introduced for one page. Where a world names its own family, that family is the set.
|
|
73
|
-
- An icon is never the only carrier of meaning: the label is the text, the glyph is the pointer.
|
|
74
|
-
|
|
75
|
-
## Layout bans
|
|
76
|
-
|
|
77
|
-
- **No centred hero above variance 5.** At the baseline the hero is asymmetric: the type on one side, the object (a plate, by token) or air on the other.
|
|
78
|
-
- **No three equal feature cards.** Three unequal objects at three sizes read as a constellation; three equal boxes read as a rank, and the chrome world bans the row by name.
|
|
79
|
-
- **No boxed card containers above density 6.** Content is separated by hairlines (`rule-soft`) and negative space; a card is for a surface that carries its own elevation vocabulary, and the chrome world has none.
|
|
80
|
-
|
|
81
|
-
## Full interaction cycles, not the successful state only
|
|
82
|
-
|
|
83
|
-
Every interactive component ships all of its states, and the page's states file (`<page>.states.json`) names them so the kit renders each:
|
|
84
|
-
|
|
85
|
-
- **Loading**: a skeleton shaped like the layout it replaces (the same columns, the same row heights, the ground tone `bg-deep`), never a spinner.
|
|
86
|
-
- **Empty**: a composed empty state that says what will be here and offers the one next action; a feed with nothing collapses its rows, its meter and its sub-line rather than promising six rows that never come.
|
|
87
|
-
- **Error**: inline, beside the thing that failed, in the world's `err` state colour on text, never a modal for a field.
|
|
88
|
-
- **Press**: tactile feedback on `:active` (`transform: scale(.98)` on the state curve); **focus** with the world's two-tone ring; **hover** on anything pressable.
|
|
89
|
-
- Every control clears the 24px floor in both axes, footer links included; primary pills sit at 46 to 48px.
|
|
90
|
-
|
|
91
|
-
## Motion rules
|
|
92
|
-
|
|
93
|
-
- **The world's curves are the easing.** Where a world names an entrance curve (the chrome world's exponential ease-out, `cubic-bezier(.16, 1, .3, 1)`) and a state curve, use them; where it names none, a curve with a little overshoot for entrances and a plain ease-out for states. No spring library: a spring is script, and script motion is banned.
|
|
94
|
-
- **Staggered list reveals** are CSS: `animation-delay` from an `--i` custom property set in the markup (`style="--i: 3"`), never a script loop.
|
|
95
|
-
- **Shared-element layout transitions and magnetic hover are not available on this platform.** Both need script driving continuous values; the first is replaced by an instant state change under a 200ms crossfade, the second by the world's hover lift (a 6 to 8px `translateY` over 400ms).
|
|
96
|
-
- Everything is dead under the Kill Switch and the rest frame is the composition.
|
|
97
|
-
|
|
98
|
-
## Performance guardrails
|
|
99
|
-
|
|
100
|
-
- Animate **`transform` and `opacity` only** (and `filter` on a plate where the world does). Never a layout property.
|
|
101
|
-
- **Grain and noise overlays** exist only where the world has that material (the expedition look's halftone screen is a look's material, not a default); when they do, they sit on a fixed, `pointer-events: none` pseudo-element.
|
|
102
|
-
- **No arbitrary `z-index`.** A named scale on `body` (`--z-bar`, `--z-band`, `--z-dialog`), reserved for systemic layers.
|
|
103
|
-
- **Full-height sections use dynamic viewport height** (`100dvh`, or the world's floored stage `max(100dvh, 900px)`), never `100vh`.
|
|
104
|
-
- `backdrop-filter` only on a fixed or sticky layer, and only where the world's glass has a blur (light glass over paper is a tint, blur 0).
|
|
105
|
-
|
|
106
|
-
## The vocabulary of premium patterns
|
|
107
|
-
|
|
108
|
-
Named so a brief can ask for one and a review can name what is missing. Each is a pattern inside the world, not a component to import:
|
|
109
|
-
|
|
110
|
-
- **The two-line display**: the h1 as two lines, the second indented, lowercase where the world's display voice is.
|
|
111
|
-
- **The tabular figure**: a real number from the ledger at display weight, tabular, with a caption.
|
|
112
|
-
- **The hairline ledger**: rows built from 1px top rules, baseline-aligned columns, no borders, no boxes.
|
|
113
|
-
- **The door pill**: two equal actions joined in one glass bar, the primary carrying the accent with the world's `accent-on` ink.
|
|
114
|
-
- **The lit rail step**: one step of a pipeline lit in the accent (hairline, dot, name), the rest in ink; exactly one is live.
|
|
115
|
-
- **The leader-line callout**: a 1px diagonal line, a figure, a caption; real numbers only, at most two per surface.
|
|
116
|
-
- **The feathered patch**: a soft patch of the ground behind glyphs that sit on art, so a ground never cuts a hard-edged box out of a plate.
|
|
117
|
-
- **The contact shadow**: the one shadow in a world, and it belongs to the object, not to a component.
|
|
118
|
-
- **The hairline band menu**: a disclosure that unfolds as a full-width band under the bar, over the page, closed by one hairline.
|
|
119
|
-
- **The poster card**: a modal on the card ground closed by one hairline, its regions flat children in the brief's order, every region hiding on null.
|
|
120
|
-
- **Pill or nothing**: every control a full pill (999px), and the one exception the world names.
|
|
121
|
-
|
|
122
|
-
## The tile-grid archetypes
|
|
123
|
-
|
|
124
|
-
Motion-first tile grids, each with its collapse under the world's breakpoint:
|
|
125
|
-
|
|
126
|
-
- **The constellation**: three different objects at deliberately unequal sizes and baseline drops; hover lifts one. Collapses to a vertical stack at unequal sizes, never to three equal circles.
|
|
127
|
-
- **The dense auto-flow**: `grid-auto-flow: dense` with interlocking spans and **zero empty cells**, three to five intentional tiles over eight cluttered ones. Collapses to one column; spans reset.
|
|
128
|
-
- **The hairline ledger grid**: fixed columns (`88px 1fr 152px 96px` in the chrome world), 1px top rules, tabular ids. Collapses to one column per row, the id and the title first.
|
|
129
|
-
- **The five-column rail**: five beats on one rail, one lit; collapses to one column with the hairlines kept.
|
|
130
|
-
|
|
131
|
-
## Pre-flight (short)
|
|
132
|
-
|
|
133
|
-
- [ ] **Mobile collapse** looked at in the 390 and 320 shots; nothing scrolls sideways; every archetype collapsed the way its entry says.
|
|
134
|
-
- [ ] **Effect cleanup**: nothing to clean, because nothing moves in script; any live-refresh timer is cleared on navigation.
|
|
135
|
-
- [ ] **State coverage**: every state in the page's states file rendered by `node modules/ui-design/kit/render.js --page <path>` and ALL CLEAN; the interaction contract PASS under `probe.js`.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|