@bongos/core 1.19.394 → 1.19.396

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 (50) hide show
  1. package/.bongos-core.json +114 -59
  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/clients/bongos-client/index.d.ts +1 -1
  6. package/config/design-tokens.neutral.json +1 -1
  7. package/config/design-tokens.schema.json +1 -1
  8. package/docs/adr/0197-the-ui-design-module.md +52 -0
  9. package/docs/adr/README.md +1 -0
  10. package/docs/api/openapi.json +4 -0
  11. package/docs/api-reference.md +1 -1
  12. package/docs/architecture.md +1 -1
  13. package/docs/copy-inventory.md +11 -9
  14. package/docs/copy-registry.json +30 -12
  15. package/docs/file-map.md +2 -0
  16. package/docs/modules-contract.md +1 -0
  17. package/docs/onboarding/slash-commands.md +1 -1
  18. package/docs/recipes/claude-design-loop.md +3 -3
  19. package/docs/recipes/figma-design-loop.md +4 -4
  20. package/modules/hall-ui/public/my-projects.css +22 -0
  21. package/modules/hall-ui/public/my-projects.html +7 -1
  22. package/modules/hall-ui/public/my-projects.js +17 -11
  23. package/modules/platform-identity/routes/my-projects.js +50 -3
  24. package/modules/ui-design/CLAUDE.md +15 -0
  25. package/{src/ui → modules/ui-design}/adapters/claude-design/index.js +1 -1
  26. package/{src/ui → modules/ui-design}/adapters/figma/index.js +3 -3
  27. package/{docs → modules/ui-design/docs}/design-contract.md +12 -12
  28. package/modules/ui-design/kit/README.md +3 -0
  29. package/modules/ui-design/module.json +11 -0
  30. package/modules/ui-design/scripts/design-sync.js +65 -0
  31. package/modules/ui-design/scripts/figma-design-sync.js +64 -0
  32. package/modules/ui-design/scripts/validate-design.js +258 -0
  33. package/modules/ui-design/skills/README.md +3 -0
  34. package/modules/ui-design/styles/README.md +3 -0
  35. package/package-lock.json +2 -2
  36. package/package.json +1 -1
  37. package/scripts/gds/copy-inventory.js +7 -5
  38. package/scripts/gds/design-sync.js +9 -59
  39. package/scripts/gds/doc-cli-guard.js +11 -5
  40. package/scripts/gds/figma-design-sync.js +9 -59
  41. package/scripts/gds/fitness-checks-identity.js +5 -3
  42. package/scripts/gds/publish-manifest.js +1 -1
  43. package/scripts/gds/validate-design.js +7 -249
  44. package/src/bongos/module-scope-map.js +18 -0
  45. package/src/module-api.js +5 -1
  46. package/tests/design_tokens_sync.mjs +1 -1
  47. package/tests/doc_cli_guard.mjs +19 -0
  48. package/tests/figma_adapter.mjs +1 -1
  49. package/tests/join_application_note.mjs +112 -0
  50. package/tests/module_loader.mjs +4 -1
@@ -1,8 +1,8 @@
1
1
  # Design contract — neutral adapter interface
2
2
 
