@urbicon-ui/mcp-server 6.1.5 → 6.1.6
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/README.md +44 -41
- package/package.json +3 -1
- package/src/data/catalog-loader.test.ts +1 -1
- package/src/data/catalog-loader.ts +12 -37
- package/src/data/component-loader.ts +5 -40
- package/src/data/design-system-loader.test.ts +1 -1
- package/src/data/design-system-loader.ts +1 -1
- package/src/data/icon-loader.test.ts +25 -80
- package/src/data/icon-loader.ts +8 -68
- package/src/data/template-loader.ts +1 -1
- package/src/data/verb-loader.ts +29 -0
- package/src/eval/eval.test.ts +16 -9
- package/src/eval/score.ts +26 -10
- package/src/index.ts +7 -14
- package/src/prompts/design-prompts.test.ts +56 -28
- package/src/prompts/design-prompts.ts +135 -104
- package/src/server.test.ts +16 -7
- package/src/server.ts +4 -7
- package/src/tools/find-components.ts +1 -1
- package/src/tools/get-design-principles.ts +1 -1
- package/src/tools/get-recipe.ts +6 -4
- package/src/tools/suggest-implementation.ts +2 -3
- package/src/tools/validate-design.ts +17 -9
- package/src/data/recipe-loader.test.ts +0 -49
- package/src/data/recipe-loader.ts +0 -131
- package/src/design-linter/heuristics.ts +0 -162
- package/src/design-linter/index.ts +0 -14
- package/src/design-linter/linter.test.ts +0 -257
- package/src/design-linter/linter.ts +0 -62
- package/src/design-linter/rules.ts +0 -348
- package/src/design-linter/tokens.test.ts +0 -80
- package/src/design-linter/tokens.ts +0 -203
- package/src/design-linter/types.ts +0 -66
- package/src/design-manifest/index.ts +0 -20
- package/src/design-manifest/manifest.test.ts +0 -175
- package/src/design-manifest/manifest.ts +0 -250
- package/src/design-manifest/scan.test.ts +0 -51
- package/src/design-manifest/scan.ts +0 -74
- package/src/design-manifest/types.ts +0 -40
- package/src/design-rubric/rubric.test.ts +0 -43
- package/src/design-rubric/rubric.ts +0 -140
- package/src/tools/get-design-context.ts +0 -43
- package/src/tools/record-design-decision.ts +0 -99
- package/src/tools/sync-design-manifest.ts +0 -92
- package/src/utils/paths.test.ts +0 -101
- package/src/utils/paths.ts +0 -78
- package/src/utils/search.test.ts +0 -141
- package/src/utils/search.ts +0 -106
package/README.md
CHANGED
|
@@ -62,23 +62,20 @@ For Cursor: see [Cursor's MCP docs](https://docs.cursor.com/context/model-contex
|
|
|
62
62
|
|
|
63
63
|
## Tools
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
| Tool | Purpose
|
|
68
|
-
| ------------------------------ |
|
|
69
|
-
| `find_components` | Fuzzy search across component names, tags, and descriptions. Filterable by package (`blocks`, `table`, `auth`).
|
|
70
|
-
| `get_component` | Full per-component documentation: props, variants, slots, examples, source link. Optional `section` argument for streamed chunks (overview / examples / variants / api / slots).
|
|
71
|
-
| `get_recipe` | Full production-ready recipe (login-form, dashboard, settings-page, etc.) with component tree, code, and notes.
|
|
72
|
-
| `suggest_implementation` | Takes a natural-language goal and returns a component-tree suggestion, relevant recipes, Style-Patterns guide, and the implementation checklist.
|
|
73
|
-
| `get_implementation_checklist` | Design-Quality checklist (visual weight, intent semantics, spacing, radius, data-driven styling, dominance, identity) — embedded directly so the LLM can self-verify.
|
|
74
|
-
| `get_css_reference` | Full token reference — surface, text, border, intent, feedback tokens, radii, z-index. Includes an explicit "do not invent tokens" guardrail.
|
|
75
|
-
| `find_icons` | Browse the
|
|
76
|
-
| `get_design_principles` | Design heuristics (Layer 5): visual hierarchy, interaction, component selection, layout, accessibility, theming (paradigms, change decision tree). Call first when generating UI. `as="rubric"` returns the 8-criterion 1–5 scoring rubric for judging a generated UI.
|
|
77
|
-
| `get_pattern` | Composition patterns (Layer 4) for page archetypes — settings-page, dashboard, form-page, tab-navigation, onboarding-guide.
|
|
78
|
-
| `validate_design` | Lint generated markup
|
|
79
|
-
| `get_design_context` | Read the project's `design.manifest.md`: chosen paradigm/theme/density, which pages use which composition patterns, and recorded design decisions (ADRs). Call at the start of a UI task to stay consistent with prior decisions. |
|
|
80
|
-
| `record_design_decision` | Append a design decision (ADR) to the manifest — record a deliberate deviation from a pattern or principle so future sessions honour it. Writes `design.manifest.md`. |
|
|
81
|
-
| `sync_design_manifest` | Scan the project source for `data-design-pattern` markers and regenerate the manifest's Pattern Usages index — makes a pattern change tractable (grep the markers → migrate every listed file). |
|
|
65
|
+
All tools are read-only (`readOnlyHint: true`) — this server never touches the consumer's filesystem. Queries are Zod-validated. (Manifest read/write moved to the `urbicon` CLI in [`@urbicon-ui/design`](../design/); a stateless remote server cannot reach a consumer's repo.)
|
|
66
|
+
|
|
67
|
+
| Tool | Purpose |
|
|
68
|
+
| ------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `find_components` | Fuzzy search across component names, tags, and descriptions. Filterable by package (`blocks`, `table`, `auth`). |
|
|
70
|
+
| `get_component` | Full per-component documentation: props, variants, slots, examples, source link. Optional `section` argument for streamed chunks (overview / examples / variants / api / slots). |
|
|
71
|
+
| `get_recipe` | Full production-ready recipe (login-form, dashboard, settings-page, etc.) with component tree, code, and notes. |
|
|
72
|
+
| `suggest_implementation` | Takes a natural-language goal and returns a component-tree suggestion, relevant recipes, Style-Patterns guide, and the implementation checklist. |
|
|
73
|
+
| `get_implementation_checklist` | Design-Quality checklist (visual weight, intent semantics, spacing, radius, data-driven styling, dominance, identity) — embedded directly so the LLM can self-verify. |
|
|
74
|
+
| `get_css_reference` | Full token reference — surface, text, border, intent, feedback tokens, radii, z-index. Includes an explicit "do not invent tokens" guardrail. |
|
|
75
|
+
| `find_icons` | Browse the 315-icon catalog by keyword, category, or name. |
|
|
76
|
+
| `get_design_principles` | Design heuristics (Layer 5): visual hierarchy, interaction, component selection, layout, accessibility, theming (paradigms, change decision tree). Call first when generating UI. `as="rubric"` returns the 8-criterion 1–5 scoring rubric for judging a generated UI. |
|
|
77
|
+
| `get_pattern` | Composition patterns (Layer 4) for page archetypes — settings-page, dashboard, form-page, tab-navigation, onboarding-guide. |
|
|
78
|
+
| `validate_design` | Lint generated markup on two axes — **correctness** (raw colours, `dark:`/`focus:` misuse, hardcoded z-index, broken dynamic classes, hallucinated tokens, foreign-library component APIs, unlabelled icon buttons; the blocking gate) and the **slop-floor** (20 system-agnostic "looks generic" heuristics: generic fonts, animated dimensions, grey-on-colour, touch targets, …; advisory). Returns a correctness score + a slop-floor score and per-finding fixes for a generate → validate → fix loop. |
|
|
82
79
|
|
|
83
80
|
## Resources
|
|
84
81
|
|
|
@@ -92,49 +89,55 @@ Most tools are read-only (`readOnlyHint: true`). The two manifest-writing tools
|
|
|
92
89
|
|
|
93
90
|
## Prompts
|
|
94
91
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
| Prompt
|
|
98
|
-
|
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
92
|
+
The full design-verb table (DESIGN-MCP-V2 §8) — client-agnostic workflows you invoke from any MCP client to run a multi-step recipe over the four design planes rather than a single-shot generation. Each recipe is the same text the local `@urbicon-ui/design` skill ships (single source, bundled via `@urbicon-ui/design-content`): it opens by reading the project's `design.manifest.md`, does the work through the read-only tools above, and closes by writing the decision back.
|
|
93
|
+
|
|
94
|
+
| Prompt | Arguments | Purpose |
|
|
95
|
+
| ---------- | ------------------------------ | ------------------------------------------------------------------------------------------ |
|
|
96
|
+
| `onboard` | `brief?` | Greenfield: interview product intent + intake, seed the manifest. |
|
|
97
|
+
| `adopt` | `brief?` | Brownfield: infer the design language from code, measure drift, seed the manifest. |
|
|
98
|
+
| `compose` | `brief?`, `variants?` | New page via generate → validate → judge → synthesise (variants + rubric + linter gate). |
|
|
99
|
+
| `redesign` | `brief?`, `code?`, `variants?` | Diagnose with linter + rubric, fix exactly the flagged weaknesses, preserve behaviour. |
|
|
100
|
+
| `polish` | `brief?`, `code?` | Small token-level fixes that raise the slop-floor score without restructuring. |
|
|
101
|
+
| `critique` | `brief?`, `code?` | Judge without changing: correctness + slop + rubric → a prioritised, verb-tagged fix-list. |
|
|
102
|
+
| `fix` | `brief?`, `code?` | Repair correctness defects (raw colours, `dark:`/`focus:`, z-index, hallucinated tokens). |
|
|
103
|
+
| `retheme` | `brief?` | Rebrand: change the token layer once, propagate across every affected file. |
|
|
104
|
+
| `audit` | `brief?` | App-wide sweep: validate the tree, check pattern cohorts, report drift over time. |
|
|
105
|
+
| `migrate` | `brief?` | Roll out a pattern/library change across every site, gated per file. |
|
|
101
106
|
|
|
102
107
|
## CLI Options
|
|
103
108
|
|
|
104
109
|
```
|
|
105
|
-
urbicon-mcp [--transport <stdio|http>] [--port <n>] [--
|
|
110
|
+
urbicon-mcp [--transport <stdio|http>] [--port <n>] [--content-dir <path>]
|
|
106
111
|
```
|
|
107
112
|
|
|
108
|
-
| Flag
|
|
109
|
-
|
|
|
110
|
-
| `--transport`
|
|
111
|
-
| `--port`
|
|
112
|
-
| `--
|
|
113
|
+
| Flag | Default | Purpose |
|
|
114
|
+
| --------------- | ------------- | -------------------------------------------------------------- |
|
|
115
|
+
| `--transport` | `stdio` | Transport mode |
|
|
116
|
+
| `--port` | `3001` | HTTP port (ignored for stdio) |
|
|
117
|
+
| `--content-dir` | auto-discover | Override the design-content bundle dir (`URBICON_CONTENT_DIR`) |
|
|
113
118
|
|
|
114
119
|
## Architecture
|
|
115
120
|
|
|
116
121
|
```
|
|
117
122
|
src/
|
|
118
|
-
├── index.ts CLI entry + arg parsing, pre-loads catalog/templates/
|
|
123
|
+
├── index.ts CLI entry + arg parsing, pre-loads catalog/templates/principles
|
|
119
124
|
├── server.ts MCP server construction (registers resources + tools)
|
|
120
125
|
├── transports/
|
|
121
126
|
│ ├── stdio.ts Stdio transport
|
|
122
127
|
│ └── http.ts Streamable HTTP transport (per-session)
|
|
123
|
-
├── tools/
|
|
124
|
-
|
|
125
|
-
├── design-manifest/ design.manifest.md parse/scan/edit for the design-context tools
|
|
128
|
+
├── tools/ 10 tools, each self-contained
|
|
129
|
+
│ (validate_design calls @urbicon-ui/design-engine)
|
|
126
130
|
├── resources/ Catalog + guide resources
|
|
127
|
-
├── data/ Loaders with in-process caching
|
|
128
|
-
│ ├── catalog-loader.ts component-catalog.json
|
|
129
|
-
│ ├── template-loader.ts
|
|
130
|
-
│ ├── recipe-loader.ts Recipes
|
|
131
|
+
├── data/ Loaders with in-process caching (read the design-content bundle)
|
|
132
|
+
│ ├── catalog-loader.ts component-catalog.json (components + recipes)
|
|
133
|
+
│ ├── template-loader.ts guide template sections
|
|
131
134
|
│ ├── design-system-loader.ts principles.md + patterns/*.md
|
|
132
|
-
│ ├── component-loader.ts
|
|
133
|
-
│ └── icon-loader.ts
|
|
134
|
-
└── utils/ search, format-catalog
|
|
135
|
+
│ ├── component-loader.ts per-component llm.txt
|
|
136
|
+
│ └── icon-loader.ts icons.json
|
|
137
|
+
└── utils/ search, format-catalog
|
|
135
138
|
```
|
|
136
139
|
|
|
137
|
-
The server reads its data from
|
|
140
|
+
The server reads its data from the version-pinned [`@urbicon-ui/design-content`](../design-content/) bundle (built by [`@urbicon-ui/docs-gen`](../docs-gen/): `component-catalog.json` with recipes, per-component `llm.txt`, design-system, guide template, `icons.json`). That means JSDoc in a component's `index.ts` is the **single source of truth**: one edit propagates to the docs site, `llms-full.txt`, and every MCP tool.
|
|
138
141
|
|
|
139
142
|
## Development
|
|
140
143
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/mcp-server",
|
|
3
|
-
"version": "6.1.
|
|
3
|
+
"version": "6.1.6",
|
|
4
4
|
"description": "Model Context Protocol server exposing the Urbicon UI component catalog, recipes and design intelligence to LLM agents",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
35
|
+
"@urbicon-ui/design-content": "6.1.6",
|
|
36
|
+
"@urbicon-ui/design-engine": "6.1.6",
|
|
35
37
|
"zod": "^4.3.6"
|
|
36
38
|
},
|
|
37
39
|
"devDependencies": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
+
import { getCatalogPath } from '@urbicon-ui/design-content';
|
|
2
3
|
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { getCatalogPath } from '../utils/paths.js';
|
|
4
4
|
import { getCachedCatalog, loadCatalog } from './catalog-loader.js';
|
|
5
5
|
|
|
6
6
|
// Integration test: exercises the real catalog JSON produced by docs-gen.
|
|
@@ -1,42 +1,17 @@
|
|
|
1
1
|
import { watch } from 'node:fs';
|
|
2
2
|
import { readFile } from 'node:fs/promises';
|
|
3
|
-
import { getCatalogPath } from '
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
keyProps: string[];
|
|
16
|
-
keyPropTypes: Record<string, string>;
|
|
17
|
-
slots: string[];
|
|
18
|
-
hasExamples: boolean;
|
|
19
|
-
relatedComponents: string[];
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface RecipeEntry {
|
|
23
|
-
id: string;
|
|
24
|
-
title: string;
|
|
25
|
-
description: string;
|
|
26
|
-
components: string[];
|
|
27
|
-
code: string;
|
|
28
|
-
features: string[];
|
|
29
|
-
/** Layer-4 composition pattern this recipe is an instance of (e.g. "dashboard"). Cross-links to `get_pattern`. */
|
|
30
|
-
pattern?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface ComponentCatalog {
|
|
34
|
-
generated: string;
|
|
35
|
-
version: string;
|
|
36
|
-
components: ComponentCatalogEntry[];
|
|
37
|
-
recipes: RecipeEntry[];
|
|
38
|
-
tags: string[];
|
|
39
|
-
}
|
|
3
|
+
import { getCatalogPath } from '@urbicon-ui/design-content';
|
|
4
|
+
import type {
|
|
5
|
+
ComponentCatalog,
|
|
6
|
+
ComponentCatalogEntry,
|
|
7
|
+
RecipeEntry
|
|
8
|
+
} from '@urbicon-ui/design-engine/search';
|
|
9
|
+
|
|
10
|
+
// The catalog schema lives in the engine now (DESIGN-MCP-V2 §5) so the `urbicon`
|
|
11
|
+
// CLI's find/get-component and this server share one authoritative type. Imported
|
|
12
|
+
// above for the loader below; re-exported here for the server's many local
|
|
13
|
+
// importers (tools, resources, format-catalog) that still source it from here.
|
|
14
|
+
export type { ComponentCatalog, ComponentCatalogEntry, RecipeEntry };
|
|
40
15
|
|
|
41
16
|
let cachedCatalog: ComponentCatalog | null = null;
|
|
42
17
|
let watcherInitialized = false;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { getComponentLlmPath } from '
|
|
2
|
+
import { getComponentLlmPath } from '@urbicon-ui/design-content';
|
|
3
3
|
|
|
4
4
|
const SEARCH_GROUPS = [
|
|
5
5
|
'blocks/primitives',
|
|
@@ -27,42 +27,7 @@ export async function loadComponentLlmTxt(slug: string): Promise<string | null>
|
|
|
27
27
|
return null;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
variants: 'variants',
|
|
35
|
-
api: 'api',
|
|
36
|
-
'slots (slotclasses keys)': 'slots'
|
|
37
|
-
};
|
|
38
|
-
|
|
39
|
-
export function extractSection(content: string, section: LlmTxtSection): string | null {
|
|
40
|
-
if (section === 'overview') {
|
|
41
|
-
const firstH3 = content.indexOf('\n### ');
|
|
42
|
-
if (firstH3 === -1) return content.trim();
|
|
43
|
-
return content.slice(0, firstH3).trim();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const lines = content.split('\n');
|
|
47
|
-
let capturing = false;
|
|
48
|
-
const result: string[] = [];
|
|
49
|
-
|
|
50
|
-
for (const line of lines) {
|
|
51
|
-
if (line.startsWith('### ')) {
|
|
52
|
-
const heading = line.slice(4).trim().toLowerCase();
|
|
53
|
-
const mapped = SECTION_HEADING_MAP[heading];
|
|
54
|
-
if (mapped === section) {
|
|
55
|
-
capturing = true;
|
|
56
|
-
result.push(line);
|
|
57
|
-
continue;
|
|
58
|
-
} else if (capturing) {
|
|
59
|
-
break;
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (capturing) {
|
|
63
|
-
result.push(line);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return result.length > 0 ? result.join('\n').trim() : null;
|
|
68
|
-
}
|
|
30
|
+
// The section parser lives in the engine now (DESIGN-MCP-V2 §5) so this server and
|
|
31
|
+
// the `urbicon` CLI extract llm.txt sections identically. Re-exported here for the
|
|
32
|
+
// server's local importers (get-component, get-recipe, …).
|
|
33
|
+
export { extractSection, type LlmTxtSection } from '@urbicon-ui/design-engine/search';
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
+
import { getDesignSystemDir } from '@urbicon-ui/design-content';
|
|
3
4
|
import { describe, expect, it } from 'vitest';
|
|
4
|
-
import { getDesignSystemDir } from '../utils/paths.js';
|
|
5
5
|
import {
|
|
6
6
|
extractPrincipleSection,
|
|
7
7
|
getPatternByName,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
|
-
import { getDesignSystemDir } from '
|
|
3
|
+
import { getDesignSystemDir } from '@urbicon-ui/design-content';
|
|
4
4
|
|
|
5
5
|
export interface PatternEntry {
|
|
6
6
|
name: string;
|
|
@@ -1,85 +1,30 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { getIconsPath } from '@urbicon-ui/design-content';
|
|
1
3
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
`;
|
|
22
|
-
|
|
23
|
-
describe('parseComponentNames', () => {
|
|
24
|
-
it('builds a name → ComponentName map from DEFAULT_ICONS', () => {
|
|
25
|
-
const map = parseComponentNames(FIXTURE);
|
|
26
|
-
expect(map.get('home')).toBe('HomeIcon');
|
|
27
|
-
expect(map.get('search')).toBe('SearchIcon');
|
|
28
|
-
expect(map.get('arrowRight')).toBe('ArrowRightIcon');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('returns an empty map when the DEFAULT_ICONS block is missing', () => {
|
|
32
|
-
const map = parseComponentNames('const NOT_IT = {};');
|
|
33
|
-
expect(map.size).toBe(0);
|
|
34
|
-
});
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
describe('parseIconMetadata', () => {
|
|
38
|
-
it('extracts label, categories, and keywords per entry', () => {
|
|
39
|
-
const entries = parseIconMetadata(FIXTURE);
|
|
40
|
-
expect(entries).toHaveLength(3);
|
|
41
|
-
|
|
42
|
-
const home = entries.find((e) => e.name === 'home');
|
|
43
|
-
expect(home).toMatchObject({
|
|
44
|
-
name: 'home',
|
|
45
|
-
componentName: 'HomeIcon',
|
|
46
|
-
label: 'Home',
|
|
47
|
-
categories: ['navigation'],
|
|
48
|
-
keywords: ['house', 'start']
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('uses the DEFAULT_ICONS mapping for componentName when available', () => {
|
|
53
|
-
const entries = parseIconMetadata(FIXTURE);
|
|
54
|
-
const search = entries.find((e) => e.name === 'search');
|
|
55
|
-
expect(search?.componentName).toBe('SearchIcon');
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('falls back to a capitalised name + "Icon" suffix when no mapping exists', () => {
|
|
59
|
-
const withoutMapping = `
|
|
60
|
-
export const ICON_METADATA = {
|
|
61
|
-
custom: { label: 'Custom', categories: ['x'], keywords: ['y'] }
|
|
62
|
-
};
|
|
63
|
-
`;
|
|
64
|
-
const entries = parseIconMetadata(withoutMapping);
|
|
65
|
-
expect(entries[0]?.componentName).toBe('CustomIcon');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('returns an empty array when ICON_METADATA block is missing', () => {
|
|
69
|
-
expect(parseIconMetadata('// nothing to see')).toEqual([]);
|
|
4
|
+
import { loadIcons } from './icon-loader.js';
|
|
5
|
+
|
|
6
|
+
// Integration test: exercises the real icons.json from the content bundle. The
|
|
7
|
+
// registry-parsing logic itself is unit-tested in docs-gen (icons.test.ts); here we
|
|
8
|
+
// only assert the loader reads + shapes the bundled JSON. Skipped on a fresh checkout
|
|
9
|
+
// before the bundle has been generated.
|
|
10
|
+
const iconsAvailable = existsSync(getIconsPath());
|
|
11
|
+
|
|
12
|
+
describe.skipIf(!iconsAvailable)('icon-loader integration', () => {
|
|
13
|
+
it('loads icons with the expected shape', async () => {
|
|
14
|
+
const icons = await loadIcons();
|
|
15
|
+
expect(icons.length).toBeGreaterThan(0);
|
|
16
|
+
for (const icon of icons) {
|
|
17
|
+
expect(icon.name).toBeTruthy();
|
|
18
|
+
expect(icon.componentName).toMatch(/Icon$/);
|
|
19
|
+
expect(typeof icon.label).toBe('string');
|
|
20
|
+
expect(Array.isArray(icon.categories)).toBe(true);
|
|
21
|
+
expect(Array.isArray(icon.keywords)).toBe(true);
|
|
22
|
+
}
|
|
70
23
|
});
|
|
71
24
|
|
|
72
|
-
it('
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
`;
|
|
78
|
-
const entries = parseIconMetadata(fixture);
|
|
79
|
-
expect(entries[0]).toMatchObject({
|
|
80
|
-
name: 'minimal',
|
|
81
|
-
categories: [],
|
|
82
|
-
keywords: []
|
|
83
|
-
});
|
|
25
|
+
it('caches between invocations', async () => {
|
|
26
|
+
const first = await loadIcons();
|
|
27
|
+
const second = await loadIcons();
|
|
28
|
+
expect(second).toBe(first);
|
|
84
29
|
});
|
|
85
30
|
});
|
package/src/data/icon-loader.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import {
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
|
|
5
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const packageRoot = resolve(__dirname, '..', '..');
|
|
2
|
+
import { getIconsPath } from '@urbicon-ui/design-content';
|
|
7
3
|
|
|
8
4
|
export interface IconEntry {
|
|
9
5
|
name: string;
|
|
@@ -15,73 +11,17 @@ export interface IconEntry {
|
|
|
15
11
|
|
|
16
12
|
let cachedIcons: IconEntry[] | null = null;
|
|
17
13
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
/** Parse DEFAULT_ICONS to get the real name → ComponentName mapping */
|
|
25
|
-
export function parseComponentNames(content: string): Map<string, string> {
|
|
26
|
-
const map = new Map<string, string>();
|
|
27
|
-
const start = content.indexOf('DEFAULT_ICONS');
|
|
28
|
-
if (start === -1) return map;
|
|
29
|
-
|
|
30
|
-
const end = content.indexOf('};', start);
|
|
31
|
-
if (end === -1) return map;
|
|
32
|
-
|
|
33
|
-
const block = content.slice(start, end);
|
|
34
|
-
const regex = /(\w+):\s*(\w+Icon)/g;
|
|
35
|
-
for (const match of block.matchAll(regex)) {
|
|
36
|
-
map.set(match[1]!, match[2]!);
|
|
37
|
-
}
|
|
38
|
-
return map;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export function parseIconMetadata(content: string): IconEntry[] {
|
|
42
|
-
const componentNames = parseComponentNames(content);
|
|
43
|
-
const entries: IconEntry[] = [];
|
|
44
|
-
|
|
45
|
-
const metadataStart = content.indexOf('ICON_METADATA');
|
|
46
|
-
if (metadataStart === -1) return [];
|
|
47
|
-
|
|
48
|
-
const entryRegex =
|
|
49
|
-
/(\w+):\s*\{\s*label:\s*'([^']*)',\s*categories:\s*\[([^\]]*)\],\s*keywords:\s*\[([^\]]*)\]\s*\}/g;
|
|
50
|
-
const block = content.slice(metadataStart);
|
|
51
|
-
|
|
52
|
-
for (const match of block.matchAll(entryRegex)) {
|
|
53
|
-
const name = match[1]!;
|
|
54
|
-
const label = match[2]!;
|
|
55
|
-
const categories = match[3]!
|
|
56
|
-
.split(',')
|
|
57
|
-
.map((s) => s.trim().replace(/^'|'$/g, ''))
|
|
58
|
-
.filter((s) => s.length > 0);
|
|
59
|
-
const keywords = match[4]!
|
|
60
|
-
.split(',')
|
|
61
|
-
.map((s) => s.trim().replace(/^'|'$/g, ''))
|
|
62
|
-
.filter((s) => s.length > 0);
|
|
63
|
-
|
|
64
|
-
const componentName =
|
|
65
|
-
componentNames.get(name) ?? `${name.charAt(0).toUpperCase() + name.slice(1)}Icon`;
|
|
66
|
-
|
|
67
|
-
entries.push({
|
|
68
|
-
name,
|
|
69
|
-
componentName,
|
|
70
|
-
label,
|
|
71
|
-
categories,
|
|
72
|
-
keywords
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
return entries;
|
|
77
|
-
}
|
|
78
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Load the bundled icon metadata (`icons.json`, emitted by docs-gen from the blocks
|
|
16
|
+
* icon registry). Read-tolerant: an absent bundle yields an empty set so `find_icons`
|
|
17
|
+
* degrades gracefully instead of crashing the server.
|
|
18
|
+
*/
|
|
79
19
|
export async function loadIcons(): Promise<IconEntry[]> {
|
|
80
20
|
if (cachedIcons) return cachedIcons;
|
|
81
21
|
|
|
82
22
|
try {
|
|
83
|
-
const
|
|
84
|
-
cachedIcons =
|
|
23
|
+
const raw = await readFile(getIconsPath(), 'utf-8');
|
|
24
|
+
cachedIcons = JSON.parse(raw) as IconEntry[];
|
|
85
25
|
} catch {
|
|
86
26
|
cachedIcons = [];
|
|
87
27
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { getVerbsDir } from '@urbicon-ui/design-content';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Loads design-verb recipes from the version-pinned content bundle
|
|
7
|
+
* (`@urbicon-ui/design-content/content/verbs/`). The recipes are the single source
|
|
8
|
+
* the local skill ships and these remote prompts serve — same text, two channels
|
|
9
|
+
* (DESIGN-MCP-V2 §8). Cached per process; read tolerant (a missing file yields the
|
|
10
|
+
* empty string, so one absent recipe never breaks server construction — the prompt
|
|
11
|
+
* wrapper degrades to a rebuild hint instead).
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const cache = new Map<string, string>();
|
|
15
|
+
|
|
16
|
+
/** Load one verb recipe body by name (e.g. `compose`), trimmed. `''` when absent. */
|
|
17
|
+
export async function loadVerb(name: string): Promise<string> {
|
|
18
|
+
const cached = cache.get(name);
|
|
19
|
+
if (cached !== undefined) return cached;
|
|
20
|
+
|
|
21
|
+
let body = '';
|
|
22
|
+
try {
|
|
23
|
+
body = (await readFile(resolve(getVerbsDir(), `${name}.md`), 'utf-8')).trim();
|
|
24
|
+
} catch {
|
|
25
|
+
body = '';
|
|
26
|
+
}
|
|
27
|
+
cache.set(name, body);
|
|
28
|
+
return body;
|
|
29
|
+
}
|
package/src/eval/eval.test.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { RUBRIC_CRITERIA } from '@urbicon-ui/design-engine/rubric';
|
|
1
2
|
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { RUBRIC_CRITERIA } from '../design-rubric/rubric.js';
|
|
3
3
|
import { EVAL_BRIEFS, getBriefById } from './briefs.js';
|
|
4
4
|
import type { EvalEntry } from './score.js';
|
|
5
5
|
import { aggregateRubric, formatAbReport, scoreImplementation } from './score.js';
|
|
@@ -28,14 +28,15 @@ describe('eval briefs', () => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
describe('scoreImplementation', () => {
|
|
31
|
-
it('scores clean code 100 with no findings', () => {
|
|
31
|
+
it('scores clean code 100/100 with no findings', () => {
|
|
32
32
|
const s = scoreImplementation('<div class="bg-surface-base text-text-primary">ok</div>');
|
|
33
|
-
expect(s.
|
|
33
|
+
expect(s.correctness).toBe(100);
|
|
34
|
+
expect(s.slop).toBe(100);
|
|
34
35
|
expect(s.errors).toBe(0);
|
|
35
36
|
});
|
|
36
|
-
it('penalises hallucinated tokens and raw colours', () => {
|
|
37
|
+
it('penalises hallucinated tokens and raw colours on the correctness axis', () => {
|
|
37
38
|
const s = scoreImplementation('<div class="bg-blue-500 text-status-bad">x</div>');
|
|
38
|
-
expect(s.
|
|
39
|
+
expect(s.correctness).toBeLessThan(100);
|
|
39
40
|
expect(s.errors + s.warnings).toBeGreaterThan(0);
|
|
40
41
|
});
|
|
41
42
|
});
|
|
@@ -61,12 +62,12 @@ describe('formatAbReport edge cases', () => {
|
|
|
61
62
|
{
|
|
62
63
|
briefId: 'x',
|
|
63
64
|
condition: 'baseline',
|
|
64
|
-
score: { linter: {
|
|
65
|
+
score: { linter: { correctness: 100, slop: 100, errors: 0, warnings: 0, infos: 0 } }
|
|
65
66
|
},
|
|
66
67
|
{
|
|
67
68
|
briefId: 'x',
|
|
68
69
|
condition: 'design-mcp',
|
|
69
|
-
score: { linter: {
|
|
70
|
+
score: { linter: { correctness: 100, slop: 100, errors: 0, warnings: 0, infos: 0 } }
|
|
70
71
|
}
|
|
71
72
|
];
|
|
72
73
|
const report = formatAbReport(entries, 'baseline', 'design-mcp');
|
|
@@ -79,12 +80,18 @@ describe('formatAbReport', () => {
|
|
|
79
80
|
{
|
|
80
81
|
briefId: 'a',
|
|
81
82
|
condition: 'baseline',
|
|
82
|
-
score: {
|
|
83
|
+
score: {
|
|
84
|
+
linter: { correctness: 70, slop: 80, errors: 1, warnings: 2, infos: 0 },
|
|
85
|
+
rubricTotal: 22
|
|
86
|
+
}
|
|
83
87
|
},
|
|
84
88
|
{
|
|
85
89
|
briefId: 'a',
|
|
86
90
|
condition: 'design-mcp',
|
|
87
|
-
score: {
|
|
91
|
+
score: {
|
|
92
|
+
linter: { correctness: 95, slop: 90, errors: 0, warnings: 0, infos: 1 },
|
|
93
|
+
rubricTotal: 31
|
|
94
|
+
}
|
|
88
95
|
}
|
|
89
96
|
];
|
|
90
97
|
const report = formatAbReport(entries, 'baseline', 'design-mcp');
|