@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.
Files changed (48) hide show
  1. package/README.md +44 -41
  2. package/package.json +3 -1
  3. package/src/data/catalog-loader.test.ts +1 -1
  4. package/src/data/catalog-loader.ts +12 -37
  5. package/src/data/component-loader.ts +5 -40
  6. package/src/data/design-system-loader.test.ts +1 -1
  7. package/src/data/design-system-loader.ts +1 -1
  8. package/src/data/icon-loader.test.ts +25 -80
  9. package/src/data/icon-loader.ts +8 -68
  10. package/src/data/template-loader.ts +1 -1
  11. package/src/data/verb-loader.ts +29 -0
  12. package/src/eval/eval.test.ts +16 -9
  13. package/src/eval/score.ts +26 -10
  14. package/src/index.ts +7 -14
  15. package/src/prompts/design-prompts.test.ts +56 -28
  16. package/src/prompts/design-prompts.ts +135 -104
  17. package/src/server.test.ts +16 -7
  18. package/src/server.ts +4 -7
  19. package/src/tools/find-components.ts +1 -1
  20. package/src/tools/get-design-principles.ts +1 -1
  21. package/src/tools/get-recipe.ts +6 -4
  22. package/src/tools/suggest-implementation.ts +2 -3
  23. package/src/tools/validate-design.ts +17 -9
  24. package/src/data/recipe-loader.test.ts +0 -49
  25. package/src/data/recipe-loader.ts +0 -131
  26. package/src/design-linter/heuristics.ts +0 -162
  27. package/src/design-linter/index.ts +0 -14
  28. package/src/design-linter/linter.test.ts +0 -257
  29. package/src/design-linter/linter.ts +0 -62
  30. package/src/design-linter/rules.ts +0 -348
  31. package/src/design-linter/tokens.test.ts +0 -80
  32. package/src/design-linter/tokens.ts +0 -203
  33. package/src/design-linter/types.ts +0 -66
  34. package/src/design-manifest/index.ts +0 -20
  35. package/src/design-manifest/manifest.test.ts +0 -175
  36. package/src/design-manifest/manifest.ts +0 -250
  37. package/src/design-manifest/scan.test.ts +0 -51
  38. package/src/design-manifest/scan.ts +0 -74
  39. package/src/design-manifest/types.ts +0 -40
  40. package/src/design-rubric/rubric.test.ts +0 -43
  41. package/src/design-rubric/rubric.ts +0 -140
  42. package/src/tools/get-design-context.ts +0 -43
  43. package/src/tools/record-design-decision.ts +0 -99
  44. package/src/tools/sync-design-manifest.ts +0 -92
  45. package/src/utils/paths.test.ts +0 -101
  46. package/src/utils/paths.ts +0 -78
  47. package/src/utils/search.test.ts +0 -141
  48. 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
