---
name: generate-app
description: Scaffold a runnable Vite + React explorer app from wiki pages. LLM picks a template (comparison-explorer or timeline), extracts a JSON fixture from the topic's pages, and stamps out a working app. Output is a directory the user `pnpm install`s and runs. Used by /generate app. Not user-invocable directly — go through /generate.
user-invocable: false
allowed-tools: Bash(which *) Bash(brew *) Bash(pnpm *) Bash(npm *) Bash(git *) Bash(mkdir *) Bash(cp *) Bash(date *) Bash(cat *) Bash(sed *) Bash(grep *) Read Write Glob Grep
---

# Generate App

Scaffold an interactive explorable web app for a topic — a mini single-page React app populated with structured data the LLM extracts from wiki pages. Output is a directory the user `pnpm install`s and runs locally.

Artifact-first — output lands in `vaults/<vault>/artifacts/app/<slug>-<date>/`.

## Usage (via /generate router)

```
/generate app <topic> [--vault <name>] [--template <name>]
```

- `--template` — force a specific template (`comparison-explorer` or `timeline`). Default: LLM auto-picks.

## Pipeline

```
wiki pages  →  LLM picks template + extracts data.json
           →  cp template/ → app-dir/
           →  write data.json into src/
           →  pnpm install (optional, on-demand)
```

## Step 1: Dependency Check

```bash
if ! which pnpm >/dev/null 2>&1; then
  echo "pnpm not found. Install: npm i -g pnpm"
  exit 1
fi
```

Node 18+ required (Vite 5). `pnpm install` is left for the user to run — scaffolding doesn't assume they want to touch their shell's package graph yet.

## Step 2: Resolve Vault + Topic

```bash
mapfile -t PAGES < <(.claude/skills/generate/lib/select-pages.sh "$VAULT_DIR" "$TOPIC")
HASH=$(.claude/skills/generate/lib/source-hash.sh "${PAGES[@]}")
```

## Step 3: LLM Picks a Template

The invoking LLM reads the selected pages and makes a structural judgement:

| If the pages look like… | Use |
|---|---|
| Side-by-side comparisons, feature matrices, "X vs Y" pages | `comparison-explorer` |
| Date-indexed events, changelogs, chronological narratives | `timeline` |
| Neither cleanly | default to `comparison-explorer` and flag it in the meta |

The user can override with `--template <name>`.

## Step 4: LLM Writes data.json

Each template ships a **schema stub** at `templates/<name>/src/data.schema.json` documenting the shape. The LLM fills the real `data.json` to match.

**comparison-explorer schema:**

```json
{
  "title": "LLM Inference Servers — Comparison",
  "topic": "inference-servers",
  "generated_at": "2026-04-18",
  "columns": [
    { "key": "throughput", "label": "Throughput" },
    { "key": "license",    "label": "License"    },
    { "key": "lang",       "label": "Language"   }
  ],
  "rows": [
    {
      "name": "vLLM",
      "source": "wiki/concepts/vllm.md",
      "values": { "throughput": "high", "license": "Apache-2.0", "lang": "Python" },
      "summary": "PagedAttention-based server. Strong concurrent request throughput."
    }
  ]
}
```

**timeline schema:**

```json
{
  "title": "LLM Scaling — A Timeline",
  "topic": "llm-scaling",
  "generated_at": "2026-04-18",
  "events": [
    {
      "date": "2017-06",
      "title": "Attention Is All You Need",
      "source": "wiki/papers/attention-is-all-you-need.md",
      "summary": "Vaswani et al. introduce the Transformer architecture.",
      "tags": ["architecture", "paper"]
    }
  ]
}
```

**Data-extraction rules the LLM follows:**

- Every row/event carries a `source` wiki page path — mandatory. Surfaced in the UI and used by Phase 2E.
- Keep `summary` to 1–2 sentences. The app UI isn't a book renderer.
- Prefer 5–20 rows/events. Fewer is OK, but a 3-row comparison isn't worth an app.
- Dates in timeline use `YYYY-MM` or `YYYY-MM-DD` — the UI sorts lexically.

## Step 5: Scaffold the App

```bash
SLUG=$(echo "$TOPIC" | tr '[:upper:] ' '[:lower:]-' | tr -cd '[:alnum:]-')
DATE=$(date +%Y-%m-%d)
APP_DIR="$VAULT_DIR/artifacts/app/$SLUG-$DATE"
TEMPLATE_DIR=".claude/skills/generate-app/templates/$TEMPLATE"

# Vault-local override wins
[ -d "$VAULT_DIR/.templates/app/$TEMPLATE" ] && TEMPLATE_DIR="$VAULT_DIR/.templates/app/$TEMPLATE"

mkdir -p "$APP_DIR"
cp -r "$TEMPLATE_DIR"/. "$APP_DIR/"

# Write the LLM-authored data.json into src/
cp "$DATA_JSON" "$APP_DIR/src/data.json"

# Swap the app's package.json name to the slug (avoids name collisions if user installs globally)
sed -i.bak "s/\"name\": \"TEMPLATE_APP_NAME\"/\"name\": \"$SLUG\"/" "$APP_DIR/package.json"
rm "$APP_DIR/package.json.bak"
```

## Step 6: Write the README

