Skip to content

Architecture

Laith0003 edited this page May 28, 2026 · 1 revision

Architecture — the engine under the slash commands

ux-skill v2 isn't a markdown plugin with a few helper scripts. It's a Python engine — 8 modules, 11 JSON manifests, 75 tests — that the slash commands sit on top of. The CLI and the MCP server are two transport layers over the same handlers. Generation is grounded in queryable data, not in an LLM "remembering" rules from a system prompt. This page is the map.


The shape

┌──────────────────────────────────────────────────────────────────┐
│  TRANSPORT                                                        │
│  ┌───────────────────┐     ┌─────────────────────────────────┐   │
│  │  ux / uxskill     │     │  ux-mcp (stdio JSON-RPC)        │   │
│  │  click CLI        │     │  14 tools to any MCP host       │   │
│  └─────────┬─────────┘     └──────────────┬──────────────────┘   │
└────────────┼──────────────────────────────┼──────────────────────┘
             │                              │
             ▼                              ▼
┌──────────────────────────────────────────────────────────────────┐
│  HANDLERS — pure dict -> dict, no I/O                            │
│                                                                  │
│  recommender.recommend(Brief) -> Recommendation                   │
│  linter.lint(paths, threshold) -> LintReport                     │
│  discovery.next_question / record / is_complete / serialize       │
│  generator.generate(recommendation) -> tokens + manifest          │
│  persist.save_master / save_page / load_master / list_pages       │
│  installer.detect_ides(root) / install(target, root)              │
└────────────────────────────┬─────────────────────────────────────┘
                             │
                             ▼
