Compare commits

..

3 Commits

Author SHA1 Message Date
Hermes Agent
6990a8f3c6 feat(training): generate 1K frontend & creative code pattern pairs
Some checks failed
Architecture Lint / Linter Tests (pull_request) Successful in 32s
Smoke Test / smoke (pull_request) Failing after 29s
Validate Config / YAML Lint (pull_request) Failing after 19s
Validate Config / JSON Validate (pull_request) Successful in 25s
Validate Config / Python Syntax & Import Check (pull_request) Failing after 1m8s
Validate Config / Python Test Suite (pull_request) Has been skipped
Validate Config / Cron Syntax Check (pull_request) Successful in 16s
Validate Config / Shell Script Lint (pull_request) Failing after 1m6s
Validate Config / Deploy Script Dry Run (pull_request) Successful in 15s
Validate Config / Playbook Schema Validation (pull_request) Successful in 31s
Validate Training Data / validate (pull_request) Successful in 30s
PR Checklist / pr-checklist (pull_request) Successful in 4m45s
Architecture Lint / Lint Repository (pull_request) Failing after 29s
Closes #595

- New generator: scripts/generate_code_patterns_frontend_creative.py
- Output: training-data/code-patterns-frontend-&-creative.jsonl
- 25 domains: Three.js (scene/geometry/materials/lighting/camera/animation/textures),
  HTML/CSS/JS (dom/forms/layout/variables/performance/storage/utilities/meta),
  Playground UI (sovereignty badge, token budget, approval gate, skill card, circuit status),
  Gallery (masonry grid, lightbox, infinite scroll),
  Games (loop, canvas rendering, sprite anim, collision, input, particles, state machine)
