@bongos/core 1.19.394 → 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.
Files changed (42) hide show
  1. package/.bongos-core.json +102 -52
  2. package/.claude/skills/design/SKILL.md +68 -0
  3. package/.claude/skills/design-sync/SKILL.md +2 -2
  4. package/.claude/skills/figma-design-sync/SKILL.md +3 -3
  5. package/config/design-tokens.neutral.json +1 -1
  6. package/config/design-tokens.schema.json +1 -1
  7. package/docs/adr/0197-the-ui-design-module.md +52 -0
  8. package/docs/adr/README.md +1 -0
  9. package/docs/architecture.md +1 -1
  10. package/docs/copy-inventory.md +1 -1
  11. package/docs/copy-registry.json +1 -1
  12. package/docs/file-map.md +2 -0
  13. package/docs/modules-contract.md +1 -0
  14. package/docs/onboarding/slash-commands.md +1 -1
  15. package/docs/recipes/claude-design-loop.md +3 -3
  16. package/docs/recipes/figma-design-loop.md +4 -4
  17. package/modules/ui-design/CLAUDE.md +15 -0
  18. package/{src/ui → modules/ui-design}/adapters/claude-design/index.js +1 -1
  19. package/{src/ui → modules/ui-design}/adapters/figma/index.js +3 -3
  20. package/{docs → modules/ui-design/docs}/design-contract.md +12 -12
  21. package/modules/ui-design/kit/README.md +3 -0
  22. package/modules/ui-design/module.json +11 -0
  23. package/modules/ui-design/scripts/design-sync.js +65 -0
  24. package/modules/ui-design/scripts/figma-design-sync.js +64 -0
  25. package/modules/ui-design/scripts/validate-design.js +258 -0
  26. package/modules/ui-design/skills/README.md +3 -0
  27. package/modules/ui-design/styles/README.md +3 -0
  28. package/package-lock.json +2 -2
  29. package/package.json +1 -1
  30. package/scripts/gds/copy-inventory.js +7 -5
  31. package/scripts/gds/design-sync.js +9 -59
  32. package/scripts/gds/doc-cli-guard.js +11 -5
  33. package/scripts/gds/figma-design-sync.js +9 -59
  34. package/scripts/gds/fitness-checks-identity.js +5 -3
  35. package/scripts/gds/publish-manifest.js +1 -1
  36. package/scripts/gds/validate-design.js +7 -249
  37. package/src/bongos/module-scope-map.js +18 -0
  38. package/src/module-api.js +3 -1
  39. package/tests/design_tokens_sync.mjs +1 -1
  40. package/tests/doc_cli_guard.mjs +19 -0
  41. package/tests/figma_adapter.mjs +1 -1
  42. package/tests/module_loader.mjs +4 -1
package/package-lock.json CHANGED
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.394",
3
+ "version": "1.19.395",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@bongos/core",
9
- "version": "1.19.394",
9
+ "version": "1.19.395",
10
10
  "license": "AGPL-3.0-or-later",
