@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.
- package/.bongos-core.json +114 -59
- 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/clients/bongos-client/index.d.ts +1 -1
- 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/api/openapi.json +4 -0
- package/docs/api-reference.md +1 -1
- package/docs/architecture.md +1 -1
- package/docs/copy-inventory.md +11 -9
- package/docs/copy-registry.json +30 -12
- 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/hall-ui/public/my-projects.css +22 -0
- package/modules/hall-ui/public/my-projects.html +7 -1
- package/modules/hall-ui/public/my-projects.js +17 -11
- package/modules/platform-identity/routes/my-projects.js +50 -3
- 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/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/join_application_note.mjs +112 -0
- 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
|
-
|
|
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 `
|
|
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/
|
|
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 `
|
|
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/
|
|
299
|
-
node scripts/
|
|
300
|
-
node scripts/
|
|
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
|
|
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 `
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
20
|
-
// says UI components live under
|
|
21
|
-
//
|
|
22
|
-
//
|
|
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
|
|
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
|
-
|
|
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');
|