3
- **Status:** Accepted · authored by [task 1377](https://example.com/builders#/task/1377) · governed by [ADR 0081](adr/<redacted>.md)
3
+ **Status:** Accepted · authored by [task 1377](https://example.com/builders#/task/1377) · governed by [ADR 0081](../../../docs/adr/<redacted>.md) · lives in the `ui-design` module since [task 1003324](https://cloudbongos.com/builders#/task/1003324) ([ADR 0197](../../../docs/adr/<redacted>.md)) — the token files it names stay in `config/` as the host boundary
4
4
 
5
- The repo is the single source of truth for design. Every design tool is a swappable adapter that reads from and writes to this contract — it does not own the source. This file is the contract every adapter must conform to. The minimal validator (`scripts/gds/validate-design.js`) enforces the machine-checkable parts.
5
+ The repo is the single source of truth for design. Every design tool is a swappable adapter that reads from and writes to this contract — it does not own the source. This file is the contract every adapter must conform to. The minimal validator (`modules/ui-design/scripts/validate-design.js`; `node scripts/gds/validate-design.js` is a thin shim over it) enforces the machine-checkable parts.
6
6
 
7
7
  ---
8
8
 
@@ -96,7 +96,7 @@ Rules:
96
96
  | Freeform canvas art | — | `assets/ui/<surface>/` | Asset |
97
97
  | Pixel-art game tiles | see `art/` pipeline | — | Art pipeline (not this contract) |
98
98
 
99
- Pixel-art game tiles (world tiles, characters, objects) are out of scope for this contract — they are governed by the `art/` pipeline and [ADR 0088](adr/<redacted>.md).
99
+ Pixel-art game tiles (world tiles, characters, objects) are out of scope for this contract — they are governed by the `art/` pipeline and [ADR 0088](../../../docs/adr/<redacted>.md).
100
100
 
101
101
  ---
102
102
 
@@ -152,7 +152,7 @@ Figma-canonical is appropriate for design-system foundations that a dedicated de
152
152
  Each design tool that integrates with this contract must provide an adapter module. An adapter lives at:
153
153
 
154
154
  ```
155
- src/ui/adapters/<id>/index.js (e.g. claude-design, figma)
155
+ modules/ui-design/adapters/<id>/index.js (e.g. claude-design, figma)
156
156
  ```
157
157
 
158
158
  ### 4.1 Required exports
@@ -235,7 +235,7 @@ module.exports = {
235
235
 
236
236
  ### 4.6 Adapter registration
237
237
 
238
- Adapters are discovered by the design subsystem at runtime. No static registry — the loader walks `src/ui/adapters/*/index.js` and requires each, verifying the `id` field is present. A missing or broken adapter is a non-fatal warning; the unaffected adapters continue loading.
238
+ Adapters are discovered by the design subsystem at runtime. No static registry — the loader walks `modules/ui-design/adapters/*/index.js` and requires each, verifying the `id` field is present. A missing or broken adapter is a non-fatal warning; the unaffected adapters continue loading.
239
239
 
240
240
  ### 4.7 Adapter error contract
241
241
 
@@ -280,7 +280,7 @@ The `--dt-` prefix is reserved for this design system. Adapters must not emit CS
280
280
 
281
281
  ## 6. Validator
282
282
 
283
- `scripts/gds/validate-design.js` is the minimal validator for this contract. It checks:
283
+ `modules/ui-design/scripts/validate-design.js` is the minimal validator for this contract (`node scripts/gds/validate-design.js` is a thin shim over it). It checks:
284
284
 
285
285
  1. `config/design-tokens.neutral.json` exists and validates against the schema.
286
286
  2. If `config/design-tokens.json` exists, it validates against the schema.
@@ -288,19 +288,19 @@ The `--dt-` prefix is reserved for this design system. Adapters must not emit CS
288
288
  4. `config/design-sources.json` is valid JSON and every surface entry has a `tool` field from the allowed enum.
289
289
  5. Every `sourceUrl` in `design-sources.json` is a valid URI string.
290
290
  6. Every `surfaceId` key in `design-sources.json` has a matching directory under `src/ui/` or `assets/ui/` (if any such directory exists — the file may precede the directories for a new instance).
291
- 7. If `src/ui/adapters/` exists, each adapter directory has an `index.js` that exports `id`, `import`, and `export`.
291
+ 7. If `modules/ui-design/adapters/` exists, each adapter directory has an `index.js` that exports `id`, `import`, and `export`.
292
292
 
293
293
  The neutral-pair lockstep (§1.3) is deliberately not a validator check — it lives in `tests/design_tokens_sync.mjs`, so an instance running only `validate-design.js` is not forced to mirror its own pack.
294
294
 
295
295
  Usage:
296
296
 
297
297
  ```
298
- node scripts/gds/validate-design.js # exits 0 on pass, 1 on failure
299
- node scripts/gds/validate-design.js --json # machine-readable report
300
- node scripts/gds/validate-design.js --strict # fail on warnings too
298
+ node modules/ui-design/scripts/validate-design.js # exits 0 on pass, 1 on failure
299
+ node modules/ui-design/scripts/validate-design.js --json # machine-readable report
300
+ node modules/ui-design/scripts/validate-design.js --strict # fail on warnings too
301
301
  ```
302
302
 
303
- The validator is hooked into `scripts/gds/fitness.js` as an opt-in check (enabled once the first adapter is committed — task 1378 or 1379).
303
+ The validator is wired into `scripts/gds/fitness.js` (`checkDesignContract` in `fitness-checks-identity.js`) and hard-fails while any adapter is present.
304
304
 
305
305
  ---
306
306
 
@@ -316,7 +316,7 @@ The validator is hooked into `scripts/gds/fitness.js` as an opt-in check (enable
316
316
 
317
317
  ## 8. Adding a new adapter
318
318
 
319
- 1. Create `src/ui/adapters/<id>/index.js` exporting `{ id, import, export }`.
319
+ 1. Create `modules/ui-design/adapters/<id>/index.js` exporting `{ id, import, export }`.
320
320
  2. Add a `<surface-id>` entry to `config/design-sources.json` with `"tool": "<id>"`.
321
321
  3. Write a task for the adapter (it unblocks from [task 1377](https://example.com/builders#/task/1377)) and ship it under a claimed Bongos task.
322
322
  4. The validator discovers the adapter automatically on the next run.
@@ -0,0 +1,3 @@
1
+ # `modules/ui-design/kit/` — reserved home for the look-before-you-ship kit
2
+
3
+ Lands with [task 1003320](https://cloudbongos.com/builders#/task/1003320): an instance-agnostic stub server, a renderer/auditor (every state × width × mode, with the contrast audit), the interaction probes and the mock checker any builder points at any surface. Until it lands, the apex probes live in session scratchpads (see the [module CLAUDE.md](../CLAUDE.md)).
@@ -0,0 +1,11 @@
1
+ {
2
+ "key": "ui-design",
3
+ "title": "UI design (the design contract, its adapters and sync, the /design playbook, the kit, the skills, the styles)",
4
+ "description": "The platform capability that lets any builder on any instance create and design good-looking, functional UIs in line with that instance's world (scoped with the owner 2026-08-28; task 1003324, ADR 0197). It ships METHOD and never a world: the neutral design contract of ADR 0081 with its tool adapters (Claude Design, Figma), their sync CLIs and the validator; the /design playbook for ui-discipline work; and the reserved homes for the look-before-you-ship kit (kit/), the vetted UI skills (skills/) and the style library (styles/) the sibling tasks land. The instance's world — its branding pack, config/design-tokens.*, config/design-sources.json and its DESIGN.md — stays in config/ and at the repo root as the host boundary this module reads and never owns (ADR 0062 §7). Cloud Bongos's own front door is this module's first customer, not its owner. default-on because every instance has a face. No routes, no migrations, no port: declaration-only like character-anim, so disabling it withdraws only the skills it contributes and touches no served surface.",
5
+ "coreVersion": "^1.19.0",
6
+ "default": true,
7
+ "maintenance": { "status": "core-maintained" },
8
+ "contributes": { "skills": ["design", "design-sync", "figma-design-sync"] },
9
+ "provides": [],
10
+ "consumes": []
11
+ }
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env node
2
+ // Build a Claude Design token-preview bundle from the repo's design system.
3
+ //
4
+ // Usage:
5
+ // node modules/ui-design/scripts/design-sync.js — build bundle, print manifest JSON
6
+ // node modules/ui-design/scripts/design-sync.js --surface <id> — limit to one surface
7
+ // (node scripts/gds/design-sync.js is a thin shim over this file — the form the skills use)
8
+ //
9
+ // Output (stdout, JSON):
10
+ // { bundlePath, files, surfaceIds, summary }
11
+ //
12
+ // The /design-sync skill calls this, then uses the DesignSync MCP tool
13
+ // to push the bundle to a Claude Design project.
14
+
15
+ const path = require('node:path');
16
+ const fs = require('node:fs');
17
+
18
+ // The design system (src/ui/, config/design-tokens.json) is HOST content (ADR 0108
19
+ // §1), and this CLI lives in a module, which may not reach src/instance-config.js
20
+ // (the one-way rule, ADR 0083). The instance root IS the repo root this module sits
21
+ // in today, so the walk-up is byte-identical to the resolveInstanceRoot() the
22
+ // core-side entry used to call; when the core installs as a package the root
23
+ // arrives through the doorway (task 1003324, ADR 0197).
24
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
25
+
26
+ function arg(flag, args) {
27
+ const i = args.indexOf(flag);
28
+ return i !== -1 ? args[i + 1] : null;
29
+ }
30
+
31
+ (async () => {
32
+ const args = process.argv.slice(2);
33
+ const targetSurface = arg('--surface', args) || undefined;
34
+
35
+ const adapter = require('../adapters/claude-design/index.js');
36
+
37
+ const result = await adapter.import({
38
+ repoRoot: REPO_ROOT,
39
+ ...(targetSurface ? { surfaces: [{ id: targetSurface, path: path.join(REPO_ROOT, 'src', 'ui', targetSurface) }] } : {}),
40
+ });
41
+
42
+ // Build the file list from the bundle dir for the skill to pass to finalize_plan
43
+ const { bundlePath, surfaceIds, summary } = result;
44
+ const files = collectFiles(bundlePath, bundlePath);
45
+
46
+ const manifest = { bundlePath, files, surfaceIds, summary };
47
+ process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
48
+ })().catch(err => {
49
+ process.stderr.write(`design-sync: ${err.message}\n`);
50
+ process.exit(1);
51
+ });
52
+
53
+ function collectFiles(dir, root) {
54
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
55
+ const out = [];
56
+ for (const e of entries) {
57
+ const full = path.join(dir, e.name);
58
+ if (e.isDirectory()) {
59
+ out.push(...collectFiles(full, root));
60
+ } else {
61
+ out.push(path.relative(root, full));
62
+ }
63
+ }
64
+ return out;
65
+ }
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env node
2
+ // Build a Figma push plan from the repo's design system (task 2048, redo of 1379).
3
+ //
4
+ // Usage:
5
+ // node modules/ui-design/scripts/figma-design-sync.js — build plan, print manifest JSON
6
+ // node modules/ui-design/scripts/figma-design-sync.js --surface <id> — limit to one surface
7
+ // (node scripts/gds/figma-design-sync.js is a thin shim over this file — the form the skills use)
8
+ //
9
+ // Output (stdout, JSON):
10
+ // { planPath, files, surfaceIds, summary }
11
+ //
12
+ // The figma-design-sync skill calls this, then executes the plan's frames
13
+ // against the live Figma MCP tool.
14
+
15
+ const path = require('node:path');
16
+ const fs = require('node:fs');
17
+
18
+ // The design system (src/ui/, config/design-tokens.json) is HOST content (ADR 0108
19
+ // §1), and this CLI lives in a module, which may not reach src/instance-config.js
20
+ // (the one-way rule, ADR 0083). The instance root IS the repo root this module sits
21
+ // in today, so the walk-up is byte-identical to the resolveInstanceRoot() the
22
+ // core-side entry used to call; when the core installs as a package the root
23
+ // arrives through the doorway (task 1003324, ADR 0197).
24
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
25
+
26
+ function arg(flag, args) {
27
+ const i = args.indexOf(flag);
28
+ return i !== -1 ? args[i + 1] : null;
29
+ }
30
+
31
+ (async () => {
32
+ const args = process.argv.slice(2);
33
+ const targetSurface = arg('--surface', args) || undefined;
34
+
35
+ const adapter = require('../adapters/figma/index.js');
36
+
37
+ const result = await adapter.import({
38
+ repoRoot: REPO_ROOT,
39
+ ...(targetSurface ? { surfaces: [{ id: targetSurface, path: path.join(REPO_ROOT, 'src', 'ui', targetSurface) }] } : {}),
40
+ });
41
+
42
+ const { planPath, surfaceIds, summary } = result;
43
+ const files = collectFiles(planPath, planPath);
44
+
45
+ const manifest = { planPath, files, surfaceIds, summary };
46
+ process.stdout.write(JSON.stringify(manifest, null, 2) + '\n');
47
+ })().catch(err => {
48
+ process.stderr.write(`figma-design-sync: ${err.message}\n`);
49
+ process.exit(1);
50
+ });
51
+
52
+ function collectFiles(dir, root) {
53
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
54
+ const out = [];
55
+ for (const e of entries) {
56
+ const full = path.join(dir, e.name);
57
+ if (e.isDirectory()) {
58
+ out.push(...collectFiles(full, root));
59
+ } else {
60
+ out.push(path.relative(root, full));
61
+ }
62
+ }
63
+ return out;
64
+ }
@@ -0,0 +1,258 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+ // validate-design.js — minimal validator for the neutral design contract (task 1377).
4
+ // See modules/ui-design/docs/design-contract.md §6 for the full spec. Lives in the
5
+ // ui-design module since task 1003324 (ADR 0197); node scripts/gds/validate-design.js
6
+ // is a thin shim over this file.
7
+ //
8
+ // Usage:
9
+ // node modules/ui-design/scripts/validate-design.js # exits 0 on pass, 1 on failure
10
+ // node modules/ui-design/scripts/validate-design.js --json # machine-readable { ok, errors, warnings }
11
+ // node modules/ui-design/scripts/validate-design.js --strict # treat warnings as errors
12
+
13
+ const fs = require('node:fs');
14
+ const path = require('node:path');
15
+
16
+ // The instance root is the repo root this module sits in (ADR 0108 §1); a module
17
+ // may not reach src/instance-config.js for it (ADR 0083).
18
+ const REPO_ROOT = path.resolve(__dirname, '..', '..', '..');
19
+ const ARGS = new Set(process.argv.slice(2));
20
+ const JSON_MODE = ARGS.has('--json');
21
+ const STRICT = ARGS.has('--strict');
22
+
23
+ const NEUTRAL_TOKENS = path.join(REPO_ROOT, 'config', 'design-tokens.neutral.json');
24
+ const INSTANCE_TOKENS = path.join(REPO_ROOT, 'config', 'design-tokens.json');
25
+ const TOKENS_SCHEMA = path.join(REPO_ROOT, 'config', 'design-tokens.schema.json');
26
+ const DESIGN_SOURCES = path.join(REPO_ROOT, 'config', 'design-sources.json');
27
+ const UI_SRC = path.join(REPO_ROOT, 'src', 'ui');
28
+ const UI_ASSETS = path.join(REPO_ROOT, 'assets', 'ui');
29
+ const ADAPTERS_DIR = path.join(__dirname, '..', 'adapters'); // the module's own adapters
30
+
31
+ const TOOL_ENUM = new Set(['claude-design', 'figma', 'hand', 'asset']);
32
+ const REQUIRED_COLORS = ['bg', 'bgCard', 'ink', 'accent'];
33
+ const REQUIRED_FONTS = ['display', 'body'];
34
+
35
+ const errors = [];
36
+ const warnings = [];
37
+
38
+ function err(msg) { errors.push(msg); }
39
+ function warn(msg) { warnings.push(msg); }
40
+
41
+ // ── helpers ──────────────────────────────────────────────────────────────────
42
+
43
+ function readJson(filePath, label) {
44
+ try {
45
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
46
+ } catch (e) {
47
+ err(`${label}: cannot read/parse — ${e.message}`);
48
+ return null;
49
+ }
50
+ }
51
+
52
+ function deepMerge(base, override) {
53
+ if (!override) return base;
54
+ const out = { ...base };
55
+ for (const [k, v] of Object.entries(override)) {
56
+ if (v && typeof v === 'object' && !Array.isArray(v) && typeof out[k] === 'object') {
57
+ out[k] = deepMerge(out[k], v);
58
+ } else {
59
+ out[k] = v;
60
+ }
61
+ }
62
+ return out;
63
+ }
64
+
65
+ function isUri(s) {
66
+ try { new URL(s); return true; } catch { return false; }
67
+ }
68
+
69
+ function dirExists(p) {
70
+ try { return fs.statSync(p).isDirectory(); } catch { return false; }
71
+ }
72
+
73
+ function fileExists(p) {
74
+ try { return fs.statSync(p).isFile(); } catch { return false; }
75
+ }
76
+
77
+ function listDirs(p) {
78
+ if (!dirExists(p)) return [];
79
+ return fs.readdirSync(p, { withFileTypes: true })
80
+ .filter(e => e.isDirectory())
81
+ .map(e => e.name);
82
+ }
83
+
84
+ // ── Check 1: neutral tokens file exists + parses ─────────────────────────────
85
+
86
+ const schema = readJson(TOKENS_SCHEMA, 'config/design-tokens.schema.json');
87
+ const neutral = readJson(NEUTRAL_TOKENS, 'config/design-tokens.neutral.json');
88
+
89
+ if (!neutral) {
90
+ err('config/design-tokens.neutral.json is missing — this is the required baseline');
91
+ }
92
+
93
+ // ── Check 2: instance tokens file, if present, parses ────────────────────────
94
+
95
+ let instance = null;
96
+ if (fileExists(INSTANCE_TOKENS)) {
97
+ instance = readJson(INSTANCE_TOKENS, 'config/design-tokens.json');
98
+ }
99
+
100
+ // ── Check 3: merged tokens satisfy required fields ───────────────────────────
101
+
102
+ const merged = neutral ? deepMerge(neutral, instance) : null;
103
+
104
+ if (merged) {
105
+ if (!merged.colors || typeof merged.colors !== 'object') {
106
+ err('merged tokens: "colors" group is missing');
107
+ } else {
108
+ for (const key of REQUIRED_COLORS) {
109
+ if (!merged.colors[key]) err(`merged tokens: colors.${key} is required but missing`);
110
+ else if (typeof merged.colors[key] !== 'string') err(`merged tokens: colors.${key} must be a string`);
111
+ }
112
+ for (const [key, val] of Object.entries(merged.colors)) {
113
+ if (typeof val !== 'string') err(`merged tokens: colors.${key} must be a string`);
114
+ }
115
+ }
116
+
117
+ if (!merged.fonts || typeof merged.fonts !== 'object') {
118
+ err('merged tokens: "fonts" group is missing');
119
+ } else {
120
+ for (const key of REQUIRED_FONTS) {
121
+ if (!merged.fonts[key]) err(`merged tokens: fonts.${key} is required but missing`);
122
+ else if (typeof merged.fonts[key] !== 'string') err(`merged tokens: fonts.${key} must be a string`);
123
+ }
124
+ if (merged.fonts.links !== undefined) {
125
+ if (!Array.isArray(merged.fonts.links)) {
126
+ err('merged tokens: fonts.links must be an array');
127
+ } else {
128
+ for (const link of merged.fonts.links) {
129
+ if (typeof link !== 'string' || !isUri(link)) {
130
+ err(`merged tokens: fonts.links entry is not a valid URI: ${link}`);
131
+ }
132
+ }
133
+ }
134
+ }
135
+ }
136
+
137
+ // Optional groups: check value types if present
138
+ if (merged.scale) {
139
+ for (const [k, v] of Object.entries(merged.scale)) {
140
+ if (typeof v !== 'string') err(`merged tokens: scale.${k} must be a string`);
141
+ }
142
+ }
143
+ if (merged.radius) {
144
+ for (const [k, v] of Object.entries(merged.radius)) {
145
+ if (typeof v !== 'string') err(`merged tokens: radius.${k} must be a string`);
146
+ }
147
+ }
148
+ if (merged.motion) {
149
+ for (const [k, v] of Object.entries(merged.motion)) {
150
+ if (typeof v !== 'string') err(`merged tokens: motion.${k} must be a string`);
151
+ }
152
+ }
153
+ }
154
+
155
+ // ── Check 4: design-sources.json is valid + entries have required fields ─────
156
+
157
+ let sources = null;
158
+ if (fileExists(DESIGN_SOURCES)) {
159
+ sources = readJson(DESIGN_SOURCES, 'config/design-sources.json');
160
+ if (sources) {
161
+ if (!sources.surfaces || typeof sources.surfaces !== 'object') {
162
+ err('config/design-sources.json: "surfaces" key must be an object');
163
+ } else {
164
+ for (const [surfaceId, entry] of Object.entries(sources.surfaces)) {
165
+ if (!entry || typeof entry !== 'object') {
166
+ err(`design-sources: surface "${surfaceId}" entry must be an object`);
167
+ continue;
168
+ }
169
+ if (!entry.tool) {
170
+ err(`design-sources: surface "${surfaceId}" is missing "tool" field`);
171
+ } else if (!TOOL_ENUM.has(entry.tool)) {
172
+ err(`design-sources: surface "${surfaceId}" tool "${entry.tool}" is not in allowed enum (${[...TOOL_ENUM].join(', ')})`);
173
+ }
174
+ if (entry.sourceUrl !== undefined) {
175
+ if (typeof entry.sourceUrl !== 'string' || !isUri(entry.sourceUrl)) {
176
+ err(`design-sources: surface "${surfaceId}" sourceUrl is not a valid URI`);
177
+ }
178
+ } else if (entry.tool && !['hand', 'asset'].includes(entry.tool)) {
179
+ warn(`design-sources: surface "${surfaceId}" tool "${entry.tool}" has no sourceUrl`);
180
+ }
181
+ if (entry.lastSyncedAt !== undefined) {
182
+ if (typeof entry.lastSyncedAt !== 'string' || isNaN(Date.parse(entry.lastSyncedAt))) {
183
+ err(`design-sources: surface "${surfaceId}" lastSyncedAt is not a valid ISO 8601 date`);
184
+ }
185
+ }
186
+ }
187
+ }
188
+ }
189
+ } else {
190
+ warn('config/design-sources.json not found — this is optional until adapters are in use');
191
+ }
192
+
193
+ // ── Check 5: design-sources surface ids match directories (if dirs exist) ─────
194
+
195
+ const hasUiSrc = dirExists(UI_SRC);
196
+ const hasUiAssets = dirExists(UI_ASSETS);
197
+
198
+ if (sources && sources.surfaces) {
199
+ const uiSrcDirs = hasUiSrc ? new Set(listDirs(UI_SRC)) : new Set();
200
+ const uiAssetsDirs = hasUiAssets ? new Set(listDirs(UI_ASSETS)) : new Set();
201
+
202
+ for (const surfaceId of Object.keys(sources.surfaces)) {
203
+ const inSrc = uiSrcDirs.has(surfaceId);
204
+ const inAssets = uiAssetsDirs.has(surfaceId);
205
+ if (!inSrc && !inAssets && (hasUiSrc || hasUiAssets)) {
206
+ warn(`design-sources: surface "${surfaceId}" has no matching directory in src/ui/ or assets/ui/`);
207
+ }
208
+ }
209
+ }
210
+
211
+ // ── Check 6: adapter directories have valid index.js ─────────────────────────
212
+
213
+ if (dirExists(ADAPTERS_DIR)) {
214
+ for (const adapterName of listDirs(ADAPTERS_DIR)) {
215
+ const indexPath = path.join(ADAPTERS_DIR, adapterName, 'index.js');
216
+ if (!fileExists(indexPath)) {
217
+ err(`adapter "${adapterName}": missing index.js at modules/ui-design/adapters/${adapterName}/index.js`);
218
+ continue;
219
+ }
220
+ let mod;
221
+ try {
222
+ mod = require(indexPath);
223
+ } catch (e) {
224
+ err(`adapter "${adapterName}": index.js threw on require — ${e.message}`);
225
+ continue;
226
+ }
227
+ if (typeof mod.id !== 'string' || !mod.id) {
228
+ err(`adapter "${adapterName}": index.js must export a non-empty string "id"`);
229
+ } else if (mod.id !== adapterName) {
230
+ warn(`adapter "${adapterName}": exported id "${mod.id}" does not match directory name`);
231
+ }
232
+ if (typeof mod.import !== 'function') {
233
+ err(`adapter "${adapterName}": index.js must export an "import" function`);
234
+ }
235
+ if (typeof mod.export !== 'function') {
236
+ err(`adapter "${adapterName}": index.js must export an "export" function`);
237
+ }
238
+ }
239
+ }
240
+
241
+ // ── Report ────────────────────────────────────────────────────────────────────
242
+
243
+ const effectiveErrors = STRICT ? [...errors, ...warnings] : errors;
244
+ const ok = effectiveErrors.length === 0;
245
+
246
+ if (JSON_MODE) {
247
+ process.stdout.write(JSON.stringify({ ok, errors, warnings }, null, 2) + '\n');
248
+ } else {
249
+ const label = ok ? '✓ design contract valid' : '✗ design contract invalid';
250
+ process.stdout.write(`${label}\n`);
251
+ for (const e of errors) process.stdout.write(` ERROR ${e}\n`);
252
+ for (const w of warnings) process.stdout.write(` WARNING ${w}\n`);
253
+ if (ok && !STRICT && warnings.length) {
254
+ process.stdout.write(` (${warnings.length} warning(s); use --strict to fail on warnings)\n`);
255
+ }
256
+ }
257
+
258
+ process.exit(ok ? 0 : 1);
@@ -0,0 +1,3 @@
1
+ # `modules/ui-design/skills/` — reserved home for the vetted UI skills
2
+
3
+ Lands with [task 1003321](https://cloudbongos.com/builders#/task/1003321): the vendored design skills (impeccable, the style/taste skills, image-to-code, imagegen, brandkit), each scanned with `/scan-before-install`, carrying a PROVENANCE sidecar and a licence on the allowlist, and prefixed with the PLATFORM PREAMBLE that reads the instance's branding pack and `DESIGN.md` before any taste rule applies. The vendoring policy is that task's ADR; [ADR 0197](../../../docs/adr/<redacted>.md) only points at it.
@@ -0,0 +1,3 @@
1
+ # `modules/ui-design/styles/` — reserved home for the style library
2
+
3
+ Lands with [task 1003323](https://cloudbongos.com/builders#/task/1003323): looks shipped as branding packs (the fifteen host-overridable tokens plus a `DESIGN.md` per look), with two alternates beside the chrome world, so an instance picks a world instead of inheriting Cloud Bongos's. A look is a pack; it is never a page's private palette.
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.396",
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.396",
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.396",
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');