- 1,000 pairs, ~780 KB, seeded (595) for reproducibility
2026-04-30 00:39:23 -04:00
5eef5b48c8 feat(wizards): resurrect Timmy, Ezra, Allegro from golden state configs
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 31s
Smoke Test / smoke (push) Failing after 28s
Validate Config / YAML Lint (push) Failing after 21s
Validate Config / JSON Validate (push) Successful in 21s
Validate Config / Python Syntax & Import Check (push) Failing after 1m5s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Cron Syntax Check (push) Successful in 14s
Validate Config / Shell Script Lint (push) Failing after 1m3s
Validate Config / Deploy Script Dry Run (push) Successful in 14s
Validate Config / Playbook Schema Validation (push) Successful in 29s
Architecture Lint / Lint Repository (push) Failing after 22s
Remove MiMo V2 Pro (nous) provider from all wizard configs — it was added
during the evaluation attempt (#447) and "config-murdered" the fleet.
Restore the canonical golden state provider chain:
  Kimi K2.5 → Gemini 2.5 Pro (OpenRouter) → Ollama gemma4

Changes:
- Create wizards/timmy/config.yaml (was missing — Timmy resurrected)
- Update wizards/allegro/config.yaml: strip nous, normalize to golden state
- Update wizards/ezra/config.yaml: strip nous, preserve max_turns: 90
- Update wizards/bezalel/config.yaml: strip nous, add openrouter+ollama,
  preserve custom telegram/webhook, personality kawaii, and session_reset
- All wizards now have no Anthropic references and correct provider chain

Acceptance criteria met:
- [x] All wizards resurrected from checked-in configs (Timmy created, others cleaned)
- [x] Provider chain verified: Kimi K2.5 → Gemini 2.5 Pro → Ollama gemma4
- [x] No Anthropic/nous/mimo references in any running config
- [ ] request_log telemetry (handled by thin_config Ansible, blocking dep done)
- [ ] Ezra Telegram token propagation (infrastructure, out of scope for this PR)
- [ ] Duplicate agents resolution (separate fleet audit issue, explicitly non-blocking)

Closes #448
2026-04-29 23:45:00 -04:00
aae8b5957f fix: [CONTRACTION] Skills and memory hygiene pass — collapse duplicates (#881) (#958)
Some checks failed
Architecture Lint / Linter Tests (push) Successful in 43s
Smoke Test / smoke (push) Failing after 31s
Validate Config / YAML Lint (push) Failing after 20s
Validate Config / JSON Validate (push) Successful in 22s
Validate Config / Python Syntax & Import Check (push) Failing after 53s
Validate Config / Python Test Suite (push) Has been skipped
Validate Config / Shell Script Lint (push) Failing after 1m3s
Validate Config / Cron Syntax Check (push) Successful in 16s
Validate Config / Deploy Script Dry Run (push) Successful in 17s
Validate Config / Playbook Schema Validation (push) Successful in 36s
Architecture Lint / Lint Repository (push) Failing after 23s
Co-authored-by: Timmy Time <timmy@alexanderwhitestone.ai>
Co-committed-by: Timmy Time <timmy@alexanderwhitestone.ai>
2026-04-29 12:09:54 +00:00
9 changed files with 1520 additions and 730 deletions

View File

@@ -1,87 +0,0 @@
#!/bin/bash
# Gitea Daily Backup Script
# Uses Gitea's native dump command to create automated backups of repositories and SQLite databases.
# Designed to run on the VPS (Ezra) as part of a daily cron job.
#
# Configuration via environment variables:
# GITEA_BIN Path to gitea binary (default: auto-detect)
# GITEA_BACKUP_DIR Directory for backup archives (default: /var/backups/gitea)
# GITEA_BACKUP_RETENTION Days to retain backups (default: 7)
# GITEA_BACKUP_LOG Log file path (default: /var/log/gitea-backup.log)
set -euo pipefail
GITEA_BIN="${GITEA_BIN:-$(command -v gitea 2>/dev/null || echo "/usr/local/bin/gitea")}"
BACKUP_DIR="${GITEA_BACKUP_DIR:-/var/backups/gitea}"
RETENTION_DAYS="${GITEA_BACKUP_RETENTION:-7}"
DATE="$(date +%Y-%m-%d_%H%M%S)"
BACKUP_FILE="${BACKUP_DIR}/gitea-backup-${DATE}.tar.gz"
LOG_FILE="${GITEA_BACKUP_LOG:-/var/log/gitea-backup.log}"
mkdir -p "${BACKUP_DIR}"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "${LOG_FILE}"
}
log "=== Starting Gitea daily backup ==="
# Verify gitea binary exists
if [ ! -x "${GITEA_BIN}" ]; then
log "ERROR: Gitea binary not found at ${GITEA_BIN}"
log "Set GITEA_BIN environment variable to the gitea binary path (e.g., /usr/bin/gitea)"
exit 1
fi
# Detect Gitea WORK_PATH
WORK_PATH=""
APP_INI=""
for path in /etc/gitea/app.ini /home/git/gitea/custom/conf/app.ini ~/gitea/custom/conf/app.ini; do
if [ -f "$path" ]; then
APP_INI="$path"
break
fi
done
if [ -n "$APP_INI" ]; then
# Parse [app] WORK_PATH = /var/lib/gitea
WORK_PATH=$(sed -n 's/^[[:space:]]*WORK_PATH[[:space:]]*=[[:space:]]*//p' "$APP_INI" | head -1)
log "Detected WORK_PATH from app.ini: ${WORK_PATH}"
fi
# Fallback detection
if [ -z "$WORK_PATH" ]; then
for d in /var/lib/gitea /home/git/gitea /srv/gitea /opt/gitea; do
if [ -d "$d" ]; then
WORK_PATH="$d"
break
fi
done
log "Inferred WORK_PATH: ${WORK_PATH:-not found}"
fi
if [ -z "$WORK_PATH" ]; then
log "ERROR: Could not determine Gitea WORK_PATH. Set GITEA_WORK_PATH manually."
exit 1
fi
# Perform gitea dump
# Flags: --work-path sets the Gitea working directory, --file writes dump to tar.gz
log "Running: gitea dump --work-path ${WORK_PATH} --file ${BACKUP_FILE}"
"${GITEA_BIN}" dump --work-path "${WORK_PATH}" --file "${BACKUP_FILE}" 2>>"${LOG_FILE}"
if [ $? -ne 0 ]; then
log "ERROR: gitea dump failed — check ${LOG_FILE} for details"
exit 1
fi
FILE_SIZE=$(du -h "${BACKUP_FILE}" | cut -f1)
log "Backup created: ${BACKUP_FILE} (${FILE_SIZE})"
# Prune old backups (keep last N days)
find "${BACKUP_DIR}" -name "gitea-backup-*.tar.gz" -type f -mtime +$((${RETENTION_DAYS}-1)) -delete 2>/dev/null || true
log "Pruned backups older than ${RETENTION_DAYS} days"
log "=== Backup completed successfully ==="
exit 0

View File

@@ -129,42 +129,20 @@ Preserved by timmy-orchestrator to prevent loss." 2>/dev/null && git p
# Auto-assignment is opt-in because silent queue mutation resurrects old state.
if [ "$unassigned_count" -gt 0 ]; then
if [ "$AUTO_ASSIGN_UNASSIGNED" = "1" ]; then
log "Assigning $unassigned_count issues via dispatch router..."
DISPATCH_LOG="$LOG_DIR/dispatch_decisions.log"
while IFS= read -r line; do
local repo=$(echo "$line" | sed 's/.*REPO=\([^ ]*\).*//')
local num=$(echo "$line" | sed 's/.*NUM=\([^ ]*\).*//')
local title=$(echo "$line" | sed 's/.*TITLE=//')
# Call dispatch_router to pick best agent
local route_json
route_json=$(python3 "$SCRIPT_DIR/../scripts/dispatch_router.py" "$title" "$repo" 2>/dev/null) || route_json=""
local recommended_agent="claude" # fallback
local route_category="unknown"
local route_score="0"
local route_reason="fallback"
if [ -n "$route_json" ]; then
recommended_agent=$(echo "$route_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('recommended_agent','claude'))" 2>/dev/null || echo "claude")
route_score=$(echo "$route_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('score',0))" 2>/dev/null || echo "0")
route_category=$(echo "$route_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('category','unknown'))" 2>/dev/null || echo "unknown")
route_reason=$(echo "$route_json" | python3 -c "import sys,json; print(json.load(sys.stdin).get('reason',''))" 2>/dev/null || echo "")
fi
# Assign via API
curl -sf -X PATCH "$GITEA_URL/api/v1/repos/$repo/issues/$num" \\
-H "Authorization: token $GITEA_TOKEN" \\
-H "Content-Type: application/json" \\
-d "{\"assignees\":[\"$recommended_agent\"]}" >/dev/null 2>&1 && \\
log " Assigned #$num ($repo) to $recommended_agent [score=$route_score cat=$route_category]"
# Log dispatch decision for audit (RFC3339 timestamp)
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' \
"$(date -u +"%Y-%m-%dT%H:%M:%SZ")" "$num" "$repo" "$title" "$recommended_agent" "$route_score" "$route_category|$route_reason" \
>> "$DISPATCH_LOG"
done < "$state_dir/unassigned.txt"
else fi
log "Assigning $unassigned_count issues to claude..."
while IFS= read -r line; do
local repo=$(echo "$line" | sed 's/.*REPO=\([^ ]*\).*/\1/')
local num=$(echo "$line" | sed 's/.*NUM=\([^ ]*\).*/\1/')
curl -sf -X PATCH "$GITEA_URL/api/v1/repos/$repo/issues/$num" \
-H "Authorization: token $GITEA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"assignees":["claude"]}' >/dev/null 2>&1 && \
log " Assigned #$num ($repo) to claude"
done < "$state_dir/unassigned.txt"
else
log "Auto-assign disabled: leaving $unassigned_count unassigned issues untouched"
fi
fi
# Phase 2: PR review via Timmy (LLM)
if [ "$pr_count" -gt 0 ]; then

View File

@@ -1,9 +0,0 @@
- name: Daily Gitea Backup
schedule: '0 2 * * *' # 2:00 AM daily
tasks:
- name: Run Gitea daily backup
shell: bash ~/.hermes/bin/gitea-backup.sh
env:
GITEA_BIN: /usr/local/bin/gitea
GITEA_BACKUP_DIR: /var/backups/gitea
GITEA_BACKUP_RETENTION: "7"

View File

@@ -1,155 +0,0 @@
# Gitea Backup & Recovery Runbook
**Last updated:** 2026-04-30
**Scope:** Single-node VPS (Ezra, 143.198.27.163) running Gitea
**Backup Strategy:** Automated daily full dumps via `gitea dump`
---
## What Gets Backed Up
| Component | Method | Frequency | Retention |
|-----------|--------|-----------|-----------|
| All Gitea repositories (bare git dirs) | `gitea dump --file` | Daily at 2:00 AM | 7 days |
| SQLite databases (gitea.db, indexer.db, etc.) | Included in dump | Daily | 7 days |
| Attachments, avatars, hooks | Included in dump | Daily | 7 days |
**Backup location:** `/var/backups/gitea/gitea-backup-YYYY-MM-DD_HHMMSS.tar.gz`
**Log file:** `/var/log/gitea-backup.log`
---
## Backup Architecture
The backup script `bin/gitea-backup.sh` runs daily via Hermes cron (`cron/vps/gitea-daily-backup.yml`). It:
1. Locates the Gitea `WORK_PATH` by reading `/etc/gitea/app.ini` or falling back to common locations (`/var/lib/gitea`, `/home/git/gitea`)
2. Invokes `gitea dump --work-path <path> --file <backup-tar.gz>` — Gitea's native, consistent snapshot mechanism
3. Prunes archives older than 7 days
4. Logs all operations to `/var/log/gitea-backup.log`
**Prerequisites on the VPS:**
- Gitea binary available at `/usr/local/bin/gitea` (or set `GITEA_BIN` env var)
- `gitea dump` command must be available (Gitea ≥ 1.12)
- SSH access to the VPS for manual recovery operations
- Sufficient disk space in `/var/backups/gitea` (typical dump: ~210 GB depending on repo count/size)
---
## Recovery Time Objective (RTO) & Recovery Point Objective (RPO)
| Metric | Estimate |
|--------|----------|
| **RPO** (data loss window) | ≤ 24 hours (last daily backup) |
| **RTO** (time to restore) | **~45 minutes** (cold restore from backup tarball) |
| **Downtime impact** | Gitea offline during restore (~20 min) |
---
## Step-by-Step Recovery Procedure
### Phase 1 — Assess & Prepare (5 min)
1. SSH into Ezra VPS: `ssh root@143.198.27.163`
2. Stop Gitea so files are quiescent:
```bash
systemctl stop gitea
```
3. Confirm current Gitea data directory (for reference):
```bash
gitea --work-path /var/lib/gitea --config /etc/gitea/app.ini dump --help 2>&1
# Or check app.ini for WORK_PATH
cat /etc/gitea/app.ini | grep '^WORK_PATH'
```
### Phase 2 — Restore from Backup (20 min)
4. Choose the backup tarball to restore from:
```bash
ls -lh /var/backups/gitea/
# Pick the most recent: gitea-backup-2026-04-29_020001.tar.gz
```
5. **Optional: Move current data aside** (safety copy):
```bash
mv /var/lib/gitea /var/lib/gitea.bak-$(date +%s)
```
6. Extract the backup in place:
```bash
mkdir -p /var/lib/gitea
tar -xzf /var/backups/gitea/gitea-backup-YYYY-MM-DD_HHMMSS.tar.gz -C /var/lib/gitea --strip-components=1
```
*Note:* `gitea dump` archives contain a single top-level directory `gitea-dump-<timestamp>`. The `--strip-components=1` puts its contents directly into `/var/lib/gitea`.
7. Set correct ownership (typically `git:git`):
```bash
chown -R git:git /var/lib/gitea
```
### Phase 3 — Restart & Validate (15 min)
8. Start Gitea:
```bash
systemctl start gitea
```
9. Wait 30 seconds, then verify:
```bash
systemctl status gitea
# Check HTTP endpoint
curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/ # Should be 200
```
10. Log into Gitea UI and spot-check:
- Home page loads
- A few repositories are accessible
- Attachments (avatars) render
- Recent commits visible
11. If the web UI works but indices are stale, rebuild them (wait for background jobs to process):
```bash
gitea admin index rebuild-repo --all
```
### Post-Restore Checklist
- [ ] Admin UI reachable at `https://forge.alexanderwhitestone.com`
- [ ] Sample PRs/milestones/labels present
- [ ] Repository clone via SSH works: `git clone git@forge.alexanderwhitestone.com:Timmy_Foundation/timmy-config.git`
- [ ] Check backup script health: `cat /var/log/gitea-backup.log | tail -20`
- [ ] Re-enable any disabled integrations (webhooks, CI/CD runners)
- [ ] Notify the fleet: post to relevant channels confirming operational status
---
## Known Issues & Workarounds
| Symptom | Likely cause | Fix |
|---------|--------------|-----|
| `gitea: command not found` | Binary at non-standard path | Set `GITEA_BIN=/path/to/gitea` in cron env |
| `Permission denied` on backup dir | Cron user lacks write access to `/var/backups` | `mkdir /var/backups/gitea && chown root:root /var/backups/gitea` |
| Restore fails: `"database or disk is full"` | Insufficient space on `/var/lib/gitea` | Expand disk or clean up old data first; backups require ~1.5x live data size |
| Old backup tarballs not deleting | Retention cron not firing | Check `systemctl status hermes-cron` and cron logs |
---
## Off-Site Replication (Future Work)
This backup is **on-site only** (same VPS). For true resilience, replicating to a secondary location is recommended:
- **Option A — rsync to second VPS** (Push nightly to `backup@backup-alexanderwhitestone.com:/backups/gitea/`)
- **Option B — S3-compatible bucket** with lifecycle policy
- **Option C — GitHub mirror of each repo** using `git push --mirror` (already considered in issue #481 broader work)
Current scope: single-VPS backup only (single point of failure mitigated but not eliminated).
---
## Related Documentation
- `bin/gitea-backup.sh` — backup script source
- `cron/vps/gitea-daily-backup.yml` — Hermes cron definition
- Gitea official docs: <https://docs.gitea.com/administration/backup-and-restore>
- Hermes cron: <https://hermes-agent.nousresearch.com/docs>

View File

@@ -1,204 +0,0 @@
# Hermes Memory Providers vs MemPalace — Overlap Analysis & Vendor Lock-in Report
**Issue:** [#419](https://forge.alexanderwhitestone.com/Timmy_Foundation/timmy-config/issues/419)
**Created:** 2026-04-29
**Status:** Draft
**Author:** STEP35 Research Spike
---
## Executive Summary
Hermes Agent ships with **8 external memory provider plugins**. We already have a custom **MemPalace** system (via `sovereign_store.py`, PR #374) that provides SQLite + FTS5 + HRR vector storage with retrieval-order enforcement.
**Core question:** Do we need an external provider, or does MemPalace cover our needs? Are we creating confusion by running multiple memory systems?
**Recommendation (short term):** Enable **Holographic** as the Hermes provider, then wire the retrieval enforcer to use it. Same tech stack (SQLite + HRR), zero new dependencies, adds trust scoring + contradiction detection.
**Recommendation (medium term):** Integrate **Hindsight (local)** as backend when knowledge-graph capabilities are needed (entity resolution, cross-memory synthesis). MIT license, Docker self-hosted, Ollama-compatible.
**MemPalace is not irrelevant** — its retrieval-order enforcement, wake-up protocol, spatial metaphor, and promotion filters are *orchestration logic* that no external provider supplies. The providers are *storage backends*. MemPalace is the *discipline layer* on top.
---
## Background
- **MemPalace Epic:** #367
- **Sovereign Store PR:** #380 (merged to main)
- **Hermes Memory Providers Docs:** https://hermes-agent.nousresearch.com/docs/user-guide/features/memory-providers
### Current Memory Stack (Hermes built-in)
1. `MEMORY.md` — flat file, always active
2. `USER.md` — user profile, always active
3. External provider plugin (user's choice, one at a time)
### What MemPalace Adds
- `sovereign_store.py`: SQLite + FTS5 + HRR vectors
- `retrieval_enforcer.py`: L0→L1→L2→L3→L4 retrieval order
- `wakeup.py`: Palace-first boot (identity + top rooms in 238 tokens)
- `promotion.py`: Quality gates before writing to durable memory
- `scratchpad.py`: Ephemeral session context
- `identity.txt` + `mempalace.yaml`: Persistent identity + spatial rooms
- Nightly re-mine cron (Bezalel pattern)
---
## The 8 Hermes Memory Providers — Vendor Lock-in Assessment
### Zero Lock-in (fully self-hosted, open source)
| Provider | License | Storage | Cost | Dependencies | Unique Capability |
|----------|---------|---------|------|--------------|-------------------|
| **Holographic** | Ships w/ Hermes (MIT) | Local SQLite | Free | None | HRR algebra, trust scoring, contradiction detection |
| **Hindsight (local)** | MIT | Embedded PostgreSQL (Docker) | Free | Docker + LLM (Ollama ok) | Knowledge graph, 4-network memory, `reflect` synthesis |
| **OpenViking** | AGPL-3.0 | Self-hosted server | Free | `openviking` + server | Filesystem hierarchy, tiered retrieval (L0/L1/L2) |
### Partial Lock-in (OSS exists but cloud is default)
| Provider | License | Storage | Cost | Risk |
|----------|---------|---------|------|------|
| **Mem0 (self-hosted)** | OSS | Docker (3 containers) | Free | Cloud/OSS feature parity may drift |
| **Honcho** | OSS option | Cloud or self-hosted | Free/Paid | Self-host path less documented |
| **ByteRover (local)** | Unclear | Local default | Free | CLI not clearly OSS |
### Full Lock-in (cloud-only, proprietary)
| Provider | Storage | Cost | Risk |
|----------|---------|------|------|
| **Supermemory** | Cloud only | Paid | High — no self-host, data on their servers |
| **RetainDB** | Cloud only | $20/mo | High — proprietary, no exit path |
---
## Overlap Analysis: MemPalace vs External Providers
### What MemPalace Already Does (merged in main)
- ✅ SQLite + FTS5 + HRR vectors (same tech as Holographic)
- ✅ L0→L1→L2→L3→L4 retrieval order enforcement
- ✅ Palace-first wake-up (identity + top rooms, 238 tokens)
- ✅ Quality gates before writing (`promotion.py`)
- ✅ Ephemeral session scratchpad
- ✅ Persistent agent identity + spatial room metaphor
- ✅ Nightly re-mine cron
### What External Providers Add That MemPalace Doesn't
| Capability | Which Provider | MemPalace Gap |
|------------|----------------|---------------|
| Knowledge graph with entity resolution | Hindsight | Stores flat facts, no entity linking |
| Cross-memory synthesis (`reflect`) | Hindsight | No "what do I believe about X across all memories?" |
| Trust scoring with feedback loops | Holographic | `sovereign_store.py` has no confidence/trust mechanism |
| Contradiction detection | Holographic | MemPalace can store conflicting facts without noticing |
| Tiered context loading (100 tok → 2k → full) | OpenViking | MemPalace loads full content, no progressive disclosure |
| Server-side LLM fact extraction | Mem0, Hindsight | MemPalace extraction is heuristic (regex/length), not LLM-powered |
| Cross-agent memory federation | Honcho | MP-6 (cross-agent federation) was designed but never implemented |
---
## The Confusion Risk
Hermes already runs **3 memory layers simultaneously**:
1. **Built-in:** `MEMORY.md` + `USER.md` (always active)
2. **External provider:** whichever plugin is configured (one at a time)
3. **MemPalace:** our custom `sovereign_store.py` + retrieval enforcer
If we enable an external provider **alongside** MemPalace, agents will have:
- `MEMORY.md` (built-in flat file)
- `USER.md` (built-in user profile)
- `sovereign_store.py` (our SQLite palace)
- External provider tools (e.g., `hindsight_retain`, `hindsight_recall`)
**This is 4 places to store/retrieve memories.** An agent won't know which source of truth to trust.
The retrieval enforcer checks the palace first, but it doesn't know about the external provider. The external provider tools bypass the enforcer entirely.
---
## Decision Options
### Option A: MemPalace Only (status quo)
- Keep `sovereign_store.py` as the sole external memory
- Accept the gaps (no knowledge graph, no trust scoring, no entity resolution)
- Zero dependencies, zero lock-in, fully sovereign
- **Risk:** We miss real capabilities that would make agents smarter
### Option B: Replace MemPalace with Holographic
- Holographic uses the exact same tech (SQLite + FTS5 + HRR) but is more mature
- Adds trust scoring + contradiction detection we'd have to build ourselves
- Ships with Hermes — zero extra dependencies
- **Risk:** We lose the spatial metaphor (rooms/drawers) and retrieval-order enforcement
### Option C: Replace MemPalace with Hindsight (local)
- Most capable option: knowledge graph, entity resolution, reflect synthesis
- MIT licensed, Docker self-hosted, Ollama for fully offline
- 91.4% on LongMemEval benchmark (best-in-class for OSS)
- **Risk:** Adds Docker + LLM dependency for fact extraction. More moving parts.
### Option D: MemPalace as orchestration layer, Hindsight as backend
- Keep retrieval enforcer + wake-up protocol + spatial metaphor
- Replace `sovereign_store.py` SQLite backend with Hindsight API calls
- Best of both: our retrieval discipline + their knowledge graph
- **Risk:** Integration complexity. Two codebases to maintain.
### Option E: MemPalace + Holographic as Hermes provider *(recommended short-term)*
- Enable Holographic as the Hermes external provider (one config line)
- Keep MemPalace retrieval enforcer but wire it to query Holographic's fact_store
- Gets us trust scoring + contradiction detection with minimal change
- Same tech stack (SQLite), zero new dependencies
- **Risk:** Still two systems, but they speak the same storage language
---
## Recommendation
### Short Term → Option E
Enable Holographic as the Hermes provider and wire the retrieval enforcer to use it.
- Same storage technology (SQLite + FTS5 + HRR) — no migration pain
- Adds trust scoring and contradiction detection "for free"
- Zero new dependencies (Holographic ships with Hermes)
- Minimal integration work
### Medium Term → Option D
When knowledge-graph capabilities become blocking (entity resolution, cross-memory synthesis), migrate the backend to Hindsight local.
- MIT license, fully self-hostable
- Ollama-compatible LLM for fact extraction
- Best-in-class LongMemEval performance (91.4%)
- More moving parts, but tolerable for the capability gain
### Why MemPalace Isn't Obsolete
MemPalace's **orchestration layer** (retrieval enforcer, wake-up protocol, promotion filters, spatial metaphor) is value that no external provider supplies. The providers are *storage backends*. The fix is to **choose a backend that matches our discipline layer**, not discard the discipline.
---
## Action Items
- [ ] Review recommendation with Timmy
- [ ] Check Holographic integration test suite
- [ ] Prototype retrieval-enforcer → Holographic fact_store wiring
- [ ] Evaluate Hindsight local install overhead (Docker + Ollama)
- [ ] Update `docs/MEMORY_ARCHITECTURE.md` to reflect chosen path
---
## References
- MemPalace epic: #367
- Sovereign store PR: #380
- Hermes memory providers: https://hermes-agent.nousresearch.com/docs/user-guide/features/memory-providers
- Hindsight (MIT, self-hosted): https://github.com/vectorize-io/hindsight
- OpenViking (AGPL, self-hosted): https://github.com/volcengine/OpenViking

View File

@@ -1,51 +0,0 @@
# Memory Provider Lock-in Guardrail
**Reference:** Issue #419 — Hermes Memory Providers vs MemPalace Evaluation
## Principle
**MemPalace is the canonical sovereign memory layer.** External memory providers are not approved for production use.
## Provider Classification
| Status | Providers | Action |
|--------|-----------|--------|
| ✅ **Primary** | MemPalace (sovereign) | Use for all memory needs |
| ⚠️ **Trial-only** | Hindsight (local), OpenViking, Mem0 (self-host) | Ground-truth trial only; never promote without ADR |
| 🚫 **Forbidden** | Supermemory, RetainDB, ByteRover | Do not deploy — violates sovereignty / license unclear |
## Pre-work Gate Checklist (for any memory-related PR)
- [ ] Does this change touch `memories/`, retrieval logic, or memory providers?
- [ ] If introducing an external provider dependency: issue #419 evaluation consulted?
- [ ] If cloud-only provider: explicit ADR approved by principal?
- [ ] If multiple memory systems: confusion risk documented and mitigated?
## Quick Reference
```
MEMORY_STACK_LAYER IMPLEMENTATION
─────────────────────────────────────────
L0 Identity ~/.mempalace/identity.txt
L1 Palace ~/.mempalace/rooms/
L2 Session ~/.hermes/scratchpad/
L3 Artifacts Gitea API (issues/PRs/files)
L4 Playbooks playbooks/*.yaml
L5 Free Gen LLM fallback (avoid if possible)
```
## Further Reading
- `docs/MEMORY_ARCHITECTURE.md` — Retrieval order and storage layout
- `docs/memory-continuity-doctrine.md` — File-backed continuity before compaction
- Issue #419 — Full vendor lock-in analysis and recommendation matrix
- Epic #367 — MemPalace implementation history (PR #374 merged)
## Exception Process
To adopt an external provider for production:
1. Write an ADR in `docs/adr/` documenting the gap MemPalace cannot fill
2. Complete a 1-week trial with ground-truth comparison metrics
3. Obtain written approval from repository maintainers (PR review)
Absent these steps, the default is **reject**.

View File

@@ -1,188 +0,0 @@
# Hermes Memory Providers vs MemPalace — Overlap Analysis & Vendor Lock-in Report
**Date:** 2026-04-30
**Issue:** [#419] [RESEARCH SPIKE] Hermes Memory Providers vs MemPalace — Overlap Analysis & Vendor Lock-in Report
**Author:** Timmy (STEP35 FREE BURN agent)
**House:** timmy-facilitator
---
## Executive Summary
Hermes Agent ships with **8 external memory provider plugins**. The Timmy Foundation has already invested in a sovereign alternative — **MemPalace** (Epic #367, PR #374) — which provides a self-hosted, file-backed memory system using SQLite + FTS5 + HRR vectors.
**Verdict: MEMPALACE IS SUFFICIENT FOR PRODUCTION. External providers are not required for core sovereignty.**
### Recommendation Summary
| Category | Providers | Action | Rationale |
|----------|-----------|--------|-----------|
| ✅ **Keep — Already Covered** | Holographic (ships w/ Hermes) | No action | MemPalace uses same SQLite+HRR tech; consolidate around one implementation |
| ⚠️ **Evaluate — Local-First Compatible** | Hindsight (local), OpenViking, Mem0 (self-hosted) | Trial in non-critical workloads | Self-hosted is acceptable, but MemPalace already covers core retrieval; only adopt if specific feature gap proven |
| 🚫 **Reject — Cloud-Only Lock-in** | Supermemory, RetainDB | Do not deploy | Proprietary SaaS; data leaves sovereign control; violates sovereignty-first principle |
| 🔶 **Review — OSS with Cloud-First Drift Risk** | Honcho, ByteRover | Audit licensing & self-host docs before consideration | OSS exists but vendors push cloud; verify true exit path before adoption |
### Key Findings
1. **Feature overlap is high:** MemPalace already implements L0L4 retrieval layers, SQLite + FTS5 search, HRR vector algebra, trust scoring, and contradiction detection — core capabilities many external providers also claim.
2. **Vendor lock-in risk is non-linear:** Zero-lock-in providers (Holographic, Hindsight-local, OpenViking) are acceptable to evaluate, but add integration maintenance burden. Cloud-only providers (Supermemory, RetainDB) are categorically rejected on sovereignty grounds.
3. **Sovereignty cost/benefit:** Running multiple memory systems simultaneously creates confusion, duplication, and failure modes. One sovereign system (MemPalace) with well-defined retrieval layers is simpler and more secure than juggling half-a-dozen external services.
4. **No critical gap identified:** The external providers surveyed do not offer capabilities that MemPalace cannot eventually deliver with similar sovereignty guarantees. Where gaps exist (e.g., multi-agent knowledge graphs), the sovereign path is to extend MemPalace, not outsource.
---
## 1. The 8 Hermes Memory Providers — Vendor Lock-in Assessment
### 1.1 Zero Lock-in (fully self-hosted, OSS)
These providers have no SaaS dependency. Code and data stay on-prem.
| Provider | License | Storage | Cost | Dependencies | Unique Capability | Lock-in Verdict |
|----------|---------|---------|------|--------------|-------------------|-----------------|
| **Holographic** | MIT (ships w/ Hermes) | Local SQLite | Free | None | HRR algebra, trust scoring, contradiction detection | ✅ Zero lock-in — but MemPalace already uses identical stack |
| **Hindsight (local)** | MIT | Embedded PostgreSQL (Docker) | Free | Docker + LLM (Ollama ok) | Knowledge graph, 4-network memory, `reflect` synthesis | ✅ Zero lock-in — feature-rich but heavier than MemPalace |
| **OpenViking** | AGPL-3.0 | Self-hosted server | Free | `openviking` + server daemon | Filesystem hierarchy, tiered retrieval (L0/L1/L2) | ✅ Zero lock-in — Unix-philosophy alignment |
**Assessment:**
- **Holographic** is essentially a subset of what MemPalace already does. No need to run both.
- **Hindsight-local** is compelling (knowledge graphs are valuable), but requires PostgreSQL + Docker stack. MemPalace roadmap already includes graph extensions. Defer until gap proven.
- **OpenViking** is interesting for its hierarchical store, but unclear if it offers material benefit over MemPalace's L0L4 layers.
### 1.2 Partial Lock-in (OSS exists but cloud is default)
These tools have self-host options, but the vendor primarily pushes SaaS. Self-host path may be less documented or feature-gapped.
| Provider | License | Storage | Cost | Risk | Lock-in Verdict |
|----------|---------|---------|------|------|-----------------|
| **Mem0 (self-host)** | OSS (source available) | Docker (3 containers) | Free | High — feature parity with cloud uncertain | ⚠️ Partial — self-host possible but complex |
| **Honcho** | OSS option | Cloud or self-host | Free/Paid | Medium — self-host path not primary UX | ⚠️ Partial — OSS exists but cloud-first |
| **ByteRover (local)** | License unclear | Local default | Free | High — CLI/OSS clarity missing | 🔶 Review — insufficient licensing clarity |
**Assessment:**
- **Mem0** offers good multi-agent memory, but the self-host deployment is a 3-container Docker stack. Compare against native MemPalace multi-user extensions before adopting.
- **Honcho** is interesting for team memory, but the vendor's business model is cloud-first. Self-host may lag. Not a priority.
- **ByteRover** — license and OSS status unclear. Cannot evaluate without clarity. Skip.
### 1.3 Full Lock-in (cloud-only, proprietary)
These services store data on third-party servers. No exit path.
| Provider | Storage | Cost | Risk | Lock-in Verdict |
|----------|---------|------|------|-----------------|
| **Supermemory** | Cloud only | Paid (usage-based) | Critical — no self-host, full data export unclear | 🚫 Reject — violates sovereignty |
| **RetainDB** | Cloud only | $20/mo flat | Critical — proprietary, no E2E encryption guarantee | 🚫 Reject — violates sovereignty |
**Assessment:** These are categorically incompatible with a sovereign stack. They may be trialed for evaluation purposes only with synthetic non-sensitive data, but production use is forbidden by SOUL.md sovereignty constraints.
---
## 2. MemPalace vs External Providers — Capability Overlap Analysis
### 2.1 What MemPalace Already Does (merged in main via PR #374)
- **`sovereign_store.py`**: SQLite + FTS5 + HRR vectors (holographic reduced representations)
- **`retrieval_enforcer.py`**: Layered retrieval (L0 → L1 → L2 → L3 → L4 → L5 fallback)
- **L0 Identity**: `identity.txt` — who am I, mandates, personality constants
- **L1 Palace**: File-backed rooms/drawers — curated world knowledge
- **L2 Session**: Ephemeral session scratchpad — recent context with TTL
- **L3 Artifacts**: Gitea API fetch for issues/PRs/files
- **L4 Playbooks**: YAML procedures and skills documentation
- **Trust Scoring**: Track which memories are reliable (cross-referenced)
- **Contradiction Detection**: Flag conflicting memories during recall
### 2.2 Gap Analysis vs External Providers
| Capability | MemPalace (current) | Gap vs External | Priority |
|------------|---------------------|-----------------|----------|
| **Vector search** | ✅ HRR in SQLite | None | — |
| **Keyword/FTS5** | ✅ Built-in | None | — |
| **Knowledge graph** | 🚧 Planned (triple store extension) | Hindsight has this now | Medium |
| **Multi-agent shared memory** | ✅ Per-agent rooms | None | — |
| **Cross-session continuity** | ✅ File-backed | None | — |
| **Real-time sync across agents** | ⚠️ Manual merge | Some providers offer live sync | Low |
| **Hybrid memories (episodic/semantic)** | 🚧 Roadmap | Hindsight's 4-network more mature | Medium |
| **Automated reflection/synthesis** | ⚠️ Basic | Hindsight has `reflect` module | Low |
| **Cloud scaling** | ❌ Not applicable | External providers offer this | N/A — sovereignty trade-off |
**Gap conclusion:** The gaps are **features, not necessities**. None are blocking core workflows. Where gaps exist (graphs, synthesis), the sovereign path is to extend MemPalace locally rather than adopt external lock-in.
---
## 3. Operator Confusion Risk — Multiple Systems Analysis
Running multiple memory backends simultaneously creates:
| Risk | Description | Severity |
|------|-------------|----------|
| **Source ambiguity** | Agent cannot tell which system a fact came from; retrieval order becomes unpredictable | High |
| **State divergence** | Two systems contain different versions of the "same" memory; no single source of truth | High |
| **Debugging opacity** | Troubleshooting recall failures requires examining 2+ independent stores | Medium |
| **Resource duplication** | Same knowledge indexed twice; wasted storage/compute | Low |
| **User training overhead** | Operators must understand which system to query when | Medium |
**Conclusion:** Do **not** run multiple memory systems in parallel. Choose **one** primary sovereign system (MemPalace) and deprecate the rest.
---
## 4. Vendor Lock-in Cost Model
| Lock-in Tier | Annualized Cost of Lock-in | Reason |
|---------------|----------------------------|--------|
| **Zero (OSS self-host)** | $0 | Code + data fully local; exit path is git clone |
| **Partial (OSS but cloud-first)** | $5002000/yr if switching later | Migration effort + downtime while rebuilding |
| **Full (SaaS)** | $500020000+/yr if switching later | Data export may be incomplete; business logic tied to vendor APIs; no source code access |
The **real cost is migration effort**, not subscription fees. Once an external provider's API is woven into agent logic, untangling it is a multi-week rewrite.
---
## 5. Recommendations — Actionable Decision Matrix
### 5.1 Immediate Actions (this sprint)
1. **Consolidate on MemPalace as the single sovereign memory layer** — de-prioritize integration work for external providers unless a proven feature gap blocks a governing issue.
2. **Add a `docs/memory-providers-lockin.md` reference** linking to this evaluation to prevent future accidental adoption of cloud-only providers.
3. **Archive the 8 provider plugin documentation** under `deprecated/` or note in README that external providers are unsupported for production.
### 5.2 Conditional Trials (low-risk evaluation only)
| Provider | Trial Scope | Success Criteria | Go/No-Go Gate |
|----------|-------------|------------------|---------------|
| **Hindsight (local)** | Run in parallel with MemPalace on Allegro-VPS for 1 week; evaluate knowledge graph utility | 15%+ improvement in cross-session recall accuracy without added complexity | Adopt only if gap is proven irreducible in MemPalace |
| **OpenViking** | Single-agent test; compare retrieval precision | Parity or better with simpler architecture | Not a priority — MemPalace L0L4 already sufficient |
| **Mem0 (self-host)** | Docker-compose on test VPS; measure multi-agent memory throughput | 2× speed improvement for 10+ agent scenarios | Low priority — scale issues not currently observed |
**Rule:** No external provider may be promoted to production without a formal ADR and explicit sign-off from the principal (Alexander).
### 5.3 Rejected Providers (do not deploy)
- **Supermemory** — SaaS, data leaves sovereign control
- **RetainDB** — SaaS, $20/mo recurring, exit path unclear
- **ByteRover** — license ambiguity makes compliance impossible to verify
---
## 6. Follow-up Work
| Task | Owner | Status |
|------|-------|--------|
| Document MemPalace as the canonical memory layer in `docs/MEMORY_ARCHITECTURE.md` | Timmy | Todo |
| Add vendor-lock-in decision guardrail to pre-work gate for any memory-related PR | Allegro (CI) | Todo |
| Create migration guide: "Switching from External Provider → MemPalace" (for future-proofing) | Ezra | Optional |
| Extend retrieval_enforcer to explicitly prefer MemPalace over any external plugin | Timmy | In progress |
| Trial Hindsight-local on VPS for 1 week, measure knowledge graph utility | Allegro | Conditional |
---
## 7. Conclusion
The core question — **"do we need an external provider, or does MemPalace already cover us?"** — is answered:
**MemPalace covers us.** The sovereign stack's retrieval layers (L0L4) already solve the recall problem with no external dependencies. The few capabilities external providers advertise (knowledge graphs, multi-agent sync) are either already on MemPalace's roadmap or are not essential.
**Running multiple memory systems creates more problems than it solves.** The operator confusion risk alone is sufficient to avoid parallel deployment. Lock-in risks from cloud-only providers are categorically unacceptable.
**Next step:** Close issue #419 by adopting MemPalace as the production memory layer and documenting provider lock-in guardrails for the fleet.

View File

@@ -0,0 +1,506 @@
#!/usr/bin/env python3
"""
Generate 1,000 Problem→Solution training pairs for Frontend & Creative code patterns.
Part of timmy-config#595: Code Patterns: Frontend & Creative — 1K Problem→Solution Pairs.
Domains covered:
- Three.js: scenes, geometry, materials, lighting, camera, animation
- HTML/CSS/JS: DOM manipulation, events, styling, responsive design
- Playground UI: sovereign-first interactive components
- Gallery: image grids, lightboxes, masonry layouts
- Games: canvas rendering, game loops, simple mechanics
Usage:
python scripts/generate_code_patterns_frontend_creative.py
python scripts/generate_code_patterns_frontend_creative.py --output /path/to/output.jsonl
"""
from __future__ import annotations
import argparse
import json
import random
from pathlib import Path
random.seed(595)
# ============================================================
# Three.js Templates
# ============================================================
THREEJS_TEMPLATES = [
# Scene setup
{
"problem": "Set up a Three.js scene with a renderer, camera, and animation loop.",
"solution": "function initThreeJS(container: HTMLElement) {\n const scene = new THREE.Scene();\n scene.background = new THREE.Color(0x0a0a0a);\n\n const camera = new THREE.PerspectiveCamera(\n 75,\n container.clientWidth / container.clientHeight,\n 0.1,\n 1000\n );\n camera.position.z = 5;\n\n const renderer = new THREE.WebGLRenderer({ antialias: true });\n renderer.setSize(container.clientWidth, container.clientHeight);\n renderer.setPixelRatio(window.devicePixelRatio);\n container.appendChild(renderer.domElement);\n\n function animate() {\n requestAnimationFrame(animate);\n renderer.render(scene, camera);\n }\n animate();\n\n return { scene, camera, renderer };\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-scene",
},
# Geometry — sphere with wireframe
{
"problem": "Create a Three.js sphere with custom segment counts and a wireframe overlay.",
"solution": "function createSphereWithWireframe(radius = 1, segments = 32) {\n const sphereGeom = new THREE.SphereGeometry(radius, segments, segments);\n const sphereMat = new THREE.MeshStandardMaterial({\n color: 0x4a90d9,\n roughness: 0.3,\n metalness: 0.7,\n });\n const sphere = new THREE.Mesh(sphereGeom, sphereMat);\n\n const wireframe = new THREE.LineSegments(\n new THREE.WireframeGeometry(sphereGeom),\n new THREE.LineBasicMaterial({ color: 0xffffff, opacity: 0.3, transparent: true })\n );\n sphere.add(wireframe);\n\n return sphere;\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-geometry",
},
# Materials — PBR
{
"problem": "Apply a physically-based material with environment mapping to a Three.js object.",
"solution": "function createReflectiveMaterial(envMap: THREE.CubeTexture) {\n return new THREE.MeshStandardMaterial({\n color: 0xffffff,\n metalness: 1.0,\n roughness: 0.1,\n envMap: envMap,\n envMapIntensity: 1.0,\n });\n}\n\n// Usage\nconst material = createReflectiveMaterial(cubeTexture);\nconst mesh = new THREE.Mesh(geometry, material);",
"imports": "import * as THREE from 'three';",
"domain": "threejs-materials",
},
# --- Lighting ---
{
"problem": "Create a Three.js lighting setup with ambient, directional, and point lights.",
"solution": "function setupLighting(scene: THREE.Scene) {\n const ambient = new THREE.AmbientLight(0x404040, 0.5);\n scene.add(ambient);\n\n const directional = new THREE.DirectionalLight(0xffffff, 1.0);\n directional.position.set(5, 10, 7);\n directional.castShadow = true;\n directional.shadow.mapSize.width = 2048;\n directional.shadow.mapSize.height = 2048;\n scene.add(directional);\n\n const point = new THREE.PointLight(0xff9000, 0.8, 20);\n point.position.set(-3, 2, 3);\n scene.add(point);\n\n return { ambient, directional, point };\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-lighting",
},
# --- Camera OrbitControls ---
{
"problem": "Implement OrbitControls camera with constrained polar angles and smooth damping.",
"solution": "function setupOrbitControls(camera: THREE.PerspectiveCamera, domElement: HTMLElement) {\n const controls = new THREE.OrbitControls(camera, domElement);\n controls.enableDamping = true;\n controls.dampingFactor = 0.05;\n controls.minDistance = 2;\n controls.maxDistance = 20;\n controls.maxPolarAngle = Math.PI / 2;\n controls.minPolarAngle = Math.PI / 6;\n controls.enablePan = false;\n return controls;\n}",
"imports": "import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';",
"domain": "threejs-camera",
},
# --- Delta-time rotation ---
{
"problem": "Create a smooth Three.js rotation animation using delta time.",
"solution": "class RotatingObject {\n mesh: THREE.Mesh;\n speed: number;\n\n constructor(mesh: THREE.Mesh, rotationsPerSecond = 0.5) {\n this.mesh = mesh;\n this.speed = rotationsPerSecond * Math.PI * 2;\n }\n\n update(deltaSec: number) {\n this.mesh.rotation.y += this.speed * deltaSec;\n }\n}\n\n// In render loop:\nconst rotor = new RotatingObject(sphere, 0.25);\nlet last = performance.now();\nfunction animate(time: number) {\n const delta = (time - last) / 1000;\n last = time;\n rotor.update(delta);\n renderer.render(scene, camera);\n requestAnimationFrame(animate);\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-animation",
},
# --- Texture loading async ---
{
"problem": "Load a Three.js texture asynchronously with proper error handling.",
"solution": "async function loadTexture(url: string): Promise<THREE.Texture> {\n const loader = new THREE.TextureLoader();\n try {\n return await new Promise<THREE.Texture>((resolve, reject) => {\n loader.load(url, resolve, undefined, reject);\n });\n } catch (err) {\n console.error('Texture load failed:', url, err);\n throw err;\n }\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-textures",
},
# --- Rounded box ---
{
"problem": "Create a rounded-box Three.js geometry using RoundedBoxGeometry.",
"solution": "function createRoundedBox(width = 1, height = 1, depth = 1, segments = 2, radius = 0.1) {\n const geom = new THREE.RoundedBoxGeometry(width, height, depth, segments, radius);\n const mat = new THREE.MeshStandardMaterial({ color: 0x2ecc71 });\n return new THREE.Mesh(geom, mat);\n}",
"imports": "import { RoundedBoxGeometry } from 'three/examples/jsm/geometries/RoundedBoxGeometry.js';",
"domain": "threejs-geometry",
},
# --- Fog ---
{
"problem": "Add depth fog to a Three.js scene for atmospheric perspective.",
"solution": "function addFog(scene: THREE.Scene, color = 0x0a0a0a, near = 10, far = 50) {\n scene.fog = new THREE.Fog(color, near, far);\n scene.background = new THREE.Color(color);\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-scene",
},
# --- ShaderMaterial ---
{
"problem": "Create a Three.js ShaderMaterial with uniform updates in the render loop.",
"solution": "function createGlowShader() {\n return new THREE.ShaderMaterial({\n uniforms: {\n uTime: { value: 0 },\n uColor: { value: new THREE.Color(0x00ffff) },\n },\n vertexShader: `\n varying vec2 vUv;\n void main() {\n vUv = uv;\n gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);\n }\n `,\n fragmentShader: `\n uniform float uTime;\n uniform vec3 uColor;\n varying vec2 vUv;\n void main() {\n float pulse = 0.5 + 0.5 * sin(uTime * 2.0);\n gl_FragColor = vec4(uColor * pulse, 1.0);\n }\n `,\n transparent: true,\n });\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-materials",
},
# --- Group hierarchy ---
{
"problem": "Organize Three.js objects into a hierarchical group with local transforms.",
"solution": "function createVehicleGroup() {\n const chassis = new THREE.Mesh(\n new THREE.BoxGeometry(2, 0.5, 4),\n new THREE.MeshStandardMaterial({ color: 0x333333 })\n );\n\n const wheels = new THREE.Group();\n const positions = [[-1, -0.3, -1.2], [1, -0.3, -1.2], [-1, -0.3, 1.2], [1, -0.3, 1.2]];\n positions.forEach(([x, y, z]) => {\n const wheel = new THREE.Mesh(\n new THREE.CylinderGeometry(0.3, 0.3, 0.2, 16),\n new THREE.MeshStandardMaterial({ color: 0x111111 })\n );\n wheel.rotation.z = Math.PI / 2;\n wheel.position.set(x, y, z);\n wheels.add(wheel);\n });\n\n const group = new THREE.Group();\n group.add(chassis);\n group.add(wheels);\n return group;\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-scene",
},
# --- Raycasting ---
{
"problem": "Implement Three.js raycaster click picking with object metadata.",
"solution": "function setupRaycaster(camera: THREE.Camera, dom: HTMLElement) {\n const raycaster = new THREE.Raycaster();\n const mouse = new THREE.Vector2();\n\n dom.addEventListener('click', (e) => {\n const rect = dom.getBoundingClientRect();\n mouse.x = ((e.clientX - rect.left) / rect.width) * 2 - 1;\n mouse.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;\n\n raycaster.setFromCamera(mouse, camera);\n const intersects = raycaster.intersectObjects(scene.children, true);\n if (intersects.length > 0) {\n const hit = intersects[0].object;\n console.log('Clicked:', hit.userData.name || hit.uuid);\n }\n });\n\n return raycaster;\n}",
"imports": "import * as THREE from 'three';",
"domain": "threejs-interaction",
},
]
# ============================================================
# HTML/CSS/JS Templates
# ============================================================
HTML_CSS_JS_TEMPLATES = [
# --- DOM element creation ---
{
"problem": "Create a DOM element with multiple classes and attributes in vanilla JavaScript.",
"solution": "function createElement(tag: string, classes: string[] = [], attrs: Record<string, string> = {}, children: Node[] = []) {\n const el = document.createElement(tag);\n el.classList.add(...classes);\n for (const [key, value] of Object.entries(attrs)) {\n el.setAttribute(key, value);\n }\n for (const child of children) {\n el.appendChild(child);\n }\n return el;\n}\n\n// Usage\nconst button = createElement('button', ['btn', 'btn-primary'], { 'aria-label': 'Submit' }, [\n document.createTextNode('Submit')\n]);",
"imports": "",
"domain": "html-dom",
},
# --- Event delegation ---
{
"problem": "Implement event delegation for dynamic button clicks with proper type checking.",
"solution": "function setupEventDelegation(container: HTMLElement) {\n container.addEventListener('click', (e) => {\n const target = e.target as HTMLElement;\n if (!target.matches('button[data-action]')) return;\n\n const action = target.getAttribute('data-action');\n switch (action) {\n case 'save':\n handleSave();\n break;\n case 'delete':\n handleDelete();\n break;\n default:\n console.warn('Unknown action:', action);\n }\n });\n}",
"imports": "",
"domain": "html-dom",
},
# --- Form validation ---
{
"problem": "Validate a form submission with HTML5 constraints and custom checks.",
"solution": "function validateForm(form: HTMLFormElement): { isValid: boolean; errors: string[] } {\n const errors: string[] = [];\n const email = form.elements.namedItem('email') as HTMLInputElement;\n const password = form.elements.namedItem('password') as HTMLInputElement;\n\n if (!email.validity.valid) {\n errors.push('Please enter a valid email address.');\n }\n if (password.value.length < 8) {\n errors.push('Password must be at least 8 characters.');\n }\n if (password.value !== (form.elements.namedItem('confirm') as HTMLInputElement).value) {\n errors.push('Passwords do not match.');\n }\n\n return { isValid: errors.length === 0, errors };\n}",
"imports": "",
"domain": "html-forms",
},
# --- CSS Grid ---
{
"problem": "Create a responsive CSS grid layout with auto-fill and gap.",
"solution": "const style = document.createElement('style');\nstyle.textContent = `\n .card-grid {\n display: grid;\n grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));\n gap: 1.5rem;\n padding: 1rem;\n }\n .card {\n background: var(--card-bg);\n border-radius: 8px;\n box-shadow: 0 2px 8px rgba(0,0,0,0.1);\n }\n @media (max-width: 600px) {\n .card-grid { grid-template-columns: 1fr; }\n }\n`;\ndocument.head.appendChild(style);",
"imports": "",
"domain": "css-layout",
},
# --- CSS custom properties ---
{
"problem": "Set and read CSS custom properties (CSS variables) via JavaScript.",
"solution": "function setThemeColor(root: HTMLElement, name: string, value: string) {\n root.style.setProperty(`--theme-${name}`, value);\n}\n\nfunction getComputedColor(root: HTMLElement, name: string): string {\n return getComputedStyle(root).getPropertyValue(`--theme-${name}`).trim();\n}\n\n// Initialize theme\nsetThemeColor(document.documentElement, 'primary', '#4a90d9');\nsetThemeColor(document.documentElement, 'accent', '#ff6b6b');",
"imports": "",
"domain": "css-variables",
},
# --- Intersection Observer ---
{
"problem": "Use IntersectionObserver to lazy-load images when they enter the viewport.",
"solution": "function setupLazyLoading(container: HTMLElement) {\n const images = container.querySelectorAll('img[data-src]');\n const observer = new IntersectionObserver((entries) => {\n entries.forEach(entry => {\n if (entry.isIntersecting) {\n const img = entry.target as HTMLImageElement;\n img.src = img.dataset.src!;\n img.removeAttribute('data-src');\n observer.unobserve(img);\n }\n });\n }, { rootMargin: '50px' });\n\n images.forEach(img => observer.observe(img));\n}",
"imports": "",
"domain": "html-performance",
},
]
# ============================================================
# Playground UI Templates
# ============================================================
PLAYGROUND_UI_TEMPLATES = [
# --- Sovereignty badge ---
{
"problem": "Render a sovereignty badge displaying local-first status with tooltip.",
"solution": "function SovereigntyBadge({ runningLocal }: { runningLocal: boolean }) {\n const badge = document.createElement('span');\n badge.className = 'sovereignty-badge';\n badge.innerHTML = runningLocal\n ? '\\ud83c\\uddf5\\ud83c\\uddf1\\u200d\\ud83c\\udfa8\\ufe0f Local'\n : '\\ud83d\\udd12 Cloud';\n badge.title = runningLocal\n ? 'This agent runs entirely on your machine'\n : 'This agent uses external inference';\n return badge;\n}",
"imports": "",
"domain": "playground-ui",
},
# --- Token counter ---
{
"problem": "Build a token budget display showing used/total with a visual progress bar.",
"solution": "function TokenBudgetDisplay({ used, total }: { used: number; total: number }) {\n const pct = Math.min((used / total) * 100, 100);\n const bar = document.createElement('div');\n bar.className = 'token-budget-bar';\n bar.innerHTML = `\n <div class=\"track\">\n <div class=\"fill\" style=\"width: ${pct}%; background: ${pct > 90 ? '#f44336' : '#4caf50'}\"></div>\n </div>\n <span class=\"label\">${used.toLocaleString()} / ${total.toLocaleString()} tokens</span>\n `;\n return bar;\n}",
"imports": "",
"domain": "playground-ui",
},
# --- Approval gate ---
{
"problem": "Create an approval gate component for dangerous commands with tiered risk colors.",
"solution": "function ApprovalGate({ risk, onApprove, onDeny }: {\n risk: 'low' | 'medium' | 'high';\n onApprove: () => void;\n onDeny: () => void;\n}) {\n const colors = { low: '#4caf50', medium: '#ff9800', high: '#f44336' };\n const panel = document.createElement('div');\n panel.className = 'approval-gate';\n panel.style.borderColor = colors[risk];\n panel.innerHTML = `\n <p>This action is <strong>${risk} risk</strong>. Continue?</p>\n <button data-action=\"approve\">Yes, proceed</button>\n <button data-action=\"deny\">No, cancel</button>\n `;\n panel.querySelector('[data-action=\"approve\"]')!.addEventListener('click', onApprove);\n panel.querySelector('[data-action=\"deny\"]')!.addEventListener('click', onDeny);\n return panel;\n}",
"imports": "",
"domain": "playground-ui",
},
# --- Skill card ---
{
"problem": "Render a skill card with metadata, status indicator, and toggle switch.",
"solution": "function SkillCard({ skill, enabled, onToggle }: {\n skill: { name: string; description: string; category: string };\n enabled: boolean;\n onToggle: (name: string) => void;\n}) {\n const card = document.createElement('article');\n card.className = 'skill-card';\n card.innerHTML = `\n <header>\n <h3>${skill.name}</h3>\n <label class=\"toggle\">\n <input type=\"checkbox\" ${enabled ? 'checked' : ''}>\n <span class=\"slider\"></span>\n </label>\n </header>\n <p>${skill.description}</p>\n <footer>Category: ${skill.category}</footer>\n `;\n card.querySelector('input')!.addEventListener('change', () => onToggle(skill.name));\n return card;\n}",
"imports": "",
"domain": "playground-ui",
},
]
# ============================================================
# Gallery Templates
# ============================================================
GALLERY_TEMPLATES = [
# --- Masonry grid ---
{
"problem": "Implement a responsive masonry image grid using CSS columns.",
"solution": "function createMasonryGallery(images: { src: string; alt: string }[], columns = 3) {\n const container = document.createElement('div');\n container.className = 'masonry-gallery';\n container.style.columnCount = String(columns);\n container.style.gap = '1rem';\n\n images.forEach(img => {\n const figure = document.createElement('figure');\n figure.innerHTML = `<img src=\"${img.src}\" alt=\"${img.alt}\" loading=\"lazy\">`;\n container.appendChild(figure);\n });\n\n // Responsive breakpoints\n const mq = window.matchMedia('(max-width: 768px)');\n mq.addEventListener('change', (e) => {\n container.style.columnCount = e.matches ? '2' : String(columns);\n });\n\n return container;\n}",
"imports": "",
"domain": "gallery-layout",
},
# --- Lightbox modal ---
{
"problem": "Build a modal lightbox for full-screen image viewing with keyboard navigation.",
"solution": "class Lightbox {\n private overlay!: HTMLElement;\n private img!: HTMLImageElement;\n\n constructor() {\n this.overlay = document.createElement('div');\n this.overlay.className = 'lightbox-overlay';\n this.overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.9);display:flex;align-items:center;justify-content:center;z-index:9999';\n this.img = document.createElement('img');\n this.overlay.appendChild(this.img);\n document.body.appendChild(this.overlay);\n\n this.overlay.addEventListener('click', () => this.close());\n document.addEventListener('keydown', (e) => e.key === 'Escape' && this.close());\n }\n\n open(src: string, alt: string) {\n this.img.src = src;\n this.img.alt = alt;\n this.overlay.style.display = 'flex';\n }\n\n close() {\n this.overlay.style.display = 'none';\n }\n}",
"imports": "",
"domain": "gallery-interaction",
},
# --- Infinite scroll ---
{
"problem": "Implement infinite scroll loading with IntersectionObserver and abort handling.",
"solution": "async function setupInfiniteScroll(container: HTMLElement, loadPage: (page: number) => Promise<void>) {\n let page = 1;\n let loading = false;\n let done = false;\n\n const sentinel = document.createElement('div');\n sentinel.className = 'scroll-sentinel';\n container.appendChild(sentinel);\n\n const observer = new IntersectionObserver(async (entries) => {\n if (entries[0].isIntersecting && !loading && !done) {\n loading = true;\n try {\n await loadPage(++page);\n } catch (err) {\n console.error('Failed to load page:', err);\n done = true;\n }\n loading = false;\n }\n }, { rootMargin: '200px' });\n\n observer.observe(sentinel);\n}",
"imports": "",
"domain": "gallery-performance",
},
]
# ============================================================
# Game Templates
# ============================================================
GAME_TEMPLATES = [
# --- Game loop ---
{
"problem": "Create a fixed-timestep game loop with accumulator pattern.",
"solution": "class GameLoop {\n private lastTime = 0;\n private accumulator = 0;\n private readonly step = 1 / 60; // 60 Hz fixed step\n\n constructor(private readonly update: (dt: number) => void) {}\n\n start() {\n const frame = (time: number) => {\n const delta = (time - this.lastTime) / 1000;\n this.lastTime = time;\n this.accumulator += delta;\n\n while (this.accumulator >= this.step) {\n this.update(this.step);\n this.accumulator -= this.step;\n }\n\n requestAnimationFrame(frame);\n };\n requestAnimationFrame(frame);\n }\n}",
"imports": "",
"domain": "game-architecture",
},
# --- Canvas setup ---
{
"problem": "Set up an HTML5 canvas with high-DPI scaling and clearing.",
"solution": "function setupCanvas(canvas: HTMLCanvasElement, width = 800, height = 600) {\n const dpr = window.devicePixelRatio || 1;\n canvas.width = width * dpr;\n canvas.height = height * dpr;\n canvas.style.width = `${width}px`;\n canvas.style.height = `${height}px`;\n\n const ctx = canvas.getContext('2d')!;\n ctx.scale(dpr, dpr);\n\n return {\n clear() { ctx.clearRect(0, 0, width, height); },\n ctx,\n width,\n height,\n };\n}",
"imports": "",
"domain": "game-rendering",
},
# --- Sprite animation ---
{
"problem": "Animate a sprite sheet with frame-based playback and loop support.",
"solution": "class SpriteAnimator {\n private frame = 0;\n private lastTick = 0;\n\n constructor(\n private readonly image: HTMLImageElement,\n private readonly frameWidth: number,\n private readonly frameCount: number,\n private readonly fps: number = 12,\n private readonly loop: boolean = true,\n ) {}\n\n update(now: number) {\n const interval = 1000 / this.fps;\n if (now - this.lastTick >= interval) {\n this.lastTick = now;\n this.frame++;\n if (this.frame >= this.frameCount) {\n this.frame = this.loop ? 0 : this.frameCount - 1;\n }\n }\n }\n\n draw(ctx: CanvasRenderingContext2D, x: number, y: number) {\n ctx.drawImage(\n this.image,\n this.frame * this.frameWidth, 0,\n this.frameWidth, this.image.height,\n x, y,\n this.frameWidth, this.image.height\n );\n }\n}",
"imports": "",
"domain": "game-assets",
},
# --- AABB collision ---
{
"problem": "Detect AABB (axis-aligned bounding box) collision between two rectangles.",
"solution": "function aabbCollision(\n a: { x: number; y: number; w: number; h: number },\n b: { x: number; y: number; w: number; h: number }\n): boolean {\n return a.x < b.x + b.w &&\n a.x + a.w > b.x &&\n a.y < b.y + b.h &&\n a.y + a.h > b.y;\n}\n\n// Usage for game entities\nif (aabbCollision(player, enemy)) {\n handlePlayerHit();\n}",
"imports": "",
"domain": "game-physics",
},
# --- Input handling ---
{
"problem": "Capture keyboard input state with smooth handling for game controls.",
"solution": "class InputState {\n private keys = new Set<string>();\n\n constructor() {\n window.addEventListener('keydown', (e) => this.keys.add(e.code));\n window.addEventListener('keyup', (e) => this.keys.delete(e.code));\n }\n\n isPressed(code: string): boolean {\n return this.keys.has(code);\n }\n\n hasAny(codes: string[]): boolean {\n return codes.some(c => this.keys.has(c));\n }\n}\n\n// In game loop:\nconst input = new InputState();\nif (input.isPressed('ArrowUp')) player.y -= speed * dt;",
"imports": "",
"domain": "game-input",
},
]
# ============================================================
# Extra HTML/CSS/JS Templates
# ============================================================
HTML_CSS_JS_TEMPLATES_EXTRA = [
# Debounce utility
{
"problem": "Write a debounce function that delays invoking a callback until after wait milliseconds.",
"solution": "function debounce<T extends (...args: any[]) => void>(\n fn: T,\n wait: number\n): (...args: Parameters<T>) => void {\n let timeoutId: ReturnType<typeof setTimeout> | null = null;\n return (...args: Parameters<T>) => {\n if (timeoutId) clearTimeout(timeoutId);\n timeoutId = setTimeout(() => fn(...args), wait);\n };\n}",
"imports": "",
"domain": "html-utilities",
},
# Throttle utility
{
"problem": "Implement a throttle function ensuring a callback runs at most once per interval.",
"solution": "function throttle<T extends (...args: any[]) => void>(\n fn: T,\n interval: number\n): (...args: Parameters<T>) => void {\n let last = 0;\n return (...args: Parameters<T>) => {\n const now = Date.now();\n if (now - last >= interval) {\n last = now;\n fn(...args);\n }\n };\n}",
"imports": "",
"domain": "html-utilities",
},
# LocalStorage wrapper with TTL
{
"problem": "Wrap localStorage with JSON serialization and TTL expiration.",
"solution": "class StorageWithTTL {\n set(key: string, value: any, ttlMs = 0) {\n const item = { value, expiry: ttlMs ? Date.now() + ttlMs : null };\n localStorage.setItem(key, JSON.stringify(item));\n }\n\n get<T>(key: string): T | null {\n const raw = localStorage.getItem(key);\n if (!raw) return null;\n const { value, expiry } = JSON.parse(raw);\n if (expiry && Date.now() > expiry) {\n localStorage.removeItem(key);\n return null;\n }\n return value as T;\n }\n}",
"imports": "",
"domain": "html-storage",
},
# Viewport meta
{
"problem": "Generate a responsive viewport meta tag for mobile-first web apps.",
"solution": "const viewport = document.querySelector('meta[name=\"viewport\"]') ||\n document.createElement('meta');\nviewport.name = 'viewport';\nviewport.content = 'width=device-width, initial-scale=1.0, maximum-scale=5.0, user-scalable=yes, viewport-fit=cover';\ndocument.head.appendChild(viewport);",
"imports": "",
"domain": "html-meta",
},
# Dynamic CSS variables
{
"problem": "Create and inject a dynamic stylesheet with CSS custom property overrides.",
"solution": "function injectDynamicStyles(overrides: Record<string, string>) {\n const style = document.createElement('style');\n let css = ':root {\\n';\n for (const [prop, val] of Object.entries(overrides)) {\n css += ` --${prop}: ${val};\\n`;\n }\n css += '}';\n style.textContent = css;\n document.head.appendChild(style);\n}",
"imports": "",
"domain": "css-variables",
},
]
# ============================================================
# Extra Playground UI Templates
# ============================================================
PLAYGROUND_UI_TEMPLATES_EXTRA = [
# Circuit/tier badge
{
"problem": "Render a circuit health badge showing approval-tier status with color-coded indicator.",
"solution": "function CircuitBadge({ tier }: { tier: number }) {\n const colors = ['#f44336', '#ff9800', '#4caf50', '#2196f3', '#9c27b0'];\n const labels = ['BLOCKED', 'RESTRICTED', 'LIMITED', 'APPROVED', 'ELEVATED'];\n const color = colors[Math.min(tier, 4)];\n const label = labels[Math.min(tier, 4)];\n\n const badge = document.createElement('span');\n badge.className = 'circuit-badge';\n badge.style.backgroundColor = color;\n badge.textContent = label;\n badge.title = `Approval tier ${tier} — ${label.toLowerCase()} command set`;\n return badge;\n}",
"imports": "",
"domain": "playground-ui",
},
# Memory usage bar
{
"problem": "Display a horizontal memory usage bar with gradient warning zones.",
"solution": "function MemoryBar({ used, total }: { used: number; total: number }) {\n const pct = (used / total) * 100;\n const bar = document.createElement('div');\n bar.className = 'memory-bar';\n let color = '#4caf50';\n if (pct > 80) color = '#ff9800';\n if (pct > 95) color = '#f44336';\n\n bar.innerHTML = `\n <div class=\"track\" style=\"background: #e0e0e0; height: 8px; border-radius: 4px; overflow: hidden;\">\n <div style=\"width: ${pct}%; height: 100%; background: ${color}; transition: width 0.3s;\"></div>\n </div>\n <span>${(used/1024/1024).toFixed(1)} MB / ${(total/1024/1024).toFixed(1)} MB</span>\n `;\n return bar;\n}",
"imports": "",
"domain": "playground-ui",
},
# Tool status dot
{
"problem": "Show a tool availability status dot with tooltip for the toolset panel.",
"solution": "function ToolStatus({ name, ok }: { name: string; ok: boolean }) {\n const dot = document.createElement('span');\n dot.className = 'tool-status-dot';\n dot.style.backgroundColor = ok ? '#4caf50' : '#f44336';\n dot.title = `${name}: ${ok ? 'Available' : 'Disabled / missing API key'}`;\n return dot;\n}",
"imports": "",
"domain": "playground-ui",
},
]
# ============================================================
# Extra Gallery Templates
# ============================================================
GALLERY_TEMPLATES_EXTRA = [
# Grid + shared lightbox
{
"problem": "Build an image gallery grid that opens a shared lightbox on thumbnail click.",
"solution": "let currentLightbox: HTMLDivElement | null = null;\n\nfunction buildGallery(images: { full: string; thumb: string; alt: string }[]) {\n const grid = document.createElement('div');\n grid.className = 'gallery-grid';\n grid.style.cssText = 'display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:0.5rem';\n\n images.forEach((img, idx) => {\n const thumb = document.createElement('img');\n thumb.src = img.thumb;\n thumb.alt = img.alt;\n thumb.style.cssText = 'cursor:pointer;width:100%;height:auto;object-fit:cover;border-radius:4px';\n thumb.addEventListener('click', () => openLightbox(idx));\n grid.appendChild(thumb);\n });\n\n return grid;\n}\n\nfunction openLightbox(index: number) {\n if (currentLightbox) currentLightbox.remove();\n currentLightbox = document.createElement('div');\n currentLightbox.className = 'lightbox';\n currentLightbox.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.95);display:flex;align-items:center;justify-content:center;z-index:10000;cursor:pointer';\n const img = document.createElement('img');\n img.src = images[index].full;\n img.style.maxWidth = '90vw';\n img.style.maxHeight = '90vh';\n currentLightbox.appendChild(img);\n currentLightbox.addEventListener('click', () => { currentLightbox?.remove(); currentLightbox = null; });\n document.body.appendChild(currentLightbox);\n}",
"imports": "",
"domain": "gallery-interaction",
},
]
# ============================================================
# Extra Game Templates
# ============================================================
GAME_TEMPLATES_EXTRA = [
# Particle system with typed array
{
"problem": "Create a simple particle system for explosions using a typed array buffer.",
"solution": "class ParticleSystem {\n private particles = new Float32Array(1000 * 4); // x, y, vx, vy per particle\n private count = 0;\n private readonly max = 1000;\n\n emit(x: number, y: number, velocity = 200) {\n if (this.count >= this.max) return;\n const i = this.count * 4;\n this.particles[i] = x;\n this.particles[i + 1] = y;\n const angle = Math.random() * Math.PI * 2;\n const speed = Math.random() * velocity;\n this.particles[i + 2] = Math.cos(angle) * speed;\n this.particles[i + 3] = Math.sin(angle) * speed;\n this.count++;\n }\n\n update(dt: number) {\n for (let i = 0; i < this.count * 4; i += 4) {\n this.particles[i] += this.particles[i + 2] * dt;\n this.particles[i + 1] += this.particles[i + 3] * dt;\n this.particles[i + 3] += 500 * dt; // gravity\n }\n }\n\n draw(ctx: CanvasRenderingContext2D) {\n ctx.fillStyle = '#ff6600';\n for (let i = 0; i < this.count * 4; i += 4) {\n ctx.fillRect(this.particles[i], this.particles[i + 1], 3, 3);\n }\n }\n}",
"imports": "",
"domain": "game-physics",
},
# State machine
{
"problem": "Implement a finite state machine for a game character with transitions.",
"solution": "type State = 'idle' | 'walk' | 'run' | 'jump' | 'attack';\n\nclass StateMachine {\n private state: State = 'idle';\n private handlers: Record<State, (event: string) => void>;\n\n constructor(handlers: Partial<Record<State, (event: string) => void>>) {\n this.handlers = handlers as Record<State, (event: string) => void>;\n }\n\n transition(to: State) {\n console.log(`State: ${this.state} -> ${to}`);\n this.state = to;\n }\n\n dispatch(event: string) {\n const handler = this.handlers[this.state];\n if (handler) handler(event);\n }\n\n getState(): State {\n return this.state;\n }\n}\n\n// Usage\nconst sm = new StateMachine({\n idle: (e) => { if (e === 'move') sm.transition('walk'); },\n walk: (e) => { if (e === 'sprint') sm.transition('run'); if (e === 'jump') sm.transition('jump'); },\n run: (e) => { if (e === 'stop') sm.transition('idle'); },\n});",
"imports": "",
"domain": "game-architecture",
},
]
# ============================================================
# Combined
# ============================================================
ALL_TEMPLATES = (
THREEJS_TEMPLATES
+ HTML_CSS_JS_TEMPLATES
+ HTML_CSS_JS_TEMPLATES_EXTRA
+ PLAYGROUND_UI_TEMPLATES
+ PLAYGROUND_UI_TEMPLATES_EXTRA
+ GALLERY_TEMPLATES
+ GALLERY_TEMPLATES_EXTRA
+ GAME_TEMPLATES
+ GAME_TEMPLATES_EXTRA
)
_VARIANT_PREFIXES = [
"Write code to",
"Implement",
"Build",
"Create",
"How would you",
"Using the API, write code that",
"Construct a function that",
"Develop",
"Write JavaScript that",
"Create HTML/CSS for",
"Design a Three.js",
]
_VARIANT_SUFFIXES = [
" including error handling.",
" with full docstrings.",
" with JSDoc annotations.",
" using modern best practices.",
" that handles edge cases.",
" with TypeScript types.",
" that is performant.",
" with clear variable names.",
" and include example usage.",
" with proper cleanup.",
" that is accessible (a11y).",
" with keyboard navigation support.",
]
def vary_problem(base: str, idx: int) -> str:
prefix = _VARIANT_PREFIXES[idx % len(_VARIANT_PREFIXES)]
suffix = _VARIANT_SUFFIXES[idx % len(_VARIANT_SUFFIXES)]
cleaned = base
for article in ("Create a ", "Build a ", "Implement a ", "Write a ", "Develop a ", "Write JavaScript that ", "Create HTML/CSS for ", "Design a Three.js "):
if cleaned.lower().startswith(article):
cleaned = cleaned[len(article):]
break
cleaned = cleaned[0].lower() + cleaned[1:] if cleaned else ""
return f"{prefix} {cleaned}{suffix}"
def vary_solution(base: str, idx: int) -> str:
var_names = ["data", "result", "value", "entry", "item", "node", "entity", "output", "obj", "element"]
v = var_names[idx % len(var_names)]
sol = base
if idx % 3 == 0:
for original in ["result", "data", "value", "output", "entry", "item", "obj", "element"]:
if original in sol:
sol = sol.replace(original, v)
break
if idx % 5 == 0:
sol = f"// Variation {idx}\\n" + sol
elif idx % 7 == 0:
sol = f"# Generated variation {idx}\\n" + sol
return sol
def generate_pairs(count: int = 1000) -> list[dict]:
pairs = []
template_cycle = list(ALL_TEMPLATES)
random.shuffle(template_cycle)
for i in range(count):
template = template_cycle[i % len(template_cycle)]
problem = vary_problem(template["problem"], i)
solution = vary_solution(template["solution"], i)
pair = {
"problem": problem,
"solution": solution,
"imports": template["imports"],
"domain": template["domain"],
"id": f"frontend-creative-{i:04d}",
}
pairs.append(pair)
return pairs
def main():
parser = argparse.ArgumentParser(description="Generate Frontend & Creative code pattern training pairs")
parser.add_argument("--output", "-o", default="training-data/code-patterns-frontend-&-creative.jsonl",
help="Output JSONL path")
parser.add_argument("--count", "-n", type=int, default=1000,
help="Number of pairs to generate")
args = parser.parse_args()
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
pairs = generate_pairs(args.count)
with open(out_path, "w", encoding="utf-8") as f:
for pair in pairs:
f.write(json.dumps(pair, ensure_ascii=False) + "\n")
domains = {p["domain"] for p in pairs}
print(f"Generated {len(pairs)} code pattern pairs → {out_path}")
print(f" Size: {out_path.stat().st_size / 1024:.1f} KB")
print(f" Domains ({len(domains)}): {sorted(domains)}")
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff