@utopia-studio-design/design-system-cli 0.8.0 → 0.8.2

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.
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  capabilityManifest, getBlock, getBlockDefinition, getBrandbookSkill, getComponent, getDoc, getLogoSkill, getMotionProfile, getRoomContext, getTemplate, getTheme,
4
4
  listBlockDefinitions, listBlocks, listComponents, listDocs, listLogoSkills, listMotionProfiles, listTemplates, listThemes, prepareLogoMakerRun,
5
- prepareBrandbookRun, repositoryDoctor, search, validateAgentMutation, validateLayout, listUxPatterns, getUxPattern, preparePatternAdaptation, findUiParts, getUiPart, prepareUiComposition, listUxExamples, getUxExample, listUxSourcePatterns, findUxSourcePatterns, getUxSourcePart,
5
+ prepareBrandbookRun, repositoryDoctor, search, validateAgentMutation, validateBrandbook, validateLayout, listUxPatterns, getUxPattern, preparePatternAdaptation, findUiParts, getUiPart, prepareUiComposition, listUxExamples, getUxExample, listUxSourcePatterns, findUxSourcePatterns, getUxSourcePart,
6
6
  } from '../lib/api.mjs'
7
7
 
8
8
  const tools = [
@@ -38,6 +38,7 @@ const tools = [
38
38
  ['prepare_logo_maker_run', 'Prepare a Brand Room-aware Logo Maker chat run using the same skill contract as Ceramic UI. This does not approve or publish the result.', { skillId: { type: 'string' }, brandName: { type: 'string' }, brief: { type: 'string' }, referenceIds: { type: 'array', items: { type: 'string' }, maxItems: 3 } }, (input) => prepareLogoMakerRun(input)],
39
39
  ['get_brandbook_skill', 'Read the Ceramic Brandbook workflow, GitHub source contract, provider rule, and Solution application rules.', {}, () => getBrandbookSkill()],
40
40
  ['prepare_brandbook_run', 'Prepare a Brand Room-aware Ceramic Brandbook run. Returns the required A/B/C direction phase and one-BRAND.md handoff prompt; it does not generate, save, or publish a release.', { brandName: { type: 'string' }, brief: { type: 'string', minLength: 12, maxLength: 4000 } }, (input) => prepareBrandbookRun(input)],
41
+ ['validate_brandbook', 'Mechanically validate the Ceramic BRAND.md contract, A/B/C refinement record, bilingual handoff, assets, and Solution showcase structure.', { path: { type: 'string' } }, (input) => validateBrandbook(input)],
41
42
  ['list_docs', 'List AI-readable design-system documents.', {}, () => listDocs()],
42
43
  ['get_docs', 'Read one design-system document.', { topic: { type: 'string' } }, ({ topic }) => getDoc(topic)],
43
44
  ['doctor', 'Validate the Ceramic source of truth.', {}, () => repositoryDoctor()],
@@ -30,6 +30,17 @@ Call `prepare_brandbook_run` with a brand name and useful brief. A logo is optio
30
30
  }
31
31
  ```
32
32
 
33
+ After the user selects and refines A, B, or C, write the single `BRAND.md` source and validate it before building or saving a release:
34
+
35
+ ```json
36
+ {
37
+ "name": "validate_brandbook",
38
+ "arguments": { "path": "/absolute/path/to/brand-directory" }
39
+ }
40
+ ```
41
+
42
+ Treat every validation error as blocking. A successful schema check does not replace visual, Arabic, licensing, or stakeholder review.
43
+
33
44
  This call prepares work; it does not generate, save, or publish a release.
34
45
 
35
46
  ## Present A, B, and C
@@ -76,4 +87,3 @@ Release checks:
76
87
  - Image provenance and rights metadata are present.
77
88
  - Marketing and Solution applications reflect the selected direction.
78
89
  - Brandbook validation passes before theme authoring begins.
79
-
@@ -0,0 +1,22 @@
1
+ # UX pattern package and website release — 2026-09-13
2
+
3
+ Built on current production commit b04761f, incorporating UX-only commit
4
+ 077596e and retaining existing Brandbook MCP discovery and preparation tools.
5
+
6
+ Published packages: design-system 0.11.0 and design-system-cli 0.8.0.
7
+ Includes 59 runnable UX examples, 20 semantic parts, 16 adaptation recipes,
8
+ and an evidence inventory of 85 entries / 218 fragments. Evidence entries
9
+ are not a claim that all original interactions have been implemented.
10
+
11
+ Validation: package/app builds, bundle budgets, CLI/MCP and standalone bundle
12
+ checks, semantic page ownership, and 38 desktop/mobile Chromium tests passed.
13
+ An initial browser run overlapped package rebuilding; the isolated rerun passed
14
+ all 38. Downloaded public npm tarballs verified all 59 examples, UX and existing
15
+ Brandbook MCP calls, plus all nine template commands and documented tokens.
16
+ The website release snapshot was generated from those public tarballs.
17
+
18
+ Rams quick_review and review_files were both requested, but the workspace
19
+ allowance was exhausted. No Rams score is claimed.
20
+
21
+ Production authentication/backend environment settings are preserved. This
22
+ release does not deploy Convex or include unrelated local room/auth changes.
package/lib/api.mjs CHANGED
@@ -7,6 +7,8 @@ import {
7
7
  validateLayoutDocument,
8
8
  } from './composition-runtime.mjs'
9
9
 
10
+ export { validateBrandbook } from './brandbook-validation.mjs'
11
+
10
12
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..')
11
13
  const workspaceRoot = resolve(packageRoot, '../..')
12
14
  const packagedDataRoot = join(packageRoot, 'data')
@@ -17,7 +19,7 @@ export const apiVersion = 1
17
19
  export const cliVersion = packageMetadata.version
18
20
  export const mcpLaunch = {
19
21
  command: 'npx',
20
- args: ['-y', '--package', '@utopia-studio-design/design-system-cli', 'utopia-ds', 'mcp'],
22
+ args: ['-y', '--package', `${packageMetadata.name}@${cliVersion}`, 'utopia-ds', 'mcp'],
21
23
  }
22
24
 
23
25
  export function paths(root = hasWorkspaceSource ? workspaceRoot : packagedDataRoot) {
@@ -459,7 +461,7 @@ export function capabilityManifest() {
459
461
  ],
460
462
  mcp: {
461
463
  ...mcpLaunch,
462
- tools: ['get_room_context', 'search', 'list_ux_source_patterns', 'find_ux_source_patterns', 'get_ux_source_part', 'list_ux_examples', 'get_ux_example', 'find_ui_parts', 'get_ui_part', 'prepare_ui_composition', 'list_patterns', 'get_pattern', 'prepare_pattern_adaptation', 'list_components', 'get_component', 'list_block_definitions', 'get_block_definition', 'validate_layout', 'validate_agent_layout_mutation', 'list_blocks', 'get_block', 'list_templates', 'get_template', 'list_themes', 'get_theme', 'list_motion_profiles', 'get_motion_profile', 'list_logo_skills', 'get_logo_skill', 'prepare_logo_maker_run', 'get_brandbook_skill', 'prepare_brandbook_run', 'list_docs', 'get_docs', 'doctor'],
464
+ tools: ['get_room_context', 'search', 'list_ux_source_patterns', 'find_ux_source_patterns', 'get_ux_source_part', 'list_ux_examples', 'get_ux_example', 'find_ui_parts', 'get_ui_part', 'prepare_ui_composition', 'list_patterns', 'get_pattern', 'prepare_pattern_adaptation', 'list_components', 'get_component', 'list_block_definitions', 'get_block_definition', 'validate_layout', 'validate_agent_layout_mutation', 'list_blocks', 'get_block', 'list_templates', 'get_template', 'list_themes', 'get_theme', 'list_motion_profiles', 'get_motion_profile', 'list_logo_skills', 'get_logo_skill', 'prepare_logo_maker_run', 'get_brandbook_skill', 'prepare_brandbook_run', 'validate_brandbook', 'list_docs', 'get_docs', 'doctor'],
463
465
  },
464
466
  }
465
467
  }
@@ -0,0 +1,112 @@
1
+ import { existsSync, readFileSync } from 'node:fs'
2
+ import { dirname, extname, isAbsolute, join, resolve } from 'node:path'
3
+
4
+ const chapters = ['brand', 'core', 'photography', 'motion', 'marketing', 'solution']
5
+ const directionFields = ['name', 'thesis', 'typography', 'color', 'imagery', 'motion', 'marketing', 'solution', 'fit', 'tradeoff']
6
+ const text = (value) => typeof value === 'string' && value.trim().length > 0
7
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
8
+ const issue = (code, message, field) => ({ code, message, ...(field ? { field } : {}) })
9
+
10
+ function resolveBrandPath(inputPath, root) {
11
+ const candidate = isAbsolute(inputPath) ? inputPath : resolve(root, inputPath)
12
+ return extname(candidate).toLowerCase() === '.md' ? candidate : join(candidate, 'BRAND.md')
13
+ }
14
+
15
+ function parseJson(raw, label, errors) {
16
+ try {
17
+ return JSON.parse(raw)
18
+ } catch {
19
+ errors.push(issue('INVALID_JSON', `${label} must use valid JSON syntax.`))
20
+ return null
21
+ }
22
+ }
23
+
24
+ export function validateBrandbook({ path: inputPath }, root = process.cwd()) {
25
+ const errors = []
26
+ const warnings = []
27
+ const suppliedPath = String(inputPath || '').trim()
28
+ if (!suppliedPath) {
29
+ return { ok: false, path: null, errors: [issue('PATH_REQUIRED', 'Provide a BRAND.md file or its containing directory.')], warnings }
30
+ }
31
+
32
+ const brandPath = resolveBrandPath(suppliedPath, root)
33
+ if (!existsSync(brandPath)) {
34
+ return { ok: false, path: brandPath, errors: [issue('BRAND_MD_NOT_FOUND', `BRAND.md was not found at ${brandPath}.`)], warnings }
35
+ }
36
+
37
+ const raw = readFileSync(brandPath, 'utf8')
38
+ const frontmatter = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/)
39
+ if (!frontmatter) errors.push(issue('FRONTMATTER_REQUIRED', 'BRAND.md must start with JSON frontmatter between --- delimiters.'))
40
+ const meta = frontmatter ? parseJson(frontmatter[1], 'BRAND.md frontmatter', errors) : null
41
+ if (meta) {
42
+ if (meta.schema !== 'ceramic.document/v1' || meta.id !== 'brand') errors.push(issue('INVALID_DOCUMENT_IDENTITY', 'Frontmatter must use schema ceramic.document/v1 and id brand.'))
43
+ if (!/^[a-z][a-z0-9-]{0,79}$/.test(meta.brandId || '')) errors.push(issue('INVALID_BRAND_ID', 'brandId must be a lowercase slug.', 'brandId'))
44
+ if (!/^\d+\.\d+\.\d+$/.test(meta.version || '')) errors.push(issue('INVALID_VERSION', 'version must be semantic versioning.', 'version'))
45
+ if (!['draft', 'review', 'approved'].includes(meta.status)) errors.push(issue('INVALID_STATUS', 'status must be draft, review, or approved.', 'status'))
46
+ if (!Array.isArray(meta.requiredLocales) || !meta.requiredLocales.includes('en') || !meta.requiredLocales.includes('ar')) {
47
+ errors.push(issue('REQUIRED_LOCALES', 'requiredLocales must include en and ar unless the user explicitly approved another scope.', 'requiredLocales'))
48
+ }
49
+ }
50
+
51
+ const blocks = [...raw.matchAll(/^```ceramic\s*\r?\n([\s\S]*?)^```\s*$/gm)]
52
+ if (blocks.length !== 1) errors.push(issue('CERAMIC_CONTRACT_COUNT', 'BRAND.md must contain exactly one fenced ceramic JSON contract.'))
53
+ const contract = blocks.length === 1 ? parseJson(blocks[0][1], 'Ceramic contract', errors) : null
54
+ if (contract) {
55
+ for (const chapter of chapters) {
56
+ if (!object(contract[chapter])) {
57
+ errors.push(issue('CHAPTER_CONTRACT_REQUIRED', `Missing ${chapter} contract.`, chapter))
58
+ continue
59
+ }
60
+ const decisions = contract[chapter].decisions
61
+ if (!Array.isArray(decisions) || decisions.length === 0) errors.push(issue('DECISIONS_REQUIRED', `${chapter} needs at least one decision.`, `${chapter}.decisions`))
62
+ if (chapter !== 'brand') {
63
+ const expected = ['marketing', 'solution'].includes(chapter) ? 'core' : 'brand'
64
+ if (contract[chapter].extends !== expected) errors.push(issue('INVALID_INHERITANCE', `${chapter}.extends must be ${expected}.`, `${chapter}.extends`))
65
+ }
66
+ }
67
+
68
+ const session = contract.brand?.directionSession
69
+ if (!object(session) || !Array.isArray(session.options) || session.options.length !== 3) {
70
+ errors.push(issue('DIRECTION_SESSION_REQUIRED', 'brand.directionSession must contain exactly three options.', 'brand.directionSession'))
71
+ } else {
72
+ if (session.options.map((option) => option?.id).join('') !== 'ABC') errors.push(issue('DIRECTION_ORDER', 'Direction options must be ordered A, B, C.', 'brand.directionSession.options'))
73
+ for (const option of session.options) {
74
+ if (!object(option) || !directionFields.every((field) => text(option[field]))) errors.push(issue('DIRECTION_INCOMPLETE', `Direction ${option?.id || '?'} is incomplete.`, 'brand.directionSession.options'))
75
+ }
76
+ if (!['A', 'B', 'C'].includes(session.selected)) errors.push(issue('DIRECTION_SELECTION_REQUIRED', 'A selected direction is required.', 'brand.directionSession.selected'))
77
+ if (!['refined', 'approved'].includes(session.status) || !text(session.refinement)) errors.push(issue('DIRECTION_REFINEMENT_REQUIRED', 'The selected direction must include a refinement note and refined or approved status.', 'brand.directionSession'))
78
+ }
79
+
80
+ const arabic = contract.brand?.localization?.ar
81
+ if (!object(arabic) || !text(arabic.displayFont) || !text(arabic.bodyFont) || !object(arabic.text) || Object.keys(arabic.text).length === 0) {
82
+ errors.push(issue('ARABIC_PARITY_REQUIRED', 'brand.localization.ar must provide displayFont, bodyFont, and a nonempty 1:1 text map.', 'brand.localization.ar'))
83
+ }
84
+
85
+ const showcases = contract.solution?.showcases
86
+ if (!Array.isArray(showcases) || showcases.length < 1 || showcases.length > 3) errors.push(issue('SOLUTION_SHOWCASES', 'solution.showcases must contain one to three brand-fit applications.', 'solution.showcases'))
87
+ else if (new Set(showcases.map((showcase) => showcase?.type)).size !== showcases.length || showcases.some((showcase) => !['phone', 'desktop', 'environment'].includes(showcase?.type))) {
88
+ errors.push(issue('SOLUTION_SHOWCASE_TYPES', 'Solution showcase types must be unique phone, desktop, or environment entries.', 'solution.showcases'))
89
+ }
90
+ }
91
+
92
+ const anchors = [...raw.matchAll(/^## .+ \{#([a-z]+)\}\s*$/gm)].map((match) => match[1])
93
+ for (const chapter of chapters) if (!anchors.includes(chapter)) errors.push(issue('CHAPTER_ANCHOR_REQUIRED', `Missing {#${chapter}} chapter anchor.`, chapter))
94
+
95
+ const manifestPath = join(dirname(brandPath), 'assets', 'manifest.json')
96
+ if (!existsSync(manifestPath)) errors.push(issue('ASSET_MANIFEST_REQUIRED', 'assets/manifest.json is required.'))
97
+ else {
98
+ const manifest = parseJson(readFileSync(manifestPath, 'utf8'), 'Asset manifest', errors)
99
+ if (manifest && (manifest.schema !== 'ceramic.assets/v1' || !Array.isArray(manifest.assets))) errors.push(issue('INVALID_ASSET_MANIFEST', 'Asset manifest must use ceramic.assets/v1 and contain an assets array.'))
100
+ if (manifest?.assets) {
101
+ const families = new Set(manifest.assets.filter((asset) => asset.kind === 'font').map((asset) => asset.family))
102
+ const arabic = contract?.brand?.localization?.ar
103
+ for (const role of ['displayFont', 'bodyFont']) if (text(arabic?.[role]) && !families.has(arabic[role])) errors.push(issue('ARABIC_FONT_NOT_BUNDLED', `Arabic ${role} ${arabic[role]} is not registered as a local font asset.`, `brand.localization.ar.${role}`))
104
+ for (const asset of manifest.assets) {
105
+ if (!text(asset.path) || !asset.path.startsWith('assets/') || asset.path.includes('..') || !existsSync(join(dirname(brandPath), asset.path))) errors.push(issue('ASSET_FILE_MISSING', `Manifest asset is missing or unsafe: ${asset.path || '(no path)'}.`, 'assets'))
106
+ }
107
+ }
108
+ }
109
+
110
+ if (meta?.status === 'approved') warnings.push(issue('APPROVAL_SCOPE', 'Schema validation does not replace visual, linguistic, licensing, or stakeholder approval.'))
111
+ return { ok: errors.length === 0, path: brandPath, errors, warnings, summary: { errorCount: errors.length, warningCount: warnings.length, chapters: anchors.filter((anchor) => chapters.includes(anchor)).length } }
112
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@utopia-studio-design/design-system-cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.2",
4
4
  "description": "AI-readable CLI and MCP server for Ceramic Design System",
5
5
  "ceramic": {
6
6
  "designSystem": "^0.11.0"