┌──────────────────────────────────────────────────────────────────┐
│  DATA — 11 manifests, 1 brand directory                          │
│                                                                  │
│  data/anti-patterns.json     (100)                                │
│  data/brands/*.json          (110)                                │
│  data/chart-types.json       (35)                                 │
│  data/components.json        (148)                                │
│  data/industries.json        (184)                                │
│  data/landing-patterns.json  (40)                                 │
│  data/motion-presets.json    (57)                                 │
│  data/palettes.json          (176)                                │
│  data/styles.json            (84)                                 │
│  data/tech-stacks.json       (25)                                 │
│  data/type-pairs.json        (70)                                 │
│  data/ux-guidelines.json     (112)                                │
└──────────────────────────────────────────────────────────────────┘

Three layers, one responsibility each: data answers questions, handlers reason, transports talk.


The 11 JSON manifests

The knowledge base. Every recommendation, every lint pass, every generated token sheet reads from these files. They're versioned, tested, and never lie to the LLM.

Manifest Path Entries What it holds
Industries data/industries.json 184 Industry profiles with recommended styles, avoid styles, exemplar brands
Styles data/styles.json 84 Style systems with compatible palettes, type pairs, components, tokens
Palettes data/palettes.json 176 Color palettes with primary / canvas / ink / accent + WCAG metadata
Type pairs data/type-pairs.json 70 Display + body + mono triples with character + tone hints
Components data/components.json 148 UI patterns (cards, navs, hero shapes, tables) with style compatibility
Motion presets data/motion-presets.json 57 Easings, durations, choreography with prefers-reduced-motion fallbacks
Anti-patterns data/anti-patterns.json 100 Regex-detectable AI-slop fingerprints with severity, why, fix
Landing patterns data/landing-patterns.json 40 Page-level scaffolds (PMF landing, dashboard, marketing site, docs site)
Chart types data/chart-types.json 35 Dashboard visualization patterns with library hints
Tech stacks data/tech-stacks.json 25 Stack-specific output conventions and import rules
UX guidelines data/ux-guidelines.json 112 Norman / Krug / 30 Laws of UX as queryable rules

Plus data/brands/ — 110 brand DESIGN.md specs as individual JSON files. See Brand Library 110.

Total: 1031 entries across 11 manifests, plus 110 brand specs. Every entry has a stable id and is referenced by name from the engine — when a recommendation says "use the geometric-warm-essay type pair," the LLM can look it up directly.


The 8 Python modules

engine/
├── __init__.py
├── data_loader.py          # in-memory cache over data/*.json
├── recommender/core.py     # 5-parallel-search reasoning engine
├── linter/core.py          # 100-rule anti-AI-slop regex linter
├── discovery/core.py       # 10-field forcing-function intake
├── generator/core.py       # tokens.css + manifest emitter
├── persist/core.py         # MASTER.md + pages/*.md persistence
├── installer/core.py       # 17-IDE adapter writer
├── mcp/server.py           # MCP stdio transport
└── cli/main.py             # click CLI transport

Each module is self-contained, exposes a small public surface, and has its own test file under tests/.

data_loader.py

Reads the 11 manifests, caches them in-memory via lru_cache, and exposes a single load(manifest_name) entry point. load_brands() walks the brand directory. stats() returns per-manifest counts — the source for ux stats and the smoke tests.

recommender/core.py

The flagship. Takes a Brief (project type, industry, audience, tone, must-haves, forbidden, stack, region) and runs the 5-parallel-search reasoning engine. Returns a Recommendation (style, palette, type pair, motion list, components list, brand exemplars, guardrails, rationale).

The recommender is fully deterministic — same brief, same output bytes. No LLM call, no learned weights, no temperature. Full deep dive: Recommender Engine.

linter/core.py

The 100-rule anti-AI-slop linter. Reads data/anti-patterns.json, compiles each rule's regex with its declared flags, walks the target paths, and emits a LintReport with findings, file, line, column, excerpt, and fix. Runs in <50ms on a typical surface. Exit code is non-zero if any finding at or above --threshold is present. Full deep dive: Linter Rules.

discovery/core.py

The 10-field discovery flow — the forcing function. Refuses to advance the workflow until every field has an answer. FIELDS is the canonical list; next_question(state) returns the next unanswered field; record(state, field, value) saves an answer; is_complete(state) is the gate. serialize(state) writes .ux/last-discovery.json.

generator/core.py

Takes a Recommendation and emits a tokens.css plus a manifest JSON the LLM can read at output time. No HTML / JSX generation happens here — generation is the LLM's job, grounded in the tokens this module produces.

persist/core.py

Writes .ux/design-system/MASTER.md from a recommendation — the human-readable persistence layer. Frontmatter is flat YAML (three scalar keys); body is Markdown with the chosen style, palette, type pair, motion presets, brand voice, do-list and avoid-list. Idempotent: same input writes the same bytes. Individual page outputs land under .ux/design-system/pages/<page>.md.

installer/core.py

The 17-IDE adapter. detect_ides(root) walks signature files (.cursor, .windsurf, .github/copilot-instructions.md, GEMINI.md, etc.) and returns every detected IDE. install(target, root) writes the right adapter file for the target. Full deep dive: Cross-IDE Distribution.

mcp/server.py

The MCP stdio transport. 14 tools mapped to handler functions. The transport layer is optional — the mcp Python package is in the [mcp] extra. Tests exercise the handlers directly, not over stdio. Full deep dive: MCP Server.

cli/main.py

The click CLI transport. Subcommands: init, install, recommend, lint, discover, generate, persist save / load / list, stats, version. Has a click-less fallback path (python engine/cli/main.py version) for environments without click installed.


The 5-parallel-search recommender pattern

This is the heart of the engine. Given a brief, the recommender runs five domain lanes plus three auxiliary lanes against the manifests. The five domain lanes are: industry, style, palette, type, motion. The auxiliary lanes are components, brands, anti-patterns (guardrails).

Brief ─────► Industry lookup (sequential — biases everything downstream)
                  │
                  ▼
            Style lane (sequential — palette/type/motion read style.compatible_*)
                  │
        ┌─────────┼──────────────┬──────────┬─────────────────┐
        ▼         ▼              ▼          ▼                 ▼
     Palette    Type pair     Motion     Components       Brand exemplars   (parallel via
                                                                             ThreadPoolExecutor)
                                                              
                                                          Guardrails (parallel from the start)
                  │
                  ▼
           Merge -> Recommendation

Industry runs first because its recommended_styles and avoid_styles hints bias the style lane. Style runs second because palette / type / motion / components all read style.compatible_palettes, style.compatible_type_pairs, etc. From there, the five auxiliary lanes fan out in parallel through a 5-worker ThreadPoolExecutor.

Each lane has a _score() function that ranks entries against the brief. The highest score wins. Forbidden entries get a -100 penalty and are dropped before the compatibility-first sort — see Recommender Engine for the scoring rules and the v2.2 forbidden-filter fix.

The whole thing returns in well under 100ms on a typical brief. No LLM call inside the recommender — the LLM consumes the output, never participates in producing it.


State persistence — .ux/last-*.json

Commands chain through state files in the project's .ux/ directory. Each command writes its output to .ux/last-<cmd>.json; the next command reads it.

File Written by Read by
.ux/last-discovery.json ux discover ux recommend
.ux/last-recommendation.json ux recommend ux design, ux component, ux system, ux dashboard, ux copy, ux motion, ux persist save
.ux/last-lint.json ux lint ux fix
.ux/last-design.json ux design ux audit, ux polish

The state directory is local to the project. It's not committed by default — .gitignore excludes .ux/last-*.json — but the MASTER.md persistence layer (below) is committable.


MASTER.md — the human-readable persistence layer

.ux/last-recommendation.json is machine-readable; MASTER.md is its human-readable twin. ux persist save writes a <project>/.ux/design-system/MASTER.md with:

  • Flat YAML frontmatterproject, version, last_updated.
  • Body — Markdown sections for chosen style, palette, type pair, motion presets, components, brand exemplars, do-list, avoid-list, and a ## Pages persisted block listing every per-page file written under pages/.

ux persist load reads MASTER.md back into a dict. ux persist list returns the per-page files. The format is hand-rolled — no PyYAML dependency. Idempotency is enforced: when the substantive body is byte-identical, the last_updated field is re-used so the file doesn't churn on every save.

This is the layer humans review and commit. Anyone cloning the project can read MASTER.md and know what design system was recommended, why, and what's been generated against it.


The transport layer — CLI and MCP

The CLI and MCP server wrap the same handlers.

CLI (engine/cli/main.py):

ux recommend --industry fintech-neobank --tone precise --tone serious
ux lint ./src --threshold high
ux persist save --project-root .

Click subcommands parse the user's args, build the right input dataclass (Brief, paths, project root), call the handler, and emit JSON via print(json.dumps(...)) or rich-formatted output with --pretty.

MCP (engine/mcp/server.py):

{
  "tool": "ux_recommend",
  "arguments": { "brief": { "industry": "fintech-neobank", "tone": ["precise"] } }
}

The stdio server registers 14 tools, each backed by a dict -> dict handler. The transport library (mcp>=1.0) is an optional extra. Importing engine.mcp works without it — the failure is deferred to the moment you try to actually start the server.

Both transports call the same handler functions. There's no duplicated logic. Tests run the handlers directly; the transport layer is verified by smoke tests.


Why Python (and not Node, and not shell)

The v1 plugin was bash + markdown. v2 had to pick a real language for the engine. Python won for three reasons:

  1. Manifests as the source of truth. The engine is mostly data — 100 anti-pattern regex rules, 184 industry profiles, 176 palettes. Python's json module reads them at startup with zero ceremony. lru_cache makes the loader a one-liner. Type hints + dataclasses keep the handler signatures honest.
  2. Regex is the linter's core. Python's re module accepts the same patterns as the v1 shell linter (with PCRE flags mapped onto re.IGNORECASE, re.MULTILINE, re.DOTALL). The port from bin/ux-lint.sh to engine/linter/core.py was line-by-line — no rewrite, no semantic drift.
  3. MCP has a first-class Python SDK. The mcp package is the canonical reference implementation. Wrapping handlers as MCP tools is a few lines per tool — far less ceremony than building a TypeScript MCP server with its own ESM build pipeline.

Node would have meant a build step, a package-lock.json, an ESM/CJS interop story, and a TypeScript config. Shell would have meant no MCP server, no real test suite, and no Pydantic input validation on the MCP boundary. Python lets the engine be one repo, one install (pip install uxskill), one CLI binary, one MCP binary.


Tests — 75 of them

tests/
├── conftest.py
├── test_data_validation.py   # 5  — manifest schema integrity
├── test_recommender.py       # 5  — Brief -> Recommendation contract
├── test_linter.py            # 7  — regex compilation + scope matching
├── test_discovery.py         # included in test_smoke for now
├── test_generator.py         # 2  — tokens emit roundtrip
├── test_persist.py           # 5  — MASTER.md roundtrip + idempotency
├── test_installer.py         # 5  — per-IDE writer paths
├── test_mcp.py               # 5  — handlers, no transport
└── test_smoke.py             # 5  — version, manifest list, discovery shape

Run them all with pytest tests/ -q. Median run time on a clean checkout is under three seconds — the recommender is deterministic, the linter is regex, the persist layer writes to a tmp_path fixture, and the MCP tests skip the transport.


Where it goes from here

The architecture is intentionally small. Adding a sixth recommender lane, a 101st anti-pattern rule, or an 18th IDE adapter is a single-file change with a single test. The split between handlers and transports means the next transport (HTTP, gRPC, a webhook) is a thin wrapper, not a rewrite.

The data files are the contract. The Python modules are the reasoning. The CLI and MCP are the doors. None of them are the LLM — the LLM is a consumer of this engine, never a participant in it.


See also: Recommender Engine · Linter Rules · MCP Server · Cross-IDE Distribution Source: github.com/Laith0003/ux-skill/tree/main/engine

Clone this wiki locally