@bongos/core 1.19.393 → 1.19.395
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 +108 -58
- package/.claude/skills/design/SKILL.md +68 -0
- package/.claude/skills/design-sync/SKILL.md +2 -2
- package/.claude/skills/figma-design-sync/SKILL.md +3 -3
- package/config/design-tokens.neutral.json +1 -1
- package/config/design-tokens.schema.json +1 -1
- package/docs/adr/0197-the-ui-design-module.md +52 -0
- package/docs/adr/README.md +1 -0
- package/docs/architecture.md +1 -1
- package/docs/copy-inventory.md +249 -236
- package/docs/copy-registry.json +543 -324
- package/docs/file-map.md +2 -0
- package/docs/modules-contract.md +1 -0
- package/docs/onboarding/slash-commands.md +1 -1
- package/docs/recipes/claude-design-loop.md +3 -3
- package/docs/recipes/figma-design-loop.md +4 -4
- package/modules/ui-design/CLAUDE.md +15 -0
- package/{src/ui → modules/ui-design}/adapters/claude-design/index.js +1 -1
- package/{src/ui → modules/ui-design}/adapters/figma/index.js +3 -3
- package/{docs → modules/ui-design/docs}/design-contract.md +12 -12
- package/modules/ui-design/kit/README.md +3 -0
- package/modules/ui-design/module.json +11 -0
- package/modules/ui-design/scripts/design-sync.js +65 -0
- package/modules/ui-design/scripts/figma-design-sync.js +64 -0
- package/modules/ui-design/scripts/validate-design.js +258 -0
- package/modules/ui-design/skills/README.md +3 -0
- package/modules/ui-design/styles/README.md +3 -0
- package/package-lock.json +2 -2
- package/package.json +1 -1
- package/public-landing/assets/cosmos.css +235 -103
- package/public-landing/index.html +228 -75
- package/public-landing/projects.html +256 -140
- package/scripts/gds/copy-inventory.js +7 -5
- package/scripts/gds/design-sync.js +9 -59
- package/scripts/gds/doc-cli-guard.js +11 -5
- package/scripts/gds/figma-design-sync.js +9 -59
- package/scripts/gds/fitness-checks-identity.js +5 -3
- package/scripts/gds/publish-manifest.js +1 -1
- package/scripts/gds/validate-design.js +7 -249
- package/src/bongos/module-scope-map.js +18 -0
- package/src/module-api.js +5 -1
- package/tests/design_tokens_sync.mjs +1 -1
- package/tests/doc_cli_guard.mjs +19 -0
- package/tests/figma_adapter.mjs +1 -1
- package/tests/landing_page.mjs +72 -2
- package/tests/module_loader.mjs +4 -1
- package/tests/projects_hub.mjs +52 -73
- package/tests/projects_hub_pre_uat.mjs +10 -8
|
@@ -1,62 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
'use strict';
|
|
3
|
+
// design-sync.js — thin shim: the Claude Design sync CLI lives in the ui-design
|
|
4
|
+
// module at modules/ui-design/scripts/design-sync.js (task 1003324, ADR 0197).
|
|
3
5
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
+
// WHY a shim rather than re-pointing every caller: `node scripts/gds/<x>.js` is
|
|
7
|
+
// the form the skills and recipes use, and the form claude-materialize rewrites to
|
|
8
|
+
// `bongos exec scripts/gds/<x>.js` when the core installs as a package — so this
|
|
9
|
+
// path stays the stable, documented entry point. Flags pass straight through:
|
|
10
|
+
// node scripts/gds/design-sync.js — build the token-preview bundle, print the manifest
|
|
6
11
|
// node scripts/gds/design-sync.js --surface <id> — limit to one surface
|
|
7
|
-
|
|
8
|
-
// Output (stdout, JSON):
|
|
9
|
-
// { bundlePath, files, surfaceIds, summary }
|
|
10
|
-
//
|
|
11
|
-
// The /design-sync skill calls this, then uses the DesignSync MCP tool
|
|
12
|
-
// to push the bundle to a Claude Design project.
|
|
13
|
-
|
|
14
|
-
const path = require('node:path');
|
|
15
|
-
const fs = require('node:fs');
|
|
16
|
-
const { resolveInstanceRoot } = require(path.resolve(__dirname, '..', '..', 'src', 'instance-config'));
|
|
17
|
-
|
|
18
|
-
// ADR 0108 §1: the design system (src/ui/, config/design-tokens.json) is host
|
|
19
|
-
// content. resolveInstanceRoot() returns the repo root today, so this is
|
|
20
|
-
// byte-identical for the live instance.
|
|
21
|
-
const REPO_ROOT = resolveInstanceRoot();
|
|
22
|
-
|
|
23
|
-
function arg(flag, args) {
|
|
24
|
-
const i = args.indexOf(flag);
|
|
25
|
-
return i !== -1 ? args[i + 1] : null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
(async () => {
|
|
29
|
-
const args = process.argv.slice(2);
|
|
30
|
-
const targetSurface = arg('--surface', args) || undefined;
|
|
31
|
-
|
|
32
|
-
const adapter = require(path.join(REPO_ROOT, 'src', 'ui', 'adapters', 'claude-design', 'index.js'));
|
|
33
|
-
|
|
34
|
-
const result = await adapter.import({
|
|
35
|
-
repoRoot: REPO_ROOT,
|
|
36
|
-
...(targetSurface ? { surfaces: [{ id: targetSurface, path: path.join(REPO_ROOT, 'src', 'ui', targetSurface) }] } : {}),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
// Build the file list from the bundle dir for the skill to pass to finalize_plan
|
|
40
|
-
const { bundlePath, surfaceIds, summary } = result;
|
|
41
|
-
const files = collectFiles(bundlePath, bundlePath);
|
|
42
|
-
|
|
43
|
-
const manifest = { bundlePath, files, surfaceIds, summary };
|
|
44
|
-
process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
|
|
45
|
-
})().catch(err => {
|
|
46
|
-
process.stderr.write(`design-sync: ${err.message}\n`);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
function collectFiles(dir, root) {
|
|
51
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
52
|
-
const out = [];
|
|
53
|
-
for (const e of entries) {
|
|
54
|
-
const full = path.join(dir, e.name);
|
|
55
|
-
if (e.isDirectory()) {
|
|
56
|
-
out.push(...collectFiles(full, root));
|
|
57
|
-
} else {
|
|
58
|
-
out.push(path.relative(root, full));
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return out;
|
|
62
|
-
}
|
|
12
|
+
require('../../modules/ui-design/scripts/design-sync.js');
|
|
@@ -196,20 +196,26 @@ const NAME = 'a doc that names a CLI matches the real CLI (G01)';
|
|
|
196
196
|
// a false alarm that pushes the next agent toward silencing the rule via
|
|
197
197
|
// DOC_CLI_ALLOW, which is exactly the outcome the rule exists to prevent.
|
|
198
198
|
//
|
|
199
|
-
// One level of
|
|
200
|
-
//
|
|
201
|
-
//
|
|
199
|
+
// One level of relative requires is enough and keeps the read bounded — either a
|
|
200
|
+
// same-directory sibling (where a carve puts things), or the module-hosted CLI a
|
|
201
|
+
// scripts/gds/ SHIM forwards to: task 1003324 moved the design CLIs into
|
|
202
|
+
// modules/ui-design/scripts/ and left `require('../../modules/ui-design/…')`
|
|
203
|
+
// shims behind, so the flag a doc names is parsed in the module file. Nothing
|
|
204
|
+
// else is followed — a require into src/ is not a CLI phase. A missing target is
|
|
205
|
+
// skipped: the point is to find the flag, not to prove the require graph.
|
|
206
|
+
const MODULE_CLI_RE = /^modules[\\/][^\\/]+[\\/]/;
|
|
202
207
|
function readCliSource(rel) {
|
|
203
208
|
const src = readFileOrNull(rel);
|
|
204
209
|
if (src === null) return null;
|
|
205
210
|
const dir = path.dirname(rel);
|
|
206
211
|
const seen = new Set([rel]);
|
|
207
212
|
const parts = [src];
|
|
208
|
-
const RE = /require\(\s*['"](
|
|
213
|
+
const RE = /require\(\s*['"](\.\.?\/[^'"]+)['"]\s*\)/g;
|
|
209
214
|
let m;
|
|
210
215
|
while ((m = RE.exec(src))) {
|
|
211
216
|
let sib = path.join(dir, m[1]);
|
|
212
217
|
if (!sib.endsWith('.js')) sib += '.js';
|
|
218
|
+
if (path.dirname(sib) !== path.normalize(dir) && !MODULE_CLI_RE.test(sib)) continue; // normalize: join() flips separators on Windows
|
|
213
219
|
if (seen.has(sib)) continue;
|
|
214
220
|
seen.add(sib);
|
|
215
221
|
const sibSrc = readFileOrNull(sib);
|
|
@@ -255,6 +261,6 @@ function main() {
|
|
|
255
261
|
if (require.main === module) main();
|
|
256
262
|
|
|
257
263
|
module.exports = {
|
|
258
|
-
checkDocNamesRealCli, scanDocs, trackedMarkdown, scriptMentionsFlag, stripLineComments,
|
|
264
|
+
checkDocNamesRealCli, scanDocs, trackedMarkdown, scriptMentionsFlag, stripLineComments, readCliSource,
|
|
259
265
|
DOC_CLI_ALLOW, DOC_CLI_RE, DOC_FLAG_RE, UNIVERSAL_FLAGS,
|
|
260
266
|
};
|
|
@@ -1,61 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
'use strict';
|
|
3
|
+
// figma-design-sync.js — thin shim: the Figma push-plan CLI lives in the ui-design
|
|
4
|
+
// module at modules/ui-design/scripts/figma-design-sync.js (task 1003324, ADR 0197).
|
|
3
5
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
//
|
|
11
|
-
// The figma-design-sync skill calls this, then executes the plan's frames
|
|
12
|
-
// against the live Figma MCP tool.
|
|
13
|
-
|
|
14
|
-
const path = require('node:path');
|
|
15
|
-
const fs = require('node:fs');
|
|
16
|
-
const { resolveInstanceRoot } = require(path.resolve(__dirname, '..', '..', 'src', 'instance-config'));
|
|
17
|
-
|
|
18
|
-
// ADR 0108 §1: the design system (src/ui/, config/design-tokens.json) is host
|
|
19
|
-
// content. resolveInstanceRoot() returns the repo root today, so this is
|
|
20
|
-
// byte-identical for the live instance.
|
|
21
|
-
const REPO_ROOT = resolveInstanceRoot();
|
|
22
|
-
|
|
23
|
-
function arg(flag, args) {
|
|
24
|
-
const i = args.indexOf(flag);
|
|
25
|
-
return i !== -1 ? args[i + 1] : null;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
(async () => {
|
|
29
|
-
const args = process.argv.slice(2);
|
|
30
|
-
const targetSurface = arg('--surface', args) || undefined;
|
|
31
|
-
|
|
32
|
-
const adapter = require(path.join(REPO_ROOT, 'src', 'ui', 'adapters', 'figma', 'index.js'));
|
|
33
|
-
|
|
34
|
-
const result = await adapter.import({
|
|
35
|
-
repoRoot: REPO_ROOT,
|
|
36
|
-
...(targetSurface ? { surfaces: [{ id: targetSurface, path: path.join(REPO_ROOT, 'src', 'ui', targetSurface) }] } : {}),
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
const { planPath, surfaceIds, summary } = result;
|
|
40
|
-
const files = collectFiles(planPath, planPath);
|
|
41
|
-
|
|
42
|
-
const manifest = { planPath, files, surfaceIds, summary };
|
|
43
|
-
process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
|
|
44
|
-
})().catch(err => {
|
|
45
|
-
process.stderr.write(`figma-design-sync: ${err.message}\n`);
|
|
46
|
-
process.exit(1);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
function collectFiles(dir, root) {
|
|
50
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
51
|
-
const out = [];
|
|
52
|
-
for (const e of entries) {
|
|
53
|
-
const full = path.join(dir, e.name);
|
|
54
|
-
if (e.isDirectory()) {
|
|
55
|
-
out.push(...collectFiles(full, root));
|
|
56
|
-
} else {
|
|
57
|
-
out.push(path.relative(root, full));
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return out;
|
|
61
|
-
}
|
|
6
|
+
// Kept because `node scripts/gds/<x>.js` is the form the skills and recipes use and
|
|
7
|
+
// the form claude-materialize rewrites to `bongos exec` for a packaged core. Flags
|
|
8
|
+
// pass straight through:
|
|
9
|
+
// node scripts/gds/figma-design-sync.js — build the Figma push plan, print the manifest
|
|
10
|
+
// node scripts/gds/figma-design-sync.js --surface <id> — limit to one surface
|
|
11
|
+
require('../../modules/ui-design/scripts/figma-design-sync.js');
|
|
@@ -274,8 +274,10 @@ function checkNeutralConfigsNoHostIdentity() {
|
|
|
274
274
|
|
|
275
275
|
function checkDesignContract() {
|
|
276
276
|
const { spawnSync } = require('node:child_process');
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
// The validator + adapters live in the ui-design module (task 1003324, ADR 0197);
|
|
278
|
+
// scripts/gds/validate-design.js is a shim over the module file spawned here.
|
|
279
|
+
const validatorPath = path.join(REPO_ROOT, 'modules', 'ui-design', 'scripts', 'validate-design.js');
|
|
280
|
+
const adaptersDir = path.join(REPO_ROOT, 'modules', 'ui-design', 'adapters');
|
|
279
281
|
const hasAdapters = (() => {
|
|
280
282
|
try { return fs.readdirSync(adaptersDir).some((e) => fs.statSync(path.join(adaptersDir, e)).isDirectory()); }
|
|
281
283
|
catch { return false; }
|
|
@@ -290,7 +292,7 @@ function checkDesignContract() {
|
|
|
290
292
|
}
|
|
291
293
|
|
|
292
294
|
return {
|
|
293
|
-
name: 'design contract valid (docs/design-contract.md / ADR 0081)',
|
|
295
|
+
name: 'design contract valid (modules/ui-design/docs/design-contract.md / ADR 0081)',
|
|
294
296
|
ok: report.ok,
|
|
295
297
|
hardFail: !report.ok && hasAdapters,
|
|
296
298
|
violations: report.errors || [],
|
|
@@ -63,6 +63,7 @@ const PUBLISH_ALLOWLIST = [
|
|
|
63
63
|
'modules/security/',
|
|
64
64
|
'modules/government/', // the same machinery under its post-ADR-0174 name (goal 1000068, task 1003003). Added in lockstep with the protected-surfaces.json entry: publish_manifest asserts every PROTECTED_GLOB is either topology-excluded or publishable, so registering the glob as protected WITHOUT this line default-denies the module from the published core — the open-source methodology would silently lose its permission machinery at the next release.
|
|
65
65
|
'modules/copy-desk/', // the artist's loop over user-facing TEXT (goal 1000074, ADR 0178). default:true, so it MUST be here — and note the ASYMMETRY that makes omitting it worse than useless: its hall surface ships regardless, because modules/hall-ui/ is allowlisted as one subtree. A released core carrying the page and the nav entry but not the routes behind them puts a tab in every instance's hall whose every call 404s. The module and its page are only ever added or removed together; tests/publish_manifest.mjs holds that pair.
|
|
66
|
+
'modules/ui-design/', // the design contract + its adapters/CLIs + the /design playbook (task 1003324, ADR 0197). default:true, so it MUST be here: a released core without it would materialize skills (design, design-sync, figma-design-sync) whose CLIs and adapters do not exist.
|
|
66
67
|
'modules/hall-ui/',
|
|
67
68
|
'modules/status-ui/',
|
|
68
69
|
'modules/dev-box/',
|
|
@@ -105,7 +106,6 @@ const PUBLISH_ALLOWLIST = [
|
|
|
105
106
|
'docs/modules-contract.md',
|
|
106
107
|
'docs/branding-contract.md',
|
|
107
108
|
'docs/branding-fill-prompt.md',
|
|
108
|
-
'docs/design-contract.md',
|
|
109
109
|
// RUNTIME DATA that happens to live under docs/, not documentation (ADR 0178).
|
|
110
110
|
// modules/copy-desk reads the registry on EVERY queue load, and nothing on a
|
|
111
111
|
// deployed box regenerates it — copy-inventory.js is a commit-it generator run
|
|
@@ -1,254 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
|
-
// validate-design.js —
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
3
|
+
// validate-design.js — thin shim: the design-contract validator lives in the
|
|
4
|
+
// ui-design module at modules/ui-design/scripts/validate-design.js (task 1003324,
|
|
5
|
+
// ADR 0197). fitness.js (checkDesignContract in fitness-checks-identity.js) spawns
|
|
6
|
+
// the module file directly; this path is the documented CLI entry point, kept
|
|
7
|
+
// because it is the form claude-materialize rewrites to `bongos exec` for a
|
|
8
|
+
// packaged core. Flags pass straight through:
|
|
7
9
|
// node scripts/gds/validate-design.js # exits 0 on pass, 1 on failure
|
|
8
10
|
// node scripts/gds/validate-design.js --json # machine-readable { ok, errors, warnings }
|
|
9
11
|
// node scripts/gds/validate-design.js --strict # treat warnings as errors
|
|
10
|
-
|
|
11
|
-
const fs = require('node:fs');
|
|
12
|
-
const path = require('node:path');
|
|
13
|
-
|
|
14
|
-
const REPO_ROOT = path.resolve(__dirname, '..', '..');
|
|
15
|
-
const ARGS = new Set(process.argv.slice(2));
|
|
16
|
-
const JSON_MODE = ARGS.has('--json');
|
|
17
|
-
const STRICT = ARGS.has('--strict');
|
|
18
|
-
|
|
19
|
-
const NEUTRAL_TOKENS = path.join(REPO_ROOT, 'config', 'design-tokens.neutral.json');
|
|
20
|
-
const INSTANCE_TOKENS = path.join(REPO_ROOT, 'config', 'design-tokens.json');
|
|
21
|
-
const TOKENS_SCHEMA = path.join(REPO_ROOT, 'config', 'design-tokens.schema.json');
|
|
22
|
-
const DESIGN_SOURCES = path.join(REPO_ROOT, 'config', 'design-sources.json');
|
|
23
|
-
const UI_SRC = path.join(REPO_ROOT, 'src', 'ui');
|
|
24
|
-
const UI_ASSETS = path.join(REPO_ROOT, 'assets', 'ui');
|
|
25
|
-
const ADAPTERS_DIR = path.join(REPO_ROOT, 'src', 'ui', 'adapters');
|
|
26
|
-
|
|
27
|
-
const TOOL_ENUM = new Set(['claude-design', 'figma', 'hand', 'asset']);
|
|
28
|
-
const REQUIRED_COLORS = ['bg', 'bgCard', 'ink', 'accent'];
|
|
29
|
-
const REQUIRED_FONTS = ['display', 'body'];
|
|
30
|
-
|
|
31
|
-
const errors = [];
|
|
32
|
-
const warnings = [];
|
|
33
|
-
|
|
34
|
-
function err(msg) { errors.push(msg); }
|
|
35
|
-
function warn(msg) { warnings.push(msg); }
|
|
36
|
-
|
|
37
|
-
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
38
|
-
|
|
39
|
-
function readJson(filePath, label) {
|
|
40
|
-
try {
|
|
41
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
42
|
-
} catch (e) {
|
|
43
|
-
err(`${label}: cannot read/parse — ${e.message}`);
|
|
44
|
-
return null;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function deepMerge(base, override) {
|
|
49
|
-
if (!override) return base;
|
|
50
|
-
const out = { ...base };
|
|
51
|
-
for (const [k, v] of Object.entries(override)) {
|
|
52
|
-
if (v && typeof v === 'object' && !Array.isArray(v) && typeof out[k] === 'object') {
|
|
53
|
-
out[k] = deepMerge(out[k], v);
|
|
54
|
-
} else {
|
|
55
|
-
out[k] = v;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return out;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isUri(s) {
|
|
62
|
-
try { new URL(s); return true; } catch { return false; }
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function dirExists(p) {
|
|
66
|
-
try { return fs.statSync(p).isDirectory(); } catch { return false; }
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function fileExists(p) {
|
|
70
|
-
try { return fs.statSync(p).isFile(); } catch { return false; }
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function listDirs(p) {
|
|
74
|
-
if (!dirExists(p)) return [];
|
|
75
|
-
return fs.readdirSync(p, { withFileTypes: true })
|
|
76
|
-
.filter(e => e.isDirectory())
|
|
77
|
-
.map(e => e.name);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// ── Check 1: neutral tokens file exists + parses ─────────────────────────────
|
|
81
|
-
|
|
82
|
-
const schema = readJson(TOKENS_SCHEMA, 'config/design-tokens.schema.json');
|
|
83
|
-
const neutral = readJson(NEUTRAL_TOKENS, 'config/design-tokens.neutral.json');
|
|
84
|
-
|
|
85
|
-
if (!neutral) {
|
|
86
|
-
err('config/design-tokens.neutral.json is missing — this is the required baseline');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// ── Check 2: instance tokens file, if present, parses ────────────────────────
|
|
90
|
-
|
|
91
|
-
let instance = null;
|
|
92
|
-
if (fileExists(INSTANCE_TOKENS)) {
|
|
93
|
-
instance = readJson(INSTANCE_TOKENS, 'config/design-tokens.json');
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ── Check 3: merged tokens satisfy required fields ───────────────────────────
|
|
97
|
-
|
|
98
|
-
const merged = neutral ? deepMerge(neutral, instance) : null;
|
|
99
|
-
|
|
100
|
-
if (merged) {
|
|
101
|
-
if (!merged.colors || typeof merged.colors !== 'object') {
|
|
102
|
-
err('merged tokens: "colors" group is missing');
|
|
103
|
-
} else {
|
|
104
|
-
for (const key of REQUIRED_COLORS) {
|
|
105
|
-
if (!merged.colors[key]) err(`merged tokens: colors.${key} is required but missing`);
|
|
106
|
-
else if (typeof merged.colors[key] !== 'string') err(`merged tokens: colors.${key} must be a string`);
|
|
107
|
-
}
|
|
108
|
-
for (const [key, val] of Object.entries(merged.colors)) {
|
|
109
|
-
if (typeof val !== 'string') err(`merged tokens: colors.${key} must be a string`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
if (!merged.fonts || typeof merged.fonts !== 'object') {
|
|
114
|
-
err('merged tokens: "fonts" group is missing');
|
|
115
|
-
} else {
|
|
116
|
-
for (const key of REQUIRED_FONTS) {
|
|
117
|
-
if (!merged.fonts[key]) err(`merged tokens: fonts.${key} is required but missing`);
|
|
118
|
-
else if (typeof merged.fonts[key] !== 'string') err(`merged tokens: fonts.${key} must be a string`);
|
|
119
|
-
}
|
|
120
|
-
if (merged.fonts.links !== undefined) {
|
|
121
|
-
if (!Array.isArray(merged.fonts.links)) {
|
|
122
|
-
err('merged tokens: fonts.links must be an array');
|
|
123
|
-
} else {
|
|
124
|
-
for (const link of merged.fonts.links) {
|
|
125
|
-
if (typeof link !== 'string' || !isUri(link)) {
|
|
126
|
-
err(`merged tokens: fonts.links entry is not a valid URI: ${link}`);
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// Optional groups: check value types if present
|
|
134
|
-
if (merged.scale) {
|
|
135
|
-
for (const [k, v] of Object.entries(merged.scale)) {
|
|
136
|
-
if (typeof v !== 'string') err(`merged tokens: scale.${k} must be a string`);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (merged.radius) {
|
|
140
|
-
for (const [k, v] of Object.entries(merged.radius)) {
|
|
141
|
-
if (typeof v !== 'string') err(`merged tokens: radius.${k} must be a string`);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (merged.motion) {
|
|
145
|
-
for (const [k, v] of Object.entries(merged.motion)) {
|
|
146
|
-
if (typeof v !== 'string') err(`merged tokens: motion.${k} must be a string`);
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ── Check 4: design-sources.json is valid + entries have required fields ─────
|
|
152
|
-
|
|
153
|
-
let sources = null;
|
|
154
|
-
if (fileExists(DESIGN_SOURCES)) {
|
|
155
|
-
sources = readJson(DESIGN_SOURCES, 'config/design-sources.json');
|
|
156
|
-
if (sources) {
|
|
157
|
-
if (!sources.surfaces || typeof sources.surfaces !== 'object') {
|
|
158
|
-
err('config/design-sources.json: "surfaces" key must be an object');
|
|
159
|
-
} else {
|
|
160
|
-
for (const [surfaceId, entry] of Object.entries(sources.surfaces)) {
|
|
161
|
-
if (!entry || typeof entry !== 'object') {
|
|
162
|
-
err(`design-sources: surface "${surfaceId}" entry must be an object`);
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
if (!entry.tool) {
|
|
166
|
-
err(`design-sources: surface "${surfaceId}" is missing "tool" field`);
|
|
167
|
-
} else if (!TOOL_ENUM.has(entry.tool)) {
|
|
168
|
-
err(`design-sources: surface "${surfaceId}" tool "${entry.tool}" is not in allowed enum (${[...TOOL_ENUM].join(', ')})`);
|
|
169
|
-
}
|
|
170
|
-
if (entry.sourceUrl !== undefined) {
|
|
171
|
-
if (typeof entry.sourceUrl !== 'string' || !isUri(entry.sourceUrl)) {
|
|
172
|
-
err(`design-sources: surface "${surfaceId}" sourceUrl is not a valid URI`);
|
|
173
|
-
}
|
|
174
|
-
} else if (entry.tool && !['hand', 'asset'].includes(entry.tool)) {
|
|
175
|
-
warn(`design-sources: surface "${surfaceId}" tool "${entry.tool}" has no sourceUrl`);
|
|
176
|
-
}
|
|
177
|
-
if (entry.lastSyncedAt !== undefined) {
|
|
178
|
-
if (typeof entry.lastSyncedAt !== 'string' || isNaN(Date.parse(entry.lastSyncedAt))) {
|
|
179
|
-
err(`design-sources: surface "${surfaceId}" lastSyncedAt is not a valid ISO 8601 date`);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
} else {
|
|
186
|
-
warn('config/design-sources.json not found — this is optional until adapters are in use');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// ── Check 5: design-sources surface ids match directories (if dirs exist) ─────
|
|
190
|
-
|
|
191
|
-
const hasUiSrc = dirExists(UI_SRC);
|
|
192
|
-
const hasUiAssets = dirExists(UI_ASSETS);
|
|
193
|
-
|
|
194
|
-
if (sources && sources.surfaces) {
|
|
195
|
-
const uiSrcDirs = hasUiSrc ? new Set(listDirs(UI_SRC).filter(n => n !== 'adapters')) : new Set();
|
|
196
|
-
const uiAssetsDirs = hasUiAssets ? new Set(listDirs(UI_ASSETS)) : new Set();
|
|
197
|
-
|
|
198
|
-
for (const surfaceId of Object.keys(sources.surfaces)) {
|
|
199
|
-
const inSrc = uiSrcDirs.has(surfaceId);
|
|
200
|
-
const inAssets = uiAssetsDirs.has(surfaceId);
|
|
201
|
-
if (!inSrc && !inAssets && (hasUiSrc || hasUiAssets)) {
|
|
202
|
-
warn(`design-sources: surface "${surfaceId}" has no matching directory in src/ui/ or assets/ui/`);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ── Check 6: adapter directories have valid index.js ─────────────────────────
|
|
208
|
-
|
|
209
|
-
if (dirExists(ADAPTERS_DIR)) {
|
|
210
|
-
for (const adapterName of listDirs(ADAPTERS_DIR)) {
|
|
211
|
-
const indexPath = path.join(ADAPTERS_DIR, adapterName, 'index.js');
|
|
212
|
-
if (!fileExists(indexPath)) {
|
|
213
|
-
err(`adapter "${adapterName}": missing index.js at src/ui/adapters/${adapterName}/index.js`);
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
216
|
-
let mod;
|
|
217
|
-
try {
|
|
218
|
-
mod = require(indexPath);
|
|
219
|
-
} catch (e) {
|
|
220
|
-
err(`adapter "${adapterName}": index.js threw on require — ${e.message}`);
|
|
221
|
-
continue;
|
|
222
|
-
}
|
|
223
|
-
if (typeof mod.id !== 'string' || !mod.id) {
|
|
224
|
-
err(`adapter "${adapterName}": index.js must export a non-empty string "id"`);
|
|
225
|
-
} else if (mod.id !== adapterName) {
|
|
226
|
-
warn(`adapter "${adapterName}": exported id "${mod.id}" does not match directory name`);
|
|
227
|
-
}
|
|
228
|
-
if (typeof mod.import !== 'function') {
|
|
229
|
-
err(`adapter "${adapterName}": index.js must export an "import" function`);
|
|
230
|
-
}
|
|
231
|
-
if (typeof mod.export !== 'function') {
|
|
232
|
-
err(`adapter "${adapterName}": index.js must export an "export" function`);
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
// ── Report ────────────────────────────────────────────────────────────────────
|
|
238
|
-
|
|
239
|
-
const effectiveErrors = STRICT ? [...errors, ...warnings] : errors;
|
|
240
|
-
const ok = effectiveErrors.length === 0;
|
|
241
|
-
|
|
242
|
-
if (JSON_MODE) {
|
|
243
|
-
process.stdout.write(JSON.stringify({ ok, errors, warnings }, null, 2) + '\n');
|
|
244
|
-
} else {
|
|
245
|
-
const label = ok ? '✓ design contract valid' : '✗ design contract invalid';
|
|
246
|
-
process.stdout.write(`${label}\n`);
|
|
247
|
-
for (const e of errors) process.stdout.write(` ERROR ${e}\n`);
|
|
248
|
-
for (const w of warnings) process.stdout.write(` WARNING ${w}\n`);
|
|
249
|
-
if (ok && !STRICT && warnings.length) {
|
|
250
|
-
process.stdout.write(` (${warnings.length} warning(s); use --strict to fail on warnings)\n`);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
process.exit(ok ? 0 : 1);
|
|
12
|
+
require('../../modules/ui-design/scripts/validate-design.js');
|
|
@@ -208,6 +208,24 @@ const MODULE_GLOBS = {
|
|
|
208
208
|
// than instance hosting, and the web-surface precedent (hall-ui/status-ui) is
|
|
209
209
|
// one key per surface (task 1002690, goal 1000059).
|
|
210
210
|
'public-landing': ['public-landing/'],
|
|
211
|
+
// --- the design capability (task 1003324, ADR 0197): the design contract, its
|
|
212
|
+
// adapters + sync CLIs + validator, the /design playbook, and the reserved homes
|
|
213
|
+
// for the kit / the vendored UI skills / the style library. default-on,
|
|
214
|
+
// declaration-only (three skills, no routes/port/migrations). The three
|
|
215
|
+
// scripts/gds/ shims are core-side entry points over the module's CLIs —
|
|
216
|
+
// listed for task affinity (the memory/ideas precedent: a command travels with
|
|
217
|
+
// its domain). The contributed skill dirs follow the devsecops note above
|
|
218
|
+
// ('.claude/skills/' stays with whichever module ships the skill); the two
|
|
219
|
+
// loop recipes are the module's how-tos. NOT here, deliberately: config/
|
|
220
|
+
// design-tokens.* + design-sources.json (the host boundary, kernel 'config/')
|
|
221
|
+
// and DESIGN.md (the instance's world); public-landing keeps its own key —
|
|
222
|
+
// the front door is this module's first customer, not its owner. ---
|
|
223
|
+
'ui-design': [
|
|
224
|
+
'modules/ui-design/',
|
|
225
|
+
'scripts/gds/design-sync.js', 'scripts/gds/figma-design-sync.js', 'scripts/gds/validate-design.js',
|
|
226
|
+
'.claude/skills/design/', '.claude/skills/design-sync/', '.claude/skills/figma-design-sync/',
|
|
227
|
+
'docs/recipes/claude-design-loop.md', 'docs/recipes/figma-design-loop.md',
|
|
228
|
+
],
|
|
211
229
|
// --- feature modules ---
|
|
212
230
|
// BV1.R49: game graduated to modules/game/ — src/rooms/, src/world/, src/seats.js,
|
|
213
231
|
// src/db.js, migrations/game/, and public/ all moved there. src/preview-server.js
|
package/src/module-api.js
CHANGED
|
@@ -1226,8 +1226,12 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
|
|
|
1226
1226
|
// landed since 1.19.391 with no explicit bump. run 33176032603. (task 1002620)
|
|
1227
1227
|
// 1.19.393 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1228
1228
|
// landed since 1.19.392 with no explicit bump. run 33177136260. (task 1002620)
|
|
1229
|
+
// 1.19.394 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1230
|
+
// landed since 1.19.393 with no explicit bump. run 33182535783. (task 1002620)
|
|
1231
|
+
// 1.19.395 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
|
|
1232
|
+
// landed since 1.19.394 with no explicit bump. run 33188021384. (task 1002620)
|
|
1229
1233
|
// ---------------------------------------------------------------------------
|
|
1230
|
-
const CORE_VERSION = '1.19.
|
|
1234
|
+
const CORE_VERSION = '1.19.395'; // CI auto-patch carrier (ADR 0161); see changelog above
|
|
1231
1235
|
|
|
1232
1236
|
// A namespaced logger so a module's log lines are attributable + consistent.
|
|
1233
1237
|
// Usage: const log = api.logger('dev-box'); log.info('mounted');
|
|
@@ -58,7 +58,7 @@ test('the fonts the two files share are the same fonts', () => {
|
|
|
58
58
|
|
|
59
59
|
test('the claude-design adapter falls back to the pack accent, never a brand literal of its own', () => {
|
|
60
60
|
const source = fs.readFileSync(
|
|
61
|
-
path.join(REPO_ROOT, '
|
|
61
|
+
path.join(REPO_ROOT, 'modules', 'ui-design', 'adapters', 'claude-design', 'index.js'), 'utf8');
|
|
62
62
|
const block = source.match(/const FALLBACK_ACCENT =[\s\S]*?\}\)\(\);/);
|
|
63
63
|
assert.ok(block, 'the adapter declares one resolved fallback accent instead of inline literals');
|
|
64
64
|
assert.match(block[0], /design-tokens\.neutral\.json/,
|
package/tests/doc_cli_guard.mjs
CHANGED
|
@@ -181,6 +181,25 @@ test('a flag present in real code produces NO warning', () => {
|
|
|
181
181
|
assert.deepEqual(warnings, []);
|
|
182
182
|
});
|
|
183
183
|
|
|
184
|
+
test('a scripts/gds/ shim is read together with the module CLI it forwards to (task 1003324)', () => {
|
|
185
|
+
// scripts/gds/validate-design.js is `require('../../modules/ui-design/scripts/validate-design.js')`
|
|
186
|
+
// plus a usage banner; `--strict` is parsed in the module file. Read alone, the shim
|
|
187
|
+
// would only ever say "named in comments" for a flag that is genuinely honoured.
|
|
188
|
+
const src = g.readCliSource('scripts/gds/validate-design.js');
|
|
189
|
+
assert.ok(src, 'the shim exists');
|
|
190
|
+
assert.ok(g.scriptMentionsFlag(g.stripLineComments(src), 'strict'),
|
|
191
|
+
'the flag is found in CODE — the module CLI was followed, not just the shim banner');
|
|
192
|
+
const { violations, warnings } = g.scanDocs(['d.md'], {
|
|
193
|
+
readDoc: () => 'run `node scripts/gds/validate-design.js --strict`',
|
|
194
|
+
readScript: g.readCliSource, allow: new Map(),
|
|
195
|
+
});
|
|
196
|
+
assert.deepEqual(violations, []);
|
|
197
|
+
assert.deepEqual(warnings, [], 'no "only in comments" warning for a shim-forwarded flag');
|
|
198
|
+
// Bounded: a require into src/ is NOT followed (it is not a CLI phase).
|
|
199
|
+
assert.ok(!g.readCliSource('scripts/gds/fitness.js').includes('function requireBuilder'),
|
|
200
|
+
'a core internal the CLI requires is not concatenated into its source');
|
|
201
|
+
});
|
|
202
|
+
|
|
184
203
|
test('stripLineComments keeps code and drops both comment forms', () => {
|
|
185
204
|
const out = g.stripLineComments("const a = '--keep'; // --dropped\n/* --alsodropped */\n");
|
|
186
205
|
assert.ok(out.includes('--keep'));
|
package/tests/figma_adapter.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import path from 'node:path';
|
|
|
16
16
|
import os from 'node:os';
|
|
17
17
|
|
|
18
18
|
const require = createRequire(import.meta.url);
|
|
19
|
-
const adapter = require('../
|
|
19
|
+
const adapter = require('../modules/ui-design/adapters/figma/index.js');
|
|
20
20
|
|
|
21
21
|
const REPO_ROOT = path.resolve(process.cwd());
|
|
22
22
|
const SURFACE_ID = `__test_figma_${process.pid}`;
|