11
11
  "dependencies": {
12
12
  "express": "^4.21.2",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bongos/core",
3
- "version": "1.19.394",
3
+ "version": "1.19.395",
4
4
  "description": "Cloud Bongos — the AI-first build platform core (GDS + platform surfaces + module system), installed as a versioned dependency (ADR 0108).",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "main": "src/platform-server.js",
@@ -16,10 +16,12 @@
16
16
  // machine-readable registry and one human report. Same posture as gen-repo-map /
17
17
  // gen-api-docs: a command you run and commit.
18
18
  //
19
- // THE SURFACE MISMATCH, RECORDED RATHER THAN ASSUMED AWAY. docs/design-contract.md
20
- // says UI components live under `src/ui/<surface>/`, and that a surface directory is
21
- // the unit a design adapter targets. On this tree `src/ui/` contains only
22
- // `adapters/` — the real UI is modules/hall-ui/public and modules/status-ui/public.
19
+ // THE SURFACE MISMATCH, RECORDED RATHER THAN ASSUMED AWAY. The design contract
20
+ // (modules/ui-design/docs/design-contract.md) says UI components live under
21
+ // `src/ui/<surface>/`, and that a surface directory is the unit a design adapter
22
+ // targets. This tree has no `src/ui/` at all (the adapters moved to
23
+ // modules/ui-design/adapters/ in task 1003324) — the real UI is modules/hall-ui/public
24
+ // and modules/status-ui/public.
23
25
  // So this tool maps REAL paths onto contract-shaped surface ids and reports the
24
26
  // mismatch in its own output. Do not "fix" that by pretending the contract layout
25
27
  // exists.
@@ -76,7 +78,7 @@ const BLIND_SPOTS = [
76
78
  'Template literals containing a nested backtick: the literal scanner stops at the first closing backtick, so a nested template inside `${...}` truncates that one literal. Rare in this tree and cheap to spot (a row that ends mid-sentence); a real JS parser is the fix if it ever matters.',
77
79
  'Runtime-composed strings: anything assembled from fragments or interpolated at render time is captured only as its literal parts. A sentence that exists only once the browser runs cannot be inventoried by reading the tree.',
78
80
  'Generated pages: `docs/api-reference.md` and the OpenAPI surface are generated from route prose; that copy belongs to its generator, not to a designer.',
79
- 'The design-contract surface layout: `docs/design-contract.md` expects `src/ui/<surface>/`, which this tree does not use (`src/ui` holds only `adapters/`). Surface ids here are a MAPPING onto real paths, not evidence the contract layout exists.',
81
+ 'The design-contract surface layout: `modules/ui-design/docs/design-contract.md` expects `src/ui/<surface>/`, which this tree does not use (there is no `src/ui/`; the adapters live in `modules/ui-design/adapters/`). Surface ids here are a MAPPING onto real paths, not evidence the contract layout exists.',
80
82
  ];
81
83
 
82
84
  // A stable id survives the string MOVING: it hashes surface + normalized text,
@@ -1,62 +1,12 @@
1
1
  #!/usr/bin/env node
2
- // Build a Claude Design token-preview bundle from the repo's design system.
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
- // Usage:
5
- // node scripts/gds/design-sync.js — build bundle, print manifest JSON
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 same-directory relative requires is enough (it is where a carve
200
- // puts things) and keeps the read bounded. A missing sibling is skipped: the
201
- // point is to find the flag, not to prove the require graph.
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*['"](\.\/[^'"]+)['"]\s*\)/g;
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
- // Build a Figma push plan from the repo's design system (task 2048, redo of 1379).
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
- // Usage:
5
- // node scripts/gds/figma-design-sync.js — build plan, print manifest JSON
6
- // node scripts/gds/figma-design-sync.js --surface <id> — limit to one surface
7
- //
8
- // Output (stdout, JSON):
9
- // { planPath, files, surfaceIds, summary }
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
- const validatorPath = path.join(REPO_ROOT, 'scripts', 'gds', 'validate-design.js');
278
- const adaptersDir = path.join(REPO_ROOT, 'src', 'ui', 'adapters');
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 — minimal validator for the neutral design contract (task 1377).
4
- // See docs/design-contract.md §6 for the full spec.
5
- //
6
- // Usage:
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
@@ -1228,8 +1228,10 @@ const { validateOrRespond, LIMITS, parseId, asyncHandler, corsPublicGet, parsePa
1228
1228
  // landed since 1.19.392 with no explicit bump. run 33177136260. (task 1002620)
1229
1229
  // 1.19.394 — CI auto-patch (publish-on-merge, ADR 0161): carrier for merges
1230
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)
1231
1233
  // ---------------------------------------------------------------------------
1232
- const CORE_VERSION = '1.19.394'; // CI auto-patch carrier (ADR 0161); see changelog above
1234
+ const CORE_VERSION = '1.19.395'; // CI auto-patch carrier (ADR 0161); see changelog above
1233
1235
 
1234
1236
  // A namespaced logger so a module's log lines are attributable + consistent.
1235
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, 'src', 'ui', 'adapters', 'claude-design', 'index.js'), 'utf8');
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/,
@@ -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'));
@@ -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('../src/ui/adapters/figma/index.js');
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}`;
@@ -176,6 +176,9 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
176
176
  // off in enabledModules(). And copy-desk (task 1003113 / R02 of goal 1000074) —
177
177
  // the copy half of the design contract: the flag + the artist's queue over
178
178
  // user-facing text, default-on because every instance shows a person words.
179
+ // And ui-design (task 1003324 / ADR 0197) — the design contract, its adapters
180
+ // and the /design playbook as a platform capability: declaration-only (three
181
+ // skills, no routes/port), default-on because every instance has a face.
179
182
  // All are discovered; none has validation
180
183
  // errors or coreVersion mismatches. Order is the readdir sort.
181
184
  //
@@ -200,7 +203,7 @@ test('the live tree discovers the extracted modules cleanly (BV1.R43/R48/R47/R49
200
203
  // That is precisely how the agents regression reached an open PR (task 1002485).
201
204
  loader._reset();
202
205
  const snap = loader.loaderSnapshot();
203
- assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui']);
206
+ assert.deepEqual(snap.loaded, ['agents', 'autonomy', 'builder-settings', 'copy-desk', 'dev-box', 'discord', 'economy', 'government', 'grading', 'hall-ui', 'ideas', 'lifecycle', 'memory', 'onboarding', 'platform-identity', 'provisioning', 'security', 'sessions', 'status-ui', 'ui-design']);
204
207
  assert.deepEqual(snap.errors, []);
205
208
  assert.deepEqual(snap.skipped, []);
206
209
  loader._reset();