# Magic Modules Patterns Reference

Detailed reference for the Magic Modules system: spec format, freshness protocol, debate protocol, registry schema, design tokens, MCP tools, worked examples, and troubleshooting.

---

## 1. Origin and Credits

Magic Modules is an adaptation of two Google Labs experiments. The spec format, the debate protocol, and the integration with Loki Mode's RARV cycle are re-implemented for this codebase. Credit for the underlying ideas belongs to the original authors.

| Project | Author | Contribution | Link |
|---------|--------|--------------|------|
| MagicModules | Roman Nurik (Google Labs) | Spec-first component generation, markdown specs as source of truth, SHA-based freshness check, single-source registry | [github.com/romannurik/MagicModules](https://github.com/romannurik/MagicModules) |
| MoMoA | Reto Meier (Google Labs) | Multi-persona debate, conflicting expert critique, consensus with HITL escalation | [github.com/retomeier/momoa](https://github.com/retomeier/momoa) |

This adaptation is not a copy. Differences from the originals:

- Specs are stored per project in `.loki/magic/specs/` rather than a global registry
- Generation supports both React and Web Components (the originals target one stack at a time)
- Debate integrates with Loki's HITL system and dashboard notifications
- Registry entries participate in the Loki knowledge graph and memory consolidation

---

## 2. Spec Format Reference

Specs are authored in Markdown with a YAML front matter block. The spec is the source of truth -- the implementation is a derived artifact.

### Template

```markdown
---
name: Button
target: both              # react | webcomponent | both
version: 1
tags: [form, action, primary]
design_tokens:
  color_primary: token.color.accent
  radius: token.radius.md
  font: token.font.body
a11y_level: AA            # A | AA | AAA
stability: stable         # experimental | beta | stable
owner: team-ui
---

# Button

## Purpose
A primary action trigger. Used when a user needs to commit to a single most-important action on a screen.

## Props
| Name | Type | Default | Description |
|------|------|---------|-------------|
| `label` | string | required | Visible label text |
| `onClick` | function | required | Handler for activation |
| `variant` | 'primary' \| 'secondary' \| 'ghost' | 'primary' | Visual weight |
| `loading` | boolean | false | Shows spinner, disables click |
| `disabled` | boolean | false | Non-interactive state |
| `iconLeft` | ReactNode \| slot | null | Optional leading icon |

## Behavior
- Click or Enter or Space activates the button
- While `loading` is true, clicks are suppressed and a spinner replaces `iconLeft`
- `disabled` sets `aria-disabled=true` and blocks pointer events
- The component never manages its own loading state -- it is controlled via the `loading` prop

## Accessibility
- Uses a native `<button>` element (React) or `role="button"` (WC)
- Focus ring is visible and uses `token.color.focus`
- `loading` announces via `aria-busy=true`
- `disabled` uses `aria-disabled` rather than the `disabled` attribute to keep focus reachable

## Visual
- Height: token.spacing.height.md
- Padding: token.spacing.x.md
- Radius: token.radius.md
- Primary variant uses token.color.accent on token.color.accent-contrast
- Transitions respect `prefers-reduced-motion`

## Test Expectations
- Renders the label
- Calls onClick on click, Enter, and Space
- Does not call onClick when `loading` or `disabled`
- Announces busy state to screen readers when loading
- Focus ring visible when keyboard-focused

## Notes
<!-- Free-form notes for downstream maintainers -->
```

### Spec Authoring Rules

1. The YAML block is required. Missing fields fail the generation gate.
2. The `## Props` table drives TypeScript types. Column order is fixed: name, type, default, description.
3. Any token reference must be of the form `token.*` and must resolve through the token resolution order.
4. The `a11y_level` field is consumed by the A11y Advocate persona to calibrate severity.
5. Free-form `## Notes` sections are preserved in the generated file as a header comment block for maintainers.

---

## 3. Freshness Protocol

Every spec has a content hash. Every generated file carries that hash. Regeneration is a function of equality.

### Hash Computation

```
spec_hash = sha256(
  normalize(front_matter_yaml) + "\n" +
  strip_comments(markdown_body) + "\n" +
  serialize(resolved_tokens)
)[:12]
```

Normalization removes whitespace differences and sorts map keys so cosmetic edits don't trigger regeneration. Token resolution is included so a token change invalidates dependents.

### Header Marker

Every generated file begins with:

```typescript
// LOKI-MAGIC-HASH: a1b2c3d4e5f6
// Component: Button (target=react, spec version=1)
// Generated by loki magic on 2026-04-18T10:00:00Z
// Do not edit directly. Edit .loki/magic/specs/Button.md and run `loki magic update`.
```

For Web Components:

```javascript
/* LOKI-MAGIC-HASH: a1b2c3d4e5f6
   Component: Button (target=webcomponent, spec version=1)
   Generated by loki magic on 2026-04-18T10:00:00Z
   Do not edit directly. Edit .loki/magic/specs/Button.md and run `loki magic update`. */
```

### Regeneration Triggers

A component is stale when any of the following is true:

| Condition | Action |
|-----------|--------|
| File hash marker != registry hash | Regenerate |
| File missing but registry entry exists | Regenerate |
| Registry entry missing but spec exists | Create registry entry and generate |
| Spec removed but file exists | Warn and list as orphan; do not auto-delete |
| Token referenced by spec changed | Regenerate all specs using that token |

### Forcing Regeneration

```bash
# Force all components regardless of freshness
loki magic update --force

# Force a specific component
loki magic update --only Button --force

# Regenerate only stale components (default behavior)
loki magic update
```

---

## 4. Debate Protocol

Debate is the quality gate for generated code. It runs after generation and before the code is committed to the repository.

### Structure

```
Round 1: Initial critique
    Each persona independently reviews the generated code.
    Output: per-persona critique with severity (info | warn | block)

Round 2: Cross-response
    Each persona reads the other personas' critiques and responds.
    May concede, counter-argue, or flag contradictions.
    Output: per-persona response + adjusted severity

Round 3: Proposal
    Personas converge on a merged set of changes or declare deadlock.
    Output: unified diff OR escalation notice
```

### Persona Prompts

All persona prompts share a common header that includes the spec, the generated code, prior round outputs, and the project design tokens. The persona-specific body follows:

#### Creative Developer

> You care about UX polish, delight, and modern patterns. Look for missed opportunities: missing hover states, abrupt transitions, unclear affordances, inconsistent motion curves. You value taste. Severity is `warn` unless the issue creates an immediate UX failure (e.g., a control the user cannot discover), in which case `block`.

#### Conservative Engineer

> You care about stability, conventions, edge cases, and backwards compatibility. Look for untyped props, missing error boundaries, hidden state, implicit coupling, race conditions, and cases where a component controls state it should accept as a prop. Severity is `block` for type safety violations, missing required error handling, or breaking changes to a stable component.

#### A11y Advocate

> You care about WCAG 2.1 AA compliance and assistive tech. Verify: semantic HTML, keyboard operation, focus order, visible focus indicators, sufficient color contrast, aria labels on icon-only controls, aria-live for dynamic regions, reduced-motion support. Severity follows the spec's `a11y_level` field: AA -> block on AA failures, AAA -> block on AAA failures, A -> block only on A failures.

#### Performance Engineer

> You care about bundle size, render cost, reflows, and network. Flag: unbounded lists without virtualization, inline object/function props that defeat memoization, synchronous layout reads during render, unnecessary re-renders, large imports (check bundle impact), missing lazy boundaries. Severity is `warn` for small components (<100 LOC), `block` for render-hot components (lists, tables, dashboards).

### Consensus Rules

| Round 3 Outcome | Action |
|-----------------|--------|
| All personas agree on diff | Apply diff; component passes debate |
| Majority agrees, minority concedes | Apply diff; record minority's `info` notes |
| Split vote, no block | Apply diff that resolves all `warn` items; leave `info` items as code comments |
| Any unresolved `block` | Escalate to HITL |
| Personas contradict each other and cannot reconcile | Escalate to HITL |

### HITL Escalation

When debate deadlocks:

1. A dashboard notification is raised with the debate transcript
2. The component is marked `status: debate_blocked` in the registry
3. The regeneration is not applied; the previous implementation remains
4. The user resolves by either editing the spec (changes the hash, triggers a new debate) or manually overriding via `loki magic debate <name> --accept round-3`

### Debate Persistence

Every debate transcript is written to `.loki/magic/debates/<component>-<hash>.json`:

```json
{
  "component": "Button",
  "spec_hash": "a1b2c3d4e5f6",
  "started_at": "2026-04-18T10:00:00Z",
  "completed_at": "2026-04-18T10:01:23Z",
  "rounds": [
    {"round": 1, "personas": { "creative": {...}, "conservative": {...}, "a11y": {...}, "performance": {...} }},
    {"round": 2, "personas": { ... }},
    {"round": 3, "outcome": "consensus", "diff": "..."}
  ],
  "final_severity": "warn",
  "applied": true
}
```

Debate transcripts participate in episodic memory so the system can surface prior rationale when a similar component is generated later.

---

## 5. Registry Schema

`.loki/magic/registry.json` is the single source of truth for what components exist, what their freshness state is, and what targets they support.

### Top-Level Shape

```json
{
  "version": 1,
  "updated_at": "2026-04-18T10:00:00Z",
  "components": {
    "Button": { ... },
    "PricingCard": { ... }
  },
  "orphans": [
    "generated/react/UnusedThing.tsx"
  ]
}
```

### Component Entry

```json
{
  "name": "Button",
  "spec_path": ".loki/magic/specs/Button.md",
  "spec_hash": "a1b2c3d4e5f6",
  "spec_version": 1,
  "targets": ["react", "webcomponent"],
  "generated": {
    "react": {
      "path": ".loki/magic/generated/react/Button.tsx",
      "hash": "a1b2c3d4e5f6",
      "generated_at": "2026-04-18T10:00:00Z"
    },
    "webcomponent": {
      "path": ".loki/magic/generated/webcomponent/Button.js",
      "hash": "a1b2c3d4e5f6",
      "generated_at": "2026-04-18T10:00:00Z"
    }
  },
  "tests": {
    "react": ".loki/magic/generated/tests/Button.test.tsx",
    "webcomponent": ".loki/magic/generated/tests/Button.spec.ts"
  },
  "snapshots": [
    ".loki/magic/snapshots/Button--primary.png",
    ".loki/magic/snapshots/Button--secondary.png",
    ".loki/magic/snapshots/Button--loading.png"
  ],
  "tags": ["form", "action", "primary"],
  "a11y_level": "AA",
  "stability": "stable",
  "owner": "team-ui",
  "last_debate": {
    "hash": "a1b2c3d4e5f6",
    "outcome": "consensus",
    "severity": "warn",
    "transcript": ".loki/magic/debates/Button-a1b2c3d4e5f6.json"
  },
  "hotspot_score": 0.23,
  "co_change_with": ["Icon", "Spinner"]
}
```

### Field Reference

| Field | Type | Purpose |
|-------|------|---------|
| `spec_hash` | string(12) | Current spec content hash |
| `spec_version` | int | Bumped manually when breaking spec changes occur |
| `targets` | string[] | Which stacks this component supports |
| `generated.<target>.hash` | string(12) | Hash of the spec that generated this file |
| `tags` | string[] | Free-form tags for filtering in `loki magic list` |
| `a11y_level` | enum | Drives A11y persona severity |
| `stability` | enum | `experimental` skips some debate rounds; `stable` runs full debate |
| `last_debate` | object | Reference to the most recent debate transcript |
| `hotspot_score` | float | Derived from git history; drives extra debate rounds |
| `co_change_with` | string[] | Components that historically change in the same commit |

### Freshness Check Logic

```
for component in registry.components:
    for target in component.targets:
        file = component.generated[target]
        if not exists(file.path):
            mark_stale(component, reason="file_missing")
        elif read_hash_header(file.path) != component.spec_hash:
            mark_stale(component, reason="hash_mismatch")
        elif file.hash != component.spec_hash:
            mark_stale(component, reason="registry_drift")
```

---

## 6. Design Token System

Design tokens are the shared vocabulary that keeps generated components visually consistent with the rest of the Loki surfaces.

### Token File Shape

`.loki/magic/tokens.json`:

```json
{
  "color": {
    "accent": "#6d28d9",
    "accent-contrast": "#ffffff",
    "focus": "#a78bfa",
    "text-primary": "#111827",
    "surface": "#ffffff"
  },
  "spacing": {
    "x": { "sm": "0.5rem", "md": "1rem", "lg": "1.5rem" },
    "y": { "sm": "0.25rem", "md": "0.5rem", "lg": "0.75rem" },
    "height": { "sm": "1.75rem", "md": "2.25rem", "lg": "2.75rem" }
  },
  "radius": {
    "sm": "0.25rem",
    "md": "0.375rem",
    "lg": "0.5rem",
    "full": "9999px"
  },
  "font": {
    "body": "system-ui, -apple-system, sans-serif",
    "mono": "ui-monospace, SFMono-Regular, monospace"
  },
  "motion": {
    "fast": "120ms",
    "base": "200ms",
    "slow": "320ms",
    "ease-standard": "cubic-bezier(0.4, 0, 0.2, 1)"
  }
}
```

### Resolution Order

1. Spec-local overrides (in the YAML front matter under `design_tokens:`)
2. Project overrides (`.loki/magic/tokens.json`)
3. Extracted defaults (computed by `loki magic tokens extract`)
4. Built-in Loki defaults (shipped with the CLI)

### Extraction

`loki magic tokens extract` scans:

- `web-app/src/` and `dashboard-ui/` for Tailwind config and CSS variables
- `web-app/src/**/*.css` for `:root` declarations
- Existing React components for recurring inline style values

It prefers the most frequent values and writes them to `.loki/magic/tokens.json` with a `source: extracted` marker so subsequent edits are not clobbered.

---

## 7. MCP Tool Reference

`mcp/magic_tools.py` exposes seven tools. Each signature, input schema, and return schema is documented below.

### `magic_generate`

Generate a new component from a description or a reference to a screenshot.

```python
magic_generate(
    name: str,
    description: str | None = None,
    target: Literal["react", "webcomponent", "both"] = "both",
    from_image: str | None = None,   # absolute path to a PNG/JPG
    tags: list[str] = [],
    run_debate: bool = True,
) -> {
    "component": str,
    "spec_path": str,
    "generated": dict[str, str],
    "debate_outcome": "consensus" | "warn" | "blocked" | "skipped"
}
```

### `magic_update`

Regenerate stale components.

```python
magic_update(
    only: list[str] | None = None,   # names; default all
    force: bool = False,
    run_debate: bool = True,
) -> {
    "regenerated": list[str],
    "skipped": list[str],
    "blocked": list[str]
}
```

### `magic_list`

List registry entries.

```python
magic_list(
    target: str | None = None,
    tag: str | None = None,
    stale_only: bool = False,
) -> list[dict]
```

### `magic_debate`

Run a debate on an existing component.

```python
magic_debate(
    name: str,
    rounds: int = 3,
    personas: list[str] | None = None,  # default: all four
    accept: str | None = None,          # "round-2" or "round-3" to force-accept
) -> {
    "outcome": "consensus" | "warn" | "blocked",
    "transcript_path": str,
    "applied": bool
}
```

### `magic_registry_stats`

Return registry health counters.

```python
magic_registry_stats() -> {
    "total": int,
    "by_target": dict[str, int],
    "stale": int,
    "orphans": int,
    "debate_blocked": int,
    "last_updated": str
}
```

### `magic_tokens_extract`

Extract design tokens from the codebase.

```python
magic_tokens_extract(
    sources: list[str] | None = None,  # default: web-app/src, dashboard-ui
    overwrite: bool = False,
) -> {
    "tokens_written": int,
    "path": str
}
```

### `magic_snapshot`

Capture a visual regression snapshot.

```python
magic_snapshot(
    name: str,
    variants: list[str] | None = None,  # default: all variants in spec
) -> {
    "snapshots": list[str]
}
```

---

## 8. Worked Examples

### Example 1: Generating a simple Button component

```bash
loki magic generate Button \
  --target both \
  --description "Primary action button with label, loading state, and optional leading icon"
```

What happens:

1. The CLI writes `.loki/magic/specs/Button.md` using the spec template. The description is fed to Sonnet to produce the Props table, Behavior, Accessibility, and Visual sections.
2. Spec hash is computed. Registry entry is created.
3. Generation runs for both `react` and `webcomponent` targets in parallel.
4. Vitest and Playwright test files are produced under `.loki/magic/generated/tests/`.
5. A four-persona debate runs. Result: `consensus` with two `warn` items (contrast on ghost variant, missing reduced-motion guard).
6. Debate diff is applied. Final files committed to the registry.

Result structure:

```
.loki/magic/
  specs/Button.md
  generated/react/Button.tsx
  generated/webcomponent/Button.js
  generated/tests/Button.test.tsx
  generated/tests/Button.spec.ts
  debates/Button-a1b2c3d4e5f6.json
  registry.json  (updated)
```

### Example 2: Generating from a screenshot (Claude Vision path)

```bash
loki magic generate PricingCard \
  --target react \
  --from-image ./mockups/pricing.png \
  --tags marketing billing
```

What happens:

1. Claude Vision analyzes the image. The output is a structured description: visual elements, their hierarchy, text content, and inferred layout.
2. The structured description is converted into a spec. Design tokens are chosen from the nearest matches in the token file.
3. The user is shown the generated spec for review before generation proceeds. The user may edit the spec; the hash is recomputed.
4. Generation, test creation, and debate proceed as in Example 1.

Notes:

- Vision extraction is a best-effort starting point. The spec is meant to be edited.
- Only images that pass a sanity check (readable text, identifiable controls) are accepted; otherwise the user is prompted to provide a description.

### Example 3: Running a debate on a complex component

```bash
loki magic debate DataTable --rounds 3
```

What happens:

1. The latest generated files for `DataTable` are loaded along with the current spec.
2. Round 1: each persona produces an independent critique. Performance Engineer flags unbounded row rendering. A11y Advocate flags missing row-group semantics. Conservative Engineer flags an implicit dependency on the parent container's width.
3. Round 2: Creative Developer concedes on the a11y point; Performance Engineer proposes virtualization.
4. Round 3: personas converge on a diff adding `react-virtuoso` for rows, `role="rowgroup"` wrappers, and an explicit `width` prop.
5. Diff is applied. A new generated file is written with a new hash. The registry is updated. The transcript is persisted.

If the A11y Advocate had flagged a `block`-severity contrast failure and no persona proposed a fix, the component would be marked `debate_blocked` and HITL would be requested.

---

## 9. Troubleshooting

| Symptom | Likely Cause | Fix |
|---------|-------------|-----|
| `LOKI-MAGIC-HASH` header missing | File edited manually or created outside the generator | Restore the header or regenerate with `loki magic update --only <name> --force` |
| Regeneration keeps triggering | Token file churn or whitespace differences in spec | Run `loki magic tokens extract --overwrite` once to stabilize; review spec for comment noise |
| Debate escalates every time | Spec is under-specified in a way that makes one persona block | Expand the Accessibility or Behavior section with explicit choices |
| Orphan listed in registry | Generated file has no matching spec | Either delete the file or restore its spec from git history |
| Generated component does not match codebase style | Tokens not extracted or overridden | Run `loki magic tokens extract`, then `loki magic update --force` |
| `magic_generate` returns `blocked` | Round 3 deadlock | Inspect `.loki/magic/debates/<component>-<hash>.json`; resolve by editing the spec or `--accept round-3` |
| React and WC variants drift | Spec was edited after only one target regenerated | Run `loki magic update --only <name>` to rebuild all declared targets |
| Registry stats show many stale | Token change invalidated many specs | Run `loki magic update` to regenerate all dependents |
| Spec version bumped but hash unchanged | Manual version bump without content edit | Add a meaningful change, or leave the version unchanged |
| Tests fail after regeneration | Generated tests reflect new behavior the spec describes | Update the consuming code to match the new API, or revise the spec to preserve old behavior |

---

## 10. Comparison to Competitors

Magic Modules fills the visual-component-generation gap in Loki Mode. The table below compares the approach to adjacent tools.

| Tool | Spec-First | Multi-Persona Debate | Design Tokens | Dual Target (React + WC) | Registry with Freshness | Open Source |
|------|-----------|---------------------|---------------|-------------------------|------------------------|-------------|
| **Loki Magic Modules** | Yes | Yes (4 personas) | Yes (extracted + overridable) | Yes | Yes (SHA hashes) | Yes |
| MagicModules (Nurik) | Yes | No | Partial | React only | Yes | Yes |
| MoMoA (Meier) | No (code-first) | Yes | No | React only | No | Yes |
| Replit Agent Design Canvas | Visual canvas | No | Partial | React only | No | No |
| bolt.new Visual Inspector | No (prompt-first) | No | No | React only | No | No |
| Cursor Compose | No | No | No | Depends on user | No | No |
| v0 by Vercel | Prompt-first, editable preview | No | Partial | React only | No | Partial (hosted) |

Key differentiators:

- **Spec-first with debate** is unique. Other tools are either spec-first without critique, or code-first without a durable spec.
- **Dual target generation** means a single spec can produce both a React component for the dashboard and a Web Component for Purple Lab.
- **Registry with freshness** lets the system detect drift and regenerate automatically, which none of the prompt-first competitors offer.
- **Deep integration with RARV and quality gates** means generated components participate in the same review lifecycle as hand-written code.

---

## See Also

- `skills/magic-modules.md` -- skill module loaded by agents when the task matches magic-modules triggers
- `skills/healing.md` -- hook patterns that protect generated files from silent manual edits
- `skills/documentation.md` -- how component specs feed into generated documentation
- `references/memory-system.md` -- how debate transcripts become episodic memory
- `references/quality-control.md` -- how debate relates to the broader code review pool