```bash
cat > "$APP_DIR/README.md" <<EOF
# $TITLE

Generated by \`/generate app\` on $(date -u +"%Y-%m-%dT%H:%M:%SZ").

Template: **$TEMPLATE**
Source wiki pages: ${#PAGES[@]}
Source hash: \`$HASH\`

## Run

\`\`\`bash
pnpm install
pnpm dev
\`\`\`

Open http://localhost:5173 — hot reload is on.

## Build

\`\`\`bash
pnpm build
pnpm preview
\`\`\`

Output in \`dist/\`. Deploy anywhere static.

## Edit the Data

All content lives in \`src/data.json\`. Edit and reload — no rebuild required.

Each entry's \`source\` field is a relative path to the wiki page it came from. Handy for Phase 2E's \`verify-artifact\` fidelity checks.

## Template-Specific Notes

See \`TEMPLATE.md\` in this directory for what the template does and how to extend it.
EOF
```

## Step 7: Version Detection

Before writing the sidecar, check for an existing artifact of the same type and topic:

```bash
ARTIFACT_TYPE="app"
EXISTING=$(ls "$VAULT_DIR/artifacts/$ARTIFACT_TYPE/"*"$TOPIC_SLUG"*.meta.yaml 2>/dev/null | sort | tail -1)
if [ -n "$EXISTING" ]; then
  PREV_VERSION=$(grep '^version:' "$EXISTING" | awk '{print $2}')
  PREV_VERSION=${PREV_VERSION:-1}
  VERSION=$((PREV_VERSION + 1))
  PREV_SLUG=$(basename "$EXISTING" .meta.yaml)
else
  VERSION=1
  PREV_SLUG=""
fi
```

The old artifact stays in place — not deleted, not overwritten. Multiple files of the same type + topic = version history. The portal discovers and displays these automatically.

Small fixes (CSS tweaks, typo corrections) should update the file in-place without incrementing the version — use judgement based on whether the content meaningfully changed.

## Step 8: Write the Sidecar

```bash
META="$APP_DIR.meta.yaml"
cat > "$META" <<EOF
generator: generate-app@0.1.0
generated-at: $(date -u +"%Y-%m-%dT%H:%M:%SZ")
topic: "<raw topic argument>"
template: $TEMPLATE
slug: $SLUG
generated-from:
$(for p in "${PAGES[@]}"; do echo "  - $p"; done)
source-hash: $HASH
entry-count: $ENTRY_COUNT
version: $VERSION
change-note: "<brief description of what changed, or 'Initial version' for v1>"
replaces: "$PREV_SLUG"
EOF
```

Sidecar sits *next to* the app directory, not inside it — so the app directory can be `pnpm install`ed cleanly without a stray YAML polluting it.

## Step 9: Commit to Vault Repo

```bash
cd "$VAULT_DIR"
# Exclude node_modules (user hasn't installed yet, but if they do between runs, don't commit it).
git add "artifacts/app/$SLUG-$DATE/" "artifacts/app/$SLUG-$DATE.meta.yaml"
git diff --cached --quiet || git commit -m "🛠️  app: scaffold $TOPIC ($TEMPLATE, $(date +%Y-%m-%d))"
```

`.gitignore` in each app includes `node_modules/` and `dist/` — the templates ship one.

## Step 10: Report to User

```
✅ App scaffolded
   Topic:       <topic>
   Template:    <comparison-explorer|timeline>
   Entries:     <N>
   Source hash: <first 12 chars>

   Directory:   vaults/<vault>/artifacts/app/<slug>-<date>/
   Sidecar:     vaults/<vault>/artifacts/app/<slug>-<date>.meta.yaml

   Run:
     cd vaults/<vault>/artifacts/app/<slug>-<date>
     pnpm install
     pnpm dev
```

## Template Authoring

Each template is a **normal Vite + React project** — no magic. To add a new template:

1. `cp -r templates/comparison-explorer templates/<your-template>`
2. Rewrite `src/App.tsx` to match the new shape.
3. Drop a `src/data.schema.json` showing the data shape.
4. Update `src/data.json` with a placeholder example.
5. Write a short `TEMPLATE.md` at the template root describing what it's for and the schema.

The scaffolder literally does `cp -r` + writes `data.json`. Any valid Vite project can be a template.

### Template Guidelines

- **Single-file components preferred.** `App.tsx` is the whole app for both shipped templates. Easier for the LLM to understand and for users to hack.
- **Observatory palette**: amber `#e0af40`, cyan `#5bbcd6`, green `#7dcea0`, bg `#0b0f14`, text `#e8eef6`. In `src/index.css`.
- **Vanilla CSS, no frameworks.** Tailwind would mean extra install steps and version churn. Static CSS is fine at this scale.
- **Data-driven only.** The template ships zero "business logic" — everything the app shows comes from `data.json`. Makes regeneration painless.
- **No router, no state lib.** If you need multi-page navigation, the app is too big for this scaffolder.

## Known Limitations (Phase 2D)

- **Two templates only.** `comparison-explorer` + `timeline`. Adding more is 30 lines of template code each — shipped when the need is real.
- **No PWA, no offline, no deploy-to-anywhere wizard.** It's a local dev app. Users deploy the `dist/` from `pnpm build` themselves.
- **Vite + React only.** No Astro/Next/Svelte variants. One framework means one set of template files to maintain.
- **LLM judgement on template pick varies.** When the topic doesn't obviously fit either template, the LLM defaults to `comparison-explorer` and notes the uncertainty in the meta sidecar.
- **pnpm is required.** `npm install` also works but the generated README and scripts say `pnpm`. Swap if you prefer — same `package.json`.

## See Also

- `.claude/skills/generate/SKILL.md` — router that dispatches here.
- `.claude/skills/generate-quiz/SKILL.md` — static HTML sibling.
- `.claude/skills/generate-flashcards/SKILL.md` — Anki sibling.
- `sites/docs/src/content/docs/reference/artifacts.md` — sidecar schema.