- Most tools are read-only (`readOnlyHint: true`). The two manifest-writing tools (`record_design_decision`, `sync_design_manifest`) write `design.manifest.md` in the project root. Queries are Zod-validated.
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 156-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 against the design rules — raw colours, `dark:`/`focus:` misuse, hardcoded z-index, broken dynamic classes, hallucinated tokens, plus distribution heuristics. Returns a 0–100 score and per-finding fixes for a generate → validate → fix loop. |
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
- Client-agnostic workflows (Option E of the design loop) invoke them from any MCP client to run the full generate validate judge synthesise process rather than a single-shot generation.
96
-
97
- | Prompt | Arguments | Purpose |
98
- | ------------- | -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
99
- | `design-page` | `brief`, `pattern?`, `variants?` | Design a new page: load context + pattern, explore N variants within the paradigm, gate each on `validate_design`, score with the rubric, synthesise the winner. |
100
- | `redesign` | `brief`, `code?`, `variants?` | Redesign an existing page: diagnose with `validate_design` + the rubric, then fix exactly the flagged weaknesses while preserving behaviour. |
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>] [--data-dir <path>]
110
+ urbicon-mcp [--transport <stdio|http>] [--port <n>] [--content-dir <path>]
106
111
  ```
107
112
 
108
- | Flag | Default | Purpose |
109
- | ------------- | ------------- | ------------------------------------------------------------ |
110
- | `--transport` | `stdio` | Transport mode |
111
- | `--port` | `3001` | HTTP port (ignored for stdio) |
112
- | `--data-dir` | auto-discover | Override the path to the generated catalog/templates/recipes |
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/recipes
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/ 13 tools, each self-contained
124
- ├── design-linter/ validate_design engine: rules, token whitelist, heuristics (pure, unit-tested)
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 llms.txt sections
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, paths
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 artifacts produced by [`@urbicon-ui/docs-gen`](../docs-gen/) (`component-catalog.json`, per-component `llms.txt`, recipes). 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.
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.5",
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 '../utils/paths.js';
4
-
5
- export interface ComponentCatalogEntry {
6
- name: string;
7
- slug: string;
8
- package: string;
9
- group: 'primitives' | 'components' | 'core' | 'auth';
10
- description: string;
11
- tags: string[];
12
- import: string;
13
- llmTxtPath: string;
14
- variants: { name: string; values: string[]; default?: string }[];
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 '../utils/paths.js';
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
- export type LlmTxtSection = 'overview' | 'examples' | 'variants' | 'api' | 'slots';
31
-
32
- const SECTION_HEADING_MAP: Record<string, LlmTxtSection> = {
33
- examples: 'examples',
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 '../utils/paths.js';
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 { parseComponentNames, parseIconMetadata } from './icon-loader.js';
3
-
4
- const FIXTURE = `
5
- // Simulated icon.context.ts content
6
- import type { Component } from 'svelte';
7
- import HomeIcon from './HomeIcon.svelte';
8
- import SearchIcon from './SearchIcon.svelte';
9
-
10
- export const DEFAULT_ICONS = {
11
- home: HomeIcon,
12
- search: SearchIcon,
13
- arrowRight: ArrowRightIcon
14
- };
15
-
16
- export const ICON_METADATA = {
17
- home: { label: 'Home', categories: ['navigation'], keywords: ['house', 'start'] },
18
- search: { label: 'Search', categories: ['navigation', 'ui'], keywords: ['find', 'lookup'] },
19
- arrowRight: { label: 'Arrow Right', categories: ['navigation'], keywords: [] }
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('handles empty categories and keywords arrays gracefully', () => {
73
- const fixture = `
74
- export const ICON_METADATA = {
75
- minimal: { label: 'Minimal', categories: [], keywords: [] }
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
  });
@@ -1,9 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { dirname, resolve } from 'node:path';
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
- function getIconRegistryPath(): string {
19
- // DEFAULT_ICONS + ICON_METADATA live in icon-registry.ts since the icon module
20
- // split (icon.context.ts now holds only the override context + resolveIcon).
21
- return resolve(packageRoot, '..', 'blocks', 'src', 'lib', 'icons', 'icon-registry.ts');
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 content = await readFile(getIconRegistryPath(), 'utf-8');
84
- cachedIcons = parseIconMetadata(content);
23
+ const raw = await readFile(getIconsPath(), 'utf-8');
24
+ cachedIcons = JSON.parse(raw) as IconEntry[];
85
25
  } catch {
86
26
  cachedIcons = [];
87
27
  }
@@ -1,5 +1,5 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { getTemplatePath } from '../utils/paths.js';
2
+ import { getTemplatePath } from '@urbicon-ui/design-content';
3
3
 
4
4
  export interface TemplateSections {
5
5
  'api-grammar': string;
@@ -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
+ }
@@ -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.score).toBe(100);
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.score).toBeLessThan(100);
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: { score: 100, errors: 0, warnings: 0, infos: 0 } }
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: { score: 100, errors: 0, warnings: 0, infos: 0 } }
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: { linter: { score: 70, errors: 1, warnings: 2, infos: 0 }, rubricTotal: 22 }
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: { linter: { score: 95, errors: 0, warnings: 0, infos: 1 }, rubricTotal: 31 }
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');