.csv`, etc.
+- Implement `fetchFirstAvailable(paths)`; try in order with `cache:'no-cache'`; handle errors gracefully with a red `` message.
+- For images or JSON models, mirror the same approach (see `d3-comparison.html`, `d3-neural.html`).
+
+#### 6.1) Data props (HtmlEmbed → embed)
+
+- HtmlEmbed accepts an optional `data` prop that can be a string (single file) or an array of strings (multiple files).
+- This prop is passed to the fragment via the `data-datafiles` HTML attribute.
+- In the embed script, read this attribute from the closest ancestor that carries it (the `HtmlEmbed` wrapper), not necessarily the chart’s direct container.
+- Recommended normalization: if a value contains no slash, automatically prefix it with `/data/` to target the public data folder.
+- If `data` is not provided, keep the usual fallback (public, then `assets/data`).
+
+Optional configuration (e.g., default metric):
+
+```js
+// In HtmlEmbed usage (MDX):
+
+
+// In the embed script (read from closest ancestor):
+let mountEl = container;
+while (mountEl && !mountEl.getAttribute?.('data-datafiles') && !mountEl.getAttribute?.('data-config')) {
+ mountEl = mountEl.parentElement;
+}
+let providedConfig = null;
+try {
+ const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null;
+ if (cfg && cfg.trim()) providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg;
+} catch(_) {}
+// Example: selecting initial metric if present
+const desired = providedConfig && providedConfig.defaultMetric ? String(providedConfig.defaultMetric) : null;
+```
+
+Examples (MDX):
+
+```mdx
+
+
+
+
+```
+
+Reading on the embed side (JS):
+
+```js
+// Find the closest ancestor that carries the attribute
+let mountEl = container;
+while (mountEl && !mountEl.getAttribute?.('data-datafiles')) {
+ mountEl = mountEl.parentElement;
+}
+let providedData = null;
+try {
+ const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null;
+ if (attr && attr.trim()) {
+ providedData = attr.trim().startsWith('[') ? JSON.parse(attr) : attr.trim();
+ }
+} catch(_) {}
+
+const DEFAULT_CSV = '/data/formatting_filters.csv';
+const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p;
+const normalizeInput = (inp) => Array.isArray(inp)
+ ? inp.map(ensureDataPrefix)
+ : (typeof inp === 'string' ? [ ensureDataPrefix(inp) ] : null);
+
+const CSV_PATHS = Array.isArray(providedData)
+ ? normalizeInput(providedData)
+ : (typeof providedData === 'string' ? normalizeInput(providedData) || [DEFAULT_CSV] : [
+ DEFAULT_CSV,
+ './assets/data/formatting_filters.csv',
+ '../assets/data/formatting_filters.csv',
+ '../../assets/data/formatting_filters.csv'
+ ]);
+
+const fetchFirstAvailable = async (paths) => {
+ for (const p of paths) {
+ try {
+ const r = await fetch(p, { cache: 'no-cache' });
+ if (r.ok) return await r.text();
+ } catch(_){}
+ }
+ throw new Error('CSV not found');
+};
+```
+
+### 7) Responsiveness and layout
+- Compute `width = container.clientWidth`, and a height derived from width (e.g., `width / 3`), with a sensible minimum height.
+- Maintain a `margin` object and derive `innerWidth/innerHeight` for plots.
+- Use a `ResizeObserver` on the container; fallback to `window.resize`.
+- Recompute scales/axes/grid on every render.
+
+### 8) Legends and labels
+- Prefer HTML for legends for wrapping and accessibility; avoid SVG-based legends.
+- Always add axis labels when applicable (e.g., `Step`, `Value`).
+- Standardize legend swatch size: 14×14px, border-radius 3px, 1px border `var(--border-color)`.
+
+#### 8.1) Required legend title: "Legend"
+- Always render a visible title above legend items with the exact text "Legend".
+- Canonical markup:
+
+```html
+
+```
+
+Minimal CSS (match project styles):
+
+```css
+.legend { display:flex; flex-direction:column; align-items:flex-start; gap:6px; }
+.legend-title { font-size:12px; font-weight:700; color: var(--text-color); }
+.legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; }
+.legend .item { display:inline-flex; align-items:center; gap:6px; white-space:nowrap; font-size:12px; color: var(--text-color); }
+.legend .swatch { width:14px; height:14px; border-radius:3px; border:1px solid var(--border-color); }
+```
+
+Recommended JS pattern to (re)build the legend:
+
+```js
+function makeLegend(seriesNames, colorFor) {
+ let legend = container.querySelector('.legend');
+ if (!legend) { legend = document.createElement('div'); legend.className = 'legend'; container.appendChild(legend); }
+ let title = legend.querySelector('.legend-title'); if (!title) { title = document.createElement('div'); title.className = 'legend-title'; title.textContent = 'Legend'; legend.appendChild(title); }
+ let items = legend.querySelector('.items'); if (!items) { items = document.createElement('div'); items.className = 'items'; legend.appendChild(items); }
+ items.innerHTML = '';
+ seriesNames.forEach(name => {
+ const el = document.createElement('span'); el.className = 'item';
+ const sw = document.createElement('span'); sw.className = 'swatch'; sw.style.background = colorFor(name);
+ const txt = document.createElement('span'); txt.textContent = name;
+ el.appendChild(sw); el.appendChild(txt); items.appendChild(el);
+ });
+}
+```
+
+### 9) Accessibility
+- Provide `alt` attributes on ` ` (see `d3-comparison.html`).
+- Provide `aria-label` on interactive buttons (e.g., the erase button in `d3-neural.html`).
+- Ensure focus-visible styles for interactive controls; avoid relying on color alone to encode meaning.
+
+### 10) Performance and updates
+- Use D3 data joins (`.data().join()` or explicit enter/merge/exit) and keep transitions short (≤200ms).
+- Recompute only what is necessary on each render; avoid repeated DOM clears if not needed.
+- Debounce or gate expensive computations, especially on `mousemove`.
+
+### 11) External dependencies
+- Load D3 (and optional TFJS) via CDN only once using an element id (e.g., `d3-cdn-script`, `tfjs-cdn-script`).
+- After `.load`, verify the expected API (e.g., `window.d3.select`).
+- Prefer pure D3 and built-ins; do not introduce new runtime dependencies unless necessary.
+
+### 12) Error handling and fallbacks
+- Fail gracefully: append a small `` with a readable message inside the container.
+- For optional models (e.g., TFJS), attempt multiple URLs and fall back to a heuristic if load fails.
+
+### 13) Printing
+- Favor vector (`svg`) or simple shapes; avoid large bitmap backgrounds.
+- Let `HtmlEmbed.astro` handle most print constraints; ensure the chart scales with width 100% and auto height.
+
+### 14) Conventions checklist (before committing)
+- Root class is unique and matches file name (`d3-`).
+- No globals added; script wrapped in an IIFE.
+- `data-mounted` guard is present to avoid double-mount.
+- Colors come from `window.ColorPalettes` (no hardcoded arrays); `--primary-color` respected.
+- Uses CSS variables for colors; dark-mode friendly.
+- Responsive: recomputes layout on resize; uses `ResizeObserver`.
+- Controls are HTML-only, accessible, and consistently styled.
+- Legends and tooltips are HTML, not SVG.
+- Data loading includes public-path-first strategy and graceful error.
+- Axes/labels are legible at small widths.
+- Code is easy to skim: clear naming, early returns, short functions.
+
+### 14.1) Agent Checklist (operational)
+- Ensure root: one `` + scoped `
+
+```
+
+
diff --git a/content/articles/multi-harness-rl/.ai/skills/deploy-to-hf-spaces/SKILL.md b/content/articles/multi-harness-rl/.ai/skills/deploy-to-hf-spaces/SKILL.md
new file mode 100644
index 00000000..5bedb7eb
--- /dev/null
+++ b/content/articles/multi-harness-rl/.ai/skills/deploy-to-hf-spaces/SKILL.md
@@ -0,0 +1,121 @@
+---
+name: deploy-to-hf-spaces
+description: Deploy the article to a Hugging Face Space. Use when the user asks to deploy, push, publish, or update their Space on Hugging Face.
+---
+
+# Deploy to Hugging Face Spaces
+
+Guide for deploying and updating a research article on Hugging Face Spaces.
+
+## Prerequisites
+
+- A Hugging Face account with an SSH key configured (`git@hf.co`)
+- Or HTTPS access with `huggingface-cli login`
+
+## First deployment
+
+### 1. Create the Space
+
+Via CLI:
+```bash
+huggingface-cli repo create
/ --repo-type space --space_sdk docker
+```
+
+Or manually at https://huggingface.co/new-space (select **Docker** SDK).
+
+### 2. Add the remote
+
+```bash
+git remote add space git@hf.co:spaces//
+```
+
+### 3. Push
+
+```bash
+git push space main
+```
+
+The Space will build automatically using the included `Dockerfile` and `nginx.conf`. Build typically takes 3-5 minutes.
+
+## Updating an existing Space
+
+```bash
+git add -A
+git commit -m "feat: describe your changes"
+git push space main
+```
+
+### Force push (when needed)
+
+If the Space history has diverged (e.g., after a factory reset or manual edit on HF):
+
+```bash
+git push space main --force
+```
+
+## Verifying deployment
+
+1. Go to `https://huggingface.co/spaces//`
+2. Check the **Factory** tab for build logs
+3. The build is complete when status shows "Running"
+4. If the page looks stale, append `?v=` to the URL to bypass CDN cache
+
+### Cache busting
+
+Hugging Face Spaces aggressively cache static assets. If changes don't appear after a successful build:
+
+1. Check that the CSS bundle hash has changed (view page source, look for `/_astro/*.css`)
+2. If unchanged, the build used a stale cache - push an empty commit to force rebuild:
+ ```bash
+ git commit --allow-empty -m "chore: trigger rebuild"
+ git push space main
+ ```
+3. Wait for the new build to complete before checking again
+
+## README tag (critical)
+
+The `README.md` YAML frontmatter **must** contain:
+
+```yaml
+tags:
+ - research-article-template
+```
+
+**NEVER remove this tag.** It is used by the [Research Article Gallery](https://huggingface.co/spaces/tfrere/research-article-gallery) to list the article. Without it, the Space becomes invisible in the gallery.
+
+## README frontmatter reference
+
+```yaml
+---
+title: "Article Title"
+emoji: 📝
+colorFrom: blue
+colorTo: indigo
+sdk: docker
+app_port: 8080
+header: mini
+pinned: false
+tags:
+ - research-article-template
+---
+```
+
+| Field | Description |
+|-------|-------------|
+| `title` | Displayed in the Space card |
+| `emoji` | Emoji shown on the Space card |
+| `colorFrom` / `colorTo` | Gradient colors for the card header |
+| `sdk: docker` | Required - uses the Dockerfile for build |
+| `app_port: 8080` | Required - nginx serves on 8080 |
+| `header: mini` | Compact header (recommended for articles) |
+| `tags` | Must include `research-article-template` |
+
+## Troubleshooting
+
+| Problem | Solution |
+|---------|----------|
+| Build fails | Check build logs in the Factory tab. Common issue: missing dependencies in `package.json` |
+| CSS/layout looks wrong | Cache issue - force rebuild with empty commit |
+| Images not showing | If using Git LFS, ensure LFS files are pushed. Otherwise check image paths |
+| PDF not generated | Playwright is included in Docker build; check build logs for errors |
+| Space shows old content | Wait 2-3 min after build completes for CDN propagation, then hard refresh |
diff --git a/content/articles/multi-harness-rl/.cursor/skills b/content/articles/multi-harness-rl/.cursor/skills
new file mode 120000
index 00000000..6838a116
--- /dev/null
+++ b/content/articles/multi-harness-rl/.cursor/skills
@@ -0,0 +1 @@
+../.ai/skills
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/.dockerignore b/content/articles/multi-harness-rl/.dockerignore
new file mode 100644
index 00000000..355e0c24
--- /dev/null
+++ b/content/articles/multi-harness-rl/.dockerignore
@@ -0,0 +1,36 @@
+# Git
+.git
+.gitignore
+
+# IDE / editor
+.cursor
+.ai
+.vscode
+*.swp
+*.swo
+
+# Documentation (not needed in image)
+*.md
+!README.md
+LICENSE
+CONTRIBUTING.md
+
+# Screenshots (dev-only, large)
+app/screenshots
+
+# Node modules (reinstalled in Docker)
+app/node_modules
+app/scripts/notion-importer/node_modules
+app/scripts/latex-importer/node_modules
+
+# Build artifacts
+app/dist
+app/.astro
+
+# PDF exports (generated at build time)
+app/public/*.pdf
+
+# Dev / local files
+prototype-project-page.html
+scripts/
+app/yarn.lock
diff --git a/content/articles/multi-harness-rl/.gitattributes b/content/articles/multi-harness-rl/.gitattributes
new file mode 100644
index 00000000..a6344aac
--- /dev/null
+++ b/content/articles/multi-harness-rl/.gitattributes
@@ -0,0 +1,35 @@
+*.7z filter=lfs diff=lfs merge=lfs -text
+*.arrow filter=lfs diff=lfs merge=lfs -text
+*.bin filter=lfs diff=lfs merge=lfs -text
+*.bz2 filter=lfs diff=lfs merge=lfs -text
+*.ckpt filter=lfs diff=lfs merge=lfs -text
+*.ftz filter=lfs diff=lfs merge=lfs -text
+*.gz filter=lfs diff=lfs merge=lfs -text
+*.h5 filter=lfs diff=lfs merge=lfs -text
+*.joblib filter=lfs diff=lfs merge=lfs -text
+*.lfs.* filter=lfs diff=lfs merge=lfs -text
+*.mlmodel filter=lfs diff=lfs merge=lfs -text
+*.model filter=lfs diff=lfs merge=lfs -text
+*.msgpack filter=lfs diff=lfs merge=lfs -text
+*.npy filter=lfs diff=lfs merge=lfs -text
+*.npz filter=lfs diff=lfs merge=lfs -text
+*.onnx filter=lfs diff=lfs merge=lfs -text
+*.ot filter=lfs diff=lfs merge=lfs -text
+*.parquet filter=lfs diff=lfs merge=lfs -text
+*.pb filter=lfs diff=lfs merge=lfs -text
+*.pickle filter=lfs diff=lfs merge=lfs -text
+*.pkl filter=lfs diff=lfs merge=lfs -text
+*.pt filter=lfs diff=lfs merge=lfs -text
+*.pth filter=lfs diff=lfs merge=lfs -text
+*.rar filter=lfs diff=lfs merge=lfs -text
+*.safetensors filter=lfs diff=lfs merge=lfs -text
+saved_model/**/* filter=lfs diff=lfs merge=lfs -text
+*.tar.* filter=lfs diff=lfs merge=lfs -text
+*.tar filter=lfs diff=lfs merge=lfs -text
+*.tflite filter=lfs diff=lfs merge=lfs -text
+*.tgz filter=lfs diff=lfs merge=lfs -text
+*.wasm filter=lfs diff=lfs merge=lfs -text
+*.xz filter=lfs diff=lfs merge=lfs -text
+*.zip filter=lfs diff=lfs merge=lfs -text
+*.zst filter=lfs diff=lfs merge=lfs -text
+*tfevents* filter=lfs diff=lfs merge=lfs -text
diff --git a/content/articles/multi-harness-rl/.gitignore b/content/articles/multi-harness-rl/.gitignore
new file mode 100644
index 00000000..d90ecbfd
--- /dev/null
+++ b/content/articles/multi-harness-rl/.gitignore
@@ -0,0 +1,56 @@
+# Python
+__pycache__
+*.py[cod]
+*.so
+.Python
+env/
+venv/
+*.egg-info/
+dist/
+build/
+*.egg
+.idea/
+.vscode/
+.astro/
+.claude/
+*.swp
+.DS_Store
+# Node
+node_modules/
+*.log
+*.env
+*.cache
+.notion-to-md
+.playwright-mcp/
+
+app/scripts/latex-to-mdx/output/
+app/scripts/notion-importer/output/**/*
+app/src/content/embeds/typography/generated
+
+# PDF export
+app/public/*.pdf
+app/public/*.png
+app/public/*.jpg
+app/public/data/**/*
+
+.astro/
+
+# Template sync temporary directories
+.template-sync/
+.temp-*/
+.backup-*/
+
+# IDE / agent
+.cursor/*
+!.cursor/skills
+
+# Temporary screenshots & exports at root
+/*.png
+/*.zip
+/rephrasing_metadata.json
+/RAPPORT-ARBRE-COMPETENCES-AUTONOMIE.md
+
+
+# Review comments pulled from the bucket stay local
+review/.pulled/
+review/__pycache__/
diff --git a/content/articles/multi-harness-rl/AGENTS.md b/content/articles/multi-harness-rl/AGENTS.md
new file mode 100644
index 00000000..38fe8964
--- /dev/null
+++ b/content/articles/multi-harness-rl/AGENTS.md
@@ -0,0 +1,80 @@
+# Agent Instructions
+
+This file provides context for AI coding agents (Claude Code, GitHub Copilot, Cursor, Windsurf, etc.) working on this project.
+
+## Project overview
+
+This is a **research article template** built with Astro + MDX. It produces interactive, web-native scientific papers with D3.js data visualizations, dark mode support, and PDF export.
+
+### Key paths
+
+| What | Where |
+|------|-------|
+| Astro app | `app/` |
+| MDX content | `app/src/content/` (article.mdx, chapters/) |
+| Components | `app/src/components/` |
+| HTML embeds (D3 charts) | `app/src/content/embeds/` |
+| Data files (CSV/JSON) | `app/src/content/assets/data/` |
+| Styles & CSS variables | `app/src/styles/` |
+| Scripts (export, import) | `app/scripts/` |
+
+### Tech stack
+
+- **Astro 4** with MDX
+- **D3.js v7** for data visualization (loaded via CDN in embeds)
+- **Svelte** for interactive components (e.g., Trackio)
+- **CSS custom properties** for theming (light/dark mode)
+
+## Agent skills
+
+Detailed guides for common tasks live in `.ai/skills/`. Read the relevant skill file before starting work:
+
+| Skill | File | When to use |
+|-------|------|-------------|
+| Create HTML embed | [`.ai/skills/create-html-embed/SKILL.md`](.ai/skills/create-html-embed/SKILL.md) | Creating D3 charts, visualizations, interactive embeds |
+| Article frontmatter | [`.ai/skills/article-frontmatter/SKILL.md`](.ai/skills/article-frontmatter/SKILL.md) | Configuring title, authors, template variant, DOI, PDF |
+| Deploy to HF Spaces | [`.ai/skills/deploy-to-hf-spaces/SKILL.md`](.ai/skills/deploy-to-hf-spaces/SKILL.md) | Deploying, pushing, or updating a Hugging Face Space |
+
+> **Cursor users**: these skills are auto-detected via a symlink at `.cursor/skills/`.
+
+## Creating HTML embed charts
+
+When asked to create a chart, visualization, or D3 embed, read the skill files:
+
+1. **Workflow & quick reference**: `.ai/skills/create-html-embed/SKILL.md`
+2. **Full conventions & directives**: `.ai/skills/create-html-embed/directives.md`
+
+### TL;DR
+
+- Embeds are self-contained `.html` files in `app/src/content/embeds/`
+- Structure: single root `` + scoped `
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/public/logos/openhands-sdk.svg b/content/articles/multi-harness-rl/app/public/logos/openhands-sdk.svg
new file mode 100644
index 00000000..4877e59f
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/public/logos/openhands-sdk.svg
@@ -0,0 +1 @@
+
diff --git a/content/articles/multi-harness-rl/app/public/logos/qwen-coder.png b/content/articles/multi-harness-rl/app/public/logos/qwen-coder.png
new file mode 100644
index 00000000..7dc1a8de
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/logos/qwen-coder.png differ
diff --git a/content/articles/multi-harness-rl/app/public/logos/swe-agent.svg b/content/articles/multi-harness-rl/app/public/logos/swe-agent.svg
new file mode 100644
index 00000000..0bee35f8
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/public/logos/swe-agent.svg
@@ -0,0 +1,184 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/public/logos/terminus-2.png b/content/articles/multi-harness-rl/app/public/logos/terminus-2.png
new file mode 100644
index 00000000..4f0347c3
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/logos/terminus-2.png differ
diff --git a/content/articles/multi-harness-rl/app/public/logos/trae-agent.png b/content/articles/multi-harness-rl/app/public/logos/trae-agent.png
new file mode 100644
index 00000000..bdf43ae7
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/logos/trae-agent.png differ
diff --git a/content/articles/multi-harness-rl/app/public/og/og-image.png b/content/articles/multi-harness-rl/app/public/og/og-image.png
new file mode 100644
index 00000000..06d6d491
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/og/og-image.png differ
diff --git a/content/articles/multi-harness-rl/app/public/papers/README.md b/content/articles/multi-harness-rl/app/public/papers/README.md
new file mode 100644
index 00000000..0c489cea
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/public/papers/README.md
@@ -0,0 +1,17 @@
+# Paper excerpt crops
+
+Each PNG is a crop rendered at 3x from the official PDF (arXiv, or the lab's own release where no arXiv version exists), used as a short
+quotation for commentary in the introduction. Nothing is retouched, recoloured
+or recomposed; the only edit is the crop rectangle. Regenerate with the anchor
+phrases below if a paper is revised.
+
+| File | Source | arXiv | Page | Passage |
+|---|---|---|---|---|
+| `laguna.png` | LAGUNA M.1/XS.2 Technical Report (poolside) | 2605.27605 | 20 | §4.3.3 Multi-harness Training |
+| `katcoder.png` | KAT-Coder-V2.5 Technical Report (KwaiKAT) | 2607.05471 | 9 | §4.1 Harness Scaling |
+| `orchard.png` | Orchard: An Open-Source Agentic Modeling Framework | 2605.15040 | 20 | "Harness lock-in is severe in single-harness training." |
+| `openforge.png` | OpenForgeRL: Train Harness-native Agents in Any Environment | 2607.21557 | 9 | Table 5, one-harness vs three-harness ablation |
+| `agentlt.png` | Agent Lightning v1.0: Towards Harnessed Agentic RL (Microsoft) | 2608.17528 | 1 | Abstract, who owns the interaction loop |
+| `kimi.png` | Kimi K3: Open Frontier Intelligence (Moonshot) | 2607.24653 | 14 | §4.2.1 Unified White-Box RL Environment |
+| `qwen.png` | Qwen3-Coder-Next Technical Report (Qwen) | 2603.00729 | 5 | §3.1.2 Synthetic Data, "Multi-turn Agentic Coding." |
+| `mimo.png` | MiMo-V2.6: Scaling Reinforcement Learning Towards Self-Improvement (Xiaomi) | none; [PDF on Hugging Face](https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Pro-RL/blob/main/MiMo_V2_6_technical_report.pdf) | 14 | §4.2.5 Multi-Harness Training, third paragraph |
diff --git a/content/articles/multi-harness-rl/app/public/papers/agentlt.png b/content/articles/multi-harness-rl/app/public/papers/agentlt.png
new file mode 100644
index 00000000..7af40549
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/papers/agentlt.png differ
diff --git a/content/articles/multi-harness-rl/app/public/papers/katcoder.png b/content/articles/multi-harness-rl/app/public/papers/katcoder.png
new file mode 100644
index 00000000..2af35ae7
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/papers/katcoder.png differ
diff --git a/content/articles/multi-harness-rl/app/public/papers/kimi.png b/content/articles/multi-harness-rl/app/public/papers/kimi.png
new file mode 100644
index 00000000..634db8d7
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/papers/kimi.png differ
diff --git a/content/articles/multi-harness-rl/app/public/papers/laguna.png b/content/articles/multi-harness-rl/app/public/papers/laguna.png
new file mode 100644
index 00000000..5a53fd7f
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/papers/laguna.png differ
diff --git a/content/articles/multi-harness-rl/app/public/papers/mimo.png b/content/articles/multi-harness-rl/app/public/papers/mimo.png
new file mode 100644
index 00000000..08e9bc02
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/papers/mimo.png differ
diff --git a/content/articles/multi-harness-rl/app/public/papers/openforge.png b/content/articles/multi-harness-rl/app/public/papers/openforge.png
new file mode 100644
index 00000000..aeb7e75b
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/papers/openforge.png differ
diff --git a/content/articles/multi-harness-rl/app/public/papers/orchard.png b/content/articles/multi-harness-rl/app/public/papers/orchard.png
new file mode 100644
index 00000000..0da4c190
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/papers/orchard.png differ
diff --git a/content/articles/multi-harness-rl/app/public/papers/qwen.png b/content/articles/multi-harness-rl/app/public/papers/qwen.png
new file mode 100644
index 00000000..bb901f90
Binary files /dev/null and b/content/articles/multi-harness-rl/app/public/papers/qwen.png differ
diff --git a/content/articles/multi-harness-rl/app/public/review/review.css b/content/articles/multi-harness-rl/app/public/review/review.css
new file mode 100644
index 00000000..13e17ba6
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/public/review/review.css
@@ -0,0 +1,135 @@
+/* Review layer. Only loaded with ?review, so none of this reaches the published page. It sits on
+ its own layer above the article and never changes the article's layout. */
+.rv-ui { --rv-accent: rgb(78,165,183); --rv-accent-soft: rgba(78,165,183,.16);
+ --rv-warn: rgb(232,137,171); --rv-card-bg: var(--surface-bg, #fff);
+ font-family: inherit; font-size: 13px; line-height: 1.45; color: var(--text-color); }
+.rv-ui [hidden], .rv-ui[hidden] { display: none !important; }
+
+/* toolbar */
+.rv-bar { position: fixed; top: 10px; right: 12px; z-index: 1003; display: flex; align-items: center; gap: 8px;
+ flex-wrap: wrap; justify-content: flex-end; max-width: calc(100vw - 24px);
+ padding: 6px 8px; border-radius: 999px; border: 1px solid var(--border-color);
+ background: var(--rv-card-bg); box-shadow: 0 4px 18px rgba(0,0,0,.14); }
+.rv-pill { font-size: 11px; font-weight: 700; letter-spacing: .04em; text-transform: uppercase; color: var(--rv-accent);
+ padding: 3px 8px; border-radius: 999px; background: var(--rv-accent-soft); }
+.rv-user { display: inline-flex; align-items: center; gap: 6px; }
+.rv-user img, .rv-meta img { width: 24px; height: 24px; border-radius: 50%; object-fit: cover; flex: none; background: var(--border-color); }
+.rv-btn { appearance: none; font: inherit; font-size: 12.5px; cursor: pointer; padding: 5px 12px; border-radius: 999px;
+ border: 1px solid var(--border-color); background: transparent; color: var(--text-color); text-decoration: none; }
+.rv-btn:hover { border-color: var(--text-color); }
+.rv-btn.is-on { border-color: var(--rv-accent); color: var(--rv-accent); font-weight: 600; }
+.rv-btn.rv-primary { background: var(--rv-accent); border-color: var(--rv-accent); color: #fff; font-weight: 600; }
+.rv-btn.rv-quiet { color: var(--muted-color); }
+.rv-btn:disabled { opacity: .5; cursor: wait; }
+.rv-link { appearance: none; border: 0; background: none; padding: 0; font: inherit; font-size: 12.5px; cursor: pointer;
+ color: var(--muted-color); }
+.rv-link:hover { color: var(--text-color); text-decoration: underline; }
+.rv-icon { appearance: none; border: 0; background: none; cursor: pointer; font: inherit; font-size: 15px; line-height: 1;
+ width: 26px; height: 26px; border-radius: 50%; display: inline-grid; place-items: center; color: var(--muted-color); flex: none; }
+.rv-icon:hover { background: var(--rv-accent-soft); color: var(--text-color); }
+.rv-resolve { color: var(--rv-accent); font-weight: 700; }
+
+/* margin cards: positioned by review.js next to their anchors */
+.rv-margin { position: absolute; top: 0; left: 0; width: 0; height: 0; z-index: 1001; }
+.rv-margin .rv-card { position: absolute; transition: top .18s ease, left .18s ease, box-shadow .18s ease; }
+.rv-card { box-sizing: border-box; border: 1px solid var(--border-color); border-radius: 10px; padding: 10px 12px; background: var(--rv-card-bg);
+ box-shadow: 0 1px 3px rgba(0,0,0,.08); cursor: pointer; }
+.rv-card:hover { box-shadow: 0 3px 12px rgba(0,0,0,.12); }
+.rv-card.is-active { cursor: default; border-color: color-mix(in srgb, var(--rv-accent) 55%, var(--border-color));
+ box-shadow: 0 8px 28px rgba(0,0,0,.22); z-index: 2; }
+.rv-card.is-resolved { opacity: .75; }
+.rv-compose { border-color: var(--rv-accent); }
+.rv-target { font-size: 12px; font-style: italic; color: var(--muted-color); background: var(--rv-accent-soft);
+ border-left: 3px solid var(--rv-accent); border-radius: 4px; padding: 4px 8px; margin-bottom: 8px; overflow-wrap: anywhere; }
+.rv-tag { font-style: normal; font-size: 10.5px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em;
+ margin-right: 6px; padding: 1px 6px; border-radius: 999px; background: var(--border-color); color: var(--text-color); }
+.rv-tag.rv-warn { background: color-mix(in srgb, var(--rv-warn) 30%, transparent); }
+.rv-msg + .rv-msg { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-color); }
+.rv-meta { display: flex; align-items: center; gap: 8px; }
+.rv-who { display: flex; flex-direction: column; min-width: 0; flex: 1; line-height: 1.2; }
+.rv-who b { font-size: 13px; }
+.rv-who span { font-size: 11.5px; color: var(--muted-color); }
+.rv-body { margin-top: 6px; overflow-wrap: anywhere; }
+.rv-card:not(.is-active) .rv-body { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
+.rv-more { margin-top: 6px; font-size: 12px; color: var(--rv-accent); font-weight: 600; }
+.rv-reacts { display: flex; flex-wrap: wrap; gap: 4px; margin-top: 6px; align-items: center; }
+.rv-reacts:empty { display: none; }
+.rv-card:not(.is-active) .rv-react-add { display: none; }
+.rv-react { appearance: none; font: inherit; font-size: 12px; cursor: pointer; padding: 1px 7px; border-radius: 999px;
+ border: 1px solid var(--border-color); background: transparent; color: var(--text-color); }
+.rv-react.is-mine { border-color: var(--rv-accent); background: var(--rv-accent-soft); }
+.rv-react-add { width: 22px; height: 22px; font-size: 13px; }
+.rv-card textarea { width: 100%; box-sizing: border-box; font: inherit; font-size: 13px; padding: 7px 9px; border-radius: 8px; margin-top: 8px;
+ border: 1px solid var(--border-color); background: transparent; color: var(--text-color); resize: vertical; }
+.rv-card textarea:focus { outline: 2px solid var(--rv-accent-soft); border-color: var(--rv-accent); }
+.rv-row { display: flex; align-items: center; gap: 10px; margin-top: 8px; }
+.rv-reply { margin-top: 4px; }
+
+/* add-comment button, menus */
+.rv-add { position: absolute; z-index: 1002; appearance: none; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer;
+ padding: 6px 12px; border-radius: 999px; border: 0; background: var(--rv-accent); color: #fff; box-shadow: 0 4px 14px rgba(0,0,0,.22); }
+.rv-menu { position: absolute; z-index: 1004; min-width: 170px; padding: 4px; border-radius: 10px; border: 1px solid var(--border-color);
+ background: var(--rv-card-bg); box-shadow: 0 8px 28px rgba(0,0,0,.22); display: flex; flex-direction: column; }
+.rv-menu button { appearance: none; border: 0; background: none; font: inherit; font-size: 13px; text-align: left; padding: 7px 10px;
+ border-radius: 6px; cursor: pointer; color: var(--text-color); }
+.rv-menu button:hover { background: var(--rv-accent-soft); }
+.rv-menu.rv-menu-emoji { flex-direction: row; min-width: 0; }
+.rv-menu.rv-menu-emoji button { font-size: 17px; padding: 5px 7px; }
+
+/* comments panel: history with filters, and the card view on narrow screens */
+.rv-panel { position: fixed; z-index: 1002; top: 58px; right: 12px; bottom: 12px; width: 370px; display: flex; flex-direction: column;
+ border-radius: 12px; border: 1px solid var(--border-color); background: var(--rv-card-bg); box-shadow: 0 10px 34px rgba(0,0,0,.24); }
+.rv-panel-head { display: flex; align-items: center; gap: 8px; padding: 10px 10px 8px 14px; border-bottom: 1px solid var(--border-color); }
+.rv-panel-head b { font-size: 14px; }
+.rv-tabs { display: flex; gap: 4px; margin-left: auto; }
+.rv-tab { appearance: none; font: inherit; font-size: 12px; cursor: pointer; padding: 3px 9px; border-radius: 999px;
+ border: 1px solid transparent; background: none; color: var(--muted-color); }
+.rv-tab.is-on { border-color: var(--rv-accent); color: var(--rv-accent); font-weight: 600; }
+.rv-panel-list { overflow-y: auto; padding: 10px; display: flex; flex-direction: column; gap: 10px; }
+.rv-note { margin: 4px 2px; color: var(--muted-color); font-size: 12.5px; }
+.rv-error { color: var(--rv-warn); }
+.rv-float-note { position: absolute; background: var(--rv-card-bg); border: 1px solid var(--rv-warn); border-radius: 8px; padding: 6px 10px; }
+
+/* highlights in the article */
+.content-grid main mark.rv-hl, mark.rv-hl { padding: 0; margin: 0; box-shadow: none; border: 0; border-radius: 2px; font-weight: inherit;
+ background: rgba(246,196,69,.30); color: inherit; border-bottom: 2px solid rgba(214,160,30,.75); cursor: pointer;
+ box-decoration-break: clone; -webkit-box-decoration-break: clone; }
+.content-grid main mark.rv-hl.is-active, mark.rv-hl.is-active { background: rgba(246,196,69,.62); }
+span.rv-probe { display: inline; width: 0; }
+
+figure.rv-fig { position: relative; }
+figure.rv-fig.rv-fig-active { outline: 2px solid rgba(214,160,30,.8); outline-offset: 4px; border-radius: 10px; }
+.rv-fig-btn { position: absolute; top: 6px; right: 6px; z-index: 20; /* above the embed title bar (z-index 10) */ appearance: none; font: inherit; font-size: 12px; font-weight: 600; cursor: pointer;
+ display: inline-flex; align-items: center; gap: 6px; padding: 4px 10px; border-radius: 999px; border: 1px solid rgb(78,165,183);
+ background: var(--surface-bg, #fff); color: rgb(78,165,183); opacity: .88; }
+.rv-fig-btn:hover { opacity: 1; }
+.rv-fig-count { display: inline-grid; place-items: center; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px;
+ background: rgb(78,165,183); color: #fff; font-size: 10.5px; }
+
+@media (max-width: 760px) {
+ .rv-panel { top: auto; left: 8px; right: 8px; width: auto; height: 55vh; }
+ .rv-bar { top: auto; bottom: 12px; }
+ .rv-ui:has(.rv-panel:not([hidden])) .rv-bar { bottom: calc(55vh + 20px); }
+}
+
+/* selection actions */
+.rv-add { position: absolute; z-index: 1002; display: inline-flex; gap: 2px; padding: 3px; border-radius: 999px;
+ background: var(--rv-card-bg, var(--surface-bg, #fff)); border: 1px solid var(--border-color); box-shadow: 0 4px 14px rgba(0,0,0,.22); }
+.rv-add button { appearance: none; font: inherit; font-size: 12.5px; font-weight: 600; cursor: pointer; padding: 5px 11px; border-radius: 999px;
+ border: 0; background: transparent; color: var(--text-color); }
+.rv-add button:first-child { background: rgb(78,165,183); color: #fff; }
+.rv-add button:hover:not(:first-child) { background: rgba(78,165,183,.16); }
+
+/* suggestions, as in Docs' suggesting mode */
+.rv-sugline { font-size: 13px; line-height: 1.5; overflow-wrap: anywhere; }
+.rv-sugkind { font-size: 11px; font-weight: 700; text-transform: uppercase; letter-spacing: .04em; color: var(--muted-color); margin-right: 6px; }
+.rv-sugline del { color: rgb(200,70,90); text-decoration-thickness: 1.5px; }
+.rv-sugline ins { color: rgb(46,140,87); text-decoration: none; background: rgba(46,160,87,.14); border-radius: 3px; padding: 0 2px; }
+.rv-card.is-suggestion { border-left: 3px solid rgb(46,160,87); }
+.rv-decide { margin-top: 8px; }
+.rv-tag.rv-ok { background: rgba(46,160,87,.25); }
+.content-grid main mark.rv-hl.rv-sug, mark.rv-hl.rv-sug { background: rgba(220,80,100,.10); border-bottom: 0;
+ text-decoration: line-through; text-decoration-color: rgb(200,70,90); text-decoration-thickness: 1.5px; }
+.content-grid main mark.rv-hl.rv-sug.is-active, mark.rv-hl.rv-sug.is-active { background: rgba(220,80,100,.22); }
+ins.rv-ins { text-decoration: none; color: rgb(46,140,87); background: rgba(46,160,87,.14); border-bottom: 2px solid rgba(46,160,87,.6);
+ border-radius: 2px; margin-left: 2px; cursor: pointer; }
diff --git a/content/articles/multi-harness-rl/app/public/review/review.js b/content/articles/multi-harness-rl/app/public/review/review.js
new file mode 100644
index 00000000..f8181337
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/public/review/review.js
@@ -0,0 +1,612 @@
+// Review layer for the article, modelled on Google Docs comments. Loaded only when the page is opened
+// with ?review (see index.astro); it talks to review/server.py at /api/review/ and never runs on the
+// published page.
+//
+// Comment cards float in the right margin, level with the text they point at, on a layer above the
+// page, so the article itself never moves. Clicking a highlight or a card makes it active: it expands,
+// lines up with its text and pushes the others aside. On narrow screens the same cards live in the
+// comments panel instead.
+(() => {
+ if (window.__mhrlReview) return;
+ window.__mhrlReview = true;
+
+ const API = '/api/review';
+ const REACTIONS = ['👍', '❤️', '😄', '🎉', '👀', '➕', '🔥'];
+ const main = document.querySelector('main');
+ if (!main) return;
+
+ const esc = (t) => String(t ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
+ const el = (html) => { const t = document.createElement('template'); t.innerHTML = html.trim(); return t.content.firstElementChild; };
+ const ago = (iso) => {
+ const s = (Date.now() - Date.parse(iso)) / 1000;
+ if (s < 60) return 'just now';
+ if (s < 3600) return `${Math.floor(s / 60)} min ago`;
+ if (s < 86400) return `${Math.floor(s / 3600)} h ago`;
+ return new Date(iso).toLocaleDateString(undefined, { day: 'numeric', month: 'short' });
+ };
+ const call = async (method, path, body) => {
+ const r = await fetch(API + path, {
+ method, credentials: 'same-origin',
+ headers: body ? { 'content-type': 'application/json', 'x-review': '1' } : { 'x-review': '1' },
+ body: body ? JSON.stringify(body) : undefined,
+ });
+ if (!r.ok) {
+ let msg = r.statusText;
+ try { msg = (await r.json()).detail || msg; } catch (e) {}
+ throw new Error(msg);
+ }
+ return r.json();
+ };
+
+ // ------------------------------------------------------------ state
+ let me = null, threads = [], lastError = '';
+ let active = null; // id of the expanded thread, or 'new' for the composer
+ let composing = null; // anchor of the comment being written
+ let editing = null; // { tid, mid } of the message being edited
+ let historyOpen = false, filter = 'open';
+ let anchors = {}; // thread id -> element it points at (first
, or the figure)
+
+ const isOwner = () => me && me.user && me.owner && me.user.username.toLowerCase() === me.owner.toLowerCase();
+ const mine = (m) => me && me.user && m.author.username === me.user.username;
+
+ // ------------------------------------------------------------ text index
+ // The article's prose as one string, whitespace collapsed, with a map back to text nodes. Figures
+ // are left out: they get their own comment button instead of text selection.
+ const skip = (n) => n && n.closest && n.closest('figure.html-embed, script, style, .rv-ui');
+ const buildIndex = () => {
+ const walker = document.createTreeWalker(main, NodeFilter.SHOW_TEXT, {
+ acceptNode: (n) => (n.nodeValue && !skip(n.parentElement) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT),
+ });
+ let text = '', map = [], prevSpace = true;
+ for (let n = walker.nextNode(); n; n = walker.nextNode()) {
+ const v = n.nodeValue;
+ for (let i = 0; i < v.length; i++) {
+ const sp = /\s/.test(v[i]);
+ if (sp && prevSpace) continue;
+ text += sp ? ' ' : v[i];
+ map.push([n, i]);
+ prevSpace = sp;
+ }
+ }
+ return { text, map };
+ };
+ const posOf = (idx, node, offset) => {
+ for (let i = 0; i < idx.map.length; i++) {
+ const [n, o] = idx.map[i];
+ if (n === node && o >= offset) return i;
+ if (n !== node && node.compareDocumentPosition(n) & Node.DOCUMENT_POSITION_FOLLOWING) return i;
+ }
+ return idx.map.length;
+ };
+ const sectionOf = (node) => {
+ let chapter = '', section = '';
+ for (const h of main.querySelectorAll('h2, h3, h4')) {
+ if (!(h.compareDocumentPosition(node) & Node.DOCUMENT_POSITION_FOLLOWING)) break;
+ if (h.tagName === 'H2') { chapter = h.textContent.trim(); section = ''; } else section = h.textContent.trim();
+ }
+ return { chapter, section };
+ };
+
+ // ------------------------------------------------------------ anchoring
+ // Figures are keyed by their title, since one embed file used several times repeats the same id.
+ const figKey = (f) => f.querySelector('.html-embed__title')?.textContent.trim() || f.id;
+ const figByKey = () => { const m = {}; main.querySelectorAll('figure.html-embed').forEach((f) => { const k = figKey(f); if (k && !m[k]) m[k] = f; }); return m; };
+ const unmark = () => {
+ main.querySelectorAll('mark.rv-hl').forEach((m) => { const p = m.parentNode; while (m.firstChild) p.insertBefore(m.firstChild, m); p.removeChild(m); p.normalize(); });
+ main.querySelectorAll('.rv-fig-count, ins.rv-ins').forEach((b) => b.remove());
+ main.querySelectorAll('figure.rv-fig-active').forEach((f) => f.classList.remove('rv-fig-active'));
+ };
+ const score = (text, at, a, q) => {
+ let s = 0;
+ const pre = a.prefix || '', suf = a.suffix || '';
+ for (let i = 1; i <= pre.length && text[at - i] === pre[pre.length - i]; i++) s++;
+ const end = at + q.length;
+ for (let i = 0; i < suf.length && text[end + i] === suf[i]; i++) s++;
+ return s;
+ };
+ const findQuote = (idx, a) => {
+ const q = (a.quote || '').replace(/\s+/g, ' ').trim();
+ if (!q) return null;
+ let best = null, bestScore = -1;
+ for (let at = idx.text.indexOf(q); at !== -1; at = idx.text.indexOf(q, at + 1)) {
+ const s = score(idx.text, at, a, q);
+ if (s > bestScore) { best = [at, at + q.length]; bestScore = s; }
+ }
+ return best;
+ };
+ const wrap = (idx, start, end, tid) => {
+ const pieces = new Map();
+ for (let i = start; i < end; i++) {
+ const [n, o] = idx.map[i];
+ const p = pieces.get(n) || [o, o];
+ p[0] = Math.min(p[0], o); p[1] = Math.max(p[1], o + 1);
+ pieces.set(n, p);
+ }
+ let first = null, last = null;
+ for (const [n, [s, e]] of pieces) {
+ if (!n.parentNode || e > n.nodeValue.length) continue;
+ const mid = n.splitText(s);
+ mid.splitText(e - s);
+ const m = document.createElement('mark');
+ m.className = 'rv-hl';
+ m.dataset.thread = tid;
+ mid.parentNode.insertBefore(m, mid);
+ m.appendChild(mid);
+ first = first || m;
+ last = m;
+ }
+ return { first, last };
+ };
+ // Open threads are highlighted; a resolved one only while it is open from the comments panel.
+ const shown = () => threads.filter((t) => t.status !== 'resolved' || t.id === active);
+ const anchorAll = () => {
+ unmark();
+ const found = {};
+ const idx = buildIndex(), figs = figByKey(), spots = [];
+ for (const t of shown()) {
+ if (t.anchor.type === 'text') {
+ const r = findQuote(idx, t.anchor);
+ if (r) spots.push([r, t]);
+ } else if (t.anchor.type === 'figure' && figs[t.anchor.figure]) {
+ found[t.id] = figs[t.anchor.figure];
+ }
+ }
+ // Highlights can overlap (a word inside a commented sentence). Each wrap splits text nodes, so the
+ // index is rebuilt before every one; the text never changes, so the offsets stay valid.
+ for (const [[s, e], t] of spots) {
+ const w = wrap(buildIndex(), s, e, t.id);
+ if (!w.first) continue;
+ found[t.id] = w.first;
+ if (t.kind === 'suggestion') {
+ // Suggestions read like Docs' suggesting mode: the old text struck through, the new text after it.
+ main.querySelectorAll(`mark.rv-hl[data-thread="${t.id}"]`).forEach((m) => m.classList.add('rv-sug'));
+ if (t.suggestion.action === 'replace') {
+ const ins = document.createElement('ins');
+ ins.className = 'rv-ins rv-ui'; ins.dataset.thread = t.id; ins.textContent = t.suggestion.text;
+ w.last.after(ins);
+ }
+ }
+ }
+ const perFig = {};
+ shown().filter((t) => t.anchor.type === 'figure' && found[t.id]).forEach((t) => { perFig[t.anchor.figure] = (perFig[t.anchor.figure] || 0) + 1; });
+ Object.entries(perFig).forEach(([k, c]) => {
+ const btn = figs[k]?.querySelector('.rv-fig-btn');
+ if (btn) btn.insertAdjacentHTML('beforeend', `${c} `);
+ });
+ return found;
+ };
+ const markActive = () => {
+ main.querySelectorAll('mark.rv-hl.is-active').forEach((m) => m.classList.remove('is-active'));
+ main.querySelectorAll('figure.rv-fig-active').forEach((f) => f.classList.remove('rv-fig-active'));
+ if (!active || active === 'new') return;
+ main.querySelectorAll(`mark.rv-hl[data-thread="${active}"]`).forEach((m) => m.classList.add('is-active'));
+ const t = threads.find((x) => x.id === active);
+ if (t && t.anchor.type === 'figure' && anchors[active]) anchors[active].classList.add('rv-fig-active');
+ };
+
+ // ------------------------------------------------------------ UI shell
+ const ui = el(`
+
+ Review mode
+
+
+ Exit
+
+
+
+
+ + Comment
+ Suggest delete
+ Suggest edit
+
+
+
`);
+ document.body.appendChild(ui);
+ const bar = ui.querySelector('.rv-bar'), margin = ui.querySelector('.rv-margin'), panel = ui.querySelector('.rv-panel');
+ const panelList = ui.querySelector('.rv-panel-list'), tabs = ui.querySelector('.rv-tabs'), addBtn = ui.querySelector('.rv-add'), menu = ui.querySelector('.rv-menu');
+
+ const inIframe = (() => { try { return window.self !== window.top; } catch (e) { return true; } })();
+ const here = () => location.pathname + location.search;
+
+ // Margin mode needs room to the right of the text; otherwise the cards live in the panel.
+ let geo = { margin: false, left: 0, width: 300 };
+ const measure = () => {
+ // The text column, not , which is wider than the prose it holds.
+ const edges = [...main.querySelectorAll('p')].filter((p) => !p.closest('figure, .rv-ui')).slice(0, 40)
+ .map((p) => p.getBoundingClientRect()).filter((r) => r.width > 0).map((r) => r.right);
+ const right = (edges.length ? Math.max(...edges) : main.getBoundingClientRect().right) + window.scrollX;
+ const room = document.documentElement.clientWidth - (right - window.scrollX) - 24;
+ geo = { margin: room >= 270, left: right + 28, width: Math.min(320, room - 12) };
+ };
+
+ // ------------------------------------------------------------ rendering
+ const reactionsHtml = (t, m) => {
+ const r = m.reactions || {};
+ const chips = Object.entries(r).filter(([, who]) => who.length).map(([e, who]) =>
+ `${e} ${who.length} `).join('');
+ return `${chips}☺
`;
+ };
+ const msgHtml = (t, m, first) => {
+ const body = editing && editing.tid === t.id && editing.mid === m.id
+ ? `
+ Save Cancel
`
+ : (m.body ? `${esc(m.body).replace(/\n/g, ' ')}
` : '');
+ return `
+
+ ${body}
+ ${reactionsHtml(t, m)}
+
`;
+ };
+ const cardHtml = (t, expanded, where) => {
+ const a = t.anchor;
+ const target = a.type === 'figure' ? `On figure: ${esc(a.title || a.figure)}` : `“${esc(a.quote.length > 160 ? a.quote.slice(0, 160) + '…' : a.quote)}”`;
+ const msgs = expanded ? t.messages : t.messages.slice(0, 1);
+ const more = !expanded && t.messages.length > 1 ? `${t.messages.length - 1} ${t.messages.length === 2 ? 'reply' : 'replies'}
` : '';
+ const tag = t.status === 'resolved' ? 'Resolved ' : (where === 'panel' && !anchors[t.id] ? 'Detached ' : '');
+ const sug = t.kind === 'suggestion' ? t.suggestion : null;
+ const decided = t.decision ? `${t.decision} ` : '';
+ const sugHtml = sug ? `${decided}${sug.action === 'delete' ? 'Delete' : 'Replace'} ${esc(a.quote)}${sug.action === 'replace' ? ` with ${esc(sug.text)} ` : ''}
+ ${t.status !== 'resolved' && isOwner() ? `✓ Accept ✕ Reject
` : ''}` : '';
+ return `
+ ${!sug && (where === 'panel' || a.type === 'figure' || !anchors[t.id]) ? `
${tag}${target}
` : ''}
+ ${sug && where === 'panel' && !anchors[t.id] && t.status !== 'resolved' ? '
Detached ' : ''}
+ ${sugHtml}
+ ${msgs.map((m, i) => msgHtml(t, m, i === 0 && !sug)).join('')}
+ ${more}
+ ${expanded ? (t.status === 'resolved'
+ ? `
Reopen
`
+ : `
`) : ''}
+
`;
+ };
+ const composerHtml = () => {
+ const a = composing;
+ const quote = esc(a.quote && a.quote.length > 220 ? a.quote.slice(0, 220) + '…' : a.quote);
+ let head = '', fields = ``, label = 'Comment';
+ if (a.suggest === 'delete') {
+ head = `Suggest deleting ${quote}
`;
+ fields = ``; label = 'Suggest';
+ } else if (a.suggest === 'replace') {
+ head = `Suggest replacing ${quote}
`;
+ fields = `
+ `; label = 'Suggest';
+ }
+ return `
+
+ ${a.type === 'figure' ? `
On figure: ${esc(a.title || a.figure)}
` : ''}
+ ${head}${fields}
+
${label} Cancel
+
`;
+ };
+
+ const renderBar = () => {
+ const u = bar.querySelector('.rv-user');
+ if (!me.user) {
+ u.innerHTML = inIframe
+ ? `Open review mode in a new tab `
+ : `Sign in with Hugging Face `;
+ } else {
+ u.innerHTML = `${esc(me.user.name)} `
+ + `sign out `;
+ }
+ const hb = bar.querySelector('[data-act="history"]');
+ hb.hidden = !me.reviewer;
+ hb.textContent = `Comments (${threads.filter((t) => t.status !== 'resolved').length})`;
+ hb.classList.toggle('is-on', historyOpen);
+ };
+
+ const renderPanel = () => {
+ panel.hidden = !(me.reviewer && (historyOpen || (!geo.margin && active)));
+ if (panel.hidden) return;
+ const counts = { open: threads.filter((t) => t.status !== 'resolved').length, resolved: threads.filter((t) => t.status === 'resolved').length, all: threads.length };
+ tabs.innerHTML = ['open', 'resolved', 'all'].map((f) => `${f[0].toUpperCase() + f.slice(1)} ${counts[f]} `).join('');
+ const list = threads.filter((t) => filter === 'all' || (filter === 'resolved' ? t.status === 'resolved' : t.status !== 'resolved'));
+ // Document order for placed threads, detached ones at the end.
+ const placed = list.filter((t) => anchors[t.id]).sort((a, b) => (anchors[a.id].compareDocumentPosition(anchors[b.id]) & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1));
+ const loose = list.filter((t) => !anchors[t.id]);
+ let html = lastError ? `${esc(lastError)}
` : '';
+ if (!geo.margin && active === 'new' && composing) html += composerHtml();
+ html += placed.concat(loose).map((t) => cardHtml(t, !geo.margin && t.id === active, 'panel')).join('');
+ if (!list.length && active !== 'new') html += `${filter === 'resolved' ? 'Nothing resolved yet.' : 'No comments yet. Select text in the article, or use the Comment button on a figure.'}
`;
+ panelList.innerHTML = html;
+ };
+
+ const renderMargin = () => {
+ margin.innerHTML = '';
+ if (!me.reviewer || !geo.margin) return;
+ if (lastError) margin.insertAdjacentHTML('beforeend', `${esc(lastError)}
`);
+ if (active === 'new' && composing) margin.insertAdjacentHTML('beforeend', composerHtml());
+ shown().filter((t) => anchors[t.id]).forEach((t) => margin.insertAdjacentHTML('beforeend', cardHtml(t, t.id === active, 'margin')));
+ layout();
+ };
+
+ // Docs-style stacking: every card wants to sit level with its anchor. The active card gets exactly
+ // that spot; cards below it flow downwards and cards above it are pushed up to make room.
+ const layout = () => {
+ if (!geo.margin) return;
+ const cards = [...margin.querySelectorAll('.rv-card')];
+ const want = (c) => {
+ const id = c.dataset.thread;
+ const target = id === 'new' ? composing?.el : anchors[id];
+ if (!target || !target.isConnected) return null;
+ return target.getBoundingClientRect().top + window.scrollY - 6;
+ };
+ const rows = cards.map((c) => ({ c, y: want(c), h: c.offsetHeight })).filter((r) => r.y != null).sort((a, b) => a.y - b.y);
+ const gap = 10;
+ let pivot = rows.findIndex((r) => r.c.classList.contains('is-active'));
+ if (pivot < 0) pivot = 0;
+ const top = new Array(rows.length);
+ if (rows.length) top[pivot] = rows[pivot].y;
+ for (let i = pivot + 1; i < rows.length; i++) top[i] = Math.max(rows[i].y, top[i - 1] + rows[i - 1].h + gap);
+ for (let i = pivot - 1; i >= 0; i--) top[i] = Math.min(rows[i].y, top[i + 1] - rows[i].h - gap);
+ rows.forEach((r, i) => {
+ r.c.style.top = `${top[i]}px`;
+ r.c.style.left = `${geo.left - (r.c.classList.contains('is-active') ? 18 : 0)}px`;
+ r.c.style.width = `${geo.width}px`;
+ });
+ const note = margin.querySelector('.rv-float-note');
+ if (note) { note.style.top = `${window.scrollY + 64}px`; note.style.left = `${geo.left}px`; note.style.width = `${geo.width}px`; }
+ };
+
+ const render = () => {
+ measure();
+ anchors = anchorAll();
+ markActive();
+ renderBar();
+ renderMargin();
+ renderPanel();
+ const ta = ui.querySelector('textarea[data-role="replace"]') || ui.querySelector('textarea[data-role="new"]') || ui.querySelector('textarea.rv-edit');
+ if (ta) { ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); }
+ };
+
+ const activate = (tid, { scroll = false } = {}) => {
+ active = tid; editing = null; closeMenu();
+ if (tid && tid !== 'new') composing = null;
+ render();
+ if (!tid) { if (history.replaceState && location.hash.startsWith('#rv-')) history.replaceState(null, '', here()); return; }
+ const card = ui.querySelector(`.rv-card[data-thread="${tid}"]`);
+ if (scroll && anchors[tid]) anchors[tid].scrollIntoView({ block: 'center', behavior: 'smooth' });
+ else if (card && !geo.margin) card.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
+ if (tid !== 'new' && history.replaceState) history.replaceState(null, '', `${here()}#rv-${tid}`);
+ };
+
+ // ------------------------------------------------------------ menus
+ const closeMenu = () => { menu.hidden = true; menu.innerHTML = ''; };
+ const openMenu = (btn, items) => {
+ menu.innerHTML = items.map(([act, label]) => `${label} `).join('');
+ menu.classList.toggle('rv-menu-emoji', items.every(([a]) => a.startsWith('react:')));
+ const r = btn.getBoundingClientRect();
+ menu.style.top = `${r.bottom + window.scrollY + 4}px`;
+ menu.style.left = `${Math.max(window.scrollX + 8, Math.min(r.right + window.scrollX - 170, window.scrollX + document.documentElement.clientWidth - 186))}px`;
+ menu.hidden = false;
+ menu.dataset.tid = btn.closest('.rv-card')?.dataset.thread || '';
+ menu.dataset.mid = btn.dataset.mid || '';
+ };
+
+ // ------------------------------------------------------------ figures
+ const addFigureButtons = () => {
+ main.querySelectorAll('figure.html-embed').forEach((f) => {
+ if (!figKey(f) || f.querySelector('.rv-fig-btn')) return;
+ f.classList.add('rv-fig');
+ const b = el(`+ Comment `);
+ b.addEventListener('click', (e) => {
+ e.stopPropagation();
+ composing = { type: 'figure', figure: figKey(f), title: f.querySelector('.html-embed__title')?.textContent.trim() || '', ...sectionOf(f), el: f };
+ activate('new');
+ });
+ f.appendChild(b);
+ });
+ };
+
+ // ------------------------------------------------------------ selection
+ const clearProbe = () => main.querySelectorAll('span.rv-probe').forEach((p) => { const parent = p.parentNode; p.remove(); if (parent) parent.normalize(); });
+ const selectionAnchor = () => {
+ const sel = window.getSelection();
+ if (!sel || sel.isCollapsed || !sel.rangeCount) return null;
+ const r = sel.getRangeAt(0);
+ if (!main.contains(r.commonAncestorContainer)) return null;
+ const startEl = r.startContainer.nodeType === 1 ? r.startContainer : r.startContainer.parentElement;
+ if (skip(startEl)) return null;
+ const idx = buildIndex();
+ const node = (c, o) => (c.nodeType === 3 ? [c, o] : [c.childNodes[o] || c, 0]);
+ const s = posOf(idx, ...node(r.startContainer, r.startOffset));
+ const e = posOf(idx, ...node(r.endContainer, r.endOffset));
+ const quote = idx.text.slice(s, e).trim();
+ if (quote.length < 2) return null;
+ const rect = r.getBoundingClientRect();
+ const a = { type: 'text', quote, prefix: idx.text.slice(Math.max(0, s - 40), s), suffix: idx.text.slice(e, e + 40), ...sectionOf(r.startContainer), rect };
+ // A zero-size marker where the selection starts, so the composer can sit level with it. It is
+ // placed only once the comment is started, so selecting text never changes the page.
+ a.place = () => { const probe = document.createElement('span'); probe.className = 'rv-probe'; const rr = r.cloneRange(); rr.collapse(true); rr.insertNode(probe); return probe; };
+ return a;
+ };
+ let pending = null;
+ const showAdd = () => {
+ if (!me || !me.reviewer) return;
+ pending = selectionAnchor();
+ if (!pending) { addBtn.hidden = true; return; }
+ addBtn.hidden = false;
+ if (geo.margin) { addBtn.style.top = `${pending.rect.top + window.scrollY}px`; addBtn.style.left = `${geo.left}px`; }
+ else { addBtn.style.top = `${pending.rect.bottom + window.scrollY + 8}px`; addBtn.style.left = `${Math.min(pending.rect.right + window.scrollX - 60, window.scrollX + document.documentElement.clientWidth - 130)}px`; }
+ };
+ const startComment = (kind = 'comment') => {
+ if (!pending) return;
+ const { rect, place, ...a } = pending;
+ if (kind !== 'comment') a.suggest = kind;
+ clearProbe();
+ a.el = place();
+ composing = a; pending = null; addBtn.hidden = true;
+ window.getSelection()?.removeAllRanges();
+ activate('new');
+ };
+ document.addEventListener('mouseup', (ev) => { if (!ev.composedPath().includes(ui)) setTimeout(showAdd, 0); });
+ document.addEventListener('keyup', (ev) => { if (ev.shiftKey && !ev.composedPath().includes(ui)) setTimeout(showAdd, 0); });
+ addBtn.addEventListener('mousedown', (e) => e.preventDefault());
+ addBtn.addEventListener('click', (e) => { const b = e.target.closest('[data-kind]'); if (b) startComment(b.dataset.kind); });
+
+ // ------------------------------------------------------------ events
+ document.addEventListener('keydown', (e) => {
+ if ((e.metaKey || e.ctrlKey) && e.altKey && e.code === 'KeyM') { e.preventDefault(); showAdd(); startComment(); }
+ if (e.key === 'Escape') {
+ closeMenu();
+ if (active) { if (active === 'new') { composing = null; clearProbe(); } activate(null); }
+ }
+ });
+ // Clicking the page: a highlight opens its thread, anywhere else closes the active one, like Docs.
+ // The path is read from the event, not the DOM: a click inside the layer re-renders it, which can
+ // detach the clicked element before this handler runs.
+ const fromUi = (e) => e.composedPath().includes(ui);
+ document.addEventListener('click', (e) => {
+ if (e.composedPath().includes(menu)) return;
+ closeMenu();
+ if (fromUi(e) || !me || !me.reviewer) return;
+ const m = e.target.closest('mark.rv-hl, ins.rv-ins');
+ if (m) { activate(m.dataset.thread); return; }
+ if (active && active !== 'new' && !window.getSelection()?.toString()) activate(null);
+ });
+
+ bar.addEventListener('click', (e) => {
+ const act = e.target.closest('[data-act]')?.dataset.act;
+ if (act === 'history') { historyOpen = !historyOpen; render(); }
+ if (act === 'exit') {
+ try { sessionStorage.removeItem('mhrl-review'); } catch (err) {}
+ const u = new URL(location.href); u.searchParams.delete('review'); u.hash = ''; location.href = u.toString();
+ }
+ });
+ panel.addEventListener('click', (e) => {
+ const f = e.target.closest('[data-filter]');
+ if (f) { filter = f.dataset.filter; renderPanel(); return; }
+ if (e.target.closest('[data-act="close-history"]')) { historyOpen = false; if (!geo.margin) active = null; render(); }
+ });
+
+ const replace = (t) => { threads = threads.map((x) => (x.id === t.id ? t : x)); };
+ const onCard = async (e) => {
+ const btn = e.target.closest('[data-act]');
+ const card = e.target.closest('.rv-card');
+ if (!card) return;
+ const tid = card.dataset.thread;
+ if (!btn) {
+ // A click on a card makes it active; from the panel it also jumps to the text.
+ e.stopPropagation();
+ if (tid !== 'new' && tid !== active && !e.target.closest('textarea')) activate(tid, { scroll: e.composedPath().includes(panel) });
+ return;
+ }
+ const act = btn.dataset.act;
+ e.stopPropagation();
+ try {
+ lastError = '';
+ if (act === 'cancel') { composing = null; clearProbe(); activate(null); }
+ if (act === 'post') {
+ const { el: _el, suggest, ...anchor } = composing;
+ const body = (card.querySelector('textarea[data-role="new"]') || card.querySelector('textarea[data-role="note"]'))?.value.trim() || '';
+ let suggestion = null;
+ if (suggest === 'delete') suggestion = { action: 'delete' };
+ if (suggest === 'replace') {
+ const text = card.querySelector('textarea[data-role="replace"]').value.trim();
+ if (!text || text === anchor.quote) return;
+ suggestion = { action: 'replace', text };
+ }
+ if (!body && !suggestion) return;
+ btn.disabled = true;
+ const t = await call('POST', '/threads', { anchor, body, suggestion });
+ threads.push(t); composing = null; clearProbe(); activate(t.id);
+ }
+ if (act === 'resolve') { replace(await call('PATCH', `/threads/${tid}`, { status: 'resolved' })); activate(null); }
+ if (act === 'accept' || act === 'reject') { replace(await call('PATCH', `/threads/${tid}`, { decision: act === 'accept' ? 'accepted' : 'rejected' })); activate(null); }
+ if (act === 'reopen') { replace(await call('PATCH', `/threads/${tid}`, { status: 'open' })); activate(tid); }
+ if (act === 'send-reply') {
+ const ta = card.querySelector('.rv-reply textarea'); const body = ta.value.trim(); if (!body) return;
+ btn.disabled = true;
+ replace(await call('POST', `/threads/${tid}/replies`, { body })); activate(tid);
+ }
+ if (act === 'cancel-reply') { const ta = card.querySelector('.rv-reply textarea'); ta.value = ''; ta.rows = 1; ta.blur(); card.querySelector('.rv-reply-actions').hidden = true; layout(); }
+ if (act === 'menu') {
+ const t = threads.find((x) => x.id === tid), m = t.messages.find((x) => x.id === btn.dataset.mid);
+ const items = [];
+ if (mine(m)) items.push(['edit', 'Edit']);
+ if (mine(m) || isOwner()) items.push(['delete', 'Delete']);
+ items.push(['link', 'Link to this comment']);
+ if (t.status === 'resolved') items.push(['reopen-menu', 'Reopen']);
+ openMenu(btn, items);
+ }
+ if (act === 'save-edit') {
+ const body = card.querySelector('textarea.rv-edit').value.trim(); if (!body) return;
+ replace(await call('PATCH', `/threads/${tid}/messages/${btn.dataset.mid}`, { body })); editing = null; activate(tid);
+ }
+ if (act === 'cancel-edit') { editing = null; render(); }
+ if (act === 'react-pick') openMenu(btn, REACTIONS.map((r) => [`react:${r}`, r]));
+ if (act === 'react') { replace(await call('POST', `/threads/${tid}/messages/${btn.dataset.mid}/reactions`, { emoji: btn.dataset.emoji })); render(); }
+ } catch (err) { lastError = err.message; render(); }
+ };
+ margin.addEventListener('click', onCard);
+ panelList.addEventListener('click', onCard);
+ // The reply box grows and shows its buttons once it has focus, as in Docs.
+ ui.addEventListener('focusin', (e) => {
+ if (e.target.matches('.rv-reply textarea')) { e.target.rows = 3; e.target.closest('.rv-reply').querySelector('.rv-reply-actions').hidden = false; layout(); }
+ });
+ ui.addEventListener('keydown', (e) => {
+ if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
+ const card = e.target.closest('.rv-card'); if (!card) return;
+ card.querySelector('[data-act="post"], [data-act="send-reply"], [data-act="save-edit"]')?.click();
+ }
+ });
+ menu.addEventListener('click', async (e) => {
+ const act = e.target.closest('[data-act]')?.dataset.act; if (!act) return;
+ const tid = menu.dataset.tid, mid = menu.dataset.mid;
+ closeMenu();
+ try {
+ lastError = '';
+ if (act === 'edit') { active = tid; editing = { tid, mid }; render(); }
+ if (act === 'delete') {
+ if (!confirm('Delete this comment?')) return;
+ const r = await call('DELETE', `/threads/${tid}/messages/${mid}`);
+ if (r.deleted) { threads = threads.filter((x) => x.id !== r.deleted); activate(null); } else { replace(r); render(); }
+ }
+ if (act === 'link') {
+ const url = `${location.origin}${location.pathname}?review#rv-${tid}`;
+ try { await navigator.clipboard.writeText(url); } catch (err) { prompt('Link to this comment', url); }
+ }
+ if (act === 'reopen-menu') { replace(await call('PATCH', `/threads/${tid}`, { status: 'open' })); activate(tid); }
+ if (act.startsWith('react:')) { replace(await call('POST', `/threads/${tid}/messages/${mid}/reactions`, { emoji: act.slice(6) })); render(); }
+ } catch (err) { lastError = err.message; render(); }
+ });
+
+ // Figures render after the page loads and change height, which moves every anchor below them.
+ let raf = 0;
+ const relayout = () => { cancelAnimationFrame(raf); raf = requestAnimationFrame(() => { measure(); renderMargin(); }); };
+ if ('ResizeObserver' in window) new ResizeObserver(relayout).observe(main);
+ window.addEventListener('resize', relayout);
+
+ // Read-only view of the state, for debugging from the console.
+ window.__rvState = () => ({ active, historyOpen, filter, geo, threads: threads.length, open: threads.filter((t) => t.status !== 'resolved').length });
+
+ // ------------------------------------------------------------ start
+ const refresh = async () => {
+ if (!me || !me.reviewer) return;
+ try { threads = await call('GET', '/threads'); lastError = ''; } catch (err) { lastError = err.message; }
+ // Don't redraw under someone who is typing.
+ const typing = document.activeElement && document.activeElement.tagName === 'TEXTAREA' && ui.contains(document.activeElement);
+ if (!typing) render();
+ };
+ (async () => {
+ try { me = await call('GET', '/status'); } catch (err) { me = null; }
+ if (!me || !me.enabled) { ui.remove(); return; } // published: leave the page untouched
+ if (me.reviewer) addFigureButtons();
+ await refresh();
+ render();
+ const m = location.hash.match(/^#rv-(.+)$/);
+ if (m && threads.some((t) => t.id === m[1])) {
+ const t = threads.find((x) => x.id === m[1]);
+ if (t.status === 'resolved') { historyOpen = true; filter = 'resolved'; }
+ activate(m[1], { scroll: true });
+ }
+ setInterval(() => { if (!document.hidden) refresh(); }, 30000);
+ })();
+})();
diff --git a/content/articles/multi-harness-rl/app/public/robots.txt b/content/articles/multi-harness-rl/app/public/robots.txt
new file mode 100644
index 00000000..22354eb5
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/public/robots.txt
@@ -0,0 +1,5 @@
+User-agent: *
+Allow: /
+
+Sitemap: https://fineenvs-multi-harness-rl.hf.space/sitemap.xml
+LLMs-Txt: /llms.txt
diff --git a/content/articles/multi-harness-rl/app/public/scripts/color-palettes.js b/content/articles/multi-harness-rl/app/public/scripts/color-palettes.js
new file mode 100644
index 00000000..370b1f46
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/public/scripts/color-palettes.js
@@ -0,0 +1,274 @@
+// Global color palettes generator and watcher
+// - Observes CSS variable --primary-color and theme changes
+// - Generates categorical, sequential, and diverging palettes (OKLCH/OKLab)
+// - Exposes results as CSS variables on :root
+// - Supports variable color counts per palette via CSS vars
+// - Dispatches a 'palettes:updated' CustomEvent after each update
+
+(() => {
+ const MODE = { cssRoot: document.documentElement };
+
+ const getCssVar = (name) => {
+ try { return getComputedStyle(MODE.cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
+ };
+ const getIntFromCssVar = (name, fallback) => {
+ const raw = getCssVar(name);
+ if (!raw) return fallback;
+ const v = parseInt(String(raw), 10);
+ if (Number.isNaN(v)) return fallback;
+ return v;
+ };
+ const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
+
+ // Color math (OKLab/OKLCH)
+ const srgbToLinear = (u) => (u <= 0.04045 ? u / 12.92 : Math.pow((u + 0.055) / 1.055, 2.4));
+ const linearToSrgb = (u) => (u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055);
+ const rgbToOklab = (r, g, b) => {
+ const rl = srgbToLinear(r), gl = srgbToLinear(g), bl = srgbToLinear(b);
+ const l = Math.cbrt(0.4122214708 * rl + 0.5363325363 * gl + 0.0514459929 * bl);
+ const m = Math.cbrt(0.2119034982 * rl + 0.6806995451 * gl + 0.1073969566 * bl);
+ const s = Math.cbrt(0.0883024619 * rl + 0.2817188376 * gl + 0.6299787005 * bl);
+ const L = 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * s;
+ const a = 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * s;
+ const b2 = 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * s;
+ return { L, a, b: b2 };
+ };
+ const oklabToRgb = (L, a, b) => {
+ const l_ = L + 0.3963377774 * a + 0.2158037573 * b;
+ const m_ = L - 0.1055613458 * a - 0.0638541728 * b;
+ const s_ = L - 0.0894841775 * a - 1.2914855480 * b;
+ const l = l_ * l_ * l_;
+ const m = m_ * m_ * m_;
+ const s = s_ * s_ * s_;
+ const r = linearToSrgb(+4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s);
+ const g = linearToSrgb(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s);
+ const b3 = linearToSrgb(-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s);
+ return { r, g, b: b3 };
+ };
+ const oklchToOklab = (L, C, hDeg) => { const h = (hDeg * Math.PI) / 180; return { L, a: C * Math.cos(h), b: C * Math.sin(h) }; };
+ const oklabToOklch = (L, a, b) => { const C = Math.sqrt(a * a + b * b); let h = Math.atan2(b, a) * 180 / Math.PI; if (h < 0) h += 360; return { L, C, h }; };
+ const clamp01 = (x) => Math.min(1, Math.max(0, x));
+ const isInGamut = ({ r, g, b }) => r >= 0 && r <= 1 && g >= 0 && g <= 1 && b >= 0 && b <= 1;
+ const toHex = ({ r, g, b }) => {
+ const R = Math.round(clamp01(r) * 255), G = Math.round(clamp01(g) * 255), B = Math.round(clamp01(b) * 255);
+ const h = (n) => n.toString(16).padStart(2, '0');
+ return `#${h(R)}${h(G)}${h(B)}`.toUpperCase();
+ };
+ const oklchToHexSafe = (L, C, h) => { let c = C; for (let i = 0; i < 12; i++) { const { a, b } = oklchToOklab(L, c, h); const rgb = oklabToRgb(L, a, b); if (isInGamut(rgb)) return toHex(rgb); c = Math.max(0, c - 0.02); } return toHex(oklabToRgb(L, 0, 0)); };
+ const parseCssColorToRgb = (css) => { try { const el = document.createElement('span'); el.style.color = css; document.body.appendChild(el); const cs = getComputedStyle(el).color; document.body.removeChild(el); const m = cs.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/i); if (!m) return null; return { r: Number(m[1]) / 255, g: Number(m[2]) / 255, b: Number(m[3]) / 255 }; } catch { return null; } };
+
+ // Get primary color in OKLCH format to preserve precision
+ const getPrimaryOKLCH = () => {
+ const css = getCssVar('--primary-color');
+ if (!css) return null;
+
+ // For OKLCH colors, return the exact values without conversion
+ if (css.includes('oklch')) {
+ const oklchMatch = css.match(/oklch\(([^)]+)\)/);
+ if (oklchMatch) {
+ const values = oklchMatch[1].split(/\s+/).map(v => parseFloat(v.trim()));
+ if (values.length >= 3) {
+ const [L, C, h] = values;
+ return { L, C, h };
+ }
+ }
+ }
+
+ // For non-OKLCH colors, convert to OKLCH for consistency
+ const rgb = parseCssColorToRgb(css);
+ if (rgb) {
+ const { L, a, b } = rgbToOklab(rgb.r, rgb.g, rgb.b);
+ const { C, h } = oklabToOklch(L, a, b);
+ return { L, C, h };
+ }
+ return null;
+ };
+
+ // Keep getPrimaryHex for backward compatibility, but now it converts from OKLCH
+ const getPrimaryHex = () => {
+ const oklch = getPrimaryOKLCH();
+ if (!oklch) return null;
+
+ const { a, b } = oklchToOklab(oklch.L, oklch.C, oklch.h);
+ const rgb = oklabToRgb(oklch.L, a, b);
+ return toHex(rgb);
+ };
+ // No count management via CSS anymore; counts are passed directly to the API
+
+ const generators = {
+ categorical: (baseOKLCH, count) => {
+ const { L, C, h } = baseOKLCH;
+ const L0 = Math.min(0.85, Math.max(0.4, L));
+ const C0 = Math.min(0.35, Math.max(0.1, C || 0.2));
+ const total = Math.max(1, Math.min(12, count || 8));
+ const hueStep = 360 / total;
+ const results = [];
+ for (let i = 0; i < total; i++) {
+ const hDeg = (h + i * hueStep) % 360;
+ const lVar = ((i % 3) - 1) * 0.04;
+ results.push(oklchToHexSafe(Math.max(0.4, Math.min(0.85, L0 + lVar)), C0, hDeg));
+ }
+ return results;
+ },
+ sequential: (baseOKLCH, count) => {
+ const { L, C, h } = baseOKLCH;
+ const total = Math.max(1, Math.min(12, count || 8));
+ const startL = Math.max(0.25, L - 0.18);
+ const endL = Math.min(0.92, L + 0.18);
+ const cBase = Math.min(0.33, Math.max(0.08, C * 0.9 + 0.06));
+ const out = [];
+ for (let i = 0; i < total; i++) {
+ const t = total === 1 ? 0 : i / (total - 1);
+ const lNow = startL * (1 - t) + endL * t;
+ const cNow = cBase * (0.85 + 0.15 * (1 - Math.abs(0.5 - t) * 2));
+ out.push(oklchToHexSafe(lNow, cNow, h));
+ }
+ return out;
+ },
+ diverging: (baseOKLCH, count) => {
+ const { L, C, h } = baseOKLCH;
+ const total = Math.max(1, Math.min(12, count || 8));
+
+ // Left endpoint: EXACT primary color (no darkening)
+ const leftLab = oklchToOklab(L, C, h);
+ // Right endpoint: complement with same L and similar C (clamped safe)
+ const compH = (h + 180) % 360;
+ const cSafe = Math.min(0.35, Math.max(0.08, C));
+ const rightLab = oklchToOklab(L, cSafe, compH);
+ const whiteLab = { L: 0.98, a: 0, b: 0 }; // center near‑white
+
+ const hexFromOKLab = (L, a, b) => toHex(oklabToRgb(L, a, b));
+ const lerp = (a, b, t) => a + (b - a) * t;
+ const lerpOKLabHex = (A, B, t) => hexFromOKLab(lerp(A.L, B.L, t), lerp(A.a, B.a, t), lerp(A.b, B.b, t));
+
+ const out = [];
+ if (total % 2 === 1) {
+ const nSide = (total - 1) >> 1; // items on each side
+ // Left side: include left endpoint exactly at index 0
+ for (let i = 0; i < nSide; i++) {
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
+ // Move from leftLab to a value close (but not equal) to white; ensure last before center is lighter
+ const tt = t * 0.9; // keep some distance from pure white before center
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
+ }
+ // Center
+ out.push(hexFromOKLab(whiteLab.L, whiteLab.a, whiteLab.b));
+ // Right side: start near white and end EXACTLY at rightLab
+ for (let i = 0; i < nSide; i++) {
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
+ const tt = Math.max(0.1, t); // avoid starting at pure white
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
+ }
+ // Ensure first and last are exact endpoints
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
+ } else {
+ const nSide = total >> 1;
+ // Left half including left endpoint, approaching white but not reaching it
+ for (let i = 0; i < nSide; i++) {
+ const t = nSide <= 1 ? 0 : (i / (nSide - 1)); // 0 .. 1
+ const tt = t * 0.9;
+ out.push(lerpOKLabHex(leftLab, whiteLab, tt));
+ }
+ // Right half: mirror from near white to exact right endpoint
+ for (let i = 0; i < nSide; i++) {
+ const t = nSide <= 1 ? 1 : ((i + 1) / nSide); // (1/n)..1
+ const tt = Math.max(0.1, t);
+ out.push(lerpOKLabHex(whiteLab, rightLab, tt));
+ }
+ if (out.length) { out[0] = hexFromOKLab(leftLab.L, leftLab.a, leftLab.b); out[out.length - 1] = hexFromOKLab(rightLab.L, rightLab.a, rightLab.b); }
+ }
+ return out;
+ }
+ };
+
+ let lastSignature = '';
+
+ const updatePalettes = () => {
+ const primaryOKLCH = getPrimaryOKLCH();
+ const primaryHex = getPrimaryHex();
+ const signature = `${primaryOKLCH?.L},${primaryOKLCH?.C},${primaryOKLCH?.h}`;
+ if (signature === lastSignature) return;
+ lastSignature = signature;
+ try { document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { }
+ };
+
+ const bootstrap = () => {
+ // Initial setup - only run once on page load
+ updatePalettes();
+
+ // Observer will handle all subsequent changes
+ const mo = new MutationObserver(() => updatePalettes());
+ mo.observe(MODE.cssRoot, { attributes: true, attributeFilter: ['style', 'data-theme'] });
+
+ // Utility: choose high-contrast (or softened) text style against an arbitrary background color
+ const pickTextStyleForBackground = (bgCss, opts = {}) => {
+ const cssRoot = document.documentElement;
+ const getCssVar = (name) => {
+ try { return getComputedStyle(cssRoot).getPropertyValue(name).trim(); } catch { return ''; }
+ };
+ const resolveCssToRgb01 = (css) => {
+ const rgb = parseCssColorToRgb(css);
+ if (!rgb) return null;
+ return rgb; // already 0..1
+ };
+ const mixRgb01 = (a, b, t) => ({ r: a.r * (1 - t) + b.r * t, g: a.g * (1 - t) + b.g * t, b: a.b * (1 - t) + b.b * t });
+ const relLum = (rgb) => {
+ const f = (u) => srgbToLinear(u);
+ return 0.2126 * f(rgb.r) + 0.7152 * f(rgb.g) + 0.0722 * f(rgb.b);
+ };
+ const contrast = (fg, bg) => {
+ const L1 = relLum(fg), L2 = relLum(bg); const a = Math.max(L1, L2), b = Math.min(L1, L2);
+ return (a + 0.05) / (b + 0.05);
+ };
+ try {
+ const bg = resolveCssToRgb01(bgCss);
+ if (!bg) return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
+ const candidatesCss = [getCssVar('--text-color') || '#111', getCssVar('--on-primary') || '#0f1115', '#000', '#fff'];
+ const candidates = candidatesCss
+ .map(css => ({ css, rgb: resolveCssToRgb01(css) }))
+ .filter(x => !!x.rgb);
+ // Pick the max contrast
+ let best = candidates[0]; let bestCR = contrast(best.rgb, bg);
+ for (let i = 1; i < candidates.length; i++) {
+ const cr = contrast(candidates[i].rgb, bg);
+ if (cr > bestCR) { best = candidates[i]; bestCR = cr; }
+ }
+ // Optional softening via blend factor (0..1), blending towards muted color
+ const blend = Math.min(1, Math.max(0, Number(opts.blend || 0)));
+ let finalRgb = best.rgb;
+ if (blend > 0) {
+ const mutedCss = getCssVar('--muted-color') || (getCssVar('--text-color') || '#111');
+ const mutedRgb = resolveCssToRgb01(mutedCss) || best.rgb;
+ finalRgb = mixRgb01(best.rgb, mutedRgb, blend);
+ }
+ const haloStrength = Math.min(1, Math.max(0, Number(opts.haloStrength == null ? 0.5 : opts.haloStrength)));
+ const stroke = (best.css === '#000' || best.css.toLowerCase() === 'black') ? `rgba(255,255,255,${0.30 + 0.40 * haloStrength})` : `rgba(0,0,0,${0.30 + 0.30 * haloStrength})`;
+ return { fill: toHex(finalRgb), stroke, strokeWidth: (opts.haloWidth == null ? 1 : Number(opts.haloWidth)) };
+ } catch {
+ return { fill: getCssVar('--text-color') || '#000', stroke: 'var(--transparent-page-contrast)', strokeWidth: 1 };
+ }
+ };
+ window.ColorPalettes = {
+ refresh: updatePalettes,
+ notify: () => { try { const primaryOKLCH = getPrimaryOKLCH(); const primaryHex = getPrimaryHex(); document.dispatchEvent(new CustomEvent('palettes:updated', { detail: { primary: primaryHex, primaryOKLCH } })); } catch { } },
+ getPrimary: () => getPrimaryHex(),
+ getPrimaryOKLCH: () => getPrimaryOKLCH(),
+ getColors: (key, count = 6) => {
+ const primaryOKLCH = getPrimaryOKLCH();
+ if (!primaryOKLCH) return [];
+ const total = Math.max(1, Math.min(12, Number(count) || 6));
+ if (key === 'categorical') return generators.categorical(primaryOKLCH, total);
+ if (key === 'sequential') return generators.sequential(primaryOKLCH, total);
+ if (key === 'diverging') return generators.diverging(primaryOKLCH, total);
+ return [];
+ },
+ getTextStyleForBackground: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {}),
+ chooseReadableText: (bgCss, opts) => pickTextStyleForBackground(bgCss, opts || {})
+ };
+ };
+
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', bootstrap, { once: true });
+ else bootstrap();
+})();
+
+
diff --git a/content/articles/multi-harness-rl/app/public/sitemap.xml b/content/articles/multi-harness-rl/app/public/sitemap.xml
new file mode 100644
index 00000000..bade0870
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/public/sitemap.xml
@@ -0,0 +1,7 @@
+
+
+
+ https://fineenvs-multi-harness-rl.hf.space/
+ 2026-09-24
+
+
diff --git a/content/articles/multi-harness-rl/app/scripts/EXPORT-PDF-BOOK.md b/content/articles/multi-harness-rl/app/scripts/EXPORT-PDF-BOOK.md
new file mode 100644
index 00000000..25ed30da
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/scripts/EXPORT-PDF-BOOK.md
@@ -0,0 +1,311 @@
+# 📚 Export PDF Book avec Paged.js
+
+Système de génération de PDF professionnel avec mise en page type livre, propulsé par **Paged.js**.
+
+## ✨ Fonctionnalités
+
+### Mise en page professionnelle
+- ✅ **Pagination automatique** avec Paged.js
+- ✅ **Running headers** : titres de chapitres en haut de page
+- ✅ **Numérotation des pages** : alternée gauche/droite
+- ✅ **Marges asymétriques** : optimisées pour reliure (recto/verso)
+- ✅ **Gestion veuves et orphelines** : évite les lignes isolées
+- ✅ **Typographie professionnelle** : justification, césure automatique
+
+### Éléments de livre
+- 📖 Compteurs automatiques : chapitres, figures, tableaux
+- 📑 Notes de bas de page (si implémentées)
+- 🔢 Numérotation hiérarchique (1.2.3, etc.)
+- 📊 Support complet des visualisations D3/Plotly
+- 🖼️ Figures avec légendes numérotées
+- 📝 Citations et références
+
+## 🚀 Utilisation
+
+### Commande de base
+
+```bash
+npm run export:pdf:book
+```
+
+Cette commande va :
+1. Builder le site Astro (si nécessaire)
+2. Démarrer un serveur preview
+3. Charger la page et injecter Paged.js
+4. Paginer le contenu automatiquement
+5. Générer le PDF dans `dist/article-book.pdf`
+
+### Options disponibles
+
+```bash
+# Thème sombre
+npm run export:pdf:book -- --theme=dark
+
+# Format personnalisé
+npm run export:pdf:book -- --format=Letter
+
+# Nom de fichier custom
+npm run export:pdf:book -- --filename=mon-livre
+
+# Combinaison d'options
+npm run export:pdf:book -- --theme=light --format=A4 --filename=thesis
+```
+
+#### Options détaillées
+
+| Option | Valeurs | Défaut | Description |
+|--------|---------|--------|-------------|
+| `--theme` | `light`, `dark` | `light` | Thème de couleur |
+| `--format` | `A4`, `Letter`, `Legal`, `A3`, `Tabloid` | `A4` | Format de page |
+| `--filename` | `string` | `article-book` | Nom du fichier de sortie |
+| `--wait` | `full`, `images`, `plotly`, `d3` | `full` | Stratégie d'attente |
+
+## 📐 Format de page
+
+Le système utilise des marges optimisées pour l'impression livre :
+
+### Pages de droite (recto)
+- Marge gauche : **25mm** (reliure)
+- Marge droite : **20mm**
+- Header droite : titre de section
+- Footer droite : numéro de page
+
+### Pages de gauche (verso)
+- Marge gauche : **20mm**
+- Marge droite : **25mm** (reliure)
+- Header gauche : titre de chapitre
+- Footer gauche : numéro de page
+
+### Première page
+- Marges augmentées (40mm haut/bas)
+- Pas de headers/footers
+- Centrée
+
+## 🎨 Personnalisation CSS
+
+Le style livre est défini dans :
+```
+app/src/styles/_print-book.css
+```
+
+### Modifier les marges
+
+```css
+@page {
+ margin-top: 20mm;
+ margin-bottom: 25mm;
+ /* ... */
+}
+
+@page :left {
+ margin-left: 20mm;
+ margin-right: 25mm;
+}
+
+@page :right {
+ margin-left: 25mm;
+ margin-right: 20mm;
+}
+```
+
+### Modifier la typographie
+
+```css
+body {
+ font-family: "Georgia", "Palatino", "Times New Roman", serif;
+ font-size: 11pt;
+ line-height: 1.6;
+}
+
+h2 {
+ font-size: 18pt;
+ /* ... */
+}
+```
+
+### Personnaliser les running headers
+
+```css
+@page :left {
+ @top-left {
+ content: string(chapter-title);
+ font-size: 9pt;
+ font-style: italic;
+ /* ... */
+ }
+}
+
+@page :right {
+ @top-right {
+ content: string(section-title);
+ /* ... */
+ }
+}
+```
+
+### Ajouter un logo/filigrane
+
+```css
+@page {
+ background-image: url('/logo.png');
+ background-position: bottom center;
+ background-size: 20mm;
+ background-repeat: no-repeat;
+}
+```
+
+## 🔧 Configuration Paged.js avancée
+
+### Hooks JavaScript personnalisés
+
+Vous pouvez ajouter des hooks Paged.js dans le script `export-pdf-book.mjs` :
+
+```javascript
+// Après l'injection de Paged.js
+await page.evaluate(() => {
+ class BookHooks extends window.Paged.Handler {
+ beforeParsed(content) {
+ // Modifier le contenu avant pagination
+ }
+
+ afterParsed(parsed) {
+ // Après l'analyse
+ }
+
+ afterRendered(pages) {
+ // Après le rendu de toutes les pages
+ console.log(`Rendered ${pages.length} pages`);
+ }
+ }
+
+ window.Paged.registerHandlers(BookHooks);
+});
+```
+
+### Forcer des sauts de page
+
+Dans votre MDX :
+
+```mdx
+## Chapitre 1
+
+Contenu...
+
+
+
+## Chapitre 2 (commence sur une nouvelle page)
+```
+
+Ou avec une classe CSS :
+
+```css
+.chapter-break {
+ break-after: page;
+}
+```
+
+## 📊 Visualisations
+
+Les graphiques D3 et Plotly sont automatiquement :
+- ✅ Redimensionnés pour le format livre
+- ✅ Rendus en haute qualité
+- ✅ Évitent les coupures de page
+- ✅ Conservent l'interactivité dans le HTML source
+
+## 🐛 Dépannage
+
+### Le PDF est vide ou incomplet
+
+```bash
+# Augmenter le temps d'attente
+npm run export:pdf:book -- --wait=full
+```
+
+### Les images ne s'affichent pas
+
+Vérifiez que les chemins d'images sont **absolus** dans le HTML :
+```html
+
+
+
+
+
+```
+
+### Les graphiques sont coupés
+
+Ajoutez dans `_print-book.css` :
+```css
+.your-chart-class {
+ max-height: 180mm !important;
+ break-inside: avoid;
+}
+```
+
+### Erreur "Paged.js not found"
+
+```bash
+# Réinstaller Paged.js
+cd app
+npm install pagedjs
+```
+
+### Le serveur ne démarre pas
+
+```bash
+# Port déjà utilisé ? Changer le port
+PREVIEW_PORT=8081 npm run export:pdf:book
+```
+
+## 📚 Ressources Paged.js
+
+- **Documentation officielle** : https://pagedjs.org/documentation/
+- **Spécifications CSS Paged Media** : https://www.w3.org/TR/css-page-3/
+- **Exemples** : https://pagedjs.org/examples/
+
+## 🆚 Différences avec export:pdf standard
+
+| Fonctionnalité | `export:pdf` | `export:pdf:book` |
+|----------------|--------------|-------------------|
+| Pagination | Navigateur standard | Paged.js professionnel |
+| Running headers | ❌ | ✅ |
+| Marges reliure | ❌ | ✅ |
+| Numérotation avancée | ❌ | ✅ |
+| Compteurs automatiques | ❌ | ✅ |
+| Gestion veuves/orphelines | Basique | Avancée |
+| Notes de bas de page | ❌ | ✅ (si activées) |
+| Contrôle typographique | Standard | Professionnel |
+| Table des matières | Manuelle | Automatique (avec CSS) |
+
+## 💡 Conseils pour un résultat optimal
+
+1. **Structurez votre contenu** avec des `` pour les chapitres
+2. **Utilisez des `` pour les sections** (apparaissent dans les headers)
+3. **Ajoutez des IDs** aux titres pour les références croisées
+4. **Optimisez les images** : résolution 300 DPI pour l'impression
+5. **Testez le rendu** avant l'impression finale
+6. **Évitez les couleurs vives** en mode print (privilégier les niveaux de gris)
+
+## 🎯 Cas d'usage
+
+Ce système est idéal pour :
+- 📘 **Thèses et mémoires**
+- 📗 **Livres techniques**
+- 📕 **Rapports académiques**
+- 📙 **Documentation longue**
+- 📓 **E-books premium**
+- 📔 **Revues scientifiques**
+
+## 🔮 Améliorations futures
+
+- [ ] Génération automatique de table des matières
+- [ ] Support des index
+- [ ] Références croisées automatiques
+- [ ] Export en EPUB
+- [ ] Templates de livre préconfigurés
+- [ ] Mode "two-up" pour visualisation double page
+
+---
+
+**Créé avec ❤️ par votre équipe template**
+
diff --git a/content/articles/multi-harness-rl/app/scripts/README-PDF-BOOK.md b/content/articles/multi-harness-rl/app/scripts/README-PDF-BOOK.md
new file mode 100644
index 00000000..5f106d9c
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/scripts/README-PDF-BOOK.md
@@ -0,0 +1,309 @@
+# 📚 Export PDF Livre - Guide Complet
+
+Système de génération de PDF professionnel avec mise en page type livre pour votre template d'article scientifique.
+
+## 🎯 Objectif
+
+Créer des PDFs de qualité professionnelle avec :
+- Typographie soignée (Georgia, justification, césure)
+- Marges asymétriques pour reliure
+- Running headers avec titres de chapitres
+- Numérotation de pages gauche/droite
+- Gestion veuves et orphelines
+- Style livre académique/éditorial
+
+## 📦 Ce qui a été créé
+
+### Fichiers créés
+
+```
+app/
+├── scripts/
+│ ├── export-pdf-book.mjs ← Version avec Paged.js (avancée, en cours)
+│ ├── export-pdf-book-simple.mjs ← Version simple (RECOMMANDÉE ✅)
+│ └── EXPORT-PDF-BOOK.md ← Documentation détaillée
+└── src/
+ └── styles/
+ └── _print-book.css ← Styles CSS Paged Media
+```
+
+### Commandes npm ajoutées
+
+```json
+{
+ "export:pdf:book": "Version Paged.js (expérimentale)",
+ "export:pdf:book:simple": "Version simple (stable ✅)"
+}
+```
+
+## 🚀 Utilisation
+
+### Commande recommandée
+
+```bash
+npm run export:pdf:book:simple
+```
+
+Le PDF sera généré dans :
+- `dist/article-book.pdf`
+- `public/article-book.pdf` (copie automatique)
+
+### Options disponibles
+
+```bash
+# Thème sombre
+npm run export:pdf:book:simple -- --theme=dark
+
+# Format Letter
+npm run export:pdf:book:simple -- --format=Letter
+
+# Nom personnalisé
+npm run export:pdf:book:simple -- --filename=ma-these
+
+# Combinaison
+npm run export:pdf:book:simple -- --theme=light --format=A4 --filename=livre
+```
+
+## 🎨 Caractéristiques du style livre
+
+### Marges
+
+```
+Pages droites (recto) │ Pages gauches (verso)
+ │
+ 20mm ──┐ │ ┌── 25mm
+ │ │ │
+ ┌───────┴──────┐ │ ┌──────┴───────┐
+ │ │ │ │ │
+ │ CONTENU │ │ │ CONTENU │
+ │ │ │ │ │
+ └──────────────┘ │ └──────────────┘
+ 25mm │ 20mm
+ (reliure) │ (reliure)
+```
+
+### Typographie
+
+- **Police** : Georgia, Palatino (serif)
+- **Taille** : 11pt
+- **Interlignage** : 1.6
+- **Alignement** : Justifié avec césure automatique
+- **Retrait** : 5mm pour les paragraphes suivants
+
+### Titres
+
+```css
+H2 (Chapitres) → 18pt, numérotés (1. 2. 3.)
+H3 (Sections) → 14pt, numérotés (1.1, 1.2)
+H4 (Sous-sections) → 12pt
+```
+
+### Compteurs automatiques
+
+- Chapitres : 1, 2, 3...
+- Sections : 1.1, 1.2, 2.1...
+- Figures : Figure 1.1, Figure 1.2...
+- Tableaux : idem
+
+## 📐 Configuration CSS
+
+Le fichier `_print-book.css` contient tous les styles. Vous pouvez personnaliser :
+
+### Changer les polices
+
+```css
+body {
+ font-family: "Baskerville", "Georgia", serif;
+ font-size: 12pt;
+}
+```
+
+### Ajuster les marges
+
+```css
+@page {
+ margin-top: 25mm;
+ margin-bottom: 30mm;
+}
+
+@page :left {
+ margin-left: 18mm;
+ margin-right: 30mm;
+}
+```
+
+### Personnaliser les headers
+
+```css
+@page :left {
+ @top-left {
+ content: string(chapter-title);
+ font-size: 10pt;
+ color: #333;
+ }
+}
+```
+
+### Forcer un saut de page
+
+Dans votre MDX :
+```mdx
+## Chapitre 1
+
+Contenu...
+
+---
+
+## Chapitre 2 (nouvelle page)
+```
+
+Ou avec CSS :
+```css
+.new-chapter {
+ break-before: page;
+}
+```
+
+## 🆚 Comparaison des versions
+
+| Fonctionnalité | Simple | Paged.js |
+|----------------|--------|----------|
+| **Stabilité** | ✅ Excellente | ⚠️ En cours |
+| **Vitesse** | ✅ Rapide | ⏱️ Plus lent |
+| **Setup** | ✅ Aucun | 📦 Paged.js requis |
+| **Marges reliure** | ✅ | ✅ |
+| **Running headers** | ⚠️ Limité | ✅ Avancé |
+| **Notes de bas de page** | ❌ | ✅ |
+| **Table matières auto** | ❌ | ✅ |
+| **Qualité typo** | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ |
+
+### Quand utiliser quelle version ?
+
+**Version Simple** (recommandée) :
+- ✅ Pour la plupart des cas d'usage
+- ✅ Stabilité prioritaire
+- ✅ Génération rapide
+- ✅ Résultats prévisibles
+
+**Version Paged.js** (expérimentale) :
+- 🔬 Pour tester les fonctionnalités avancées
+- 📚 Si vous avez besoin de notes de bas de page
+- 📖 Pour des tables des matières générées automatiquement
+- ⚠️ Nécessite plus de tests
+
+## 🐛 Dépannage
+
+### Le PDF est vide
+
+```bash
+# Reconstruire d'abord
+npm run build
+npm run export:pdf:book:simple
+```
+
+### Les images manquent
+
+Vérifiez que les chemins sont absolus :
+```html
+
+
+
+
+
+```
+
+### Les graphiques sont coupés
+
+Dans `_print-book.css`, ajoutez :
+```css
+.your-chart {
+ max-height: 200mm;
+ break-inside: avoid;
+}
+```
+
+### Port 8080 déjà utilisé
+
+```bash
+PREVIEW_PORT=8081 npm run export:pdf:book:simple
+```
+
+## 🎓 Prochaines étapes
+
+### Améliorations possibles
+
+1. **Finaliser Paged.js** pour les fonctionnalités avancées
+2. **Table des matières automatique** avec numéros de page
+3. **Index** généré automatiquement
+4. **Références croisées** (Voir Figure 2.3, etc.)
+5. **Templates prédéfinis** :
+ - Thèse académique
+ - Rapport technique
+ - Livre scientifique
+ - Documentation
+
+### Contribuer
+
+Les styles sont dans `_print-book.css`. Pour proposer des améliorations :
+
+1. Testez avec votre contenu
+2. Modifiez le CSS
+3. Générez le PDF
+4. Partagez vos modifications !
+
+## 📚 Ressources
+
+### CSS Paged Media
+
+- [W3C Spec](https://www.w3.org/TR/css-page-3/)
+- [CSS Tricks Guide](https://css-tricks.com/css-paged-media-guide/)
+- [Print CSS Documentation](https://www.smashingmagazine.com/2015/01/designing-for-print-with-css/)
+
+### Paged.js
+
+- [Documentation](https://pagedjs.org/documentation/)
+- [Exemples](https://pagedjs.org/examples/)
+- [W3C Paged Media](https://www.w3.org/TR/css-page-3/)
+
+### Typographie de livre
+
+- [Butterick's Practical Typography](https://practicaltypography.com/)
+- [The Elements of Typographic Style](http://webtypography.net/)
+
+## 💡 Cas d'usage
+
+Ce système est idéal pour :
+
+- 📘 **Thèses de doctorat**
+- 📗 **Mémoires de master**
+- 📕 **Rapports de recherche**
+- 📙 **Documentation technique**
+- 📓 **Livres blancs**
+- 📔 **Livres auto-publiés**
+- 📚 **Collections d'articles**
+
+## 🎉 Résultat
+
+Avec ce système, vous obtenez :
+
+✅ **PDF prêt pour l'impression**
+- Marges correctes pour reliure
+- Typographie professionnelle
+- Mise en page cohérente
+
+✅ **Qualité éditoriale**
+- Numérotation automatique
+- Gestion des veuves/orphelines
+- Césure propre
+
+✅ **Workflow moderne**
+- Écriture en MDX
+- Build automatisé
+- Un seul fichier source
+
+---
+
+**Créé avec ❤️ pour le Research Article Template**
+
+*Profitez de votre nouveau système d'export PDF livre !* 📚✨
+
diff --git a/content/articles/multi-harness-rl/app/scripts/export-latex.mjs b/content/articles/multi-harness-rl/app/scripts/export-latex.mjs
new file mode 100755
index 00000000..d4efc9d2
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/scripts/export-latex.mjs
@@ -0,0 +1,358 @@
+#!/usr/bin/env node
+import { spawn } from 'node:child_process';
+import { promises as fs } from 'node:fs';
+import { resolve, dirname, basename, extname } from 'node:path';
+import process from 'node:process';
+
+async function run(command, args = [], options = {}) {
+ return new Promise((resolvePromise, reject) => {
+ const child = spawn(command, args, { stdio: 'inherit', shell: false, ...options });
+ child.on('error', reject);
+ child.on('exit', (code) => {
+ if (code === 0) resolvePromise(undefined);
+ else reject(new Error(`${command} ${args.join(' ')} exited with code ${code}`));
+ });
+ });
+}
+
+function parseArgs(argv) {
+ const out = {};
+ for (const arg of argv.slice(2)) {
+ if (!arg.startsWith('--')) continue;
+ const [k, v] = arg.replace(/^--/, '').split('=');
+ out[k] = v === undefined ? true : v;
+ }
+ return out;
+}
+
+function slugify(text) {
+ return String(text || '')
+ .normalize('NFKD')
+ .replace(/\p{Diacritic}+/gu, '')
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ .slice(0, 120) || 'article';
+}
+
+async function checkPandocInstalled() {
+ try {
+ await run('pandoc', ['--version'], { stdio: 'pipe' });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+async function readMdxFile(filePath) {
+ try {
+ const content = await fs.readFile(filePath, 'utf-8');
+ return content;
+ } catch (error) {
+ console.warn(`Warning: Could not read ${filePath}:`, error.message);
+ return '';
+ }
+}
+
+function extractFrontmatter(content) {
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n/);
+ if (!frontmatterMatch) return { frontmatter: {}, content };
+
+ const frontmatterText = frontmatterMatch[1];
+ const contentWithoutFrontmatter = content.replace(frontmatterMatch[0], '');
+
+ // More robust YAML parsing that handles complex structures
+ const frontmatter = {};
+ const lines = frontmatterText.split('\n');
+ let currentKey = null;
+ let currentValue = '';
+ let inMultiLineValue = false;
+ let multiLineOperator = null; // '>' or '|'
+
+ for (const line of lines) {
+ // Check if this is a new key
+ if (line.match(/^[a-zA-Z_][a-zA-Z0-9_]*\s*:/) && !inMultiLineValue) {
+ // Save previous key if exists
+ if (currentKey) {
+ frontmatter[currentKey] = currentValue.trim();
+ }
+
+ const [key, ...valueParts] = line.split(':');
+ currentKey = key.trim();
+ currentValue = valueParts.join(':').trim();
+
+ // Check for multi-line operators
+ if (currentValue.endsWith('>') || currentValue.endsWith('|')) {
+ multiLineOperator = currentValue.slice(-1);
+ currentValue = currentValue.slice(0, -1).trim();
+ inMultiLineValue = true;
+ } else if (currentValue) {
+ inMultiLineValue = false;
+ } else {
+ inMultiLineValue = true;
+ }
+ } else if (currentKey && (inMultiLineValue || line.match(/^\s/))) {
+ // Continuation line or nested content
+ if (inMultiLineValue) {
+ if (line.trim() === '' && multiLineOperator === '>') {
+ // Empty line in folded style should become space
+ currentValue += ' ';
+ } else {
+ const lineContent = line.startsWith(' ') ? line : ' ' + line;
+ currentValue += lineContent;
+ }
+ } else {
+ currentValue += '\n' + line;
+ }
+ }
+ }
+
+ // Save the last key
+ if (currentKey) {
+ frontmatter[currentKey] = currentValue.trim();
+ }
+
+ return { frontmatter, content: contentWithoutFrontmatter };
+}
+
+function cleanMdxToMarkdown(content) {
+ // Remove import statements
+ content = content.replace(/^import .+?;?\s*$/gm, '');
+
+ // Remove JSX component calls like
+ content = content.replace(/<[A-Z][a-zA-Z0-9]*\s*\/>/g, '');
+
+ // Convert JSX components to simpler markdown
+ // Handle Sidenote components specially
+ content = content.replace(/([\s\S]*?)<\/Sidenote>/g, (match, innerContent) => {
+ // Extract main content and aside content
+ const asideMatch = innerContent.match(/([\s\S]*?)<\/Fragment>/);
+ const mainContent = innerContent.replace(/[\s\S]*?<\/Fragment>/, '').trim();
+ const asideContent = asideMatch ? asideMatch[1].trim() : '';
+
+ let result = mainContent;
+ if (asideContent) {
+ result += `\n\n> **Note:** ${asideContent}`;
+ }
+ return result;
+ });
+
+ // Handle Note components
+ content = content.replace(/]*>([\s\S]*?)<\/Note>/g, (match, innerContent) => {
+ return `\n> **Note:** ${innerContent.trim()}\n`;
+ });
+
+ // Handle Wide and FullWidth components
+ content = content.replace(/<(Wide|FullWidth)>([\s\S]*?)<\/\1>/g, '$2');
+
+ // Handle HtmlEmbed components (convert to simple text)
+ content = content.replace(/]*\/>/g, '*[Interactive content not available in LaTeX]*');
+
+ // Remove remaining JSX fragments
+ content = content.replace(/]*>([\s\S]*?)<\/Fragment>/g, '$1');
+ content = content.replace(/<[A-Z][a-zA-Z0-9]*[^>]*>([\s\S]*?)<\/[A-Z][a-zA-Z0-9]*>/g, '$1');
+
+ // Clean up className attributes
+ content = content.replace(/className="[^"]*"/g, '');
+
+ // Clean up extra whitespace
+ content = content.replace(/\n{3,}/g, '\n\n');
+
+ // Clean up characters that might cause YAML parsing issues
+ // Remove any potential YAML-style markers that might interfere
+ content = content.replace(/^---$/gm, '');
+ content = content.replace(/^\s*&\s+/gm, ''); // Remove YAML aliases
+
+ return content.trim();
+}
+
+async function processChapterImports(content, contentDir) {
+ let processedContent = content;
+
+ // First, extract all import statements and their corresponding component calls
+ const importPattern = /import\s+(\w+)\s+from\s+["']\.\/chapters\/([^"']+)["'];?/g;
+ const imports = new Map();
+ let match;
+
+ // Collect all imports
+ while ((match = importPattern.exec(content)) !== null) {
+ const [fullImport, componentName, chapterPath] = match;
+ imports.set(componentName, { path: chapterPath, importStatement: fullImport });
+ }
+
+ // Remove all import statements
+ processedContent = processedContent.replace(importPattern, '');
+
+ // Process each component call
+ for (const [componentName, { path: chapterPath }] of imports) {
+ const componentCallPattern = new RegExp(`<${componentName}\\s*\\/>`, 'g');
+
+ try {
+ const chapterFile = resolve(contentDir, 'chapters', chapterPath);
+ const chapterContent = await readMdxFile(chapterFile);
+ const { content: chapterMarkdown } = extractFrontmatter(chapterContent);
+ const cleanChapter = cleanMdxToMarkdown(chapterMarkdown);
+
+ processedContent = processedContent.replace(componentCallPattern, cleanChapter);
+ console.log(`✅ Processed chapter: ${chapterPath}`);
+ } catch (error) {
+ console.warn(`Warning: Could not process chapter ${chapterPath}:`, error.message);
+ processedContent = processedContent.replace(componentCallPattern, `\n*[Chapter ${chapterPath} could not be loaded]*\n`);
+ }
+ }
+
+ return processedContent;
+}
+
+function createLatexPreamble(frontmatter) {
+ const title = frontmatter.title ? frontmatter.title.replace(/\n/g, ' ') : 'Untitled Article';
+ const subtitle = frontmatter.subtitle || '';
+ const authors = frontmatter.authors || '';
+ const date = frontmatter.published || '';
+
+ return `\\documentclass[11pt,a4paper]{article}
+\\usepackage[utf8]{inputenc}
+\\usepackage[T1]{fontenc}
+\\usepackage{amsmath,amsfonts,amssymb}
+\\usepackage{graphicx}
+\\usepackage{hyperref}
+\\usepackage{booktabs}
+\\usepackage{longtable}
+\\usepackage{array}
+\\usepackage{multirow}
+\\usepackage{wrapfig}
+\\usepackage{float}
+\\usepackage{colortbl}
+\\usepackage{pdflscape}
+\\usepackage{tabu}
+\\usepackage{threeparttable}
+\\usepackage{threeparttablex}
+\\usepackage{ulem}
+\\usepackage{makecell}
+\\usepackage{xcolor}
+\\usepackage{listings}
+\\usepackage{fancyvrb}
+\\usepackage{geometry}
+\\geometry{margin=1in}
+
+\\title{${title}${subtitle ? `\\\\\\large ${subtitle}` : ''}}
+${authors ? `\\author{${authors}}` : ''}
+${date ? `\\date{${date}}` : ''}
+
+\\begin{document}
+\\maketitle
+\\tableofcontents
+\\newpage
+
+`;
+}
+
+async function main() {
+ const cwd = process.cwd();
+ const args = parseArgs(process.argv);
+
+ // Check if pandoc is installed
+ const hasPandoc = await checkPandocInstalled();
+ if (!hasPandoc) {
+ console.error('❌ Pandoc is not installed. Please install it first:');
+ console.error(' macOS: brew install pandoc');
+ console.error(' Ubuntu: apt-get install pandoc');
+ console.error(' Windows: choco install pandoc');
+ process.exit(1);
+ }
+
+ const contentDir = resolve(cwd, 'src/content');
+ const articleFile = resolve(contentDir, 'article.mdx');
+
+ // Check if article.mdx exists
+ try {
+ await fs.access(articleFile);
+ } catch {
+ console.error(`❌ Could not find article.mdx at ${articleFile}`);
+ process.exit(1);
+ }
+
+ console.log('> Reading article content...');
+ const articleContent = await readMdxFile(articleFile);
+ const { frontmatter, content } = extractFrontmatter(articleContent);
+
+ console.log('> Processing chapters...');
+ const processedContent = await processChapterImports(content, contentDir);
+
+ console.log('> Converting MDX to Markdown...');
+ const markdownContent = cleanMdxToMarkdown(processedContent);
+
+ // Generate output filename
+ const title = frontmatter.title ? frontmatter.title.replace(/\n/g, ' ') : 'article';
+ const outFileBase = args.filename ? String(args.filename).replace(/\.(tex|pdf)$/i, '') : slugify(title);
+
+ // Create temporary markdown file (ensure it's pure markdown without YAML frontmatter)
+ const tempMdFile = resolve(cwd, 'temp-article.md');
+
+ // Clean the markdown content to ensure no YAML frontmatter remains
+ let cleanMarkdown = markdownContent;
+ // Remove any potential YAML frontmatter that might have leaked through
+ cleanMarkdown = cleanMarkdown.replace(/^---\n[\s\S]*?\n---\n/, '');
+ // Remove any standalone YAML blocks that might cause issues
+ cleanMarkdown = cleanMarkdown.replace(/^---\n([\s\S]*?)\n---$/gm, '');
+
+ await fs.writeFile(tempMdFile, cleanMarkdown);
+
+
+ console.log('> Converting to LaTeX with Pandoc...');
+ const outputLatex = resolve(cwd, 'dist', `${outFileBase}.tex`);
+
+ // Ensure dist directory exists
+ await fs.mkdir(resolve(cwd, 'dist'), { recursive: true });
+
+ // Pandoc conversion arguments
+ const pandocArgs = [
+ tempMdFile,
+ '-o', outputLatex,
+ '--from=markdown-yaml_metadata_block', // Explicitly exclude YAML metadata parsing
+ '--to=latex',
+ '--standalone',
+ '--toc',
+ '--number-sections',
+ '--highlight-style=tango',
+ '--listings'
+ ];
+
+ // Add bibliography if it exists
+ const bibFile = resolve(contentDir, 'bibliography.bib');
+ try {
+ await fs.access(bibFile);
+ pandocArgs.push('--bibliography', bibFile);
+ pandocArgs.push('--citeproc');
+ console.log('✅ Found bibliography file, including citations');
+ } catch {
+ console.log('ℹ️ No bibliography file found');
+ }
+
+ try {
+ await run('pandoc', pandocArgs);
+ console.log(`✅ LaTeX generated: ${outputLatex}`);
+
+ // Optionally compile to PDF if requested
+ if (args.pdf) {
+ console.log('> Compiling LaTeX to PDF...');
+ const outputPdf = resolve(cwd, 'dist', `${outFileBase}.pdf`);
+ await run('pdflatex', ['-output-directory', resolve(cwd, 'dist'), outputLatex]);
+ console.log(`✅ PDF generated: ${outputPdf}`);
+ }
+
+ } catch (error) {
+ console.error('❌ Pandoc conversion failed:', error.message);
+ process.exit(1);
+ } finally {
+ // Clean up temporary file
+ try {
+ await fs.unlink(tempMdFile);
+ } catch { }
+ }
+}
+
+main().catch((err) => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/content/articles/multi-harness-rl/app/scripts/export-pdf-book-simple.mjs b/content/articles/multi-harness-rl/app/scripts/export-pdf-book-simple.mjs
new file mode 100755
index 00000000..ca80c40d
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/scripts/export-pdf-book-simple.mjs
@@ -0,0 +1,626 @@
+#!/usr/bin/env node
+/**
+ * Export PDF Book – Clean "A Book Apart" style
+ *
+ * Uses Playwright native print (no Paged.js) for maximum reliability.
+ * Features:
+ * - Auto-generated Table of Contents from H2/H3 headings
+ * - Page numbers "X / Y" via Playwright displayHeaderFooter
+ * - Banner/hero visualization preserved
+ * - Charts/embeds properly sized
+ * - Clean minimal typography
+ *
+ * Usage:
+ * npm run export:pdf:book:simple
+ * npm run export:pdf:book:simple -- --theme=light --format=A4
+ */
+
+import { spawn } from 'node:child_process';
+import { setTimeout as delay } from 'node:timers/promises';
+import { chromium } from 'playwright';
+import { resolve, dirname, basename, join } from 'node:path';
+import { promises as fs } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import process from 'node:process';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+// ============================================================================
+// Utilities
+// ============================================================================
+
+async function run(command, args = [], options = {}) {
+ return new Promise((ok, fail) => {
+ const child = spawn(command, args, { stdio: 'inherit', shell: false, ...options });
+ child.on('error', fail);
+ child.on('exit', (code) => {
+ if (code === 0) ok(); else fail(new Error(`${command} ${args.join(' ')} exited ${code}`));
+ });
+ });
+}
+
+async function waitForServer(url, timeoutMs = 60_000) {
+ const start = Date.now();
+ while (Date.now() - start < timeoutMs) {
+ try { const r = await fetch(url); if (r.ok) return; } catch {}
+ await delay(500);
+ }
+ throw new Error(`Server did not start within ${timeoutMs}ms: ${url}`);
+}
+
+function parseArgs(argv) {
+ const out = {};
+ for (const arg of argv.slice(2)) {
+ if (!arg.startsWith('--')) continue;
+ const [k, v] = arg.replace(/^--/, '').split('=');
+ out[k] = v === undefined ? true : v;
+ }
+ return out;
+}
+
+function slugify(text) {
+ return String(text || '')
+ .normalize('NFKD').replace(/\p{Diacritic}+/gu, '')
+ .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
+ .slice(0, 120) || 'article';
+}
+
+// ============================================================================
+// Content-readiness helpers
+// ============================================================================
+
+async function waitForImages(page, timeoutMs = 15_000) {
+ await page.evaluate(async (timeout) => {
+ const deadline = Date.now() + timeout;
+ const pending = Array.from(document.images || []).filter(i => !i.complete || i.naturalWidth === 0);
+ await Promise.race([
+ Promise.all(pending.map(i => new Promise(r => {
+ if (i.complete && i.naturalWidth !== 0) return r();
+ i.addEventListener('load', r, { once: true });
+ i.addEventListener('error', r, { once: true });
+ }))),
+ new Promise(r => setTimeout(r, Math.max(0, deadline - Date.now())))
+ ]);
+ }, timeoutMs);
+}
+
+async function waitForD3(page, timeoutMs = 20_000) {
+ try {
+ await page.evaluate(async (timeout) => {
+ const start = Date.now();
+ const ready = () => {
+ const hero = document.querySelector('.hero-banner');
+ if (hero) return !!hero.querySelector('svg circle, svg path, svg rect, svg g');
+ const c = [...document.querySelectorAll('.d3-line'), ...document.querySelectorAll('.d3-bar')];
+ return !c.length || c.every(el => el.querySelector('svg circle, svg path, svg rect, svg g'));
+ };
+ while (!ready() && Date.now() - start < timeout) await new Promise(r => setTimeout(r, 200));
+ }, timeoutMs);
+ } catch {}
+}
+
+async function waitForPlotly(page, timeoutMs = 20_000) {
+ try {
+ await page.evaluate(async (timeout) => {
+ const start = Date.now();
+ while (!document.querySelectorAll('.js-plotly-plot').length && Date.now() - start < timeout)
+ await new Promise(r => setTimeout(r, 200));
+ const ok = () => Array.from(document.querySelectorAll('.js-plotly-plot')).every(e => e.querySelector('svg.main-svg'));
+ while (!ok() && Date.now() - start < timeout) await new Promise(r => setTimeout(r, 200));
+ }, timeoutMs);
+ } catch {}
+}
+
+async function waitForHtmlEmbeds(page, timeoutMs = 15_000) {
+ await page.evaluate(async (timeout) => {
+ const start = Date.now();
+ const ready = (e) => {
+ try {
+ if (!e.querySelector('svg, canvas, div[id^="frag-"]')) return false;
+ for (const s of e.querySelectorAll('svg'))
+ if (!s.querySelector('path, circle, rect, line, polygon, g')) return false;
+ return true;
+ } catch { return false; }
+ };
+ while (Date.now() - start < timeout) {
+ const embeds = Array.from(document.querySelectorAll('.html-embed__card'));
+ if (!embeds.length || embeds.every(ready)) break;
+ await new Promise(r => setTimeout(r, 300));
+ }
+ }, timeoutMs);
+}
+
+async function waitForStableLayout(page, timeoutMs = 5_000) {
+ const start = Date.now();
+ let last = await page.evaluate(() => (document.scrollingElement || document.body).scrollHeight);
+ let stable = 0;
+ while (Date.now() - start < timeoutMs && stable < 3) {
+ await page.waitForTimeout(250);
+ const now = await page.evaluate(() => (document.scrollingElement || document.body).scrollHeight);
+ if (now === last) stable++; else { stable = 0; last = now; }
+ }
+}
+
+async function injectSvgViewBoxes(page) {
+ return page.evaluate(() => {
+ let fixed = 0, skipped = 0, errors = 0;
+ document.querySelectorAll('.html-embed__card svg, [class^="d3-"] svg, [class*=" d3-"] svg').forEach(svg => {
+ try {
+ if (svg.getAttribute('viewBox')) { skipped++; return; }
+ const r = svg.getBoundingClientRect();
+ const w = r.width || parseFloat(svg.getAttribute('width')) || 0;
+ const h = r.height || parseFloat(svg.getAttribute('height')) || 0;
+ if (w > 0 && h > 0) {
+ svg.setAttribute('viewBox', `0 0 ${Math.round(w)} ${Math.round(h)}`);
+ svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
+ svg.removeAttribute('width'); svg.removeAttribute('height');
+ svg.style.width = '100%'; svg.style.height = 'auto'; svg.style.maxWidth = '100%';
+ fixed++;
+ } else skipped++;
+ } catch { errors++; }
+ });
+ return { fixed, skipped, errors };
+ });
+}
+
+async function openAllAccordions(page) {
+ const count = await page.evaluate(() => {
+ let opened = 0;
+ document.querySelectorAll('details.accordion, details').forEach(d => {
+ if (!d.hasAttribute('open')) {
+ d.setAttribute('open', '');
+ const w = d.querySelector('.accordion__content-wrapper');
+ if (w) { w.style.height = 'auto'; w.style.overflow = 'visible'; }
+ opened++;
+ }
+ });
+ return opened;
+ });
+ if (count > 0) await waitForStableLayout(page, 2_000);
+ return count;
+}
+
+// ============================================================================
+// Table of Contents generation
+// ============================================================================
+
+async function injectTableOfContents(page) {
+ return page.evaluate(() => {
+ const headings = Array.from(document.querySelectorAll('main h2, main h3'));
+ if (headings.length === 0) return 0;
+
+ let chapterNum = 0;
+ let sectionNum = 0;
+ let tocHtml = '';
+ tocHtml += 'Contents ';
+ tocHtml += '';
+
+ let currentChapterHtml = '';
+ let sectionsHtml = '';
+
+ const flushChapter = () => {
+ if (currentChapterHtml) {
+ if (sectionsHtml) {
+ currentChapterHtml += `${sectionsHtml} `;
+ sectionsHtml = '';
+ }
+ tocHtml += currentChapterHtml + '';
+ currentChapterHtml = '';
+ }
+ };
+
+ for (const h of headings) {
+ const text = h.textContent.trim();
+ if (!text) continue;
+
+ if (h.tagName === 'H2') {
+ flushChapter();
+ chapterNum++;
+ sectionNum = 0;
+ currentChapterHtml = ``;
+ currentChapterHtml += `${chapterNum} `;
+ currentChapterHtml += `${text} `;
+ } else if (h.tagName === 'H3' && chapterNum > 0) {
+ sectionNum++;
+ sectionsHtml += ` `;
+ sectionsHtml += `${chapterNum}.${sectionNum} `;
+ sectionsHtml += `${text} `;
+ sectionsHtml += ` `;
+ }
+ }
+
+ flushChapter();
+ tocHtml += ' ';
+
+ // Insert after meta header, before main content
+ const meta = document.querySelector('header.meta');
+ const main = document.querySelector('main');
+ const insertTarget = meta || (main ? main.parentNode : document.body);
+
+ if (meta && meta.nextSibling) {
+ meta.parentNode.insertBefore(
+ Object.assign(document.createElement('div'), { innerHTML: tocHtml }).firstChild,
+ meta.nextSibling
+ );
+ } else if (main) {
+ main.parentNode.insertBefore(
+ Object.assign(document.createElement('div'), { innerHTML: tocHtml }).firstChild,
+ main
+ );
+ }
+
+ return chapterNum;
+ });
+}
+
+// ============================================================================
+// Main
+// ============================================================================
+
+async function main() {
+ const cwd = process.cwd();
+ const port = Number(process.env.PREVIEW_PORT || 8080);
+ const baseUrl = `http://127.0.0.1:${port}/`;
+ const args = parseArgs(process.argv);
+
+ const theme = (args.theme === 'dark' || args.theme === 'light') ? args.theme : 'light';
+ const format = args.format || 'A4';
+ const wait = args.wait || 'full';
+
+ let outFileBase = (args.filename && String(args.filename).replace(/\.pdf$/i, '')) || '';
+
+ // Build if needed
+ const distDir = resolve(cwd, 'dist');
+ let hasDist = false;
+ try { const st = await fs.stat(distDir); hasDist = st?.isDirectory(); } catch {}
+ if (!hasDist) {
+ console.log('📦 Building Astro site…');
+ await run('npm', ['run', 'build']);
+ } else {
+ console.log('✓ dist/ exists, skipping build');
+ }
+
+ // Start preview
+ console.log('🚀 Starting preview server…');
+ const preview = spawn('npm', ['run', 'preview'], { cwd, stdio: 'inherit', detached: true });
+ const previewExit = new Promise(r => preview.on('close', (code, signal) => r({ code, signal })));
+
+ try {
+ await waitForServer(baseUrl, 60_000);
+ console.log('✓ Server ready');
+
+ const browser = await chromium.launch({ headless: true });
+ try {
+ const context = await browser.newContext();
+ await context.addInitScript((desired) => {
+ try {
+ localStorage.setItem('theme', desired);
+ if (document?.documentElement) document.documentElement.dataset.theme = desired;
+ } catch {}
+ }, theme);
+
+ const page = await context.newPage();
+ await page.setViewportSize({ width: 1200, height: 1600 });
+
+ console.log('📄 Loading page…');
+ await page.goto(baseUrl, { waitUntil: 'load', timeout: 60_000 });
+ try { await page.waitForFunction(() => !!window.d3, { timeout: 8_000 }); } catch {}
+ try { await page.waitForFunction(() => !!window.Plotly, { timeout: 5_000 }); } catch {}
+
+ if (!outFileBase) {
+ const fromBtn = await page.evaluate(() => {
+ const btn = document.getElementById('download-pdf-btn');
+ return btn?.getAttribute('data-pdf-filename') || '';
+ });
+ if (fromBtn) {
+ outFileBase = String(fromBtn).replace(/\.pdf$/i, '') + '-book';
+ } else {
+ const title = await page.evaluate(() => {
+ const h1 = document.querySelector('h1.hero-title');
+ return (h1?.textContent || document.title || '').replace(/\s+/g, ' ').trim();
+ });
+ outFileBase = slugify(title) + '-book';
+ }
+ }
+
+ // Scroll entire page to trigger lazy-loaded content (IntersectionObserver)
+ console.log('📜 Scrolling page to trigger lazy-loaded content…');
+ await page.evaluate(async () => {
+ const step = window.innerHeight * 0.8;
+ const max = document.body.scrollHeight;
+ for (let y = 0; y < max; y += step) {
+ window.scrollTo(0, y);
+ await new Promise(r => setTimeout(r, 80));
+ }
+ window.scrollTo(0, 0);
+ });
+ await page.waitForTimeout(3000);
+
+ // Wait for all content
+ if (wait === 'images' || wait === 'full') { console.log('⏳ Images…'); await waitForImages(page); }
+ if (wait === 'd3' || wait === 'full') { console.log('⏳ D3…'); await waitForD3(page); }
+ if (wait === 'plotly' || wait === 'full') { console.log('⏳ Plotly…'); await waitForPlotly(page); }
+ if (wait === 'full') {
+ console.log('⏳ HTML embeds…');
+ await waitForHtmlEmbeds(page);
+ await waitForStableLayout(page);
+ }
+
+ // Prepare content
+ console.log('📂 Opening accordions…');
+ const acc = await openAllAccordions(page);
+ console.log(` ${acc} accordion(s) opened`);
+
+ console.log('🔧 Fixing SVG viewBoxes…');
+ const vb = await injectSvgViewBoxes(page);
+ console.log(` Fixed: ${vb.fixed}, Skipped: ${vb.skipped}`);
+
+ // Replace embeds with pre-captured screenshots from screenshot-elements.mjs
+ const screenshotsDir = resolve(cwd, 'screenshots');
+ let hasScreenshots = false;
+ try { const st = await fs.stat(screenshotsDir); hasScreenshots = st?.isDirectory(); } catch {}
+
+ if (hasScreenshots) {
+ console.log('📸 Loading pre-captured screenshots…');
+ const pngFiles = (await fs.readdir(screenshotsDir)).filter(f => f.endsWith('.png'));
+ const embedScreenshots = new Map();
+ const MIN_SCREENSHOT_BYTES = 10_000; // Skip tiny screenshots (likely just UI controls)
+ for (const f of pngFiles) {
+ if (/--option-|--combo-|--btn-|--all-options|--open-select|--cb-/.test(f)) continue;
+ const match = f.match(/^(\d+)-embed--(.+)\.png$/);
+ if (match) {
+ const filePath = join(screenshotsDir, f);
+ const stat = await fs.stat(filePath);
+ if (stat.size < MIN_SCREENSHOT_BYTES) {
+ console.log(` ⏭️ Skipping tiny screenshot: ${f} (${stat.size} bytes)`);
+ continue;
+ }
+ embedScreenshots.set(match[2], filePath);
+ }
+ }
+ console.log(` ${embedScreenshots.size} embed screenshot(s) available`);
+
+ const replaceStats = await page.evaluate(async (screenshotEntries) => {
+ let replaced = 0, hidden = 0;
+ const embeds = document.querySelectorAll('.html-embed');
+
+ for (const embed of embeds) {
+ const card = embed.querySelector('.html-embed__card');
+ if (!card) continue;
+
+ const titleEl = embed.querySelector('.html-embed__title');
+ const titleText = titleEl?.textContent || '';
+ const btn = embed.querySelector('.html-embed__download');
+ const filename = btn?.getAttribute('data-filename') || '';
+ const slugify = (t) => String(t || '').trim().toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
+
+ // Try title first (screenshot-elements.mjs uses title), then data-filename
+ const slugFromTitle = slugify(titleText);
+ const slugFromFile = slugify(filename);
+ const entry = screenshotEntries.find(([key]) => key === slugFromTitle || key === slugFromFile);
+
+ if (entry) {
+ const [, b64] = entry;
+ card.innerHTML = ` `;
+ card.style.padding = '0';
+ card.style.border = 'none';
+ card.style.background = 'transparent';
+ card.style.borderRadius = '0';
+ replaced++;
+ } else {
+ // No screenshot — hide unless it's inside the hero/banner
+ const inHero = embed.closest('.hero, .hero-banner');
+ if (inHero) {
+ // Keep hero/banner embeds even without screenshot
+ } else {
+ embed.style.display = 'none';
+ hidden++;
+ }
+ }
+ }
+ return { replaced, hidden };
+ }, await Promise.all(
+ Array.from(embedScreenshots.entries()).map(async ([slug, filePath]) => {
+ const buf = await fs.readFile(filePath);
+ return [slug, buf.toString('base64')];
+ })
+ ));
+ console.log(` ${replaceStats.replaced} embed(s) replaced with screenshots, ${replaceStats.hidden} empty embed(s) hidden`);
+ } else {
+ console.log('⚠️ No screenshots/ folder found. Run "npm run export:images" first for best results.');
+ console.log(' Falling back to live rendering…');
+ }
+
+ // Force all iframes and wide embeds to fit; hide broken images
+ await page.evaluate(() => {
+ document.querySelectorAll('iframe').forEach(el => el.style.display = 'none');
+ document.querySelectorAll('.html-embed--wide').forEach(el => {
+ el.style.width = '100%';
+ el.style.marginLeft = '0';
+ el.style.transform = 'none';
+ el.style.padding = '0';
+ });
+ // Hide images that failed to load (gray placeholders)
+ document.querySelectorAll('img').forEach(img => {
+ if (!img.complete || img.naturalWidth === 0) {
+ const wrapper = img.closest('.image-wrapper, figure');
+ if (wrapper) wrapper.style.display = 'none';
+ else img.style.display = 'none';
+ }
+ });
+ });
+
+ // Number all figures sequentially
+ console.log('🔢 Numbering figures…');
+ const figureStats = await page.evaluate(() => {
+ const mainEl = document.querySelector('main');
+ if (!mainEl) return { figures: 0 };
+
+ let figNum = 0;
+ const figures = mainEl.querySelectorAll('figure');
+
+ for (const fig of figures) {
+ if (fig.closest('.hero') || fig.closest('header.meta') || fig.closest('.book-toc')) continue;
+ if (fig.style.display === 'none' || fig.closest('[style*="display: none"]')) continue;
+
+ // Skip nested figures (only process the deepest figure)
+ if (fig.querySelector('figure')) continue;
+
+ const desc = fig.querySelector('.html-embed__desc') || fig.querySelector('figcaption:last-of-type');
+ const titleCap = fig.querySelector('.html-embed__title');
+
+ const hasVisual = fig.querySelector('img, svg, canvas, .html-embed__card');
+ if (!hasVisual && !desc) continue;
+
+ figNum++;
+
+ if (desc && desc !== titleCap) {
+ let text = desc.textContent || '';
+ const figMatch = text.match(/^\s*Figure\s+\d+(\.\d+)?\s*/i);
+ if (figMatch) {
+ const afterFig = text.slice(figMatch[0].length);
+ const cleaned = afterFig.replace(/^[^a-zA-Z0-9(]*/, '');
+ desc.textContent = '';
+ desc.innerHTML = `Figure ${figNum} \u00B7 ${cleaned}`;
+ } else {
+ const html = desc.innerHTML;
+ desc.innerHTML = `Figure ${figNum} \u00B7 ${html}`;
+ }
+ }
+ }
+
+ return { figures: figNum };
+ });
+ console.log(` ${figureStats.figures} figure(s) numbered`);
+
+ // Inject chapter/section numbers into headings (CSS counters unreliable in Chromium print)
+ console.log('🔢 Numbering chapters and sections…');
+ const numberingStats = await page.evaluate(() => {
+ const mainEl = document.querySelector('main');
+ if (!mainEl) return { chapters: 0, sections: 0 };
+
+ let chapterNum = 0;
+ let sectionNum = 0;
+ let chapters = 0;
+ let sections = 0;
+
+ // Get all headings inside main (skip hero, meta, etc.)
+ const headings = mainEl.querySelectorAll('h2, h3');
+
+ for (const h of headings) {
+ // Skip headings inside the abstract or that are part of special sections
+ if (h.closest('.hero') || h.closest('header.meta') || h.closest('.book-toc')) continue;
+
+ if (h.tagName === 'H2') {
+ chapterNum++;
+ sectionNum = 0;
+ h.setAttribute('data-chapter-num', String(chapterNum));
+ chapters++;
+ } else if (h.tagName === 'H3') {
+ sectionNum++;
+ h.setAttribute('data-section-num', `${chapterNum}.${sectionNum}`);
+ sections++;
+ }
+ }
+
+ return { chapters, sections };
+ });
+ console.log(` ${numberingStats.chapters} chapter(s), ${numberingStats.sections} section(s) numbered`);
+
+ // Wrap chapter headings in dedicated full-page openers
+ console.log('📖 Creating chapter opening pages…');
+ const chapterPages = await page.evaluate(() => {
+ const mainEl = document.querySelector('main');
+ if (!mainEl) return 0;
+
+ let count = 0;
+ const h2s = Array.from(mainEl.querySelectorAll('h2[data-chapter-num]'));
+
+ for (const h2 of h2s) {
+ const wrapper = document.createElement('div');
+ wrapper.className = 'book-chapter-opener';
+ h2.parentNode.insertBefore(wrapper, h2);
+ wrapper.appendChild(h2);
+ count++;
+ }
+
+ return count;
+ });
+ console.log(` ${chapterPages} chapter opener(s) created`);
+
+ // Generate Table of Contents
+ console.log('📋 Generating Table of Contents…');
+ const tocChapters = await injectTableOfContents(page);
+ console.log(` ${tocChapters} chapter(s) in ToC`);
+
+ // Activate print mode and inject book CSS
+ await page.emulateMedia({ media: 'print' });
+
+ console.log('📚 Applying book styles…');
+ const bookCssPath = resolve(__dirname, '..', 'src', 'styles', '_print-book.css');
+ const bookCss = await fs.readFile(bookCssPath, 'utf-8');
+ await page.addStyleTag({ content: bookCss });
+ await page.waitForTimeout(1_000);
+
+ // Header & footer templates for page numbers
+ const footerTemplate = `
+
+ /
+
+ `;
+
+ const headerTemplate = `
`;
+
+ // Generate PDF
+ const outPath = resolve(cwd, 'dist', `${outFileBase}.pdf`);
+ console.log('🖨️ Generating PDF…');
+
+ await page.pdf({
+ path: outPath,
+ format,
+ printBackground: true,
+ displayHeaderFooter: true,
+ headerTemplate,
+ footerTemplate,
+ preferCSSPageSize: false,
+ margin: { top: '22mm', right: '22mm', bottom: '28mm', left: '22mm' },
+ });
+
+ const stats = await fs.stat(outPath);
+ const sizeKB = Math.round(stats.size / 1024);
+ console.log(`✅ PDF generated: ${outPath} (${sizeKB} KB)`);
+
+ if (sizeKB < 10) console.warn('⚠️ PDF very small – content might be missing');
+
+ const publicPath = resolve(cwd, 'public', `${outFileBase}.pdf`);
+ try {
+ await fs.mkdir(resolve(cwd, 'public'), { recursive: true });
+ await fs.copyFile(outPath, publicPath);
+ console.log(`✅ Copied to: ${publicPath}`);
+ } catch (e) { console.warn('⚠️ Copy failed:', e?.message); }
+
+ } finally {
+ await browser.close();
+ }
+ } finally {
+ console.log('🛑 Stopping preview server…');
+ try {
+ if (process.platform !== 'win32') { try { process.kill(-preview.pid, 'SIGINT'); } catch {} }
+ try { preview.kill('SIGINT'); } catch {}
+ await Promise.race([previewExit, delay(3_000)]);
+ if (!preview.killed) {
+ if (process.platform !== 'win32') { try { process.kill(-preview.pid, 'SIGKILL'); } catch {} }
+ try { preview.kill('SIGKILL'); } catch {}
+ await Promise.race([previewExit, delay(1_000)]);
+ }
+ } catch {}
+ }
+
+ console.log('');
+ console.log('╔═══════════════════════════════════════════════════════════════╗');
+ console.log('║ 📚 BOOK PDF GENERATED 📚 ║');
+ console.log('╚═══════════════════════════════════════════════════════════════╝');
+ console.log('');
+}
+
+main().catch(err => { console.error('❌', err); process.exit(1); });
diff --git a/content/articles/multi-harness-rl/app/scripts/export-pdf-book.mjs b/content/articles/multi-harness-rl/app/scripts/export-pdf-book.mjs
new file mode 100755
index 00000000..aa0499de
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/scripts/export-pdf-book.mjs
@@ -0,0 +1,612 @@
+#!/usr/bin/env node
+/**
+ * Export PDF Book with Paged.js
+ *
+ * Generates a professional book-quality PDF using Playwright + Paged.js polyfill.
+ * Paged.js implements W3C CSS Paged Media specs that browsers don't support natively:
+ * - @page :left / :right (alternating margins for binding)
+ * - Running headers via string-set
+ * - Footnotes via float: footnote
+ * - Named pages (@page chapter)
+ * - target-counter() for ToC page numbers
+ * - bookmark-level for PDF outline
+ *
+ * Usage:
+ * npm run export:pdf:book
+ * npm run export:pdf:book -- --theme=light --format=A4
+ *
+ * Options:
+ * --theme=light|dark Color theme (default: light)
+ * --format=A4|Letter Page format (default: A4)
+ * --filename=xxx Output filename (default: -book)
+ * --wait=full Wait strategy (default: full)
+ * --debug Keep browser open for inspection
+ */
+
+import { spawn } from 'node:child_process';
+import { setTimeout as delay } from 'node:timers/promises';
+import { chromium } from 'playwright';
+import { resolve, dirname } from 'node:path';
+import { promises as fs } from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import process from 'node:process';
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+
+// ============================================================================
+// Utilities
+// ============================================================================
+
+async function run(command, args = [], options = {}) {
+ return new Promise((ok, fail) => {
+ const child = spawn(command, args, { stdio: 'inherit', shell: false, ...options });
+ child.on('error', fail);
+ child.on('exit', (code) => {
+ if (code === 0) ok(); else fail(new Error(`${command} ${args.join(' ')} exited ${code}`));
+ });
+ });
+}
+
+async function waitForServer(url, timeoutMs = 60_000) {
+ const start = Date.now();
+ while (Date.now() - start < timeoutMs) {
+ try { const r = await fetch(url); if (r.ok) return; } catch {}
+ await delay(500);
+ }
+ throw new Error(`Server did not start within ${timeoutMs}ms: ${url}`);
+}
+
+function parseArgs(argv) {
+ const out = {};
+ for (const arg of argv.slice(2)) {
+ if (!arg.startsWith('--')) continue;
+ const [k, v] = arg.replace(/^--/, '').split('=');
+ out[k] = v === undefined ? true : v;
+ }
+ return out;
+}
+
+function slugify(text) {
+ return String(text || '')
+ .normalize('NFKD').replace(/\p{Diacritic}+/gu, '')
+ .toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '')
+ .slice(0, 120) || 'article';
+}
+
+// ============================================================================
+// Content-readiness helpers (ported from export-pdf.mjs)
+// ============================================================================
+
+async function waitForImages(page, timeoutMs = 15_000) {
+ await page.evaluate(async (timeout) => {
+ const deadline = Date.now() + timeout;
+ const imgs = Array.from(document.images || []);
+ const pending = imgs.filter(i => !i.complete || i.naturalWidth === 0);
+ await Promise.race([
+ Promise.all(pending.map(i => new Promise(r => {
+ if (i.complete && i.naturalWidth !== 0) return r();
+ i.addEventListener('load', r, { once: true });
+ i.addEventListener('error', r, { once: true });
+ }))),
+ new Promise(r => setTimeout(r, Math.max(0, deadline - Date.now())))
+ ]);
+ }, timeoutMs);
+}
+
+async function waitForD3(page, timeoutMs = 20_000) {
+ try {
+ await page.evaluate(async (timeout) => {
+ const start = Date.now();
+ const ready = () => {
+ const hero = document.querySelector('.hero-banner');
+ if (hero) return !!hero.querySelector('svg circle, svg path, svg rect, svg g');
+ const containers = [
+ ...document.querySelectorAll('.d3-line'),
+ ...document.querySelectorAll('.d3-bar'),
+ ...document.querySelectorAll('[class^="d3-"]'),
+ ];
+ return !containers.length || containers.every(c => c.querySelector('svg circle, svg path, svg rect, svg g'));
+ };
+ while (!ready() && Date.now() - start < timeout) await new Promise(r => setTimeout(r, 200));
+ }, timeoutMs);
+ } catch {}
+}
+
+async function waitForPlotly(page, timeoutMs = 20_000) {
+ try {
+ await page.evaluate(async (timeout) => {
+ const start = Date.now();
+ const has = () => document.querySelectorAll('.js-plotly-plot').length > 0;
+ while (!has() && Date.now() - start < timeout) await new Promise(r => setTimeout(r, 200));
+ const ok = () => Array.from(document.querySelectorAll('.js-plotly-plot')).every(e => e.querySelector('svg.main-svg'));
+ while (!ok() && Date.now() - start < timeout) await new Promise(r => setTimeout(r, 200));
+ }, timeoutMs);
+ } catch {}
+}
+
+async function waitForHtmlEmbeds(page, timeoutMs = 15_000) {
+ await page.evaluate(async (timeout) => {
+ const start = Date.now();
+ const isReady = (embed) => {
+ try {
+ const has = embed.querySelector('svg, canvas, div[id^="frag-"]');
+ if (!has) return false;
+ for (const svg of embed.querySelectorAll('svg')) {
+ if (!svg.querySelector('path, circle, rect, line, polygon, g')) return false;
+ }
+ return true;
+ } catch { return false; }
+ };
+ while (Date.now() - start < timeout) {
+ const embeds = Array.from(document.querySelectorAll('.html-embed__card'));
+ if (!embeds.length || embeds.every(isReady)) break;
+ await new Promise(r => setTimeout(r, 300));
+ }
+ }, timeoutMs);
+}
+
+async function waitForStableLayout(page, timeoutMs = 5_000) {
+ const start = Date.now();
+ let last = await page.evaluate(() =>
+ (document.scrollingElement || document.body).scrollHeight
+ );
+ let stable = 0;
+ while (Date.now() - start < timeoutMs && stable < 3) {
+ await page.waitForTimeout(250);
+ const now = await page.evaluate(() =>
+ (document.scrollingElement || document.body).scrollHeight
+ );
+ if (now === last) stable++; else { stable = 0; last = now; }
+ }
+}
+
+// ============================================================================
+// SVG viewBox injection (ensures SVGs scale properly after Paged.js reflows)
+// ============================================================================
+
+async function injectSvgViewBoxes(page) {
+ return page.evaluate(() => {
+ let fixed = 0, skipped = 0, errors = 0;
+ const sel = [
+ '.html-embed__card svg', '[class^="d3-"] svg', '[class*=" d3-"] svg',
+ ].join(', ');
+ document.querySelectorAll(sel).forEach(svg => {
+ try {
+ if (svg.getAttribute('viewBox')) { skipped++; return; }
+ const r = svg.getBoundingClientRect();
+ const w = r.width || svg.clientWidth || parseFloat(svg.getAttribute('width')) || 0;
+ const h = r.height || svg.clientHeight || parseFloat(svg.getAttribute('height')) || 0;
+ if (w > 0 && h > 0) {
+ svg.setAttribute('viewBox', `0 0 ${Math.round(w)} ${Math.round(h)}`);
+ svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
+ svg.removeAttribute('width');
+ svg.removeAttribute('height');
+ svg.style.width = '100%';
+ svg.style.height = 'auto';
+ svg.style.maxWidth = '100%';
+ fixed++;
+ } else { skipped++; }
+ } catch { errors++; }
+ });
+ return { fixed, skipped, errors };
+ });
+}
+
+// ============================================================================
+// Open all accordions so their content is visible in the book
+// ============================================================================
+
+async function openAllAccordions(page) {
+ const count = await page.evaluate(() => {
+ let opened = 0;
+ document.querySelectorAll('details.accordion, details').forEach(d => {
+ if (!d.hasAttribute('open')) {
+ d.setAttribute('open', '');
+ const w = d.querySelector('.accordion__content-wrapper');
+ if (w) { w.style.height = 'auto'; w.style.overflow = 'visible'; }
+ opened++;
+ }
+ });
+ return opened;
+ });
+ if (count > 0) await waitForStableLayout(page, 2_000);
+ return count;
+}
+
+// ============================================================================
+// Main
+// ============================================================================
+
+async function main() {
+ const cwd = process.cwd();
+ const port = Number(process.env.PREVIEW_PORT || 8080);
+ const baseUrl = `http://127.0.0.1:${port}/`;
+ const args = parseArgs(process.argv);
+
+ const theme = (args.theme === 'dark' || args.theme === 'light') ? args.theme : 'light';
+ const format = args.format || 'A4';
+ const wait = args.wait || 'full';
+ const debug = !!args.debug;
+
+ let outFileBase = (args.filename && String(args.filename).replace(/\.pdf$/i, '')) || '';
+
+ // -- Build if needed -------------------------------------------------------
+ const distDir = resolve(cwd, 'dist');
+ let hasDist = false;
+ try { const st = await fs.stat(distDir); hasDist = st?.isDirectory(); } catch {}
+ if (!hasDist) {
+ console.log('📦 Building Astro site…');
+ await run('npm', ['run', 'build']);
+ } else {
+ console.log('✓ dist/ exists, skipping build');
+ }
+
+ // -- Start preview server --------------------------------------------------
+ console.log('🚀 Starting preview server…');
+ const preview = spawn('npm', ['run', 'preview'], { cwd, stdio: 'inherit', detached: true });
+ const previewExit = new Promise(r => preview.on('close', (code, signal) => r({ code, signal })));
+
+ try {
+ await waitForServer(baseUrl, 60_000);
+ console.log('✓ Server ready');
+
+ // -- Launch browser ------------------------------------------------------
+ const browser = await chromium.launch({ headless: !debug });
+ try {
+ const context = await browser.newContext();
+ await context.addInitScript((desired) => {
+ try {
+ localStorage.setItem('theme', desired);
+ if (document?.documentElement) document.documentElement.dataset.theme = desired;
+ } catch {}
+ }, theme);
+
+ const page = await context.newPage();
+ await page.setViewportSize({ width: 1200, height: 1600 });
+
+ // -- Load page and wait for content ------------------------------------
+ console.log('📄 Loading page…');
+ await page.goto(baseUrl, { waitUntil: 'load', timeout: 60_000 });
+
+ try { await page.waitForFunction(() => !!window.d3, { timeout: 8_000 }); } catch {}
+ try { await page.waitForFunction(() => !!window.Plotly, { timeout: 5_000 }); } catch {}
+
+ // Derive filename from page if not provided
+ if (!outFileBase) {
+ const fromBtn = await page.evaluate(() => {
+ const btn = document.getElementById('download-pdf-btn');
+ return btn?.getAttribute('data-pdf-filename') || '';
+ });
+ if (fromBtn) {
+ outFileBase = String(fromBtn).replace(/\.pdf$/i, '') + '-book';
+ } else {
+ const title = await page.evaluate(() => {
+ const h1 = document.querySelector('h1.hero-title');
+ return (h1?.textContent || document.title || '').replace(/\s+/g, ' ').trim();
+ });
+ outFileBase = slugify(title) + '-book';
+ }
+ }
+
+ if (wait === 'images' || wait === 'full') {
+ console.log('⏳ Waiting for images…');
+ await waitForImages(page);
+ }
+ if (wait === 'd3' || wait === 'full') {
+ console.log('⏳ Waiting for D3…');
+ await waitForD3(page);
+ }
+ if (wait === 'plotly' || wait === 'full') {
+ console.log('⏳ Waiting for Plotly…');
+ await waitForPlotly(page);
+ }
+ if (wait === 'full') {
+ console.log('⏳ Waiting for HTML embeds…');
+ await waitForHtmlEmbeds(page);
+ await waitForStableLayout(page);
+ }
+
+ // -- Prepare content for book ------------------------------------------
+ console.log('📂 Opening accordions…');
+ const accordionCount = await openAllAccordions(page);
+ console.log(` ${accordionCount} accordion(s) opened`);
+
+ console.log('🔧 Fixing SVG viewBox attributes…');
+ const vb = await injectSvgViewBoxes(page);
+ console.log(` Fixed: ${vb.fixed}, Skipped: ${vb.skipped}, Errors: ${vb.errors}`);
+
+ // Hide web-only UI elements before Paged.js processes the DOM
+ await page.evaluate(() => {
+ const hide = [
+ '#theme-toggle', '.table-of-contents', '.toc-mobile-toggle',
+ '.toc-mobile-backdrop', '.toc-mobile-sidebar', '.right-aside',
+ 'nav', '.code-lang-chip', '.meta-container-cell--pdf', 'button',
+ '.interactive-only',
+ ];
+ hide.forEach(sel => {
+ document.querySelectorAll(sel).forEach(el => { el.style.display = 'none'; });
+ });
+ // Force single column
+ const grid = document.querySelector('.content-grid');
+ if (grid) grid.style.gridTemplateColumns = '1fr';
+ const main = document.querySelector('main');
+ if (main) { main.style.maxWidth = 'none'; main.style.padding = '0'; }
+ });
+
+ // -- Prepare book CSS ---------------------------------------------------
+ console.log('📚 Preparing book styles…');
+ const bookCssPath = resolve(__dirname, '..', 'src', 'styles', '_print-book.css');
+ const bookCss = await fs.readFile(bookCssPath, 'utf-8');
+ // Strip @media print wrapper – Paged.js works in screen mode
+ const unwrappedCss = bookCss
+ .replace(/^@media\s+print\s*\{/, '')
+ .replace(/\}\s*$/, '');
+
+ // Neutralize external stylesheet references that cause CORS/XHR errors
+ // in Paged.js. Inline their content and remove @import rules.
+ console.log('🔗 Neutralizing external stylesheets for Paged.js…');
+ const neutralized = await page.evaluate(async () => {
+ let count = 0;
+ // 1. Replace with inline
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Footer.astro b/content/articles/multi-harness-rl/app/src/components/Footer.astro
new file mode 100644
index 00000000..1612b3dc
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Footer.astro
@@ -0,0 +1,474 @@
+---
+interface Props {
+ citationText: string;
+ bibtex: string;
+ licence?: string;
+ doi?: string;
+ template?: string;
+}
+const { citationText, bibtex, licence, doi, template = "article" } = Astro.props as Props;
+const isPaper = template === "paper";
+---
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/FullWidth.astro b/content/articles/multi-harness-rl/app/src/components/FullWidth.astro
new file mode 100644
index 00000000..0842ddd3
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/FullWidth.astro
@@ -0,0 +1,9 @@
+---
+const { class: className, ...props } = Astro.props;
+const wrapperClass = ["full-width", className].filter(Boolean).join(" ");
+---
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Glossary.astro b/content/articles/multi-harness-rl/app/src/components/Glossary.astro
new file mode 100644
index 00000000..28336086
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Glossary.astro
@@ -0,0 +1,337 @@
+---
+interface Props {
+ /** The word or term to define */
+ term: string;
+ /** The definition of the term */
+ definition: string;
+ /** Optional CSS class to apply to the term */
+ class?: string;
+ /** Optional style to apply to the term */
+ style?: string;
+ /** Tooltip position (top, bottom, left, right) */
+ position?: "top" | "bottom" | "left" | "right";
+ /** Delay before showing tooltip in ms */
+ delay?: number;
+ /** Disable tooltip on mobile */
+ disableOnMobile?: boolean;
+}
+
+const {
+ term,
+ definition,
+ class: className = "",
+ style: inlineStyle = "",
+ position = "top",
+ delay = 300,
+ disableOnMobile = false,
+} = Astro.props as Props;
+
+// Generate a unique ID for this component
+const tooltipId = `glossary-${Math.random().toString(36).slice(2)}`;
+---
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/HeroArticle.astro b/content/articles/multi-harness-rl/app/src/components/HeroArticle.astro
new file mode 100644
index 00000000..4c229b45
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/HeroArticle.astro
@@ -0,0 +1,845 @@
+---
+import HtmlEmbed from "./HtmlEmbed.astro";
+import {
+ type HeroProps,
+ type Author,
+ normalizeAuthors,
+ stripHtml,
+ slugify,
+} from "./heroUtils";
+
+const {
+ title,
+ titleRaw,
+ description,
+ authors = [],
+ affiliations = [],
+ affiliation,
+ published,
+ doi,
+ pdfProOnly = false,
+ showPdf = true,
+ repo,
+} = Astro.props as HeroProps;
+
+const normalizedAuthors: Author[] = normalizeAuthors(authors as any);
+
+const authorAffiliationIndexSet = new Set();
+for (const author of normalizedAuthors) {
+ const indices = Array.isArray(author.affiliationIndices)
+ ? author.affiliationIndices
+ : [];
+ for (const idx of indices) {
+ if (typeof idx === "number") authorAffiliationIndexSet.add(idx);
+ }
+}
+const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1;
+const hasMultipleAffiliations =
+ Array.isArray(affiliations) && affiliations.length > 1;
+
+const pdfBase = titleRaw ? stripHtml(titleRaw) : stripHtml(title);
+const pdfFilename = `${slugify(pdfBase)}.pdf`;
+
+// The repo button's label used to be a hardcoded string, so it kept naming the
+// template author's repo no matter what `repo` pointed at. Derive it.
+const repoName = repo ? String(repo).replace(/\/+$/, "").split("/").pop() : "";
+---
+
+
+
+ {/* Two heroes are built for this article and the reader picks. Each panel is
+ lazily mounted and re-measures on becoming visible, so a hidden one costs
+ nothing until it is shown:
+ banner-opaque.html, the harness grid, one wire each, tokens after
+ banner-seam.html, the rollout fleet, the seam, the token field
+ banner.html, the galaxy inherited from the template, is still in the repo
+ but no longer offered. */}
+ {/* Only the harness grid is shown for now. To offer the rollout fleet again, add a
+ second panel with banner-seam.html (data-banner="seam") and the picker below:
+
+ Harnesses
+ Rollouts
+
+ The picker script further down already handles both. */}
+
+ {description && {description}
}
+
+
+
+
+
+
+
+
+{showPdf && (
+
+)}
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/HeroPaper.astro b/content/articles/multi-harness-rl/app/src/components/HeroPaper.astro
new file mode 100644
index 00000000..a797534c
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/HeroPaper.astro
@@ -0,0 +1,210 @@
+---
+import {
+ type HeroProps,
+ type Author,
+ normalizeAuthors,
+ getLinkIcon,
+} from "./heroUtils";
+
+const {
+ title,
+ description,
+ authors = [],
+ affiliations = [],
+ affiliation,
+ published,
+ links = [],
+} = Astro.props as HeroProps;
+
+const normalizedAuthors: Author[] = normalizeAuthors(authors as any);
+
+const authorAffiliationIndexSet = new Set();
+for (const author of normalizedAuthors) {
+ const indices = Array.isArray(author.affiliationIndices)
+ ? author.affiliationIndices
+ : [];
+ for (const idx of indices) {
+ if (typeof idx === "number") authorAffiliationIndexSet.add(idx);
+ }
+}
+const shouldShowAffiliationSupers = authorAffiliationIndexSet.size > 1;
+
+// Resolve affiliations: prefer array, fallback to legacy string
+const resolvedAffiliations =
+ Array.isArray(affiliations) && affiliations.length > 0
+ ? affiliations
+ : affiliation
+ ? [{ id: 1, name: affiliation }]
+ : [];
+---
+
+
+
+ {description && {description}
}
+
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/HfUser.astro b/content/articles/multi-harness-rl/app/src/components/HfUser.astro
new file mode 100644
index 00000000..7d2e596a
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/HfUser.astro
@@ -0,0 +1,96 @@
+---
+interface Props {
+ username: string;
+ name?: string;
+ url?: string;
+ avatarUrl?: string;
+}
+
+const { username, name, url, avatarUrl } = Astro.props as Props;
+const profileUrl = url ?? `https://huggingface.co/${encodeURIComponent(username)}`;
+const displayName = name ?? username;
+const imgSrc = avatarUrl ?? `https://huggingface.co/api/users/${encodeURIComponent(username)}/avatar`;
+---
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/HtmlEmbed.astro b/content/articles/multi-harness-rl/app/src/components/HtmlEmbed.astro
new file mode 100644
index 00000000..a4957c62
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/HtmlEmbed.astro
@@ -0,0 +1,927 @@
+---
+interface Props {
+ src: string;
+ title?: string;
+ desc?: string;
+ caption?: string;
+ frameless?: boolean;
+ wide?: boolean;
+ align?: "left" | "center" | "right";
+ id?: string;
+ data?: string | string[];
+ config?: any;
+ downloadable?: boolean;
+}
+const {
+ src,
+ title,
+ desc,
+ caption,
+ frameless = false,
+ wide = false,
+ align = "left",
+ id: providedId,
+ data,
+ config,
+ downloadable = false,
+} = Astro.props as Props;
+
+// Generate filename from src (used for download and as default ID)
+const downloadFilename = src.replace(/\.html$/, '').replace(/^.*\//, '');
+
+// Use provided ID or generate from filename
+const id = providedId || downloadFilename;
+
+// Load all .html embeds under src/content/embeds/** as strings (dev & build)
+const embeds = (import.meta as any).glob("../content/embeds/**/*.html", {
+ query: "?raw",
+ import: "default",
+ eager: true,
+}) as Record;
+
+function resolveFragment(requested: string): string | null {
+ // Allow both "banner.html" and "embeds/banner.html"
+ const needle = requested.replace(/^\/*/, "");
+ for (const [key, html] of Object.entries(embeds)) {
+ if (
+ key.endsWith("/" + needle) ||
+ key.endsWith("/" + needle.replace(/^embeds\//, ""))
+ ) {
+ return html;
+ }
+ }
+ return null;
+}
+
+const html = resolveFragment(src);
+const mountId = `frag-${Math.random().toString(36).slice(2)}`;
+const dataAttr = Array.isArray(data)
+ ? JSON.stringify(data)
+ : typeof data === "string"
+ ? data
+ : undefined;
+const configAttr =
+ typeof config === "string"
+ ? config
+ : config != null
+ ? JSON.stringify(config)
+ : undefined;
+
+// Apply the ID to the HTML content if provided
+const htmlWithId =
+ id && html
+ ? html.replace(/]*>/, `
`)
+ : html;
+---
+
+{
+ html ? (
+
+ {title && (
+
+ {title}
+
+ )}
+
+
+ {downloadable && (
+
+
+
+
+
+
+
+ )}
+
+ {(desc || caption) && (
+
+ )}
+
+ ) : (
+
+
+
+
Embed not found
+
+ The requested embed could not be loaded: {src}
+
+
+
+
+ )
+}
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Image.astro b/content/articles/multi-harness-rl/app/src/components/Image.astro
new file mode 100644
index 00000000..c1719828
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Image.astro
@@ -0,0 +1,629 @@
+---
+// @ts-ignore - types provided by Astro at runtime
+import { Image as AstroImage } from "astro:assets";
+
+interface Props {
+ /** Source image imported via astro:assets */
+ src: any;
+ /** Alt text for accessibility */
+ alt: string;
+ /** Optional HTML string caption (use slot caption for rich content) */
+ caption?: string;
+ /** Optional class to apply on the
wrapper when caption is used */
+ figureClass?: string;
+ /** Enable medium-zoom behavior on this image */
+ zoomable?: boolean;
+ /** Show a download button overlay and enable download flow */
+ downloadable?: boolean;
+ /** Optional explicit file name to use on download */
+ downloadName?: string;
+ /** Optional explicit source URL to download instead of currentSrc */
+ downloadSrc?: string;
+ /** Optional link that wraps the image (not the caption) */
+ linkHref?: string;
+ /** Optional target for the link (default: _blank when linkHref provided) */
+ linkTarget?: string;
+ /** Optional rel for the link (default: noopener noreferrer when linkHref provided) */
+ linkRel?: string;
+ /** Make the image span full width */
+ fullWidth?: boolean;
+ /** Any additional attributes should be forwarded to the underlying */
+ [key: string]: any;
+}
+
+const {
+ caption,
+ figureClass,
+ zoomable,
+ downloadable,
+ downloadName,
+ downloadSrc,
+ linkHref,
+ linkTarget,
+ linkRel,
+ fullWidth,
+ src,
+ ...imgProps
+} = Astro.props as Props;
+
+// Try to get original image src from ImageMetadata object
+const getOriginalSrc = (imageSrc: any): string => {
+ if (typeof imageSrc === "string") {
+ return imageSrc;
+ }
+ // ImageMetadata from astro:assets has a 'src' property with the URL
+ if (imageSrc && typeof imageSrc === "object") {
+ // Check if this is an import with ?url (returns the original path directly)
+ if ("default" in imageSrc && typeof imageSrc.default === "string") {
+ return imageSrc.default;
+ }
+ // Try src property (optimized version)
+ if ("src" in imageSrc && typeof imageSrc.src === "string") {
+ return imageSrc.src;
+ }
+ // Try pathname property
+ if ("pathname" in imageSrc && typeof imageSrc.pathname === "string") {
+ return imageSrc.pathname;
+ }
+ // Fallback: try all string properties
+ for (const key of Object.keys(imageSrc)) {
+ if (typeof imageSrc[key] === "string" && imageSrc[key].length > 0) {
+ return imageSrc[key];
+ }
+ }
+ }
+ return "";
+};
+
+const originalSrc = getOriginalSrc(src);
+
+// Apply safe defaults without requiring callers to specify them
+const MAX_DISPLAY_WIDTH = 1800;
+const resolvedFormat = (imgProps as any).format || "webp";
+
+// If no explicit width is provided and we have metadata, cap to MAX_DISPLAY_WIDTH
+let resolvedWidth = (imgProps as any).width;
+if (
+ !resolvedWidth &&
+ src &&
+ typeof src === "object" &&
+ typeof src.width === "number"
+) {
+ resolvedWidth = Math.min(src.width, MAX_DISPLAY_WIDTH);
+}
+const hasCaptionSlot = Astro.slots.has("caption");
+const hasCaption =
+ hasCaptionSlot || (typeof caption === "string" && caption.length > 0);
+const hasTitle = Astro.slots.has("title");
+const uid = `ri_${Math.random().toString(36).slice(2)}`;
+// The zoom handler reads the literal "true" from this data attribute.
+const dataZoomable = String(zoomable !== false || !!(imgProps as any)["data-zoomable"]);
+const dataDownloadable =
+ downloadable !== false || !!(imgProps as any)["data-downloadable"];
+const hasLink = typeof linkHref === "string" && linkHref.length > 0;
+const resolvedTarget = hasLink ? linkTarget || "_blank" : undefined;
+const resolvedRel = hasLink ? linkRel || "noopener noreferrer" : undefined;
+
+// Use downloadSrc if provided, otherwise try to use the original src
+const resolvedDownloadSrc = downloadSrc || originalSrc;
+---
+
+
+ {
+ hasCaption ? (
+
+ {dataDownloadable ? (
+
+ {hasLink ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ ) : hasLink ? (
+
+
+
+ ) : (
+
+ )}
+
+ {hasCaptionSlot ? (
+
+ ) : (
+ caption &&
+ )}
+
+
+ ) : dataDownloadable ? (
+
+ {hasLink ? (
+
+
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ ) : hasLink ? (
+
+
+
+ ) : (
+
+ )
+ }
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Note.astro b/content/articles/multi-harness-rl/app/src/components/Note.astro
new file mode 100644
index 00000000..5da6b436
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Note.astro
@@ -0,0 +1,124 @@
+---
+interface Props {
+ title?: string;
+ emoji?: string;
+ class?: string;
+ variant?: "neutral" | "info" | "success" | "danger";
+}
+const {
+ title,
+ emoji,
+ class: className,
+ variant = "neutral",
+ ...props
+} = Astro.props as Props;
+const wrapperClass = ["note", `note--${variant}`, className]
+ .filter(Boolean)
+ .join(" ");
+---
+
+
+ {title ? (
+
+
+ ) : (
+
+
+ {
+ emoji && (
+
+ {emoji}
+
+ )
+ }
+
+
+ )}
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Quote.astro b/content/articles/multi-harness-rl/app/src/components/Quote.astro
new file mode 100644
index 00000000..a1b654a0
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Quote.astro
@@ -0,0 +1,140 @@
+---
+interface Props {
+ author?: string;
+ source?: string;
+}
+
+const { author, source } = Astro.props;
+---
+
+
+
+
+
+ {
+ (author || source) && (
+
+ )
+ }
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/RawHtml.astro b/content/articles/multi-harness-rl/app/src/components/RawHtml.astro
new file mode 100644
index 00000000..45eee0db
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/RawHtml.astro
@@ -0,0 +1,6 @@
+---
+const { html, class: className, ariaLabel } = Astro.props;
+---
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Reference.astro b/content/articles/multi-harness-rl/app/src/components/Reference.astro
new file mode 100644
index 00000000..1b3a75ac
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Reference.astro
@@ -0,0 +1,74 @@
+---
+interface Props {
+ /** ID unique pour la référence */
+ id: string;
+ /** Légende HTML pour la référence */
+ caption: string;
+}
+
+const { id, caption } = Astro.props as Props;
+---
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Seo.astro b/content/articles/multi-harness-rl/app/src/components/Seo.astro
new file mode 100644
index 00000000..9db1433b
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Seo.astro
@@ -0,0 +1,97 @@
+---
+interface Props {
+ title: string;
+ description?: string;
+ authors?: string[];
+ published?: string;
+ modified?: string;
+ tags?: string[];
+ image?: string; // Preferred absolute URL
+ canonical?: string; // Public URL of the article, used for canonical and og:url
+ twitter?: string; // Author handle, e.g. "@name"
+ authorUrl?: string;
+}
+
+const {
+ title,
+ description = "",
+ authors = [],
+ published,
+ modified,
+ tags = [],
+ image,
+ canonical,
+ twitter,
+ authorUrl,
+} = Astro.props as Props;
+
+// The page is served from a *.hf.space iframe, so the shareable URL is set explicitly when known.
+const url = canonical || (Astro.url?.toString?.() ?? "");
+const site = (Astro.site ? String(Astro.site) : "") as string;
+
+// Build base URL: prefer Astro.site, fallback to origin from Astro.url
+const baseUrl = site || (Astro.url ? `${Astro.url.protocol}//${Astro.url.host}` : "");
+
+const ogImage =
+ image && image.length > 0
+ ? image.startsWith("http")
+ ? image
+ : baseUrl
+ ? new URL(image, baseUrl).toString()
+ : image
+ : undefined;
+
+const jsonLd = {
+ "@context": "https://schema.org",
+ "@type": "Article",
+ headline: title,
+ description: description || undefined,
+ datePublished: published || undefined,
+ dateModified: modified || published || undefined,
+ author: authors.map((name, i) => ({ "@type": "Person", name, url: i === 0 && authorUrl ? authorUrl : undefined })),
+ publisher: {
+ "@type": "Organization",
+ name: "Hugging Face",
+ url: "https://huggingface.co",
+ },
+ keywords: tags.length ? tags.join(", ") : undefined,
+ mainEntityOfPage: url || undefined,
+ image: ogImage ? [ogImage] : undefined,
+};
+---
+
+{title}
+
+{authors.length > 0 && }
+{tags.length > 0 && }
+
+
+
+
+
+{description && }
+
+
+{baseUrl && }
+{ogImage && }
+{ogImage && }
+{ogImage && }
+{ogImage && }
+{ogImage && ogImage.endsWith(".png") && }
+{published && }
+{modified && }
+{authors.map((a) => )}
+{tags.map((t) => )}
+
+
+{twitter && }
+{twitter && }
+
+{description && }
+{ogImage && }
+{ogImage && }
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Sidenote.astro b/content/articles/multi-harness-rl/app/src/components/Sidenote.astro
new file mode 100644
index 00000000..3ec5e379
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Sidenote.astro
@@ -0,0 +1,270 @@
+---
+
+---
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Stack.astro b/content/articles/multi-harness-rl/app/src/components/Stack.astro
new file mode 100644
index 00000000..9c140653
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Stack.astro
@@ -0,0 +1,141 @@
+---
+interface Props {
+ /** Layout mode: number of columns or 'auto' for responsive */
+ layout?: "2-column" | "3-column" | "4-column" | "auto";
+ /** Gap between items - can be a predefined size or custom value (e.g., "2rem", "20px", "1.5em") */
+ gap?: "small" | "medium" | "large" | string;
+ /** Optional class to apply on the wrapper */
+ class?: string;
+ /** Optional ID for the stack */
+ id?: string;
+}
+
+const {
+ layout = "2-column",
+ gap = "medium",
+ class: className,
+ id,
+} = Astro.props as Props;
+
+const getGapSize = () => {
+ switch (gap) {
+ case "small":
+ return "0.5rem";
+ case "medium":
+ return "1rem";
+ case "large":
+ return "1.5rem";
+ default:
+ return gap;
+ }
+};
+
+const gapSize = getGapSize();
+---
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/TableOfContents.astro b/content/articles/multi-harness-rl/app/src/components/TableOfContents.astro
new file mode 100644
index 00000000..a3206816
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/TableOfContents.astro
@@ -0,0 +1,1152 @@
+---
+export interface Props {
+ tableOfContentAutoCollapse?: boolean;
+}
+const { tableOfContentAutoCollapse = false } = Astro.props as Props;
+---
+
+
+ Table of Contents
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/ThemeToggle.astro b/content/articles/multi-harness-rl/app/src/components/ThemeToggle.astro
new file mode 100644
index 00000000..05e0a5c1
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/ThemeToggle.astro
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/Wide.astro b/content/articles/multi-harness-rl/app/src/components/Wide.astro
new file mode 100644
index 00000000..4223b655
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/Wide.astro
@@ -0,0 +1,9 @@
+---
+const { class: className, ...props } = Astro.props;
+const wrapperClass = ["wide", className].filter(Boolean).join(" ");
+---
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/components/heroUtils.ts b/content/articles/multi-harness-rl/app/src/components/heroUtils.ts
new file mode 100644
index 00000000..a3e9a7d9
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/components/heroUtils.ts
@@ -0,0 +1,87 @@
+export type Author = {
+ name: string;
+ url?: string;
+ affiliationIndices?: number[];
+};
+
+export interface HeroProps {
+ title: string;
+ titleRaw?: string;
+ description?: string;
+ authors?: Array<
+ string | { name: string; url?: string; affiliationIndices?: number[] }
+ >;
+ affiliations?: Array<{ id: number; name: string; url?: string }>;
+ affiliation?: string;
+ published?: string;
+ doi?: string;
+ pdfProOnly?: boolean;
+ showPdf?: boolean;
+ links?: Array<{ label: string; url: string; icon?: string }>;
+ repo?: string;
+}
+
+export function normalizeAuthors(
+ input: Array<
+ | string
+ | {
+ name?: string;
+ url?: string;
+ link?: string;
+ affiliationIndices?: number[];
+ }
+ >,
+): Author[] {
+ return (Array.isArray(input) ? input : [])
+ .map((a) => {
+ if (typeof a === "string") {
+ return { name: a } as Author;
+ }
+ const name = (a?.name ?? "").toString();
+ const url = (a?.url ?? a?.link) as string | undefined;
+ const affiliationIndices = Array.isArray((a as any)?.affiliationIndices)
+ ? (a as any).affiliationIndices
+ : undefined;
+ return { name, url, affiliationIndices } as Author;
+ })
+ .filter((a) => a.name && a.name.trim().length > 0);
+}
+
+export function stripHtml(text: string): string {
+ return String(text || "").replace(/<[^>]*>/g, "");
+}
+
+export function slugify(text: string): string {
+ return (
+ String(text || "")
+ .normalize("NFKD")
+ .replace(/\p{Diacritic}+/gu, "")
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "-")
+ .replace(/^-+|-+$/g, "")
+ .slice(0, 120) || "article"
+ );
+}
+
+export const LINK_ICONS: Record = {
+ github: ` `,
+ paper: ` `,
+ arxiv: ` `,
+ code: ` `,
+ demo: ` `,
+ data: ` `,
+ dataset: ` `,
+ model: ` `,
+ video: ` `,
+ blog: ` `,
+};
+
+export function getLinkIcon(label: string, icon?: string): string | null {
+ if (icon && LINK_ICONS[icon.toLowerCase()])
+ return LINK_ICONS[icon.toLowerCase()];
+ const key = label.toLowerCase().trim();
+ for (const [keyword, svg] of Object.entries(LINK_ICONS)) {
+ if (key === keyword || key.includes(keyword)) return svg;
+ }
+ return null;
+}
diff --git a/content/articles/multi-harness-rl/app/src/content/article.mdx b/content/articles/multi-harness-rl/app/src/content/article.mdx
new file mode 100644
index 00000000..5ed6f085
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/article.mdx
@@ -0,0 +1,73 @@
+---
+title: "The ultimate guide to multi-harness RL"
+description: "The same model performs differently in every agent harness. Now you can train it with RL inside the ones people actually use, like Claude Code and Codex, and improve it across all of them."
+authors:
+ - name: "Adithya S Kolavi"
+ url: "https://huggingface.co/AdithyaSK"
+ affiliations: [1]
+ - name: "Joel Niklaus"
+ url: "https://huggingface.co/joelniklaus"
+ affiliations: [1]
+ - name: "Sergio Paniego Blanco"
+ url: "https://huggingface.co/sergiopaniego"
+ affiliations: [1]
+ - name: "Leonie Monigatti"
+ url: "https://huggingface.co/iamleonie"
+ affiliations: [2]
+ - name: "Amine Dirhoussi"
+ url: "https://huggingface.co/aminediroHF"
+ affiliations: [1]
+ - name: "Ben Burtenshaw"
+ url: "https://huggingface.co/burtenshaw"
+ affiliations: [1]
+ - name: "Lewis Tunstall"
+ url: "https://huggingface.co/lewtun"
+ affiliations: [1]
+ - name: "Leandro von Werra"
+ url: "https://huggingface.co/lvwerra"
+ affiliations: [1]
+affiliations:
+ - name: "Hugging Face"
+ url: "https://huggingface.co"
+ - name: "Liquid AI"
+ url: "https://www.liquid.ai"
+published: "September 24, 2026"
+repo: "https://github.com/adithya-s-k/FineEnvs"
+seoThumbImage: "https://huggingface.co/spaces/FineEnvs/multi-harness-rl/resolve/main/app/public/og/og-image.png"
+canonicalUrl: "https://huggingface.co/spaces/FineEnvs/multi-harness-rl"
+twitter: "@adithya_s_k"
+tags:
+ - multi-harness RL
+ - reinforcement learning
+ - agent harness
+ - GRPO
+ - OpenEnv
+ - Harbor
+ - TRL
+ - Claude Code
+ - Codex
+ - OpenCode
+ - small language models
+template: "article"
+tableOfContentsAutoCollapse: true
+showPdf: true
+---
+
+{/*
+ This import list IS the chapter registry — there is no glob and no ordering config.
+ To add, remove or reorder a section, edit both the imports and the element list below.
+*/}
+
+import Introduction from "./chapters/introduction.mdx";
+import Basics from "./chapters/basics.mdx";
+import WhyMultiHarness from "./chapters/why-multi-harness.mdx";
+import OpenEnv from "./chapters/openenv.mdx";
+import Training from "./chapters/training.mdx";
+import Conclusions from "./chapters/conclusions.mdx";
+
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/.gitkeep b/content/articles/multi-harness-rl/app/src/content/assets/data/.gitkeep
new file mode 100644
index 00000000..e69de29b
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/harbor-tasks.json b/content/articles/multi-harness-rl/app/src/content/assets/data/harbor-tasks.json
new file mode 100644
index 00000000..5f2d9ce1
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/assets/data/harbor-tasks.json
@@ -0,0 +1,49 @@
+{
+ "source": "FineEnvs/SmolDataEnvs-harbor-train",
+ "tasks": [
+ {
+ "id": "0000_324_324276_qa_3",
+ "question": "What is the most common job role interest among new coders?",
+ "answer": "Web Development",
+ "level": 1,
+ "tier": "easy",
+ "kaggle": "freecodecamp/2016-new-coder-survey-",
+ "files": {
+ "task.toml": "schema_version = \"1.2\"\nartifacts = []\n\n[task]\nname = \"smoldataenvs-train/0000_324_324276_qa_3\"\ndescription = \"What is the most common job role interest among new coders?\"\nauthors = []\nkeywords = [\"smoldataenvs\", \"data-analysis\", \"kaggle\"]\n\n[metadata]\nsource_dataset = \"jupyter-agent/jupyter-agent-dataset\"\nsource_row_id = \"0000/324/324276.ipynb_qa_3\"\nkaggle_dataset_name = \"freecodecamp/2016-new-coder-survey-\"\ngold_answer = \"Web Development\"\nreward_mode_initial = \"exact_short\"\npackage_tier = 1\ndifficulty_level = 1\ndifficulty = \"easy\"\ndifficulty_tier = \"easy\"\n\n[environment]\nbuild_timeout_sec = 600.0\nos = \"linux\"\ncpus = 1\nmemory_mb = 1024\nstorage_mb = 5120\ngpus = 0\nallow_internet = true\nmcp_servers = []\n\n# Pre-agent hook: Harbor runs the command AFTER container start and BEFORE the\n# agent setup begins. We use it to pull this task's bucket prefix into\n# /home/user/input/. See environment/pull_bucket.py.\n[environment.healthcheck]\ncommand = \"python3 /opt/pull_bucket.py && [ -n \\\"$(ls /home/user/input)\\\" ]\"\ninterval_sec = 2.0\ntimeout_sec = 180.0\nstart_period_sec = 5.0\nstart_interval_sec = 2.0\nretries = 30\n\n[environment.env]\nHF_BUCKET = \"AdithyaSK/jupyter-agent-kaggle-all\"\nBUCKET_PREFIX = \"freecodecamp__2016-new-coder-survey-\"\nHF_TOKEN = \"${HF_TOKEN}\"\nKAGGLE_DATASET_NAME = \"freecodecamp/2016-new-coder-survey-\"\n\n[verifier]\ntimeout_sec = 120.0\n\n[verifier.env]\nEXPECTED_ANSWER = \"Web Development\"\nQUESTION = \"What is the most common job role interest among new coders?\"\nREWARD_MODE = \"flexible\"\nATOL = \"0.0\"\nRTOL = \"0.0\"\n\n[agent]\n# Capped at 600s (10 min) to kill the long-tail stuck-agent cases without\n# cutting off legitimate complex trials. Median Phase B trial is 60-120s;\n# legitimate L4/L5 tasks can hit 200-300s; anything past 600s is almost\n# certainly a stuck agent loop.\ntimeout_sec = 600.0\n\n[solution.env]\n",
+ "instruction.md": "You are a data-analysis agent working in a sandbox. Use your code-execution tool to inspect the files and compute the answer.\n\nFiles (in /home/user/input, no subfolders):\n- 2016-FCC-New-Coders-Survey-Data.csv\n\nInstalled: pandas, numpy, matplotlib, seaborn, scipy, scikit-learn, statsmodels, tabulate, sqlite3, plotly (pip install more if needed).\n\nQuestion:\nWhat is the most common job role interest among new coders?\n\nWork it out step by step — inspect the data first (head, shape, dtypes), then compute.\n\nAnswer with a single clean value: a bare number (no commas or units, e.g. 95293), a short label, yes/no, or a comma-separated list. Keep decimal precision. If there's no applicable answer, write: Not Applicable\n\nWrite only that value to /workdir/answer.txt (e.g. `echo -n \"\" > /workdir/answer.txt`), then stop.",
+ "environment/Dockerfile": "FROM docker.io/savatar101/env-data-agent-train:base\nWORKDIR /workdir\n"
+ }
+ },
+ {
+ "id": "0000_369_369503_qa_1",
+ "question": "What percentage of all matches have a goal difference of zero (i.e., draws)?",
+ "answer": "25.4%",
+ "level": 2,
+ "tier": "medium",
+ "kaggle": "hugomathien/soccer",
+ "files": {
+ "task.toml": "schema_version = \"1.2\"\nartifacts = []\n\n[task]\nname = \"smoldataenvs-train/0000_369_369503_qa_1\"\ndescription = \"What percentage of all matches have a goal difference of zero (i.e., draws)?\"\nauthors = []\nkeywords = [\"smoldataenvs\", \"data-analysis\", \"kaggle\"]\n\n[metadata]\nsource_dataset = \"jupyter-agent/jupyter-agent-dataset\"\nsource_row_id = \"0000/369/369503.ipynb_qa_1\"\nkaggle_dataset_name = \"hugomathien/soccer\"\ngold_answer = \"25.4%\"\nreward_mode_initial = \"flexible\"\npackage_tier = 3\ndifficulty_level = 2\ndifficulty = \"medium\"\ndifficulty_tier = \"medium\"\n\n[environment]\nbuild_timeout_sec = 600.0\nos = \"linux\"\ncpus = 2\nmemory_mb = 4096\nstorage_mb = 10240\ngpus = 0\nallow_internet = true\nmcp_servers = []\n\n# Pre-agent hook: Harbor runs the command AFTER container start and BEFORE the\n# agent setup begins. We use it to pull this task's bucket prefix into\n# /home/user/input/. See environment/pull_bucket.py.\n[environment.healthcheck]\ncommand = \"python3 /opt/pull_bucket.py && [ -n \\\"$(ls /home/user/input)\\\" ]\"\ninterval_sec = 2.0\ntimeout_sec = 180.0\nstart_period_sec = 5.0\nstart_interval_sec = 2.0\nretries = 30\n\n[environment.env]\nHF_BUCKET = \"AdithyaSK/jupyter-agent-kaggle-all\"\nBUCKET_PREFIX = \"hugomathien__soccer\"\nHF_TOKEN = \"${HF_TOKEN}\"\nKAGGLE_DATASET_NAME = \"hugomathien/soccer\"\n\n[verifier]\ntimeout_sec = 120.0\n\n[verifier.env]\nEXPECTED_ANSWER = \"25.4%\"\nQUESTION = \"What percentage of all matches have a goal difference of zero (i.e., draws)?\"\nREWARD_MODE = \"flexible\"\n\n[agent]\ntimeout_sec = 900.0\n\n[solution.env]\n",
+ "instruction.md": "You are a data-analysis agent working in a sandbox. Use your code-execution tool to inspect the files and compute the answer.\n\nFiles (in /home/user/input, no subfolders):\n- database.sqlite\n\nInstalled: pandas, numpy, matplotlib, seaborn, scipy, scikit-learn, statsmodels, tabulate, sqlite3, plotly (pip install more if needed).\n\nQuestion:\nWhat percentage of all matches have a goal difference of zero (i.e., draws)?\n\nWork it out step by step — inspect the data first (head, shape, dtypes), then compute.\n\nExpress the value as a percentage (e.g. 95.5), not a fraction.\n\nAnswer with a single clean value: a bare number (no commas or units, e.g. 95293), a short label, yes/no, or a comma-separated list. Keep decimal precision. If there's no applicable answer, write: Not Applicable\n\nWrite only that value to /workdir/answer.txt (e.g. `echo -n \"\" > /workdir/answer.txt`), then stop.",
+ "environment/Dockerfile": "FROM docker.io/savatar101/env-data-agent-train:base\nWORKDIR /workdir\n"
+ }
+ },
+ {
+ "id": "0001_085_1085629_qa_4",
+ "question": "What is the accuracy of the KNN model when using only the top two features from the Feature Importance ranking (fnlwgt and age) with the optimal K value?",
+ "answer": "0.762",
+ "level": 4,
+ "tier": "hard",
+ "kaggle": "uciml/adult-census-income",
+ "files": {
+ "task.toml": "schema_version = \"1.2\"\nartifacts = []\n\n[task]\nname = \"smoldataenvs-train/0001_085_1085629_qa_4\"\ndescription = \"What is the accuracy of the KNN model when using only the top two features from the Feature Importance ranking (fnlwgt and age) with the optimal K value?\"\nauthors = []\nkeywords = [\"smoldataenvs\", \"data-analysis\", \"kaggle\"]\n\n[metadata]\nsource_dataset = \"jupyter-agent/jupyter-agent-dataset\"\nsource_row_id = \"0001/085/1085629.ipynb_qa_4\"\nkaggle_dataset_name = \"uciml/adult-census-income\"\ngold_answer = \"0.762\"\nreward_mode_initial = \"numeric\"\npackage_tier = 1\ndifficulty_level = 4\ndifficulty = \"hard\"\ndifficulty_tier = \"hard\"\n\n[environment]\nbuild_timeout_sec = 600.0\nos = \"linux\"\ncpus = 1\nmemory_mb = 1024\nstorage_mb = 5120\ngpus = 0\nallow_internet = true\nmcp_servers = []\n\n# Pre-agent hook: Harbor runs the command AFTER container start and BEFORE the\n# agent setup begins. We use it to pull this task's bucket prefix into\n# /home/user/input/. See environment/pull_bucket.py.\n[environment.healthcheck]\ncommand = \"python3 /opt/pull_bucket.py && [ -n \\\"$(ls /home/user/input)\\\" ]\"\ninterval_sec = 2.0\ntimeout_sec = 180.0\nstart_period_sec = 5.0\nstart_interval_sec = 2.0\nretries = 30\n\n[environment.env]\nHF_BUCKET = \"AdithyaSK/jupyter-agent-kaggle-all\"\nBUCKET_PREFIX = \"uciml__adult-census-income\"\nHF_TOKEN = \"${HF_TOKEN}\"\nKAGGLE_DATASET_NAME = \"uciml/adult-census-income\"\n\n[verifier]\ntimeout_sec = 120.0\n\n[verifier.env]\nEXPECTED_ANSWER = \"0.762\"\nQUESTION = \"What is the accuracy of the KNN model when using only the top two features from the Feature Importance ranking (fnlwgt and age) with the optimal K value?\"\nREWARD_MODE = \"flexible\"\nATOL = \"0.001\"\nRTOL = \"0.005\"\n\n[agent]\n# Capped at 600s (10 min) to kill the long-tail stuck-agent cases without\n# cutting off legitimate complex trials. Median Phase B trial is 60-120s;\n# legitimate L4/L5 tasks can hit 200-300s; anything past 600s is almost\n# certainly a stuck agent loop.\ntimeout_sec = 600.0\n\n[solution.env]\n",
+ "instruction.md": "You are a data-analysis agent working in a sandbox. Use your code-execution tool to inspect the files and compute the answer.\n\nFiles (in /home/user/input, no subfolders):\n- adult.csv\n\nInstalled: pandas, numpy, matplotlib, seaborn, scipy, scikit-learn, statsmodels, tabulate, sqlite3, plotly (pip install more if needed).\n\nQuestion:\nWhat is the accuracy of the KNN model when using only the top two features from the Feature Importance ranking (fnlwgt and age) with the optimal K value?\n\nWork it out step by step — inspect the data first (head, shape, dtypes), then compute.\n\nExpress the value as a percentage (e.g. 95.5), not a fraction.\n\nAnswer with a single clean value: a bare number (no commas or units, e.g. 95293), a short label, yes/no, or a comma-separated list. Keep decimal precision. If there's no applicable answer, write: Not Applicable\n\nWrite only that value to /workdir/answer.txt (e.g. `echo -n \"\" > /workdir/answer.txt`), then stop.",
+ "environment/Dockerfile": "FROM docker.io/savatar101/env-data-agent-train:base\nWORKDIR /workdir\n"
+ }
+ }
+ ],
+ "shared": {
+ "environment/pull_bucket.py": "\"\"\"Pull this task's bucket prefix into /home/user/input/.\n\nInvoked by Harbor's [environment.healthcheck] command (declared in task.toml)\n— runs after container start, before the agent. Idempotent: skips work if\nfiles are already present from a prior pull.\n\"\"\"\n\nimport os\nimport sys\nfrom pathlib import Path\n\nfrom huggingface_hub import download_bucket_files, list_bucket_tree\n\n\ndef main() -> int:\n bucket = os.environ.get(\"HF_BUCKET\") or \"AdithyaSK/jupyter-agent-kaggle-all\"\n prefix = os.environ[\"BUCKET_PREFIX\"].rstrip(\"/\") + \"/\"\n dest = Path(\"/home/user/input\")\n dest.mkdir(parents=True, exist_ok=True)\n\n existing = [p for p in dest.iterdir() if p.is_file()]\n if existing:\n print(f\"[pull_bucket] {dest}/ already has {len(existing)} file(s); skipping\", flush=True)\n return 0\n\n targets = [\n (it.path, str(dest / Path(it.path).name))\n for it in list_bucket_tree(bucket, prefix=prefix, recursive=True)\n if getattr(it, \"type\", None) == \"file\"\n ]\n if not targets:\n print(f\"[pull_bucket] FATAL: no files at hf://buckets/{bucket}/{prefix}\", flush=True)\n return 2\n\n download_bucket_files(bucket, files=targets)\n print(f\"[pull_bucket] staged {len(targets)} file(s) from {bucket}/{prefix}\", flush=True)\n return 0\n\n\nif __name__ == \"__main__\":\n sys.exit(main())\n",
+ "tests/test.sh": "#!/usr/bin/env bash\nset -u\nmkdir -p /logs/verifier\n\nanswer_path=\"/workdir/answer.txt\"\nif [ ! -s \"$answer_path\" ]; then\n echo \"0.0\" > /logs/verifier/reward.txt\n echo \"[grader] no answer at $answer_path\" >&2\n exit 0\nfi\n\npip install --quiet math-verify >/dev/null 2>&1 || true\npython3 /tests/grader.py < \"$answer_path\" > /logs/verifier/reward.txt\n",
+ "tests/grader.py": "\"\"\"Deterministic short-answer grader v2 — NO LLM judge.\n\nTiers (all offline/deterministic):\n 1. Exact (case-insensitive, whitespace-collapsed)\n 2. Numeric (abs/rel tolerance from ATOL/RTOL; gated to clean-number golds)\n + percent<->fraction bridge (e.g. gold 96.00 == pred 0.9621)\n 3. List (comma-separated): split, strip, order-insensitive; numeric-tolerant per element\n (fixes 'a, b' vs 'a,b' spacing and reordering)\n 4. Math-Verify (symbolic/numeric equivalence)\n\"\"\"\nfrom __future__ import annotations\nimport os, re, sys\nfrom dataclasses import dataclass\n\n_NUMERIC_RE = re.compile(r\"-?\\d+(?:[.,]\\d+)?(?:[eE][-+]?\\d+)?\")\n\n\n@dataclass\nclass GradeResult:\n reward: float\n method: str\n\n\ndef _normalize(s: str) -> str:\n return re.sub(r\"\\s+\", \" \", (s or \"\").strip().lower())\n\n\ndef _to_float(s: str):\n if not s:\n return None\n m = _NUMERIC_RE.search(str(s).replace(\",\", \"\"))\n if not m:\n return None\n try:\n return float(m.group(0))\n except ValueError:\n return None\n\n\ndef _num_close(g, c, abs_tol, rel_tol) -> bool:\n return abs(g - c) <= abs_tol or abs(g - c) / max(abs(g), 1e-9) <= rel_tol\n\n\ndef _is_clean_number(s: str) -> bool:\n t = (s or \"\").strip().strip(\"%$\").strip().replace(\",\", \"\")\n return bool(_NUMERIC_RE.fullmatch(t))\n\n\ndef _elem_match(a, b, abs_tol, rel_tol) -> bool:\n if _normalize(a) == _normalize(b):\n return True\n fa, fb = _to_float(a), _to_float(b)\n if fa is not None and fb is not None:\n return _num_close(fa, fb, abs_tol, rel_tol)\n return False\n\n\ndef _list_match(gold: str, cand: str, abs_tol, rel_tol) -> bool:\n gl = [x.strip() for x in gold.split(\",\") if x.strip()]\n cl = [x.strip() for x in cand.split(\",\") if x.strip()]\n if len(gl) < 2 or len(gl) != len(cl):\n return False\n for gs, cs in ((gl, cl), (sorted(gl, key=str.lower), sorted(cl, key=str.lower))):\n if all(_elem_match(a, b, abs_tol, rel_tol) for a, b in zip(gs, cs)):\n return True\n return False\n\n\ndef _math_verify_match(gold: str, candidate: str) -> bool:\n try:\n from math_verify import parse, verify\n return bool(verify(parse(gold), parse(candidate), timeout_seconds=5))\n except Exception:\n return False\n\n\ndef grade(gold: str, candidate: str, *, question: str = \"\", reward_mode: str = \"\",\n judge: bool = True, judge_model=None, rel_tol: float = 1e-3, abs_tol: float = 1e-3) -> GradeResult:\n if not gold or candidate is None:\n return GradeResult(0.0, \"miss\")\n\n # Tier 1: exact\n if _normalize(gold) == _normalize(candidate):\n return GradeResult(1.0, \"exact\")\n\n # Tier 2: numeric (clean-number gold) + percent/fraction bridge\n if reward_mode in (\"numeric\", \"flexible\") or _is_clean_number(gold):\n g, c = _to_float(gold), _to_float(candidate)\n if g is not None and c is not None:\n if _num_close(g, c, abs_tol, rel_tol):\n return GradeResult(1.0, \"numeric\")\n # percent<->fraction: one side is a fraction (<1), the other a percent (>=1)\n if (0 < abs(c) < 1 <= abs(g)) or (0 < abs(g) < 1 <= abs(c)):\n if _num_close(g, c * 100, abs_tol, rel_tol) or _num_close(g, c / 100, abs_tol, rel_tol):\n return GradeResult(1.0, \"numeric_scaled\")\n\n # Tier 3: list (comma-separated), order-insensitive, per-element tolerant\n if reward_mode in (\"list\", \"list_csv\") or (\",\" in gold and \",\" in candidate):\n if _list_match(gold, candidate, abs_tol, rel_tol):\n return GradeResult(1.0, \"list\")\n\n # Tier 4: math-verify\n if _math_verify_match(gold, candidate):\n return GradeResult(1.0, \"math_verify\")\n\n return GradeResult(0.0, \"miss\")\n\n\ndef _tool_calls():\n try:\n with open(\"/workdir/.n_tool_calls\") as fh:\n return int(fh.read().strip())\n except (OSError, ValueError):\n pass\n raw = os.environ.get(\"N_TOOL_CALLS\")\n if raw is not None:\n try:\n return int(str(raw).strip())\n except ValueError:\n return None\n return None\n\n\ndef _tool_efficiency(n):\n if n is None:\n return None\n budget = float(os.environ.get(\"TOOL_BUDGET\", \"15\") or \"15\")\n if budget <= 0:\n return None\n return max(0.0, min(1.0, 1.0 - n / budget))\n\n\ndef _tols():\n def _f(name):\n try:\n return float(os.environ.get(name, \"\") or 1e-3)\n except ValueError:\n return 1e-3\n return _f(\"ATOL\"), _f(\"RTOL\")\n\n\ndef main_cli() -> int:\n gold = (os.environ.get(\"EXPECTED_ANSWER\") or \"\").strip()\n question = (os.environ.get(\"QUESTION\") or \"\").strip()\n candidate = sys.stdin.read().strip()\n at, rt = _tols()\n r = grade(gold, candidate, question=question, reward_mode=os.environ.get(\"REWARD_MODE\", \"\") or \"\", abs_tol=at, rel_tol=rt)\n print(f\"{r.reward:.1f}\")\n print(f\"[grader] gold={gold!r} pred={candidate[:80]!r} reward={r.reward} method={r.method}\", file=sys.stderr)\n return 0\n\n\ndef main_json() -> int:\n import json\n gold = (os.environ.get(\"EXPECTED_ANSWER\") or \"\").strip()\n question = (os.environ.get(\"QUESTION\") or \"\").strip()\n candidate = sys.stdin.read().strip()\n if not candidate:\n print(json.dumps({\"correctness\": 0.0, \"submission\": 0.0, \"tool_efficiency\": 0.0}))\n return 0\n at, rt = _tols()\n r = grade(gold, candidate, question=question, reward_mode=os.environ.get(\"REWARD_MODE\", \"\") or \"\", abs_tol=at, rel_tol=rt)\n n = _tool_calls()\n print(json.dumps({\"correctness\": float(r.reward), \"submission\": 1.0, \"tool_efficiency\": _tool_efficiency(n)}))\n print(f\"[grader] gold={gold!r} pred={candidate[:80]!r} correctness={r.reward} method={r.method}\", file=sys.stderr)\n return 0\n\n\nif __name__ == \"__main__\":\n if \"--json\" in sys.argv[1:]:\n raise SystemExit(main_json())\n raise SystemExit(main_cli())\n"
+ }
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/harness-disclosure.json b/content/articles/multi-harness-rl/app/src/content/assets/data/harness-disclosure.json
new file mode 100644
index 00000000..d722a4bc
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/assets/data/harness-disclosure.json
@@ -0,0 +1,101 @@
+{
+ "note": "Each release sits at the most its report says about harnesses: naming none, naming one per benchmark row, measuring across several, or training across several.",
+ "levels": [
+ "names none",
+ "names one per row",
+ "measures across",
+ "trains across"
+ ],
+ "items": [
+ {
+ "label": "Cohere Command A",
+ "date": "2025-04",
+ "level": 0,
+ "detail": "The words harness and scaffold appear zero times."
+ },
+ {
+ "label": "Qwen3-Coder",
+ "date": "2025-07",
+ "level": 0,
+ "detail": "Ships Qwen Code, Claude Code and Cline instructions, never names its own."
+ },
+ {
+ "label": "Kimi K2",
+ "date": "2025-07",
+ "level": 1,
+ "detail": "Terminal-Bench listed twice, 25.0 Terminus and 30.0 in-house."
+ },
+ {
+ "label": "gpt-oss",
+ "date": "2025-08",
+ "level": 0,
+ "detail": "SWE-bench on n=477 tasks, scaffold unnamed."
+ },
+ {
+ "label": "Seed-OSS",
+ "date": "2025-08",
+ "level": 1,
+ "detail": "Names the harness inside the row label, with two rank reversals."
+ },
+ {
+ "label": "MiniMax M2",
+ "date": "2025-10",
+ "level": 1,
+ "detail": "A different named harness for each benchmark."
+ },
+ {
+ "label": "DeepSeek-V3.2",
+ "date": "2025-12",
+ "level": 2,
+ "detail": "Checked across Claude Code and RooCode, and incompatible with Terminus."
+ },
+ {
+ "label": "Qwen3-Coder-Next",
+ "date": "2026-03",
+ "level": 3,
+ "detail": "Generates its agentic training data in six different harnesses."
+ },
+ {
+ "label": "Orchard",
+ "date": "2026-05",
+ "level": 3,
+ "detail": "Trains across harnesses, and measures 62.4 falling to 3.6 on an unseen one."
+ },
+ {
+ "label": "Polar",
+ "date": "2026-05",
+ "level": 3,
+ "detail": "RL against four harnesses through an endpoint proxy."
+ },
+ {
+ "label": "poolside Laguna",
+ "date": "2026-05",
+ "level": 3,
+ "detail": "1.3B tokens of trajectories from three harnesses it does not own."
+ },
+ {
+ "label": "KAT-Coder-V2.5",
+ "date": "2026-07",
+ "level": 3,
+ "detail": "Coins harness scaling and names three ways a model overfits."
+ },
+ {
+ "label": "OpenForgeRL",
+ "date": "2026-07",
+ "level": 3,
+ "detail": "A controlled comparison of training on one harness against three."
+ },
+ {
+ "label": "Kimi K3",
+ "date": "2026-07",
+ "level": 3,
+ "detail": "RL across harnesses assembled from composable modules."
+ },
+ {
+ "label": "MiMo-V2.6",
+ "date": "2026-09",
+ "level": 3,
+ "detail": "Multi-harness RL on a pool of task-adapted mini-harnesses."
+ }
+ ]
+}
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/harness-ecosystem.json b/content/articles/multi-harness-rl/app/src/content/assets/data/harness-ecosystem.json
new file mode 100644
index 00000000..752b382e
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/assets/data/harness-ecosystem.json
@@ -0,0 +1,186 @@
+{
+ "note": "The model API a harness speaks decides whether a capture proxy can sit in front of it without changes to the harness.",
+ "columns": [
+ "Harness",
+ "Maker",
+ "Model API it speaks",
+ "Tool calling",
+ "What makes it distinct"
+ ],
+ "rows": [
+ [
+ "Claude Code",
+ "Anthropic",
+ "Anthropic Messages",
+ "yes",
+ "The de facto reference implementation, with its agent source unpublished"
+ ],
+ [
+ "Codex",
+ "OpenAI",
+ "OpenAI Responses",
+ "yes",
+ "The only harness to have deliberately deleted chat completions"
+ ],
+ [
+ "OpenCode",
+ "Anomaly",
+ "all four",
+ "yes",
+ "Headless daemon with a published OpenAPI schema"
+ ],
+ [
+ "Goose",
+ "Agentic AI Foundation",
+ "Anthropic, chat completions or Google",
+ "yes",
+ "Most MCP-native, and can treat other harnesses as model providers"
+ ],
+ [
+ "SWE-agent",
+ "Princeton, Stanford",
+ "chat completions",
+ "yes",
+ "Tools installed into the container as shell scripts"
+ ],
+ [
+ "Mini-SWE-Agent",
+ "SWE-agent team",
+ "chat completions, Responses or plain text",
+ "one tool",
+ "Deliberately minimal, stateless steps, built to avoid overfitting"
+ ],
+ [
+ "OpenHands SDK",
+ "OpenHands",
+ "chat completions",
+ "yes",
+ "Event-stream architecture, agent server over REST"
+ ],
+ [
+ "Aider",
+ "Aider-AI",
+ "chat completions",
+ "no",
+ "Parses edit formats out of prose instead of calling tools"
+ ],
+ [
+ "Qwen Code",
+ "Alibaba",
+ "several, Google’s by default",
+ "yes",
+ "Forked from Gemini CLI, canonical form still Google's"
+ ],
+ [
+ "Kimi CLI",
+ "Moonshot",
+ "chat completions",
+ "yes",
+ "A coding agent that is also a shell"
+ ],
+ [
+ "Terminus 2",
+ "Laude Institute",
+ "chat completions",
+ "no",
+ "Uses no tool-calling API at all, plain text parsing"
+ ],
+ [
+ "Hermes",
+ "Nous Research",
+ "three, switchable",
+ "yes",
+ "Persistent multi-surface agent with a learning loop"
+ ]
+ ],
+ "apis": [
+ "Anthropic Messages",
+ "OpenAI Responses",
+ "Google generateContent",
+ "chat completions",
+ "plain text"
+ ],
+ "api_support": {
+ "Claude Code": [
+ 2,
+ 0,
+ 0,
+ 0,
+ 0
+ ],
+ "Codex": [
+ 0,
+ 2,
+ 0,
+ 0,
+ 0
+ ],
+ "OpenCode": [
+ 2,
+ 2,
+ 2,
+ 2,
+ 0
+ ],
+ "Goose": [
+ 2,
+ 0,
+ 2,
+ 2,
+ 0
+ ],
+ "SWE-agent": [
+ 0,
+ 0,
+ 0,
+ 2,
+ 0
+ ],
+ "Mini-SWE-Agent": [
+ 0,
+ 1,
+ 0,
+ 2,
+ 1
+ ],
+ "OpenHands SDK": [
+ 0,
+ 0,
+ 0,
+ 2,
+ 0
+ ],
+ "Aider": [
+ 0,
+ 0,
+ 0,
+ 2,
+ 0
+ ],
+ "Qwen Code": [
+ 0,
+ 0,
+ 2,
+ 1,
+ 0
+ ],
+ "Kimi CLI": [
+ 0,
+ 0,
+ 0,
+ 2,
+ 0
+ ],
+ "Terminus 2": [
+ 0,
+ 0,
+ 0,
+ 2,
+ 0
+ ],
+ "Hermes": null
+ },
+ "api_note": {
+ "Hermes": "three APIs, switchable"
+ }
+}
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/lfm-savings-heat.json b/content/articles/multi-harness-rl/app/src/content/assets/data/lfm-savings-heat.json
new file mode 100644
index 00000000..ccdf73af
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/assets/data/lfm-savings-heat.json
@@ -0,0 +1 @@
+{"snapshot":"23 Sep 2026 \u00b7 21:44 UTC","harnesses":["opencode","claude-code","codex","mini-swe-agent"],"cell":["tool_n","tool_sav","tok_n","out_sav","in_sav"],"runs":{"opencode":[{"step":100,"partial":true,"cells":[[66,26.2,66,31.7,20.3],[60,-0.7,60,2.0,5.1],[74,-3.4,74,-3.9,1.0],[132,1.2,132,6.1,4.4]]},{"step":200,"partial":true,"cells":[[71,29.5,71,27.3,24.5],[57,-9.9,57,-15.6,-5.6],[81,2.3,81,3.4,8.6],[134,-2.2,134,-5.0,-0.6]]},{"step":300,"partial":true,"cells":[[66,31.2,66,29.2,29.2],[60,1.9,60,-18.8,5.2],[81,6.8,81,5.3,9.7],[135,-1.5,135,2.8,3.8]]},{"step":400,"partial":true,"cells":[[66,25.2,66,14.8,20.0],[61,-2.2,61,-18.9,2.1],[85,6.5,85,-4.7,6.0],[140,2.0,140,-0.6,4.9]]},{"step":500,"partial":false,"cells":[[64,32.9,64,23.9,28.9],[51,-5.2,51,-61.9,-3.2],[77,11.1,77,7.6,14.4],[136,0.6,136,-3.9,3.4]]},{"step":600,"partial":false,"cells":[[75,34.7,75,23.0,30.1],[62,8.6,62,-27.2,12.2],[83,14.8,83,9.3,16.5],[135,3.9,135,2.3,11.7]]},{"step":700,"partial":false,"cells":[[70,32.3,70,0.7,25.2],[58,6.6,58,-38.5,8.1],[78,17.1,78,-7.9,18.2],[134,-0.7,134,-13.3,2.3]]},{"step":800,"partial":false,"cells":[[72,37.1,72,31.0,32.3],[66,5.9,66,-40.4,6.8],[78,23.5,78,8.7,24.6],[133,-0.0,133,-17.8,-1.6]]},{"step":900,"partial":false,"cells":[[73,34.9,73,28.9,31.9],[62,7.3,62,0.9,13.3],[79,18.2,79,-1.9,15.9],[128,3.4,128,-12.6,9.2]]},{"step":1000,"partial":false,"cells":[[74,36.4,74,35.3,32.4],[55,-9.6,55,-55.4,-9.2],[71,23.6,71,5.1,23.3],[137,2.1,137,-6.5,-0.0]]}],"multi":[{"step":100,"partial":true,"cells":[[59,22.6,59,30.6,24.9],[56,2.9,56,6.1,3.2],[81,14.6,81,25.4,20.2],[130,5.7,130,9.7,12.7]]},{"step":200,"partial":true,"cells":[[67,32.2,67,35.9,30.2],[61,9.7,61,11.9,9.6],[92,20.0,92,20.0,22.3],[135,7.2,135,8.5,12.7]]},{"step":300,"partial":false,"cells":[[60,30.7,60,28.8,28.4],[64,12.1,64,5.0,14.4],[90,33.1,90,29.2,32.5],[139,6.9,139,-0.8,0.5]]},{"step":400,"partial":false,"cells":[[60,24.1,60,26.5,18.5],[67,19.1,67,5.7,19.5],[83,41.4,83,34.0,35.4],[133,12.9,133,17.1,15.9]]},{"step":500,"partial":false,"cells":[[65,28.8,65,30.9,31.0],[72,19.5,72,17.2,19.8],[86,40.6,86,38.0,38.2],[131,12.6,131,8.6,3.9]]},{"step":600,"partial":false,"cells":[[68,31.6,68,44.2,27.9],[69,15.8,69,18.5,21.9],[79,43.7,79,45.9,37.6],[129,12.1,129,8.5,5.7]]},{"step":700,"partial":false,"cells":[[68,31.4,68,44.7,39.8],[71,19.2,71,12.6,19.5],[88,46.8,88,33.9,37.4],[134,14.8,134,11.7,10.3]]},{"step":800,"partial":false,"cells":[[68,23.2,68,23.6,29.4],[68,18.2,68,33.1,21.6],[85,49.9,85,41.1,42.9],[133,10.5,133,-2.0,-8.8]]},{"step":850,"partial":false,"cells":[[70,25.4,70,28.3,21.5],[73,15.6,73,7.9,16.4],[83,50.3,83,34.9,40.7],[134,15.6,134,12.5,10.5]]},{"step":900,"partial":false,"cells":[[67,34.8,67,37.6,34.2],[68,18.2,68,8.9,16.7],[87,52.4,87,45.5,45.2],[130,14.8,130,5.6,6.8]]},{"step":1000,"partial":false,"cells":[[66,32.8,66,37.9,27.2],[69,28.3,69,16.8,24.8],[89,53.0,89,39.7,45.9],[132,16.2,132,16.6,11.2]]}]}}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/reward-group-audit.json b/content/articles/multi-harness-rl/app/src/content/assets/data/reward-group-audit.json
new file mode 100644
index 00000000..dd5c0eab
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/assets/data/reward-group-audit.json
@@ -0,0 +1,65 @@
+{
+ "description": "GRPO groups with at least one rollout admitted to a retained optimizer update. Counts include partially scorable groups. Optimizer steps are not group counts.",
+ "audited_at": "2026-10-01",
+ "method": "Join group rollout_ids to efficiency records and optimizer_rollouts.jsonl. All scorable correctness values must be 1, and max(reward)-min(reward) must exceed 1e-9 for efficiency-only contrast.",
+ "retained_segments": {
+ "opencode": {
+ "train-83820": [
+ 1,
+ 428
+ ],
+ "train-84025": [
+ 429,
+ 1000
+ ]
+ },
+ "multi-harness": {
+ "train-83822": [
+ 1,
+ 292
+ ],
+ "train-84027": [
+ 293,
+ 850
+ ],
+ "train-84319": [
+ 851,
+ 1000
+ ]
+ }
+ },
+ "excluded_updates": "Multi-harness train-84027 steps 851\u2013867 were discarded on resume from checkpoint 850.",
+ "source_root": "experiments/lfm25-medium-hard1000-20260921",
+ "runs": {
+ "opencode": {
+ "groups_with_committed_optimizer_admission": {
+ "mixed": 308,
+ "all_wrong": 134,
+ "all_correct_efficiency_contrast": 97,
+ "all_correct_no_contrast": 16
+ },
+ "total": 555,
+ "eight_scorable_rollouts": {
+ "mixed": 301,
+ "all_wrong": 133,
+ "all_correct_efficiency_contrast": 96,
+ "all_correct_no_contrast": 16
+ }
+ },
+ "multi-harness": {
+ "groups_with_committed_optimizer_admission": {
+ "all_wrong": 172,
+ "mixed": 358,
+ "all_correct_efficiency_contrast": 155,
+ "all_correct_no_contrast": 1
+ },
+ "total": 686,
+ "eight_scorable_rollouts": {
+ "all_wrong": 158,
+ "mixed": 348,
+ "all_correct_efficiency_contrast": 153,
+ "all_correct_no_contrast": 1
+ }
+ }
+ }
+}
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/scaffold-reversal.json b/content/articles/multi-harness-rl/app/src/content/assets/data/scaffold-reversal.json
new file mode 100644
index 00000000..f7132808
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/assets/data/scaffold-reversal.json
@@ -0,0 +1,50 @@
+{
+ "source": "Seed-OSS-36B-Instruct model card, ByteDance Seed",
+ "url": "https://huggingface.co/ByteDance-Seed/Seed-OSS-36B-Instruct",
+ "benchmark": "SWE-Bench Verified",
+ "note": "AgentLess 4*10 is best-of-n sampling, so absolute gaps mix scaffold with test-time compute. The rank reversals survive that; the raw point gaps do not.",
+ "models": [
+ "Seed1.6-Thinking-0715",
+ "Qwen3-30B-A3B-Thinking-2507",
+ "Qwen3-32B",
+ "Seed-OSS-36B-Instruct"
+ ],
+ "scaffolds": [
+ {
+ "name": "OpenHands",
+ "scores": [
+ 41.8,
+ 31.0,
+ 23.4,
+ 56.0
+ ]
+ },
+ {
+ "name": "AgentLess 4*10",
+ "scores": [
+ 48.4,
+ 33.5,
+ 39.7,
+ 47.0
+ ]
+ }
+ ],
+ "reversals": [
+ {
+ "a": "Seed-OSS-36B-Instruct",
+ "b": "Seed1.6-Thinking-0715",
+ "under": "OpenHands",
+ "margin": 14.2,
+ "flips_under": "AgentLess 4*10",
+ "flipped_margin": 1.4
+ },
+ {
+ "a": "Qwen3-30B-A3B-Thinking-2507",
+ "b": "Qwen3-32B",
+ "under": "OpenHands",
+ "margin": 7.6,
+ "flips_under": "AgentLess 4*10",
+ "flipped_margin": 6.2
+ }
+ ]
+}
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/sft-vs-rl.json b/content/articles/multi-harness-rl/app/src/content/assets/data/sft-vs-rl.json
new file mode 100644
index 00000000..44037628
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/assets/data/sft-vs-rl.json
@@ -0,0 +1,231 @@
+{
+ "about": "LFM2.5-2.6B on the 1,000 test cells (250 tasks x 4 harnesses), pass@1. SFT runs are two epochs of TRL SFT on successful Qwen3.8-27B rollouts (FineEnvs/SmolDataEnvs-multiharness-sft); RL runs are the article's Async GRPO runs at their best evaluated checkpoint (step 900 for OpenCode only, step 700 for multi-harness), picked on this test set. Tool-call savings = 100 x (1 - sum(run calls) / sum(base calls)) over task/harness pairs solved by both the base model and the run; n = matched pairs. The base model has 998 of 1,000 cells graded. Source: SFT/RL handoff packet, 29 September 2026.",
+ "harnesses": [
+ "opencode",
+ "claude-code",
+ "codex",
+ "mini-swe-agent"
+ ],
+ "harness_labels": {
+ "opencode": "OpenCode",
+ "claude-code": "Claude Code",
+ "codex": "Codex",
+ "mini-swe-agent": "Mini-SWE-Agent"
+ },
+ "runs": {
+ "base": {
+ "label": "LFM base model",
+ "method": "base",
+ "data": null,
+ "pass": {
+ "overall": 42.2,
+ "opencode": 33.6,
+ "claude-code": 33.2,
+ "codex": 40.0,
+ "mini-swe-agent": 62.1
+ },
+ "savings": {
+ "overall": 0,
+ "n": 421
+ }
+ },
+ "sft_opencode_e1": {
+ "label": "SFT · OpenCode only, epoch 1",
+ "method": "sft",
+ "data": "opencode",
+ "epoch": 1,
+ "pass": {
+ "overall": 45.1,
+ "opencode": 51.2,
+ "claude-code": 28.8,
+ "codex": 42.4,
+ "mini-swe-agent": 58.0
+ },
+ "savings": {
+ "overall": 14.0,
+ "n": 310,
+ "opencode": [
+ 26.2,
+ 67
+ ],
+ "claude-code": [
+ -2.0,
+ 43
+ ],
+ "codex": [
+ 36.9,
+ 77
+ ],
+ "mini-swe-agent": [
+ -1.5,
+ 123
+ ]
+ }
+ },
+ "sft_opencode_e2": {
+ "label": "SFT · OpenCode only",
+ "method": "sft",
+ "data": "opencode",
+ "epoch": 2,
+ "pass": {
+ "overall": 47.5,
+ "opencode": 51.6,
+ "claude-code": 33.2,
+ "codex": 46.4,
+ "mini-swe-agent": 58.8
+ },
+ "savings": {
+ "overall": 8.5,
+ "n": 309,
+ "opencode": [
+ 17.1,
+ 61
+ ],
+ "claude-code": [
+ -10.5,
+ 46
+ ],
+ "codex": [
+ 31.7,
+ 80
+ ],
+ "mini-swe-agent": [
+ -4.8,
+ 122
+ ]
+ }
+ },
+ "sft_multi_e1": {
+ "label": "SFT · multi-harness, epoch 1",
+ "method": "sft",
+ "data": "multi",
+ "epoch": 1,
+ "pass": {
+ "overall": 38.3,
+ "opencode": 22.4,
+ "claude-code": 40.8,
+ "codex": 42.4,
+ "mini-swe-agent": 47.6
+ },
+ "savings": {
+ "overall": 23.5,
+ "n": 255,
+ "opencode": [
+ 22.6,
+ 28
+ ],
+ "claude-code": [
+ 20.4,
+ 57
+ ],
+ "codex": [
+ 44.3,
+ 69
+ ],
+ "mini-swe-agent": [
+ 11.0,
+ 101
+ ]
+ }
+ },
+ "sft_multi_e2": {
+ "label": "SFT · multi-harness",
+ "method": "sft",
+ "data": "multi",
+ "epoch": 2,
+ "pass": {
+ "overall": 43.1,
+ "opencode": 38.0,
+ "claude-code": 42.4,
+ "codex": 46.8,
+ "mini-swe-agent": 45.2
+ },
+ "savings": {
+ "overall": 24.2,
+ "n": 276,
+ "opencode": [
+ 18.0,
+ 46
+ ],
+ "claude-code": [
+ 27.1,
+ 57
+ ],
+ "codex": [
+ 41.8,
+ 74
+ ],
+ "mini-swe-agent": [
+ 10.8,
+ 99
+ ]
+ }
+ },
+ "rl_opencode": {
+ "label": "RL · OpenCode only, best checkpoint (step 900)",
+ "method": "rl",
+ "data": "opencode",
+ "step": 900,
+ "pass": {
+ "overall": 52.4,
+ "opencode": 56.0,
+ "claude-code": 43.6,
+ "codex": 48.4,
+ "mini-swe-agent": 61.6
+ },
+ "savings": {
+ "overall": 13.2,
+ "n": 342,
+ "opencode": [
+ 34.9,
+ 73
+ ],
+ "claude-code": [
+ 7.3,
+ 62
+ ],
+ "codex": [
+ 18.2,
+ 79
+ ],
+ "mini-swe-agent": [
+ 3.4,
+ 128
+ ]
+ }
+ },
+ "rl_multi": {
+ "label": "RL · multi-harness, best checkpoint (step 700)",
+ "method": "rl",
+ "data": "multi",
+ "step": 700,
+ "pass": {
+ "overall": 54.6,
+ "opencode": 51.2,
+ "claude-code": 48.8,
+ "codex": 53.6,
+ "mini-swe-agent": 64.8
+ },
+ "savings": {
+ "overall": 26.7,
+ "n": 361,
+ "opencode": [
+ 31.4,
+ 68
+ ],
+ "claude-code": [
+ 19.2,
+ 71
+ ],
+ "codex": [
+ 46.8,
+ 88
+ ],
+ "mini-swe-agent": [
+ 14.8,
+ 134
+ ]
+ }
+ }
+ }
+}
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/data/training-results.json b/content/articles/multi-harness-rl/app/src/content/assets/data/training-results.json
new file mode 100644
index 00000000..924147a0
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/assets/data/training-results.json
@@ -0,0 +1 @@
+{"harnesses":["opencode","claude-code","codex","mini-swe-agent"],"harness_labels":{"opencode":"OpenCode","claude-code":"Claude Code","codex":"Codex","mini-swe-agent":"Mini-SWE-Agent"},"evals":{"lfm_harbor_opencode":{"label":"LFM2.5-2.6B \u00b7 OpenCode only","points":[{"step":0,"overall":42.18,"correct":421,"graded":998,"complete":false,"h":{"claude-code":33.2,"codex":40.0,"mini-swe-agent":62.1,"opencode":33.6}},{"step":100,"overall":47.97,"correct":473,"graded":986,"complete":false,"h":{"claude-code":37.5,"codex":44.0,"mini-swe-agent":63.11,"opencode":47.54}},{"step":200,"overall":51.17,"correct":501,"graded":979,"complete":false,"h":{"claude-code":39.59,"codex":49.6,"mini-swe-agent":61.79,"opencode":53.78}},{"step":300,"overall":50.15,"correct":496,"graded":989,"complete":false,"h":{"claude-code":39.76,"codex":47.6,"mini-swe-agent":63.97,"opencode":49.38}},{"step":400,"overall":50.65,"correct":506,"graded":999,"complete":false,"h":{"claude-code":40.16,"codex":47.2,"mini-swe-agent":66.4,"opencode":48.8}},{"step":500,"overall":50.1,"correct":501,"graded":1000,"complete":true,"h":{"claude-code":37.2,"codex":49.2,"mini-swe-agent":64.8,"opencode":49.2}},{"step":600,"overall":51.8,"correct":518,"graded":1000,"complete":true,"h":{"claude-code":43.2,"codex":46.0,"mini-swe-agent":64.8,"opencode":53.2}},{"step":700,"overall":50.8,"correct":508,"graded":1000,"complete":true,"h":{"claude-code":40.8,"codex":51.6,"mini-swe-agent":60.4,"opencode":50.4}},{"step":800,"overall":51.2,"correct":512,"graded":1000,"complete":true,"h":{"claude-code":44.0,"codex":46.8,"mini-swe-agent":62.0,"opencode":52.0}},{"step":900,"overall":52.4,"correct":524,"graded":1000,"complete":true,"h":{"claude-code":43.6,"codex":48.4,"mini-swe-agent":61.6,"opencode":56.0}},{"step":1000,"overall":52.3,"correct":523,"graded":1000,"complete":true,"h":{"claude-code":42.0,"codex":43.2,"mini-swe-agent":66.0,"opencode":58.0}}]},"lfm_harbor_multi":{"label":"LFM2.5-2.6B \u00b7 multi-harness","points":[{"step":0,"overall":42.18,"correct":421,"graded":998,"complete":false,"h":{"claude-code":33.2,"codex":40.0,"mini-swe-agent":62.1,"opencode":33.6}},{"step":100,"overall":47.06,"correct":465,"graded":988,"complete":false,"h":{"claude-code":33.06,"codex":51.6,"mini-swe-agent":60.98,"opencode":42.62}},{"step":200,"overall":52.63,"correct":520,"graded":988,"complete":false,"h":{"claude-code":39.2,"codex":54.4,"mini-swe-agent":66.53,"opencode":50.42}},{"step":300,"overall":50.7,"correct":507,"graded":1000,"complete":true,"h":{"claude-code":43.6,"codex":55.6,"mini-swe-agent":63.6,"opencode":40.0}},{"step":400,"overall":51.0,"correct":510,"graded":1000,"complete":true,"h":{"claude-code":48.0,"codex":50.0,"mini-swe-agent":62.8,"opencode":43.2}},{"step":500,"overall":52.1,"correct":521,"graded":1000,"complete":true,"h":{"claude-code":47.6,"codex":53.2,"mini-swe-agent":62.0,"opencode":45.6}},{"step":600,"overall":50.7,"correct":507,"graded":1000,"complete":true,"h":{"claude-code":47.2,"codex":46.4,"mini-swe-agent":62.8,"opencode":46.4}},{"step":700,"overall":54.6,"correct":546,"graded":1000,"complete":true,"h":{"claude-code":48.8,"codex":53.6,"mini-swe-agent":64.8,"opencode":51.2}},{"step":800,"overall":54.0,"correct":540,"graded":1000,"complete":true,"h":{"claude-code":46.0,"codex":56.4,"mini-swe-agent":64.0,"opencode":49.6}},{"step":850,"overall":54.5,"correct":545,"graded":1000,"complete":true,"h":{"claude-code":50.4,"codex":53.2,"mini-swe-agent":63.2,"opencode":51.2}},{"step":900,"overall":52.0,"correct":520,"graded":1000,"complete":true,"h":{"claude-code":46.8,"codex":54.0,"mini-swe-agent":60.0,"opencode":47.2}},{"step":1000,"overall":54.2,"correct":542,"graded":1000,"complete":true,"h":{"claude-code":48.8,"codex":53.6,"mini-swe-agent":64.8,"opencode":49.6}}]},"qwen_harbor_opencode":{"label":"Qwen3.5-2B \u00b7 OpenCode only","points":[{"step":0,"overall":14.6,"correct":146,"graded":1000,"complete":true,"h":{"claude-code":16.8,"codex":16.4,"mini-swe-agent":14.4,"opencode":10.8}},{"step":100,"overall":24.5,"correct":245,"graded":1000,"complete":true,"h":{"claude-code":25.2,"codex":30.4,"mini-swe-agent":20.4,"opencode":22.0}},{"step":200,"overall":27.1,"correct":271,"graded":1000,"complete":true,"h":{"claude-code":28.4,"codex":30.8,"mini-swe-agent":20.0,"opencode":29.2}},{"step":300,"overall":28.5,"correct":285,"graded":1000,"complete":true,"h":{"claude-code":30.4,"codex":32.0,"mini-swe-agent":23.6,"opencode":28.0}},{"step":400,"overall":32.8,"correct":328,"graded":1000,"complete":true,"h":{"claude-code":33.6,"codex":33.2,"mini-swe-agent":28.8,"opencode":35.6}},{"step":500,"overall":33.0,"correct":330,"graded":1000,"complete":true,"h":{"claude-code":32.4,"codex":37.2,"mini-swe-agent":29.6,"opencode":32.8}},{"step":600,"overall":33.0,"correct":330,"graded":1000,"complete":true,"h":{"claude-code":31.2,"codex":34.4,"mini-swe-agent":30.8,"opencode":35.6}},{"step":700,"overall":39.5,"correct":395,"graded":1000,"complete":true,"h":{"claude-code":46.4,"codex":37.2,"mini-swe-agent":34.4,"opencode":40.0}},{"step":800,"overall":33.1,"correct":331,"graded":1000,"complete":true,"h":{"claude-code":38.8,"codex":35.6,"mini-swe-agent":31.6,"opencode":26.4}},{"step":900,"overall":29.6,"correct":296,"graded":1000,"complete":true,"h":{"claude-code":35.2,"codex":27.2,"mini-swe-agent":26.0,"opencode":30.0}},{"step":1000,"overall":26.4,"correct":264,"graded":1000,"complete":true,"h":{"claude-code":30.8,"codex":32.4,"mini-swe-agent":20.4,"opencode":22.0}}]},"qwen_harbor_multi":{"label":"Qwen3.5-2B \u00b7 multi-harness","points":[{"step":0,"overall":14.6,"correct":146,"graded":1000,"complete":true,"h":{"claude-code":16.8,"codex":16.4,"mini-swe-agent":14.4,"opencode":10.8}},{"step":100,"overall":24.8,"correct":248,"graded":1000,"complete":true,"h":{"claude-code":27.6,"codex":28.0,"mini-swe-agent":19.2,"opencode":24.4}},{"step":200,"overall":26.3,"correct":263,"graded":1000,"complete":true,"h":{"claude-code":30.0,"codex":26.4,"mini-swe-agent":18.4,"opencode":30.4}},{"step":300,"overall":28.6,"correct":286,"graded":1000,"complete":true,"h":{"claude-code":33.2,"codex":29.6,"mini-swe-agent":22.0,"opencode":29.6}},{"step":400,"overall":33.3,"correct":333,"graded":1000,"complete":true,"h":{"claude-code":36.8,"codex":34.8,"mini-swe-agent":28.8,"opencode":32.8}},{"step":500,"overall":37.0,"correct":370,"graded":1000,"complete":true,"h":{"claude-code":44.8,"codex":39.2,"mini-swe-agent":31.2,"opencode":32.8}},{"step":600,"overall":31.8,"correct":318,"graded":1000,"complete":true,"h":{"claude-code":30.0,"codex":32.4,"mini-swe-agent":30.8,"opencode":34.0}},{"step":684,"overall":32.1,"correct":321,"graded":1000,"complete":true,"h":{"claude-code":35.6,"codex":33.2,"mini-swe-agent":30.4,"opencode":29.2}},{"step":700,"overall":28.8,"correct":288,"graded":1000,"complete":true,"h":{"claude-code":31.6,"codex":32.0,"mini-swe-agent":30.4,"opencode":21.2}},{"step":800,"overall":27.0,"correct":270,"graded":1000,"complete":true,"h":{"claude-code":32.0,"codex":26.8,"mini-swe-agent":26.0,"opencode":23.2}},{"step":900,"overall":22.7,"correct":227,"graded":1000,"complete":true,"h":{"claude-code":36.4,"codex":19.2,"mini-swe-agent":32.8,"opencode":2.4}},{"step":1000,"overall":26.3,"correct":263,"graded":1000,"complete":true,"h":{"claude-code":38.8,"codex":24.8,"mini-swe-agent":36.0,"opencode":5.6}}]},"qwen_native_opencode":{"label":"Qwen3.5-2B \u00b7 native OpenCode","points":[{"step":0,"overall":15.9,"correct":159,"graded":1000,"complete":true,"h":{"claude-code":16.8,"codex":15.2,"mini-swe-agent":18.8,"opencode":12.8}},{"step":100,"overall":19.7,"correct":197,"graded":1000,"complete":true,"h":{"claude-code":20.4,"codex":18.0,"mini-swe-agent":20.8,"opencode":19.6}},{"step":200,"overall":22.1,"correct":221,"graded":1000,"complete":true,"h":{"claude-code":24.0,"codex":25.6,"mini-swe-agent":21.6,"opencode":17.2}},{"step":300,"overall":21.6,"correct":216,"graded":1000,"complete":true,"h":{"claude-code":29.2,"codex":16.8,"mini-swe-agent":19.6,"opencode":20.8}},{"step":400,"overall":26.4,"correct":264,"graded":1000,"complete":true,"h":{"claude-code":32.4,"codex":25.6,"mini-swe-agent":27.2,"opencode":20.4}},{"step":500,"overall":23.1,"correct":231,"graded":1000,"complete":true,"h":{"claude-code":26.4,"codex":18.8,"mini-swe-agent":29.2,"opencode":18.0}},{"step":600,"overall":25.1,"correct":251,"graded":1000,"complete":true,"h":{"claude-code":32.4,"codex":18.4,"mini-swe-agent":32.8,"opencode":16.8}},{"step":700,"overall":23.2,"correct":232,"graded":1000,"complete":true,"h":{"claude-code":29.6,"codex":14.4,"mini-swe-agent":30.4,"opencode":18.4}},{"step":800,"overall":25.6,"correct":256,"graded":1000,"complete":true,"h":{"claude-code":32.4,"codex":18.0,"mini-swe-agent":32.0,"opencode":20.0}},{"step":900,"overall":25.3,"correct":253,"graded":1000,"complete":true,"h":{"claude-code":34.0,"codex":14.4,"mini-swe-agent":34.0,"opencode":18.8}},{"step":1000,"overall":29.8,"correct":298,"graded":1000,"complete":true,"h":{"claude-code":33.2,"codex":29.6,"mini-swe-agent":36.0,"opencode":20.4}}]}},"efficiency":{"lfm_harbor_opencode":[{"step":0,"calls":8.87,"tool_savings":-0.0,"combined":0.4093,"out":3789,"in":154443},{"step":100,"calls":8.5,"tool_savings":3.74,"combined":0.4659,"out":3366,"in":144785},{"step":200,"calls":8.42,"tool_savings":3.26,"combined":0.4971,"out":3417,"in":140529},{"step":300,"calls":8.12,"tool_savings":6.24,"combined":0.4874,"out":3348,"in":135777},{"step":400,"calls":7.9,"tool_savings":5.91,"combined":0.4925,"out":3861,"in":128869},{"step":500,"calls":7.75,"tool_savings":7.33,"combined":0.4872,"out":4229,"in":128137},{"step":600,"calls":7.51,"tool_savings":12.48,"combined":0.5039,"out":3971,"in":125816},{"step":700,"calls":7.34,"tool_savings":10.32,"combined":0.4944,"out":4136,"in":124613},{"step":800,"calls":7.29,"tool_savings":12.97,"combined":0.4985,"out":4014,"in":125450},{"step":900,"calls":7.49,"tool_savings":13.19,"combined":0.5103,"out":4106,"in":130645},{"step":1000,"calls":7.67,"tool_savings":11.39,"combined":0.509,"out":3978,"in":130012}],"lfm_harbor_multi":[{"step":0,"calls":8.87,"tool_savings":-0.0,"combined":0.4093,"out":3789,"in":154443},{"step":100,"calls":8.03,"tool_savings":10.11,"combined":0.4576,"out":3027,"in":136202},{"step":200,"calls":7.62,"tool_savings":15.01,"combined":0.5122,"out":3064,"in":128780},{"step":300,"calls":7.09,"tool_savings":18.38,"combined":0.4939,"out":3374,"in":124960},{"step":400,"calls":6.71,"tool_savings":23.02,"combined":0.4973,"out":3188,"in":123967},{"step":500,"calls":6.91,"tool_savings":23.74,"combined":0.5079,"out":3374,"in":125621},{"step":600,"calls":6.83,"tool_savings":23.71,"combined":0.4946,"out":3306,"in":126831},{"step":700,"calls":6.79,"tool_savings":26.69,"combined":0.5328,"out":3336,"in":123712},{"step":800,"calls":6.57,"tool_savings":24.17,"combined":0.5269,"out":3190,"in":122444},{"step":850,"calls":6.41,"tool_savings":25.79,"combined":0.532,"out":3547,"in":126111},{"step":900,"calls":6.31,"tool_savings":28.56,"combined":0.508,"out":3364,"in":119374},{"step":1000,"calls":5.91,"tool_savings":31.07,"combined":0.5297,"out":3122,"in":110426}]},"final_by_harness":{"lfm_harbor_opencode":{"opencode":{"calls":5.0,"out":2778,"out_sav":35.3,"in":148425,"in_sav":32.4,"tool_sav":36.4},"claude-code":{"calls":8.61,"out":5080,"out_sav":-55.4,"in":247886,"in_sav":-9.2,"tool_sav":-9.6},"codex":{"calls":7.83,"out":4273,"out_sav":5.1,"in":75525,"in_sav":23.3,"tool_sav":23.6},"mini-swe-agent":{"calls":9.26,"out":3780,"out_sav":-6.5,"in":48210,"in_sav":-0.0,"tool_sav":2.1}},"lfm_harbor_multi":{"opencode":{"calls":4.64,"out":2505,"out_sav":37.9,"in":132281,"in_sav":27.2,"tool_sav":32.8},"claude-code":{"calls":6.32,"out":3580,"out_sav":16.8,"in":212614,"in_sav":24.8,"tool_sav":28.3},"codex":{"calls":4.78,"out":2831,"out_sav":39.7,"in":51735,"in_sav":45.9,"tool_sav":53.0},"mini-swe-agent":{"calls":7.89,"out":3574,"out_sav":16.6,"in":45072,"in_sav":11.2,"tool_sav":16.2}}},"source":"2026-09-24 run audit: data/evaluations.csv, lfm_evaluation_all_metrics.csv, lfm_eval_tokens.json, lfm_eval_efficiency.json","train_windows":{"qwen_harbor_multi":[{"start":1,"end":100,"reward":0.372,"correct":null,"calls":14.092,"zero_grad":17},{"start":101,"end":200,"reward":0.334,"correct":null,"calls":15.787,"zero_grad":41},{"start":201,"end":300,"reward":0.308,"correct":null,"calls":16.205,"zero_grad":34},{"start":301,"end":400,"reward":0.421,"correct":null,"calls":16.046,"zero_grad":49},{"start":401,"end":500,"reward":0.33,"correct":null,"calls":15.979,"zero_grad":36},{"start":501,"end":600,"reward":0.425,"correct":null,"calls":14.886,"zero_grad":30},{"start":601,"end":700,"reward":0.434,"correct":null,"calls":14.81,"zero_grad":36},{"start":701,"end":800,"reward":0.353,"correct":null,"calls":14.598,"zero_grad":39},{"start":801,"end":900,"reward":0.359,"correct":null,"calls":14.187,"zero_grad":35},{"start":901,"end":1000,"reward":0.41,"correct":null,"calls":12.231,"zero_grad":32}],"qwen_native_opencode":[{"start":1,"end":100,"reward":0.438,"correct":null,"calls":6.595,"zero_grad":34},{"start":101,"end":200,"reward":0.204,"correct":null,"calls":7.461,"zero_grad":58},{"start":201,"end":300,"reward":0.276,"correct":null,"calls":8.044,"zero_grad":57},{"start":301,"end":400,"reward":0.173,"correct":null,"calls":8.15,"zero_grad":66},{"start":401,"end":500,"reward":0.262,"correct":null,"calls":7.858,"zero_grad":63},{"start":501,"end":600,"reward":0.124,"correct":null,"calls":7.765,"zero_grad":59},{"start":601,"end":700,"reward":0.311,"correct":null,"calls":6.122,"zero_grad":51},{"start":701,"end":800,"reward":0.271,"correct":null,"calls":5.85,"zero_grad":64},{"start":801,"end":900,"reward":0.378,"correct":null,"calls":5.843,"zero_grad":59},{"start":901,"end":1000,"reward":0.187,"correct":null,"calls":6.795,"zero_grad":72}],"qwen_harbor_opencode":[{"start":1,"end":100,"reward":0.461,"correct":null,"calls":14.713,"zero_grad":35},{"start":101,"end":200,"reward":0.246,"correct":null,"calls":15.661,"zero_grad":39},{"start":201,"end":300,"reward":0.371,"correct":null,"calls":16.07,"zero_grad":39},{"start":301,"end":400,"reward":0.311,"correct":null,"calls":19.034,"zero_grad":33},{"start":401,"end":500,"reward":0.468,"correct":null,"calls":17.237,"zero_grad":48},{"start":501,"end":600,"reward":0.45,"correct":null,"calls":16.579,"zero_grad":52},{"start":601,"end":700,"reward":0.444,"correct":null,"calls":15.193,"zero_grad":43},{"start":701,"end":800,"reward":0.438,"correct":null,"calls":15.827,"zero_grad":26},{"start":801,"end":900,"reward":0.434,"correct":null,"calls":13.613,"zero_grad":31},{"start":901,"end":1000,"reward":0.339,"correct":null,"calls":17.967,"zero_grad":34}],"lfm_harbor_opencode":[{"start":1,"end":100,"reward":0.422,"correct":0.398,"calls":6.347,"zero_grad":24},{"start":101,"end":200,"reward":0.531,"correct":0.506,"calls":5.642,"zero_grad":14},{"start":201,"end":300,"reward":0.42,"correct":0.392,"calls":4.99,"zero_grad":25},{"start":301,"end":400,"reward":0.603,"correct":0.572,"calls":5.225,"zero_grad":14},{"start":401,"end":500,"reward":0.581,"correct":0.486,"calls":4.437,"zero_grad":21},{"start":501,"end":600,"reward":0.574,"correct":0.537,"calls":4.735,"zero_grad":18},{"start":601,"end":700,"reward":0.534,"correct":0.517,"calls":4.288,"zero_grad":16},{"start":701,"end":800,"reward":0.616,"correct":0.563,"calls":4.275,"zero_grad":17},{"start":801,"end":900,"reward":0.586,"correct":0.525,"calls":4.563,"zero_grad":24},{"start":901,"end":1000,"reward":0.502,"correct":0.472,"calls":4.466,"zero_grad":15}],"lfm_harbor_multi":[{"start":1,"end":100,"reward":0.389,"correct":0.366,"calls":9.091,"zero_grad":24},{"start":101,"end":200,"reward":0.408,"correct":0.447,"calls":8.365,"zero_grad":26},{"start":201,"end":300,"reward":0.404,"correct":0.376,"calls":7.744,"zero_grad":18},{"start":301,"end":400,"reward":0.593,"correct":0.537,"calls":7.101,"zero_grad":17},{"start":401,"end":500,"reward":0.48,"correct":0.474,"calls":6.848,"zero_grad":15},{"start":501,"end":600,"reward":0.513,"correct":0.533,"calls":7.015,"zero_grad":12},{"start":601,"end":700,"reward":0.515,"correct":0.459,"calls":6.873,"zero_grad":23},{"start":701,"end":800,"reward":0.542,"correct":0.533,"calls":6.433,"zero_grad":11},{"start":801,"end":900,"reward":0.501,"correct":0.527,"calls":7.556,"zero_grad":11},{"start":901,"end":1000,"reward":0.533,"correct":0.524,"calls":6.451,"zero_grad":20}]},"efficiency_by_harness":{"lfm_harbor_opencode":{"opencode":[{"step":0,"calls":6.53,"tool_savings":0.0,"out":3093.72,"in":185545.96},{"step":100,"calls":5.71,"tool_savings":26.15,"out":2364.44,"in":173387.08},{"step":200,"calls":5.29,"tool_savings":29.52,"out":2372.35,"in":156268.86},{"step":300,"calls":5.21,"tool_savings":31.19,"out":2408.17,"in":151739.17},{"step":400,"calls":4.97,"tool_savings":25.2,"out":2921.87,"in":143685.65},{"step":500,"calls":4.62,"tool_savings":32.87,"out":2841.91,"in":135981.25},{"step":600,"calls":4.58,"tool_savings":34.71,"out":2670.87,"in":132074.69},{"step":700,"calls":4.68,"tool_savings":32.34,"out":3005.56,"in":136644.12},{"step":800,"calls":4.58,"tool_savings":37.15,"out":2537.96,"in":133303.56},{"step":900,"calls":4.97,"tool_savings":34.94,"out":2974.21,"in":147205.87},{"step":1000,"calls":5.0,"tool_savings":36.38,"out":2777.86,"in":148425.1}],"claude-code":[{"step":0,"calls":8.86,"tool_savings":0.0,"out":4019.01,"in":277310.02},{"step":100,"calls":8.86,"tool_savings":-0.74,"out":3771.98,"in":261508.58},{"step":200,"calls":8.93,"tool_savings":-9.95,"out":3964.38,"in":268557.14},{"step":300,"calls":8.36,"tool_savings":1.88,"out":3725.35,"in":256445.38},{"step":400,"calls":8.29,"tool_savings":-2.22,"out":4764.33,"in":238418.59},{"step":500,"calls":8.7,"tool_savings":-5.23,"out":5975.64,"in":252144.56},{"step":600,"calls":8.09,"tool_savings":8.55,"out":5193.95,"in":246526.97},{"step":700,"calls":7.8,"tool_savings":6.65,"out":5619.05,"in":242006.2},{"step":800,"calls":7.95,"tool_savings":5.91,"out":5483.64,"in":250224.63},{"step":900,"calls":8.23,"tool_savings":7.29,"out":5246.19,"in":254825.68},{"step":1000,"calls":8.61,"tool_savings":-9.6,"out":5080.18,"in":247886.04}],"codex":[{"step":0,"calls":10.4,"tool_savings":0.0,"out":4459.1,"in":102182.63},{"step":100,"calls":9.99,"tool_savings":-3.4,"out":3994.8,"in":96358.69},{"step":200,"calls":9.96,"tool_savings":2.29,"out":3989.82,"in":92990.35},{"step":300,"calls":9.35,"tool_savings":6.76,"out":4000.72,"in":88880.64},{"step":400,"calls":9.06,"tool_savings":6.5,"out":4141.77,"in":86190.22},{"step":500,"calls":8.6,"tool_savings":11.13,"out":4484.01,"in":80059.25},{"step":600,"calls":8.17,"tool_savings":14.77,"out":4305.26,"in":78243.87},{"step":700,"calls":7.6,"tool_savings":17.08,"out":4108.38,"in":72044.34},{"step":800,"calls":7.5,"tool_savings":23.53,"out":4218.93,"in":71345.41},{"step":900,"calls":7.72,"tool_savings":18.24,"out":4428.21,"in":75233.69},{"step":1000,"calls":7.83,"tool_savings":23.58,"out":4272.7,"in":75525.38}],"mini-swe-agent":[{"step":0,"calls":9.69,"tool_savings":0.0,"out":3581.54,"in":51913.78},{"step":100,"calls":9.41,"tool_savings":1.21,"out":3309.46,"in":47161.23},{"step":200,"calls":9.37,"tool_savings":-2.22,"out":3300.79,"in":46106.8},{"step":300,"calls":9.51,"tool_savings":-1.54,"out":3230.19,"in":45895.17},{"step":400,"calls":9.29,"tool_savings":1.95,"out":3617.96,"in":47620.36},{"step":500,"calls":9.08,"tool_savings":0.63,"out":3615.98,"in":44363.44},{"step":600,"calls":9.18,"tool_savings":3.91,"out":3712.72,"in":46417.72},{"step":700,"calls":9.29,"tool_savings":-0.75,"out":3812.82,"in":47758.56},{"step":800,"calls":9.13,"tool_savings":-0.0,"out":3815.84,"in":46928.16},{"step":900,"calls":9.04,"tool_savings":3.38,"out":3776.42,"in":45313.01},{"step":1000,"calls":9.26,"tool_savings":2.08,"out":3779.54,"in":48209.76}]},"lfm_harbor_multi":{"opencode":[{"step":0,"calls":6.53,"tool_savings":0.0,"out":3093.72,"in":185545.96},{"step":100,"calls":5.38,"tool_savings":22.58,"out":2210.52,"in":144293.86},{"step":200,"calls":5.24,"tool_savings":32.21,"out":2316.58,"in":148560.65},{"step":300,"calls":5.32,"tool_savings":30.68,"out":2858.77,"in":152012.41},{"step":400,"calls":5.0,"tool_savings":24.06,"out":2587.73,"in":144485.47},{"step":500,"calls":5.65,"tool_savings":28.82,"out":2981.67,"in":147553.59},{"step":600,"calls":5.4,"tool_savings":31.57,"out":2719.2,"in":156557.34},{"step":700,"calls":5.48,"tool_savings":31.4,"out":2684.59,"in":127999.11},{"step":800,"calls":5.41,"tool_savings":23.24,"out":2647.42,"in":151503.35},{"step":850,"calls":4.94,"tool_savings":25.45,"out":2747.17,"in":143534.57},{"step":900,"calls":4.98,"tool_savings":34.75,"out":2799.6,"in":146666.36},{"step":1000,"calls":4.64,"tool_savings":32.82,"out":2504.92,"in":132281.34}],"claude-code":[{"step":0,"calls":8.86,"tool_savings":0.0,"out":4019.01,"in":277310.02},{"step":100,"calls":8.67,"tool_savings":2.89,"out":3543.17,"in":272247.56},{"step":200,"calls":7.76,"tool_savings":9.7,"out":3502.73,"in":243246.37},{"step":300,"calls":7.5,"tool_savings":12.12,"out":3839.65,"in":232185.16},{"step":400,"calls":7.26,"tool_savings":19.09,"out":3823.22,"in":237567.09},{"step":500,"calls":7.35,"tool_savings":19.54,"out":4118.37,"in":241486.18},{"step":600,"calls":7.44,"tool_savings":15.77,"out":3990.32,"in":238916.07},{"step":700,"calls":7.68,"tool_savings":19.23,"out":4108.99,"in":254298.79},{"step":800,"calls":7.02,"tool_savings":18.22,"out":3605.97,"in":228564.46},{"step":850,"calls":7.44,"tool_savings":15.64,"out":4299.11,"in":253773.16},{"step":900,"calls":6.98,"tool_savings":18.16,"out":4222.19,"in":226042.62},{"step":1000,"calls":6.32,"tool_savings":28.3,"out":3579.8,"in":212613.63}],"codex":[{"step":0,"calls":10.4,"tool_savings":0.0,"out":4459.1,"in":102182.63},{"step":100,"calls":8.91,"tool_savings":14.62,"out":3246.4,"in":83780.04},{"step":200,"calls":8.36,"tool_savings":19.97,"out":3101.64,"in":78487.79},{"step":300,"calls":6.75,"tool_savings":33.11,"out":3105.33,"in":67301.73},{"step":400,"calls":5.85,"tool_savings":41.38,"out":2820.76,"in":63789.99},{"step":500,"calls":6.13,"tool_savings":40.62,"out":2998.32,"in":64105.28},{"step":600,"calls":5.78,"tool_savings":43.69,"out":3055.0,"in":62636.46},{"step":700,"calls":5.64,"tool_savings":46.83,"out":3201.46,"in":64788.66},{"step":800,"calls":5.33,"tool_savings":49.93,"out":2959.12,"in":59031.36},{"step":850,"calls":5.08,"tool_savings":50.3,"out":3412.16,"in":58326.03},{"step":900,"calls":5.07,"tool_savings":52.4,"out":3084.22,"in":58155.75},{"step":1000,"calls":4.78,"tool_savings":52.98,"out":2830.58,"in":51735.27}],"mini-swe-agent":[{"step":0,"calls":9.69,"tool_savings":0.0,"out":3581.54,"in":51913.78},{"step":100,"calls":9.12,"tool_savings":5.66,"out":3094.76,"in":44299.87},{"step":200,"calls":9.02,"tool_savings":7.16,"out":3307.69,"in":44946.01},{"step":300,"calls":8.8,"tool_savings":6.95,"out":3691.65,"in":48339.44},{"step":400,"calls":8.72,"tool_savings":12.92,"out":3519.55,"in":50024.03},{"step":500,"calls":8.52,"tool_savings":12.61,"out":3396.23,"in":49340.8},{"step":600,"calls":8.71,"tool_savings":12.07,"out":3459.28,"in":49213.44},{"step":700,"calls":8.37,"tool_savings":14.85,"out":3350.36,"in":47760.59},{"step":800,"calls":8.54,"tool_savings":10.49,"out":3547.94,"in":50675.92},{"step":850,"calls":8.15,"tool_savings":15.6,"out":3729.26,"in":48810.95},{"step":900,"calls":8.2,"tool_savings":14.8,"out":3348.4,"in":46631.11},{"step":1000,"calls":7.89,"tool_savings":16.22,"out":3574.45,"in":45072.42}]}},"train_windows_by_harness":{"lfm_harbor_multi":{"all":[{"start":1,"end":100,"correct":0.397,"reward":0.423,"calls":9.09,"bonus":0.0267,"rollouts":537},{"start":101,"end":200,"correct":0.457,"reward":0.488,"calls":8.16,"bonus":0.0307,"rollouts":529},{"start":201,"end":300,"correct":0.418,"reward":0.448,"calls":7.74,"bonus":0.0296,"rollouts":521},{"start":301,"end":400,"correct":0.566,"reward":0.606,"calls":7.08,"bonus":0.0405,"rollouts":569},{"start":401,"end":500,"correct":0.506,"reward":0.542,"calls":6.88,"bonus":0.0353,"rollouts":545},{"start":501,"end":600,"correct":0.526,"reward":0.564,"calls":6.74,"bonus":0.0379,"rollouts":506},{"start":601,"end":700,"correct":0.504,"reward":0.54,"calls":6.61,"bonus":0.036,"rollouts":534},{"start":701,"end":800,"correct":0.573,"reward":0.614,"calls":6.39,"bonus":0.0415,"rollouts":569},{"start":801,"end":900,"correct":0.499,"reward":0.534,"calls":7.3,"bonus":0.0353,"rollouts":583},{"start":901,"end":1000,"correct":0.54,"reward":0.58,"calls":6.11,"bonus":0.0397,"rollouts":535}],"claude-code":[{"start":1,"end":100,"correct":0.292,"reward":0.311,"calls":9.26,"bonus":0.0192,"rollouts":144},{"start":101,"end":200,"correct":0.265,"reward":0.283,"calls":8.75,"bonus":0.0179,"rollouts":136},{"start":201,"end":300,"correct":0.322,"reward":0.346,"calls":7.42,"bonus":0.0234,"rollouts":149},{"start":301,"end":400,"correct":0.556,"reward":0.596,"calls":6.63,"bonus":0.04,"rollouts":124},{"start":401,"end":500,"correct":0.395,"reward":0.423,"calls":7.18,"bonus":0.0276,"rollouts":119},{"start":501,"end":600,"correct":0.425,"reward":0.455,"calls":7.85,"bonus":0.0299,"rollouts":146},{"start":601,"end":700,"correct":0.34,"reward":0.363,"calls":7.46,"bonus":0.0234,"rollouts":147},{"start":701,"end":800,"correct":0.401,"reward":0.43,"calls":6.5,"bonus":0.029,"rollouts":152},{"start":801,"end":900,"correct":0.52,"reward":0.554,"calls":8.24,"bonus":0.0345,"rollouts":127},{"start":901,"end":1000,"correct":0.466,"reward":0.501,"calls":6.64,"bonus":0.0345,"rollouts":163}],"codex":[{"start":1,"end":100,"correct":0.412,"reward":0.439,"calls":9.89,"bonus":0.0268,"rollouts":153},{"start":101,"end":200,"correct":0.462,"reward":0.492,"calls":9.42,"bonus":0.0301,"rollouts":106},{"start":201,"end":300,"correct":0.503,"reward":0.538,"calls":8.64,"bonus":0.0347,"rollouts":143},{"start":301,"end":400,"correct":0.528,"reward":0.568,"calls":6.37,"bonus":0.0401,"rollouts":127},{"start":401,"end":500,"correct":0.522,"reward":0.561,"calls":5.99,"bonus":0.0392,"rollouts":136},{"start":501,"end":600,"correct":0.611,"reward":0.656,"calls":5.9,"bonus":0.0453,"rollouts":144},{"start":601,"end":700,"correct":0.597,"reward":0.641,"calls":6.42,"bonus":0.0439,"rollouts":62},{"start":701,"end":800,"correct":0.533,"reward":0.574,"calls":5.04,"bonus":0.0408,"rollouts":120},{"start":801,"end":900,"correct":0.491,"reward":0.528,"calls":5.78,"bonus":0.0373,"rollouts":163},{"start":901,"end":1000,"correct":0.5,"reward":0.538,"calls":5.45,"bonus":0.038,"rollouts":156}],"mini-swe-agent":[{"start":1,"end":100,"correct":0.523,"reward":0.558,"calls":9.27,"bonus":0.0342,"rollouts":128},{"start":101,"end":200,"correct":0.706,"reward":0.751,"calls":9.08,"bonus":0.0447,"rollouts":136},{"start":201,"end":300,"correct":0.511,"reward":0.545,"calls":9.4,"bonus":0.0335,"rollouts":88},{"start":301,"end":400,"correct":0.589,"reward":0.627,"calls":8.82,"bonus":0.0383,"rollouts":192},{"start":401,"end":500,"correct":0.756,"reward":0.806,"calls":8.24,"bonus":0.0494,"rollouts":160},{"start":501,"end":600,"correct":0.694,"reward":0.741,"calls":7.71,"bonus":0.0468,"rollouts":72},{"start":601,"end":700,"correct":0.562,"reward":0.6,"calls":7.67,"bonus":0.0379,"rollouts":144},{"start":701,"end":800,"correct":0.691,"reward":0.739,"calls":7.84,"bonus":0.0475,"rollouts":175},{"start":801,"end":900,"correct":0.535,"reward":0.571,"calls":8.93,"bonus":0.0361,"rollouts":198},{"start":901,"end":1000,"correct":0.741,"reward":0.791,"calls":8.13,"bonus":0.0501,"rollouts":112}],"opencode":[{"start":1,"end":100,"correct":0.366,"reward":0.394,"calls":7.58,"bonus":0.0275,"rollouts":112},{"start":101,"end":200,"correct":0.404,"reward":0.434,"calls":5.91,"bonus":0.0302,"rollouts":151},{"start":201,"end":300,"correct":0.376,"reward":0.404,"calls":6.13,"bonus":0.0284,"rollouts":141},{"start":301,"end":400,"correct":0.579,"reward":0.624,"calls":5.6,"bonus":0.0448,"rollouts":126},{"start":401,"end":500,"correct":0.285,"reward":0.305,"calls":5.87,"bonus":0.0207,"rollouts":130},{"start":501,"end":600,"correct":0.458,"reward":0.493,"calls":5.95,"bonus":0.0343,"rollouts":144},{"start":601,"end":700,"correct":0.558,"reward":0.6,"calls":5.16,"bonus":0.042,"rollouts":181},{"start":701,"end":800,"correct":0.656,"reward":0.705,"calls":5.49,"bonus":0.0492,"rollouts":122},{"start":801,"end":900,"correct":0.411,"reward":0.442,"calls":5.24,"bonus":0.0313,"rollouts":95},{"start":901,"end":1000,"correct":0.5,"reward":0.539,"calls":4.09,"bonus":0.0392,"rollouts":104}]},"lfm_harbor_opencode":{"all":[{"start":1,"end":100,"correct":0.4,"reward":0.43,"calls":6.72,"bonus":0.0298,"rollouts":430},{"start":101,"end":200,"correct":0.506,"reward":0.543,"calls":6.0,"bonus":0.0374,"rollouts":437},{"start":201,"end":300,"correct":0.402,"reward":0.433,"calls":5.35,"bonus":0.0308,"rollouts":443},{"start":301,"end":400,"correct":0.578,"reward":0.622,"calls":5.47,"bonus":0.0442,"rollouts":450},{"start":401,"end":500,"correct":0.553,"reward":0.596,"calls":4.78,"bonus":0.0433,"rollouts":432},{"start":501,"end":600,"correct":0.544,"reward":0.587,"calls":4.96,"bonus":0.0425,"rollouts":439},{"start":601,"end":700,"correct":0.51,"reward":0.55,"calls":4.48,"bonus":0.0399,"rollouts":437},{"start":701,"end":800,"correct":0.567,"reward":0.611,"calls":4.71,"bonus":0.0443,"rollouts":434},{"start":801,"end":900,"correct":0.538,"reward":0.58,"calls":4.8,"bonus":0.0419,"rollouts":446},{"start":901,"end":1000,"correct":0.473,"reward":0.51,"calls":4.75,"bonus":0.037,"rollouts":433}],"opencode":[{"start":1,"end":100,"correct":0.4,"reward":0.43,"calls":6.72,"bonus":0.0298,"rollouts":430},{"start":101,"end":200,"correct":0.506,"reward":0.543,"calls":6.0,"bonus":0.0374,"rollouts":437},{"start":201,"end":300,"correct":0.402,"reward":0.433,"calls":5.35,"bonus":0.0308,"rollouts":443},{"start":301,"end":400,"correct":0.578,"reward":0.622,"calls":5.47,"bonus":0.0442,"rollouts":450},{"start":401,"end":500,"correct":0.553,"reward":0.596,"calls":4.78,"bonus":0.0433,"rollouts":432},{"start":501,"end":600,"correct":0.544,"reward":0.587,"calls":4.96,"bonus":0.0425,"rollouts":439},{"start":601,"end":700,"correct":0.51,"reward":0.55,"calls":4.48,"bonus":0.0399,"rollouts":437},{"start":701,"end":800,"correct":0.567,"reward":0.611,"calls":4.71,"bonus":0.0443,"rollouts":434},{"start":801,"end":900,"correct":0.538,"reward":0.58,"calls":4.8,"bonus":0.0419,"rollouts":446},{"start":901,"end":1000,"correct":0.473,"reward":0.51,"calls":4.75,"bonus":0.037,"rollouts":433}]}},"example_groups":[{"label":"All eight correct","harness":"claude-code","difficulty":"hard","step":80,"task":"0022_609_22609620_qa_3","rollouts":[{"correct":1,"calls":10,"reward":1.06},{"correct":1,"calls":4,"reward":1.0789},{"correct":1,"calls":6,"reward":1.0714},{"correct":1,"calls":4,"reward":1.0789},{"correct":1,"calls":9,"reward":1.0625},{"correct":1,"calls":8,"reward":1.0652},{"correct":1,"calls":5,"reward":1.075},{"correct":1,"calls":9,"reward":1.0625}]},{"label":"Mixed results","harness":"codex","difficulty":"medium","step":26,"task":"0105_060_105060710_qa_5","rollouts":[{"correct":0,"calls":12,"reward":0.0},{"correct":1,"calls":7,"reward":1.0682},{"correct":1,"calls":8,"reward":1.0652},{"correct":0,"calls":18,"reward":0.0},{"correct":1,"calls":16,"reward":1.0484},{"correct":0,"calls":7,"reward":0.0},{"correct":0,"calls":3,"reward":0.0},{"correct":1,"calls":6,"reward":1.0714}]}],"train_steps":{"lfm_harbor_opencode":{"step":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000],"reward":[0.0,0.0,0.662,0.0,0.5376,0.2679,0.0,0.3604,1.0811,0.5396,0.8005,0.0,0.0,0.0,0.0,0.8056,0.6726,0.802,0.5357,0.0,0.0,0.43,0.8002,0.8063,0.0,0.0,0.1339,0.797,1.0627,0.0,0.0,0.0,0.5344,1.0833,1.0812,0.0,0.0,0.8062,0.5395,1.0802,0.4325,1.072,0.86,0.0,0.0,0.0,0.0,0.2708,0.2656,0.4018,0.2697,1.0824,0.5354,1.0791,0.8114,1.0802,0.65,0.8082,1.0811,0.8082,0.5385,0.0,0.2708,0.5395,0.2708,0.0,0.0,0.2663,0.2656,0.3533,0.0,0.0,0.2143,1.0771,1.0679,1.0687,0.7966,0.8035,0.8082,0.8072,0.0,0.2708,0.2621,0.7205,0.8533,0.43,0.0,0.2136,0.0,0.2697,0.0,0.0,0.0,0.0,0.5395,0.8065,0.0,0.0,0.4294,0.5351,0.3963,0.8077,1.0786,1.0811,0.7702,0.9273,0.8613,0.2679,0.2656,0.0,0.0,0.2644,0.0,0.0,0.4002,0.795,0.0,0.267,0.5395,0.2697,0.0,0.8125,1.0822,0.9397,0.538,0.5344,0.8054,1.0745,1.0671,0.8022,0.5376,0.8072,0.8037,0.0,0.0,0.2697,0.0,0.0,0.6717,0.2708,0.2679,0.8474,0.7952,0.267,0.5313,0.0,0.0,0.0,0.0,0.0,0.8088,1.0791,0.5375,0.5375,0.5395,0.2697,0.0,0.8087,1.08,0.5387,0.0,0.4233,0.641,0.4261,0.0,0.2679,0.4031,0.8616,0.5385,0.8026,0.5385,0.2656,0.5417,0.8114,0.8093,1.0741,1.0728,0.6381,1.0626,1.0691,0.5342,0.0,0.0,0.0,0.0,1.0734,1.0757,1.0767,0.8617,0.7222,0.0,0.5395,1.0798,0.6734,0.2677,1.0724,1.0742,1.0753,0.5395,0.2679,0.5385,1.0748,0.8114,0.0,0.0,0.2697,0.805,1.0777,0.6755,0.0,0.1349,0.2697,0.0,0.0,0.0,0.5342,0.2687,0.0,0.0,0.0,0.0,0.0,0.0,0.5385,0.2708,1.0811,0.8125,0.0,0.5315,0.8045,0.1344,0.8046,0.5395,0.0,0.0,0.65,0.5417,0.0,1.0774,1.0761,0.7193,0.0,0.0,0.6411,0.2697,0.0,0.2167,1.0792,0.9463,1.08,1.0711,1.0703,1.0804,0.8084,1.0791,0.4012,0.5312,0.803,0.8093,0.8054,0.2679,0.0,0.2697,1.0811,0.5385,0.0,0.1349,0.6754,0.5406,0.5396,0.0,0.2697,1.0702,1.0772,0.213,0.4046,0.8077,0.0,0.2644,0.0,0.0,0.0,0.0,0.0,0.0,0.5375,0.267,0.8082,0.5385,0.267,0.8072,1.08,0.2697,0.0,0.2687,0.2687,0.0,0.0,0.0,0.0,0.5417,0.0,0.8632,1.0781,0.0,0.0,0.6411,0.2708,0.8105,0.8103,1.0822,1.0822,0.0,0.0,1.0782,1.0789,0.807,0.5396,0.8092,1.0833,1.0811,0.9468,0.8087,1.0771,1.0683,0.5375,0.863,0.2721,0.2708,0.0,0.0,0.5395,0.0,0.0,0.8042,0.8125,0.2708,0.0,0.6698,0.1344,0.5376,0.0,0.5291,0.2663,0.2687,0.0,0.1514,0.267,0.2154,0.9458,1.0802,0.5406,0.6743,0.8114,0.0,0.5289,0.2103,0.2656,0.0,1.0546,0.7017,0.0,0.0,0.0,0.2158,1.0771,0.8568,1.0789,0.5385,0.2697,0.0,0.0,0.2167,0.8115,0.0,0.1344,0.9393,1.0702,0.7962,1.0644,1.0789,1.0751,1.0825,1.0802,0.7176,0.4293,0.6755,1.0789,1.0802,1.0816,1.0667,1.0811,1.0796,1.0811,1.08,1.0822,1.0709,0.9319,0.8125,0.8114,0.8054,0.8045,0.0,1.0781,1.0822,1.0822,0.8103,0.6474,0.2687,1.0791,1.077,0.8065,0.5366,0.5395,0.2697,0.5376,0.6444,1.0814,0.0,0.0,1.0806,0.0,0.5401,1.0751,0.5385,0.267,0.6765,1.0811,0.9345,0.0,0.0,1.0822,1.0781,0.2697,1.0833,0.8125,0.2708,0.0,0.5417,0.5406,1.0833,1.0822,1.0833,1.0833,0.9383,0.5375,0.8092,0.5417,0.8104,1.0811,1.0833,0.4261,0.0,0.0,0.0,0.0,1.0756,0.8125,1.0812,1.0822,0.5406,0.5417,0.2708,0.0,0.0,0.0,0.0,0.8037,1.0734,0.8114,1.0802,0.5376,1.0804,0.902,0.2687,1.0737,1.0793,1.0811,0.2708,0.0,0.0,0.0,0.8104,1.0833,0.8005,0.0,0.0,0.2143,0.0,0.2097,0.0,0.0,0.0,0.4052,0.619,0.5357,0.6388,0.0,0.6419,1.0675,0.2708,0.2708,1.0774,0.8114,1.0825,1.0826,0.8126,0.8125,0.0,0.0,0.7222,1.081,1.08,1.0789,0.0,0.0,0.5357,0.2679,0.5342,0.8082,1.0712,1.078,1.0742,1.0811,0.5366,0.8037,0.0,0.5312,1.0757,0.5332,0.2687,0.0,0.0,0.8093,0.2692,0.8125,1.0833,1.0833,1.0833,0.1354,0.2167,0.8063,0.8125,1.0833,1.0725,0.5366,0.8026,0.8635,0.0,0.2708,0.5396,1.0772,1.0833,1.083,0.6735,1.0812,1.0801,1.0751,0.5376,0.4286,0.8055,0.5375,0.4301,1.0741,0.8037,0.0,0.0,0.2708,0.0,0.2708,0.2708,0.5406,0.0,0.2708,1.0825,1.0833,1.0791,0.5406,0.0,0.2143,0.8019,0.8038,0.0,0.2687,0.5313,0.2663,0.8095,0.4325,0.0,0.0,0.0,0.0,0.7991,0.1806,0.8125,0.0,0.6474,0.3548,0.6474,0.0,0.267,1.0687,1.0613,0.7938,0.4286,0.43,0.5387,0.8065,0.0,0.5358,0.2663,0.5341,0.5406,0.8076,0.8125,0.2697,0.0,0.5406,1.0822,1.0752,1.0777,0.5385,0.0,0.267,0.0,0.5395,0.2679,0.0,0.0,0.0,0.0,0.2687,1.0781,0.0,0.0,0.0,0.2708,0.5375,0.8092,0.5417,1.0833,1.0822,1.0811,0.5406,0.0,0.0,0.0,0.0,0.2663,0.5313,0.8063,0.4333,0.8092,0.8114,1.0833,1.0833,1.0833,1.0833,1.0833,0.9457,1.0806,1.0811,1.0812,1.0811,0.5417,0.2656,0.0,0.0,0.0,0.2708,0.6429,0.8095,1.0833,1.0806,0.5406,0.0,0.65,1.0833,0.5396,0.8062,0.0,0.2679,0.2697,0.5323,0.267,0.0,0.0,0.2167,0.8114,0.5406,0.0,0.5395,1.0782,0.5385,0.2697,0.2708,0.5417,1.0794,1.0744,0.6482,1.0833,0.8125,1.0833,0.5417,0.0,0.5429,0.0,0.4326,0.6397,1.0745,1.0833,1.0758,1.0723,0.5385,1.0755,0.8982,1.0732,0.9891,0.8037,0.5362,0.2629,0.8082,1.0811,0.4325,0.0,0.0,0.0,0.0,0.0,0.5396,1.07,0.5366,0.8114,1.0833,0.8093,0.4301,0.6771,0.1354,0.0,0.0,0.0,0.2697,0.0,0.5406,0.5385,0.8058,0.8125,1.0833,1.0833,1.0781,0.5352,1.0741,1.0833,0.5417,0.0,0.2125,0.5366,0.2687,1.08,0.5395,0.536,0.0,0.0,0.5406,1.0791,0.7553,0.2697,0.8059,0.5406,1.08,0.6452,0.8055,0.2687,0.2708,0.5395,1.0789,0.8632,1.078,0.8114,1.0822,0.5417,0.0,1.08,1.0807,1.0833,1.0833,0.9385,0.3584,0.2697,1.0811,0.5406,0.0,0.2687,1.0774,0.6476,1.0714,0.8082,0.0,0.0,0.5417,0.5417,1.0735,0.9393,0.2703,0.2687,0.2687,1.0833,1.0792,0.0,0.0,0.0,0.532,1.0717,0.8076,0.2708,0.0,0.8093,1.0811,0.0,0.0,0.8125,1.0833,0.3611,0.0,0.4308,0.8114,1.076,1.0817,1.0797,1.0821,1.0816,1.0833,0.1354,0.6419,0.8014,0.0,0.0,0.8098,0.1354,0.0,0.5406,0.8114,1.0763,1.0751,0.8114,1.0811,0.6467,0.8072,0.6716,0.53,1.062,0.2708,1.0827,0.2629,0.8558,0.1798,0.2167,0.267,0.2692,0.2697,0.0,0.0,0.0,0.5357,0.2679,0.7875,1.0822,1.0833,1.0822,1.0731,0.8082,0.0,0.0,0.5406,0.8125,1.0811,1.0833,0.0,0.0,0.801,0.802,0.1344,0.7208,1.0822,1.08,0.0,0.0,0.1332,0.2708,0.1806,0.8125,1.0833,1.0833,1.0833,0.8634,1.0817,0.6401,0.267,0.0,0.807,0.8093,1.0807,0.8667,1.0812,0.4301,0.0,0.2687,0.8032,0.8093,0.5406,0.5344,0.4308,1.0802,1.0736,1.0781,0.2708,0.0,0.0,0.4333,1.0833,0.9028,1.0833,0.5406,0.5406,0.8103,0.0,0.267,0.2656,0.0,0.5396,1.0793,0.8022,0.2708,0.5385,0.0,0.0,0.0,0.8073,1.0833,1.0796,1.073,0.0,1.0833,1.0822,0.8985,0.8039,0.536,0.5333,1.0745,0.8093,0.8082,0.8093,0.8088,0.9453,0.8028,1.0729,0.2687,0.0,0.5417,0.5417,0.5417,1.0833,0.2708,0.0,0.2708,0.0,0.0,0.0,0.0,0.9431,0.9371,0.2691,0.0,0.5417,0.8125,0.5406,0.8114,0.2708,0.0,0.8072,0.8632,0.2697,0.2697,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.4333,0.5387,0.6409,0.2125,0.0,0.0,0.0,0.804,0.0,0.2679],"correct":[0.2143,1.0,0.3333,0.6667,null,0.0,null,0.875,null,0.5,null,0.0,0.0,null,null,0.7333,0.0,null,0.0,null,0.0,0.875,null,0.0,null,0.125,0.875,null,0.0,null,0.25,null,null,1.0,0.0,null,0.625,null,0.75,null,null,0.4375,null,null,null,0.1818,0.0,null,0.2727,0.0,0.875,null,0.875,null,1.0,null,0.75,null,0.75,null,0.0,null,0.5,null,0.0,null,0.25,null,0.25,null,0.0,null,1.0,1.0,null,0.75,null,0.875,null,0.0,null,0.375,null,0.75,0.375,null,0.125,null,0.125,null,0.0,null,0.0,null,0.625,null,0.125,null,0.4286,null,0.375,1.0,1.0,null,0.75,null,0.5,null,0.0,null,0.125,null,0.0,null,0.625,0.375,null,0.375,null,0.0,null,1.0,null,0.875,0.375,null,1.0,null,0.875,null,0.625,null,0.25,null,0.125,null,0.0,null,0.75,0.25,null,0.75,null,0.25,null,0.0,null,0.0,null,0.0,1.0,0.5714,null,0.625,null,0.0,null,1.0,null,0.125,null,0.75,null,0.0,null,0.5,0.5,null,0.75,null,0.125,null,0.875,null,0.875,null,0.875,null,1.0,null,0.0,null,0.0,null,0.75,null,1.0,0.875,null,0.0,null,1.0,null,0.375,0.875,null,0.875,null,0.25,null,1.0,null,0.0,null,0.0,0.875,null,0.875,0.0,null,0.25,null,0.0,null,0.0,0.375,null,0.0,null,0.0,0.0,null,0.25,null,1.0,null,0.0,null,0.875,0.125,0.625,null,0.0,null,null,0.6,0.0,null,0.875,null,null,0.25,null,null,0.125,null,null,0.9333,1.0,1.0,null,0.875,null,null,0.6923,0.3333,null,1.0,0.5,null,0.0,null,0.875,null,0.0,null,0.5,0.625,null,0.0,null,1.0,null,0.25,null,1.0,0.0,null,0.125,null,0.0,null,0.0,null,0.25,null,0.625,null,0.25,null,1.0,null,0.0,null,0.25,null,0.0,null,0.0,null,0.6,0.0,null,null,0.1818,0.2,null,0.75,null,1.0,null,0.0,null,null,1.0,0.5714,null,null,0.9333,1.0,null,0.8182,1.0,null,0.75,null,0.5,null,0.0,null,0.25,null,0.0,null,0.875,null,0.0,null,0.75,0.25,null,0.125,0.375,null,0.0,null,0.25,null,0.625,1.0,null,0.5556,1.0,0.125,null,0.375,null,0.0,null,0.4667,0.0,null,0.0,null,0.875,null,0.875,null,0.125,null,0.0,null,1.0,0.0,null,0.75,1.0,0.875,null,null,1.0,1.0,null,null,0.5455,0.625,1.0,1.0,null,1.0,null,1.0,1.0,null,1.0,null,0.875,0.75,null,null,0.375,null,null,1.0,0.0,null,0.375,null,null,0.9,0.6667,null,0.375,null,0.625,null,null,0.8,0.0,0.0,0.5333,0.4444,null,0.6667,0.0,null,0.875,null,0.4667,0.0,1.0,0.75,0.0,null,0.4667,1.0,null,0.5,null,null,1.0,1.0,null,0.875,null,0.6667,0.0,null,1.0,null,0.125,null,null,0.0769,0.0,0.75,1.0,0.7857,null,1.0,0.25,null,0.0,null,0.0,null,0.875,null,null,0.75,0.25,null,0.7857,0.0,1.0,1.0,1.0,0.125,null,0.0,null,null,0.7692,0.6667,null,0.0,0.3333,0.0,0.1429,null,null,0.2857,0.5,null,null,0.2667,null,0.0,0.5333,null,0.0,null,0.8,null,1.0,1.0,0.5,null,0.5,0.5,null,null,0.6154,0.3333,null,0.2857,0.5,null,0.875,null,1.0,null,null,0.9,0.5,0.25,null,null,0.6923,0.3333,null,0.0,null,1.0,0.0,1.0,null,1.0,null,0.0,0.5,null,1.0,null,0.6667,1.0,null,0.125,null,0.75,null,1.0,null,0.625,1.0,1.0,0.875,null,0.375,null,0.5,null,1.0,null,0.0,null,0.0,0.125,null,0.5,null,0.0,null,1.0,null,0.875,null,0.0,null,0.875,null,0.0,null,0.5,null,0.625,null,0.0,null,0.0,null,0.375,null,1.0,0.125,null,0.625,null,0.0,null,1.0,null,0.7857,0.0,null,null,0.4,1.0,null,0.5,null,0.375,null,0.75,null,0.0,null,1.0,null,1.0,1.0,0.0,null,0.25,null,0.25,null,0.0,null,0.0,null,0.625,null,0.0,null,0.125,null,0.625,null,1.0,null,1.0,null,0.0,null,0.0,null,0.25,null,0.5,null,0.75,null,1.0,null,1.0,null,1.0,null,0.875,1.0,null,1.0,null,0.125,null,0.0,0.125,null,0.375,null,1.0,null,1.0,0.0,null,0.875,null,0.5,null,0.0,0.375,null,0.25,null,0.0,null,0.75,null,0.0,null,1.0,null,0.25,null,0.0,1.0,1.0,null,0.75,null,1.0,null,0.0,null,0.375,null,0.75,null,1.0,null,0.875,null,0.875,null,0.875,null,0.625,1.0,0.25,null,1.0,null,0.0,null,0.0,0.0,null,0.625,null,0.625,null,1.0,null,0.5,null,0.75,0.0,null,0.125,null,0.0,0.25,null,0.625,null,1.0,null,1.0,null,0.5,1.0,null,0.0,null,0.375,null,0.75,null,0.375,null,0.125,null,0.875,null,0.125,0.5,null,1.0,null,0.5,null,0.125,null,1.0,null,0.75,null,1.0,null,0.0,1.0,null,1.0,null,null,0.5385,0.375,0.3333,0.4,1.0,null,0.75,null,null,0.7,0.0,null,null,0.7333,1.0,null,0.25,0.25,null,null,0.5333,0.0,null,0.375,null,null,0.75,null,0.0,null,0.875,null,0.0,null,1.0,null,0.0,null,0.75,null,1.0,null,1.0,null,1.0,null,0.0,0.625,null,0.0,null,0.875,0.0,null,0.75,null,0.875,null,1.0,null,0.625,null,0.375,0.875,null,0.5556,0.5714,null,null,0.2143,null,0.0,0.1429,0.1,null,null,null,0.5,null,1.0,null,1.0,0.8333,null,0.0,null,0.625,null,1.0,null,null,0.1111,0.7143,null,0.125,null,0.9375,null,null,0.0714,null,0.4,0.0,null,1.0,null,1.0,null,0.75,null,0.375,null,0.0,1.0,0.75,null,1.0,null,0.0,null,0.625,null,0.75,null,0.5,null,1.0,null,0.375,null,0.0,null,0.875,null,1.0,null,0.0,0.625,null,0.125,null,0.125,null,1.0,null,0.5,null,0.125,null,0.0,null,1.0,null,1.0,0.5,null,1.0,null,0.75,null,0.5,null,0.875,null,0.75,null,0.75,0.625,1.0,null,0.0,null,0.625,null,0.75,null,0.0,null,0.125,null,0.0,null,0.125,1.0,0.875,0.0,null,0.75,null,0.625,null,0.0,null,0.875,null,0.125,null,0.0,null,0.0,0.0,null,0.0,null,0.5,null,0.375,null,0.0,null,0.375,null,0.25,null],"calls":[5.25,7.25,9.625,8.75,3.6667,8.5,6.8,3.5,3.5,9.25,6.5,4.125,10.25,2.6667,8.4,8.5,6.375,9.0,8.25,6.0,8.125,8.6,6.0,5.5,5.0,7.25,8.375,10.5,9.0,15.5,7.5,4.0,6.0,2.75,3.0,6.75,8.5,4.25,4.0,3.75,2.6,5.75,4.4,2.5,4.8,4.75,4.25,11.375,6.25,4.5,6.25,3.1111,10.3333,4.0,4.0,3.75,3.4,3.75,3.5,4.25,7.0,11.3333,6.5,3.5,4.75,6.3333,8.25,7.0,9.75,11.6667,10.5,9.5,6.1,4.25,7.5,7.0,7.5,6.25,4.5,6.0,10.5,7.5,5.125,4.4167,6.4,2.8,8.0,5.0,3.8,2.5,5.75,13.2857,11.25,4.2,5.0,4.75,6.25,4.75,6.0,4.25,7.375,4.375,4.625,3.5,3.8571,3.2857,5.2,6.0,6.5,6.0,4.5,7.5,5.25,6.75,6.25,9.625,9.75,5.5,3.5,7.0,7.75,3.25,2.75,5.625,3.875,5.25,5.0,5.25,6.6,6.5,3.75,4.25,5.0,7.0,13.0,6.5,3.0,2.75,4.625,3.25,3.75,10.0,8.75,6.75,6.25,3.8,5.0,4.4,4.5,3.375,4.0,4.0,4.5,4.25,4.0,6.0,7.75,4.0,3.5,4.5,3.75,8.6,9.8,8.6,6.5,7.25,5.125,6.2,9.25,5.0,4.5,7.25,5.0,3.25,3.75,5.0,5.75,7.0,8.75,6.25,6.25,4.4,8.25,11.0,5.8,5.5,4.75,4.0,4.0,5.1667,12.25,5.75,3.6,6.875,3.875,4.25,4.0,5.0,4.0,4.5,4.25,5.0,3.5,4.8333,5.75,6.5,6.0,4.25,4.25,2.0,2.75,5.125,8.0,10.0,9.8333,6.25,3.75,5.2,8.2,7.0,6.8,4.25,4.75,5.0,4.25,3.25,4.25,2.25,7.5,4.75,5.5,4.75,5.75,4.1667,3.0,3.4,2.5,6.75,4.25,3.5,4.0,5.0,4.0,5.8,4.5,1.25,2.4,3.875,3.375,3.75,6.25,5.75,3.75,3.75,4.0,3.125,8.0,5.125,4.0,4.25,4.0,6.75,7.75,3.5,3.0,3.0,2.5,3.875,3.25,3.0,7.0,7.25,6.5,4.25,3.2,3.5,4.0,3.0,4.5,12.25,9.25,6.0,5.5,5.75,5.875,4.25,6.25,4.25,8.0,8.5,4.75,3.5,6.5,5.75,7.25,7.5,5.0,5.75,2.25,2.0,3.0,4.5,3.4,4.25,5.75,6.0,6.2,4.5,3.25,3.0,3.0,3.25,3.0,4.75,4.0,3.5,4.25,4.0,3.25,3.0,3.5,3.25,3.5,4.5,7.25,8.75,3.4,2.75,4.5,2.0,2.5,3.5,4.25,5.0,7.0,3.5,6.25,4.6667,5.25,4.125,6.75,7.875,9.75,5.5,6.0,8.75,10.5714,6.75,5.4,3.375,3.75,3.25,3.375,3.75,5.75,9.5,6.0,6.625,9.875,11.8,12.0,10.75,7.5,5.75,7.0,4.5,5.8,3.75,4.0,8.75,9.75,6.25,8.4,3.375,3.6,5.0,4.875,6.125,8.5,8.0,4.0,5.0,3.2,3.75,4.25,5.7,4.375,4.0,3.5,3.4,6.5,3.375,3.625,3.5,3.5,3.25,6.125,7.875,2.75,3.5,5.25,5.25,5.0,4.25,3.25,3.25,4.0,3.6,4.75,3.5,4.25,5.0,7.25,4.25,3.25,6.25,3.8,3.3333,2.0,5.0,3.375,4.5,3.5,5.0,4.25,6.5,3.875,3.5,6.875,3.75,6.0,3.25,4.25,7.25,3.0,3.5,3.75,3.25,4.25,5.25,2.75,3.0,3.0,3.0,5.75,3.75,3.75,3.25,3.0,3.25,3.0,6.4,6.0,3.5,3.2,8.25,4.625,2.75,3.0,3.25,3.0,3.0,3.75,5.0,2.75,5.25,5.0,5.0,5.0,3.0,3.25,4.25,3.5,3.0,4.5,5.5,4.0,3.5,2.75,3.0,7.6,9.75,3.25,2.8,6.5,2.5,3.25,3.4,6.0,12.0,10.75,4.75,5.75,3.5,2.7143,3.5,5.4,6.25,5.2,7.0,4.5,2.25,4.5,2.75,2.8,3.1667,3.25,2.5,8.5,9.75,2.5,3.2857,3.75,4.0,7.5,6.0,5.75,6.25,4.5,3.75,5.2,4.25,5.25,3.125,5.0,5.0,10.0,9.0,4.75,7.0,6.75,4.25,3.5,3.75,4.5,3.25,3.0,3.0,2.25,3.75,3.8,4.75,3.25,3.0,5.4,4.5,5.5,3.8,2.0,2.25,3.5,4.5,3.0,2.5833,5.0,3.375,3.75,4.5,5.0,5.4,5.75,5.5,7.6,4.75,4.5,5.4,5.6667,4.0,1.75,2.25,3.25,3.5,2.75,3.5,3.0,3.0,3.5,3.0,3.0,4.4,6.0,4.75,9.0,6.0,7.5,8.0,3.5,3.2,9.75,9.4,7.6,6.25,8.0,8.6667,3.75,4.75,3.8,10.0,3.8,3.0,4.25,6.75,9.5,10.0,4.8,4.8,4.0,4.125,3.0,5.25,5.75,4.75,4.0,4.0,3.0,5.5,6.25,4.5,2.75,5.0,4.25,5.75,7.0,7.25,5.8571,3.75,5.75,4.0,3.5,3.75,2.75,5.25,4.25,4.0,3.5,3.0,2.25,3.25,3.75,3.75,3.0,3.25,3.25,3.25,3.5,4.5,5.8,5.2,6.75,8.75,4.0,4.8,3.5,3.25,2.75,3.0,3.0,2.75,3.0,3.375,3.625,3.5,3.5,3.5,6.0,6.0,5.125,4.0,4.4,3.25,6.2,4.5,3.0,3.125,3.0,3.0,3.0,3.0,4.0,5.25,3.875,4.875,4.25,7.25,6.25,3.25,2.0,2.4,2.75,3.5,3.75,3.75,4.2,3.5,4.25,3.0417,3.625,3.9,5.0,3.2,3.0,3.0,3.0,2.75,5.0,2.75,2.0,3.6,5.2,4.75,3.0,4.5,5.5,6.25,3.75,4.0,5.0,4.0,5.375,6.875,7.75,5.25,3.25,2.6,3.25,5.4,8.25,4.25,6.0,4.25,5.5,4.0,4.0,3.0,4.5,4.0,3.0,3.0,3.0,3.0,2.5,3.375,3.75,6.0,3.5,6.75,3.25,3.0,3.0,3.5,6.125,4.25,2.75,4.0,5.2,4.6,5.5,3.25,3.75,4.25,4.75,5.0,8.0,3.75,3.5,3.9,4.75,5.0,4.0,3.25,4.8,5.25,3.5,2.75,3.75,4.0,4.0,3.75,2.75,3.25,5.0,11.8571,3.5,3.4,3.0,3.0,5.0,4.6667,2.5,3.5,3.5,3.0,3.5,4.2,3.8,4.75,3.0,8.0,8.75,3.0,2.75,5.25,4.875,2.875,3.25,4.75,2.8333,3.8,2.0,3.5,4.8,7.75,5.75,4.375,2.5,3.0,3.5,3.5,6.5,2.5,2.75,2.5,4.5,4.25,4.8,3.75,4.8,3.4,3.8571,3.2857,3.4,3.0,4.375,6.4,6.5,4.4,5.25,3.875,2.375,3.0,2.75,2.75,4.5,5.0,3.25,3.25,4.4,5.0,5.0,7.375,8.75,2.75,3.1429,9.25,6.4,3.8333,5.0,9.0,3.5,5.0,5.2,3.0,9.0,5.5,7.5,11.75,3.125,3.0,3.0,5.4,4.5,7.0,6.5,3.0,2.75,3.5,2.75,3.75,4.25,7.5,7.5,3.25,3.3333,3.25,3.625,4.25,5.75,5.375,4.25,6.0,3.25,2.75,3.0,3.0,3.6,3.4,6.8,9.75,6.75,5.0,4.0,3.4,2.8,3.0,6.2,3.0,7.0,5.5,4.0,4.25,5.25,3.8,3.5,5.4,4.0,3.0,3.0,2.75,3.0,3.0,2.8333,3.0,3.625,4.625,3.5,3.75,4.25,5.75,8.2857,6.75,3.5,5.5,3.0,4.0,5.25,5.5,3.0,4.5,3.0,3.875,5.5,7.2,3.0,3.0,4.5,5.25,7.25,5.75,5.0,4.25,4.0,3.5,3.875,4.75,6.125,5.0,3.25,3.0,3.25,3.0,3.25,2.8,4.75,3.6,3.5,3.5,3.0,3.5,2.875,3.75,5.75,4.75,5.4,3.0,2.75,3.0,3.0,6.25,6.7778,4.25,4.0,4.25,3.75,6.75,6.25,5.75,7.25,9.0,5.4,4.0,2.6,3.75,5.6,10.4,4.25,3.75,5.25,5.25,5.4,6.75]},"lfm_harbor_multi":{"step":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000],"reward":[0.0,0.0,0.0889,0.5513,0.3526,0.8329,0.9746,0.5831,0.1765,0.0,0.7613,0.163,0.1339,0.9289,0.0,0.0,0.0,0.9342,0.7246,0.1155,0.0,0.0182,0.5307,0.2656,0.2663,0.0,0.6655,0.9355,0.0,0.9836,0.235,0.0,0.0,0.4265,0.1902,0.0,0.1366,0.1937,0.0,0.0,0.0,0.0,0.9359,0.463,0.9293,0.8042,0.8083,0.5355,0.9311,0.4446,0.2634,0.9347,0.267,0.0,0.1984,0.022,0.4251,0.0,0.0,0.6676,1.0636,0.8048,1.0741,0.5417,0.8103,0.0,0.0,0.4016,1.0696,0.3473,0.5342,0.0,0.0,0.0,0.3982,0.3999,0.3978,0.2639,0.3956,0.7654,1.0715,1.0676,0.5385,0.9415,0.2669,0.0,0.0,0.0,0.0353,0.0,0.25,0.3417,0.0,0.5238,0.5345,0.4006,0.2158,1.0833,1.0752,0.5362,0.6694,0.2687,1.0646,0.88,0.2644,0.2687,0.0,0.0,0.0,0.4917,0.4019,0.0,0.2107,0.2679,0.5101,0.0,0.0,0.0,0.0,0.5385,1.0752,0.4862,0.2744,0.0,0.0,0.0,0.9323,1.0648,1.0691,0.0,0.0,0.5323,0.1332,1.0661,0.8864,0.3957,0.4062,0.8093,0.5368,1.0722,1.0527,1.0626,1.0608,0.6071,0.0,0.0,0.1339,0.6162,1.0671,0.5757,1.0778,1.025,0.4691,0.1923,0.0,1.0607,0.8055,1.0707,0.7871,0.2652,0.7563,0.7864,0.0,0.0,0.0443,0.9368,0.6754,0.2687,0.6686,0.0,0.0,0.0,0.0211,0.1335,0.0,0.0,0.0,0.0,0.0,0.0,0.0791,0.5349,0.0,0.9275,0.2663,0.3969,0.1918,0.0,0.1784,0.0889,0.2374,0.5798,0.8991,0.0345,0.0,0.0,0.0,1.0733,1.0711,0.4556,0.8728,0.4568,1.0669,1.0678,0.6418,1.0741,1.0675,0.0,0.0536,0.2117,0.0,0.0,0.674,0.2687,0.3995,0.9318,0.265,0.2136,0.6679,1.0735,0.0,0.0,0.0,0.0,0.0,0.8312,0.224,0.7568,1.0698,1.0719,0.1962,0.0,0.2346,1.0246,0.0,0.5276,0.1339,0.2657,0.5376,1.0791,0.672,0.1344,0.0,0.0,0.4562,0.0,0.5402,0.8103,0.0,0.0,0.6707,0.8495,0.2653,1.0812,0.8125,1.0707,1.0695,0.2687,0.5395,0.0,0.7554,0.0,0.0,0.1328,0.0,0.0,1.073,0.8162,0.1891,0.0,0.0,0.2697,0.0,0.2629,0.0,0.0742,0.0,0.0,0.265,0.5839,0.0,0.2378,1.0756,0.8092,1.074,0.6735,0.1325,0.0357,0.0,0.0656,0.5308,1.0704,0.2125,0.2679,0.0,0.0,0.3644,0.188,1.0733,0.0,0.0,0.1322,0.8032,0.2162,0.0,0.795,1.0627,1.0656,1.0746,0.6735,0.1339,0.9935,1.0785,1.0761,0.8103,0.6725,0.943,0.8066,0.5385,0.8037,0.1069,0.0,0.279,0.0,0.0,0.3533,0.6844,1.0654,1.0776,1.078,0.9354,0.9382,1.0802,0.9379,0.3999,0.0,0.72,0.2663,0.0,1.0754,0.0,1.0775,0.0,0.2675,1.0771,1.0695,0.0,0.0,0.0,1.0624,0.5741,0.7486,0.2889,1.08,0.8074,0.2693,0.0,0.0,0.5321,1.0745,1.0779,1.0742,0.9377,1.0649,0.3395,1.0614,0.4165,0.9372,0.3506,0.1322,0.1325,0.8308,0.8987,1.0586,1.078,0.8015,0.1332,0.1322,0.5294,0.9277,0.9343,1.0692,1.0688,1.0576,1.0692,1.0825,0.8635,0.0,0.0,0.2639,0.5333,0.8092,1.0766,0.8092,0.1344,0.0,0.0,0.0,0.0,0.0,0.0,0.5406,0.0,0.0,0.4031,0.2136,0.5755,0.0,0.0,0.0,0.1939,1.0634,0.5317,0.5417,0.2167,0.0,0.323,0.7608,0.3215,0.0,0.43,0.476,0.707,0.8,0.2122,0.7973,0.5334,1.0622,0.9376,0.3988,1.0749,0.2571,0.0,0.1332,0.0,0.0,0.9355,0.4012,0.8926,1.0691,1.0744,0.8763,0.4018,0.0,0.8041,0.9381,0.2653,0.0896,0.5782,0.6693,0.769,1.0738,1.0743,0.8049,0.5349,0.5366,0.428,0.2136,0.7998,0.9378,1.053,1.0664,0.8958,0.0,0.0,0.1335,0.5407,1.076,0.5374,0.0,0.3571,0.8613,0.25,0.0,0.0203,0.8019,0.4042,0.0,0.0,0.0,0.0765,0.5366,1.0767,0.9108,0.7719,0.3561,1.0618,0.0,0.0,1.0608,1.0543,0.6719,0.2158,1.073,0.0,0.0,0.5309,0.8037,0.1531,0.0,0.0,0.5174,0.0,0.0,0.8059,1.0703,1.0735,1.074,0.8586,0.1798,0.2685,0.8063,0.5342,0.1514,0.0,0.2397,0.0,0.1654,1.0767,0.9877,0.0,0.215,0.3552,0.6696,1.0775,1.0759,0.0,0.0,0.0,0.2697,0.0,0.5332,0.6569,0.8072,1.0761,1.0711,1.0758,0.5417,0.2687,1.0822,0.716,1.0655,1.0776,1.0705,0.9307,0.8546,0.0,0.3068,0.2337,0.1011,0.258,1.0767,0.0,0.6614,1.0701,0.8125,1.0811,0.4042,0.4397,0.0,0.0,0.5368,0.0,0.2629,0.6692,0.6182,1.0713,0.0,0.0,0.9416,0.4029,0.5011,0.1704,1.0616,1.0695,1.0649,0.6743,0.0,0.6172,0.5406,0.1354,0.1325,0.9328,0.9299,0.2997,0.4286,0.9368,0.7996,0.2634,1.0665,0.5337,0.6669,1.0767,0.0371,0.0,0.0,0.0,0.0,0.1065,0.0,0.8123,0.3409,0.7441,0.2992,1.0772,0.0,0.0,0.0,0.1344,0.936,0.6579,0.0,0.0,0.6761,1.0811,1.0833,0.3331,0.0,0.5396,0.8072,1.0756,0.515,0.0,1.0671,0.0,0.0,0.0,0.0,0.5385,0.0,0.0921,0.1344,1.078,1.076,0.0,0.0,0.5319,0.0,0.1339,1.0694,1.067,0.8014,1.0791,1.0744,1.0695,1.0755,0.0,0.7906,1.0654,0.8243,1.0833,0.9422,0.301,0.0,1.0679,0.9417,0.0,0.0,0.0,0.0,1.0723,0.6745,0.2683,0.5278,0.0,0.0,1.0707,0.0,0.1332,0.7151,0.8104,0.1349,1.0776,0.1349,0.0,0.6227,0.0,0.0,0.6685,1.0653,0.5524,1.0698,1.0616,1.0677,0.0,0.1344,0.802,0.0,0.0,0.5363,1.078,0.997,1.0729,1.0704,0.9363,0.8017,0.1326,0.4122,0.0331,0.7977,0.4301,0.4316,0.67,0.4256,0.1637,0.0,0.0,0.2667,0.5314,0.806,0.6707,1.0791,1.0822,0.3072,1.061,0.9013,0.5345,0.9384,0.086,0.0,0.0411,0.1515,0.0,0.1786,0.8729,0.673,1.0119,0.3044,0.0341,0.2663,0.1199,0.0,0.4979,0.2708,0.6743,0.5406,1.0613,0.1322,0.6635,0.8068,0.6478,0.6056,0.6735,0.7982,0.8062,0.0,0.5387,0.8464,0.94,0.8095,0.2697,0.0,1.08,1.0746,0.8114,0.0,0.0,0.6714,1.0682,0.7491,0.5277,1.0706,0.2675,1.0754,0.886,0.2158,0.0,1.0683,1.0741,0.8034,0.0487,0.0,0.1469,0.466,0.3583,1.0746,1.078,1.0731,0.5385,1.0697,1.0706,0.2688,0.2481,0.6706,1.0746,0.3316,0.0,0.0561,0.4052,0.0,0.2679,0.0,0.9352,1.0765,1.0709,0.1339,0.8062,1.0705,0.8417,0.0,0.2656,0.267,0.0,0.0,0.0,0.0,0.6595,1.067,0.6655,0.0166,0.0,0.2857,1.0716,1.0578,0.6081,1.0675,0.2693,0.0,0.8112,0.2675,0.8125,0.5385,0.2708,0.2657,1.0501,0.8969,0.6239,0.864,0.464,1.0589,1.0607,1.0072,0.2675,0.4014,0.1339,0.8062,1.0705,0.8326,0.8011,0.1659,0.9458,1.0822,0.9388,0.8014,0.3829,0.0,0.0,0.7443,0.207,0.0,1.0595,0.2657,0.0356,0.0,0.1806,0.1349,0.0,0.1071,0.9771,1.0738,1.0771,1.0549,0.5695,1.053,0.0822,0.34,0.2668,0.803,0.5362,0.5349,0.6656,0.1325,0.1339,0.2683,0.9301,0.8033,0.0,0.0,0.0,0.4333,0.4001,1.0768,0.4891,0.0,0.6729,0.9378,0.938,0.5362,0.4027,1.0742,0.6694,0.9336,0.5746,0.4972,0.0,0.0,0.3993,0.0,0.0,0.103,0.1959,0.0,0.0,0.6676,0.2337,1.0602,0.7995,0.2675,0.2656,0.2043,0.0,0.0,0.5797,1.0757,1.0789,1.0753,1.0791,0.8056,0.0,0.2679,1.0745,0.9431,0.659,0.0,1.0044,1.0719,0.1783,0.2679,0.9423,1.0794,0.9019,1.0833,0.2708,0.0,0.8082,0.2697,1.0761,0.5358,1.0786,0.1354,0.0,0.1433,0.8015,0.0,0.0,1.0307,0.8707,1.0731,0.8894,0.9757,0.3949,0.0,0.0,0.2167,0.7222,1.0798,0.8092,0.0,0.213,0.5327,0.0376,0.0,0.0,1.0743,1.0737,1.0686,0.3328,0.2143,0.2885,0.6418,1.073,0.8041,0.0,0.0,0.8483,0.1349,0.8032,0.2708,0.0,1.0806,0.0,0.0,0.0,0.0,0.0,0.0,1.0737,1.0705,1.0648,1.0738,1.0748,0.0,0.0,1.0822,0.8095,0.4904,1.078,1.0767,0.8464,0.0],"correct":[0.0,0.5,0.3636,null,0.0,0.75,null,0.75,0.0,null,0.3125,null,null,0.4667,0.0,0.0,null,0.75,null,0.0,0.1429,0.0,0.5,null,0.0,0.5,1.0,0.0,null,0.5,0.0,0.0,1.0,0.6667,0.0,null,0.875,0.0,0.0,0.0,null,0.0,0.75,0.5,0.25,0.625,null,1.0,0.5,0.25,null,0.7778,0.1429,null,null,0.625,null,0.3333,null,0.0,1.0,0.8,0.25,null,0.0,null,null,0.4667,1.0,0.0,0.3636,0.0,null,0.2308,0.1667,0.4,0.4167,0.0,0.25,1.0,null,0.625,null,1.0,0.0,null,0.0,0.125,0.0,0.1667,0.5,null,0.25,null,0.875,0.0,0.875,null,0.875,0.25,0.5714,null,1.0,0.625,0.25,null,0.0,0.0,null,0.625,0.2857,0.125,null,0.375,null,0.0,0.0,0.0,null,1.0,0.5714,null,0.0,0.0,null,null,1.0,0.5,0.0,null,0.0,0.2857,1.0,1.0,0.25,null,0.625,0.75,null,1.0,null,1.0,null,0.0,null,0.0,0.5,null,0.9333,0.0,null,0.4,null,1.0,1.0,null,0.9091,0.8,0.375,null,0.6667,null,0.0,null,1.0,0.75,0.0,0.875,0.0,null,0.0,0.0,0.4286,0.0,null,0.0,0.0,null,0.0,null,1.0,0.0,null,1.0,0.0,0.75,0.0,1.0,1.0,0.125,null,0.875,null,0.0,null,0.125,null,1.0,1.0,0.5,null,1.0,1.0,0.8571,0.0,0.8889,0.0,0.4,null,0.0,null,0.1818,0.4545,0.2222,0.5,0.6667,0.3333,0.0,1.0,0.0,0.0,0.0,0.0,0.2222,null,0.5,null,1.0,null,0.25,1.0,0.4286,null,0.0,null,0.5,0.25,0.125,1.0,null,0.2857,0.0,null,0.125,0.125,null,0.75,null,0.0,null,0.875,0.5,0.875,null,null,1.0,1.0,0.4667,0.25,null,0.3333,0.0833,0.0,0.0,null,null,0.7778,0.0,0.0909,0.0,0.25,0.1111,null,0.0,0.125,0.0,0.0,0.25,null,0.125,null,1.0,0.875,null,1.0,0.25,0.125,0.0,null,0.5,1.0,null,0.1333,null,0.0,0.2857,1.0,null,null,0.0769,0.5,0.0,0.75,0.0,null,1.0,1.0,1.0,1.0,0.5,0.0,1.0,null,0.875,null,0.5,1.0,0.5,null,1.0,0.125,0.0,0.0,0.0,null,0.75,1.0,null,null,1.0,0.75,1.0,null,0.875,0.0,0.375,null,0.0,null,0.5714,0.0,0.5333,0.25,0.8889,null,0.8889,0.0,0.0,null,0.7143,0.8333,0.25,null,0.875,null,0.125,0.0,1.0,1.0,1.0,null,0.875,1.0,0.75,0.8571,0.0,0.0,0.1429,null,0.25,0.0,0.75,null,1.0,1.0,0.625,0.0,0.125,0.875,0.875,1.0,1.0,1.0,null,1.0,null,0.25,0.0,null,0.375,null,1.0,null,0.125,0.0,null,0.0,null,0.0,null,0.25,null,0.0,null,0.375,0.0,0.1429,0.0,0.0,null,1.0,1.0,0.375,null,0.0,null,0.625,0.875,0.0,null,0.5,0.5,0.6667,null,0.75,0.5,null,1.0,0.375,0.0,0.75,0.1111,null,null,0.0,null,1.0,0.25,1.0,1.0,0.0,0.6,null,0.25,1.0,0.625,0.125,0.25,0.375,1.0,0.875,null,0.75,1.0,0.375,null,0.375,null,0.875,1.0,1.0,null,0.0,0.0,0.125,0.0,1.0,1.0,0.0,null,0.625,null,0.0,null,0.375,0.875,0.0,null,0.0,0.0,0.8333,null,0.875,null,0.125,1.0,0.0,null,1.0,1.0,0.9,0.6154,0.0,null,0.0,null,0.75,null,0.0,null,0.25,0.0,0.0,null,1.0,1.0,0.875,null,0.125,null,1.0,0.375,null,0.125,null,0.0,1.0,1.0,null,0.125,null,0.375,0.375,1.0,1.0,0.0,0.1429,null,0.0,0.25,null,0.5,0.875,null,1.0,null,0.6875,null,null,0.875,null,1.0,1.0,1.0,0.75,0.0,null,0.2667,0.0,1.0,null,null,0.4,0.8889,null,null,0.5,null,0.0,null,0.1538,0.3333,null,null,0.75,0.25,null,null,0.875,null,0.375,0.375,null,1.0,1.0,0.125,null,null,0.75,null,0.0,0.875,0.875,0.375,null,1.0,0.6923,0.6667,1.0,1.0,0.5625,null,0.7273,0.0,0.0,null,null,0.125,0.0,0.5,1.0,0.2857,1.0,1.0,0.0,null,null,0.4667,1.0,0.5,0.0,null,0.625,null,0.8,0.1667,0.8,0.5,0.0,null,0.7857,1.0,0.0,0.5,0.0,0.0,null,0.2727,0.0,null,0.1667,0.0,0.6667,0.0,0.4,0.0,null,0.0,1.0,0.875,null,1.0,null,null,0.8889,0.8,0.375,0.75,0.5,0.2,1.0,0.0,null,0.5,null,0.5385,0.0,0.0,0.0,0.0,1.0,0.25,0.125,0.0,null,null,0.8,0.3333,0.5,0.875,0.0,1.0,null,0.0,0.25,0.0,null,null,0.8,0.8,1.0,1.0,0.1111,null,null,0.4615,0.5556,0.0,0.0,null,0.9375,null,null,1.0,0.75,0.625,0.25,null,0.625,null,0.625,null,0.7333,0.0,0.0,null,0.0,0.5,0.75,0.625,1.0,1.0,null,0.875,null,0.0,0.875,1.0,0.0,null,0.375,0.0,null,1.0,0.875,0.375,0.375,null,0.375,null,0.0,1.0,0.0,0.5,0.25,null,0.875,0.0,0.75,0.875,0.75,0.375,0.625,null,0.5,null,0.375,0.875,0.75,0.125,1.0,1.0,null,0.8889,0.4615,0.5556,null,0.5,0.8333,0.0,null,1.0,0.2308,0.7,0.0,null,1.0,1.0,1.0,0.25,0.0,0.5,0.625,0.0,null,1.0,null,0.5,1.0,1.0,null,0.25,null,1.0,1.0,0.0,0.25,0.5,0.125,null,null,0.4286,0.5,0.6,0.0,null,0.7857,1.0,0.0,0.1429,0.0,null,0.0,0.0,0.0,0.0,0.875,1.0,0.5,0.0,1.0,0.75,null,0.875,null,1.0,0.0,null,1.0,0.0,0.75,null,0.0,0.375,0.875,0.5,0.5,null,1.0,1.0,1.0,0.8333,0.0,0.5,0.0,1.0,null,0.5,null,0.875,1.0,null,0.875,0.5,0.0,0.0,0.625,null,0.125,1.0,1.0,0.3333,0.0,0.0,0.4286,0.0,0.125,0.0,1.0,1.0,1.0,0.75,1.0,1.0,0.125,null,0.625,1.0,0.5,null,0.125,0.25,0.25,0.625,1.0,0.0,null,0.125,null,0.125,1.0,0.625,0.125,null,0.875,1.0,0.75,0.0,1.0,0.875,0.5,0.625,null,0.0,1.0,0.1429,null,0.0,1.0,1.0,0.0,null,0.0,0.625,null,0.875,0.375,0.0,0.875,0.0,0.25,0.5,1.0,null,null,0.8667,1.0,0.0,null,1.0,1.0,0.75,0.0,null,0.875,null,1.0,0.125,1.0,0.875,1.0,null,0.0,null,0.625,null,1.0,0.5,1.0,0.0,0.25,0.0,0.25,0.25,0.75,null,1.0,0.875,null,0.5,0.0,null,0.25,null,1.0,0.0,0.0,1.0,1.0,0.125,0.0,0.8333,1.0,null,0.875,null,0.25,null,0.875,null,1.0,0.0,null,0.625,0.125,0.625,0.125,null,null,1.0,0.0,0.0,0.0,0.0,null,null,1.0,1.0,1.0,0.6667,0.0,0.3333,null,0.875,0.0,1.0,1.0,0.5,0.2308,null],"calls":[6.5,8.5,13.4667,15.75,12.4167,9.8214,5.4545,10.125,14.1667,8.1579,8.2286,11.2609,7.5417,8.5,5.3333,7.75,17.0,10.8039,9.1818,10.4865,10.2683,14.7184,9.8333,9.25,9.625,12.5,8.5208,7.0,12.625,8.3077,12.1389,7.5714,4.875,4.8333,8.3929,7.7273,7.641,8.7879,12.5263,11.24,9.3913,7.8,10.2059,8.4,6.3556,4.375,4.25,4.25,9.2083,10.875,12.25,6.625,8.5,5.5,11.7037,16.4062,10.4,10.9394,11.2121,7.9583,8.5,7.75,5.25,5.0,6.5,12.25,11.75,8.75,6.5833,12.6279,6.75,6.5,8.0,10.875,10.625,7.0,9.0,10.625,13.625,11.0556,6.2,7.3571,7.25,4.5,7.7917,11.814,8.8864,6.625,11.3667,11.8261,8.6765,12.1622,8.3333,13.25,10.875,8.125,6.7,2.4,4.5,6.0,6.125,3.75,8.125,11.0417,7.875,5.0,10.25,8.75,2.8,5.0417,8.875,5.375,8.4,9.85,8.6,4.8194,10.9565,14.2241,5.75,5.625,4.9444,7.5758,13.04,10.1951,11.5625,4.75,7.625,7.5,6.5,5.0,1.0,8.375,10.0,7.125,7.9583,9.5,5.125,3.75,7.5,6.0,12.625,8.9474,9.7826,7.4286,3.5,10.75,11.0,10.75,7.381,9.1923,4.125,9.4643,5.913,10.7179,12.1628,8.875,5.25,6.375,13.0,11.5,8.2917,5.8667,6.6875,8.1053,13.7708,5.75,6.5,9.625,7.25,12.25,11.0833,14.75,10.26,7.0,15.093,13.9487,7.0,5.5,16.7419,11.2609,15.5185,9.0,15.0,10.25,4.875,7.875,5.9545,4.1176,13.5556,6.2083,7.0556,5.9231,3.3333,14.9355,6.8182,8.1818,6.75,5.375,5.875,8.1905,5.3125,6.2143,7.375,7.4167,8.4,5.3077,7.4706,13.717,13.525,9.8,7.2,2.0,3.875,5.125,5.75,7.5833,7.625,5.7,5.375,5.4167,5.84,8.2424,7.125,12.2609,11.2292,8.4444,7.875,9.381,6.5,6.1667,12.9474,14.9167,5.3913,6.625,5.0,9.375,9.4583,12.2917,7.1667,3.75,7.125,6.25,5.5,7.0,3.3846,4.4444,2.8333,3.75,2.75,2.2,5.125,9.2667,8.375,3.5,4.5,5.8333,6.625,5.7917,2.5,1.75,7.3333,15.6,13.5,13.375,7.0,8.75,5.5714,7.1176,9.0588,11.2368,9.5833,4.25,6.5,11.625,12.6964,12.5909,12.3654,9.0,9.25,6.0909,8.1905,5.0,4.875,4.25,5.3,6.2083,7.875,14.8333,14.6279,11.375,11.125,6.1667,5.6,8.25,7.25,9.5,9.25,13.5098,5.5,14.5128,10.3793,11.25,5.375,3.8,3.6,8.125,8.75,7.375,5.25,4.5,7.125,4.8462,4.1667,5.0,3.5,4.625,4.2917,4.25,3.25,4.875,10.075,15.3276,12.7755,8.1724,5.15,10.4167,13.0,8.25,4.3636,4.25,6.9583,6.0,3.75,6.25,5.5,4.375,5.5,11.875,11.875,4.7917,2.0,4.75,9.25,7.25,4.5,6.375,8.3529,11.84,8.625,9.0,8.375,3.3077,2.8,3.75,4.25,6.25,1.8571,6.3333,5.0625,5.1,4.2727,5.3125,5.75,8.2105,10.8537,9.5641,10.9756,6.3438,12.3333,9.5,7.875,6.5833,6.9474,11.2292,4.2917,6.375,6.5,13.75,10.0,9.5,7.375,6.25,6.8182,11.8148,6.8333,3.0,4.1333,9.9167,8.8,12.5,7.5,4.5,4.6154,4.6875,6.75,8.75,9.0,11.25,11.2,6.25,4.25,2.75,3.75,7.5,5.125,7.8,11.0909,5.4211,11.3103,7.0588,9.4091,8.375,5.375,3.75,3.4,7.25,4.5,5.0,4.7,7.0,6.1333,9.2632,8.7143,8.75,10.3,8.375,8.875,9.125,6.375,9.5,5.0,14.8667,15.8936,6.875,4.0,4.0,6.125,5.4167,5.9167,7.05,5.25,6.2273,5.0,9.0,6.875,6.625,7.5833,4.9167,6.2917,7.25,4.1429,5.3846,5.2083,6.625,5.25,9.0,4.4,4.6,5.75,6.125,14.1667,7.625,10.1538,6.625,4.8333,5.6667,3.625,4.8333,5.25,4.0,6.1667,9.625,9.1471,10.093,10.9038,7.75,4.5,13.25,12.9333,12.7241,9.7143,3.5,4.5714,4.3846,4.6667,8.2,8.1,3.3077,3.8125,9.625,12.375,5.0,5.0,5.4375,5.6,3.75,7.25,6.25,1.7143,1.8,8.4643,9.5429,6.0,6.8,4.25,5.75,5.45,5.28,4.9333,5.6667,7.5833,4.875,5.75,6.0,7.3103,9.75,10.7838,8.6538,4.5714,4.1667,5.0,4.1,5.875,6.375,4.0417,4.6667,12.5,10.0238,2.0,4.0,11.6667,9.5,14.0,4.25,4.5,5.9583,4.75,3.0,2.75,3.25,5.8889,8.2,3.875,6.375,8.25,5.4,2.8333,13.625,9.4688,15.7547,14.1321,4.5833,5.25,7.4762,6.4286,3.25,3.5,11.5517,11.7241,3.5,6.75,7.5,10.0,12.75,6.25,8.1818,6.0714,4.75,3.75,4.5625,3.75,7.9412,9.3684,9.9583,6.5833,8.1667,6.6316,4.75,4.0,3.0,5.125,5.75,7.75,8.125,3.5556,10.2857,7.25,7.75,14.25,7.25,6.875,8.5,4.25,8.0345,10.0238,10.1429,9.2917,9.3125,11.7333,6.625,6.76,8.44,11.1765,9.84,4.5,4.75,6.8,3.5,2.875,6.625,13.5,5.1,9.8148,3.375,3.25,3.0,9.3143,14.02,4.0,4.0,4.8696,4.5217,7.0,7.25,1.6667,3.5,8.25,2.75,4.25,11.6552,12.7714,7.0,3.25,3.5,5.72,5.8333,7.25,7.75,6.875,6.625,7.5,5.5,4.0,5.0,6.625,4.625,14.5769,13.5,8.25,8.0,3.0,4.25,10.8095,6.0,6.875,4.1875,14.2708,15.74,7.6,6.5714,5.75,3.5,6.5,8.4615,8.3333,2.0,6.125,7.25,4.5,7.875,3.0,2.25,4.25,6.375,13.0227,9.2941,6.5714,5.0,6.5,8.0571,11.0,6.375,9.8974,7.25,6.875,4.375,6.9167,2.2,2.8,5.0,4.0,4.8571,5.6429,6.3846,6.0,6.875,16.4375,14.3171,14.6562,7.0,4.9,3.2,5.125,8.2,5.9231,12.6571,10.9091,8.0,8.375,6.125,6.0,3.875,3.25,12.1667,10.1538,8.1795,7.125,5.75,13.14,6.9655,5.7308,4.9286,3.5,5.5833,4.8125,4.8333,13.2,11.9556,15.5484,9.125,2.2222,5.0,8.8214,6.0,3.625,3.0,10.125,6.25,7.375,6.0,6.6,4.4375,4.375,7.0,4.75,5.0,4.25,6.875,4.5,3.25,4.1,4.5455,3.375,5.125,3.75,7.875,11.0952,5.4167,7.0,11.4516,12.25,5.625,4.1667,4.9565,5.3043,5.36,4.5833,6.875,5.0,8.125,8.0,13.0,5.6818,8.3125,7.0,5.1333,4.25,5.4375,5.875,6.0,6.0,4.25,5.4615,5.375,5.125,6.8462,8.8333,5.2632,3.625,3.6,4.0,3.2083,6.375,4.6364,6.25,6.375,5.0,6.3125,5.8571,7.5,10.25,9.25,7.5833,7.5,7.125,6.5417,10.0,7.0,9.0,11.5,6.0,10.4667,6.0,11.0625,10.5,7.45,3.125,4.0,4.5417,4.4583,3.0,6.5,3.625,7.75,9.5287,8.5625,6.125,6.8571,3.6429,10.5909,10.0811,5.25,10.625,7.875,10.5,5.5,6.3333,6.2222,7.05,7.8462,3.5,3.25,6.25,6.125,3.7917,11.0833,12.875,6.3667,4.8095,7.2632,10.125,9.5833,12.9,11.4737,5.2143,5.875,5.875,10.7,5.6364,5.3571,4.4167,12.6765,11.9231,13.6111,5.8462,9.2857,6.625,6.9583,6.125,6.5,8.75,9.75,8.875,6.125,8.75,5.3333,3.0,3.2,7.4,7.2,12.875,4.25,6.7083,3.5,4.25,5.75,5.625,6.25,5.75,5.125,6.5,7.125,12.1515,12.5882,9.75,11.0,8.875,8.0,12.4186,9.8571,5.6364,6.45,5.6,5.5625,6.5652,9.963,8.625,6.625,7.625,9.6154,12.7273,11.2188,5.5,4.8667,4.0,5.0,4.0,4.625,10.1667,7.0,5.25,6.0,13.25,13.0,5.4333,5.9091,14.3056,5.0,4.575,3.75,3.375,3.0,3.75,3.75,3.75,4.25,4.625,5.625,3.75,2.125,11.7931,11.6333,6.25,11.0167,17.0,9.5652,5.6875,5.5455,6.8333,6.2121,8.875,3.3333,3.75,3.4,3.0,3.8,3.75,5.2941,4.4,6.5,14.614,9.4194,7.5455,5.2857,5.4667,6.913,13.0667,11.6,8.3077,6.4,5.5,4.75,4.0,3.6,5.5,4.125,5.0,4.75,3.0,3.375,6.3333,3.0,3.75,6.9643,10.5357,5.625,5.25,6.125,8.125,5.3571,5.0769,8.5417,11.2105,2.5,3.5,4.5455,4.25,4.5714,6.2917,7.8]},"qwen_harbor_opencode":{"step":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000],"reward":[0.8,0.25,0.0,0.3889,0.8333,0.2857,0.6667,0.7143,0.75,0.5,0.4286,1.0,0.5714,0.5,0.1667,0.0,0.7143,0.0,0.4,0.6667,0.0,0.0,0.0,0.0,1.0,0.75,1.0,0.75,0.0,0.3333,1.0,0.75,0.75,0.8571,0.0,0.2857,0.8889,0.5714,1.0,1.0,1.0,0.75,0.0,0.0,0.0,0.0,1.0,0.5,0.75,0.4444,1.0,1.0,1.0,0.7143,1.0,0.0,0.0,0.8571,1.0,0.25,0.8333,1.0,1.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5,0.0,0.2,0.5714,0.6667,1.0,0.0,0.0,0.0,0.25,1.0,0.6,0.7778,0.0,0.3636,0.2143,0.0,0.0,0.75,0.4167,0.0,0.4,1.0,0.75,0.9,0.1429,0.0,0.1667,0.25,0.0,0.0,0.0,0.0,0.5556,0.0,0.75,0.75,0.0,0.0,0.0,0.0,0.75,0.5,0.3333,0.5556,0.3,0.0,0.0,0.0,0.0,0.0,0.6,0.0,0.0,0.0,0.2,0.3333,0.25,0.5,0.0,0.0,0.0,0.0,0.0,0.1538,0.3846,0.0,0.0,0.0,0.0,0.0,0.2222,0.75,0.2727,0.0,0.0,0.0,0.0,0.5556,0.1429,0.2727,0.4,1.0,0.5,0.0,0.2857,0.3333,0.7778,0.0,0.6667,0.5714,0.0,0.0,0.0,0.0,0.0,0.0,0.2,1.0,0.5,0.8,1.0,0.0,0.7857,0.0,0.25,0.5,0.2222,0.0,0.2727,0.5294,0.5,0.6667,0.0,0.0,0.0,0.1429,0.2222,0.0,0.0,0.0,0.6364,1.0,0.2,0.0,0.0,0.0,0.15,0.0833,0.1818,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.6364,1.0,0.3333,0.0,0.0,0.0,0.2222,0.5,0.7,1.0,0.5455,0.7692,0.0,0.0,0.8182,0.8,1.0,1.0,0.8333,1.0,0.8889,0.7273,0.7857,0.7273,0.25,0.0,1.0,1.0,0.6667,0.5,0.875,1.0,0.2222,0.0,0.0,0.0,0.0,0.1111,0.1429,0.0,0.5455,1.0,1.0,1.0,0.5714,0.0,1.0,0.0,0.75,1.0,0.4286,0.8,0.0,0.0,0.0,0.1111,0.0,0.0,0.0,0.0,0.0,0.5,0.25,0.25,1.0,0.5556,0.0,0.0,0.3333,0.3,0.6667,0.5,1.0,0.5556,0.2222,0.0,0.5,0.2222,0.0,0.0,0.0,0.0,1.0,0.25,0.0,0.0,0.3,0.4545,0.0,0.0,1.0,0.7273,0.0,0.0,0.5,0.3333,1.0,1.0,0.7,1.0,0.5,0.6667,1.0,0.375,0.0,0.0,0.0,0.1667,0.0,0.0,0.75,0.6667,0.4,0.2857,0.1818,0.3,0.0,0.0,0.0,0.0,0.6,0.75,0.0,0.4286,0.2222,0.2222,0.0,0.1429,0.0,0.4,0.0,0.0,0.25,0.2222,0.5,0.0,0.5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25,0.4167,0.0,0.0,0.1667,0.0,0.4,0.4444,0.1818,0.7273,1.0,0.4,0.0,0.2222,0.0,0.2857,0.0,0.1429,1.0,0.5556,0.3333,0.5714,0.0,0.3333,0.75,1.0,1.0,0.2857,0.0,0.0,0.0,0.4,0.4444,0.25,0.0,0.0,0.0,0.5,1.0,0.75,1.0,0.25,0.0,0.4444,1.0,0.3636,0.0,0.2857,0.4545,0.0,0.0,0.0,0.0,0.4,0.0,0.5,0.7273,0.5,0.2727,1.0,1.0,0.3571,0.0,0.0,0.7778,1.0,1.0,0.6667,0.75,0.6667,0.6,0.8889,1.0,0.5556,0.6,0.0,0.0,0.2222,1.0,0.75,0.0,0.0,0.0,0.5833,1.0,0.75,0.5,0.5,0.6667,1.0,0.4,0.7273,0.5,0.0,0.0,0.0,0.0,0.5,0.3333,0.0,0.0,0.5,0.4545,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.7273,1.0,0.0,1.0,1.0,1.0,0.8182,1.0,0.4545,1.0,1.0,1.0,1.0,1.0,1.0,0.0,0.0,0.5,0.4,0.0,0.0,1.0,0.8,0.75,0.6667,0.0,0.0,0.0,0.2143,0.0,1.0,1.0,1.0,1.0,0.2,0.0,0.0,0.0,1.0,1.0,0.0,1.0,1.0,0.4545,0.25,1.0,0.2727,0.0,0.0,1.0,0.7273,0.0,0.0,0.0,0.0,0.0,0.0,0.7273,1.0,1.0,0.75,0.0,0.625,0.0,0.2,0.75,0.5556,0.4545,1.0,0.6,0.125,0.0,0.7692,0.5455,0.0,0.0,0.0,0.0,0.6667,0.5455,1.0,1.0,1.0,1.0,0.0,0.0,0.6,0.7,0.0,0.75,1.0,0.0,0.0,0.1818,0.0,0.25,0.3,0.0,0.0,0.7,0.8,0.75,0.75,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,0.7273,0.75,0.8,0.75,1.0,0.7273,1.0,0.75,1.0,1.0,0.4,0.5,0.1429,0.0,0.0,0.0,0.4444,1.0,0.0,0.0,0.75,0.1667,0.5,0.8667,1.0,0.6,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.8,0.8182,0.0,0.0,0.0,0.0,0.0,0.0,0.1818,0.0,0.75,1.0,0.0,0.0,0.0,0.25,0.3333,1.0,0.7059,1.0,1.0,0.5714,0.6667,0.2105,0.8,0.0,0.1111,0.2857,1.0,1.0,0.85,1.0,1.0,0.375,0.0833,0.0,0.6667,0.8,1.0,1.0,0.7273,0.5556,0.4,0.0,0.0,0.5,1.0,0.5,0.0,0.0,0.0,0.1562,0.1176,0.0,0.0,0.0,0.0,0.3636,0.5455,0.2105,0.0,0.5833,0.9,0.1429,0.3125,0.0833,0.4545,1.0,0.4444,0.5385,0.8333,1.0,1.0,0.75,1.0,1.0,0.0,0.0,0.0,0.0,0.0,0.1,1.0,1.0,1.0,1.0,0.0,0.0,0.0,0.0,1.0,1.0,1.0,0.4375,0.3333,0.0,0.875,0.75,1.0,1.0,0.3333,0.75,1.0,1.0,1.0,1.0,0.8,0.625,0.5333,0.0,0.0,0.093,0.6667,0.2778,0.0,0.2857,1.0,1.0,1.0,0.8,1.0,1.0,0.75,0.5556,0.4167,0.0,0.0,0.0,1.0,0.2632,0.2,0.1667,0.1667,0.7143,0.6111,0.5,0.3,0.0,0.7692,0.0,1.0,0.1333,0.0,0.0,0.0,0.2222,0.5,0.619,0.0,0.6875,1.0,0.8,0.0,0.625,0.25,0.0,0.0,0.1304,0.7143,0.5,0.0,0.2381,1.0,0.25,0.6667,0.2222,0.0,0.2,0.0,0.7273,1.0,0.8889,0.1333,0.0,0.25,0.2222,0.0,0.5455,0.25,0.0476,0.0,0.0476,0.0,0.0,0.0,1.0,1.0,1.0,0.8333,1.0,0.5714,0.0,1.0,1.0,0.8333,0.75,0.0,0.0,0.0,1.0,0.8571,0.0,0.0488,0.2,0.5,0.0,0.0,0.0,0.0,0.0,0.0,0.12,0.7692,0.1154,1.0,0.6429,0.0,0.0,0.8182,1.0,1.0,0.5,0.0,1.0,0.7778,1.0,1.0,0.1176,0.0,0.0,0.0,0.0,0.75,0.2727,0.5,0.1304,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5,1.0,0.8,0.0,0.0909,1.0,0.8182,1.0,0.05,0.8333,1.0,1.0,0.1154,1.0,1.0,0.5385,0.8571,0.8889,1.0,0.75,0.1053,0.0,0.1429,0.5833,0.6667,1.0,1.0,0.25,0.1765,0.0,0.4,0.3333,0.5,0.0,0.3333,0.2222,0.0,0.0,0.0,0.1333,0.2667,0.8333,1.0,0.0741,0.0,0.0,0.0,0.1,0.75,0.875,0.3,0.2632,1.0,1.0,1.0,0.4286,0.0,0.25,0.4444,0.3636,1.0,0.2667,0.0,0.0,0.0,0.0,0.5,1.0,0.8333,0.3333,0.0,0.2857,0.6667,0.0,0.0,0.625,0.1364,0.0,0.0769,0.8,0.2,0.7778,0.6429,0.2857,0.0,0.0,0.0,0.0,0.2222,1.0,0.6667,0.7,0.0,0.0,0.0741,0.0,0.1724,0.0,0.0,0.0,0.2727,0.8182,0.7,1.0,0.7,0.1429,0.3529,1.0,0.125,0.0,0.2727,0.5,0.3,1.0,0.375,0.1667,0.3571,1.0,0.6,0.0,0.6,1.0,0.4,0.0,0.0909,1.0,0.3333,0.2308,0.6,0.0,0.0,0.0,0.0,0.125,0.0909,0.1923,0.1875,0.3333,0.4286,0.0,0.0,0.0,0.3,0.25],"correct":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"calls":[4.4,4.125,10.75,10.2222,6.5,8.5714,10.6667,4.5714,11.25,6.25,12.0,13.25,8.2857,5.625,8.0,10.4444,14.0,15.7143,11.9,13.9167,16.1,13.4545,12.2222,13.5,16.0,14.25,15.8571,15.75,14.75,16.0,16.0,12.75,14.0,16.5714,15.6,16.5714,17.6667,18.7143,16.5556,15.875,16.2857,16.0,16.1429,16.0,15.6,16.0,16.25,15.3333,17.0,15.7778,16.6667,16.5,16.5,16.0714,16.375,16.5,15.5,14.5714,16.2,16.75,16.5,16.2857,16.75,16.25,15.75,14.5,15.4286,16.6364,11.25,17.4444,17.0,16.5,16.2,16.75,16.4,15.0,16.5,16.6,16.8,15.7143,17.0,16.5,16.25,17.0,16.875,16.5,17.0,16.4,15.7778,16.5,16.6364,17.0,16.0,16.625,16.5,16.6667,15.8333,16.0,16.8333,16.5,15.1,13.1429,16.1818,16.3333,16.25,16.4444,16.75,17.25,17.25,17.0,16.4615,17.5,17.5,16.7778,16.6,17.0,16.7778,16.3333,9.625,11.6667,16.7778,16.3,16.6667,16.6667,16.0,8.7917,11.6,16.6,17.4167,18.1667,17.5,16.0,17.4444,17.0,17.5,16.9,15.5,17.1,14.4444,14.3636,11.9231,15.0,17.5556,11.8667,17.1,17.5,17.0,17.6667,16.25,8.0909,6.0,17.0,16.1111,17.3636,16.6667,16.5714,16.7273,17.6,17.0,10.0833,2.875,14.5714,16.3333,16.7778,17.0,16.7778,16.7857,16.2308,16.0,16.0,15.7059,14.5714,16.5,16.4,17.0,9.1667,15.0667,16.4545,17.0,17.0,17.0,16.25,16.5,16.3333,16.25,16.7273,16.0588,16.625,16.6667,18.2,15.8333,16.2222,16.0,16.6111,15.9333,15.6364,15.1429,13.0909,16.6667,16.4,16.5833,14.7143,16.3,17.5,16.1667,16.8182,15.9167,15.3571,15.8571,15.8,13.25,13.2222,16.0,17.0,17.0,15.6667,16.5,14.625,16.2222,17.0,16.75,17.4,17.0,16.0,16.4615,16.2,16.7,16.1818,14.5,17.1429,17.4286,16.6667,16.0,14.6667,16.7273,16.5,16.8182,15.875,16.5,17.25,17.0,16.5,17.0,16.375,16.8333,16.2222,16.3,16.7143,16.0,17.0,16.4444,16.0,17.25,16.8182,16.25,16.75,17.7857,16.1429,16.3333,16.5714,16.3333,15.75,17.0,16.5714,15.6,9.4444,8.7,12.8333,17.6667,16.7,17.2222,8.3,13.625,17.0,16.75,15.75,16.5833,16.2,17.1111,16.75,16.5,16.5556,18.5,15.0556,9.75,17.4444,15.6667,16.7778,16.8,15.1,14.3333,15.0833,20.0,19.0,17.0,17.1667,17.625,18.1818,14.8,15.2,20.2727,18.4615,18.375,17.875,17.3636,17.2,17.4286,19.75,17.6667,17.5,18.2,18.4,17.1111,18.25,19.7778,19.1429,17.75,20.0,18.0,17.3333,18.5,18.25,19.0909,21.25,17.1111,17.0,18.1429,18.5455,16.3,18.5,19.25,21.4444,20.4286,16.0,16.25,17.0,20.4286,19.2222,17.6667,9.0,14.8571,17.0,23.0,18.5,22.0,16.25,16.6667,16.25,18.75,17.25,16.0,18.5,18.1,16.25,19.25,17.0,16.7143,17.6667,19.8571,18.5,17.4167,18.8333,20.4583,18.9792,122.6667,18.4,17.5556,15.7273,17.5455,17.5455,16.6,18.1,17.0,16.75,16.4286,16.75,14.5714,18.0,17.5556,17.8889,17.7143,18.6,17.3333,18.0,18.1429,17.5455,19.4286,20.9091,21.0,17.4286,18.3,17.7778,17.0,15.4444,17.0,18.5556,17.0,21.1,18.75,18.0,17.8333,18.0,22.7778,18.5,20.7273,18.0,18.6429,18.0909,17.4444,17.5,17.5,17.5,16.2,16.0,18.75,17.8182,17.7143,18.0,16.3636,17.0,13.9286,15.7692,18.0,20.3333,16.4,17.0,14.6667,16.75,19.0,15.6,16.1111,17.75,16.2222,16.7,14.5455,16.0,18.1111,18.0,16.75,17.6,18.0,20.0,17.1667,17.8333,12.25,9.75,16.5,17.1667,16.6667,17.6,16.9091,17.25,17.3636,18.3333,18.4286,11.9167,10.15,16.2833,22.5,18.3333,16.375,21.0909,23.0909,17.2857,17.75,16.75,18.0,17.2,17.6667,18.1818,17.0909,17.75,16.9091,16.5,17.0,17.0,17.4167,17.0,18.0,19.9167,17.75,17.1667,17.7143,17.1429,16.6667,16.6,16.25,16.0,17.8,17.0,17.25,17.2,17.25,17.0,17.1429,17.0,16.2,16.4286,16.9,17.2727,16.5556,16.8571,16.8333,16.6,17.7778,15.8,17.25,16.25,16.2,16.25,17.0,17.3,17.2727,16.75,17.25,16.7273,17.0,15.25,17.2727,18.0909,17.2727,16.4615,16.6364,16.6,17.1111,12.75,17.0909,16.5556,17.2857,16.5,18.125,17.75,17.0,17.4,16.5,17.0,15.6364,16.4,16.8,20.375,15.0,17.2308,16.9091,16.1818,16.6923,16.3333,15.2308,17.0,17.3636,17.2857,16.9,17.0,17.4,17.5714,16.875,17.4,17.8,15.0,17.0,17.5,16.6,15.75,17.4545,16.7692,16.25,16.1,16.75,16.0,17.3,17.6,20.5,17.5,19.5,17.0,17.0,17.0,16.5,16.1429,16.0,14.5,16.5455,16.0,17.2,15.75,17.2222,16.1818,18.0,14.0,16.8,16.75,13.4,15.0,15.4286,13.5556,15.2727,14.8,13.8889,15.25,16.0,13.4167,15.5,13.5,15.75,16.4667,16.0,11.6,13.75,15.4167,13.4167,15.5,14.8889,18.1875,13.7778,15.0,15.0909,2.875,11.6,12.6,25.0,16.6667,13.8,12.7273,11.2,16.75,16.0,19.7059,13.8889,15.75,11.75,11.8333,16.75,18.4706,14.9,15.8182,16.1429,16.25,16.0526,14.4,12.3125,12.3333,15.7143,16.4,17.3077,17.35,14.7778,14.2222,13.9375,17.3333,17.0,16.6667,15.4,16.2857,15.8182,14.9091,14.6667,19.1,18.8889,13.8077,9.25,15.5,16.0,15.4444,11.0,16.6,17.9219,17.6176,16.5556,16.5556,17.1818,14.9,17.1364,11.8182,16.1053,19.3077,17.3333,13.1,17.1429,14.4375,16.1667,24.9091,17.0,10.8889,15.3846,11.3333,14.5,12.2222,14.5,13.3333,18.5,15.8824,19.0,13.5,14.3571,16.6154,16.0,13.2,14.9091,14.7692,13.0769,9.3077,18.0294,51.8929,14.5789,14.5,14.3333,17.0,19.6875,17.6667,51.2143,13.75,14.75,16.1111,90.3,18.6667,16.25,12.0,13.9091,12.25,12.2727,14.2667,12.875,19.4667,17.1481,18.5556,19.6279,13.5,18.2778,15.3636,16.0952,13.0,23.1111,18.1818,13.8,13.5,12.9286,13.8333,15.2222,11.75,15.95,18.9231,18.3571,13.0,24.8947,20.9333,14.1667,15.8889,14.0,11.2222,12.0,13.5,12.1875,9.6154,11.4444,11.0,10.4667,22.9,19.32,12.75,15.1111,16.4,13.2857,17.2,15.6875,14.2941,13.7,12.4444,12.5,13.75,18.9091,20.925,16.6522,14.1429,14.6667,14.4,15.5238,12.2353,7.875,8.3333,15.8889,17.4138,5.2,8.8333,10.0909,11.3636,12.6667,16.4,6.5,7.875,12.0,12.7059,9.0,14.375,16.381,10.75,17.8095,17.3922,4.8,6.75,8.75,9.5,10.6667,8.0833,11.6154,9.7143,17.0909,8.7857,9.25,7.3333,7.75,17.7273,19.7692,17.0556,10.7143,10.2857,14.4783,18.9268,18.8,12.125,19.2973,19.3902,11.9412,16.1765,4.625,14.3333,17.88,8.1538,14.3077,12.3077,11.9286,18.0,16.9333,10.0909,10.6923,12.9,14.1,20.2857,11.25,12.1111,9.6667,11.0,15.5882,11.0,14.0,17.1351,4.375,10.25,11.0909,11.75,15.4348,18.02,16.25,15.625,17.8286,10.5556,9.5,8.25,13.0,9.5,13.0,14.625,11.0909,9.5,12.5455,10.8,18.0,9.6667,11.3333,13.0,15.7308,9.8571,12.3333,16.1538,12.9286,11.8889,12.8571,10.25,18.3158,19.4,15.9524,10.25,8.5556,11.0,13.0,17.5,14.2941,12.25,15.8667,14.0,17.6667,18.1667,13.625,14.6667,20.4,21.9167,21.0,19.4667,19.2,13.4167,11.5,17.5926,13.7,14.5556,13.9167,15.5,14.5,15.0,16.3,17.4737,11.5556,15.125,16.25,18.0,19.3158,12.4167,16.1111,17.6364,14.4444,19.8,22.3478,20.125,21.5111,18.6136,13.9,13.3333,13.8333,18.4444,17.5556,20.2143,12.8333,13.5714,14.5,15.25,19.1818,12.4286,18.5769,16.8,24.9333,11.8889,13.2857,12.2857,14.2667,11.7778,20.2273,8.0,9.1111,7.125,13.6667,18.0,19.5714,20.619,24.1852,22.7879,21.2069,23.2,18.65,19.5429,17.8182,12.8182,13.6,14.6364,19.1,24.2857,20.5294,14.0,17.125,17.25,18.0,15.5,14.1,12.75,16.875,19.6667,14.8571,17.25,15.2,19.4444,16.3,15.4545,23.0,20.4,20.7727,15.4,20.8889,21.2308,23.4,27.6,25.1429,31.5,26.4,25.625,27.0909,22.8846,25.25,24.8333,24.0,18.375,22.5556,22.5294,18.7,23.625]},"qwen_harbor_multi":{"step":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000],"reward":[0.7284,0.3906,0.0,0.6018,0.4792,0.1104,0.2917,0.0938,0.0,0.2125,1.0,0.7236,0.0938,0.1458,0.875,0.0,0.1684,0.491,0.6157,0.0613,0.1738,0.5296,0.1074,0.018,0.3914,0.0295,0.6152,0.2578,0.2704,0.8047,0.5,0.0,0.4139,0.9062,0.0,0.1587,1.0,0.25,0.4286,0.75,0.0,0.25,0.0,0.0,0.4167,0.8571,0.8333,0.25,0.0,0.625,0.25,0.7625,0.5,0.5,0.8571,0.0,0.0,1.0,0.5441,1.0,1.0,0.625,0.0,0.25,0.0,0.0,0.375,0.0,0.0,0.2083,0.75,0.0625,0.0,0.0,0.75,0.625,0.5625,0.2727,0.0,0.0,0.5,0.3269,0.2,1.0,0.2857,0.0,0.0,0.0,0.375,0.0,0.9615,0.7869,0.9,0.1,1.0,1.0,0.7143,0.0,0.0,0.25,0.0,0.125,1.0,0.9444,1.0,0.0,0.1,0.4,1.0,0.8095,0.7143,0.625,0.5,0.0,0.0,0.0,0.5,0.9167,0.625,0.0,0.0,0.1667,0.625,0.0,0.2222,0.25,0.0,0.0,0.0,1.0,1.0,0.7917,0.0,0.9808,1.0,0.3148,0.0,0.0,0.0,0.2857,0.75,0.0526,0.5333,0.8611,0.375,0.3571,0.75,0.2222,0.4,0.375,0.0,0.0,0.5,0.2,0.0,0.8333,0.5161,0.6702,0.375,0.0,0.0,0.0263,0.0,0.0,0.0,0.0,0.0,0.0,0.0143,0.5,0.5,1.0,0.5,0.25,0.875,0.0,0.8696,0.7463,0.0,0.0,0.125,0.5714,0.7143,0.6875,0.4821,0.2885,0.32,0.5417,0.125,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5,0.125,0.0,0.0,0.0,0.0,0.0,0.0,0.25,0.8182,0.0,0.0,0.0,0.0,0.0,0.25,0.5556,0.2456,0.4909,0.7,0.0,0.0,0.3235,0.7778,0.0938,0.75,0.25,0.0106,0.0,0.0,0.0,0.375,1.0,0.7727,0.75,0.25,1.0,0.0,0.0,0.5,0.0,0.0,0.0588,0.25,0.1429,0.6667,0.0,0.0,0.125,0.3,0.0,0.0,0.0,0.0833,0.7241,0.9796,0.75,0.0,0.0,0.3111,0.0,0.4167,0.0,0.7391,1.0,0.7143,0.6,0.0,0.3333,1.0,0.5385,0.5208,0.0,0.0,0.0,0.25,0.4,0.75,0.2857,0.0,0.5,0.0,0.2,0.5,0.25,1.0,0.5,0.0,0.0,0.0,0.025,0.125,0.25,1.0,0.9,0.7541,0.9412,0.25,0.0,0.0,0.4906,0.0,1.0,0.5,0.0,0.25,1.0,1.0,0.0,0.0,0.0,1.0,0.75,0.125,0.75,1.0,0.7308,0.8421,0.0,0.0,0.0,0.3,1.0,0.6,0.75,0.25,0.0,0.75,1.0,1.0,1.0,1.0,0.7391,0.25,0.9286,1.0,0.8611,0.125,0.0,0.0,0.0,0.1429,0.0,0.6,0.1429,0.0,0.0,0.0,0.0,0.125,0.25,0.25,0.9474,1.0,0.9706,0.375,0.6,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.7273,1.0,0.9898,0.5,0.5,0.25,0.75,0.0,0.0,0.0,0.0,0.0,0.9091,1.0,0.9259,0.375,0.6667,1.0,0.125,0.0,0.0667,0.5,0.125,0.8182,0.5,1.0,0.75,0.1,0.2326,0.4474,1.0,0.9853,0.25,0.0,0.0,0.0,0.2333,0.4571,0.25,0.0714,0.625,0.75,0.5667,0.5167,0.4706,0.0,0.875,0.7143,0.75,0.6667,0.5,0.3333,0.0,0.375,0.0,0.5556,0.0,0.0,0.0,0.0,0.7,0.7778,0.0,0.625,0.9861,1.0,0.6226,0.0,0.0,0.0,0.0139,0.5,0.2857,0.3333,0.6296,1.0,0.9787,0.0,0.0,0.0,0.0,0.0,0.125,0.75,0.7857,0.1818,0.0,0.0,0.3529,0.1875,0.0,0.0,0.35,0.0833,0.5,0.0,0.0,0.125,0.0,0.0,0.3333,0.2373,0.8421,0.625,0.0714,0.0,0.5,0.125,0.25,0.75,0.75,1.0,0.75,0.875,0.5833,0.875,0.0278,0.0,0.4848,0.0,0.0441,0.5,0.0,0.0,0.0098,0.6,0.7778,0.4286,0.75,0.0,0.1111,0.75,0.0,0.0,0.0,0.0,0.0,0.0,0.625,0.5333,0.5455,0.3636,0.25,0.3265,0.9474,1.0,1.0,0.5,0.5,0.5,0.125,0.1429,0.2,0.0,0.25,0.125,0.9,1.0,1.0,1.0,1.0,1.0,0.625,0.2333,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5,1.0,0.119,0.0,0.0417,0.75,0.625,0.8,1.0,0.25,0.0,1.0,0.75,1.0,0.75,0.64,0.0,0.0294,0.5,0.0,0.0,0.2667,0.0227,0.875,0.75,0.7222,0.7581,0.9896,0.0,1.0,1.0,0.1667,0.7143,0.0,0.0,0.0,0.125,0.0,0.5,0.25,0.0,0.125,0.0,0.125,0.0,0.0,0.125,1.0,1.0,1.0,0.4848,0.0,0.075,0.9375,1.0,0.3333,0.0,0.0,0.0,0.1316,0.875,0.375,0.7143,0.5,1.0,0.125,0.0,0.9434,1.0,0.8421,0.0,0.1875,0.375,0.35,0.0,0.0,0.913,0.3462,1.0,0.7917,0.2,0.375,0.375,0.0,0.125,0.3333,0.0,0.8718,0.5,0.9861,0.875,1.0,0.7273,0.5,1.0,0.7143,0.0,0.0,0.0426,1.0,1.0,1.0,1.0,0.75,0.4915,1.0,0.0,0.75,0.4167,0.0,0.0,0.0,1.0,0.75,0.25,0.0,1.0,0.8036,0.625,0.4167,0.0,1.0,0.875,0.0,0.0,0.75,0.7895,0.875,0.0,0.0,0.0,0.0,0.0,0.0,0.875,0.7255,1.0,0.25,0.4,1.0,0.0,0.3333,0.2222,0.0,0.0,0.1818,0.5,0.5,0.75,0.4,0.0,0.0,0.0,0.5,0.6,0.0,0.7719,1.0,0.75,0.0,0.0,0.0,0.0,0.125,0.2917,0.0,0.2727,0.0,0.0,0.875,0.75,0.0,0.9808,1.0,0.8947,0.25,0.875,1.0,0.75,0.625,0.0,0.3,0.0192,0.0,0.0,0.0,0.75,1.0,0.4583,0.1,0.0,0.0,0.0,0.3365,0.625,0.2,0.2,0.0,0.7143,1.0,1.0,0.875,1.0,0.6,0.0,0.0,0.4375,0.0,0.0,1.0,0.7344,0.0,0.0,1.0,0.0,0.0,0.5,0.0,0.0,0.0,0.5,0.6607,0.0357,0.0,0.25,0.1429,0.0,0.9167,1.0,1.0,0.7273,0.0,0.0,0.2917,0.1429,0.0,0.3,0.0,0.0,0.0,0.0,0.25,0.0667,0.5,0.75,0.0,0.1333,0.0,0.0,0.0,0.0,0.0,0.0,0.3214,0.9167,0.0,0.875,0.0,0.2857,0.6182,1.0,0.875,0.875,0.0,0.75,0.0,0.2459,1.0,1.0,0.25,0.75,0.4,0.0,0.0,0.8333,0.8571,0.0556,0.0,0.1111,0.0606,0.2581,0.9394,0.0,0.0,0.0,0.1905,0.75,0.0,0.0,0.9904,0.9245,0.7273,0.4167,0.2143,0.8571,0.0,0.0,0.0,0.0,0.25,0.3,0.0,0.68,1.0,0.9808,0.0,0.0,0.5,0.3399,0.114,0.0,0.0,0.0,0.0417,0.6,0.75,0.2105,0.7385,0.4324,0.625,0.5833,0.0833,0.3846,0.2857,0.0714,0.5,0.5,0.3333,0.0,0.4,0.0,0.4545,0.25,0.625,0.875,0.75,0.0,0.25,0.0,0.0,0.0,0.4359,1.0,1.0,1.0,0.625,0.0,0.0,0.1538,0.875,0.0,0.2537,0.0,0.0,0.5312,0.2143,1.0,1.0,0.75,0.0,0.0625,0.0,0.1765,1.0,0.0625,0.0,0.2656,0.0,0.0,0.4,1.0,0.4167,0.0,0.6939,0.5094,0.1333,1.0,1.0,0.0,0.8095,1.0,1.0,1.0,1.0,0.1667,0.0,0.7,0.9216,0.7288,0.8684,0.0,0.6667,0.375,1.0,0.75,0.8438,0.0,0.0,0.4286,1.0,0.25,0.5,0.75,0.5,0.1,0.6,0.0,0.0,0.0,0.05,1.0,0.5088,0.2623,0.0,0.7222,0.0,1.0,0.6667,0.3333,0.0,0.0,0.625,0.5,0.0,0.9,0.7778,0.4,0.0,0.2,0.5,0.0,0.375,0.4583,1.0,0.5278,0.6182,0.0,0.0,0.0,0.0,0.25,0.0,0.125,0.2222,0.5714,1.0,0.98,0.0,0.0,0.75,0.25,0.0,0.6739,0.0,0.375,0.0,0.4,0.7,1.0,0.625,0.0,0.0,0.0,0.0,0.0,0.25,0.3265],"correct":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"calls":[9.3333,15.0469,17.0,7.1016,12.9167,14.1531,17.125,16.8125,16.3438,11.0583,7.1875,12.9222,8.1875,14.1094,17.0,14.9688,13.7792,13.2948,15.4098,14.0731,14.3753,15.4012,13.4273,13.0843,15.7339,15.1913,16.4164,16.6953,14.9147,15.3516,5.5,11.3333,13.5722,16.7396,16.7541,5.7963,16.2615,5.5,8.3429,5.5,15.9787,12.375,14.0,16.2,13.85,6.0,8.8333,14.25,6.2,11.875,15.1667,11.7875,15.15,15.5,15.5238,17.0,13.7059,15.64,17.4265,10.75,14.72,16.125,14.5,15.0,15.875,17.0,14.75,5.3846,16.4098,16.3333,15.0,15.0625,17.0,16.9444,15.875,14.375,15.4375,15.4545,8.2,17.5,17.0,16.9808,14.9,15.5,16.0714,17.0,16.0,15.75,15.5,15.3333,16.9615,15.7049,16.8,15.1,15.9524,16.0,15.4286,15.4,16.9189,17.0,16.963,15.375,16.0,16.2222,17.0,17.0,16.7,16.4,15.75,16.7143,17.0,17.1875,8.6667,6.1579,10.0714,15.0,14.875,15.8333,16.25,15.125,15.6667,13.7778,14.125,15.0,15.7778,17.0,17.0,11.6061,14.9643,16.0,15.5,17.4583,15.625,16.9808,17.0,17.0,17.0,16.9062,15.875,15.5714,15.5,15.3684,17.0,17.0,13.0,15.9286,15.75,15.7778,15.0,16.375,15.0,16.4,16.1,15.0,15.9,16.8056,17.0,16.8723,15.5,14.125,17.5152,18.6053,14.5,15.5,18.0811,17.0,17.0,17.0,16.5286,16.625,15.25,15.25,16.375,15.5,12.875,16.0,16.7391,17.0,16.9902,15.625,16.5,14.8571,15.4286,15.9792,17.0,17.0,17.2,16.4167,16.75,16.0,15.1667,16.9623,16.7612,16.7391,15.2222,12.2,16.375,15.5,16.625,16.5,15.7143,15.0,17.0,16.5152,14.4167,15.0,15.8182,15.5,15.25,16.5,16.434,16.7612,15.75,15.5556,17.2456,17.0,17.0,15.625,16.875,16.9706,17.0,16.6562,16.0,15.875,16.9468,17.0,17.3421,13.3333,15.125,16.375,17.0,17.0,15.25,15.25,15.25,15.5,16.0,17.0,17.0,16.9706,16.0,15.2857,14.6,15.7143,16.1429,16.6667,16.6,15.1,15.4286,15.6667,16.75,17.0,17.0,15.875,16.9074,17.0,17.0,17.0,16.1667,15.5,16.7391,17.0,14.119,15.4,16.0,13.0,17.2558,17.0,18.4167,16.9861,16.75,16.0,16.0,15.8,15.75,15.4286,17.75,16.625,16.25,13.6,15.5,17.0,17.0,14.625,14.25,16.7895,17.0,16.8625,16.375,16.625,16.0,16.9,17.0,17.0,17.0,17.0,16.75,17.0,17.0,15.75,12.5,16.875,12.5,15.6441,16.7612,13.4,15.75,15.625,14.3125,14.75,15.5,16.75,17.0,17.1731,16.6842,15.3333,16.8966,17.0,16.2,15.75,16.4,16.5,16.375,16.625,15.625,15.75,15.0833,17.9615,17.5,17.3913,14.5,14.5,16.1273,17.5,15.625,16.9744,17.0,16.7667,15.2143,12.4444,14.6,14.7857,15.25,15.2162,17.0,16.9211,15.0,13.6667,15.5,16.7368,16.7612,16.0588,15.875,16.4,16.0,16.375,17.0,17.0,17.0,16.9886,14.9,15.5556,16.3636,15.7143,16.9796,15.5,15.3,14.5,16.5,16.5833,15.6667,15.5,15.75,15.6429,16.8182,17.0,16.9815,16.25,15.8333,15.5,18.625,17.25,16.8833,16.625,15.875,14.9091,17.0,16.9167,15.5,16.9,17.3023,15.8947,17.0,16.9902,16.5,16.1944,15.1111,16.7059,16.75,16.4286,13.875,15.0714,15.7083,16.0,16.9,17.0,16.9265,15.5,16.625,13.2857,15.5,16.0,14.75,16.0833,16.0,16.75,15.0,15.5556,15.2917,17.0,17.25,16.875,15.8,15.7778,14.9167,15.25,17.8889,18.5,16.6981,17.0,17.2833,17.0,16.9861,17.0,9.0714,13.7778,16.1481,17.0,15.5319,14.875,15.5,14.25,16.0,16.1875,16.5,15.125,15.0,14.4545,16.2593,16.75,16.6863,16.9375,17.25,16.9762,15.9,16.8333,17.0,16.9667,16.4,16.625,11.8333,16.0,16.9167,17.2542,17.0,17.0,15.0,14.5,12.9,16.0,15.375,14.75,15.375,17.0,17.0,16.625,13.9167,16.0,16.9444,17.0,17.7273,20.7302,18.4265,11.8333,16.8,17.0,16.6765,15.7,15.1111,14.7143,16.5,14.4,13.8889,16.875,17.0,15.6,14.75,16.7368,17.0,17.0,13.75,13.1667,14.9091,10.7273,17.0,17.3163,16.9474,17.0,16.9712,16.625,15.1667,11.4444,14.6667,14.6429,14.2,15.0909,16.0,15.25,16.875,17.0,16.9904,16.975,17.0,17.1058,13.0,13.3,15.7778,11.3,15.6154,15.375,16.8,17.0,17.0,16.7895,14.2222,16.2143,17.0,16.9583,13.625,13.375,12.6,15.8,17.0,17.0,16.375,16.75,16.9737,17.0,16.4,2.3333,5.7353,16.75,16.7083,16.9375,17.0,16.9773,13.75,9.3125,10.1481,17.0,16.8229,12.25,15.1429,14.0,13.6667,13.4286,4.5,15.52,15.125,14.25,14.5,12.75,14.75,16.75,14.125,15.875,13.9792,16.9592,17.0,16.9,10.0,16.8696,17.0,17.0,17.0,16.85,15.625,14.4,14.7778,14.4167,16.9444,18.5,16.9474,11.375,15.2083,11.4286,11.125,16.375,8.5,15.2308,16.8868,17.0,15.5526,15.5,17.1875,16.375,17.2,17.0,17.9167,14.1304,17.0,15.5,12.5417,15.2,13.75,15.125,16.1,15.7917,15.5833,16.0,16.7949,17.0,16.6111,8.125,11.3333,13.9091,15.25,15.5,15.4286,16.8824,17.0,17.2766,15.25,13.8947,17.0,16.9773,15.8333,17.0,17.0,16.75,13.25,13.2083,15.7917,12.5,13.25,15.9844,17.0,12.0,11.5,17.0,15.1786,16.625,12.1667,16.0,8.5,16.375,15.0,14.6667,20.75,23.4737,9.375,14.4444,15.7,14.4,14.7143,17.0,17.0,16.0417,16.7059,15.8113,16.5,16.4,16.0833,11.3333,8.9167,13.6667,9.5455,3.8571,5.6818,9.0,13.2917,15.5,7.8,8.7333,15.8958,15.4167,15.0,15.6,16.25,17.0,17.0,14.625,15.375,9.7143,17.0,17.0,16.375,10.5417,15.9333,16.3455,15.7143,15.5,17.0,9.375,13.5,16.9808,16.5152,14.9737,15.875,16.375,16.3333,15.25,14.625,15.1429,15.0,16.6731,17.0,16.8563,16.85,17.0,16.9848,16.0833,14.3667,13.3333,15.64,17.5,17.0,11.25,12.8,8.4,13.3333,15.1905,17.25,16.9783,14.5,17.75,16.6182,15.375,13.6667,9.9375,5.8,11.0,16.7612,16.0312,12.5556,11.75,6.75,17.0,17.0,16.5,16.0204,17.0,14.75,15.0,11.2321,16.8036,17.0,16.375,6.6429,10.9231,9.5833,16.9082,17.0,13.9091,15.1111,14.8,12.7083,12.8571,14.75,11.8,8.7273,15.45,17.0,16.9504,13.55,12.5833,13.5,12.25,7.25,15.5333,17.0,16.6579,16.3,15.8462,16.7,17.0,16.9881,16.4167,14.6875,16.75,13.5714,12.7143,16.0545,16.7612,10.5,8.75,15.75,15.75,17.0,17.0,16.7627,17.0,14.875,16.125,15.6,13.25,13.75,15.4167,14.5714,16.9444,17.0,17.0,16.9697,17.0,16.8485,15.3333,16.9184,17.0,16.0952,15.75,15.8667,16.125,18.25,15.6604,15.8182,15.0,14.3571,13.0,15.3056,16.6,15.5,16.625,16.25,13.0,13.875,15.24,17.0,17.0,16.5,16.65,17.0,16.6209,16.3772,17.0,16.5833,15.0,14.7917,13.3,14.5,15.9474,16.2923,12.5135,14.875,15.5833,2.3333,16.7179,17.0,14.8571,15.75,13.75,6.0,5.0,16.0,16.5246,16.303,13.0,12.75,7.125,12.25,13.5,15.75,9.5,11.8,1.0,15.2564,14.3621,12.0667,12.375,11.75,14.5,3.375,10.5385,15.375,17.0,16.7612,1.0,1.0,17.6562,17.3571,16.5,16.2,11.5,1.0,16.875,17.0175,15.2941,13.25,11.6875,15.8,16.0625,11.8148,10.5,13.6,15.0,15.9583,2.625,16.0,16.7736,14.3667,13.8333,8.4583,13.875,15.1905,17.0,16.36,16.0,11.375,13.6667,13.1111,12.7,15.3922,15.8814,15.8947,6.4375,12.75,14.0,15.2558,16.5,13.7812,6.6667,13.0,15.8571,15.25,12.75,14.25,14.75,16.5,1.8,9.4,10.3448,14.6364,14.5577,13.1833,15.7105,12.807,14.3115,17.0,11.8889,11.8571,13.625,10.1667,5.8889,3.6667,5.375,12.75,9.25,6.5,13.8,12.5556,14.0,7.625,5.8,7.4,6.9,9.375,11.2083,15.125,9.3611,15.9818,13.6471,5.3,3.7143,16.75,14.75,9.7,10.5,10.3333,9.1429,10.4146,15.95,12.125,10.875,14.0,15.2083,16.2833,15.413,15.75,11.125,13.5,14.2,14.1,11.1667,8.7083,13.7778,4.75,15.7581,14.6724,2.5,14.0,18.0]},"qwen_native_opencode":{"step":[1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190,191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253,254,255,256,257,258,259,260,261,262,263,264,265,266,267,268,269,270,271,272,273,274,275,276,277,278,279,280,281,282,283,284,285,286,287,288,289,290,291,292,293,294,295,296,297,298,299,300,301,302,303,304,305,306,307,308,309,310,311,312,313,314,315,316,317,318,319,320,321,322,323,324,325,326,327,328,329,330,331,332,333,334,335,336,337,338,339,340,341,342,343,344,345,346,347,348,349,350,351,352,353,354,355,356,357,358,359,360,361,362,363,364,365,366,367,368,369,370,371,372,373,374,375,376,377,378,379,380,381,382,383,384,385,386,387,388,389,390,391,392,393,394,395,396,397,398,399,400,401,402,403,404,405,406,407,408,409,410,411,412,413,414,415,416,417,418,419,420,421,422,423,424,425,426,427,428,429,430,431,432,433,434,435,436,437,438,439,440,441,442,443,444,445,446,447,448,449,450,451,452,453,454,455,456,457,458,459,460,461,462,463,464,465,466,467,468,469,470,471,472,473,474,475,476,477,478,479,480,481,482,483,484,485,486,487,488,489,490,491,492,493,494,495,496,497,498,499,500,501,502,503,504,505,506,507,508,509,510,511,512,513,514,515,516,517,518,519,520,521,522,523,524,525,526,527,528,529,530,531,532,533,534,535,536,537,538,539,540,541,542,543,544,545,546,547,548,549,550,551,552,553,554,555,556,557,558,559,560,561,562,563,564,565,566,567,568,569,570,571,572,573,574,575,576,577,578,579,580,581,582,583,584,585,586,587,588,589,590,591,592,593,594,595,596,597,598,599,600,601,602,603,604,605,606,607,608,609,610,611,612,613,614,615,616,617,618,619,620,621,622,623,624,625,626,627,628,629,630,631,632,633,634,635,636,637,638,639,640,641,642,643,644,645,646,647,648,649,650,651,652,653,654,655,656,657,658,659,660,661,662,663,664,665,666,667,668,669,670,671,672,673,674,675,676,677,678,679,680,681,682,683,684,685,686,687,688,689,690,691,692,693,694,695,696,697,698,699,700,701,702,703,704,705,706,707,708,709,710,711,712,713,714,715,716,717,718,719,720,721,722,723,724,725,726,727,728,729,730,731,732,733,734,735,736,737,738,739,740,741,742,743,744,745,746,747,748,749,750,751,752,753,754,755,756,757,758,759,760,761,762,763,764,765,766,767,768,769,770,771,772,773,774,775,776,777,778,779,780,781,782,783,784,785,786,787,788,789,790,791,792,793,794,795,796,797,798,799,800,801,802,803,804,805,806,807,808,809,810,811,812,813,814,815,816,817,818,819,820,821,822,823,824,825,826,827,828,829,830,831,832,833,834,835,836,837,838,839,840,841,842,843,844,845,846,847,848,849,850,851,852,853,854,855,856,857,858,859,860,861,862,863,864,865,866,867,868,869,870,871,872,873,874,875,876,877,878,879,880,881,882,883,884,885,886,887,888,889,890,891,892,893,894,895,896,897,898,899,900,901,902,903,904,905,906,907,908,909,910,911,912,913,914,915,916,917,918,919,920,921,922,923,924,925,926,927,928,929,930,931,932,933,934,935,936,937,938,939,940,941,942,943,944,945,946,947,948,949,950,951,952,953,954,955,956,957,958,959,960,961,962,963,964,965,966,967,968,969,970,971,972,973,974,975,976,977,978,979,980,981,982,983,984,985,986,987,988,989,990,991,992,993,994,995,996,997,998,999,1000],"reward":[0.6,0.625,1.0,0.6667,1.0,0.2857,0.7778,1.0,1.0,0.4167,0.6667,0.0,0.0,0.0,0.0,0.25,0.2222,0.0,0.0,0.25,0.6,0.2857,0.7143,0.6667,0.875,0.7273,1.0,0.7273,0.75,0.25,0.5714,1.0,0.1429,0.0,0.5,0.75,0.4286,0.0,0.6,1.0,1.0,1.0,0.8333,0.6,0.0,0.0,0.0,0.2,0.0,0.25,0.75,0.75,0.8,0.625,1.0,1.0,1.0,0.25,0.0,0.0,0.3077,0.0,0.0,1.0,0.8,1.0,1.0,0.0,0.0,0.0,0.3,1.0,0.8182,0.0,0.0,0.0,0.0,0.0,0.0,0.75,0.25,0.2222,0.0,0.6923,0.1667,0.0,0.0,0.1,0.7143,1.0,0.75,0.0,0.0,0.0,0.125,0.8,0.5,0.0,0.1429,1.0,1.0,1.0,1.0,0.75,0.1429,0.0,0.0,0.0,0.0,0.0,0.3333,0.0,0.0,0.0,0.5714,0.3333,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.6667,0.75,0.0,0.0,0.0,0.0,0.2857,0.4286,0.5,0.25,0.0,0.0,0.0,0.0,0.0,0.3333,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25,0.0,0.0,0.2,0.0,0.6667,0.5,0.8571,0.4,0.0,0.0,0.4444,1.0,0.5,0.0,0.0,0.0,0.0,0.3333,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.4444,1.0,0.9286,1.0,0.2222,0.0,0.875,0.5714,0.3333,0.0,0.0,0.0,0.75,0.75,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25,0.0,0.0,0.0,0.0,0.0,0.3333,0.0,0.0,0.75,0.5,1.0,1.0,0.0,0.0,0.2,0.0,1.0,1.0,0.5714,1.0,0.75,0.0,0.2857,0.0,0.0,0.0,1.0,0.0,1.0,0.7143,1.0,1.0,1.0,1.0,0.0,0.0,0.0,0.0,0.1667,1.0,1.0,0.6,0.0,0.0,0.0,0.0,0.0,0.75,0.25,0.1667,0.0,0.0,0.0,0.0,0.3333,0.0,0.5,0.0,0.0,0.0,0.0,0.0,0.75,0.375,0.0,0.75,0.5,0.6667,0.4167,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.25,0.0,0.9,0.0,0.0,0.0,0.0,0.0,0.6667,0.75,0.5,0.0,0.0,0.0,0.5,0.9333,0.5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.3,0.0,0.0,0.0,0.0,0.0,0.7708,0.0,0.0,0.0,0.25,0.0,0.25,0.2,0.0,0.0,0.0,1.0,0.75,0.6,0.0,0.0,0.0,0.0,0.625,1.0,0.625,0.0,0.0,0.0,0.0,0.0,0.1667,0.6667,0.0,0.0,0.25,0.0,0.625,0.1667,0.0,0.0,0.0,0.0,0.0,0.5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.75,1.0,0.875,0.0,0.0,0.5,0.25,0.0,0.0,0.0,0.0,0.0,0.3333,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.125,0.5,0.75,0.0,0.0,0.75,0.8333,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.4167,0.5,0.25,0.0,0.0,0.0,0.25,0.25,1.0,1.0,0.0,0.125,0.25,0.25,0.0,0.0,0.0,0.0,0.75,1.0,0.6667,0.0,0.5,0.3333,0.1667,0.0,0.75,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.75,0.5,0.0,0.0,0.0,0.0,0.0,0.0,0.1667,0.625,0.0,1.0,1.0,0.0,0.0,0.0,0.5,0.5,0.0,0.0,0.0,0.25,0.75,0.0,0.3333,0.0,0.0,0.25,0.25,1.0,0.2083,0.0,0.0,0.0,0.5,0.25,0.0,0.0,0.4,0.4,0.0,0.0,0.0,1.0,0.0,0.4,0.75,0.0,0.2917,1.0,0.75,0.0,0.0,0.0,0.0,0.0,0.9167,0.75,0.0,0.0,0.0,0.0,0.0,0.25,0.5,0.2857,0.0,0.0,0.0,0.0,0.0,0.2,0.0,0.0,0.0,0.0,0.0,0.2857,0.125,0.0714,0.0,0.75,0.75,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25,0.75,1.0,0.5,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.625,0.2,0.75,0.6,0.0,0.0,0.0,0.0,0.1667,0.0,0.0,0.1667,0.0,0.0,0.0,0.0,0.6667,0.375,0.2,0.0,0.25,0.25,0.0667,0.8,0.1818,0.0,0.0,0.0,0.0,0.25,0.1429,0.0,0.0,0.0,0.15,0.0,0.0,0.0,0.0,0.0,1.0,0.0833,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.05,0.0,0.0,0.0,0.0,0.0,0.0,1.0,0.0,0.0,0.0,0.5,0.1667,0.8,0.0,0.0,0.25,1.0,0.0,0.0,0.6667,0.75,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.75,1.0,0.0909,0.0,0.0769,0.5,1.0,0.5,0.25,0.0,0.0,0.0,1.0,0.25,0.0,0.0,1.0,0.75,1.0,0.5,0.2,0.5,1.0,1.0,1.0,0.75,0.0,0.0,0.0,0.1429,0.0,0.0,0.0,0.0,0.25,0.4167,0.0,0.3333,0.3333,0.5,0.25,0.75,0.2,0.0,0.0,0.0,0.5,0.625,0.4,0.0,0.0,0.0,0.5,1.0,1.0,1.0,1.0,0.5,0.0,0.25,0.25,0.0,0.0,0.4,0.0,0.0,0.0,0.0,0.0,0.0,0.8571,0.7143,0.4,0.0,0.2857,0.5714,0.0,0.0,0.0,0.0,0.75,0.25,0.0,0.0,0.0,0.0,1.0,1.0,1.0,0.2,0.0,0.0,0.0,0.0,0.0,0.4167,0.4167,0.0,0.0,0.4,0.0,0.1667,1.0,0.75,0.0,0.0,0.375,0.0,0.1667,0.875,0.25,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.5,1.0,0.6,0.0,0.875,0.8,0.0,0.0,0.0,0.0,0.0,0.0,0.25,0.5,1.0,0.6,0.0,0.3333,1.0,1.0,0.7778,0.2857,0.0,0.0,0.0,0.6,0.8333,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.75,1.0,1.0,0.8,0.1667,0.0,0.0,0.0,0.0,0.0,0.5714,0.25,0.0,0.0,1.0,1.0,0.0,0.0,1.0,1.0,0.0,1.0,1.0,0.0,0.0,0.0,0.75,1.0,0.5,0.4,1.0,1.0,1.0,0.8,0.0,0.5714,1.0,0.8,1.0,0.3333,0.0,0.0,0.0,0.0,0.0,0.8571,1.0,0.4,0.25,0.0,0.8571,1.0,0.4,0.0,0.75,0.75,0.0,0.0,0.0,0.0,0.0,0.0,0.2,0.5,0.1429,0.0,0.0,0.0,0.0,0.0,0.2857,0.375,0.3333,1.0,0.5455,0.6923,0.375,0.0,0.0,0.0,0.2857,0.0,0.0,0.0,0.75,1.0,0.1429,0.0,0.0,0.2857,0.0,0.0,0.0,0.0,0.0,0.0,0.25,0.0,0.4444,1.0,0.0,0.6,0.0,0.0,0.8571,1.0,0.1818,0.0,0.6667,0.8571,1.0,1.0,0.6,1.0,0.25,0.0,0.5625,0.2,0.0,0.0,0.0,0.1667,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.3333,1.0,0.5,0.75,0.875,0.0,0.5,0.7778,0.0,0.0,0.4286,0.0,1.0,1.0,0.0,0.0,0.0,0.0,0.125,0.2222,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.25,0.0,0.25,0.0,0.0,0.0,0.3333,0.625,1.0,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.6923,0.0833,0.0,0.0,0.75,1.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.4286,0.4667,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.0,0.0,1.0,1.0,0.0,0.0,0.16,0.0,0.0,0.0,0.0],"correct":[null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null,null],"calls":[4.6,8.75,4.25,11.5,3.6667,11.7143,3.4444,5.6667,4.2,7.25,5.1111,7.0,3.4,5.375,3.25,4.0,4.2222,8.5455,3.0,4.0,11.0,11.0,2.8571,4.7778,6.125,5.3636,7.3636,10.4545,2.4167,8.0,7.0,3.0,6.4286,12.3636,8.5,2.75,3.5714,4.2,6.6,3.25,3.0,3.25,3.3333,4.4,8.5,2.8,3.5833,7.8,3.0,3.25,3.0,3.0,3.2,8.625,3.0,3.0,3.25,4.625,12.1667,5.5,6.3846,4.8,13.25,3.6667,6.8,3.0,4.7143,5.8333,15.8333,6.1818,13.7,5.2857,9.5455,4.0,9.4615,14.25,13.75,7.75,12.8,7.0,11.5,4.6667,8.6667,4.3846,8.75,4.2857,8.5714,9.3,10.1429,7.8333,6.5,3.7778,6.9091,16.3333,14.625,2.8,7.375,6.5833,13.0,3.25,4.9375,3.3,3.8,5.0,6.5714,8.75,7.2,9.0,5.7143,12.6667,7.1667,11.4,9.2,10.25,5.7143,3.0,3.4545,3.2,6.8333,14.25,9.8,14.6,6.4,2.75,2.9167,3.0,8.0,6.6667,3.4444,3.8571,9.5714,11.4286,13.3333,10.0,11.1667,11.5,9.1429,8.6667,12.0,8.8333,15.2,10.4,8.9,10.5714,5.8571,9.4,10.75,8.875,5.5,10.5,13.4,5.1429,4.8,9.0,4.0,5.6667,8.8571,13.0,6.875,4.7778,5.7778,3.5714,5.0,7.0,7.8,2.0,4.3333,10.5,5.2857,4.4,7.5,5.75,6.8,6.5714,7.1667,3.9231,3.6667,3.25,4.2143,6.75,4.7778,9.0,5.5,4.7143,5.2222,13.25,4.125,5.0,6.25,3.75,6.8571,12.1667,5.125,4.375,9.5455,14.125,10.6667,12.125,8.5,9.8333,13.5,11.25,5.3333,6.7143,3.75,6.0,8.375,11.3333,13.6667,7.0,3.5,5.5714,3.3333,4.6667,14.6667,10.1429,10.0,11.2,13.0,3.25,4.0,3.0,3.1429,3.75,7.4286,6.6,12.75,3.0,3.0,7.1429,8.75,13.25,3.8889,11.7143,4.3333,13.0,14.75,5.0,15.8333,4.5556,6.4286,3.2857,3.2857,3.25,3.1,5.125,7.8333,5.7,14.5,12.875,3.3,3.5,3.0,7.1667,10.5,8.75,10.0,8.8571,6.875,4.75,7.5,13.5,13.7,10.3,4.5714,8.3333,9.8333,5.5,4.8889,4.8889,6.5,11.0,15.5,6.25,4.625,9.75,8.5,9.0,5.5833,6.1667,5.75,11.2,12.2857,12.9167,14.75,14.5,10.8,5.4,7.125,12.0,5.9,6.9167,13.7143,6.8571,14.75,6.25,4.5,3.25,8.0,14.4,12.5,14.75,9.125,4.7333,7.5833,9.8333,10.1667,8.5833,3.1,4.0,10.1429,5.4,4.9,7.2917,6.375,9.375,11.9167,13.8667,7.2083,7.1,5.625,10.875,6.5,7.1,4.75,6.0,12.1667,9.4444,15.75,9.3,6.0,4.8,5.5833,12.9167,15.375,7.2917,3.4167,3.6667,7.625,4.25,10.8,5.25,8.5,5.0,10.5,8.4583,5.125,7.9167,14.0,5.8333,4.5,7.875,11.1667,13.4583,6.125,7.1667,11.625,9.125,11.5,10.25,11.25,12.0,7.8333,5.875,9.0,15.25,5.75,3.125,7.25,3.75,8.9167,8.75,6.9167,5.0,4.9,4.25,5.0833,4.2083,5.7083,3.875,9.0,14.2,10.3333,13.4,13.0,5.5,5.4167,6.25,4.25,7.2083,7.375,11.1667,14.6,3.0,8.2083,11.25,7.0417,7.75,13.3,3.5,12.6,14.4667,11.9167,6.5,4.0,3.5,4.625,9.25,12.125,14.5,5.0,7.5714,4.125,3.125,5.0,7.0,14.0,6.5,4.7917,4.625,3.4167,3.0,4.8333,11.6,7.75,11.0,10.5417,9.25,7.5,5.4,10.8333,4.0,5.5,7.375,10.5,13.2,6.5,7.25,8.5,12.0,9.6667,11.4167,11.3333,9.25,7.0,4.875,11.2,3.5,3.75,9.8,12.8,6.0,6.0,3.0,5.5,6.0,6.5,4.0,3.25,4.125,9.3333,13.8571,13.7917,10.0417,10.4167,5.25,5.625,9.75,10.7917,4.9167,7.625,9.0,8.5714,13.6667,8.6,7.2,11.9167,6.0,6.2,4.875,12.75,10.5,8.875,5.5,5.9167,3.25,4.75,8.3333,10.4286,12.0,6.25,6.2,3.4167,4.5,3.6,5.75,10.75,9.7143,12.5,9.25,7.75,7.5714,7.8,3.375,4.8333,5.8571,8.3333,8.6,8.6,9.0,6.3333,3.9167,3.0,8.2857,10.875,7.8571,9.7143,3.25,3.75,5.5556,7.5,12.1333,13.7778,11.2857,12.0,13.5417,12.8889,11.3571,3.75,5.25,5.1667,14.2941,7.7,5.0,3.4167,3.0,4.25,14.0,10.4375,11.75,7.5,14.5,11.8182,7.0,10.0,7.0,3.25,5.2667,13.3333,6.0,3.9167,5.8,9.5,8.25,14.1176,5.5,9.0,10.5714,12.0,8.9,5.8333,6.125,7.5,12.3333,8.5,12.875,11.4,5.0,12.2727,4.84,5.0,3.75,4.125,3.5,8.4286,9.9667,7.7,4.8,5.4167,6.9167,8.4444,3.0,11.75,13.5556,3.5,7.1667,12.625,9.2,8.8,3.0,3.0,4.0,4.2857,3.25,7.3333,11.95,13.0909,4.25,5.0,3.0,4.75,11.1667,3.5,5.8,4.25,10.3333,3.0,3.3333,5.1,8.3333,3.75,7.35,4.125,3.5,4.25,6.5,10.75,3.25,5.8,9.25,10.75,14.25,10.5,9.9,12.25,6.125,3.75,5.7273,6.2308,10.7692,3.0,3.5,5.0,4.25,6.5,4.5,10.625,3.25,8.0,3.5,4.5,3.75,3.8333,3.75,4.75,9.4,10.0,3.5,3.6,6.0,7.0,7.8571,11.5,4.25,6.2857,11.25,7.75,10.25,5.75,3.75,3.3333,4.75,6.0,5.5,3.25,3.25,3.5,8.6,13.25,8.75,3.5,4.0,4.5,6.4,3.0,3.0,6.1667,9.125,3.4,3.0,3.0,3.0,3.25,5.0,4.9167,3.5,8.1667,10.8,8.2,4.25,5.6667,5.8,5.6,8.25,11.25,6.6429,5.2857,5.8,3.5,4.8571,10.0,5.5,3.75,4.25,10.25,5.25,2.25,3.75,4.0,3.6667,10.0333,3.0,4.1111,3.125,9.0,13.1429,7.6667,12.25,5.1429,8.2857,9.25,8.8333,5.0,10.7917,8.8,8.375,7.6667,3.0,3.75,6.0,10.0,3.5,8.75,11.5,3.0,4.75,4.25,4.75,11.2,5.6,7.8,5.25,3.5,4.0,4.0,3.4,5.5,5.25,3.0,3.0,3.6,4.75,9.5,6.8571,6.8,5.0,7.75,3.0,3.0,3.0,3.3333,3.25,4.0,3.3333,3.0,3.5,4.4,3.0,4.6,6.3333,8.8333,12.7143,12.6,14.5,7.75,4.8889,4.3333,3.0,3.0,4.5,4.0,3.5,4.25,5.0,5.0,3.2,3.8333,4.0,4.8,3.5,5.0,8.5714,5.1429,4.25,10.5,12.7,4.0,4.0,3.1667,6.6,3.4,4.6667,4.6667,3.0,4.4444,7.0,11.0,3.0,3.0,3.0,10.25,14.3,4.75,5.75,3.0,4.0,11.125,4.7143,4.25,4.7,5.4545,6.5,8.0,5.0,3.8,5.1667,13.75,4.7143,3.0,3.0,3.5,4.125,3.4286,3.25,3.8,3.4,3.25,5.0,6.7143,13.0,6.7143,12.8333,6.7143,4.7143,5.4,3.5,3.4286,7.0,6.4545,5.2,5.0,3.0,4.1429,12.625,7.8333,4.0,4.2727,4.6923,8.625,13.4,10.6667,11.8333,5.7143,3.5,4.0,4.3333,3.25,3.4,3.0,6.8889,7.4444,9.0,5.5,5.25,6.2857,8.1429,6.8,11.2,6.25,5.0,4.7778,3.25,4.8333,8.8,9.4,11.1667,4.2857,3.0,4.2727,4.5556,7.1111,3.4286,4.25,3.5,3.4,3.2857,5.0,10.6667,3.0,3.4,3.75,5.5,5.2,5.8333,6.75,6.8,3.25,4.1429,4.5,6.6667,3.75,6.8,13.3333,7.6667,5.5,7.0,3.0,2.875,7.75,3.0,4.5556,4.5,13.8485,4.4286,5.1667,3.4,4.0,3.5,4.75,12.2222,8.0,3.75,4.6667,5.4,3.5714,4.5556,4.2,5.7692,8.8,3.3,4.3333,5.75,6.0,6.1,6.0,3.25,8.6154,2.6667,3.625,3.4,5.75,6.0,8.5,13.4,15.3125,6.45,12.4583,7.5789,14.1429,3.4,8.8182,3.75,4.3077,8.1667,3.0,4.1111,2.75,7.0,4.3333,5.0,11.8125,5.3077,5.9,16.0,16.0,3.5714,8.8333,5.1538,14.3469,5.1111,7.8,5.5,15.2326,6.45,14.069,3.3077,5.9,3.75,6.6154,14.359,16.0577,4.125,3.5,4.4545,14.5,8.48,3.0,7.1,9.6667,14.15]}}}
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/image/joel-harness-pairing.png b/content/articles/multi-harness-rl/app/src/content/assets/image/joel-harness-pairing.png
new file mode 100644
index 00000000..5c9d85bd
Binary files /dev/null and b/content/articles/multi-harness-rl/app/src/content/assets/image/joel-harness-pairing.png differ
diff --git a/content/articles/multi-harness-rl/app/src/content/assets/image/placeholder.png b/content/articles/multi-harness-rl/app/src/content/assets/image/placeholder.png
new file mode 100644
index 00000000..6bc17e77
Binary files /dev/null and b/content/articles/multi-harness-rl/app/src/content/assets/image/placeholder.png differ
diff --git a/content/articles/multi-harness-rl/app/src/content/bibliography.bib b/content/articles/multi-harness-rl/app/src/content/bibliography.bib
new file mode 100644
index 00000000..517ae6fe
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/bibliography.bib
@@ -0,0 +1,314 @@
+@misc{kolavi2026rlenvironments,
+ title = {The ultimate guide to RL environments: building and scaling them in the LLM era},
+ author = {Kolavi, Adithya S and Tunstall, Lewis and von Werra, Leandro and
+ Gallou{\'e}dec, Quentin and Dirhoussi, Amine and Burtenshaw, Ben and
+ Paniego, Sergio},
+ year = {2026},
+ howpublished = {\url{https://huggingface.co/spaces/AdithyaSK/rl-environments-guide}},
+ note = {Hugging Face Space}
+}
+
+@misc{openenv2026,
+ title = {{OpenEnv}: Agentic Execution Environments},
+ author = {{Meta PyTorch} and {Hugging Face}},
+ year = {2025},
+ howpublished = {\url{https://github.com/huggingface/OpenEnv}}
+}
+
+@misc{openenv2026harbor,
+ title = {Harbor integration: serve {Harbor} task datasets through {OpenEnv} as trainable environments},
+ author = {Kolavi, Adithya S},
+ year = {2026},
+ howpublished = {\url{https://github.com/huggingface/OpenEnv/pull/1036}},
+ note = {OpenEnv pull request \#1036}
+}
+
+@misc{trl,
+ title = {{TRL}: Transformers Reinforcement Learning},
+ author = {von Werra, Leandro and Belkada, Younes and Tunstall, Lewis and
+ Beeching, Edward and Thrush, Tristan and Lambert, Nathan and
+ Huang, Shengyi and Rasul, Kashif and Gallou{\'e}dec, Quentin},
+ year = {2020},
+ howpublished = {\url{https://github.com/huggingface/trl}}
+}
+
+@misc{bytedance2025seedoss,
+ title = {{Seed-OSS-36B-Instruct} Model Card},
+ author = {{ByteDance Seed}},
+ year = {2025},
+ howpublished = {\url{https://huggingface.co/ByteDance-Seed/Seed-OSS-36B-Instruct}}
+}
+
+@article{cohere2025commanda,
+ title = {Command A: An Enterprise-Ready Large Language Model},
+ author = {{Cohere}},
+ journal = {arXiv preprint arXiv:2504.00698},
+ year = {2025},
+ month = apr,
+ eprint = {2504.00698},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2504.00698}
+}
+
+@misc{anthropic2024swebench,
+ title = {Raising the bar on {SWE}-bench {V}erified with {C}laude 3.5 {S}onnet},
+ author = {{Anthropic}},
+ year = {2024},
+ howpublished = {\url{https://www.anthropic.com/engineering/swe-bench-sonnet}}
+}
+
+@article{yu2026openforgerl,
+ title = {{OpenForgeRL}: Train Harness-native Agents in Any Environment},
+ author = {Yu, Xiao and Peng, Baolin and Xu, Ruize and Zou, Hao and Wu, Qianhui and Cheng, Hao and Yao, Wenlin and Singh, Nikhil and Yu, Zhou and Gao, Jianfeng},
+ journal = {arXiv preprint arXiv:2607.21557},
+ year = {2026},
+ eprint = {2607.21557},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2607.21557}
+}
+
+@article{peng2026orchard,
+ title = {Orchard: An Open-Source Agentic Modeling Framework},
+ author = {Peng, Baolin and Yao, Wenlin and Wu, Qianhui and Cheng, Hao and Yu, Xiao and Yang, Rui and Ge, Tao and Sordoni, Alessandro and Yuan, Xingdi and Shen, Yelong and He, Pengcheng and Zhang, Tong and Yu, Zhou and Gao, Jianfeng},
+ journal = {arXiv preprint arXiv:2605.15040},
+ year = {2026},
+ eprint = {2605.15040},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2605.15040}
+}
+
+@article{kwaikat2026katcoder,
+ title = {{KAT-Coder-V2.5} Technical Report},
+ author = {{KwaiKAT Team}},
+ journal = {arXiv preprint arXiv:2607.05471},
+ year = {2026},
+ eprint = {2607.05471},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2607.05471}
+}
+
+@article{xu2026polar,
+ title = {Polar: Agentic {RL} on Any Harness at Scale},
+ author = {Xu, Binfeng and Zhang, Hao and Zhang, Shaokun and Han, Songyang and Liu, Mingjie and Hu, Jian and Diao, Shizhe and Jin, Zhenghui and Zou, Yunheng and Demoret, Michael and Kautz, Jan and Dong, Yi},
+ journal = {arXiv preprint arXiv:2605.24220},
+ year = {2026},
+ eprint = {2605.24220},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2605.24220}
+}
+
+@article{kimi2025k2,
+ title = {{Kimi K2}: Open Agentic Intelligence},
+ author = {{Kimi Team}},
+ journal = {arXiv preprint arXiv:2507.20534},
+ year = {2025},
+ eprint = {2507.20534},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2507.20534}
+}
+
+@misc{minimax2025m2card,
+ title = {{MiniMax-M2} Model Card},
+ author = {{MiniMax}},
+ year = {2025},
+ howpublished = {\url{https://huggingface.co/MiniMaxAI/MiniMax-M2}}
+}
+
+@article{minimax2026m2series,
+ title = {The {MiniMax-M2} Series: Mini Activations Unleashing Max Real-World Intelligence},
+ author = {{MiniMax}},
+ journal = {arXiv preprint arXiv:2605.26494},
+ year = {2026},
+ eprint = {2605.26494},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2605.26494}
+}
+
+@article{deepseek2025v32,
+ title = {{DeepSeek-V3.2}: Pushing the Frontier of Open Large Language Models},
+ author = {{DeepSeek-AI}},
+ journal = {arXiv preprint arXiv:2512.02556},
+ year = {2025},
+ eprint = {2512.02556},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2512.02556}
+}
+
+@article{openai2025gptoss,
+ title = {{gpt-oss-120b} and {gpt-oss-20b} Model Card},
+ author = {{OpenAI}},
+ journal = {arXiv preprint arXiv:2508.10925},
+ year = {2025},
+ eprint = {2508.10925},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2508.10925}
+}
+
+@misc{minimax2025m1card,
+ title = {{MiniMax-M1-80k} Model Card},
+ author = {{MiniMax}},
+ year = {2025},
+ howpublished = {\url{https://huggingface.co/MiniMaxAI/MiniMax-M1-80k}}
+}
+
+@misc{swebench2026experiments,
+ title = {{SWE-bench} Experiments Repository and Leaderboards},
+ author = {{SWE-bench Team}},
+ year = {2026},
+ howpublished = {\url{https://github.com/SWE-bench/experiments}}
+}
+
+@article{vats2026scaffoldeffect,
+ title = {The Scaffold Effect in Coding Agents: Harness Choice as a Hidden Variable in Coding-Agent Evaluation},
+ author = {Vats, Naman and Golev, Oleg},
+ journal = {arXiv preprint arXiv:2607.22585},
+ year = {2026},
+ eprint = {2607.22585},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2607.22585}
+}
+
+@article{zhang2026stopcomparing,
+ title = {Stop Comparing {LLM} Agents Without Disclosing the Harness},
+ author = {Zhang, Yunbei and Wang, Janet and Ge, Yingqiang and Xu, Weijie and Hamm, Jihun and Reddy, Chandan K.},
+ journal = {arXiv preprint arXiv:2605.23950},
+ year = {2026},
+ eprint = {2605.23950},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2605.23950}
+}
+
+@article{he2026agentlightning,
+ title = {{Agent Lightning v1.0}: Towards Harnessed Agentic {RL}},
+ author = {He, Zhiyuan and Zhang, Siwei and Zhou, Zhiwen and Yang, Yuqing and Kang, Yu and Zhang, Yuge and Qiu, Luna K. and Tsui, Tin Yan and Xu, Jiahang and Luo, Chong},
+ journal = {arXiv preprint arXiv:2608.17528},
+ year = {2026},
+ eprint = {2608.17528},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2608.17528}
+}
+
+@misc{brand2025benchmarking,
+ title = {Why benchmarking is hard},
+ author = {Brand, Florian and Denain, Jean-Stanislas},
+ year = {2025},
+ howpublished = {Epoch AI Gradient Updates, \url{https://epoch.ai/gradient-updates/why-benchmarking-is-hard}}
+}
+
+@misc{poolside2026lagunas21,
+ title = {Introducing {Laguna S 2.1}},
+ author = {{Poolside}},
+ year = {2026},
+ howpublished = {\url{https://poolside.ai/blog/introducing-laguna-s-2-1}}
+}
+@article{poolside2026laguna,
+ title = {{Laguna M.1/XS.2} Technical Report},
+ author = {{Poolside}},
+ journal = {arXiv preprint arXiv:2605.27605},
+ year = {2026},
+ eprint = {2605.27605},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2605.27605}
+}
+@misc{zai2025glm47,
+ title = {{GLM-4.7}},
+ author = {{Z.ai}},
+ year = {2025},
+ howpublished = {\url{https://huggingface.co/zai-org/GLM-4.7}}
+}
+@misc{gallouedec2026tito,
+ title = {Agentic {RL}: Token-In, Token-Out Done Right},
+ author = {Gallou{\'e}dec, Quentin and Rasul, Kashif},
+ year = {2026},
+ month = {5},
+ howpublished = {\url{https://huggingface.co/blog/huggingface/tito}},
+ note = {Hugging Face blog}
+}
+
+@article{kimi2026k3,
+ title = {{Kimi K3}: Open Frontier Intelligence},
+ author = {{Kimi Team} and Bai, Tongtong and Bai, Yifan and Bao, Yiping and others},
+ journal = {arXiv preprint arXiv:2607.24653},
+ year = {2026},
+ eprint = {2607.24653},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2607.24653}
+}
+
+@article{qwen2026codernext,
+ title = {{Qwen3-Coder-Next} Technical Report},
+ author = {{Qwen Team}},
+ journal = {arXiv preprint arXiv:2603.00729},
+ year = {2026},
+ eprint = {2603.00729},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2603.00729}
+}
+
+@misc{xiaomi2026mimov26,
+ title = {{MiMo-V2.6}: Scaling Reinforcement Learning Towards Self-Improvement},
+ author = {{LLM-Core Xiaomi}},
+ year = {2026},
+ howpublished = {\url{https://huggingface.co/XiaomiMiMo/MiMo-V2.6-Pro-RL/blob/main/MiMo_V2_6_technical_report.pdf}}
+}
+
+@article{shao2024deepseekmath,
+ title = {{DeepSeekMath}: Pushing the Limits of Mathematical Reasoning in Open Language Models},
+ author = {Shao, Zhihong and Wang, Peiyi and Zhu, Qihao and Xu, Runxin and Song, Junxiao and Bi, Xiao and Zhang, Haowei and Zhang, Mingchuan and Li, Y. K. and Wu, Y. and Guo, Daya},
+ journal = {arXiv preprint arXiv:2402.03300},
+ year = {2024},
+ eprint = {2402.03300},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2402.03300}
+}
+
+@inproceedings{kwon2023vllm,
+ title = {Efficient Memory Management for Large Language Model Serving with {PagedAttention}},
+ author = {Kwon, Woosuk and Li, Zhuohan and Zhuang, Siyuan and Sheng, Ying and Zheng, Lianmin and Yu, Cody Hao and Gonzalez, Joseph E. and Zhang, Hao and Stoica, Ion},
+ booktitle = {Proceedings of the 29th Symposium on Operating Systems Principles},
+ year = {2023},
+ eprint = {2309.06180},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2309.06180}
+}
+
+@inproceedings{jimenez2024swebench,
+ title = {{SWE}-bench: Can Language Models Resolve Real-World {GitHub} Issues?},
+ author = {Jimenez, Carlos E. and Yang, John and Wettig, Alexander and Yao, Shunyu and Pei, Kexin and Press, Ofir and Narasimhan, Karthik},
+ booktitle = {The Twelfth International Conference on Learning Representations},
+ year = {2024},
+ eprint = {2310.06770},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2310.06770}
+}
+
+@article{deng2025swebenchpro,
+ title = {{SWE-Bench Pro}: Can {AI} Agents Solve Long-Horizon Software Engineering Tasks?},
+ author = {Deng, Xiang and Da, Jeff and Pan, Edwin and He, Yannis Yiming and others},
+ journal = {arXiv preprint arXiv:2509.16941},
+ year = {2025},
+ eprint = {2509.16941},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2509.16941}
+}
+
+@article{merrill2026terminalbench,
+ title = {{Terminal-Bench}: Benchmarking Agents on Hard, Realistic Tasks in Command Line Interfaces},
+ author = {Merrill, Mike A. and Shaw, Alexander G. and Carlini, Nicholas and Li, Boxuan and others},
+ journal = {arXiv preprint arXiv:2601.11868},
+ year = {2026},
+ eprint = {2601.11868},
+ archivePrefix = {arXiv},
+ url = {https://huggingface.co/papers/2601.11868}
+}
+
+@article{williams1992reinforce,
+ title = {Simple statistical gradient-following algorithms for connectionist reinforcement learning},
+ author = {Williams, Ronald J.},
+ journal = {Machine Learning},
+ volume = {8},
+ number = {3},
+ pages = {229--256},
+ year = {1992},
+ doi = {10.1007/BF00992696}
+}
diff --git a/content/articles/multi-harness-rl/app/src/content/chapters/basics.mdx b/content/articles/multi-harness-rl/app/src/content/chapters/basics.mdx
new file mode 100644
index 00000000..85365e10
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/chapters/basics.mdx
@@ -0,0 +1,174 @@
+import HtmlEmbed from "../../components/HtmlEmbed.astro";
+import Note from "../../components/Note.astro";
+
+## The basics
+
+### What is a harness?
+
+A 2026 position paper on benchmark disclosure defines an agent harness as "the software layer
+between the model and the task that constructs the context the model sees, mediates its tool calls,
+validates its outputs, and decides when to retry, escalate, or stop" [@zhang2026stopcomparing]. To
+the model, the harness is its interface to the task. To a trainer, it is a program someone else
+wrote, which the trainer cannot change.
+
+A few neighbouring terms are easy to mix up with it, so this is how the article uses them:
+
+- The **policy** is the model being trained: the weights that turn a prompt into a completion, with
+ no loop and no memory between calls. We also call it the model.
+- The **agent** is the policy running inside a harness, the whole program that plans, calls tools
+ and decides when to stop.
+- The **sandbox** is the isolated place where actions actually run, such as a container, a VM or a
+ tmux session.
+- A **benchmark** is a fixed task set plus a protocol for comparing results on it.
+
+Hugging Face's [agent glossary](https://huggingface.co/blog/agent-glossary) covers the same terms in
+more detail.
+
+The model chooses the tool and its arguments. The harness executes the call and adds the result
+back into the model's context before asking it what to do next.
+
+{/* VIZ: d3-harness-anatomy: one harness opened up. Its own loop, its own tool surface, its own
+ sandbox, and exactly one wire leaving it toward an OpenAI-compatible endpoint. Everything
+ inside the box is the harness's business; the single wire is the only surface a trainer can
+ stand on. */}
+
+
+### What is agentic RL?
+
+Agentic RL is reinforcement learning for a model that acts over many steps. The actions are tool
+calls, the observations are tool results, and the reward comes from a grader that looks at the
+finished trajectory. A single rollout has this shape: write, run, read the result, decide what to try
+next, and eventually submit something a grader can score. The trainer runs a group of these
+rollouts for each task and learns from how their rewards compare.
+
+
+
+### What are RL environments?
+
+An **RL environment** is the stateful system a policy acts on. The environment takes an action,
+updates its internal state, returns an observation, and at some point returns a reward. So the
+environment owns the state, the transitions and the reward, while a harness runs the loop.
+
+The figure below puts the training loop, which samples, scores and updates, next to the environment
+it acts on.
+
+{/* Carried over from the previous guide so the two articles share one picture of the anatomy. */}
+
+
+The environment box holds a task set, a prompt template, tools, observations, an execution backend,
+world state, a reward rule and a termination condition. Frameworks do not always give each piece a
+name. In the box labelled "Tools / Harness", harness means the tool surface, not the whole agent
+program. This figure and the rollout above come from
+[The ultimate guide to RL environments](https://huggingface.co/spaces/AdithyaSK/rl-environments-guide),
+which covers each component with an example if you want to go deeper.
+
+#### White-box vs black-box RL environments
+
+*Who actually drives the rollout?*
+
+In a **white-box** environment, the trainer owns the loop. The trainer samples an action, calls
+`env.step()`, reads the observation, and then samples again. The environment waits to be called.
+Every token the policy produced is already in the trainer's hands, because the trainer is the one
+that sampled it. All six frameworks compared in the earlier guide work this way, and it is what TRL's
+[`GRPOTrainer`](https://huggingface.co/docs/trl/grpo_trainer) expects when you hand it an environment factory.
+
+In a **black-box** environment, the harness owns the loop. It starts inside a sandbox, runs its
+own loop, calls its own tools, compacts its own context, and stops when it decides to. It is an agent
+program that was never written with training in mind. The trainer is outside that box. All it sees is a sequence of calls arriving at a model endpoint.
+
+Microsoft's Agent Lightning team named the two modes [@he2026agentlightning]:
+
+> "In traditional agentic RL, the training engine owns the environment interaction loop. In
+> harnessed agentic RL, the harness owns this loop, while the training engine observes only a
+> sequence of LLM request-response pairs."
+
+Harnesses differ wildly on the inside, but every one of them has to call a model, and the model API
+is the one interface guaranteed to exist outside the harness. Everything else, the harness's state
+and the environment's, is hidden from the trainer.
+
+
+The KAT-Coder report [@kwaikat2026katcoder] uses these terms to describe how a harness organises
+its trajectories. It calls Mini-SWE-Agent white-box because it has a simple loop without trajectory
+compression, and Claude Code, Codex, OpenClaw and OpenHands black-box because they compress and
+reorganise their context. In this article, the distinction is who owns the rollout loop: the trainer
+or the harness. Context handling and ownership of the loop are separate choices.
+
+
+Moving the loop into the harness changes two things. The first is who makes each call, and in what
+order.
+
+{/* VIZ: d3-who-drives: sequence diagram, toggled between the two architectures. Arrow colour is
+ the initiating actor and the right rail marks whether the trainer observes each event, so the
+ asymmetry shows up as a count rather than as an assertion. */}
+
+
+The second is what the trainer ends up recording. A harness could report events the trainer did not
+cause, but nothing obliges it to. The figure below shows one rollout three times: as it happened, and
+as each architecture records it.
+
+{/* VIZ: d3-trainer-record: the same episode as three columns, ground truth against the record
+ each architecture leaves. Hatched rows never reach the trainer; dashed rows survive only as
+ text echoed into the next prompt. */}
+
+
+#### Why rewards are not enough
+
+An on-policy policy gradient [@williams1992reinforce] needs two things per token: which token was
+sampled, and the probability it was sampled with. A harness that hands back text and a score gives
+you neither.
+
+Different token sequences can produce the same text. If you save only the text and tokenize it
+again, you can get different token IDs from those the model sampled. TRL's
+[write-up](https://huggingface.co/blog/huggingface/tito) puts it directly: "in RL, you optimize on the
+exact tokens the model produced" [@gallouedec2026tito].
+
+When a harness builds the next prompt, it may add role markers or change whitespace in a tool call.
+Some harnesses also repair malformed JSON. If the trainer learns from this edited text, it may
+update the model using a response it never generated.
+
+The probabilities need to be recorded during generation too. In asynchronous RL, the model may
+have been updated since a rollout was sampled. Recomputing log probabilities with the current
+weights does not recover the probabilities used during that rollout. Importance sampling compares
+the current probability with the one used during generation; using the current value for both
+would make their ratio 1 even when the policies differ.
+
+
+
+To avoid both errors, the sampler must return token ids and per-token log probabilities, and the
+trainer must keep them. Never re-encode text you decoded. Keep the sampled ids and append the
+template's suffix by id concatenation. This works because most chat templates extend token for token
+when a tool message is appended, and eighteen of the nineteen models tested in TRL's write-up do so
+unmodified. It is also why a system that wants the exact tokens has to intercept at the
+model endpoint rather than at the text boundary.
+
+Other teams keep exact tokens from the trainer's side instead. Prime Intellect's
+[renderers](https://github.com/PrimeIntellect-ai/renderers) library turns a model's chat template into
+Python code that renders messages to token ids and extends a multi-turn rollout without re-rendering
+what the model sampled. It needs the trainer to own the loop, and its
+[announcement](https://www.primeintellect.ai/blog/renderers) warns that harnesses which repair tool
+calls or compact their history break it, which is the case the capture proxy is built for.
+
+Not everyone agrees the token-level machinery is necessary at all. OpenForgeRL builds the same proxy architecture and reconstructs training samples from the prompt
+and response pairs the proxy collects. The paper never mentions token ids or log probabilities, and
+it still reports the strongest multi-harness results published so far [@yu2026openforgerl]. This
+article takes the token-faithful side, and the question is not settled.
diff --git a/content/articles/multi-harness-rl/app/src/content/chapters/conclusions.mdx b/content/articles/multi-harness-rl/app/src/content/chapters/conclusions.mdx
new file mode 100644
index 00000000..0d007a34
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/chapters/conclusions.mdx
@@ -0,0 +1,37 @@
+## Conclusions
+
+*Results compiled through 29 September 2026. Article updated 1 October 2026.*
+
+**Multi-harness RL improved both task performance and tool use in our small-model experiments.**
+On SmolDataEnvs, LFM2.5-2.6B went from 42.2% to 54.2% pass@1, with gains under all four harnesses:
+OpenCode, Claude Code, Codex and Mini-SWE-Agent. Training only in OpenCode also improved the model,
+including in other harnesses, but the multi-harness run spread its efficiency gains across all four.
+A small open model can learn to work better with several agent interfaces through the same training
+pipeline.
+
+The tool-efficiency reward made the way an agent solves a task part of the training objective.
+Our earlier Qwen runs rewarded correctness alone, and some learned to make increasingly long
+sequences of tool calls. For LFM, we added a small bonus for correct solutions that used fewer
+calls. Tool calls fell during training. At the final checkpoint, the multi-harness model used
+**31% fewer tool calls on held-out tasks that both it and the base model solved**, with savings
+under every harness. The bonus also gave GRPO a learning signal when all scorable rollouts in a
+group were correct but took different numbers of calls. Correctness remained essential: a wrong
+answer earned no efficiency bonus.
+
+OpenEnv's capture proxy makes this practical without rewriting the harnesses or building a separate
+training loop for each one. With a compatible inference backend, it records the exact prompt and
+completion tokens, generation log probabilities and loss masks that training needs. Harbor supplies
+the tasks and sandboxes, and the same TRL trainer learns from the resulting traces. The harness
+keeps its own tools, context management and execution loop. Changing which harness produces the
+rollout does not require reimplementing those behaviours inside the trainer.
+
+These are small experiments on one task family, with one seed per setup and unequal data and compute
+exposure. They support the gains over the base model, but do not establish a general ranking of
+harness mixes. RL produced larger accuracy gains than SFT in these runs; SFT also reduced tool use.
+We did not run LFM without the efficiency bonus, so its contribution cannot be separated from the
+other changes to the recipe.
+
+The broader opportunity is to train for both successful answers and efficient execution across the
+interfaces people actually use. This work provides an open, working path to do that: keep the real
+harnesses, capture the evidence needed for learning, and improve the model through its own
+interactions with them.
diff --git a/content/articles/multi-harness-rl/app/src/content/chapters/introduction.mdx b/content/articles/multi-harness-rl/app/src/content/chapters/introduction.mdx
new file mode 100644
index 00000000..72c6f6bf
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/chapters/introduction.mdx
@@ -0,0 +1,55 @@
+import Image from "../../components/Image.astro";
+import joelPlot from "../assets/image/joel-harness-pairing.png";
+
+## Introduction
+
+If you use AI to write code, you have probably used the same model through more than one tool:
+Claude models in [Claude Code](https://code.claude.com/docs/en/overview), [Cursor](https://cursor.com), [Pi](https://pi.dev), or [Cline](https://cline.bot). And you may have noticed it doesn't behave
+the same way in each. It plans differently, reaches for different tools, and finishes tasks in one
+that it gets stuck on in another.
+
+The difference can be measured, and it comes from the program wrapped around the model, called an
+**agent harness**. It runs the loop, decides which tools the model gets, writes the context the
+model reads, makes sense of what the model sends back, and decides when to stop. Change the harness,
+and you change what the model sees and what it is allowed to do, so the results change too. On
+SWE-bench Pro, [Joel Niklaus measured](https://x.com/joelniklaus/status/2085725862142623875) the same
+model, GLM-5.2, at 23% in one harness and 52% in another. The ranking doesn't carry over between
+models either: Codex was the second-best of ten harnesses for GLM-5.2 and the ninth for Gemma 4
+26B-A4B.
+
+
+
+The harness matters most for the open-weight models you run yourself. A model that was never
+trained in your harness can struggle there. It calls tools the harness doesn't have, or writes
+output the harness can't read. Training it in one harness doesn't fix this either, because it then
+learns that harness's habits and struggles in the others.
+
+The fix is to train the model inside the harnesses themselves, with reinforcement learning on
+multi-step tool use, which is called **agentic reinforcement learning (agentic RL)**. The question
+behind this work is whether you can take a small open model, train it that way inside the harnesses
+people actually use, such as Claude Code, Codex and OpenCode, and see it improve across all of them.
+The harnesses run exactly as they ship: none of them was written with training in mind, and we don't
+change their code. This article introduces an open
+framework that makes it possible, built on [OpenEnv](https://github.com/huggingface/OpenEnv), with
+[Harbor](https://harborframework.com/) supplying the tasks and sandboxes and [TRL](https://github.com/huggingface/trl) as the trainer.
+
+The article has two parts. The first is a guide: [the basics](#the-basics) of harnesses, agentic RL and
+RL environments, then [why models need training across harnesses](#why-train-across-harnesses), with
+what recent papers and model reports show. The second is our own work: [how the framework
+works](#how-the-framework-works), and [what happened when we used it](#training-small-models-across-harnesses) to
+train a small open model, [LFM2.5-2.6B](https://huggingface.co/LiquidAI/LFM2.5-2.6B), across four harnesses at once.
+Trained across all four, it went from solving 42% of held-out tasks to 54%, averaged over the four
+harnesses, and used 31% fewer tool calls on the tasks it already solved. The same model trained in
+OpenCode alone improved mostly in OpenCode, and much less in the three harnesses it never trained in.
+We also compare RL with **supervised fine-tuning (SFT)** on a larger model's successful rollouts,
+which helped less. Our earlier runs with [Qwen3.5-2B](https://huggingface.co/Qwen/Qwen3.5-2B) are in
+a collapsible section of the training chapter.
diff --git a/content/articles/multi-harness-rl/app/src/content/chapters/openenv.mdx b/content/articles/multi-harness-rl/app/src/content/chapters/openenv.mdx
new file mode 100644
index 00000000..46b66530
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/chapters/openenv.mdx
@@ -0,0 +1,390 @@
+import Accordion from "../../components/Accordion.astro";
+import HtmlEmbed from "../../components/HtmlEmbed.astro";
+
+## How the framework works
+
+The framework has three parts. OpenEnv is the interface the harness, the environment and the trainer
+all connect to, and its capture proxy records the tokens. Harbor supplies the tasks and the
+sandboxes. TRL trains on what the proxy recorded. This chapter follows one rollout through all of
+them, then covers each part in turn.
+
+### OpenEnv
+
+OpenEnv is a standard interface for RL environments [@openenv2026]. It offers [Gymnasium](https://gymnasium.farama.org)-style `reset`, `step`, and
+`state` over a client and server transport, with typed actions and observations, packaged as Docker
+and publishable to the Hub. It began as a Meta PyTorch and Hugging Face collaboration and now lives
+at [`huggingface/OpenEnv`](https://github.com/huggingface/OpenEnv), steered by a committee of twelve organisations, under BSD-3-Clause.
+
+In this article, OpenEnv is the shared interface that the harness, the environment, and the trainer
+all connect to. It does not train anything or define rewards. The **capture proxy** records what RL
+training needs from any agent that calls a model API. The **Harbor integration** [@openenv2026harbor]
+serves Harbor's containerised tasks as OpenEnv environments, with the harness and sandbox chosen per rollout.
+
+Training through the typed `TrainingTrace` API needs [OpenEnv 0.7.0](https://github.com/huggingface/OpenEnv/releases/tag/v0.7.0)
+or later and **Python 3.12** for the Harbor extra. On Python 3.10 or 3.11, `openenv[harbor]` does not
+install the Harbor dependencies. The `harbor_env` package also comes from the repository's `envs/`
+directory, not the OpenEnv wheel. For a checkout from main:
+
+```bash
+# Run in a Python 3.12 virtual environment.
+git clone --depth 1 --branch main https://github.com/huggingface/OpenEnv.git
+python -m pip install -e './OpenEnv[harbor]'
+export PYTHONPATH="$PWD/OpenEnv/envs${PYTHONPATH:+:$PYTHONPATH}"
+```
+
+See TRL's [Harbor example](https://github.com/huggingface/trl/tree/main/examples/async_grpo_harbor)
+for the trainer and inference-server dependencies and the two-GPU launch commands.
+
+### One rollout, end to end
+
+Before looking at each part, here is the whole path. The figure follows one rollout, three turns
+long, of a task from [SmolDataEnvs](https://huggingface.co/collections/FineEnvs/smoldataenvs), our
+suite of data-analysis tasks built from real Kaggle notebooks, through every part of the system. Switching the harness changes only how Harbor wires it to the proxy, the format of its model
+calls and replies, and the names of its tools.
+
+
+
+The trainer and the inference server, vLLM [@kwon2023vllm], each get their own GPU. The OpenEnv
+server can run next to them or on a Hugging Face Space, and every rollout gets its own sandbox. The
+hardware for our runs is in the [training setup](#setup). The harness inside the sandbox never talks
+to vLLM or to the trainer. It reaches the model only through the capture proxy, and the only
+credential it holds for that is a session id.
+
+### The capture proxy
+
+A harness finds the proxy the way it would find any model provider, through a base URL and an API
+key. The API key is a capture session id minted for that rollout, which is how one proxy on one port
+serves every concurrent rollout. A call carrying a key the proxy never registered gets a 401.
+
+
+
+Harnesses speak four different model APIs, as the [harness
+table](#harness-ecosystem-matrix) shows. The proxy works out which one a request uses from its path,
+then its headers, then the shape of its body.
+It converts the request to chat completions with converters vendored from NVIDIA's Polar gateway [@xu2026polar],
+and replays the answer in the caller's own format.
+
+
+
+The call to the engine never streams. The proxy waits for the whole completion, stores it, and
+replays it to the harness as a stream if the harness asked for one. Apart from a later first token,
+the harness cannot tell the difference, and the proxy never has to parse partial deltas.
+
+#### What gets recorded
+
+For each call the proxy asks the engine for the prompt's token ids, the sampled token ids and a
+logprob for every sampled token. It also samples from the full distribution, with no `top_p` or
+`top_k`. Truncating the distribution biases what the policy samples away from what it would sample
+on its own, which in RL is a known route to entropy collapse, and vLLM computes its processed
+logprobs after the truncation, so they would not match the policy either. In our runs, moving `top_p`
+to 1.0 took the importance ratio from 0.985–0.993 to 0.9984–0.9999. On vLLM this needs two server
+flags:
+
+```bash
+vllm serve --return-tokens-as-token-ids --logprobs-mode processed_logprobs
+```
+
+[SGLang](https://github.com/sgl-project/sglang) works too.[^sglang]
+
+[^sglang]: SGLang returns the sampled token ids from v0.5.17 for non-streaming requests, and v0.5.20 adds them for streaming. The proxy never streams from the engine, so v0.5.17 or later should work, though we only tested a build from SGLang's main branch.
+
+The proxy does not take any of this on trust. The first time it sees an engine it sends a probe and
+grades the result, and it also checks whether the logprobs are raw or processed. An engine below the
+`tokens` level still serves rollouts, but they are marked as evaluation only, and asking for training
+data from one raises an error. Hosted APIs such as OpenAI, Anthropic and [Hugging Face Inference
+Providers](https://huggingface.co/docs/inference-providers/index) land there, so they can evaluate a harness but not train through it.
+
+
+
+#### From calls to training sequences
+
+Harnesses retry, spawn subagents and compact their context, so a rollout is rarely one clean
+conversation. The proxy stores each model call as a node and links it to the earlier call whose prompt and
+completion form the longest exact token prefix of its own prompt. A retry shows up as a sibling that
+never continued. A subagent has its own system prompt, so its first call extends nothing and starts a
+new root, and a compacted context does the same.
+
+
+
+Each path from a root to a leaf becomes one training sequence. Context tokens get a loss mask of 0
+and sampled tokens get 1. A turn whose logprobs are missing or misaligned stays in the sequence as
+context and is never used as a target. These checks run as each call arrives rather than at export,
+because a misaligned turn is easiest to diagnose while the proxy still knows which turn it was.
+
+
+
+#### Capturing your own harness
+
+Nothing in the proxy is specific to Harbor, so any program that calls one of the four APIs can be
+captured. Start the proxy, mint a session with `POST /sessions`, give your agent the proxy's URL as
+its base URL and the session id as its API key, and read the rollout back from
+`GET /sessions/{id}/rollout`.
+
+```bash
+python -m openenv.core.harness.capture.server --llm-url http://127.0.0.1:8000 --model
+```
+
+The proxy binds to localhost by default. On any other interface it needs an admin key, which guards
+the routes that mint and read sessions.
+
+The proxy is a single process. In our runs its health check started to starve at around 200
+concurrent sessions, and the process crashed at 320. A rollout that hits its model-call budget is
+also stopped by the proxy itself, so that final reply is never part of the training data.
+
+### Harbor
+
+[Harbor](https://harborframework.com/), from the team behind Terminal-Bench [@merrill2026terminalbench], runs agents against containerised tasks and keeps the task, the harness and the sandbox
+independent of each other. [Version 0.22.0](https://github.com/harbor-framework/harbor/releases/tag/v0.22.0) ships adapters for more than 40 agent harnesses and [26
+execution backends](https://github.com/harbor-framework/harbor/blob/v0.22.0/src/harbor/models/environment_type.py), from local Docker to [Daytona](https://www.daytona.io), [Modal](https://modal.com) and E2B. About 80 task datasets already
+use its format, including Terminal-Bench and SWE-bench [@jimenez2024swebench]. Any harness can attempt
+any task on any backend, so one task set can be trained against many harnesses, and a run is one
+command that names the dataset, the agent and the backend. Adithya wrote about why it is the right
+abstraction for coding RL in [RL Coding Environments 101: Why Harbor
+Exists](https://x.com/adithya_s_k/article/2054961319179420035).
+
+A Harbor task is a directory with an instruction, an environment and a test script. The ones below
+come from SmolDataEnvs, the task suite used for every run in this article. Its questions come from
+Kaggle notebooks in the [jupyter-agent dataset](https://huggingface.co/datasets/jupyter-agent/jupyter-agent-dataset),
+each with an automatic grader.
+Pick a task and a file to read it as the agent and the verifier see it.
+
+
+
+The task names no harness, sandbox backend or trainer, because those are chosen when it runs. The
+[Harbor visualiser](https://huggingface.co/spaces/HuggingFaceH4/harbor-visualiser) opens the whole training set the same way, all 5,000 tasks.
+
+
+
+> **Note:** Harbor calls its sandbox backends *environments*. In this article, *sandbox* means the
+> box the agent runs in, and *environment* means the OpenEnv server.
+
+### Serving Harbor through OpenEnv
+
+Harbor already runs an agent against a task and returns a score. A trainer also needs the tokens the
+policy generated and their probabilities. Harbor's [RL documentation](https://github.com/harbor-framework/harbor/blob/v0.22.0/docs/content/docs/training-workflows/rl.mdx) lists two ways to get them:
+intercept the tokens at the inference server, or have the harness return them with its result. The
+second needs a harness that puts its tokens in the result metadata, and the docs say that support is
+still being added to [Terminus 2](https://github.com/harbor-framework/harbor/blob/v0.22.0/docs/content/docs/agents/terminus-2.mdx), Harbor's own harness. The
+integration takes the first, through the capture proxy, so it works with any harness that calls a
+model API.
+
+The trainer talks to the environment over HTTP instead of running rollouts in its own process. An
+earlier in-process version let an exception from inside someone else's agent reach the training loop,
+where one crashed rank left the others waiting at the NCCL barrier indefinitely. Now nothing raises
+across that boundary. A failed rollout comes back with `ok=False` and `reward=None`, and the trainer
+handles a value instead of catching an exception.
+
+#### The four commands
+
+| Command | What it does |
+|---|---|
+| `openenv harbor info` | Reports what this machine can run: whether the engine returns token ids, which sandbox backends have working credentials, which datasets resolve and how many tasks each holds, and which harnesses are validated. Starts nothing. |
+| `openenv harbor rollout` | Runs rollouts without an environment server. If a rollout works here and fails under `serve`, the fault is in the serving layer. |
+| `openenv harbor serve` | The environment server: a Task API, a `run_rollout` tool over MCP, a web UI and the capture proxy. |
+| `openenv harbor push` | Deploys the same server to a Hugging Face Space, with configuration as Space variables and credentials as secrets. |
+
+```bash
+openenv harbor info --llm-url $LLM --dataset org/train,org/eval
+openenv harbor rollout --llm-url $LLM --dataset org/train --task-index 0 -n 5 --harness opencode --sandbox e2b
+openenv harbor serve --dataset org/train,org/eval
+openenv harbor push --llm-url $LLM --dataset org/train,org/eval --repo-id you/harbor-env
+```
+
+`rollout` requires `--llm-url` and has no default, because a stale endpoint produces rollouts that
+look fine and carry no token ids. `serve` can start without one, because each rollout names its own
+engine. That let one deployment serve both training, pointed at the trainer's vLLM, and evaluation.
+Here `-n 5` (or `--n-tasks 5`) runs tasks 0 through 4 once each, starting at `--task-index 0`.
+It does not sample five attempts at one task. The E2B example matches the reported experiments;
+the companion tutorial uses Daytona. A rollout prints one line when it finishes:
+
+```
+[opencode / e2b] task 0: 0000_369_369503_qa_1 ...
+ ok reward=1.00 turns=9 roots=2 multi-turn tokens=1043 atif=match 48s
+```
+
+
+
+The next two parts cover how each harness is wired to the proxy and how rewards and captures are
+checked. The [Harbor page of the OpenEnv docs](https://huggingface.co/docs/openenv/environments/harbor) has the full reference.
+
+
+
+#### Wiring each harness
+
+Harbor already knows how to install and launch each harness. OpenEnv adds only the place each one
+reads its model URL and API key from, kept as one table entry per harness, so supporting a new
+harness means adding an entry rather than writing a new environment. Ten harnesses have passed the
+capture contract and the trace check described below, across 16,000 rollouts on the SmolDataEnvs test
+set.
+
+
+
+Most harnesses read environment variables, and OpenEnv sets those for the agent's run only. The
+session id [Codex](https://github.com/openai/codex) receives as `OPENAI_API_KEY` therefore never reaches the verifier, which may need a
+real key of its own. [Claude Code](https://code.claude.com/docs/en/overview) and [Gemini CLI](https://github.com/google-gemini/gemini-cli) also read the variables in the server process, so
+they get them there too. [OpenCode](https://opencode.ai) and [Pi](https://pi.dev) take a config file written into the sandbox, and Terminus 2
+runs on the host and is handed the URL and key directly.
+
+#### Picking the reward, and checking the capture
+
+Harbor's verifier can return several named scores, and GRPO [@shao2024deepseekmath] needs one number per rollout. The
+integration uses the only score if there is one, or the one named `reward`, and otherwise asks the
+caller to choose. It never combines scores itself, because whatever weighting it picked would become
+the training objective. An earlier run had a `+0.2 for submitting anything` term. The policy learned
+to submit immediately, and held-out accuracy fell from 0.740 to 0.178 while training reward still
+looked healthy. When the verifier never runs, the reward stays `None` all the way to the trainer, so
+a sandbox that died is not scored as a wrong answer.
+
+The proxy's own checks can only look at data it captured. As an independent check, each rollout is
+also compared with the trajectory file Harbor writes itself, in its [ATIF](https://docs.harborframework.com/core-concepts/agents/atif) format, call by call: the
+number of turns, the completion tokens per call, and which calls count as agent steps. Any mismatch
+fails the rollout.
+
+```
+ATIF completion_tokens : [37, 36, 104, 264, 255, 119, 32, 27] total 874
+intercept turn_lengths : [37, 36, 104, 264, 255, 119, 32, 27] total 874
+ATIF step-1 prompt_tokens 7990 == intercept prompt_len 7990
+```
+
+This caught a harness that sent an empty tools array, got a 400 from the inference server and was
+cut short, leaving a rollout graph that looked well formed. The proxy now drops empty tools arrays
+before forwarding.
+
+
+
+#### Deploying it and training against it
+
+On one machine, `serve` listens on two ports, one for trainers and the web UI and one for the capture
+proxy. Sandboxes usually run somewhere else, so the proxy reaches them through a tunnel, set with
+`--expose` and [Gradio](https://www.gradio.app) by default. When the server itself runs on a Hugging Face Space, it has only
+one port, so the proxy is mounted at `/capture`. That Space has to be public, because the agent in
+the sandbox cannot send the auth header a private Space requires. It stays safe because the proxy
+only answers registered session ids, and minting one needs an admin key. The sandboxes themselves
+stay on E2B or another Harbor backend, one per rollout.
+
+Before training, raise the server's concurrency limit, `max_concurrent_envs`. It caps how many
+rollouts one server runs at once, and the default of four is fewer than the eight rollouts in each
+of our GRPO groups. Past the limit, `serve` rejects new rollouts with `CAPACITY_REACHED`.
+
+OpenEnv's `HarborSessionFactory` produces sessions in the shape TRL's `HarnessRolloutWorker`
+expects when the agent owns the loop. The worker was merged in
+[TRL #6947](https://github.com/huggingface/trl/pull/6947) and lives under `trl.experimental`.
+As of 1 October 2026, it is on main but not in the latest release, v1.14.1. Install TRL from main
+until the next release:
+
+```bash
+python -m pip install 'trl @ git+https://github.com/huggingface/trl.git@main'
+```
+
+Each session requests one rollout, waits for it, and returns a typed `TrainingTrace` through
+`fetch_training_trace()`, with token IDs, behavior logprobs and loss masks. TRL consumes that object
+directly. Given a prepared task dataset and the model's tokenizer, the worker wiring is:
+
+```python
+from functools import partial
+
+from harbor_env.harness import HarborSessionFactory
+from trl.experimental.async_grpo.openenv_harness import HarnessRolloutWorker
+
+
+def correctness_reward(outcome):
+ return outcome.env_reward # None keeps a failed verifier unscorable.
+
+
+def make_rollout_worker(dataset, tokenizer, server_url, vllm_url, model):
+ factory = partial(
+ HarborSessionFactory,
+ server_url=server_url,
+ split="FineEnvs/SmolDataEnvs-harbor-train",
+ llm_url=vllm_url,
+ model=model,
+ harness="opencode",
+ sandbox="daytona",
+ reward_key="correctness,reward",
+ )
+ return HarnessRolloutWorker(
+ harness_session_factory=factory,
+ harness_adapter=None,
+ rollout_reward_fn=correctness_reward,
+ model_name=model,
+ dataset=dataset,
+ processing_class=tokenizer,
+ reward_funcs=[],
+ num_generations=8,
+ max_inflight_tasks=8,
+ vllm_server_url=vllm_url,
+ max_tokens=4096,
+ temperature=1.0,
+ top_p=1.0,
+ top_k=-1,
+ chat_template_kwargs={"enable_thinking": False},
+ )
+```
+
+Pass this worker as `rollout_worker` to `AsyncGRPOTrainer`. The worker calls the factory with
+`sampling=` before generation, so the capture policy matches training. `harness_adapter=None`
+lets the harness run its own loop. This minimal callback forwards correctness; the
+[training setup](#what-we-rewarded) adds the tool-efficiency bonus. The
+[full TRL example](https://github.com/huggingface/trl/blob/main/examples/async_grpo_harbor/async_grpo_harbor.py)
+also shows task discovery, `AsyncGRPOConfig`, the trainer and checkpoint saving.
+
+In our runs every rollout in a GRPO group used the same harness, so the advantage compared actions
+rather than harnesses. The sampling temperature sent with each rollout also matched the one the
+trainer used to recompute logprobs.
+
+Try the [SmolDataEnvs Harbor Space](https://huggingface.co/spaces/FineEnvs/smoldataenv-multi-harness-harbor).
+Choose a task, connect a model and run a harness to inspect its rollout and traces. The
+[training tutorial](https://github.com/adithya-s-k/FineEnvs/tree/main/05-multi-harness-rl)
+includes the same environment source and commands for HF Jobs or a local cluster.
diff --git a/content/articles/multi-harness-rl/app/src/content/chapters/training.mdx b/content/articles/multi-harness-rl/app/src/content/chapters/training.mdx
new file mode 100644
index 00000000..7adb4221
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/chapters/training.mdx
@@ -0,0 +1,410 @@
+import Accordion from "../../components/Accordion.astro";
+import HtmlEmbed from "../../components/HtmlEmbed.astro";
+import Note from "../../components/Note.astro";
+
+## Training small models across harnesses
+
+We trained [LFM2.5-2.6B](https://huggingface.co/LiquidAI/LFM2.5-2.6B) with the setup from the
+previous chapter, once in OpenCode alone and once across four harnesses, and then compared both runs
+with supervised fine-tuning on a larger model's rollouts. Our first runs used
+[Qwen3.5-2B](https://huggingface.co/Qwen/Qwen3.5-2B). They shaped the LFM setup, and they are in a
+collapsible section at the end.
+
+### Setup
+
+- **Tasks.** 1,000 [SmolDataEnvs](https://huggingface.co/collections/FineEnvs/smoldataenvs) training tasks, 400 medium and 600 hard, in one fixed order shared
+ by both runs. The test set is 250 held-out tasks with no overlap in notebook, question, or
+ instruction.
+- **Two runs.** One trains in OpenCode only. The other is multi-harness ([OpenCode](https://opencode.ai), [Claude Code](https://code.claude.com/docs/en/overview),
+ [Codex](https://github.com/openai/codex), and [Mini-SWE-Agent](https://github.com/SWE-agent/mini-swe-agent)), where each GRPO [@shao2024deepseekmath] group of eight rollouts uses one of the
+ four harnesses.
+- **Evaluation.** Both runs are evaluated the same way, including under the three harnesses the
+ OpenCode-only run never trained in. Every 100 steps, each of the 250 test tasks runs once under
+ each of the four harnesses, 1,000 cells in all. A cell counts as solved when its first graded
+ attempt is correct.
+- **Recipe.** [Async GRPO](https://huggingface.co/docs/trl/async_grpo_trainer) in TRL for 1,000 steps. Async means generation and training run
+ apart. One H100 trains while a second serves the policy with vLLM [@kwon2023vllm] and keeps producing
+ rollouts, so training never waits for the slowest agent. The price is that rollouts come from
+ weights usually two optimizer steps old, and never more than four.
+- **Hardware and time.** Two H100s per run on the Hugging Face cluster, and one [E2B](https://e2b.dev) sandbox per
+ rollout with 1 CPU and 4 GB of memory. Training took about 32 hours for the OpenCode-only run and
+ 46 hours for the multi-harness run, not counting evaluation, queueing and restarts. We did not
+ track the full cost closely enough to quote it.
+
+The companion tutorial has readable [multi-harness](https://github.com/adithya-s-k/FineEnvs/blob/main/05-multi-harness-rl/train/multi_harness.py)
+and [native OpenCode](https://github.com/adithya-s-k/FineEnvs/blob/main/05-multi-harness-rl/train/opencode.py)
+training scripts, plus [HF Jobs and Slurm instructions](https://github.com/adithya-s-k/FineEnvs/blob/main/05-multi-harness-rl/REPRODUCE.md).
+The reported LFM runs used Harbor for both policies and E2B sandboxes. The current tutorial uses
+Daytona and includes native OpenCode as a separate comparison, so it is not an exact replay of
+those historical runs.
+
+Each cell runs once, so a point or two between neighbouring checkpoints is noise. Trust the trend
+across checkpoints over any single one.
+
+### Where the base model starts
+
+
+
+The baseline evaluation already shows the problem from the earlier chapters. The same weights solve
+62% of tasks under Mini-SWE-Agent, but only 33% under Claude Code. LFM2.5-2.6B was not trained in
+our four harnesses, as far as its release says: Liquid AI names Hermes Agent and OpenClaw as its
+[training harnesses](https://www.liquid.ai/blog/lfm2-5-2-6b).
+
+### What we rewarded
+
+We rewarded LFM for two things:
+
+- **Correctness:** 1 for a correct answer and 0 for a wrong one.
+- **Tool efficiency:** a small bonus, at most 0.1, for a correct answer that takes fewer calls. A
+ wrong answer still scores 0, so the model can't earn the bonus by giving up early.
+
+The bonus comes from the Qwen runs, which were rewarded for correctness alone. That gives the model no
+reason to stop exploring once it has the answer, and in an early Qwen run the tool calls per rollout
+crept from 13 to 41.
+
+
+
+The bonus is at most 0.1, but GRPO learns from differences inside a group, so even a small bonus
+counts. GRPO runs eight rollouts of the same task and trains on how each one compares with the group's
+average. When all eight are correct, correctness alone makes them identical, and the group teaches
+nothing. The bonus is then the only thing that tells them apart, and it favours the shortest
+solutions.
+
+Here are two real groups from the multi-harness run, scored both ways.
+
+
+
+In 97 of 555 OpenCode-only groups (17.5%) and 155 of 686 multi-harness groups (22.6%), every
+scorable rollout was correct but tool counts differed. The efficiency bonus supplied their only
+reward contrast. These [audited counts](/data/reward-group-audit.json) cover groups that contributed
+to the retained training updates; they exclude work discarded on resume. Tool counts come from the
+harness's own trace. The reward audit found no formula mismatches and no bonus on wrong answers.
+
+In both LFM runs, tool calls per rollout fell during training and under every harness, as the
+curves below show. There was no LFM run without the bonus, so we cannot say the bonus caused this on
+its own.
+
+### Training curves
+
+
+
+The share of correct rollouts climbs over the first few hundred steps in both LFM runs. After that
+it rises and falls with the difficulty of each stretch of tasks, which is the same for both runs
+because they follow the same task order. Tool calls fall throughout. On the test set, the OpenCode-only model performs better than the
+multi-harness model under OpenCode (58% vs 50%), while the multi-harness model performs better under
+Claude Code (49% vs 42%) and Codex (54% vs 43%).
+
+The tool-call curves look reversed between training and test. During training, the multi-harness run
+makes more calls per rollout, because its average includes Claude Code, Codex and Mini-SWE-Agent
+rollouts, and those harnesses make more calls than OpenCode even before any training. On the test set,
+both models run under all four harnesses, and there the multi-harness model makes fewer calls under
+every one of them. The OpenCode-only model learned to use fewer calls in OpenCode, and that carried
+over only in part, mainly to Codex.
+
+### Accuracy
+
+
+
+Both runs climb in accuracy, by 10 points for OpenCode-only and 12 for multi-harness, most of it in
+the first 200 steps, and hold it to step 1,000.
+They end at 52.3% for OpenCode-only and 54.2% for multi-harness. Both gains are well outside the
+noise. The 1.9-point gap between the runs is within the noise, so overall accuracy does not separate
+them.
+
+
+
+The per-harness split is the clearest result. The OpenCode-only model is best under OpenCode, the
+harness it trained in. The multi-harness model is better under Claude Code and Codex, and the two tie
+under Mini-SWE-Agent. Most of the OpenCode-only model's gain is in OpenCode (34 → 58%), while the
+multi-harness model gained under all four.
+
+### Tool calls and tokens
+
+
+
+Both models solve more tasks with fewer tool calls, and the multi-harness model cuts more, 31% fewer calls than the base model on the tasks both solved, against 11% for
+OpenCode-only. Fewer calls also means less history resent on every call, so input tokens fall with
+them. The biggest saving is under Codex, where the multi-harness model uses about half the calls. The
+clear exception is OpenCode-only under Claude Code, a harness it never trained in, where it used more
+calls and more tokens than the base model.
+
+
+
+The multi-harness model's Codex savings grow at almost every checkpoint, and by step 1,000 every
+harness is blue on all three measures. The OpenCode-only model saves steadily in OpenCode and more
+and more in Codex. Under Claude Code, it generated more tokens than the base model at nearly every
+checkpoint, up to 62% more at step 500.
+
+### How much each run saw
+
+Both runs took 1,000 optimizer steps, but they did not see the same amount of data.
+
+| | OpenCode only | Multi-harness |
+|---|---:|---:|
+| Distinct training tasks | 555 | 626 |
+| Supervised tokens | 12.5M | 18.7M |
+| Tokens processed in training | 162M | 451M |
+
+Claude Code rewrites its own history as it goes, so each of its rollouts became about eight training
+rows, each carrying its own copy of the context, while the other harnesses averaged about one. With
+one seed per run and unequal exposure, the comparison between the two runs is observational.
+
+### SFT vs RL
+
+RL is not the only way to learn from a harness. The capture that gives RL its exact tokens also
+records whole trajectories, so a larger model's successful runs can be used for supervised
+fine-tuning (SFT). We tried this on LFM2.5-2.6B to see how far imitation gets next to the RL runs
+above.
+
+#### The experiment
+
+We ran [Qwen3.8-27B](https://huggingface.co/Qwen/Qwen3.8-27B) as a teacher on SmolDataEnvs training
+tasks in all four harnesses, through the same OpenEnv and Harbor setup, with sandboxes on
+[Daytona](https://www.daytona.io). Each task got up to three attempts per harness, and we kept the
+first one the verifier graded correct. That gave 3,189 successful rollouts from 888 tasks, 377 medium
+and 511 hard.
+
+Every assistant turn in a rollout becomes one training example. The conversation up to that turn,
+tool outputs included, is the context, and the loss is on the next assistant response only. The
+examples are tokenized with LFM's own chat template, not the teacher's.
+
+We trained two SFT models from the same LFM2.5-2.6B base as the RL runs:
+
+- **OpenCode SFT** on the 801 OpenCode rollouts, 4,825 examples.
+- **Multi-harness SFT** on all 3,189 rollouts, 17,929 examples, shuffled together with no balancing
+ between harnesses.
+
+Both are full fine-tunes with TRL's SFT trainer for two epochs, at a learning rate of 3e-6 and an
+effective batch size of 8. Each epoch is evaluated on the same 1,000 test cells as the RL runs.
+
+The data is public as [SmolDataEnvs-multiharness-sft](https://huggingface.co/datasets/FineEnvs/SmolDataEnvs-multiharness-sft).
+It has one config per harness, an `all` config with the four together, and configs already tokenized
+for LFM2.5-2.6B and Qwen3.5-2B, next to a [training script](https://huggingface.co/datasets/FineEnvs/SmolDataEnvs-multiharness-sft/blob/main/train_sft.py).
+The raw teacher rollouts are in [qwen38-27b-harbor-rollouts](https://huggingface.co/datasets/FineEnvs/qwen38-27b-harbor-rollouts).
+
+The two methods learn from different things:
+
+| | SFT | RL |
+|---|---|---|
+| Learns from | the teacher's successful rollouts | its own rollouts |
+| Training signal | the teacher's next response | correctness, plus the tool-call bonus |
+| Rewards fewer tool calls | no | yes |
+| Training tasks (OpenCode / multi-harness) | 801 / 888 | 555 / 626 |
+| Supervised tokens, whole run | 1.2M / 8.2M | 12.5M / 18.7M |
+| Training time | 1.5 h / 6.5 h | 32 h / 46 h |
+
+The SFT times are the trainer's own loop and leave out collecting the teacher's rollouts, which took
+a 27B model through all four harnesses.
+
+#### Results
+
+
+
+For RL we use each run's best checkpoint on the test set: step 900 for OpenCode only and step 700
+for multi-harness. Choosing on the test set flatters RL slightly, but both are within half a point of
+their step-1,000 scores.
+
+**RL ends ahead of both SFT runs.** Multi-harness RL reaches 54.6%. The better SFT model, OpenCode
+SFT, reaches 47.5%, and multi-harness SFT 43.1%, 11.5 points below the RL model trained in the same
+four harnesses.
+
+**OpenCode SFT looks like a smaller OpenCode RL.** Almost all of its gain is in OpenCode, from 33.6%
+to 51.6%, close to OpenCode RL's 56.0%. Under Claude Code it stays where the base model was, 33.2%,
+and under Mini-SWE-Agent it drops a little, from 62.1% to 58.8%. Training in one harness mostly
+helped that harness, by imitation or by RL.
+
+**Multi-harness SFT spreads the gain, then loses it in Mini-SWE-Agent.** It improves under Claude
+Code (33.2 → 42.4%), Codex (40.0 → 46.8%) and OpenCode (33.6 → 38.0%). Under Mini-SWE-Agent it falls
+from 62.1% to 45.2%, which cancels the rest, so overall it ends at 43.1%, within the noise of the base
+model's 42.2%. After the first epoch it was below the base model, at 38.3%, with OpenCode down to
+22.4%. Multi-harness RL improved under all four, Mini-SWE-Agent included. We have not yet worked out
+why SFT hurt Mini-SWE-Agent.
+
+
+
+**SFT cuts tool calls without being rewarded for it.** SFT has no efficiency term, yet on the tasks
+both it and the base model solved, multi-harness SFT makes 24.2% fewer tool calls, with fewer calls
+under every harness. OpenCode SFT makes 8.5% fewer overall, but more than the base model under Claude
+Code (10.5% more) and Mini-SWE-Agent (4.8% more). RL shows the same pattern: training across the four
+harnesses cut calls everywhere, 26.7% for multi-harness RL at step 700 and 31.1% by step 1,000, and
+training in OpenCode alone cut them unevenly.
+
+**What this comparison can't tell us.** The two SFT runs differ in more than the harness mix. The
+multi-harness run has about seven times the supervised tokens and nearly four times the steps. SFT
+and RL also differ in objective, data source, task pool and compute, and each setup ran once. These
+are observations, not a ranking of methods. The runs we want next are SFT followed by RL against RL
+from the base model, harness mixes compared on the 692 tasks the teacher solved in all four
+harnesses, and SFT mixes with equal supervised tokens per harness.
+
+
+
+#### The runs
+
+Qwen came first, trained with the correctness-only reward on an easier pool of 1,000 tasks (150 easy,
+600 medium, 250 hard). It starts almost three times lower than LFM on the same test set, and the
+Qwen3.5 model card names no training harness.
+
+
+
+There were three Qwen runs, all evaluated on the same 1,000 test cells:
+
+- **Harbor multi-harness**, rotating the four harnesses across GRPO groups.
+- **Harbor OpenCode-only**, the same pipeline with one harness.
+- **Standalone OpenCode**, OpenCode in its own environment outside Harbor, running in [Daytona](https://www.daytona.io).
+
+
+
+
+
+#### Up, then down
+
+All three improved at first, which showed that an unmodified harness can be trained through once
+you have the exact tokens. Multi-harness more than doubled, from 14.6% to 37.0% at step 500.
+OpenCode-only peaked at 39.5% at step 700. Then both fell back to about 26% by step 1,000. The
+standalone run rose more slowly and ended at its best, 29.8%.
+
+The standalone run trained only in OpenCode, yet it gained about twice as much under Claude Code,
+Codex and Mini-SWE-Agent as under OpenCode.
+
+#### Why the two Harbor runs declined
+
+The two declines look different in the traces.
+
+- **Multi-harness ran out of output budget.** Late in training, answers grew until they hit the
+ 4,096-token output limit used in evaluation. Cells cut off at that limit rose from 9 to 556 of
+ 1,000, often a long explanation where the calculation should have been. Training allowed responses
+ up to 16,384 tokens, so the model learned a habit that evaluation cut short. Under OpenCode, calls
+ per task collapsed from 16 to 3.
+- **OpenCode-only kept working without finishing.** Calls per task rose from 17 to 21, and in the
+ cells where submission was tracked, the share that submitted an answer fell from 69% to 41%.
+- **A resume bug replayed old tasks.** After the multi-harness run restarted at step 684, almost every
+ new rollout came from a task it had already seen. That changed its late training, but the decline
+ started at step 500, before the restart, so the bug does not explain its start.
+
+#### Tool calls without a bonus
+
+With correctness as the only reward, tool calls per rollout in the Qwen OpenCode-only run doubled
+from about 10 to 20 during training, and the Qwen multi-harness run stayed between 11 and 16. In
+both LFM runs, which had the bonus, they fell.
+
+
+
+#### Other things the Qwen runs showed
+
+- **Many steps taught nothing.** Between 35% and 58% of optimizer steps had no reward contrast in any
+ group, because all eight rollouts were right or all were wrong.
+- **Claude Code dominated the training data.** Each harness got about a quarter of the
+ rollouts, but Claude Code produced 77% of the training rows and 35% of the supervised tokens.
+- **SETA**, a synchronous run in a plain bash environment with its own evaluator, went from 18.8% to
+ 38.0% in 150 steps before we stopped it on purpose.
+- **Harder data did not rescue the decline.** Continuing the multi-harness checkpoint from step 500
+ on 500 new hard tasks, on H200s through [Hugging Face Jobs](https://huggingface.co/docs/huggingface_hub/guides/jobs), did not recover the peak. Nearly two thirds of its steps had no reward
+ contrast, and even counting every ungraded cell as correct, its final score could not reach its
+ starting 37.0%.
+
+#### What changed for LFM
+
+The LFM setup changed three things. The reward gained the tool-call bonus, so all-correct groups
+still carry a signal and long, unfinished loops cost something. The training pool moved to medium and
+hard tasks. And the output cap was 4,096 tokens in both training and evaluation, so the model could
+not learn answers that evaluation would cut off.
+
+
+
+### Reproducing it
+
+- [SmolDataEnvs Multi-harness RL collection](https://huggingface.co/collections/FineEnvs/smoldataenvs-multi-harness-rl-6abdfaaa8d74dacd481d5212): everything below in one place, plus the base models.
+- Trained models: LFM [multi-harness RL](https://huggingface.co/FineEnvs/LFM2.5-2.6B-multiharness-RL) and [OpenCode-only RL](https://huggingface.co/FineEnvs/LFM2.5-2.6B-opencode-RL) at step 1,000, with the best checkpoints (steps 700 and 900) on `step-700` and `step-900` branches; LFM [multi-harness SFT](https://huggingface.co/FineEnvs/LFM2.5-2.6B-multiharness-SFT) and [OpenCode SFT](https://huggingface.co/FineEnvs/LFM2.5-2.6B-opencode-SFT) after epoch 2; and the best Qwen checkpoints, [multi-harness](https://huggingface.co/FineEnvs/Qwen3.5-2B-multiharness-RL) (step 500), [OpenCode-only](https://huggingface.co/FineEnvs/Qwen3.5-2B-opencode-RL) (step 700) and [standalone OpenCode](https://huggingface.co/FineEnvs/Qwen3.5-2B-opencode-standalone-RL) (step 1,000).
+- [Training comparison dashboard](https://huggingface.co/spaces/FineEnvs/data-agent-training-comparison-trackio): every training curve and checkpoint evaluation.
+- [Harbor environment](https://huggingface.co/spaces/FineEnvs/smoldataenv-multi-harness-harbor) and [standalone OpenCode environment](https://huggingface.co/spaces/FineEnvs/smoldataenv-multi-harness-opencode): the current companion environment servers. The standalone `opencode_env` is deprecated since OpenEnv 0.7.0 and is retained for the historical comparison; new runs should use Harbor with `harness="opencode"`.
+- [SmolDataEnvs-multiharness-sft](https://huggingface.co/datasets/FineEnvs/SmolDataEnvs-multiharness-sft): the SFT data, per harness and pre-tokenized for LFM2.5-2.6B and Qwen3.5-2B, with a training script.
+- Merged integrations: [OpenEnv #1036](https://github.com/huggingface/OpenEnv/pull/1036) for the capture proxy and Harbor, [OpenEnv #1280](https://github.com/huggingface/OpenEnv/pull/1280) for the typed `TrainingTrace` API, and [TRL #6947](https://github.com/huggingface/trl/pull/6947) for the worker. TRL's [Harbor example](https://github.com/huggingface/trl/tree/main/examples/async_grpo_harbor) shows the full trainer wiring; install TRL from main until that worker is released.
+
+
+Every run in this article uses a small model, 2 to 2.6 billion parameters, trained for 1,000 steps on
+a single seed. We are now setting up larger runs with bigger models on the same stack, and we will
+add the results here when they are in. Stay tuned.
+
diff --git a/content/articles/multi-harness-rl/app/src/content/chapters/why-multi-harness.mdx b/content/articles/multi-harness-rl/app/src/content/chapters/why-multi-harness.mdx
new file mode 100644
index 00000000..acaf077c
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/chapters/why-multi-harness.mdx
@@ -0,0 +1,177 @@
+import HtmlEmbed from "../../components/HtmlEmbed.astro";
+
+## Why train across harnesses
+
+### Benchmark scores now come with a harness attached
+
+Model cards have started saying which harness a score came from. GLM-4.7 promises "significant
+improvements on complex tasks in mainstream agent frameworks such as Claude Code, Kilo Code, Cline,
+and Roo Code" [@zai2025glm47]. Kimi K2 reports Terminal-Bench [@merrill2026terminalbench] twice: 25.0 under Terminus and 30.0
+under Moonshot's own framework [@kimi2025k2]. MiniMax M2 names a harness for almost every benchmark
+it lists [@minimax2025m2card]. DeepSeek-V3.2's thinking mode wouldn't run under Terminus at all, so
+its Terminal-Bench number had to come from a different harness [@deepseek2025v32].
+
+The harness can even change which model comes out ahead. @zhang2026stopcomparing took three models that sit
+within three points of each other on a public coding leaderboard: GLM-5.1, GPT-5.4, and Kimi K2.6.
+They ran all three on the same 100 SWE-bench Verified [@jimenez2024swebench] tasks under three
+harness configurations, with everything else held fixed. The configurations build on each other:
+**Minimal** has verbose tools, no context compression and no retries, **Improved** trims the tools
+and adds compression and retries, and **Full** adds self-checks, drift checks and rollback.
+
+
+
+Each model comes out on top under a different harness. Swapping the harness moved GLM-5.1 by 13
+points, while swapping the model inside the same harness moved the score by only 2.5 to 5.
+[Joel Niklaus found the same](https://x.com/joelniklaus/status/2085725862142623875) across ten harnesses on SWE-bench Pro.
+
+You can see the same thing outside a controlled test. Claude Opus 4.5 scores 45.9% on SWE-bench Pro [@deng2025swebenchpro]
+on Scale's [SEAL leaderboard](https://labs.scale.com/leaderboard/swe_bench_pro) and 55.4% inside Claude Code, with the same weights
+[@zhang2026stopcomparing].
+
+### When a model leaves the harness it was trained in
+
+A model can perform well in the harness it was trained in and struggle when moved to another.
+The Orchard paper measured this transfer across harnesses [@peng2026orchard]. It took two
+open coding models trained with one or two harnesses and ran them under three, including Kimi-CLI,
+which none of the systems compared had used in training. Orchard's own model ran alongside them. It
+was fine-tuned on trajectories from OpenHands and Mini-SWE-Agent, then trained with RL in
+Mini-SWE-Agent.
+
+
+
+Moving OpenSWE-32B from [OpenHands](https://github.com/OpenHands/OpenHands), the harness the paper treats as its native one, to [Mini-SWE-Agent](https://github.com/SWE-agent/mini-swe-agent)
+costs 7.5 points. Under [Kimi-CLI](https://github.com/MoonshotAI/kimi-cli), it drops 58.8 points, to 3.6% on SWE-bench Verified, and scores
+zero on Terminal-Bench 2.0. Scale-SWE stops producing valid tool calls anywhere but its home harness.
+Orchard's model stays between 45.0 and 64.3 on SWE-bench Verified across all three.
+
+The paper calls these a **degraded resolve rate**, where the model still works but solves less, and
+a **catastrophic format failure**, where its output stops being usable at all. Each row of that
+figure is one fixed model, and each benchmark uses the same tasks under every harness, so the drop
+comes from the harness.
+
+### Why models overfit to a single harness
+
+The KwaiKAT team names the cause [@kwaikat2026katcoder]:
+
+> "In agentic RL, if training relies solely on a single fixed harness, the model often learns not
+> 'how to solve the task' but 'how to solve the task under that particular harness's interface
+> conventions.'"
+
+They split it into three kinds of overfitting: to the harness's action **format**, to its **context
+structure**, and to its **control flow**, the retries and stop conditions the model learns to lean on.
+
+
+
+Harnesses differ along exactly those lines. They speak different **API dialects**, meaning the
+request and response format they use to talk to a model server: OpenAI chat-completions, OpenAI
+Responses, Anthropic Messages, Google Gemini. They differ in whether they use tool calling at all,
+how they compact context, and how much of the control flow they take away from the model.
+
+{/* VIZ: harness-ecosystem-matrix: scannable HTML table. Columns: harness, then the API dialects
+ it speaks, and whether it uses tool calls. */}
+
+
+Mini-SWE-Agent's [README](https://github.com/SWE-agent/mini-swe-agent) lists avoiding scaffold
+overfitting as a reason for its simple design. [Aider](https://aider.chat/) uses a different interface:
+it asks the model to write [edit blocks](https://aider.chat/docs/more/edit-formats.html) in prose
+instead of structured tool calls. A model trained on tool calls has to learn that output format too.
+
+In practice, the failure often shows up as an error rather than a lower score. A model trained in one
+harness calls a tool by that harness's name for it, with that harness's argument names, and a
+harness that spells them differently rejects the call before the tool ever runs.
+
+
+
+The list of harnesses keeps growing. [Harbor 0.22.0](https://github.com/harbor-framework/harbor/releases/tag/v0.22.0) ships adapters for more than 40 of them, and [OpenEnv](https://github.com/huggingface/OpenEnv) has
+validated ten end-to-end, so a model trained on a few will keep meeting ones it never saw.
+
+### Frontier labs now train across harnesses on purpose
+
+KwaiKAT's answer, which they call harness scaling, is to vary the harness during training.
+Poolside's Laguna report does this [@poolside2026laguna]:
+
+> "To encourage generalization across diverse agent harnesses, our training data includes
+> trajectories from external frameworks such as OpenHands, OpenCode, and Mini-SWE-Agent."
+
+They added 1.3 billion tokens of these trajectories to their supervised training and kept each
+harness's own habits in on purpose. Reinforcement learning then runs only in the harness they ship in
+production, `pool`, and the report doesn't say why the other harnesses stop there.
+
+Kimi K3's RL environment builds harnesses such as Claude Code and [Codex](https://github.com/openai/codex) from composable modules, so
+the model trains on many harness configurations instead of one [@kimi2026k3]. Qwen3-Coder-Next
+generates its agentic coding data in six different harnesses [@qwen2026codernext], and MiMo-V2.6
+trains on a pool of task-adapted mini-harnesses [@xiaomi2026mimov26]. Liquid AI does the same at a
+much smaller size. For [LFM2.5-2.6B](https://www.liquid.ai/blog/lfm2-5-2-6b), the model we train later,
+Liquid picks a harness at random for each training task. Their release post gives the reason:
+"training directly inside Hermes Agent, OpenClaw, and other harnesses exposes the model to their
+tools, system prompts, and interaction patterns, helping it work reliably across agent environments."
+
+OpenForgeRL checked whether that variety costs peak performance. It trained the same model on one
+harness and on three, and the three-harness model scored higher even on the single harness's home
+turf, 48.5 to 46.0, and almost twice as high under Codex [@yu2026openforgerl].
+
+{/* VIZ: paper-receipts: tabbed excerpt viewer. Each tab shows a real crop from the official
+ PDF of the passage being cited, with page reference and a link. Crops live in
+ app/public/papers/, with provenance in that folder's README. */}
+
+
+Model reports have only recently started saying which harness a number came from. In 2025, most
+named none or one per benchmark. By 2026, every report on the timeline below trains across several.
+
+
+
+### What is missing
+
+What is still missing is an open, reliable way to do this with any harness. It has to capture
+everything RL training uses from harnesses you don't control, and plug into whichever trainer you
+already use.
+
+It also doesn't take a frontier-sized budget. Agent Lightning takes Qwen3.5-9B from 41.8% to 56.4% on SWE-bench
+Verified with roughly 6,000 training examples [@he2026agentlightning]. The same paper names the hard
+part. The harness owns the loop, so the trainer only sees requests and responses going past, and
+turning those back into training samples is still an open problem.
+
+We built this on OpenEnv [@openenv2026], around a capture proxy that sits between the harness and the model. Every
+call the agent makes passes through the proxy, which records the prompt and completion tokens as the
+model saw and produced them, with their log probabilities. It speaks the API formats coding agents
+use, so Claude Code, Codex, and [OpenCode](https://opencode.ai) run unmodified, and a custom harness only has to point at it.
+Because it all sits behind OpenEnv's open environment interface, the same setup can feed different
+trainers, and ours was TRL [@trl].
+
+On top of that, the Harbor integration [@openenv2026harbor] serves [Harbor's](https://harborframework.com/) containerized tasks as
+OpenEnv environments, so the harness and the sandbox become settings you choose per rollout.
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/ablation-matrix.html b/content/articles/multi-harness-rl/app/src/content/embeds/ablation-matrix.html
new file mode 100644
index 00000000..b7d85796
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/ablation-matrix.html
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/banner-opaque.html b/content/articles/multi-harness-rl/app/src/content/embeds/banner-opaque.html
new file mode 100644
index 00000000..60c0a431
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/banner-opaque.html
@@ -0,0 +1,734 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/banner-seam.html b/content/articles/multi-harness-rl/app/src/content/embeds/banner-seam.html
new file mode 100644
index 00000000..1ac47c7b
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/banner-seam.html
@@ -0,0 +1,676 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/banner.html b/content/articles/multi-harness-rl/app/src/content/embeds/banner.html
new file mode 100644
index 00000000..37f6caa9
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/banner.html
@@ -0,0 +1,258 @@
+
+
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-anatomy-hero.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-anatomy-hero.html
new file mode 100644
index 00000000..0c281343
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-anatomy-hero.html
@@ -0,0 +1,413 @@
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-capture-levels.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-capture-levels.html
new file mode 100644
index 00000000..3aa4544e
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-capture-levels.html
@@ -0,0 +1,56 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-capture-proxy.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-capture-proxy.html
new file mode 100644
index 00000000..dc92a1a3
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-capture-proxy.html
@@ -0,0 +1,281 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-dialects-funnel.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-dialects-funnel.html
new file mode 100644
index 00000000..615a5704
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-dialects-funnel.html
@@ -0,0 +1,186 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-eval-comparison.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-eval-comparison.html
new file mode 100644
index 00000000..1cf8fd8b
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-eval-comparison.html
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-format-mismatch.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-format-mismatch.html
new file mode 100644
index 00000000..be3a00b6
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-format-mismatch.html
@@ -0,0 +1,79 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-full-stack.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-full-stack.html
new file mode 100644
index 00000000..d87294ef
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-full-stack.html
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-harbor-task.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-harbor-task.html
new file mode 100644
index 00000000..569549a5
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-harbor-task.html
@@ -0,0 +1,158 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-harness-anatomy.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-harness-anatomy.html
new file mode 100644
index 00000000..8e8d0743
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-harness-anatomy.html
@@ -0,0 +1,252 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-harness-lockin.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-harness-lockin.html
new file mode 100644
index 00000000..bc56d8f6
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-harness-lockin.html
@@ -0,0 +1,130 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-llm-rl-coding.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-llm-rl-coding.html
new file mode 100644
index 00000000..cf104e31
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-llm-rl-coding.html
@@ -0,0 +1,696 @@
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-openenv-system.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-openenv-system.html
new file mode 100644
index 00000000..99160606
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-openenv-system.html
@@ -0,0 +1,312 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-overfit-modes.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-overfit-modes.html
new file mode 100644
index 00000000..c0a5c802
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-overfit-modes.html
@@ -0,0 +1,140 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-posttraining-timeline.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-posttraining-timeline.html
new file mode 100644
index 00000000..3b967054
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-posttraining-timeline.html
@@ -0,0 +1,158 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-rollout-graph.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-rollout-graph.html
new file mode 100644
index 00000000..b8ae13f6
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-rollout-graph.html
@@ -0,0 +1,193 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-savings-heat.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-savings-heat.html
new file mode 100644
index 00000000..39f62d47
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-savings-heat.html
@@ -0,0 +1,165 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-scaffold-reversal.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-scaffold-reversal.html
new file mode 100644
index 00000000..a99f5a9f
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-scaffold-reversal.html
@@ -0,0 +1,130 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-serve-flow.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-serve-flow.html
new file mode 100644
index 00000000..db6022ff
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-serve-flow.html
@@ -0,0 +1,64 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-sft-vs-rl.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-sft-vs-rl.html
new file mode 100644
index 00000000..4750fd1a
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-sft-vs-rl.html
@@ -0,0 +1,180 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-token-contract.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-token-contract.html
new file mode 100644
index 00000000..7c6878aa
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-token-contract.html
@@ -0,0 +1,126 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-token-drift.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-token-drift.html
new file mode 100644
index 00000000..7b0e79bf
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-token-drift.html
@@ -0,0 +1,147 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-train-results.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-train-results.html
new file mode 100644
index 00000000..afa0429b
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-train-results.html
@@ -0,0 +1,468 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-trainer-record.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-trainer-record.html
new file mode 100644
index 00000000..a46ebf74
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-trainer-record.html
@@ -0,0 +1,165 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-training-curves.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-training-curves.html
new file mode 100644
index 00000000..c235cc41
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-training-curves.html
@@ -0,0 +1,65 @@
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/d3-who-drives.html b/content/articles/multi-harness-rl/app/src/content/embeds/d3-who-drives.html
new file mode 100644
index 00000000..868ea40a
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/d3-who-drives.html
@@ -0,0 +1,255 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/harness-ecosystem-matrix.html b/content/articles/multi-harness-rl/app/src/content/embeds/harness-ecosystem-matrix.html
new file mode 100644
index 00000000..9f17393d
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/harness-ecosystem-matrix.html
@@ -0,0 +1,90 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/harness-seams.html b/content/articles/multi-harness-rl/app/src/content/embeds/harness-seams.html
new file mode 100644
index 00000000..118a3f4c
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/harness-seams.html
@@ -0,0 +1,140 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/content/embeds/paper-receipts.html b/content/articles/multi-harness-rl/app/src/content/embeds/paper-receipts.html
new file mode 100644
index 00000000..9843aa2f
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/content/embeds/paper-receipts.html
@@ -0,0 +1,177 @@
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/env.d.ts b/content/articles/multi-harness-rl/app/src/env.d.ts
new file mode 100644
index 00000000..85641e97
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/env.d.ts
@@ -0,0 +1,7 @@
+///
+///
+///
+
+interface ImportMeta {
+ readonly env: ImportMetaEnv;
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/pages/dataviz.astro b/content/articles/multi-harness-rl/app/src/pages/dataviz.astro
new file mode 100644
index 00000000..9846652b
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/pages/dataviz.astro
@@ -0,0 +1,612 @@
+---
+import HtmlEmbed from "../components/HtmlEmbed.astro";
+import Image from "../components/Image.astro";
+import Wide from "../components/Wide.astro";
+import Stack from "../components/Stack.astro";
+import Seo from "../components/Seo.astro";
+import ThemeToggle from "../components/ThemeToggle.astro";
+import { loadAllVisualsFromMDX } from "../utils/extract-embeds.mjs";
+import * as ArticleMod from "../content/article.mdx";
+import "katex/dist/katex.min.css";
+import "../styles/global.css";
+
+// Import all images from assets/image via glob
+// @ts-ignore - Vite glob import
+const imageModules = (import.meta as any).glob('../content/assets/image/**/*.{png,jpg,jpeg,gif,webp,svg}', { eager: true });
+
+// Create a map from filename to image module
+const imageMap = new Map();
+for (const [path, module] of Object.entries(imageModules)) {
+ // Extract filename from path
+ const filename = path.split('/').pop();
+ if (filename) {
+ imageMap.set(filename, (module as any).default);
+ // Also map without extension for flexibility
+ const nameWithoutExt = filename.replace(/\.[^.]+$/, '');
+ imageMap.set(nameWithoutExt, (module as any).default);
+ }
+}
+
+// Helper to find image by filename or variable name
+function findImage(nameOrFilename: string) {
+ if (!nameOrFilename) return null;
+
+ // 1. Try exact filename match (e.g. "placeholder.png")
+ if (imageMap.has(nameOrFilename)) {
+ return imageMap.get(nameOrFilename);
+ }
+
+ // 2. Try without extension (e.g. "placeholder")
+ const withoutExt = nameOrFilename.replace(/\.[^.]+$/, '');
+ if (imageMap.has(withoutExt)) {
+ return imageMap.get(withoutExt);
+ }
+
+ // 3. Normalized fuzzy match (for auto-generated names from Notion etc.)
+ const normalizedVar = nameOrFilename.toLowerCase().replace(/[-_]/g, '');
+ for (const [filename, img] of imageMap.entries()) {
+ const normalizedFilename = filename.toLowerCase().replace(/[-_]/g, '');
+ if (normalizedFilename.includes(normalizedVar) || normalizedVar.includes(normalizedFilename.replace(/\.[^.]+$/, ''))) {
+ return img;
+ }
+ }
+
+ // 4. Partial match fallback
+ const varParts = nameOrFilename.split(/[-_]/);
+ for (const [filename, img] of imageMap.entries()) {
+ const matchCount = varParts.filter(part =>
+ part.length > 3 && filename.toLowerCase().includes(part.toLowerCase())
+ ).length;
+ if (matchCount >= 3) {
+ return img;
+ }
+ }
+
+ return null;
+}
+
+// Import article metadata
+const articleFM = (ArticleMod as any).frontmatter ?? {};
+// Keep tags but remove other HTML
+const articleTitle = String(articleFM?.title ?? "Article")
+ .replace(/<(?!br\s*\/?)[^>]+>/gi, "") // Remove all HTML except
+ .replace(/\\n/g, " ")
+ .trim();
+const articleDesc = articleFM?.description ?? "";
+
+// Page metadata
+const pageTitle = `Visualizations - ${articleTitle}`;
+const pageDesc = articleDesc;
+
+// Load all visual elements from the content
+const allVisuals = loadAllVisualsFromMDX();
+
+// Filter out skipGallery items
+const visuals = allVisuals.filter((item: any) => !item.skipGallery);
+const embedCount = visuals.filter((v: any) => v.type === 'embed').length + 1; // +1 for banner
+const imageCount = visuals.filter((v: any) => v.type === 'image' || v.type === 'stack').length;
+const tableCount = visuals.filter((v: any) => v.type === 'table').length;
+
+// Generate a clean display name for each item (null if no meaningful name)
+function getDisplayName(item: any): string | null {
+ if (item.type === 'embed') {
+ // Prefer title, else format filename
+ if (item.title) return item.title;
+ const filename = item.src?.replace(/\.html$/, '').replace(/^.*\//, '') || '';
+ // Convert kebab-case to Title Case
+ return filename
+ .split(/[-_]/)
+ .map((word: string) => word.charAt(0).toUpperCase() + word.slice(1))
+ .join(' ');
+ } else if (item.type === 'image') {
+ // Prefer alt text
+ if (item.alt && item.alt !== 'Image') return item.alt;
+ if (item.caption) return item.caption;
+ return null; // No meaningful name
+ } else if (item.type === 'stack') {
+ // Use first image caption/alt or generic label
+ const imgs = item.images || [];
+ const firstCaption = imgs.find((i: any) => i.caption)?.caption;
+ const firstAlt = imgs.find((i: any) => i.alt && i.alt !== 'Image')?.alt;
+ return firstCaption || firstAlt || `Stack (${imgs.length} images)`;
+ } else if (item.type === 'table') {
+ // Tables don't have reliable names
+ return null;
+ }
+ return null;
+}
+
+// Get type label for display
+function getTypeLabel(type: string): string {
+ switch (type) {
+ case 'embed': return 'Chart';
+
+ case 'image': return 'Image';
+ case 'stack': return 'Stack';
+ case 'table': return 'Table';
+ default: return 'Visual';
+ }
+}
+
+// Generate clean anchor slug - only for embeds with a real identifier
+function generateSlug(item: any): string | null {
+ // Only embeds have reliable identifiers (filename)
+ if (item.type === 'embed' && item.src) {
+ const filename = item.src.replace(/\.html$/, '').replace(/^.*\//, '');
+ return filename
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, '-')
+ .replace(/^-+|-+$/g, '')
+ .substring(0, 40);
+ }
+ // No anchor for tables, images without proper ID
+ return null;
+}
+
+// Add anchor IDs and display names to visuals
+const usedSlugs = new Set();
+let displayIndex = 0; // Counter for displayed items (excludes banner)
+const visualsWithMeta = visuals.map((item: any) => {
+ const displayName = getDisplayName(item);
+ let slug = generateSlug(item);
+
+ // Handle duplicates by adding a number suffix
+ if (slug && usedSlugs.has(slug)) {
+ let counter = 2;
+ while (usedSlugs.has(`${slug}-${counter}`)) {
+ counter++;
+ }
+ slug = `${slug}-${counter}`;
+ }
+ if (slug) usedSlugs.add(slug);
+
+ // Only increment index for non-banner items
+ if (item.type !== 'banner') {
+ displayIndex++;
+ }
+
+ return {
+ ...item,
+ displayName,
+ anchorId: slug, // null if no identifier
+ index: displayIndex
+ };
+});
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {visualsWithMeta.filter((item: any) => item.type !== 'banner').map((item: any) => {
+ const isWide = item.wide || item.fullWidth;
+
+ return (
+
+
+ {!item.hideCaption && (
+
+ )}
+
+
+ {(item.type === 'embed' || item.type === 'banner') && !isWide && (
+
+ )}
+
+ {(item.type === 'embed' || item.type === 'banner') && isWide && (
+
+
+
+ )}
+
+ {item.type === 'image' && (() => {
+ // Prefer resolvedFilename (from MDX imports), fallback to variable name
+ const imgSrc = findImage(item.resolvedFilename || item.src);
+ return imgSrc ? (
+
+ ) : (
+
+ Image
+ {item.src}
+
+ );
+ })()}
+
+ {item.type === 'stack' && (
+
+ {(item.images || []).map((img: any) => {
+ const imgSrc = findImage(img.resolvedFilename || img.src);
+ return imgSrc ? (
+
+ ) : (
+
+ Image
+ {img.src}
+
+ );
+ })}
+
+ )}
+
+ {item.type === 'table' && (
+
+ )}
+
+
+ );
+ })}
+
+
+
+
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/pages/index.astro b/content/articles/multi-harness-rl/app/src/pages/index.astro
new file mode 100644
index 00000000..eb4d68d6
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/pages/index.astro
@@ -0,0 +1,463 @@
+---
+import * as ArticleMod from "../content/article.mdx";
+
+import HeroArticle from "../components/HeroArticle.astro";
+import HeroPaper from "../components/HeroPaper.astro";
+import Footer from "../components/Footer.astro";
+import ThemeToggle from "../components/ThemeToggle.astro";
+import Seo from "../components/Seo.astro";
+import TableOfContents from "../components/TableOfContents.astro";
+import HtmlEmbed from "../components/HtmlEmbed.astro";
+// Default OG image served from public/
+const ogDefaultUrl = "/thumb.auto.jpg";
+import "katex/dist/katex.min.css";
+import "../styles/global.css";
+const articleFM = (ArticleMod as any).frontmatter ?? {};
+const Article = (ArticleMod as any).default;
+// Helper to strip HTML tags
+const stripHtml = (text: string) => String(text || "").replace(/<[^>]*>/g, "");
+// docTitle without HTML tags for SEO/meta
+const docTitle = stripHtml(articleFM?.title ?? "Untitled article");
+// Allow explicit line breaks in the title via "\n" or YAML newlines
+const docTitleHtml = (articleFM?.title ?? "Untitled article")
+ .replace(/\\n/g, " ")
+ .replace(/\n/g, " ");
+const subtitle = articleFM?.subtitle ?? "";
+const description = articleFM?.description ?? "";
+// Accept authors as string[] or array of objects { name, url, affiliations? }
+const rawAuthors = (articleFM as any)?.authors ?? [];
+type Affiliation = { id: number; name: string; url?: string };
+type Author = { name: string; url?: string; affiliationIndices?: number[] };
+
+// Normalize affiliations from frontmatter: supports strings or objects { id?, name, url? }
+const rawAffils =
+ (articleFM as any)?.affiliations ?? (articleFM as any)?.affiliation ?? [];
+const normalizedAffiliations: Affiliation[] = (() => {
+ const seen: Map = new Map();
+ const list: Affiliation[] = [];
+ const pushUnique = (name: string, url?: string) => {
+ const key = `${String(name).trim()}|${url ? String(url).trim() : ""}`;
+ if (seen.has(key)) return seen.get(key)!;
+ const id = list.length + 1;
+ list.push({
+ id,
+ name: String(name).trim(),
+ url: url ? String(url) : undefined,
+ });
+ seen.set(key, id);
+ return id;
+ };
+ const input = Array.isArray(rawAffils)
+ ? rawAffils
+ : rawAffils
+ ? [rawAffils]
+ : [];
+ for (const a of input) {
+ if (typeof a === "string") {
+ pushUnique(a);
+ } else if (a && typeof a === "object") {
+ const name = a.name ?? a.label ?? a.text ?? a.affiliation ?? "";
+ if (!String(name).trim()) continue;
+ const url = a.url || a.link;
+ // Respect provided numeric id for display stability if present and sequential; otherwise reassign
+ pushUnique(String(name), url ? String(url) : undefined);
+ }
+ }
+ return list;
+})();
+
+// Helper: ensure an affiliation exists and return its id
+const ensureAffiliation = (val: any): number | undefined => {
+ if (val == null) return undefined;
+ if (typeof val === "number" && Number.isFinite(val) && val > 0) {
+ return Math.floor(val);
+ }
+ const name =
+ typeof val === "string"
+ ? val
+ : (val?.name ?? val?.label ?? val?.text ?? val?.affiliation);
+ if (!name || !String(name).trim()) return undefined;
+ const existing = normalizedAffiliations.find(
+ (a) => a.name === String(name).trim(),
+ );
+ if (existing) return existing.id;
+ const id = normalizedAffiliations.length + 1;
+ normalizedAffiliations.push({
+ id,
+ name: String(name).trim(),
+ url: val?.url || val?.link,
+ });
+ return id;
+};
+
+// Normalize authors and map affiliations -> indices (Distill-like)
+const normalizedAuthors: Author[] = (
+ Array.isArray(rawAuthors) ? rawAuthors : []
+)
+ .map((a: any) => {
+ if (typeof a === "string") {
+ return { name: a } as Author;
+ }
+ const name = String(a?.name || "").trim();
+ const url = a?.url || a?.link;
+ let indices: number[] | undefined = undefined;
+ const raw = a?.affiliations ?? a?.affiliation ?? a?.affils;
+ if (raw != null) {
+ const entries = Array.isArray(raw) ? raw : [raw];
+ const ids = entries
+ .map(ensureAffiliation)
+ .filter((x): x is number => typeof x === "number");
+ const unique = Array.from(new Set(ids)).sort((x, y) => x - y);
+ if (unique.length) indices = unique;
+ }
+ return { name, url, affiliationIndices: indices } as Author;
+ })
+ .filter((a: Author) => a.name && a.name.trim().length > 0);
+const authorNames: string[] = normalizedAuthors.map((a) => a.name);
+const published = articleFM?.published ?? undefined;
+const template: string = (articleFM as any)?.template ?? "article";
+const tags = articleFM?.tags ?? [];
+// Prefer seoThumbImage from frontmatter if provided
+const fmOg = articleFM?.seoThumbImage as string | undefined;
+const imageAbs: string =
+ fmOg && fmOg.startsWith("http")
+ ? fmOg
+ : Astro.site
+ ? new URL(fmOg ?? ogDefaultUrl, Astro.site).toString()
+ : (fmOg ?? ogDefaultUrl);
+
+// ---- Build citation text & BibTeX from frontmatter ----
+const rawTitle = articleFM?.title ?? "Untitled article";
+const titleFlat = stripHtml(String(rawTitle))
+ .replace(/\\n/g, " ")
+ .replace(/\n/g, " ")
+ .replace(/\s+/g, " ")
+ .trim();
+const extractYear = (val: string | undefined): number | undefined => {
+ if (!val) return undefined;
+ const d = new Date(val);
+ if (!Number.isNaN(d.getTime())) return d.getFullYear();
+ const m = String(val).match(/(19|20)\d{2}/);
+ return m ? Number(m[0]) : undefined;
+};
+
+const year = extractYear(published);
+const citationAuthorsText = authorNames.join(", ");
+const citationText = `${citationAuthorsText}${year ? ` (${year})` : ""}. "${titleFlat}".`;
+
+const authorsBib = authorNames.join(" and ");
+const keyAuthor = (authorNames[0] || "article")
+ .split(/\s+/)
+ .slice(-1)[0]
+ .toLowerCase();
+const keyTitle = titleFlat
+ .toLowerCase()
+ .replace(/[^a-z0-9]+/g, "_")
+ .replace(/^_|_$/g, "");
+const bibKey = `${keyAuthor}${year ?? ""}_${keyTitle}`;
+const doi = (ArticleMod as any)?.frontmatter?.doi
+ ? String((ArticleMod as any).frontmatter.doi)
+ : undefined;
+const bibtex = `@misc{${bibKey},\n title={${titleFlat}},\n author={${authorsBib}},\n ${year ? `year={${year}},\n ` : ""}${doi ? `doi={${doi}}` : ""}\n}`;
+const envCollapse = false;
+const tableOfContentAutoCollapse = Boolean(
+ (articleFM as any)?.tableOfContentAutoCollapse ??
+ (articleFM as any)?.tableOfContentsAutoCollapse ??
+ envCollapse,
+);
+// Licence note (HTML allowed)
+const licence =
+ (articleFM as any)?.licence ??
+ (articleFM as any)?.license ??
+ (articleFM as any)?.licenseNote;
+
+// External links (paper, code, data, demo, etc.) shown in paper template hero
+const links: Array<{ label: string; url: string; icon?: string }> =
+ ((articleFM as any)?.links ?? []).map((l: any) => ({
+ label: l.label ?? l.name ?? l.text ?? "Link",
+ url: l.url ?? l.href ?? l.link ?? "#",
+ icon: l.icon,
+ }));
+---
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {template === "paper" ? (
+
+ ) : (
+
+ )}
+
+ {template === "paper" && (
+
+
+
+ )}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/content/articles/multi-harness-rl/app/src/scripts/README.md b/content/articles/multi-harness-rl/app/src/scripts/README.md
new file mode 100644
index 00000000..9a7369ee
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/scripts/README.md
@@ -0,0 +1,65 @@
+# Mermaid Zoom Integration
+
+Ce module fournit une intégration du zoom medium-zoom pour les diagrammes Mermaid dans le projet Astro.
+
+## Fonctionnalités
+
+- ✅ **Zoom medium-zoom identique aux images** : Utilise la vraie bibliothèque medium-zoom
+- ✅ **Conversion SVG → Image** : Convertit les diagrammes Mermaid en images pour le zoom
+- ✅ **Adaptation au thème** : Reconversion automatique lors des changements de thème
+- ✅ **Fallback robuste** : Zoom custom si la conversion échoue
+- ✅ **Auto-initialisation** : Détection automatique des nouveaux diagrammes
+
+## Architecture
+
+### Fichiers
+
+- `src/scripts/mermaid-zoom.js` : Module principal avec toute la logique
+- `src/pages/index.astro` : Import du module (ligne 209)
+- `src/styles/components/_mermaid.css` : Styles CSS pour les wrappers
+
+### Fonctions principales
+
+- `convertSvgToImageForMediumZoom()` : Convertit SVG en image pour medium-zoom
+- `setupMermaidZoom()` : Wrappe les diagrammes et initialise le zoom
+- `initMermaidZoom()` : Fonction principale d'initialisation
+- `openMermaidZoom()` / `closeMermaidZoom()` : Zoom custom de fallback
+
+### API globale
+
+```javascript
+window.MermaidZoom = {
+ init: initMermaidZoom, // Initialiser le zoom
+ setup: setupMermaidZoom, // Wrapper et conversion
+ cleanup: cleanupMermaidObservers, // Nettoyer les observers
+ convertSvgToImage: convertSvgToImageForMediumZoom,
+ openZoom: openMermaidZoom, // Ouvrir zoom custom
+ closeZoom: closeMermaidZoom // Fermer zoom custom
+};
+```
+
+## Comment ça marche
+
+1. **Détection** : Le script détecte tous les éléments `.mermaid`
+2. **Wrapper** : Crée un `div.mermaid-zoom-wrapper` autour de chaque diagramme
+3. **Conversion** : Convertit le SVG en image ` ` avec `data-zoomable="1"`
+4. **Medium-zoom** : Initialise `window.mediumZoom()` sur l'image générée
+5. **Thème** : Observer pour reconvertir lors des changements de thème
+
+## Avantages de cette approche
+
+- **Vrai medium-zoom** : Comportement identique aux images existantes
+- **Pas d'erreur canvas** : Évite l'erreur "Tainted canvases" en utilisant des URLs blob
+- **Réactivité au thème** : Les diagrammes s'adaptent automatiquement
+- **Code modulaire** : Facile à maintenir et déboguer
+- **Performance** : Une seule conversion par diagramme
+
+## Debug
+
+Le module inclut des logs détaillés :
+- `🚀 Mermaid Zoom Script loaded`
+- `🔍 Found X Mermaid elements`
+- `📦 Wrapping Mermaid element X`
+- `🖼️ SVG converted to img element`
+- `🎉 REAL medium-zoom initialized on Mermaid!`
+- `🎨 Theme changed, reconverting Mermaid image`
diff --git a/content/articles/multi-harness-rl/app/src/scripts/mermaid-zoom-optimized.js b/content/articles/multi-harness-rl/app/src/scripts/mermaid-zoom-optimized.js
new file mode 100644
index 00000000..5729ee69
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/scripts/mermaid-zoom-optimized.js
@@ -0,0 +1,294 @@
+/**
+ * Mermaid Zoom Integration - Version Optimisée
+ * Système léger pour beaucoup de diagrammes Mermaid
+ */
+
+console.log("🚀 Mermaid Zoom Script v16.0 loaded - OPTIMIZED for many diagrams");
+
+// Cache pour les images générées
+const imageCache = new Map();
+
+// Fonction pour appliquer les styles Mermaid au SVG selon le thème
+function applyMermaidStylesToSvg(svgElement) {
+ try {
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
+
+ // Couleurs selon le thème
+ const colors = isDark ? {
+ nodeFill: '#1a1a1a',
+ nodeStroke: '#ffffff',
+ clusterFill: '#2a2a2a',
+ clusterStroke: '#ffffff',
+ pathStroke: '#ffffff',
+ textColor: '#ffffff'
+ } : {
+ nodeFill: '#ffffff',
+ nodeStroke: '#333333',
+ clusterFill: '#f9f9f9',
+ clusterStroke: '#333333',
+ pathStroke: '#333333',
+ textColor: '#333333'
+ };
+
+ // Appliquer border-radius et couleurs
+ const rects = svgElement.querySelectorAll('rect:not(.flowchart-link), .node rect, .nodeLabel rect');
+ rects.forEach(rect => {
+ rect.setAttribute('rx', '8');
+ rect.setAttribute('ry', '8');
+ rect.setAttribute('fill', colors.nodeFill);
+ rect.setAttribute('stroke', colors.nodeStroke);
+ });
+
+ const clusterRects = svgElement.querySelectorAll('.cluster rect');
+ clusterRects.forEach(rect => {
+ rect.setAttribute('rx', '8');
+ rect.setAttribute('ry', '8');
+ rect.setAttribute('fill', colors.clusterFill);
+ rect.setAttribute('stroke', colors.clusterStroke);
+ });
+
+ const paths = svgElement.querySelectorAll('.edgePath');
+ paths.forEach(path => {
+ path.setAttribute('stroke', colors.pathStroke);
+ });
+
+ const textElements = svgElement.querySelectorAll('text, .nodeLabel text, .edgeLabel text');
+ textElements.forEach(text => {
+ text.setAttribute('fill', colors.textColor);
+ });
+
+ } catch (error) {
+ console.error('❌ Error applying styles to SVG:', error);
+ }
+}
+
+// Fonction pour générer une clé de cache unique
+function getCacheKey(svgElement, theme) {
+ const svgContent = svgElement.outerHTML;
+ const hash = svgContent.length + svgContent.slice(0, 100);
+ return `${hash}-${theme}`;
+}
+
+// Fonction pour convertir SVG en image (avec cache)
+function convertSvgToImageCached(svgElement, wrapper, theme) {
+ const cacheKey = getCacheKey(svgElement, theme);
+
+ // Vérifier le cache
+ if (imageCache.has(cacheKey)) {
+ console.log(`📦 Using cached image for theme: ${theme}`);
+ return imageCache.get(cacheKey);
+ }
+
+ try {
+ const wrapperRect = wrapper.getBoundingClientRect();
+ const wrapperWidth = Math.round(wrapperRect.width);
+ const wrapperHeight = Math.round(wrapperRect.height);
+
+ // Cloner le SVG
+ const clonedSvg = svgElement.cloneNode(true);
+ applyMermaidStylesToSvg(clonedSvg);
+
+ // Créer une image 2x plus grande pour le zoom
+ const zoomFactor = 2;
+ const imageWidth = wrapperWidth * zoomFactor;
+ const imageHeight = wrapperHeight * zoomFactor;
+
+ clonedSvg.setAttribute('width', imageWidth);
+ clonedSvg.setAttribute('height', imageHeight);
+
+ // Créer l'URL de l'image
+ const svgData = new XMLSerializer().serializeToString(clonedSvg);
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
+ const svgUrl = URL.createObjectURL(svgBlob);
+
+ // Créer l'élément image
+ const imgElement = document.createElement('img');
+ imgElement.src = svgUrl;
+ imgElement.style.width = `${wrapperWidth}px`;
+ imgElement.style.height = `${wrapperHeight}px`;
+ imgElement.style.display = 'block';
+ imgElement.setAttribute('data-zoomable', '1');
+ imgElement.classList.add('mermaid-zoom-image');
+
+ const result = {
+ imgElement,
+ svgUrl,
+ dimensions: { wrapperWidth, wrapperHeight, imageWidth, imageHeight }
+ };
+
+ // Mettre en cache
+ imageCache.set(cacheKey, result);
+ console.log(`💾 Cached image for theme: ${theme}`);
+
+ return result;
+
+ } catch (error) {
+ console.error("❌ Error converting SVG to image:", error);
+ return null;
+ }
+}
+
+// Fonction pour initialiser le zoom sur un diagramme Mermaid
+function initMermaidZoom(mermaidEl, index) {
+ // Vérifier si déjà wrappé
+ if (mermaidEl.parentElement?.classList.contains("mermaid-zoom-wrapper")) {
+ return;
+ }
+
+ console.log(`📦 Setting up zoom for Mermaid ${index}`);
+
+ // Créer le wrapper
+ const wrapper = document.createElement("div");
+ wrapper.className = "mermaid-zoom-wrapper";
+ wrapper.setAttribute("data-zoomable", "1");
+ wrapper.style.display = "block";
+ wrapper.style.width = "100%";
+ wrapper.style.maxWidth = "100%";
+
+ // Insérer le wrapper avant l'élément Mermaid
+ mermaidEl.parentNode.insertBefore(wrapper, mermaidEl);
+ wrapper.appendChild(mermaidEl);
+
+ // Ajouter l'événement de clic (conversion à la demande)
+ wrapper.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ const svgElement = mermaidEl.querySelector("svg");
+ if (!svgElement) return;
+
+ const currentTheme = document.documentElement.getAttribute("data-theme");
+ console.log(`🖱️ Mermaid ${index} clicked, converting for zoom`);
+
+ // Convertir l'image (avec cache)
+ const result = convertSvgToImageCached(svgElement, wrapper, currentTheme);
+ if (!result) return;
+
+ // Remplacer le contenu par l'image
+ wrapper.innerHTML = '';
+ wrapper.appendChild(result.imgElement);
+
+ // Initialiser medium-zoom
+ const isDark = currentTheme === "dark";
+ const background = isDark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)";
+
+ const zoomInstance = window.mediumZoom(result.imgElement, {
+ background,
+ margin: 24,
+ scrollOffset: 0,
+ });
+
+ console.log(`🎉 Zoom initialized for Mermaid ${index}`);
+
+ // Forcer les z-index élevés
+ const forceZIndex = () => {
+ const overlay = document.querySelector('.medium-zoom-overlay');
+ const zoomedImage = document.querySelector('.medium-zoom-image--opened');
+
+ if (overlay) {
+ overlay.style.zIndex = '9999999';
+ overlay.style.setProperty('z-index', '9999999', 'important');
+ }
+
+ if (zoomedImage) {
+ zoomedImage.style.zIndex = '10000000';
+ zoomedImage.style.setProperty('z-index', '10000000', 'important');
+ zoomedImage.style.filter = 'none';
+ zoomedImage.style.setProperty('filter', 'none', 'important');
+ }
+ };
+
+ // Écouter les événements de zoom
+ result.imgElement.addEventListener('zoom:open', forceZIndex);
+ result.imgElement.addEventListener('zoom:opened', forceZIndex);
+
+ // Observer pour forcer les styles
+ const observer = new MutationObserver(forceZIndex);
+ observer.observe(document.body, { childList: true, subtree: true });
+
+ // Nettoyer l'observer après 5 secondes
+ setTimeout(() => observer.disconnect(), 5000);
+ });
+}
+
+// Fonction principale optimisée
+function setupMermaidZoomOptimized() {
+ const mermaidElements = document.querySelectorAll(".mermaid");
+ console.log(`🔍 Found ${mermaidElements.length} Mermaid elements`);
+
+ mermaidElements.forEach((mermaidEl, index) => {
+ initMermaidZoom(mermaidEl, index);
+ });
+}
+
+// Observer global optimisé (un seul observer pour tout)
+function setupGlobalObserver() {
+ const observer = new MutationObserver((mutations) => {
+ let shouldUpdate = false;
+
+ mutations.forEach((mutation) => {
+ if (mutation.type === 'childList') {
+ // Vérifier si de nouveaux diagrammes Mermaid ont été ajoutés
+ mutation.addedNodes.forEach(node => {
+ if (node.nodeType === 1) { // Element node
+ if (node.classList?.contains('mermaid') ||
+ node.querySelector?.('.mermaid')) {
+ shouldUpdate = true;
+ }
+ }
+ });
+ }
+ });
+
+ if (shouldUpdate) {
+ console.log(`🔄 New Mermaid diagrams detected, updating...`);
+ setTimeout(setupMermaidZoomOptimized, 100);
+ }
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+
+ console.log(`👁️ Global observer started`);
+}
+
+// Initialisation optimisée
+function initMermaidZoomOptimized() {
+ console.log("🎯 Initializing optimized Mermaid zoom");
+
+ // Attendre que mediumZoom soit disponible
+ const checkMediumZoom = () => {
+ if (window.mediumZoom) {
+ console.log("✅ mediumZoom found, initializing optimized Mermaid zoom");
+ setupMermaidZoomOptimized();
+ setupGlobalObserver();
+ } else {
+ console.log("⏳ Waiting for mediumZoom...");
+ setTimeout(checkMediumZoom, 100);
+ }
+ };
+
+ checkMediumZoom();
+}
+
+// Auto-initialisation
+if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", initMermaidZoomOptimized);
+} else {
+ initMermaidZoomOptimized();
+}
+
+// Aussi après le chargement complet
+window.addEventListener("load", () => {
+ setTimeout(initMermaidZoomOptimized, 1000);
+});
+
+// Nettoyer le cache périodiquement (éviter les fuites mémoire)
+setInterval(() => {
+ if (imageCache.size > 20) {
+ console.log(`🧹 Cleaning image cache (${imageCache.size} items)`);
+ imageCache.clear();
+ }
+}, 60000); // Toutes les minutes
diff --git a/content/articles/multi-harness-rl/app/src/scripts/mermaid-zoom.js b/content/articles/multi-harness-rl/app/src/scripts/mermaid-zoom.js
new file mode 100644
index 00000000..1759be9f
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/scripts/mermaid-zoom.js
@@ -0,0 +1,858 @@
+/**
+ * Mermaid Zoom Integration
+ * Provides medium-zoom functionality for Mermaid diagrams
+ */
+
+// Configuration - Désactiver le zoom Mermaid
+const MERMAID_ZOOM_ENABLED = false;
+
+console.log("🚀 Mermaid Zoom Script v20.0 loaded - DISABLED");
+
+// Si désactivé, ne rien faire
+if (!MERMAID_ZOOM_ENABLED) {
+ console.log("🚫 Mermaid zoom is disabled, skipping initialization");
+ // Export vide pour éviter les erreurs
+ window.mermaidZoom = {
+ init: () => { },
+ cleanup: () => { },
+ convertSvgToImage: () => { },
+ openZoom: () => { },
+ closeZoom: () => { }
+ };
+ // Arrêter l'exécution ici sans lancer d'erreur
+ // Le reste du code ne s'exécutera pas car tout est dans le bloc if ci-dessous
+} else {
+
+ // Fonction pour appliquer les styles Mermaid au SVG selon le thème
+ function applyMermaidStylesToSvg(svgElement) {
+ try {
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
+ console.log(`🎨 Applying Mermaid styles for theme: ${isDark ? 'dark' : 'light'}`);
+
+ // Couleurs selon le thème - avec plus de contraste pour le zoom
+ const colors = isDark ? {
+ nodeFill: '#1a1a1a',
+ nodeStroke: '#ffffff',
+ nodeStrokeWidth: '2', // Plus épais pour plus de visibilité
+ clusterFill: '#2a2a2a',
+ clusterStroke: '#ffffff',
+ clusterStrokeWidth: '2',
+ pathStroke: '#ffffff',
+ pathStrokeWidth: '2', // Plus épais pour les flèches
+ textColor: '#ffffff',
+ linkColor: '#ffffff' // Couleur spécifique pour les liens
+ } : {
+ nodeFill: '#ffffff',
+ nodeStroke: '#000000', // Noir pur pour plus de contraste
+ nodeStrokeWidth: '2',
+ clusterFill: '#f9f9f9',
+ clusterStroke: '#000000', // Noir pur
+ clusterStrokeWidth: '2',
+ pathStroke: '#000000', // Noir pur pour les flèches
+ pathStrokeWidth: '2',
+ textColor: '#000000', // Noir pur
+ linkColor: '#000000' // Noir pur pour les liens
+ };
+
+ // Appliquer border-radius aux rectangles
+ const rects = svgElement.querySelectorAll('rect:not(.flowchart-link), .node rect, .nodeLabel rect');
+ rects.forEach(rect => {
+ rect.setAttribute('rx', '8');
+ rect.setAttribute('ry', '8');
+ rect.setAttribute('fill', colors.nodeFill);
+ rect.setAttribute('stroke', colors.nodeStroke);
+ rect.setAttribute('stroke-width', colors.nodeStrokeWidth);
+ });
+
+ // Appliquer border-radius et couleurs aux clusters
+ const clusterRects = svgElement.querySelectorAll('.cluster rect');
+ clusterRects.forEach(rect => {
+ rect.setAttribute('rx', '8');
+ rect.setAttribute('ry', '8');
+ rect.setAttribute('fill', colors.clusterFill);
+ rect.setAttribute('stroke', colors.clusterStroke);
+ rect.setAttribute('stroke-width', colors.clusterStrokeWidth);
+ });
+
+ // Appliquer les couleurs aux nœuds
+ const nodes = svgElement.querySelectorAll('.node');
+ nodes.forEach(node => {
+ const rect = node.querySelector('rect');
+ if (rect) {
+ rect.setAttribute('fill', colors.nodeFill);
+ rect.setAttribute('stroke', colors.nodeStroke);
+ rect.setAttribute('stroke-width', colors.nodeStrokeWidth);
+ }
+ });
+
+ // Appliquer les couleurs aux clusters
+ const clusters = svgElement.querySelectorAll('.cluster');
+ clusters.forEach(cluster => {
+ const rect = cluster.querySelector('rect');
+ if (rect) {
+ rect.setAttribute('fill', colors.clusterFill);
+ rect.setAttribute('stroke', colors.clusterStroke);
+ rect.setAttribute('stroke-width', colors.clusterStrokeWidth);
+ }
+ });
+
+ // Appliquer les couleurs aux chemins et flèches
+ const paths = svgElement.querySelectorAll('.edgePath, path, .flowchart-link');
+ paths.forEach(path => {
+ path.setAttribute('stroke', colors.pathStroke);
+ path.setAttribute('stroke-width', colors.pathStrokeWidth);
+ });
+
+ // Appliquer les couleurs aux liens spécifiquement
+ const links = svgElement.querySelectorAll('.flowchart-link, .edgeLabel');
+ links.forEach(link => {
+ link.setAttribute('stroke', colors.linkColor);
+ link.setAttribute('fill', colors.linkColor);
+ });
+
+ // Appliquer les couleurs au texte
+ const textElements = svgElement.querySelectorAll('text, .nodeLabel text, .edgeLabel text');
+ textElements.forEach(text => {
+ text.setAttribute('fill', colors.textColor);
+ });
+
+ // Appliquer les couleurs aux marqueurs de flèches
+ const markers = svgElement.querySelectorAll('marker, marker path');
+ markers.forEach(marker => {
+ marker.setAttribute('fill', colors.pathStroke);
+ marker.setAttribute('stroke', colors.pathStroke);
+ });
+
+ console.log(`🎨 Applied Mermaid styles: ${rects.length} rects, ${clusters.length} clusters, ${paths.length} paths, ${textElements.length} text elements`);
+
+ } catch (error) {
+ console.error('❌ Error applying styles to SVG:', error);
+ }
+ }
+
+ // Fonction pour extraire les styles CSS appliqués au diagramme Mermaid
+ function getComputedStylesForMermaid(mermaidElement) {
+ try {
+ // Styles CSS essentiels pour Mermaid (hardcodés pour éviter les problèmes CORS)
+ const essentialStyles = `
+ .mermaid rect:not(.flowchart-link),
+ .mermaid .node rect,
+ .mermaid .nodeLabel rect {
+ rx: 8px !important;
+ ry: 8px !important;
+ }
+
+ .mermaid .cluster rect {
+ rx: 8px !important;
+ ry: 8px !important;
+ }
+
+ .mermaid .nodeLabel p {
+ color: black !important;
+ }
+
+ .mermaid .edgeLabel {
+ color: black !important;
+ }
+
+ .mermaid .edgePath {
+ stroke: #333 !important;
+ }
+
+ .mermaid .node {
+ fill: #fff !important;
+ stroke: #333 !important;
+ }
+
+ .mermaid .cluster {
+ fill: #f9f9f9 !important;
+ stroke: #333 !important;
+ }
+ `;
+
+ console.log(`🎨 Using essential CSS styles for Mermaid`);
+ return essentialStyles;
+
+ } catch (error) {
+ console.error('❌ Error getting CSS styles:', error);
+ return null;
+ }
+ }
+
+ // Fonction pour forcer Mermaid à se re-rendre avec le bon thème
+ function forceMermaidThemeUpdate(mermaidElement) {
+ try {
+ // Obtenir le thème actuel
+ const currentTheme = document.documentElement.getAttribute("data-theme");
+ console.log(`🎨 Forcing Mermaid theme update to: ${currentTheme}`);
+
+ // Trouver le diagramme Mermaid original
+ const mermaidCode = mermaidElement.textContent || mermaidElement.innerText;
+ if (!mermaidCode) {
+ console.log(`❌ No Mermaid code found to re-render`);
+ return false;
+ }
+
+ // Forcer le re-rendu de Mermaid avec le nouveau thème
+ if (window.mermaid) {
+ // Utiliser l'API Mermaid pour re-rendre avec le bon thème
+ const config = {
+ theme: currentTheme === 'dark' ? 'dark' : 'neutral',
+ autoTheme: true
+ };
+
+ console.log(`🔄 Re-rendering Mermaid with config:`, config);
+
+ // Re-rendre le diagramme
+ mermaid.init(undefined, mermaidElement);
+
+ return true;
+ } else {
+ console.log(`❌ Mermaid API not available`);
+ return false;
+ }
+ } catch (error) {
+ console.error(`❌ Error forcing Mermaid theme update:`, error);
+ return false;
+ }
+ }
+
+ // Fonction pour convertir SVG en image en préservant EXACTEMENT les dimensions
+ function convertSvgToImagePreservingDimensions(svgElement, wrapper, originalMermaid) {
+ console.log(`🔄 Converting SVG to image with EXACT dimension preservation`);
+
+ try {
+ // Obtenir les dimensions EXACTES du wrapper actuel
+ const wrapperRect = wrapper.getBoundingClientRect();
+ const wrapperWidth = Math.round(wrapperRect.width);
+ const wrapperHeight = Math.round(wrapperRect.height);
+
+ console.log(`📏 Wrapper dimensions:`, { width: wrapperWidth, height: wrapperHeight });
+
+ // Cloner le SVG
+ const clonedSvg = svgElement.cloneNode(true);
+
+ // Appliquer les styles Mermaid
+ applyMermaidStylesToSvg(clonedSvg);
+
+ // Créer une image PLUS GRANDE pour permettre un vrai zoom (2x la taille)
+ const zoomFactor = 2;
+ const imageWidth = wrapperWidth * zoomFactor;
+ const imageHeight = wrapperHeight * zoomFactor;
+
+ // Forcer les dimensions plus grandes sur le SVG
+ clonedSvg.setAttribute('width', imageWidth);
+ clonedSvg.setAttribute('height', imageHeight);
+ clonedSvg.style.width = `${imageWidth}px`;
+ clonedSvg.style.height = `${imageHeight}px`;
+
+ console.log(`🔍 Creating zoomable image:`, {
+ original: { width: wrapperWidth, height: wrapperHeight },
+ zoomed: { width: imageWidth, height: imageHeight },
+ factor: zoomFactor
+ });
+
+ // Créer une URL data pour le SVG
+ const svgData = new XMLSerializer().serializeToString(clonedSvg);
+ const svgBlob = new Blob([svgData], { type: 'image/svg+xml;charset=utf-8' });
+ const svgUrl = URL.createObjectURL(svgBlob);
+
+ // Créer un élément img avec les dimensions du wrapper (affichage normal)
+ const imgElement = document.createElement('img');
+ imgElement.src = svgUrl;
+ imgElement.style.width = `${wrapperWidth}px`; // Affichage normal
+ imgElement.style.height = `${wrapperHeight}px`; // Affichage normal
+ imgElement.style.display = 'block';
+ imgElement.setAttribute('data-zoomable', '1');
+ imgElement.classList.add('mermaid-zoom-image'); // Classe spécifique pour Mermaid
+
+ // Ajouter les attributs pour medium-zoom (dimensions réelles de l'image)
+ imgElement.setAttribute('data-zoom-width', imageWidth);
+ imgElement.setAttribute('data-zoom-height', imageHeight);
+
+ console.log(`🖼️ Image created with exact dimensions:`, { width: wrapperWidth, height: wrapperHeight });
+
+ // Remplacer le contenu du wrapper par l'image
+ wrapper.innerHTML = '';
+ wrapper.appendChild(imgElement);
+
+ // Retirer la classe "converting" pour restaurer l'opacité normale
+ wrapper.classList.remove("converting");
+
+ // Attendre que l'image soit chargée puis initialiser medium-zoom
+ imgElement.onload = () => {
+ console.log(`✅ Image loaded, initializing REAL medium-zoom`);
+
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
+ const background = isDark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)";
+
+ // Utiliser VRAI medium-zoom
+ const zoomInstance = window.mediumZoom(imgElement, {
+ background,
+ margin: 24,
+ scrollOffset: 0,
+ });
+
+ console.log(`🎉 REAL medium-zoom initialized with exact dimensions!`);
+
+ // Forcer les bonnes couleurs en JavaScript (contournement du CSS global)
+ const forceCorrectColors = () => {
+ // Trouver l'image zoomée dans le DOM
+ const zoomedImage = document.querySelector('.medium-zoom-image--opened');
+ if (zoomedImage && zoomedImage.classList.contains('mermaid-zoom-image')) {
+ console.log(`🎨 Forcing correct colors for Mermaid zoom image`);
+ zoomedImage.style.filter = 'none';
+ zoomedImage.style.setProperty('filter', 'none', 'important');
+ }
+
+ // Forcer les z-index élevés
+ const overlay = document.querySelector('.medium-zoom-overlay');
+ if (overlay) {
+ console.log(`🔝 Forcing high z-index for overlay`);
+ overlay.style.zIndex = '9999999';
+ overlay.style.setProperty('z-index', '9999999', 'important');
+ }
+
+ if (zoomedImage) {
+ console.log(`🔝 Forcing high z-index for zoomed image`);
+ zoomedImage.style.zIndex = '10000000';
+ zoomedImage.style.setProperty('z-index', '10000000', 'important');
+ }
+ };
+
+ // Écouter les événements de zoom pour appliquer les bonnes couleurs
+ imgElement.addEventListener('zoom:open', forceCorrectColors);
+ imgElement.addEventListener('zoom:opened', forceCorrectColors);
+
+ // Observer pour les changements de thème
+ const themeObserver = new MutationObserver(() => {
+ console.log(`🎨 Theme changed, forcing Mermaid re-render with new theme`);
+
+ // Forcer Mermaid à se re-rendre avec le nouveau thème
+ const currentTheme = document.documentElement.getAttribute("data-theme");
+ console.log(`🎨 Current theme: ${currentTheme}`);
+
+ // Attendre un peu pour que Mermaid détecte le changement de thème
+ setTimeout(() => {
+ // Forcer Mermaid à se re-rendre avec le nouveau thème
+ const themeUpdated = forceMermaidThemeUpdate(originalMermaid);
+
+ if (themeUpdated) {
+ // Attendre que le re-rendu soit terminé
+ setTimeout(() => {
+ // Re-obtenir le SVG mis à jour
+ const updatedSvgElement = originalMermaid.querySelector("svg");
+ if (updatedSvgElement) {
+ console.log(`🔄 Re-converting with updated SVG for theme: ${currentTheme}`);
+ convertSvgToImagePreservingDimensions(updatedSvgElement, wrapper, originalMermaid);
+ } else {
+ console.log(`❌ No updated SVG found after theme change`);
+ }
+ }, 200); // Délai pour laisser Mermaid finir le re-rendu
+ } else {
+ console.log(`❌ Failed to update Mermaid theme, using current SVG`);
+ // Fallback: utiliser le SVG actuel même s'il n'est pas mis à jour
+ const currentSvgElement = originalMermaid.querySelector("svg");
+ if (currentSvgElement) {
+ convertSvgToImagePreservingDimensions(currentSvgElement, wrapper, originalMermaid);
+ }
+ }
+ }, 100); // Petit délai pour laisser Mermaid se mettre à jour
+ });
+
+ themeObserver.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+
+ wrapper._themeObserver = themeObserver;
+ };
+
+ imgElement.onerror = (error) => {
+ console.error(`❌ Error loading SVG as image:`, error);
+ // Fallback: utiliser le zoom custom
+ wrapper.innerHTML = '';
+ wrapper.appendChild(originalMermaid);
+ wrapper.classList.remove("converting");
+ wrapper.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openMermaidZoom(wrapper, originalMermaid);
+ });
+ };
+
+ } catch (error) {
+ console.error("❌ Error converting SVG to image:", error);
+ // Fallback: utiliser le zoom custom
+ wrapper.classList.remove("converting");
+ wrapper.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openMermaidZoom(wrapper, originalMermaid);
+ });
+ }
+ }
+
+ // Fonction pour initialiser medium-zoom directement sur le SVG
+ function initializeMediumZoomOnSvg(svgElement, wrapper, originalMermaid) {
+ try {
+ // S'assurer que le SVG a des dimensions fixes pour éviter le flicker
+ const bbox = svgElement.getBBox();
+ const width = bbox.width || svgElement.clientWidth || 800;
+ const height = bbox.height || svgElement.clientHeight || 600;
+
+ // Fixer les dimensions du SVG pour éviter les changements
+ svgElement.setAttribute('width', width);
+ svgElement.setAttribute('height', height);
+ svgElement.style.width = '100%';
+ svgElement.style.height = 'auto';
+ svgElement.style.display = 'block';
+
+ // Retirer la classe "converting" pour restaurer l'opacité normale
+ wrapper.classList.remove("converting");
+
+ console.log(`📏 SVG dimensions fixed:`, { width, height });
+
+ // Initialiser medium-zoom directement sur le wrapper SVG
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
+ const background = isDark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)";
+
+ // Utiliser medium-zoom sur le wrapper qui contient le SVG
+ window.mediumZoom(wrapper, {
+ background,
+ margin: 24,
+ scrollOffset: 0,
+ });
+
+ console.log(`🎉 Medium-zoom initialized directly on SVG wrapper!`);
+
+ // Observer pour les changements de thème
+ const themeObserver = new MutationObserver(() => {
+ console.log(`🎨 Theme changed, updating medium-zoom background`);
+ // Re-initialiser medium-zoom avec le nouveau thème
+ initializeMediumZoomOnSvg(svgElement, wrapper, originalMermaid);
+ });
+
+ themeObserver.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+
+ // Stocker l'observer pour le nettoyer plus tard
+ wrapper._themeObserver = themeObserver;
+
+ } catch (error) {
+ console.error("❌ Error initializing medium-zoom on SVG:", error);
+ // Fallback: utiliser le zoom custom
+ wrapper.classList.remove("converting");
+ wrapper.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openMermaidZoom(wrapper, originalMermaid);
+ });
+ }
+ }
+
+ // Fonction pour attendre que Mermaid soit stable (évite le flicker)
+ function waitForMermaidStable(mermaidEl, wrapper, index) {
+ // Approche simplifiée : attendre un délai fixe puis convertir
+ console.log(`⏳ Waiting for Mermaid ${index} to stabilize...`);
+
+ setTimeout(() => {
+ const svgElement = mermaidEl.querySelector("svg");
+ if (svgElement) {
+ console.log(`🎯 Converting Mermaid ${index} to image`);
+ convertSvgToImageForMediumZoom(svgElement, wrapper, mermaidEl);
+ } else {
+ console.log(`❌ No SVG found in Mermaid element ${index}`);
+ }
+ }, 2000); // Attendre 2 secondes puis convertir
+ }
+
+ // Fonction pour convertir SVG en image SANS canvas (pour éviter l'erreur de sécurité)
+ function convertSvgToImageForMediumZoom(svgElement, wrapper, originalMermaid) {
+ console.log(`🔄 Converting SVG to image for REAL medium-zoom`);
+
+ try {
+ // Cloner le SVG
+ const clonedSvg = svgElement.cloneNode(true);
+
+ // Appliquer directement les styles SVG pour préserver l'apparence
+ applyMermaidStylesToSvg(clonedSvg);
+
+ // S'assurer que le SVG a des dimensions
+ const bbox = clonedSvg.getBBox();
+ const width = bbox.width || clonedSvg.clientWidth || 800;
+ const height = bbox.height || clonedSvg.clientHeight || 600;
+
+ console.log(`📏 SVG dimensions:`, { width, height });
+
+ // Créer une image directement à partir du SVG (sans canvas)
+ const svgData = new XMLSerializer().serializeToString(clonedSvg);
+
+ // Ajouter les dimensions au SVG
+ clonedSvg.setAttribute("width", width);
+ clonedSvg.setAttribute("height", height);
+ const svgWithDimensions = new XMLSerializer().serializeToString(clonedSvg);
+
+ // Créer une URL data pour le SVG
+ const svgBlob = new Blob([svgWithDimensions], {
+ type: "image/svg+xml;charset=utf-8",
+ });
+ const svgUrl = URL.createObjectURL(svgBlob);
+
+ // Créer un élément img avec le SVG
+ const imgElement = document.createElement("img");
+ imgElement.src = svgUrl;
+ imgElement.style.width = "100%";
+ imgElement.style.height = "auto";
+ imgElement.style.display = "block";
+ imgElement.setAttribute("data-zoomable", "1");
+
+ // Remplacer le contenu du wrapper par l'image
+ wrapper.innerHTML = "";
+ wrapper.appendChild(imgElement);
+
+ // Retirer la classe "converting" pour restaurer l'opacité normale
+ wrapper.classList.remove("converting");
+
+ console.log(`🖼️ SVG converted to img element`);
+
+ // Attendre que l'image soit chargée puis initialiser medium-zoom
+ imgElement.onload = () => {
+ console.log(`✅ Image loaded, initializing REAL medium-zoom`);
+
+ const isDark = document.documentElement.getAttribute("data-theme") === "dark";
+ const background = isDark ? "rgba(0,0,0,.9)" : "rgba(0,0,0,.85)";
+
+ // Utiliser VRAI medium-zoom
+ window.mediumZoom(imgElement, {
+ background,
+ margin: 24,
+ scrollOffset: 0,
+ });
+
+ console.log(`🎉 REAL medium-zoom initialized on Mermaid!`);
+
+ // Observer pour les changements de thème - reconvertir l'image
+ const themeObserver = new MutationObserver(() => {
+ console.log(`🎨 Theme changed, reconverting Mermaid image`);
+ // Reconverter l'image avec le nouveau thème
+ convertSvgToImageForMediumZoom(svgElement, wrapper, originalMermaid);
+ });
+
+ themeObserver.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ["data-theme"],
+ });
+
+ // Stocker l'observer pour le nettoyer plus tard
+ wrapper._themeObserver = themeObserver;
+ };
+
+ imgElement.onerror = (error) => {
+ console.error(`❌ Error loading SVG as image:`, error);
+ // Fallback: utiliser le zoom custom
+ wrapper.innerHTML = "";
+ wrapper.appendChild(originalMermaid);
+ wrapper.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openMermaidZoom(wrapper, originalMermaid);
+ });
+ };
+
+ } catch (error) {
+ console.error("❌ Error converting SVG to image:", error);
+ // Fallback: utiliser le zoom custom
+ wrapper.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ openMermaidZoom(wrapper, originalMermaid);
+ });
+ }
+ }
+
+ // Fonction pour ouvrir le zoom custom des diagrammes Mermaid (comme medium-zoom)
+ function openMermaidZoom(wrapper, mermaidElement) {
+ // Créer l'overlay (comme medium-zoom)
+ const overlay = document.createElement("div");
+ overlay.className = "medium-zoom-overlay";
+ overlay.style.cssText = `
+ position: fixed;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: rgba(0, 0, 0, 0.85);
+ z-index: 9999;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ cursor: zoom-out;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ `;
+
+ // Créer l'image zoomée (comme medium-zoom)
+ const zoomImage = document.createElement("div");
+ zoomImage.className = "medium-zoom-image";
+ zoomImage.style.cssText = `
+ max-width: 90%;
+ max-height: 90%;
+ background: white;
+ border-radius: 8px;
+ padding: 20px;
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
+ transform: scale(0.8);
+ transition: transform 0.3s ease;
+ overflow: auto;
+ `;
+
+ // Cloner le diagramme Mermaid
+ const clonedMermaid = mermaidElement.cloneNode(true);
+ clonedMermaid.style.cssText = `
+ width: 100%;
+ height: auto;
+ max-width: none;
+ display: block;
+ `;
+
+ zoomImage.appendChild(clonedMermaid);
+ overlay.appendChild(zoomImage);
+
+ // Ajouter l'overlay au body
+ document.body.appendChild(overlay);
+
+ // Animation d'ouverture (comme medium-zoom)
+ requestAnimationFrame(() => {
+ overlay.style.opacity = "1";
+ zoomImage.style.transform = "scale(1)";
+ });
+
+ // Fermer au clic sur l'overlay
+ overlay.addEventListener("click", (e) => {
+ if (e.target === overlay) {
+ closeMermaidZoom(overlay);
+ }
+ });
+
+ // Fermer avec Escape
+ const handleEscape = (e) => {
+ if (e.key === "Escape") {
+ closeMermaidZoom(overlay);
+ document.removeEventListener("keydown", handleEscape);
+ }
+ };
+ document.addEventListener("keydown", handleEscape);
+
+ // Fermer au scroll (comme medium-zoom)
+ const handleScroll = () => {
+ closeMermaidZoom(overlay);
+ };
+ window.addEventListener("wheel", handleScroll, { passive: true });
+ window.addEventListener("touchmove", handleScroll, { passive: true });
+ window.addEventListener("scroll", handleScroll, { passive: true });
+
+ // Stocker les handlers pour les nettoyer
+ overlay._handlers = { handleEscape, handleScroll };
+ }
+
+ function closeMermaidZoom(overlay) {
+ // Animation de fermeture
+ overlay.style.opacity = "0";
+ const zoomImage = overlay.querySelector(".medium-zoom-image");
+ if (zoomImage) {
+ zoomImage.style.transform = "scale(0.8)";
+ }
+
+ // Nettoyer les event listeners
+ if (overlay._handlers) {
+ document.removeEventListener("keydown", overlay._handlers.handleEscape);
+ window.removeEventListener("wheel", overlay._handlers.handleScroll);
+ window.removeEventListener("touchmove", overlay._handlers.handleScroll);
+ window.removeEventListener("scroll", overlay._handlers.handleScroll);
+ }
+
+ // Supprimer l'overlay après l'animation
+ setTimeout(() => {
+ if (document.body.contains(overlay)) {
+ document.body.removeChild(overlay);
+ }
+ }, 300);
+ }
+
+ // Fonction pour nettoyer les observers
+ function cleanupMermaidObservers(wrapper) {
+ if (wrapper._themeObserver) {
+ wrapper._themeObserver.disconnect();
+ wrapper._themeObserver = null;
+ }
+ }
+
+ // Fonction principale pour initialiser le zoom Mermaid
+ function setupMermaidZoom() {
+ console.log(`🎯 setupMermaidZoom called`);
+ const mermaidElements = document.querySelectorAll(".mermaid");
+ console.log(`🔍 Found ${mermaidElements.length} Mermaid elements`);
+
+ let processedCount = 0;
+ mermaidElements.forEach((mermaidEl, index) => {
+ // Vérifier si déjà wrappé
+ if (
+ mermaidEl.parentElement &&
+ mermaidEl.parentElement.classList.contains("mermaid-zoom-wrapper")
+ ) {
+ console.log(`📦 Mermaid ${index} already wrapped`);
+ processedCount++;
+ return;
+ }
+
+ console.log(`📦 Wrapping Mermaid element ${index}`);
+
+ // Créer le wrapper
+ const wrapper = document.createElement("div");
+ wrapper.className = "mermaid-zoom-wrapper";
+ wrapper.setAttribute("data-zoomable", "1");
+
+ // Insérer le wrapper avant l'élément Mermaid
+ mermaidEl.parentNode.insertBefore(wrapper, mermaidEl);
+
+ // Déplacer l'élément Mermaid dans le wrapper
+ wrapper.appendChild(mermaidEl);
+
+ // S'assurer que le wrapper a des dimensions
+ wrapper.style.display = "block";
+ wrapper.style.width = "100%";
+ wrapper.style.maxWidth = "100%";
+
+ // Ajouter la classe "converting" pour masquer le flicker
+ wrapper.classList.add("converting");
+
+ // Convertir SVG en image pour utiliser VRAI medium-zoom
+ console.log(`✅ Converting Mermaid to image for REAL medium-zoom`);
+
+ // Utiliser le VRAI medium-zoom avec conversion SVG → image SANS flicker
+ console.log(`✅ Setting up REAL medium-zoom for Mermaid ${index}`);
+
+ // Attendre que Mermaid soit stable puis convertir avec préservation des dimensions
+ setTimeout(() => {
+ const svgElement = mermaidEl.querySelector("svg");
+ if (svgElement) {
+ console.log(`🎯 Converting SVG to image with dimension preservation`);
+ convertSvgToImagePreservingDimensions(svgElement, wrapper, mermaidEl);
+ } else {
+ console.log(`❌ No SVG found in Mermaid element ${index}`);
+ }
+ }, 1000); // Réduit de 2000ms à 1000ms pour être plus rapide
+ });
+
+ console.log(`✅ Processed ${processedCount} already wrapped, ${mermaidElements.length - processedCount} new diagrams`);
+ }
+
+ // Observer global pour forcer les bonnes couleurs et z-index sur les images Mermaid zoomées
+ function setupGlobalMermaidColorFix() {
+ const observer = new MutationObserver((mutations) => {
+ mutations.forEach((mutation) => {
+ if (mutation.type === 'childList') {
+ // Vérifier si une image Mermaid est zoomée
+ const zoomedMermaidImage = document.querySelector('.medium-zoom-image--opened.mermaid-zoom-image');
+ if (zoomedMermaidImage) {
+ console.log(`🎨 Global fix: Found zoomed Mermaid image, forcing correct colors and z-index`);
+ zoomedMermaidImage.style.filter = 'none';
+ zoomedMermaidImage.style.setProperty('filter', 'none', 'important');
+ zoomedMermaidImage.style.zIndex = '10000000';
+ zoomedMermaidImage.style.setProperty('z-index', '10000000', 'important');
+ }
+
+ // Vérifier si l'overlay medium-zoom existe
+ const overlay = document.querySelector('.medium-zoom-overlay');
+ if (overlay) {
+ console.log(`🔝 Global fix: Found medium-zoom overlay, forcing high z-index`);
+ overlay.style.zIndex = '9999999';
+ overlay.style.setProperty('z-index', '9999999', 'important');
+ }
+ }
+ });
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true
+ });
+
+ console.log(`🎨 Global Mermaid color and z-index fix observer started`);
+ }
+
+ // Fonction pour initialiser le zoom Mermaid avec retry
+ function initMermaidZoom() {
+ console.log("🎯 initMermaidZoom called");
+
+ // Attendre que mediumZoom soit disponible
+ const checkMediumZoom = () => {
+ if (window.mediumZoom) {
+ console.log("✅ mediumZoom found, initializing Mermaid zoom");
+ setupMermaidZoom();
+ } else {
+ console.log("⏳ Waiting for mediumZoom...");
+ setTimeout(checkMediumZoom, 100);
+ }
+ };
+
+ checkMediumZoom();
+ }
+
+ // Export des fonctions pour utilisation globale
+ window.MermaidZoom = {
+ init: initMermaidZoom,
+ setup: setupMermaidZoom,
+ cleanup: cleanupMermaidObservers,
+ convertSvgToImage: convertSvgToImageForMediumZoom,
+ openZoom: openMermaidZoom,
+ closeZoom: closeMermaidZoom
+ };
+
+ // Auto-initialisation si le DOM est déjà chargé
+ if (document.readyState === "loading") {
+ document.addEventListener("DOMContentLoaded", () => {
+ initMermaidZoom();
+ setupGlobalMermaidColorFix();
+ });
+ } else {
+ initMermaidZoom();
+ setupGlobalMermaidColorFix();
+ }
+
+ // Aussi après le chargement complet
+ window.addEventListener("load", () => {
+ setTimeout(() => {
+ initMermaidZoom();
+ setupGlobalMermaidColorFix();
+ }, 1000);
+ });
+
+ // Observer simple pour les nouveaux diagrammes Mermaid (avec debounce)
+ let resizeTimeout;
+ const observer = new MutationObserver(() => {
+ // Debounce pour éviter les appels multiples
+ clearTimeout(resizeTimeout);
+ resizeTimeout = setTimeout(() => {
+ // Vérifier s'il y a vraiment de nouveaux diagrammes
+ const mermaidElements = document.querySelectorAll(".mermaid");
+ const wrappedElements = document.querySelectorAll(".mermaid-zoom-wrapper");
+
+ // Seulement si il y a plus de diagrammes que de wrappers
+ if (mermaidElements.length > wrappedElements.length) {
+ console.log(`🔄 New Mermaid diagrams detected: ${mermaidElements.length} total, ${wrappedElements.length} wrapped`);
+ initMermaidZoom();
+ }
+ }, 500); // Délai plus long pour éviter les appels fréquents
+ });
+
+ observer.observe(document.body, {
+ childList: true,
+ subtree: true,
+ });
+
+ console.log("🚀 Mermaid Zoom Script v19.0 loaded - DEBOUNCED observer to prevent resize loops");
+} // Fin du bloc else (MERMAID_ZOOM_ENABLED)
diff --git a/content/articles/multi-harness-rl/app/src/styles/_base.css b/content/articles/multi-harness-rl/app/src/styles/_base.css
new file mode 100644
index 00000000..20d1d7de
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/_base.css
@@ -0,0 +1,168 @@
+@import "https://fonts.googleapis.com/css2?family=Source+Sans+Pro:ital,wght@0,200..900;1,200..900&display=swap";
+
+html {
+ font-size: 16px;
+ line-height: 1.6;
+ background-color: var(--page-bg);
+ overflow-x: hidden;
+}
+
+.content-grid main {
+ color: var(--text-color);
+}
+
+.content-grid main p {
+ margin: 0 0 var(--spacing-3);
+}
+
+.content-grid main h2 {
+ font-weight: 600;
+ font-size: clamp(22px, 2.6vw, 32px);
+ line-height: 1.2;
+ margin: var(--spacing-10) 0 var(--spacing-5);
+ padding-bottom: var(--spacing-2);
+ border-bottom: 1px solid var(--border-color);
+}
+
+.content-grid main h3 {
+ font-weight: 700;
+ font-size: clamp(18px, 2.1vw, 22px);
+ line-height: 1.25;
+ margin: var(--spacing-8) 0 var(--spacing-4);
+}
+
+.content-grid main h4 {
+ font-weight: 600;
+ text-transform: uppercase;
+ font-size: 16px;
+ line-height: 1.2;
+ margin: var(--spacing-6) 0 var(--spacing-4);
+}
+
+.content-grid main a {
+ color: var(--primary-color);
+ text-decoration: none;
+ background: var(--surface-bg);
+ border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent);
+}
+
+.content-grid main a:hover {
+ color: var(--primary-color-hover);
+ border-bottom: 1px solid color-mix(in srgb, var(--primary-color, #007AFF) 40%, transparent);
+}
+
+/* Do not underline heading links inside the article (not the TOC) */
+.content-grid main h2 a,
+.content-grid main h3 a,
+.content-grid main h4 a,
+.content-grid main h5 a,
+.content-grid main h6 a {
+ color: inherit;
+ background: none;
+ border-bottom: none;
+ text-decoration: none;
+}
+
+.content-grid main h2 a:hover,
+.content-grid main h3 a:hover,
+.content-grid main h4 a:hover,
+.content-grid main h5 a:hover,
+.content-grid main h6 a:hover {
+ color: inherit;
+ border-bottom: none;
+ text-decoration: none;
+}
+
+.content-grid main ul,
+.content-grid main ol {
+ padding-left: 24px;
+ margin: 0 0 var(--spacing-3);
+}
+
+.content-grid main li {
+ margin-bottom: var(--spacing-2);
+}
+
+.content-grid main li:last-child {
+ margin-bottom: 0;
+}
+
+.content-grid main blockquote {
+ border-left: 2px solid var(--border-color);
+ padding-left: var(--spacing-4);
+ font-style: italic;
+ color: var(--muted-color);
+ margin: var(--spacing-4) 0;
+}
+
+.content-grid main hr {
+ border: none;
+ border-bottom: 1px solid var(--border-color);
+ margin: var(--spacing-5) 0;
+}
+
+.muted {
+ color: var(--muted-color);
+}
+
+[data-footnote-ref] {
+ margin-left: 4px;
+}
+
+.content-grid main mark {
+ background-color: color-mix(in srgb, var(--primary-color, #007AFF) 10%, transparent);
+ border: 1px solid color-mix(in srgb, var(--primary-color) 18%, transparent);
+ color: inherit;
+ padding: 4px 6px;
+ border-radius: 4px;
+ font-weight: 500;
+ box-decoration-break: clone;
+ -webkit-box-decoration-break: clone;
+}
+
+.feature-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 12px;
+ margin: 46px 0;
+}
+
+.feature-card {
+ display: flex;
+ flex-direction: column;
+ padding: 16px;
+ border: 1px solid color-mix(in srgb, var(--primary-color) 40%, transparent);
+ ;
+ background: color-mix(in srgb, var(--primary-color, #007AFF) 05%, transparent) !important;
+ border-radius: 8px;
+ text-decoration: none;
+ color: inherit;
+ transition: all 0.2s ease;
+}
+
+.feature-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+}
+
+.feature-card strong {
+ font-size: 14px;
+ font-weight: 600;
+ color: var(--text-color);
+ color: var(--primary-color) !important;
+ margin-bottom: 0px !important;
+}
+
+.feature-card span {
+ font-size: 12px;
+ color: var(--muted-color);
+ color: var(--primary-color) !important;
+ margin-bottom: 0px !important;
+ opacity: 1;
+}
+
+.katex .tag {
+ background: none;
+ border: none;
+ opacity: 0.4;
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/_layout.css b/content/articles/multi-harness-rl/app/src/styles/_layout.css
new file mode 100644
index 00000000..217c7b63
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/_layout.css
@@ -0,0 +1,384 @@
+/* ============================================================================ */
+/* Layout – 3-column grid (Table of Contents / Article / Aside) */
+/* ============================================================================ */
+
+.content-grid {
+ max-width: 1400px;
+ margin: 0 auto;
+ padding: 0 var(--content-padding-x);
+ margin-top: 40px;
+ display: grid;
+ grid-template-columns: 240px minmax(0, 880px) 180px;
+ gap: 32px;
+ align-items: start;
+}
+
+/* The column is sized for figures, not for prose. Charts, diagrams and tables
+ use its full 880px; running text is capped near a readable measure so the
+ wider column does not turn every paragraph into a 120-character line.
+ Note this is a cap, not a centred breakout: translating figures outward
+ instead would overlap the table of contents in the left column. */
+.content-grid > main > :is(p, ul, ol, dl, blockquote, h2, h3, h4, h5, h6, hr) {
+ max-width: 760px;
+}
+
+.content-grid > main > figure.html-embed,
+.content-grid > main > .html-embed,
+.content-grid > main > .table-wrapper,
+.content-grid > main > .wide,
+.content-grid > main > figure {
+ max-width: 100%;
+}
+
+.content-grid>main {
+ max-width: 100%;
+ margin: 0;
+ padding: 0;
+}
+
+.content-grid>main>*:first-child {
+ margin-top: 0;
+}
+
+@media (--bp-content-collapse) {
+ .content-grid {
+ overflow: hidden;
+ display: block;
+ margin-top: var(--spacing-2);
+ }
+
+ .content-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .table-of-contents {
+ position: static;
+ display: none;
+ }
+
+ .toc-mobile-toggle {
+ display: flex;
+ }
+
+ .toc-mobile-backdrop {
+ display: block;
+ }
+
+ .toc-mobile-sidebar {
+ display: flex;
+ }
+
+ .footer-inner {
+ grid-template-columns: 1fr;
+ gap: 16px;
+ }
+
+ .footer-inner>.footer-heading {
+ grid-column: auto;
+ margin-top: 16px;
+ }
+
+ .footer-inner {
+ display: block;
+ padding: 40px 16px;
+ }
+}
+
+
+
+/* ============================================================================ */
+/* Width helpers – slightly wider than main column, and full-width to viewport */
+/* ---------------------------------------------------------------------------- */
+
+.wide,
+.full-width {
+ box-sizing: border-box;
+ position: relative;
+ z-index: var(--z-elevated);
+ background-color: var(--page-bg);
+}
+
+.wide {
+ /* Target up to ~1100px while staying within viewport minus page gutters */
+ width: min(1100px, 100vw - var(--content-padding-x) * 4);
+ margin-left: 50%;
+ transform: translateX(-50%);
+ padding: calc(var(--content-padding-x)*4);
+ border-radius: calc(var(--button-radius)*4);
+ background-color: var(--page-bg);
+ -webkit-mask:
+ linear-gradient(to right, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%),
+ linear-gradient(to bottom, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%);
+ -webkit-mask-composite: intersect;
+ mask:
+ linear-gradient(to right, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%),
+ linear-gradient(to bottom, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%);
+ mask-composite: intersect;
+}
+
+.wide>* {
+ margin-bottom: 0 !important;
+}
+
+.full-width {
+ /* Span the full viewport width and center relative to viewport */
+ width: 100vw;
+ margin-left: calc(50% - 50vw);
+ margin-right: calc(50% - 50vw);
+ padding: calc(var(--content-padding-x)*4);
+ border-radius: calc(var(--button-radius)*4);
+ background-color: var(--page-bg);
+ -webkit-mask:
+ linear-gradient(to bottom, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%);
+ mask:
+ linear-gradient(to bottom, transparent 0px, black 20px, black calc(100% - 20px), transparent 100%);
+}
+
+.full-width figure figcaption {
+ text-align: center !important;
+}
+
+@media (--bp-content-collapse) {
+
+ .wide,
+ .full-width {
+ width: 100%;
+ margin-left: 0;
+ margin-right: 0;
+ padding: 0;
+ transform: none;
+ }
+}
+
+/* ============================================================================ */
+/* Theme toggle placement */
+/* ============================================================================ */
+
+#theme-toggle {
+ position: absolute;
+ top: var(--spacing-4);
+ left: var(--spacing-4);
+ margin: 0;
+ z-index: var(--z-overlay);
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ border: 1px solid var(--border-color);
+ background: var(--page-bg);
+ box-shadow: 0 2px 12px rgba(0,0,0,.08);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0;
+ cursor: pointer;
+ color: var(--text-color);
+ transition: transform 150ms ease, box-shadow 150ms ease;
+}
+#theme-toggle:active {
+ transform: scale(0.92);
+}
+
+@media (--bp-content-collapse) {
+ #theme-toggle {
+ display: none;
+ }
+}
+
+/* ------------------------------------------------------------------------- */
+/* Hero meta bar responsiveness */
+/* One cell per row on phones. The previous two-up layout put author beside */
+/* affiliation and published beside the PDF button, which on a narrow screen */
+/* read as two ragged columns and truncated the longer values. */
+/* ------------------------------------------------------------------------- */
+@media (--bp-md) {
+ header.meta .meta-container {
+ display: flex;
+ flex-wrap: wrap;
+ row-gap: 14px;
+ column-gap: 8px;
+ max-width: 100%;
+ padding: 0 var(--spacing-4);
+ }
+
+ header.meta .meta-container .meta-container-cell {
+ flex: 1 1 100%;
+ min-width: 0;
+ max-width: none;
+ text-align: left;
+ }
+}
+
+@media (--bp-xxs) {
+ header.meta .meta-container .meta-container-cell {
+ flex-basis: 100%;
+ text-align: left;
+ }
+
+ /* Center ordered list numbers within meta (e.g., affiliations) */
+ header.meta .affiliations {
+ list-style-position: inside;
+ padding-left: 0;
+ margin-left: 0;
+ }
+
+ header.meta .affiliations li {
+ text-align: center;
+ }
+}
+
+
+/* ------------------------------------------------------------------------- */
+/* D3 neural embed responsiveness */
+/* Stack canvas (left) over network (right) on small screens */
+/* ------------------------------------------------------------------------- */
+@media (--bp-md) {
+ .d3-neural .panel {
+ flex-direction: column;
+ }
+
+ .d3-neural .panel .left {
+ flex: 0 0 auto;
+ width: 100%;
+ }
+
+ .d3-neural .panel .right {
+ flex: 0 0 auto;
+ width: 100%;
+ min-width: 0;
+ }
+}
+
+/* ============================================================================ */
+/* Auto figure numbering */
+/* Applies a CSS counter across all figure-bearing components: */
+/* Reference (.reference-wrapper), Image (.image-wrapper figure), */
+/* and HtmlEmbed (.html-embed) */
+/* ============================================================================ */
+
+.content-grid > main {
+ counter-reset: figure;
+}
+
+/* Increment on every figure-like component */
+.content-grid > main .reference-wrapper,
+.content-grid > main .image-wrapper > figure,
+.content-grid > main figure.html-embed {
+ counter-increment: figure;
+}
+
+/* Display "Figure N · " before each figcaption */
+.content-grid > main .reference-wrapper .reference__caption::before,
+.content-grid > main .image-wrapper > figure > figcaption::before,
+.content-grid > main figure.html-embed > .html-embed__desc::before {
+ content: "Figure " counter(figure) " · ";
+ font-weight: 600;
+}
+
+/* ============================================================================ */
+/* "paper" template - academic project-page layout (single centered column) */
+/* Activated by frontmatter `template: "paper"` -> data-template="paper" */
+/* Reference: academic project pages like diffusion-cot.github.io */
+/* ============================================================================ */
+
+/* ---- Paper hero banner: placed between meta and content in index.astro ---- */
+
+.paper-hero-banner {
+ max-width: 920px;
+ margin: 32px auto 0;
+ padding: 0 16px;
+}
+
+.paper-hero-banner :global(.html-embed) {
+ border-radius: 8px;
+ overflow: hidden;
+}
+
+.paper-hero-banner :global(.html-embed__card) {
+ border-radius: 8px;
+}
+
+/* Thin separator between banner area and content */
+[data-template="paper"] .paper-hero-banner + .content-grid {
+ margin-top: 48px;
+ border-top: 1px solid var(--border-color);
+ padding-top: 40px;
+}
+
+/* When no banner, separator is directly below meta */
+[data-template="paper"] .meta + .content-grid,
+[data-template="paper"] .hero-paper-meta + .content-grid {
+ margin-top: 32px;
+ border-top: 1px solid var(--border-color);
+ padding-top: 40px;
+}
+
+/* ---- Content grid: single centered column, no TOC ---- */
+
+[data-template="paper"] .content-grid {
+ grid-template-columns: minmax(0, 780px);
+ justify-content: center;
+ margin-top: 0;
+}
+
+[data-template="paper"] .table-of-contents {
+ display: none;
+}
+
+/* ---- Typography: clean left-aligned headings ---- */
+
+[data-template="paper"] .content-grid > main h2 {
+ text-align: left;
+ font-size: 1.75em;
+ font-weight: 700;
+ margin-top: 2.5em;
+ margin-bottom: 0.8em;
+ border-bottom: none;
+ padding-bottom: 0;
+}
+
+[data-template="paper"] .content-grid > main h2 a {
+ background: none;
+}
+
+[data-template="paper"] .content-grid > main h2:first-child,
+[data-template="paper"] .content-grid > main > *:first-child h2 {
+ margin-top: 0;
+}
+
+[data-template="paper"] .content-grid > main h3 {
+ font-size: 1.3em;
+ font-weight: 600;
+ margin-top: 2em;
+ margin-bottom: 0.6em;
+}
+
+/* ---- No figure numbering ---- */
+
+[data-template="paper"] .content-grid > main {
+ counter-reset: none;
+}
+
+[data-template="paper"] .content-grid > main .reference-wrapper .reference__caption::before,
+[data-template="paper"] .content-grid > main .image-wrapper > figure > figcaption::before,
+[data-template="paper"] .content-grid > main figure.html-embed > .html-embed__desc::before {
+ content: none;
+}
+
+/* ---- Footer: simple single-column block layout ---- */
+
+[data-template="paper"] .footer-inner {
+ display: block;
+ max-width: 780px;
+}
+
+[data-template="paper"] .references-block > .footer-heading,
+[data-template="paper"] .reuse-block > .footer-heading {
+ text-align: left;
+ padding-right: 0;
+ margin-top: 16px;
+ margin-bottom: 8px;
+}
+
+[data-template="paper"] .template-credit p {
+ margin-top: 32px;
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/_print-book.css b/content/articles/multi-harness-rl/app/src/styles/_print-book.css
new file mode 100644
index 00000000..dd1846a1
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/_print-book.css
@@ -0,0 +1,1002 @@
+/* ==========================================================================
+ Book Print Styles — "A Book Apart" inspired
+
+ Design principles:
+ - Georgia body, Source Sans Pro headings (ABA uses Expo Serif + Titling Gothic)
+ - Single accent color (#E85D3A — warm orange-red, ABA signature)
+ - Generous white space, the page breathes
+ - Chapter openings: large number + title on their own page
+ - Figures numbered and captioned cleanly
+ - Minimal decoration — typography does the work
+ ========================================================================== */
+
+@media print {
+
+ /* PAGE */
+ @page {
+ size: A4;
+ margin: 28mm 24mm 32mm 24mm;
+ }
+
+ /* RESET */
+ *, *::before, *::after { box-sizing: border-box; }
+
+ html {
+ font-size: 11.5pt;
+ line-height: 1.65;
+ }
+
+ body {
+ font-family: Georgia, "Palatino Linotype", "Book Antiqua", Palatino, serif;
+ color: #1a1a1a;
+ background: white;
+ margin: 0;
+ padding: 0;
+ font-kerning: normal;
+ font-variant-ligatures: common-ligatures;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+ }
+
+ /* LAYOUT */
+ .content-grid {
+ display: block !important;
+ max-width: none !important;
+ }
+
+ main {
+ max-width: none !important;
+ padding: 0 !important;
+ }
+
+ /* ========================================================================
+ HIDDEN ELEMENTS
+ ======================================================================== */
+
+ #theme-toggle,
+ .table-of-contents,
+ .toc-mobile-toggle,
+ .toc-mobile-backdrop,
+ .toc-mobile-sidebar,
+ .right-aside,
+ nav:not(.book-toc),
+ .code-lang-chip,
+ .meta-container-cell--pdf,
+ .interactive-only,
+ .html-embed__download,
+ .accordion__chevron,
+ button,
+ .button,
+ a.button {
+ display: none !important;
+ }
+
+ /* ========================================================================
+ TITLE PAGE
+ ======================================================================== */
+
+ .hero {
+ text-align: left;
+ break-after: page;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ min-height: 220mm;
+ padding: 0;
+ }
+
+ .hero-title {
+ font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 36pt;
+ font-weight: 800;
+ line-height: 1.08;
+ margin: 0 0 8mm;
+ color: #1a1a1a;
+ letter-spacing: -0.8pt;
+ }
+
+ .hero-banner {
+ width: 100%;
+ max-width: 130mm;
+ margin: 0 auto 15mm;
+ overflow: hidden;
+ }
+
+ .hero-banner svg {
+ width: 100% !important;
+ height: auto !important;
+ max-height: 70mm !important;
+ display: block;
+ }
+
+ .hero-desc {
+ font-size: 13pt;
+ color: #555;
+ font-style: normal;
+ margin: 0 0 0;
+ max-width: 100%;
+ line-height: 1.5;
+ border-top: 0.75pt solid #1a1a1a;
+ padding-top: 4mm;
+ }
+
+ .hero a {
+ border: none;
+ text-decoration: none;
+ font-weight: inherit;
+ color: inherit;
+ }
+
+ /* ========================================================================
+ META (author, date, DOI) — right after title page
+ ======================================================================== */
+
+ .meta {
+ padding: 0;
+ margin: 0;
+ border: none;
+ break-after: page;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ min-height: 200mm;
+ padding-bottom: 25mm;
+ }
+
+ .meta-container {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 5mm 15mm;
+ font-size: 9.5pt;
+ color: #444;
+ max-width: none;
+ padding: 0;
+ }
+
+ .meta-container-cell {
+ text-align: left;
+ max-width: none;
+ }
+
+ .meta-container-cell h3 {
+ font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 8pt;
+ text-transform: uppercase;
+ letter-spacing: 1.5pt;
+ color: #999;
+ margin: 0 0 1.5mm;
+ font-weight: 600;
+ }
+
+ .meta-container-cell p,
+ .meta-container-cell ul {
+ margin: 0;
+ }
+
+ .meta a {
+ color: #1a1a1a !important;
+ text-decoration: none !important;
+ border: none !important;
+ }
+
+ .meta-container-cell p,
+ .meta-container-cell a {
+ font-size: 11pt;
+ line-height: 1.5;
+ }
+
+ .meta h3,
+ .hero h3,
+ header h3 {
+ font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ }
+
+ .authors {
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ }
+
+ .authors li {
+ display: inline;
+ }
+
+ .authors li::after {
+ content: ", ";
+ }
+
+ .authors li:last-child::after {
+ content: "";
+ }
+
+ .authors li {
+ padding: 0;
+ }
+
+ /* ========================================================================
+ TABLE OF CONTENTS
+ ======================================================================== */
+
+ .book-toc {
+ break-after: page;
+ padding: 0;
+ break-inside: avoid;
+ }
+
+ .book-toc__title {
+ font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 12pt;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 3pt;
+ text-align: left;
+ margin: 0 0 10mm;
+ color: #1a1a1a;
+ padding: 0;
+ border: none;
+ break-before: auto;
+ counter-increment: none;
+ }
+
+ .book-toc__title::before,
+ .book-toc__title::after {
+ content: none !important;
+ display: none !important;
+ }
+
+ .book-toc__list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ }
+
+ .book-toc__chapter {
+ padding: 1.5mm 0;
+ border: none;
+ display: grid;
+ grid-template-columns: 10mm 1fr;
+ align-items: baseline;
+ gap: 0;
+ }
+
+ .book-toc__number {
+ font-family: "Source Sans Pro", sans-serif;
+ font-size: 11pt;
+ font-weight: 700;
+ color: #E85D3A;
+ text-align: right;
+ padding-right: 4mm;
+ }
+
+ .book-toc__label {
+ font-size: 11pt;
+ font-weight: 600;
+ color: #1a1a1a;
+ }
+
+ .book-toc__sections {
+ list-style: none;
+ margin: 0;
+ padding: 0 0 1mm 0;
+ grid-column: 2;
+ }
+
+ .book-toc__section {
+ display: grid;
+ grid-template-columns: 10mm 1fr;
+ align-items: baseline;
+ padding: 0.5mm 0;
+ font-size: 9pt;
+ color: #666;
+ }
+
+ .book-toc__section-number {
+ font-family: "Source Sans Pro", sans-serif;
+ font-size: 8.5pt;
+ font-weight: 600;
+ color: #999;
+ text-align: right;
+ padding-right: 3mm;
+ }
+
+ .book-toc__section-number {
+ font-weight: 400;
+ color: #aaa;
+ min-width: 10mm;
+ }
+
+ /* ========================================================================
+ HEADINGS
+ ======================================================================== */
+
+ /* ── Chapter opener (dedicated page) ── */
+
+ .book-chapter-opener {
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ min-height: 160mm;
+ padding-bottom: 30mm;
+ break-before: page;
+ break-after: page;
+ break-inside: avoid;
+ page-break-before: always;
+ page-break-after: always;
+ }
+
+ h2 {
+ font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 28pt;
+ font-weight: 800;
+ margin: 0;
+ padding: 0;
+ line-height: 1.1;
+ color: #1a1a1a;
+ letter-spacing: -0.5pt;
+ break-after: avoid-page;
+ }
+
+ .book-chapter-opener h2 {
+ break-before: auto;
+ border-bottom: 0.75pt solid #1a1a1a;
+ padding-bottom: 5mm;
+ }
+
+ h2:not(.book-chapter-opener h2) {
+ break-before: page;
+ }
+
+ h2[data-chapter-num]::before {
+ content: attr(data-chapter-num);
+ display: block;
+ font-family: "Source Sans Pro", sans-serif;
+ font-size: 80pt;
+ font-weight: 800;
+ color: #E85D3A;
+ line-height: 1;
+ margin-bottom: 8mm;
+ letter-spacing: -1pt;
+ }
+
+ h2::after {
+ content: none;
+ }
+
+ .hero + section h2:first-child,
+ .hero + div h2:first-child {
+ break-before: auto;
+ }
+
+ .hero + section h2:first-child::before,
+ .hero + div h2:first-child::before,
+ .hero + section h2:first-child::after,
+ .hero + div h2:first-child::after {
+ content: none;
+ }
+
+ h3 {
+ font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 14pt;
+ font-weight: 700;
+ margin: 10mm 0 4mm;
+ line-height: 1.3;
+ color: #1a1a1a;
+ break-after: avoid-page;
+ }
+
+ h3[data-section-num]::before {
+ content: attr(data-section-num) "\2003";
+ font-weight: 400;
+ color: #E85D3A;
+ font-size: 12pt;
+ }
+
+ h4 {
+ font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 11pt;
+ font-weight: 700;
+ margin: 7mm 0 3mm;
+ color: #1a1a1a;
+ text-transform: uppercase;
+ letter-spacing: 0.5pt;
+ break-after: avoid-page;
+ }
+
+ h5, h6 {
+ font-family: "Source Sans Pro", "Helvetica Neue", Helvetica, Arial, sans-serif;
+ font-size: 10.5pt;
+ font-weight: 700;
+ margin: 5mm 0 2mm;
+ color: #333;
+ }
+
+ /* ========================================================================
+ BODY TEXT
+ ======================================================================== */
+
+ p {
+ text-align: left;
+ margin: 0 0 3.5mm;
+ line-height: 1.7;
+ text-indent: 0;
+ hyphens: auto;
+ -webkit-hyphens: auto;
+ orphans: 3;
+ widows: 3;
+ }
+
+ h2 + p,
+ h3 + p,
+ h4 + p {
+ text-indent: 0;
+ }
+
+ strong { font-weight: 700; }
+ em { font-style: italic; }
+
+ a {
+ color: #E85D3A;
+ text-decoration: none;
+ border: none;
+ }
+
+ a[href^="http"]::after {
+ content: "";
+ }
+
+ /* ========================================================================
+ LISTS
+ ======================================================================== */
+
+ ul, ol {
+ margin: 0 0 4mm 5mm;
+ padding-left: 4mm;
+ }
+
+ li {
+ margin-bottom: 1.5mm;
+ line-height: 1.6;
+ }
+
+ li code,
+ .tag,
+ .badge {
+ background: #f3f3f3;
+ border: 0.5pt solid #ddd;
+ padding: 0.2mm 1.5mm;
+ border-radius: 1pt;
+ font-size: 8pt;
+ color: #444;
+ white-space: nowrap;
+ }
+
+ /* ========================================================================
+ CODE
+ ======================================================================== */
+
+ pre, code {
+ font-family: "SF Mono", "Monaco", "Inconsolata", "Fira Mono", "Courier New", monospace;
+ font-size: 8pt;
+ }
+
+ pre {
+ background: #f7f7f7;
+ padding: 4mm 5mm;
+ border: none;
+ border-left: 3pt solid #E85D3A;
+ overflow-x: hidden;
+ word-wrap: break-word;
+ white-space: pre-wrap;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ margin: 5mm 0;
+ line-height: 1.55;
+ }
+
+ code {
+ background: #f3f3f3;
+ padding: 0.3pt 2pt;
+ border-radius: 1pt;
+ }
+
+ pre code {
+ background: none;
+ padding: 0;
+ border-radius: 0;
+ }
+
+ /* ========================================================================
+ FIGURES & IMAGES
+ ======================================================================== */
+
+ figure {
+ margin: 8mm 0;
+ text-align: center;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ figure img {
+ max-width: 100%;
+ height: auto;
+ }
+
+ figcaption {
+ font-family: "Source Sans Pro", sans-serif;
+ font-size: 8.5pt;
+ color: #666;
+ margin-top: 2.5mm;
+ text-align: left;
+ line-height: 1.45;
+ }
+
+ figcaption strong {
+ color: #1a1a1a;
+ font-weight: 700;
+ }
+
+ /* ========================================================================
+ TABLES
+ ======================================================================== */
+
+ table {
+ width: 100%;
+ border-collapse: collapse;
+ margin: 5mm 0;
+ font-size: 9.5pt;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ background: none !important;
+ background-color: transparent !important;
+ background-image: none !important;
+ }
+
+ thead { display: table-header-group; }
+
+ th,
+ td {
+ background: none !important;
+ background-color: transparent !important;
+ background-image: none !important;
+ box-shadow: none !important;
+ }
+
+ th {
+ font-family: "Source Sans Pro", sans-serif;
+ font-weight: 700;
+ text-align: left;
+ padding: 2mm 2.5mm;
+ border: none;
+ border-bottom: 1.5pt solid #1a1a1a;
+ font-size: 8pt;
+ letter-spacing: 0.3pt;
+ text-transform: uppercase;
+ color: #1a1a1a;
+ }
+
+ td {
+ padding: 2mm 2.5mm;
+ border: none;
+ border-bottom: 0.5pt solid #e0e0e0;
+ vertical-align: top;
+ color: #1a1a1a;
+ }
+
+ tr,
+ tr:nth-child(even),
+ tr:nth-child(odd) {
+ background: none !important;
+ background-color: transparent !important;
+ }
+
+ tr:last-child td {
+ border-bottom: 1pt solid #1a1a1a;
+ }
+
+ td code,
+ th code {
+ background: #f5f5f5 !important;
+ box-shadow: none !important;
+ }
+
+ /* ========================================================================
+ BLOCKQUOTES
+ ======================================================================== */
+
+ blockquote {
+ margin: 6mm 0 6mm 0;
+ padding: 0 0 0 5mm;
+ border-left: 3pt solid #E85D3A;
+ font-style: italic;
+ color: #444;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ blockquote p {
+ text-indent: 0 !important;
+ }
+
+ /* ========================================================================
+ HTML EMBEDS (now replaced by screenshots)
+ ======================================================================== */
+
+ .html-embed {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ margin: 6mm 0;
+ width: 100%;
+ }
+
+ .html-embed--wide,
+ .html-embed--full {
+ width: 100% !important;
+ margin-left: 0 !important;
+ transform: none !important;
+ padding: 0 !important;
+ mask: none !important;
+ -webkit-mask: none !important;
+ background: none !important;
+ background-color: transparent !important;
+ background-image: none !important;
+ }
+
+ /* Strip backgrounds/gradients from wide and full-width wrappers */
+ .wide,
+ .full-width {
+ width: 100% !important;
+ margin-left: 0 !important;
+ transform: none !important;
+ background: none !important;
+ background-color: transparent !important;
+ background-image: none !important;
+ mask: none !important;
+ -webkit-mask: none !important;
+ }
+
+ .wide > *,
+ .full-width > * {
+ background: none !important;
+ background-color: transparent !important;
+ background-image: none !important;
+ }
+
+ .html-embed__card {
+ width: 100% !important;
+ max-width: 100% !important;
+ overflow: hidden !important;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ background: white !important;
+ border: none !important;
+ border-radius: 0;
+ padding: 0 !important;
+ }
+
+ .html-embed__card.is-frameless {
+ padding: 0 !important;
+ background: transparent !important;
+ }
+
+ .html-embed__card > div[id^="frag-"] {
+ width: 100% !important;
+ max-width: 100% !important;
+ overflow: hidden !important;
+ }
+
+ .html-embed__card svg {
+ max-width: 100% !important;
+ max-height: 120mm !important;
+ height: auto !important;
+ display: block !important;
+ margin: 0 auto !important;
+ }
+
+ .html-embed__card svg[viewBox] {
+ width: 100% !important;
+ height: auto !important;
+ }
+
+ .html-embed__card canvas {
+ max-width: 100% !important;
+ max-height: 120mm !important;
+ height: auto !important;
+ display: block !important;
+ margin: 0 auto !important;
+ }
+
+ .html-embed__card img {
+ max-width: 100% !important;
+ height: auto !important;
+ display: block !important;
+ }
+
+ .html-embed__card .chart-card {
+ width: 100% !important;
+ background: transparent !important;
+ border: none !important;
+ padding: 0 !important;
+ }
+
+ .html-embed__title {
+ font-family: "Source Sans Pro", sans-serif;
+ font-size: 8.5pt;
+ font-weight: 700;
+ color: #1a1a1a;
+ margin-bottom: 2mm;
+ text-transform: uppercase;
+ letter-spacing: 0.8pt;
+ }
+
+ .html-embed__desc {
+ font-family: "Source Sans Pro", sans-serif;
+ font-size: 8.5pt;
+ color: #666;
+ font-style: italic;
+ margin-top: 2.5mm;
+ line-height: 1.45;
+ }
+
+ .html-embed__desc strong,
+ figcaption strong {
+ font-style: normal;
+ color: #1a1a1a;
+ font-weight: 700;
+ }
+
+ /* Disable CSS counter-based figure numbering (JS handles it) */
+ .content-grid > main .reference-wrapper .reference__caption::before,
+ .content-grid > main .image-wrapper > figure > figcaption::before,
+ .content-grid > main figure.html-embed > .html-embed__desc::before {
+ content: none !important;
+ }
+
+ .html-embed__card .legend {
+ font-size: 7.5pt !important;
+ }
+
+ .html-embed__card .tooltip,
+ .html-embed__card .d3-tooltip,
+ .html-embed__card [class*="tooltip"] {
+ display: none !important;
+ }
+
+ .html-embed__card input[type="range"],
+ .html-embed__card select,
+ .html-embed__card .controls,
+ .html-embed__card .plotly_controls {
+ opacity: 0.4;
+ pointer-events: none;
+ }
+
+ /* ========================================================================
+ D3/PLOTLY
+ ======================================================================== */
+
+ svg, canvas {
+ max-width: 100%;
+ height: auto;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ .js-plotly-plot,
+ .d3-line,
+ .d3-bar {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ margin: 5mm 0;
+ background: white !important;
+ }
+
+ /* ========================================================================
+ ACCORDIONS (opened for print)
+ ======================================================================== */
+
+ .accordion,
+ details.accordion,
+ details {
+ margin: 5mm 0;
+ border: none;
+ border-left: 3pt solid #ddd;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ .accordion__summary,
+ details.accordion > summary,
+ details > summary {
+ font-family: "Source Sans Pro", sans-serif;
+ font-size: 9.5pt;
+ font-weight: 700;
+ padding: 2mm 4mm !important;
+ background: transparent !important;
+ border: none !important;
+ color: #1a1a1a !important;
+ list-style: none;
+ }
+
+ details > summary::-webkit-details-marker {
+ display: none;
+ }
+
+ .accordion__content-wrapper {
+ height: auto !important;
+ overflow: visible !important;
+ }
+
+ .accordion__content {
+ padding: 2mm 4mm !important;
+ }
+
+ /* ========================================================================
+ NOTES
+ ======================================================================== */
+
+ .note {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ margin: 5mm 0;
+ padding: 4mm 5mm;
+ border: none;
+ border-left: 3pt solid #E85D3A;
+ background: #fdf8f6;
+ }
+
+ .note--warning {
+ border-left-color: #d4a017;
+ background: #fdfaf0;
+ }
+
+ .note--danger {
+ border-left-color: #c62828;
+ background: #fdf2f2;
+ }
+
+ .note--info {
+ border-left-color: #1565c0;
+ background: #f0f5fd;
+ }
+
+ .note--success {
+ border-left-color: #2e7d32;
+ background: #f2faf3;
+ }
+
+ /* ========================================================================
+ STACKS (grid cards)
+ ======================================================================== */
+
+ .stack {
+ display: grid !important;
+ gap: 3mm !important;
+ margin: 5mm 0 !important;
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ .stack[data-layout="2-column"] { grid-template-columns: repeat(2, 1fr) !important; }
+ .stack[data-layout="3-column"] { grid-template-columns: repeat(3, 1fr) !important; }
+ .stack[data-layout="4-column"] { grid-template-columns: repeat(2, 1fr) !important; }
+
+ .stack > * {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ /* ========================================================================
+ IMAGE WRAPPER
+ ======================================================================== */
+
+ .image-wrapper {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ margin: 5mm 0;
+ }
+
+ .image-wrapper img {
+ max-width: 100% !important;
+ height: auto !important;
+ display: block;
+ }
+
+ .image-credit {
+ font-size: 7pt;
+ color: #999;
+ margin-top: 1mm;
+ }
+
+ /* ========================================================================
+ FOOTNOTE MARKS & SUPERSCRIPT
+ ======================================================================== */
+
+ sup a {
+ font-size: 7.5pt;
+ color: #E85D3A;
+ text-decoration: none;
+ border: none;
+ }
+
+ /* ========================================================================
+ HORIZONTAL RULES
+ ======================================================================== */
+
+ hr {
+ border: none;
+ border-top: 0.5pt solid #ddd;
+ margin: 8mm 0;
+ }
+
+ /* ========================================================================
+ CITATIONS & REFERENCES
+ ======================================================================== */
+
+ .citation {
+ font-size: 0.9em;
+ color: #666;
+ }
+
+ .references {
+ break-before: page;
+ }
+
+ .references h2 {
+ font-size: 11pt;
+ text-transform: uppercase;
+ letter-spacing: 3pt;
+ font-weight: 700;
+ break-before: auto;
+ }
+
+ .references h2::before,
+ .references h2::after {
+ content: none !important;
+ }
+
+ .reference-item {
+ margin-bottom: 2.5mm;
+ text-indent: -5mm;
+ margin-left: 5mm;
+ font-size: 9pt;
+ line-height: 1.5;
+ }
+
+ /* ========================================================================
+ ABSTRACT
+ ======================================================================== */
+
+ .hero + section,
+ .hero + div,
+ .abstract {
+ padding: 5mm 0;
+ background: transparent;
+ border: none;
+ border-top: 0.75pt solid #1a1a1a;
+ border-bottom: 0.75pt solid #ddd;
+ margin-bottom: 8mm;
+ break-inside: avoid;
+ }
+
+ .hero + section h2:first-child,
+ .hero + div h2:first-child,
+ .abstract h2 {
+ font-size: 11pt;
+ text-transform: uppercase;
+ letter-spacing: 3pt;
+ font-weight: 700;
+ color: #1a1a1a;
+ margin-bottom: 3mm;
+ padding: 0;
+ border: none;
+ break-before: auto;
+ }
+
+ .hero + section h2:first-child::before,
+ .hero + div h2:first-child::before,
+ .abstract h2::before,
+ .hero + section h2:first-child::after,
+ .hero + div h2:first-child::after,
+ .abstract h2::after {
+ content: none !important;
+ }
+}
diff --git a/content/articles/multi-harness-rl/app/src/styles/_print.css b/content/articles/multi-harness-rl/app/src/styles/_print.css
new file mode 100644
index 00000000..162b7918
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/_print.css
@@ -0,0 +1,128 @@
+/* ============================================================================ */
+/* Print styles */
+/* ========================================================================= */
+@media print {
+
+ html,
+ body {
+ background: #fff;
+ }
+
+ /* Margins handled by Playwright; avoid extra global margins */
+ body {
+ margin: 0;
+ }
+
+ /* Keep the banner (hero), hide non-essential UI elements */
+ #theme-toggle {
+ display: none !important;
+ }
+
+ /* Links: remove underline and background */
+ .content-grid main a {
+ text-decoration: none;
+ background: none;
+ border-bottom: 1px solid rgba(0, 0, 0, .2);
+ }
+
+ /* Avoid breaks inside complex blocks */
+ .content-grid main pre,
+ .content-grid main blockquote,
+ .content-grid main table,
+ .content-grid main figure {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ /* Soft page breaks around main headings */
+ .content-grid main h2 {
+ page-break-before: auto;
+ page-break-after: avoid;
+ break-after: avoid-page;
+ }
+
+ /* Small icon labels not needed when printing */
+ .code-lang-chip {
+ display: none !important;
+ }
+
+ /* Adjust more contrasty colors for print */
+ :root {
+ --surface-bg: #fff;
+ --border-color: rgba(0, 0, 0, .2);
+ --link-underline: rgba(0, 0, 0, .3);
+ --link-underline-hover: rgba(0, 0, 0, .4);
+ }
+
+ /* Force single column to reduce widows/orphans and awkward breaks */
+ .content-grid {
+ grid-template-columns: 1fr !important;
+ }
+
+ .table-of-contents,
+ .right-aside,
+ .toc-mobile-toggle,
+ .toc-mobile-backdrop,
+ .toc-mobile-sidebar {
+ display: none !important;
+ }
+
+ main>nav:first-of-type {
+ display: none !important;
+ }
+
+ /* Avoid page breaks inside complex visual blocks */
+ .hero,
+ .hero-banner,
+ .html-embed__card,
+ .js-plotly-plot,
+ figure,
+ pre,
+ table,
+ blockquote,
+ .wide,
+ .full-width,
+ .stack > *,
+ .card,
+ .palettes,
+ .palette-card,
+ .color-picker,
+ .note {
+ break-inside: avoid;
+ page-break-inside: avoid;
+ }
+
+ /* Prefer keeping header+lead together */
+ .hero {
+ page-break-after: avoid;
+ }
+
+ /* Center the hero banner and constrain all its children generically */
+ .hero-banner {
+ width: 100% !important;
+ max-width: 980px !important;
+ margin-left: auto !important;
+ margin-right: auto !important;
+ overflow: hidden;
+ }
+
+ .hero-banner > *,
+ .hero-banner svg,
+ .hero-banner canvas,
+ .hero-banner img,
+ .hero-banner video {
+ max-width: 100% !important;
+ width: 100% !important;
+ height: auto !important;
+ margin-left: auto !important;
+ margin-right: auto !important;
+ display: block;
+ }
+}
+
+
+@media print {
+ .meta-container-cell--pdf {
+ display: none !important;
+ }
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/_reset.css b/content/articles/multi-harness-rl/app/src/styles/_reset.css
new file mode 100644
index 00000000..fdcbd8f8
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/_reset.css
@@ -0,0 +1,13 @@
+html { box-sizing: border-box; }
+*, *::before, *::after { box-sizing: inherit; }
+body { margin: 0; font-family: var(--default-font-family); color: var(--text-color); }
+audio { display: block; width: 100%; }
+
+img,
+picture {
+ max-width: 100%;
+ height: auto;
+ display: block;
+ position: relative;
+ z-index: var(--z-elevated);
+}
diff --git a/content/articles/multi-harness-rl/app/src/styles/_variables.css b/content/articles/multi-harness-rl/app/src/styles/_variables.css
new file mode 100644
index 00000000..57ab3d4a
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/_variables.css
@@ -0,0 +1,126 @@
+/* ============================================================================ */
+/* Design Tokens */
+/* ============================================================================ */
+:root {
+ /* Neutrals */
+ --neutral-900: rgb(17, 24, 39);
+ --neutral-600: rgb(107, 114, 128);
+ --neutral-400: rgb(185, 185, 185);
+ --neutral-300: rgb(228, 228, 228);
+ --neutral-200: rgb(245, 245, 245);
+ --neutral-50: rgb(249, 250, 251);
+
+ --default-font-family: Source Sans Pro, ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ --font-mono: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+
+ /* Brand (OKLCH base + derived states) */
+ --primary-base: oklch(0.75 0.12 47);
+ --primary-color: var(--primary-base);
+ --primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h);
+ --primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
+ --on-primary: #ffffff;
+
+ /* Semantic colors */
+ --danger-color: oklch(0.65 0.19 25);
+ --success-color: oklch(0.65 0.15 145);
+ --info-color: oklch(0.65 0.12 230);
+
+ /* Text & Surfaces */
+ --page-bg: #fff;
+ --surface-bg: #f9f9f9;
+ --text-color: rgba(0, 0, 0, .85);
+ --transparent-page-contrast: rgba(255, 255, 255, .85);
+ --muted-color: rgba(0, 0, 0, .6);
+ --border-color: rgba(0, 0, 0, .1);
+ --code-bg: #f6f8fa;
+
+ /* Links */
+ --link-underline: var(--primary-color);
+ --link-underline-hover: var(--primary-color-hover);
+
+ /* Spacing scale */
+ --spacing-1: 8px;
+ --spacing-2: 12px;
+ --spacing-3: 16px;
+ --spacing-4: 24px;
+ --spacing-5: 32px;
+ --spacing-6: 40px;
+ --spacing-7: 48px;
+ --spacing-8: 56px;
+ --spacing-9: 64px;
+ --spacing-10: 72px;
+
+ /* Custom Media aliases compiled by PostCSS */
+ @custom-media --bp-xxs (max-width: 320px);
+ @custom-media --bp-xs (max-width: 480px);
+ @custom-media --bp-sm (max-width: 640px);
+ @custom-media --bp-md (max-width: 768px);
+ @custom-media --bp-lg (max-width: 1024px);
+ @custom-media --bp-xl (max-width: 1280px);
+ @custom-media --bp-content-collapse (max-width: 1100px);
+
+ /* Layout */
+ --content-padding-x: 16px;
+ /* default page gutter */
+ --block-spacing-y: var(--spacing-4);
+ /* default vertical spacing between block components */
+
+ /* Config */
+ --palette-count: 8;
+
+ /* Button tokens */
+ --button-radius: 6px;
+ --button-padding-x: 16px;
+ --button-padding-y: 10px;
+ --button-font-size: 14px;
+ --button-icon-padding: 8px;
+ /* Big button */
+ --button-big-padding-x: 16px;
+ --button-big-padding-y: 12px;
+ --button-big-font-size: 16px;
+ --button-big-icon-padding: 12px;
+
+ /* Table tokens */
+ --table-border-radius: 8px;
+ --table-header-bg: oklch(from var(--surface-bg) calc(l - 0.02) c h);
+ --table-row-odd-bg: oklch(from var(--surface-bg) calc(l - 0.01) c h);
+
+ /* Z-index */
+ --z-base: 0;
+ --z-content: 1;
+ --z-elevated: 10;
+ --z-overlay: 1000;
+ --z-modal: 1100;
+ --z-tooltip: 1200;
+
+ /* Charts (global) */
+ --axis-color: var(--muted-color);
+ --tick-color: var(--text-color);
+ --grid-color: rgba(0, 0, 0, .08);
+}
+
+/* ============================================================================ */
+/* Dark Theme Overrides */
+/* ============================================================================ */
+[data-theme="dark"] {
+ --page-bg: #0f1115;
+ --surface-bg: #07080a;
+ --text-color: rgba(255, 255, 255, .9);
+ --muted-color: rgba(255, 255, 255, .7);
+ --border-color: rgba(255, 255, 255, .15);
+ --code-bg: #12151b;
+ --transparent-page-contrast: rgba(0, 0, 0, .85);
+
+ /* Charts (global) */
+ --axis-color: var(--muted-color);
+ --tick-color: var(--muted-color);
+ --grid-color: rgba(255, 255, 255, .10);
+
+ /* Primary (lower L in dark) */
+ --primary-color-hover: oklch(from var(--primary-color) calc(l - 0.05) c h);
+ --primary-color-active: oklch(from var(--primary-color) calc(l - 0.10) c h);
+ --on-primary: #0f1115;
+
+ color-scheme: dark;
+ background: var(--page-bg);
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/components/_button.css b/content/articles/multi-harness-rl/app/src/styles/components/_button.css
new file mode 100644
index 00000000..879af358
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/components/_button.css
@@ -0,0 +1,58 @@
+button, .button {
+ appearance: none;
+ background: linear-gradient(15deg, var(--primary-color) 0%, var(--primary-color-hover) 35%);
+ color: white;
+ border: 1px solid transparent;
+ border-radius: var(--button-radius);
+ padding: var(--button-padding-y) var(--button-padding-x);
+ font-size: var(--button-font-size);
+ line-height: 1;
+ cursor: pointer;
+ display: inline-block;
+ text-decoration: none;
+ transition: background-color .15s ease, border-color .15s ease, box-shadow .15s ease, transform .02s ease;
+ }
+ /* Icon-only buttons: equal X/Y padding */
+ button:has(> svg:only-child),
+ .button:has(> svg:only-child) {
+ padding: var(--button-icon-padding);
+ }
+ button:hover, .button:hover {
+ filter: brightness(96%);
+ }
+ button:active, .button:active {
+ transform: translateY(1px);
+ }
+ button:focus-visible, .button:focus-visible {
+ outline: none;
+ }
+ button:disabled, .button:disabled {
+ opacity: .6;
+ cursor: not-allowed;
+ }
+
+ /* Ghost/Muted button: subtle outline, primary color text/border */
+ .button--ghost {
+ background: transparent !important;
+ color: var(--primary-color) !important;
+ border-color: var(--primary-color) !important;
+ }
+ .button--ghost:hover {
+ color: var(--primary-color-hover) !important;
+ border-color: var(--primary-color-hover) !important;
+ filter: none;
+ }
+
+ /* Big button: larger padding and font size */
+ .button.button--big {
+ padding: var(--button-big-padding-y) var(--button-big-padding-x);
+ font-size: var(--button-big-font-size);
+ }
+ .button.button--big:has(> svg:only-child) {
+ padding: var(--button-big-icon-padding);
+ }
+
+ .button-group .button {
+ margin: 5px;
+ }
+
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/components/_card.css b/content/articles/multi-harness-rl/app/src/styles/components/_card.css
new file mode 100644
index 00000000..41ff9164
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/components/_card.css
@@ -0,0 +1,138 @@
+.card {
+ background: var(--surface-bg);
+ border: 1px solid var(--border-color) !important;
+ border-radius: 12px;
+ padding: var(--spacing-3);
+ z-index: calc(var(--z-elevated) + 1);
+ position: relative;
+ margin-bottom: 0;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+ transition: all 0.2s ease;
+ overflow: hidden;
+ text-decoration: none;
+ color: inherit;
+ display: block;
+}
+
+.card:hover {
+ text-decoration: none;
+ color: inherit;
+}
+
+.card.no-padding,
+.card.card--p0 {
+ padding: 0;
+}
+
+.card:hover {
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+}
+
+
+.card figcaption {
+ color: var(--text-color) !important;
+ font-weight: 600 !important;
+ font-size: 1rem !important;
+ text-align: center !important;
+ padding: var(--spacing-3) !important;
+ margin: 0 !important;
+}
+
+.card .image-wrapper,
+.card .image-wrapper figure {
+ margin: 0 !important;
+ width: 100% !important;
+ display: block !important;
+}
+
+.card img {
+ border-radius: 8px 8px 0 0 !important;
+ width: 100% !important;
+ max-width: 100% !important;
+ height: 120px !important;
+ object-fit: cover !important;
+ display: block !important;
+ border-bottom: 1px solid var(--border-color) !important;
+ transition: all 0.2s ease;
+}
+
+.card.template-card img {
+ height: auto !important;
+ object-fit: contain !important;
+}
+
+
+.card h3,
+.card h4,
+.card h5 {
+ margin-top: 0 !important;
+ font-weight: 900 !important;
+ width: 100%;
+}
+
+.card::after {
+ display: none !important;
+}
+
+.card-title-container {
+ padding: var(--spacing-3) var(--spacing-4) var(--spacing-4) var(--spacing-4);
+}
+
+.card-title {
+ color: var(--text-color);
+ font-size: 0.9rem;
+ margin: 0 0 var(--spacing-1) 0 !important;
+ font-weight: 600;
+ line-height: 1.4;
+ white-space: normal;
+}
+
+.card-subtitle {
+ color: var(--muted-color);
+ font-size: 0.75rem;
+ margin: 0 !important;
+ line-height: 1.5;
+ white-space: normal;
+}
+
+.card-cta {
+ flex: 1;
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: var(--surface-bg);
+ border: 2px dashed var(--border-color) !important;
+}
+
+.card-cta:hover {
+ transform: none;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
+ border: 2px dashed var(--border-color) !important;
+}
+
+.card-cta-content {
+ text-align: center;
+ padding: var(--spacing-4);
+}
+
+.card-cta h3,
+.card-cta h4,
+.card-cta h5 {
+ color: var(--text-color);
+ font-weight: 900 !important;
+ margin-bottom: var(--spacing-1) !important;
+}
+
+.card-cta p {
+ color: var(--muted-color);
+ font-size: 0.9rem;
+ margin: 0 !important;
+}
+
+@media print {
+ .card,
+ .card-cta {
+ box-shadow: none;
+ }
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/components/_code.css b/content/articles/multi-harness-rl/app/src/styles/components/_code.css
new file mode 100644
index 00000000..b45c47b8
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/components/_code.css
@@ -0,0 +1,357 @@
+/* ============================================================================ */
+/* Inline code */
+/* ============================================================================ */
+code {
+ font-size: 14px;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
+ border-radius: 0.3em;
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+ font-weight: 400;
+ line-height: 1.5;
+}
+
+p code,
+.note code {
+ white-space: nowrap;
+ padding: calc(var(--spacing-1)/3) calc(var(--spacing-1)/2);
+}
+
+
+/* ============================================================================ */
+/* Shiki code blocks */
+/* ============================================================================ */
+.astro-code {
+ position: relative;
+ border: 1px solid var(--border-color);
+ background-color: var(--surface-bg) !important;
+ border-radius: 6px;
+ padding: 0;
+ font-size: 14px;
+ --code-gutter-width: 2.5em;
+}
+
+/* Shared sizing & horizontal scroll for code containers */
+.astro-code,
+section.content-grid pre {
+ width: 100%;
+ max-width: 100%;
+ box-sizing: border-box;
+ -webkit-overflow-scrolling: touch;
+ padding: 0;
+ margin-bottom: var(--block-spacing-y) !important;
+ overflow-x: auto;
+}
+
+section.content-grid pre.astro-code {
+ margin: 0;
+ padding: var(--spacing-1) 0;
+}
+
+section.content-grid pre code {
+ display: inline-block;
+ min-width: 100%;
+}
+
+/* Plain-text blocks: always wrap lines (prompts, prose, etc.) */
+.astro-code[data-language="text"] {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.astro-code[data-language="text"] .line {
+ display: block;
+}
+
+/* Wrap long lines only on small screens to prevent layout overflow */
+@media (--bp-content-collapse) {
+
+ .astro-code,
+ section.content-grid pre {
+ white-space: pre-wrap;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+ }
+
+ section.content-grid pre code {
+ white-space: pre-wrap;
+ display: block;
+ min-width: 0;
+ }
+}
+
+/* Themes */
+[data-theme='light'] .astro-code {
+ background-color: var(--code-bg);
+}
+
+/* Apply token color from per-span vars exposed by Shiki dual themes */
+[data-theme='light'] .astro-code span {
+ color: var(--shiki-light) !important;
+}
+
+[data-theme='dark'] .astro-code span {
+ color: var(--shiki-dark) !important;
+}
+
+/* Optional: boost contrast for light theme */
+[data-theme='light'] .astro-code {
+ --shiki-foreground: #24292f;
+ --shiki-background: #ffffff;
+}
+
+/* Line numbers for Shiki-rendered code blocks */
+.astro-code code {
+ counter-reset: astro-code-line;
+ display: block;
+ background: none;
+ border: none;
+}
+
+.astro-code .line {
+ display: inline-block;
+ position: relative;
+ padding-left: calc(var(--code-gutter-width) + var(--spacing-1));
+ min-height: 1.25em;
+}
+
+.astro-code .line::before {
+ counter-increment: astro-code-line;
+ content: counter(astro-code-line);
+ position: absolute;
+ left: 0;
+ top: 0;
+ bottom: 0;
+ width: calc(var(--code-gutter-width));
+ text-align: right;
+ color: var(--muted-color);
+ opacity: .3;
+ user-select: none;
+ padding-right: var(--spacing-2);
+ border-right: 1px solid var(--border-color);
+}
+
+.astro-code .line:empty::after {
+ content: "\00a0";
+}
+
+/* Hide trailing empty line added by parsers */
+.astro-code code>.line:last-child:empty {
+ display: none;
+}
+
+/* ============================================================================ */
+/* Non-Shiki pre wrapper (rehype) */
+/* ============================================================================ */
+.code-card {
+ position: relative;
+}
+
+.code-card .code-copy {
+ position: absolute;
+ top: var(--spacing-2);
+ right: var(--spacing-2);
+ z-index: 3;
+ display: none;
+}
+
+.code-card:hover .code-copy {
+ display: block;
+}
+
+.code-card .code-copy svg {
+ width: 16px;
+ height: 16px;
+ display: block;
+ fill: currentColor;
+}
+
+.code-card pre {
+ margin: 0 0 var(--spacing-1);
+}
+
+/* When no copy button (single-line), keep the label in the top-right corner */
+.code-card.no-copy::after {
+ top: 8px;
+ right: 8px;
+}
+
+/* ============================================================================ */
+/* Contextual overrides */
+/* ============================================================================ */
+/* Inside Accordions: remove padding and border on code containers */
+.accordion .astro-code {
+ padding: 0;
+ border: none;
+}
+
+.accordion .astro-code {
+ margin-bottom: 0 !important;
+}
+
+.accordion .code-output {
+ border: none;
+ border-top: 1px solid var(--border-color) !important;
+}
+
+.accordion pre {
+ margin-bottom: 0 !important;
+}
+
+.accordion .code-card pre {
+ margin: 0 !important;
+}
+
+/* ============================================================================ */
+/* Language/extension vignette (bottom-right, discreet) */
+/* ============================================================================ */
+/* .astro-code::after {
+ content: attr(data-language);
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ font-size: 10px;
+ line-height: 1;
+ text-transform: uppercase;
+ color: var(--muted-color);
+ background: var(--surface-bg);
+ border-top: 1px solid var(--border-color);
+ border-left: 1px solid var(--border-color);
+ opacity: 1;
+ border-radius: 8px 0 0 0;
+ padding: 4px 6px;
+ pointer-events: none;
+} */
+
+/* Fallback if Shiki uses data-lang instead of data-language */
+/* .astro-code[data-lang]::after { content: attr(data-lang); }
+.astro-code[data-language="typescript"]::after { content: "ts"; }
+.astro-code[data-language="tsx"]::after { content: "tsx"; }
+.astro-code[data-language="javascript"]::after,
+.astro-code[data-language="node"]::after,
+.astro-code[data-language="jsx"]::after { content: "js"; }
+.astro-code[data-language="python"]::after { content: "py"; }
+.astro-code[data-language="bash"]::after,
+.astro-code[data-language="shell"]::after,
+.astro-code[data-language="sh"]::after { content: "sh"; }
+.astro-code[data-language="markdown"]::after { content: "md"; }
+.astro-code[data-language="yaml"]::after,
+.astro-code[data-language="yml"]::after { content: "yml"; }
+.astro-code[data-language="html"]::after { content: "html"; }
+.astro-code[data-language="css"]::after { content: "css"; }
+.astro-code[data-language="json"]::after { content: "json"; } */
+
+/* In Accordions, keep same bottom-right placement */
+.accordion .astro-code::after {
+ right: 0;
+ bottom: 0;
+}
+
+/* ============================================================================ */
+/* Results blocks glued to code blocks */
+/* ============================================================================ */
+.code-output {
+ position: relative;
+ background: oklch(from var(--code-bg) calc(l - 0.005) c h);
+ border: 1px solid var(--border-color);
+ border-radius: 6px;
+ margin-top: 0;
+ margin-bottom: var(--block-spacing-y);
+ padding: 0 !important;
+}
+
+.code-output pre {
+ padding: calc(var(--spacing-3) + 6px) var(--spacing-3) var(--spacing-3) var(--spacing-3) !important;
+}
+
+/* If immediately following a code container, keep tight visual connection */
+.code-card+.code-output,
+.astro-code+.code-output,
+section.content-grid pre+.code-output {
+ margin-top: 0;
+ border-top: none;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ box-shadow: inset 0 8px 12px -12px rgba(0, 0, 0, 0.15);
+}
+
+/* Remove bottom margin on code when immediately followed by results */
+.astro-code:has(+ .code-output) {
+ margin-bottom: 0 !important;
+}
+
+.code-card:has(+ .code-output) .astro-code {
+ margin-bottom: 0 !important;
+}
+
+section.content-grid pre:has(+ .code-output) {
+ margin-bottom: 0 !important;
+}
+
+/* Remove bottom border radius on code when followed by results */
+.astro-code:has(+ .code-output) {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+.code-card:has(+ .code-output) .astro-code {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+section.content-grid pre:has(+ .code-output) {
+ border-bottom-left-radius: 0;
+ border-bottom-right-radius: 0;
+}
+
+/* Small top-left label */
+.code-output::before {
+ content: "Output";
+ position: absolute;
+ top: 0;
+ right: 0;
+ font-size: 10px;
+ line-height: 1;
+ color: var(--muted-color);
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ border-top: none;
+ border-right: none;
+ border-radius: 0 0 0 6px;
+ padding: 10px 10px;
+}
+
+/* Very subtle top shadow so results feels under the code */
+.code-output {}
+
+.code-output> :where(*):first-child {
+ margin-top: 0 !important;
+}
+
+.code-output> :where(*):last-child {
+ margin-bottom: 0 !important;
+}
+
+/* ============================================================================ */
+/* Optional filename tag above code blocks */
+/* ============================================================================ */
+.code-filename {
+ display: inline-block;
+ font-size: 12px;
+ line-height: 1;
+ color: var(--muted-color);
+ background: var(--surface-bg);
+ border: 1px solid var(--border-color);
+ border-bottom: none;
+ border-radius: 6px 6px 0 0;
+ padding: 4px 8px;
+ margin: 0;
+}
+
+.code-filename+.code-card .astro-code,
+.code-filename+.astro-code,
+.code-filename+section.content-grid pre {
+ border-top-left-radius: 0;
+ border-top-right-radius: 6px;
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/components/_form.css b/content/articles/multi-harness-rl/app/src/styles/components/_form.css
new file mode 100644
index 00000000..77d11b85
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/components/_form.css
@@ -0,0 +1,243 @@
+/* ============================================================================ */
+/* Form Elements - Modern Minimal Design */
+/* ============================================================================ */
+
+/* Select styling with modern chevron */
+select {
+ background-color: var(--page-bg);
+ border: 1px solid color-mix(in srgb, var(--primary-color) 50%, var(--border-color));
+ border-radius: var(--button-radius);
+ padding: var(--button-padding-y) calc(var(--button-padding-x)*2) var(--button-padding-y) var(--button-padding-x);
+ font-family: var(--default-font-family);
+ font-size: var(--button-font-size);
+ color: var(--text-color);
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E");
+ background-repeat: no-repeat;
+ background-position: right calc(var(--button-padding-x) + 2px) center;
+ background-size: 12px;
+ cursor: pointer;
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ -webkit-appearance: none;
+ -moz-appearance: none;
+ appearance: none;
+}
+
+select:hover,
+select:focus,
+select:active {
+ border-color: var(--primary-color);
+}
+
+select:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
+}
+
+select:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background-color: var(--surface-bg);
+}
+
+/* Dark theme select chevron */
+[data-theme="dark"] select {
+ background-image: url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23bbb' d='M6 8.825L1.175 4 2.35 2.825 6 6.475 9.65 2.825 10.825 4z'/%3E%3C/svg%3E");
+}
+
+/* Checkbox styling */
+input[type="checkbox"] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border-color);
+ border-radius: 3px;
+ background-color: var(--page-bg);
+ cursor: pointer;
+ position: relative;
+ transition: all 0.2s ease;
+ margin-right: var(--spacing-2);
+}
+
+input[type="checkbox"]:hover {
+ border-color: var(--primary-color);
+}
+
+input[type="checkbox"]:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
+}
+
+input[type="checkbox"]:checked {
+ background-color: var(--primary-color);
+ border-color: var(--primary-color);
+}
+
+input[type="checkbox"]:checked::before {
+ content: '';
+ position: absolute;
+ top: 1px;
+ left: 4px;
+ width: 4px;
+ height: 8px;
+ border: solid var(--on-primary);
+ border-width: 0 2px 2px 0;
+ transform: rotate(45deg);
+}
+
+input[type="checkbox"]:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Radio button styling */
+input[type="radio"] {
+ appearance: none;
+ width: 16px;
+ height: 16px;
+ border: 2px solid var(--border-color);
+ border-radius: 50%;
+ background-color: var(--page-bg);
+ cursor: pointer;
+ position: relative;
+ transition: all 0.2s ease;
+ margin-right: var(--spacing-2);
+}
+
+input[type="radio"]:hover {
+ border-color: var(--primary-color);
+}
+
+input[type="radio"]:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
+}
+
+input[type="radio"]:checked {
+ border-color: var(--primary-color);
+}
+
+input[type="radio"]:checked::before {
+ content: '';
+ position: absolute;
+ top: 2px;
+ left: 2px;
+ width: 8px;
+ height: 8px;
+ border-radius: 50%;
+ background-color: var(--primary-color);
+}
+
+input[type="radio"]:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+/* Input text styling for consistency */
+input[type="text"],
+input[type="email"],
+input[type="password"],
+input[type="number"],
+input[type="url"],
+input[type="search"],
+textarea {
+ appearance: none;
+ background-color: var(--page-bg);
+ border: 1px solid var(--border-color);
+ border-radius: var(--button-radius);
+ padding: var(--button-padding-y) var(--button-padding-x);
+ font-family: var(--default-font-family);
+ font-size: var(--button-font-size);
+ color: var(--text-color);
+ transition: border-color 0.2s ease, box-shadow 0.2s ease;
+ width: 100%;
+}
+
+input[type="text"]:hover,
+input[type="email"]:hover,
+input[type="password"]:hover,
+input[type="number"]:hover,
+input[type="url"]:hover,
+input[type="search"]:hover,
+textarea:hover {
+ border-color: var(--primary-color);
+}
+
+input[type="text"]:focus,
+input[type="email"]:focus,
+input[type="password"]:focus,
+input[type="number"]:focus,
+input[type="url"]:focus,
+input[type="search"]:focus,
+textarea:focus {
+ outline: none;
+ border-color: var(--primary-color);
+ box-shadow: 0 0 0 2px rgba(from var(--primary-color) r g b / 0.1);
+}
+
+input[type="text"]:disabled,
+input[type="email"]:disabled,
+input[type="password"]:disabled,
+input[type="number"]:disabled,
+input[type="url"]:disabled,
+input[type="search"]:disabled,
+textarea:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+ background-color: var(--surface-bg);
+}
+
+/* Label styling */
+label {
+ display: flex;
+ align-items: center;
+ font-size: var(--button-font-size);
+ color: var(--text-color);
+ cursor: pointer;
+ margin-bottom: 0;
+ line-height: 1.4;
+ user-select: none;
+}
+
+/* Form group spacing */
+.form-group {
+ margin-bottom: var(--spacing-4);
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-2);
+}
+
+.form-group label {
+ margin-bottom: 0;
+}
+
+/* Alternative: for vertical form groups */
+.form-group.vertical {
+ flex-direction: column;
+ align-items: flex-start;
+}
+
+.form-group.vertical label {
+ margin-bottom: var(--spacing-1);
+}
+
+/* For inline form elements */
+.form-inline {
+ display: flex;
+ align-items: center;
+ gap: var(--spacing-2);
+ margin-bottom: var(--spacing-3);
+}
+
+.form-inline label {
+ margin-bottom: 0;
+}
+
+/* Ensure labels in flex containers don't break alignment */
+div[style*="display: flex"] label,
+div[class*="flex"] label {
+ margin-bottom: 0 !important;
+ align-self: center;
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/components/_mermaid.css b/content/articles/multi-harness-rl/app/src/styles/components/_mermaid.css
new file mode 100644
index 00000000..64f1ffd6
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/components/_mermaid.css
@@ -0,0 +1,250 @@
+.mermaid .nodeLabel p {
+ color: var(--text-color) !important;
+}
+
+.mermaid .cluster-label .nodeLabel {
+ color: var(--text-color) !important;
+}
+
+/* Styles pour le texte des subgraphs/clusters - gérer les foreignObject */
+.mermaid .cluster-label text,
+.mermaid .cluster-label .nodeLabel text,
+.mermaid .cluster-label foreignObject,
+.mermaid .cluster-label foreignObject div,
+.mermaid .cluster-label foreignObject span {
+ color: var(--text-color) !important;
+ fill: var(--text-color) !important;
+}
+
+/* Styles spécifiques pour les clusters/subgraphs */
+.mermaid .cluster rect {
+ fill: transparent !important;
+ stroke: var(--border-color) !important;
+ stroke-width: 1px !important;
+}
+
+.mermaid .cluster-label {
+ color: var(--text-color) !important;
+ fill: var(--text-color) !important;
+}
+
+/* Masquer le flicker pendant la conversion */
+.mermaid-zoom-wrapper.converting {
+ opacity: 0.7;
+ transition: opacity 0.2s ease;
+}
+
+.mermaid-zoom-wrapper.converting .mermaid {
+ opacity: 0.7;
+ transition: opacity 0.2s ease;
+}
+
+/* Assurer que les diagrammes Mermaid prennent toute la largeur */
+.mermaid {
+ width: 100% !important;
+ max-width: 100% !important;
+ display: block !important;
+}
+
+/* Styles pour les SVG Mermaid */
+.mermaid svg {
+ width: 100% !important;
+ max-width: 100% !important;
+ height: auto !important;
+}
+
+/* Styles pour les wrappers de zoom Mermaid */
+.mermaid-zoom-wrapper {
+ display: block !important;
+ width: 100% !important;
+ max-width: 100% !important;
+ cursor: zoom-in;
+ position: relative;
+}
+
+.mermaid-zoom-wrapper .mermaid {
+ width: 100% !important;
+ max-width: 100% !important;
+ display: block !important;
+}
+
+.mermaid-zoom-wrapper .mermaid svg {
+ width: 100% !important;
+ max-width: 100% !important;
+ height: auto !important;
+ display: block !important;
+}
+
+/* Styles pour les nœuds avec bords arrondis et bordure interne */
+.mermaid rect:not(.flowchart-link),
+.mermaid .node rect,
+.mermaid .nodeLabel rect,
+.mermaid .cluster rect {
+ rx: 8px !important;
+ ry: 8px !important;
+ stroke: color-mix(in srgb, var(--text-color) 20%, transparent) !important;
+ stroke-width: 1px !important;
+ /* Décalage pour créer l'effet de bordure interne */
+}
+
+/* Styles pour les diagrammes Mermaid zoomables */
+.mermaid-zoom-wrapper:hover {
+ opacity: 0.95;
+ transition: opacity 0.2s ease;
+}
+
+/* Appliquer le même style que les images zoomables */
+.mermaid-zoom-wrapper[data-zoomable="1"] {
+ cursor: zoom-in;
+}
+
+/* Styles pour le mode zoom ouvert */
+.medium-zoom--opened .mermaid-zoom-wrapper {
+ cursor: zoom-out;
+}
+
+/* Exclure les images Mermaid du filtre d'inversion en mode sombre */
+/* Le filtre global s'applique à .medium-zoom-image--opened */
+/* Nous devons être plus spécifique pour les images Mermaid */
+
+/* Exception pour les images Mermaid - pas de filtre d'inversion */
+:global([data-theme="dark"]) .mermaid-zoom-wrapper img:global(.medium-zoom-image--opened) {
+ filter: none !important;
+}
+
+/* Alternative: cibler directement l'image dans le wrapper Mermaid */
+:global([data-theme="dark"]) .mermaid-zoom-wrapper :global(.medium-zoom-image--opened) {
+ filter: none !important;
+}
+
+/* Encore plus spécifique: cibler l'image générée par notre script */
+:global([data-theme="dark"]) .mermaid-zoom-wrapper img[data-zoomable="1"]:global(.medium-zoom-image--opened) {
+ filter: none !important;
+}
+
+/* Forcer un z-index très élevé pour medium-zoom (au-dessus de tout) */
+:global(.medium-zoom-overlay) {
+ z-index: 9999999 !important;
+}
+
+:global(.medium-zoom-image--opened) {
+ z-index: 10000000 !important;
+}
+
+/* Cibler la classe spécifique mermaid-zoom-image */
+:global([data-theme="dark"]) .mermaid-zoom-image:global(.medium-zoom-image--opened) {
+ filter: none !important;
+}
+
+/* Overlay medium-zoom avec z-index très élevé pour Mermaid */
+:global(.medium-zoom-overlay):has(.mermaid-zoom-wrapper) {
+ z-index: 9999999 !important;
+}
+
+/* Z-index très élevé pour les images Mermaid */
+:global(.mermaid-zoom-image):global(.medium-zoom-image--opened) {
+ z-index: 10000000 !important;
+}
+
+:global(.mermaid-zoom-wrapper):has(:global(.medium-zoom-image--opened)) {
+ z-index: 10000000 !important;
+}
+
+/* Masquer les autres diagrammes quand un est zoomé */
+:global(.medium-zoom--opened) .mermaid-zoom-wrapper {
+ opacity: 0;
+ z-index: calc(var(--z-base) - 1);
+ transition: opacity 0.3s ease;
+}
+
+/* Le diagramme zoomé reste visible */
+:global(.medium-zoom--opened) .mermaid-zoom-wrapper:has(.medium-zoom--opened) {
+ opacity: 1;
+ z-index: var(--z-overlay);
+}
+
+/* Fallback pour les navigateurs sans support :has() */
+:global(.medium-zoom--opened) .mermaid-zoom-wrapper.zoom-active {
+ opacity: 1 !important;
+ z-index: var(--z-overlay) !important;
+}
+
+/* Styles spécifiques pour différents types de diagrammes */
+
+/* Flowchart/Graph - styles généraux */
+.mermaid .node rect,
+.mermaid .node circle,
+.mermaid .node ellipse,
+.mermaid .label rect {
+ rx: 8px !important;
+ ry: 8px !important;
+}
+
+/* Flowchart - nœuds principaux */
+.mermaid .flowchart-node rect,
+.mermaid .flowchart-label rect {
+ rx: 8px !important;
+ ry: 8px !important;
+}
+
+/* Sequence diagram */
+.mermaid .actor rect,
+.mermaid .participant rect {
+ rx: 8px !important;
+ ry: 8px !important;
+}
+
+/* ER diagram */
+.mermaid .entityBox rect,
+.mermaid .er .entityBox rect {
+ rx: 6px !important;
+ ry: 6px !important;
+}
+
+/* Gantt chart */
+.mermaid .section0 rect,
+.mermaid .section1 rect,
+.mermaid .section2 rect,
+.mermaid .section3 rect {
+ rx: 4px !important;
+ ry: 4px !important;
+}
+
+/* Gitgraph */
+.mermaid .commit rect {
+ rx: 12px !important;
+ ry: 12px !important;
+}
+
+/* Mindmap */
+.mermaid .mindmap-node rect {
+ rx: 10px !important;
+ ry: 10px !important;
+}
+
+/* Sankey */
+.mermaid .sankey .node rect {
+ rx: 4px !important;
+ ry: 4px !important;
+}
+
+/* Timeline */
+.mermaid .timeline rect {
+ rx: 6px !important;
+ ry: 6px !important;
+}
+
+/* Pie chart */
+.mermaid .pieTitleText {
+ rx: 8px !important;
+ ry: 8px !important;
+}
+
+/* Journey */
+.mermaid .journey .section0 rect,
+.mermaid .journey .section1 rect,
+.mermaid .journey .section2 rect,
+.mermaid .journey .section3 rect {
+ rx: 8px !important;
+ ry: 8px !important;
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/components/_table.css b/content/articles/multi-harness-rl/app/src/styles/components/_table.css
new file mode 100644
index 00000000..95239114
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/components/_table.css
@@ -0,0 +1,121 @@
+.content-grid main table {
+ border-collapse: collapse;
+ table-layout: auto;
+ margin: 0;
+ background-color: var(--surface-bg);
+}
+
+.content-grid main th,
+.content-grid main td {
+ border-bottom: 1px solid var(--border-color);
+ padding: 6px 8px;
+ font-size: 15px;
+ white-space: nowrap;
+ /* prevent squashing; allow horizontal scroll instead */
+ word-break: auto-phrase;
+ /* white-space: break-spaces; */
+ vertical-align: top;
+}
+
+.content-grid main thead th {
+ border-bottom: 1px solid var(--border-color);
+}
+
+.content-grid main thead th {
+ border-bottom: 1px solid var(--border-color);
+}
+
+.content-grid main thead th {
+ background: var(--table-header-bg);
+ padding-top: 10px;
+ padding-bottom: 10px;
+ font-weight: 600;
+}
+
+.content-grid main hr {
+ border: none;
+ border-bottom: 1px solid var(--border-color);
+ margin: var(--spacing-5) 0;
+}
+
+/* Scroll wrapper: keeps table 100% width but enables horizontal scroll when needed */
+.content-grid main .table-scroll {
+ width: 100%;
+ overflow-x: auto;
+ -webkit-overflow-scrolling: touch;
+ border: 1px solid var(--border-color);
+ border-radius: var(--table-border-radius);
+ background: var(--surface-bg);
+ margin: 0 0 var(--block-spacing-y);
+}
+
+.content-grid main .table-scroll>table {
+ width: fit-content;
+ min-width: 100%;
+ max-width: none;
+}
+
+/* Vertical dividers between columns (no outer right border) */
+.content-grid main .table-scroll>table th,
+.content-grid main .table-scroll>table td {
+ border-right: 1px solid var(--border-color);
+}
+
+.content-grid main .table-scroll>table th:last-child,
+.content-grid main .table-scroll>table td:last-child {
+ border-right: none;
+}
+
+.content-grid main .table-scroll>table thead th:first-child {
+ border-top-left-radius: var(--table-border-radius);
+}
+
+.content-grid main .table-scroll>table thead th:last-child {
+ border-top-right-radius: var(--table-border-radius);
+}
+
+.content-grid main .table-scroll>table tbody tr:last-child td:first-child {
+ border-bottom-left-radius: var(--table-border-radius);
+}
+
+.content-grid main .table-scroll>table tbody tr:last-child td:last-child {
+ border-bottom-right-radius: var(--table-border-radius);
+}
+
+/* Zebra striping for odd rows */
+.content-grid main .table-scroll>table tbody tr:nth-child(odd) td {
+ background: var(--table-row-odd-bg);
+}
+
+/* Remove bottom border on last row */
+.content-grid main .table-scroll>table tbody tr:last-child td {
+ border-bottom: none;
+}
+
+/* Accordion context: remove outer borders/radius and fit content flush */
+.accordion .accordion__content .table-scroll {
+ border: none;
+ border-radius: 0;
+ margin: 0;
+ margin-bottom: 0 !important;
+}
+
+/* Ensure no bottom margin even if table isn't wrapped (fallback) */
+.accordion .accordion__content table {
+ margin: 0 !important;
+}
+
+.accordion .accordion__content .table-scroll>table thead th:first-child,
+.accordion .accordion__content .table-scroll>table thead th:last-child,
+.accordion .accordion__content .table-scroll>table tbody tr:last-child td:first-child,
+.accordion .accordion__content .table-scroll>table tbody tr:last-child td:last-child {
+ border-radius: 0;
+}
+
+/* Fallback for browsers without fit-content support */
+@supports not (width: fit-content) {
+ .content-grid main .table-scroll>table {
+ width: max-content;
+ min-width: 100%;
+ }
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/styles/components/_tag.css b/content/articles/multi-harness-rl/app/src/styles/components/_tag.css
new file mode 100644
index 00000000..b12285a3
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/components/_tag.css
@@ -0,0 +1,14 @@
+.tag-list { display: flex; flex-wrap: wrap; gap: 8px; margin: 8px 0 16px; }
+
+.tag {
+ display: inline-flex;
+ align-items: center;
+ gap: 6px;
+ padding: 8px 12px;
+ font-size: 12px;
+ line-height: 1;
+ border-radius: var(--button-radius);
+ background: var(--surface-bg);
+ border: 1px solid var(--border-color);
+ color: var(--text-color);
+}
diff --git a/content/articles/multi-harness-rl/app/src/styles/global.css b/content/articles/multi-harness-rl/app/src/styles/global.css
new file mode 100644
index 00000000..3dde0c5f
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/styles/global.css
@@ -0,0 +1,54 @@
+@import './_variables.css';
+@import './_reset.css';
+@import './_base.css';
+@import './_layout.css';
+@import './_print.css';
+@import './components/_code.css';
+@import './components/_button.css';
+@import './components/_table.css';
+@import './components/_tag.css';
+@import './components/_card.css';
+@import './components/_form.css';
+@import './components/_mermaid.css';
+
+.demo-wide,
+.demo-full-width {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ min-height: 150px;
+ color: var(--muted-color);
+ font-size: 12px;
+ border: 2px dashed var(--border-color);
+ border-radius: 8px;
+ background: var(--surface-bg);
+ margin-bottom: var(--block-spacing-y);
+}
+
+.mermaid {
+ background: none !important;
+ margin-bottom: var(--block-spacing-y) !important;
+}
+
+/* Iframes avec smoothing=0 : compensation du padding interne */
+iframe[src*="smoothing=0"] {
+ width: calc(100% + 120px) !important;
+ margin-left: -61px;
+ transform: scaleX(0.94);
+ /* Respecte le thème courant au lieu de forcer le blanc */
+ background-color: var(--surface-bg, transparent) !important;
+}
+
+/* Padding mobile pour les iframes */
+@media (max-width: 768px) {
+ iframe[src*="smoothing=0"] {
+ padding: 0 20px;
+ }
+}
+
+/* Fond adaptatif pour le thème sombre */
+[data-theme="dark"] iframe[src*="smoothing=0"] {
+ background-color: var(--surface-bg, #0f1115) !important;
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/app/src/utils/extract-embeds.mjs b/content/articles/multi-harness-rl/app/src/utils/extract-embeds.mjs
new file mode 100644
index 00000000..c9ceaec5
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/src/utils/extract-embeds.mjs
@@ -0,0 +1,1089 @@
+import { readFileSync, readdirSync, statSync } from 'fs';
+import { join, relative, dirname } from 'path';
+import { fileURLToPath } from 'url';
+import { createHash } from 'crypto';
+
+/**
+ * Extract HtmlEmbed, Image components and tables from MDX/Markdown content
+ * Simple utility to find visual elements and their props
+ */
+
+/**
+ * Parse image import statements from MDX content.
+ * Returns a Map of variable name → filename (just the basename).
+ * e.g. import placeholder from '../../assets/image/placeholder.png'
+ * → Map { 'placeholder' => 'placeholder.png' }
+ */
+function parseImageImports(content) {
+ const importMap = new Map();
+ const importPattern = /import\s+(\w+)\s+from\s+["']([^"']+)["']/g;
+ let match;
+ while ((match = importPattern.exec(content)) !== null) {
+ const varName = match[1];
+ const importPath = match[2];
+ // Extract just the filename from the path
+ const filename = importPath.split('/').pop();
+ if (filename && /\.(png|jpe?g|gif|webp|svg)$/i.test(filename)) {
+ importMap.set(varName, filename);
+ }
+ }
+ return importMap;
+}
+
+/**
+ * Strip fenced code blocks (``` ... ```) from content.
+ * Replaces code block content with whitespace of equal length
+ * to preserve character positions for downstream extraction.
+ */
+function stripCodeBlocks(content) {
+ return content.replace(/```[\s\S]*?```/g, (match) => ' '.repeat(match.length));
+}
+
+/**
+ * Simple Markdown to HTML converter for table cells
+ * Handles: links, bold, italic, code, strikethrough
+ */
+function markdownToHtml(md) {
+ if (!md) return '';
+
+ let html = md;
+
+ // Escape HTML entities first (but not for already-converted content)
+ // Skip if it already looks like HTML
+ if (!html.includes('')) {
+ html = html
+ .replace(/&/g, '&')
+ .replace(//g, '>');
+ }
+
+ // Links: [text](url)
+ html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, ' $1 ');
+
+ // Bold: **text** or __text__
+ html = html.replace(/\*\*([^*]+)\*\*/g, '$1 ');
+ html = html.replace(/__([^_]+)__/g, '$1 ');
+
+ // Italic: *text* or _text_ (but not inside words)
+ html = html.replace(/(?$1');
+ html = html.replace(/(?$1');
+
+ // Inline code: `code`
+ html = html.replace(/`([^`]+)`/g, '$1');
+
+ // Strikethrough: ~~text~~
+ html = html.replace(/~~([^~]+)~~/g, '$1');
+
+ // Checkboxes (common in tables)
+ html = html.replace(/\[x\]/gi, '✅');
+ html = html.replace(/\[ \]/g, '❌');
+
+ return html;
+}
+
+/**
+ * Extract Image components from MDX content
+ */
+export function extractImages(content) {
+ const images = [];
+
+ // Match components
+ const imagePattern = /]*\/>/gi;
+ let match;
+
+ while ((match = imagePattern.exec(content)) !== null) {
+ const tag = match[0];
+
+ // Extract src attribute (variable reference like {myImage})
+ const srcMatch = tag.match(/src\s*=\s*\{([^}]+)\}/i);
+ const src = srcMatch ? srcMatch[1].trim() : null;
+
+ // Extract alt
+ const altMatch = tag.match(/alt\s*=\s*["']([^"']+)["']/i);
+ const alt = altMatch ? altMatch[1] : 'Image';
+
+ // Extract caption
+ const captionMatch = tag.match(/caption\s*=\s*["']([^"']+)["']/i) ||
+ tag.match(/caption\s*=\s*\{`([^`]+)`\}/i);
+ const caption = captionMatch ? captionMatch[1] : null;
+
+ // Extract id
+ const idMatch = tag.match(/id\s*=\s*["']([^"']+)["']/i);
+ const id = idMatch ? idMatch[1] : null;
+
+ // Extract skipGallery
+ const skipGallery = /\bskipGallery\b/i.test(tag);
+
+ if (src) {
+ images.push({
+ type: 'image',
+ src,
+ alt,
+ caption,
+ id,
+ skipGallery
+ });
+ }
+ }
+
+ return images;
+}
+
+/**
+ * Split a markdown table row on pipe characters, respecting backtick spans.
+ * Pipes inside `inline code` are treated as literal text, not separators.
+ * Strips the leading/trailing empty strings from outer pipes but keeps
+ * internal empty cells (e.g. | val | | → ['val', '']).
+ */
+function splitTableRow(row) {
+ const raw = [];
+ let current = '';
+ let inBacktick = false;
+
+ for (let i = 0; i < row.length; i++) {
+ const ch = row[i];
+ if (ch === '`') {
+ inBacktick = !inBacktick;
+ current += ch;
+ } else if (ch === '|' && !inBacktick) {
+ raw.push(current);
+ current = '';
+ } else {
+ current += ch;
+ }
+ }
+ raw.push(current);
+
+ // Remove leading/trailing empty strings from outer | delimiters
+ // but preserve internal empty cells
+ let start = 0;
+ let end = raw.length - 1;
+ while (start <= end && !raw[start].trim()) start++;
+ while (end >= start && !raw[end].trim()) end--;
+
+ return raw.slice(start, end + 1).map(c => c.trim());
+}
+
+/**
+ * Parse a markdown table row into exactly `expectedCols` cells.
+ * First splits respecting backticks (splitTableRow), then if the row
+ * has too many cells (e.g. unescaped | in values), merges overflow
+ * cells back together to match the expected column count.
+ */
+function parseTableRow(row, expectedCols) {
+ let cells = splitTableRow(row);
+
+ if (cells.length <= expectedCols) return cells;
+
+ // Too many cells: merge overflow into the last "overflowing" column.
+ // Strategy: keep first (expectedCols - 1) cells, merge the rest into one,
+ // then take the last cell separately (it's usually the description).
+ // This handles: | val | No | 'a' | 'b' | 'c' | Description |
+ // → [val, No, 'a' | 'b' | 'c', Description]
+ const head = cells.slice(0, expectedCols - 1);
+ const tail = cells.slice(expectedCols - 1);
+ // The last element is the final column; everything in between is the overflowing column
+ if (tail.length > 1) {
+ const lastCell = tail.pop();
+ const merged = tail.join(' | ');
+ return [...head, merged, lastCell].slice(0, expectedCols);
+ }
+
+ return cells.slice(0, expectedCols);
+}
+
+/**
+ * Extract markdown tables from content
+ */
+export function extractTables(content) {
+ const tables = [];
+
+ // Match markdown tables (lines starting with |)
+ // A table has at least a header row, separator row, and one data row
+ const tablePattern = /(\|[^\n]+\|\n\|[-:\s|]+\|\n(?:\|[^\n]+\|\n?)+)/g;
+ let match;
+ let tableIndex = 0;
+
+ while ((match = tablePattern.exec(content)) !== null) {
+ const tableContent = match[1].trim();
+ const rows = tableContent.split('\n').filter(row => row.trim());
+
+ if (rows.length >= 3) {
+ // Parse header (filter empty = no empty header columns)
+ const headerRow = rows[0];
+ const headers = splitTableRow(headerRow)
+ .filter(cell => cell !== '')
+ .map(cell => markdownToHtml(cell));
+
+ const expectedCols = headers.length;
+
+ // Parse data rows (skip separator at index 1)
+ // Use parseTableRow to handle overflow pipes (e.g. union types)
+ // Keep empty cells to preserve column alignment
+ const dataRows = rows.slice(2).map(row => {
+ return parseTableRow(row, expectedCols)
+ .map(cell => markdownToHtml(cell));
+ });
+
+ tables.push({
+ type: 'table',
+ id: `table-${tableIndex++}`,
+ headers,
+ rows: dataRows,
+ raw: tableContent
+ });
+ }
+ }
+
+ return tables;
+}
+
+export function extractHtmlEmbeds(rawContent) {
+ const embeds = [];
+
+ // Strip code blocks to avoid extracting components from code examples
+ const content = stripCodeBlocks(rawContent);
+
+ // First, find all Wide components and mark their content
+ // Pattern to match ... blocks
+ const widePattern = /([\s\S]*?)<\/Wide>/gi;
+ const wideBlocks = [];
+ let wideMatch;
+ while ((wideMatch = widePattern.exec(content)) !== null) {
+ wideBlocks.push({
+ start: wideMatch.index,
+ end: wideMatch.index + wideMatch[0].length,
+ content: wideMatch[0]
+ });
+ }
+
+ // Helper to check if an embed is inside a Wide block
+ const isInsideWide = (embedStartIndex) => {
+ return wideBlocks.some(block =>
+ embedStartIndex >= block.start && embedStartIndex < block.end
+ );
+ };
+
+ // Pattern to match HtmlEmbed opening tags
+ const embedPattern = / while respecting string boundaries
+ let pos = matchIndex + 10; // After " 0 ? content[pos - 1] : '';
+
+ match += char;
+
+ // Track string boundaries (template strings, single, double quotes)
+ if (!inString) {
+ if ((char === '`' || char === '"' || char === "'") && prevChar !== '\\') {
+ inString = true;
+ stringDelim = char;
+ }
+ } else {
+ if (char === stringDelim && prevChar !== '\\') {
+ inString = false;
+ stringDelim = null;
+ }
+ }
+
+ // Track JSX expression braces (for config={{...}}, data={{...}}, etc.)
+ if (!inString) {
+ if (char === '{') {
+ inJSXBraces++;
+ } else if (char === '}') {
+ inJSXBraces--;
+ }
+ }
+
+ // Check for closing /> - only valid if not in string AND all JSX braces are closed
+ if (!inString && inJSXBraces === 0 && char === '/' && pos + 1 < content.length && content[pos + 1] === '>') {
+ match += '>';
+ break;
+ }
+
+ pos++;
+ }
+
+ // If config={{ is present, we need to find the real closing after }}
+ // Check if config={{ is there but the match doesn't include the full config (doesn't end with }})
+ if (match.includes('config={{') && !match.includes('}}')) {
+ // The match was cut off at the first /> it found (probably in desc)
+ // We need to find the real closing after }}
+
+ // Find where config={{ starts
+ const configStart = match.indexOf('config={{');
+ if (configStart >= 0) {
+ // Look for the matching }} after this, starting from the content
+ let braceCount = 2; // We're inside {{
+ let pos = matchIndex + configStart + 9; // After "config={{"
+ let foundEnd = false;
+
+ while (pos < content.length) {
+ const char = content[pos];
+ const prevChar = pos > 0 ? content[pos - 1] : '';
+
+ // Track strings to avoid counting braces inside strings
+ if ((char === '"' || char === "'" || char === '`') && prevChar !== '\\') {
+ // We hit a string delimiter - skip the entire string
+ const stringDelim = char;
+ pos++;
+ while (pos < content.length) {
+ if (content[pos] === stringDelim && content[pos - 1] !== '\\') {
+ break;
+ }
+ // Handle template string ${...}
+ if (stringDelim === '`' && content[pos] === '$' && pos + 1 < content.length && content[pos + 1] === '{') {
+ // Skip ${...} without counting
+ pos += 2;
+ let innerBraces = 1;
+ while (pos < content.length && innerBraces > 0) {
+ if (content[pos] === '{') innerBraces++;
+ if (content[pos] === '}') innerBraces--;
+ pos++;
+ }
+ continue;
+ }
+ pos++;
+ }
+ pos++; // Skip the closing quote
+ continue;
+ }
+
+ if (char === '{') braceCount++;
+ if (char === '}') {
+ braceCount--;
+ if (braceCount === 0) {
+ // Found the closing }}
+ // Now look for /> after optional whitespace
+ pos++;
+ while (pos < content.length && /\s/.test(content[pos])) {
+ pos++;
+ }
+ if (pos < content.length && content[pos] === '/' && pos + 1 < content.length && content[pos + 1] === '>') {
+ // Found the real closing
+ match = content.substring(matchIndex, pos + 2);
+ foundEnd = true;
+ break;
+ }
+ }
+ }
+ pos++;
+ }
+
+ if (!foundEnd) {
+ // Fallback: try to find }} /> pattern after match
+ const after = content.substring(matchIndex + match.length);
+ const endPattern = after.match(/\}\}\s*\/>/);
+ if (endPattern) {
+ match = content.substring(matchIndex, matchIndex + match.length + endPattern.index + endPattern[0].length);
+ }
+ }
+ }
+ }
+
+ // Helper function to extract attribute value supporting multiline
+ const extractAttr = (attrName, content) => {
+ // Try JSX template strings first: desc={`...`}
+ const templateMatch = content.match(new RegExp(`${attrName}\\s*=\\s*\\{\`([\\s\\S]*?)\`\\}`, 'i'));
+ if (templateMatch) return templateMatch[1].trim();
+
+ // Try single quotes: desc='...'
+ const singleQuoteMatch = content.match(new RegExp(`${attrName}\\s*=\\s*'([\\s\\S]*?)'`, 'i'));
+ if (singleQuoteMatch) return singleQuoteMatch[1].trim();
+
+ // Try double quotes: desc="..."
+ const doubleQuoteMatch = content.match(new RegExp(`${attrName}\\s*=\\s*"([\\s\\S]*?)"`, 'i'));
+ if (doubleQuoteMatch) return doubleQuoteMatch[1].trim();
+
+ return undefined;
+ };
+
+ // Extract src attribute (required)
+ const src = extractAttr('src', match);
+ if (!src) continue;
+
+ // Extract optional attributes
+ const title = extractAttr('title', match);
+ const desc = extractAttr('desc', match);
+ const id = extractAttr('id', match);
+ const data = extractAttr('data', match);
+ const frameless = /\bframeless\b/i.test(match);
+ const wideAttr = /\bwide\b/i.test(match);
+ const skipGallery = /\bskipGallery\b/i.test(match);
+
+ // Extract config attribute - JSX object format: config={{ ... }}
+ let config = null;
+
+ // Pattern to match config={{ ... }} with balanced braces
+ const jsxConfigRegex = /config\s*=\s*\{\{/i;
+ const jsxConfigMatch = match.match(jsxConfigRegex);
+
+ if (jsxConfigMatch) {
+ try {
+ // Find the start position after config={{
+ const configStart = jsxConfigMatch.index;
+ const startPos = match.indexOf('{{', configStart) + 2;
+
+ // Find matching closing braces with better handling
+ let braceCount = 1; // Start at 1 because we're inside {{
+ let inString = false;
+ let stringChar = null;
+ let pos = startPos;
+
+ for (; pos < match.length; pos++) {
+ const char = match[pos];
+ const prevChar = pos > 0 ? match[pos - 1] : '';
+ const nextChar = pos < match.length - 1 ? match[pos + 1] : '';
+
+ // Handle string literals - check for template strings too
+ if (!inString) {
+ if (char === '`') {
+ inString = true;
+ stringChar = '`';
+ } else if (char === '"' && prevChar !== '\\') {
+ inString = true;
+ stringChar = '"';
+ } else if (char === "'" && prevChar !== '\\') {
+ inString = true;
+ stringChar = "'";
+ }
+ } else {
+ // Check for end of string
+ if (char === stringChar && prevChar !== '\\') {
+ inString = false;
+ stringChar = null;
+ }
+ // Template strings can contain ${...} - handle that
+ if (stringChar === '`' && char === '$' && nextChar === '{') {
+ // Skip the ${ but don't count it as a brace yet
+ pos++; // Skip $
+ braceCount++; // Count the { we're about to see
+ continue;
+ }
+ }
+
+ if (!inString) {
+ if (char === '{') {
+ braceCount++;
+ } else if (char === '}') {
+ braceCount--;
+ if (braceCount === 0) {
+ // Found matching closing }}
+ break;
+ }
+ }
+ }
+ }
+
+ if (braceCount !== 0) {
+ throw new Error(`Unbalanced braces: braceCount=${braceCount}`);
+ }
+
+ // Extract the JSX object content
+ let jsxContent = match.substring(startPos, pos).trim();
+
+ // Instead of converting to JSON, evaluate the JavaScript object directly
+ // This is safer in a build context (not in browser)
+ try {
+ // Wrap in parentheses and braces to make it a valid expression
+ const jsCode = `({${jsxContent}})`;
+
+ // Use Function constructor to safely evaluate (no access to local scope)
+ // This is safe because we're in Node.js build time, not browser runtime
+ config = new Function('return ' + jsCode)();
+ } catch (evalError) {
+ // If eval fails, try the JSON approach as fallback
+ // Fallback: try JSON parsing
+ let jsonStr = jsxContent;
+
+ // Add braces around the content
+ jsonStr = '{' + jsonStr + '}';
+
+ // Quote unquoted keys
+ for (let pass = 0; pass < 5; pass++) {
+ jsonStr = jsonStr.replace(/([{,\[\s])([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":');
+ jsonStr = jsonStr.replace(/^([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/gm, '"$1":');
+ }
+
+ // Replace single quotes with double quotes
+ jsonStr = jsonStr.replace(/'/g, '"');
+
+ // Remove trailing commas
+ jsonStr = jsonStr.replace(/,\s*([}\]])/g, '$1');
+
+ try {
+ config = JSON.parse(jsonStr);
+ } catch (jsonError) {
+ // Both methods failed - log warning but don't throw
+ console.warn('[extract-embeds] Config parsing failed:', jsonError.message);
+ }
+ }
+ } catch (e) {
+ // If parsing fails, keep config as null
+ // Component will handle missing config
+ }
+ }
+
+ // Fallback: try standard attribute extraction (for string-based config)
+ if (!config) {
+ const configAttr = extractAttr('config', match);
+ if (configAttr) {
+ try {
+ config = JSON.parse(configAttr);
+ } catch (e) {
+ // Keep as string if not valid JSON
+ config = configAttr;
+ }
+ }
+ }
+
+ // Check if this embed is inside a Wide component OR has wide prop
+ const isWide = isInsideWide(matchIndex) || wideAttr;
+
+ embeds.push({
+ src,
+ title,
+ desc,
+ id,
+ frameless,
+ data,
+ config,
+ wide: isWide,
+ skipGallery
+ });
+ }
+
+ return embeds;
+}
+
+/**
+ * Recursively find all MDX files in a directory
+ * Skips demo chapters by default to avoid missing embeds
+ */
+function findMdxFiles(dir, baseDir = dir, files = [], skipDemo = true) {
+ const entries = readdirSync(dir);
+
+ for (const entry of entries) {
+ const fullPath = join(dir, entry);
+ const stat = statSync(fullPath);
+
+ if (stat.isDirectory()) {
+ // Skip demo directory if skipDemo is true
+ if (skipDemo && entry === 'demo') {
+ continue;
+ }
+ findMdxFiles(fullPath, baseDir, files, skipDemo);
+ } else if (entry.endsWith('.mdx')) {
+ files.push(fullPath);
+ }
+ }
+
+ return files;
+}
+
+/**
+ * Parse imports and chapter usage order from article.mdx
+ */
+function parseArticleChapters(articleContent, contentDir) {
+ const chapterMap = new Map(); // Component name -> file path
+ const chapterOrder = []; // Ordered list of file paths
+
+ // Extract import statements
+ const importPattern = /import\s+(\w+)\s+from\s+["'](.\/chapters\/[^"']+)["']/g;
+ let match;
+ while ((match = importPattern.exec(articleContent)) !== null) {
+ const [, componentName, importPath] = match;
+ const fullPath = join(contentDir, importPath);
+ chapterMap.set(componentName, fullPath);
+ }
+
+ // Extract chapter usage order (e.g., )
+ const usagePattern = /<(\w+)\s*\/>/g;
+ while ((match = usagePattern.exec(articleContent)) !== null) {
+ const componentName = match[1];
+ if (chapterMap.has(componentName)) {
+ const chapterPath = chapterMap.get(componentName);
+ if (!chapterOrder.includes(chapterPath)) {
+ chapterOrder.push(chapterPath);
+ }
+ }
+ }
+
+ return chapterOrder;
+}
+
+/**
+ * Build a unique identity key for an embed.
+ *
+ * Strategy (in priority order):
+ * 1. `id` — if the author gave an explicit id, it's unique by convention.
+ * 2. `src` + deterministic hash of (config, data) — same template with
+ * different parameters produces different keys.
+ * 3. `src` alone — for embeds with no config/data (unique HTML file).
+ *
+ * This allows the same generic template (e.g. d3-line-chart.html) to appear
+ * multiple times when each instance carries a different config, while still
+ * deduplicating true duplicates (same src + same config that appear in both
+ * article.mdx and a chapter).
+ */
+function embedKey(embed) {
+ if (embed.id) return `id:${embed.id}`;
+
+ const hasConfig = embed.config != null;
+ const hasData = embed.data != null;
+
+ if (!hasConfig && !hasData) return `src:${embed.src}`;
+
+ // Deterministic hash of the variable parts
+ const payload = JSON.stringify({ config: embed.config ?? null, data: embed.data ?? null });
+ const hash = createHash('sha1').update(payload).digest('hex').slice(0, 10);
+ return `src:${embed.src}#${hash}`;
+}
+
+/**
+ * Load and extract embeds from MDX content files, following article structure
+ */
+export function loadEmbedsFromMDX() {
+ // Get absolute path to content directory
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = dirname(__filename);
+
+ // Try to resolve content directory - works in both dev and build
+ let contentDir = join(__dirname, '../content');
+
+ if (!statSync(contentDir, { throwIfNoEntry: false })) {
+ contentDir = join(__dirname, '../../src/content');
+ }
+
+ if (!statSync(contentDir, { throwIfNoEntry: false })) {
+ contentDir = join(__dirname, '../../../src/content');
+ }
+
+ const allEmbeds = [];
+ const articleFile = join(contentDir, 'article.mdx');
+
+ try {
+ // Read main article file
+ const articleContent = readFileSync(articleFile, 'utf-8');
+
+ // Extract embeds from main article first
+ const articleEmbeds = extractHtmlEmbeds(articleContent);
+ articleEmbeds.forEach(embed => {
+ embed.sourceFile = 'content/article.mdx';
+ });
+ allEmbeds.push(...articleEmbeds);
+
+ // Parse chapter order from article
+ const chapterOrder = parseArticleChapters(articleContent, contentDir);
+
+ // Extract embeds from chapters in order
+ for (const chapterPath of chapterOrder) {
+ try {
+ const chapterContent = readFileSync(chapterPath, 'utf-8');
+ const embeds = extractHtmlEmbeds(chapterContent);
+
+ // Add source file info (relative path)
+ const relativePath = relative(contentDir, chapterPath);
+ embeds.forEach(embed => {
+ embed.sourceFile = `content/${relativePath}`;
+ });
+
+ allEmbeds.push(...embeds);
+ } catch (error) {
+ console.error(`Error reading chapter ${chapterPath}:`, error);
+ }
+ }
+
+ // Also include any other MDX files not in chapters (for completeness)
+ const allMdxFiles = findMdxFiles(contentDir, contentDir, [], false);
+ const processedFiles = new Set([articleFile, ...chapterOrder]);
+
+ for (const filePath of allMdxFiles) {
+ if (!processedFiles.has(filePath)) {
+ try {
+ const rawContent = readFileSync(filePath, 'utf-8');
+ const embeds = extractHtmlEmbeds(rawContent);
+ const relativePath = relative(contentDir, filePath);
+ embeds.forEach(embed => {
+ embed.sourceFile = `content/${relativePath}`;
+ });
+ allEmbeds.push(...embeds);
+ } catch (error) {
+ console.error(`Error reading ${filePath}:`, error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error processing article:', error);
+ // Fallback to old behavior if article.mdx can't be read
+ const mdxFiles = findMdxFiles(contentDir, contentDir, [], false);
+ for (const filePath of mdxFiles) {
+ try {
+ const rawContent = readFileSync(filePath, 'utf-8');
+ const embeds = extractHtmlEmbeds(rawContent);
+ const relativePath = relative(contentDir, filePath);
+ embeds.forEach(embed => {
+ embed.sourceFile = `content/${relativePath}`;
+ });
+ allEmbeds.push(...embeds);
+ } catch (err) {
+ console.error(`Error reading ${filePath}:`, err);
+ }
+ }
+ }
+
+ // Remove true duplicates (same identity) keeping first occurrence (= order of appearance).
+ // Identity = id (if set), or src + hash(config, data). This means the same generic
+ // template with different configs produces distinct entries.
+ const seen = new Map();
+ const uniqueEmbeds = [];
+ for (const embed of allEmbeds) {
+ const key = embedKey(embed);
+ if (!seen.has(key)) {
+ seen.set(key, true);
+ uniqueEmbeds.push(embed);
+ }
+ }
+
+ return uniqueEmbeds;
+}
+
+/**
+ * Helper to extract attribute from tag content
+ */
+function extractAttrFromTag(attrName, tagContent) {
+ // Try JSX template strings first: attr={`...`}
+ const templateMatch = tagContent.match(new RegExp(`${attrName}\\s*=\\s*\\{\`([\\s\\S]*?)\`\\}`, 'i'));
+ if (templateMatch) return templateMatch[1].trim();
+
+ // Try single quotes: attr='...'
+ const singleQuoteMatch = tagContent.match(new RegExp(`${attrName}\\s*=\\s*'([\\s\\S]*?)'`, 'i'));
+ if (singleQuoteMatch) return singleQuoteMatch[1].trim();
+
+ // Try double quotes: attr="..."
+ const doubleQuoteMatch = tagContent.match(new RegExp(`${attrName}\\s*=\\s*"([\\s\\S]*?)"`, 'i'));
+ if (doubleQuoteMatch) return doubleQuoteMatch[1].trim();
+
+ return undefined;
+}
+
+/**
+ * Check if position is inside a Wide component
+ */
+function isPositionInsideWide(content, position) {
+ const widePattern = /([\s\S]*?)<\/Wide>/gi;
+ let match;
+ while ((match = widePattern.exec(content)) !== null) {
+ if (position >= match.index && position < match.index + match[0].length) {
+ return true;
+ }
+ }
+ return false;
+}
+
+/**
+ * Extract all visual elements from content with their position
+ * Returns sorted by position (order of appearance)
+ */
+function extractAllVisualsWithPosition(rawContent) {
+ const visuals = [];
+
+ // Parse image imports before stripping code blocks (imports are never in code blocks)
+ const imageImports = parseImageImports(rawContent);
+
+ // Strip code blocks to avoid extracting components from code examples
+ const content = stripCodeBlocks(rawContent);
+
+ // Extract HtmlEmbeds with position and ALL props
+ const embedPattern = / 0 ? content[pos - 1] : '';
+ tagContent += char;
+
+ if (!inString) {
+ if ((char === '`' || char === '"' || char === "'") && prevChar !== '\\') {
+ inString = true;
+ stringDelim = char;
+ }
+ } else {
+ if (char === stringDelim && prevChar !== '\\') {
+ inString = false;
+ stringDelim = null;
+ }
+ }
+
+ if (!inString) {
+ if (char === '{') inJSXBraces++;
+ else if (char === '}') inJSXBraces--;
+ }
+
+ if (!inString && inJSXBraces === 0 && char === '/' && pos + 1 < content.length && content[pos + 1] === '>') {
+ tagContent += '>';
+ break;
+ }
+ pos++;
+ }
+
+ // Extract all props
+ const src = extractAttrFromTag('src', tagContent);
+ if (src) {
+ const title = extractAttrFromTag('title', tagContent);
+ const desc = extractAttrFromTag('desc', tagContent);
+ const id = extractAttrFromTag('id', tagContent);
+ const data = extractAttrFromTag('data', tagContent);
+ const frameless = /\bframeless\b/i.test(tagContent);
+ const wideAttr = /\bwide\b/i.test(tagContent);
+ const skipGallery = /\bskipGallery\b/i.test(tagContent);
+
+ // Parse config if present
+ let config = null;
+ const jsxConfigMatch = tagContent.match(/config\s*=\s*\{\{/i);
+ if (jsxConfigMatch) {
+ try {
+ const configStart = tagContent.indexOf('{{', jsxConfigMatch.index) + 2;
+ let braceCount = 1;
+ let configEnd = configStart;
+ for (let i = configStart; i < tagContent.length && braceCount > 0; i++) {
+ if (tagContent[i] === '{') braceCount++;
+ if (tagContent[i] === '}') braceCount--;
+ if (braceCount === 0) configEnd = i;
+ }
+ const jsxContent = tagContent.substring(configStart, configEnd).trim();
+ config = new Function('return ({' + jsxContent + '})')();
+ } catch (e) {
+ // Config parsing failed, keep null
+ }
+ }
+
+ const isWide = isPositionInsideWide(content, position) || wideAttr;
+
+ visuals.push({
+ type: 'embed',
+ position,
+ src,
+ title,
+ desc,
+ id,
+ data,
+ frameless,
+ config,
+ wide: isWide,
+ skipGallery
+ });
+ }
+ }
+
+ // Find all Stack blocks to detect grouped images
+ const stackBlocks = [];
+ const stackPattern = /([\s\S]*?)<\/Stack>/gi;
+ while ((match = stackPattern.exec(content)) !== null) {
+ const stackAttrs = match[1];
+ const stackContent = match[2];
+ const stackStart = match.index;
+ const stackEnd = stackStart + match[0].length;
+
+ // Check if this Stack contains components
+ const innerImages = [];
+ const innerImagePattern = /]*)\/?>/gi;
+ let imgMatch;
+ while ((imgMatch = innerImagePattern.exec(stackContent)) !== null) {
+ const tag = imgMatch[0];
+ const srcM = tag.match(/src\s*=\s*\{([^}]+)\}/i);
+ if (srcM) {
+ const varName = srcM[1].trim();
+ const altM = tag.match(/alt\s*=\s*["']([^"']+)["']/i);
+ const captionM = tag.match(/caption\s*=\s*["']([^"']+)["']/i);
+ const imgSkipGallery = /\bskipGallery\b/i.test(tag);
+ innerImages.push({
+ src: varName,
+ resolvedFilename: imageImports.get(varName) || null,
+ alt: altM ? altM[1] : 'Image',
+ caption: captionM ? captionM[1] : null,
+ skipGallery: imgSkipGallery,
+ });
+ }
+ }
+
+ if (innerImages.length > 0) {
+ // Extract Stack layout/gap props
+ const layoutM = stackAttrs.match(/layout\s*=\s*["']([^"']+)["']/i);
+ const gapM = stackAttrs.match(/gap\s*=\s*["']([^"']+)["']/i);
+
+ // If ALL images have skipGallery, the whole stack is skipped
+ const allSkipped = innerImages.every(img => img.skipGallery);
+
+ stackBlocks.push({ start: stackStart, end: stackEnd });
+ visuals.push({
+ type: 'stack',
+ position: stackStart,
+ images: innerImages,
+ layout: layoutM ? layoutM[1] : '2-column',
+ gap: gapM ? gapM[1] : 'medium',
+ skipGallery: allSkipped,
+ });
+ }
+ }
+
+ // Helper to check if position is inside a Stack block
+ const isInsideStack = (pos) => {
+ return stackBlocks.some(b => pos >= b.start && pos < b.end);
+ };
+
+ // Extract standalone Images (not inside Stack)
+ const imagePattern = /]*\/>/gi;
+ while ((match = imagePattern.exec(content)) !== null) {
+ // Skip images already captured inside a Stack
+ if (isInsideStack(match.index)) continue;
+
+ const srcMatch = match[0].match(/src\s*=\s*\{([^}]+)\}/i);
+ if (srcMatch) {
+ const varName = srcMatch[1].trim();
+ const altMatch = match[0].match(/alt\s*=\s*["']([^"']+)["']/i);
+ const captionMatch = match[0].match(/caption\s*=\s*["']([^"']+)["']/i);
+ const skipGallery = /\bskipGallery\b/i.test(match[0]);
+ const resolvedFilename = imageImports.get(varName) || null;
+ visuals.push({
+ type: 'image',
+ position: match.index,
+ src: varName,
+ resolvedFilename,
+ alt: altMatch ? altMatch[1] : 'Image',
+ caption: captionMatch ? captionMatch[1] : null,
+ skipGallery,
+ });
+ }
+ }
+
+ // Extract Tables with position
+ const tablePattern = /(\|[^\n]+\|\n\|[-:\s|]+\|\n(?:\|[^\n]+\|\n?)+)/g;
+ let tableIndex = 0;
+ while ((match = tablePattern.exec(content)) !== null) {
+ const tableContent = match[1].trim();
+ const rows = tableContent.split('\n').filter(row => row.trim());
+
+ if (rows.length >= 3) {
+ const headerRow = rows[0];
+ const headers = splitTableRow(headerRow)
+ .filter(cell => cell !== '')
+ .map(cell => markdownToHtml(cell));
+
+ const expectedCols = headers.length;
+
+ const dataRows = rows.slice(2).map(row => {
+ return parseTableRow(row, expectedCols)
+ .map(cell => markdownToHtml(cell));
+ });
+
+ visuals.push({
+ type: 'table',
+ position: match.index,
+ id: `table-${tableIndex++}`,
+ headers,
+ rows: dataRows,
+ });
+ }
+ }
+
+ // Sort by position (order of appearance)
+ visuals.sort((a, b) => a.position - b.position);
+
+ return visuals;
+}
+
+/**
+ * Load all visual elements (embeds, images, tables) from MDX content files
+ * Returns them in order of appearance in the article
+ */
+export function loadAllVisualsFromMDX() {
+ const __filename = fileURLToPath(import.meta.url);
+ const __dirname = dirname(__filename);
+
+ let contentDir = join(__dirname, '../content');
+ if (!statSync(contentDir, { throwIfNoEntry: false })) {
+ contentDir = join(__dirname, '../../src/content');
+ }
+ if (!statSync(contentDir, { throwIfNoEntry: false })) {
+ contentDir = join(__dirname, '../../../src/content');
+ }
+
+ const allVisuals = [];
+ const articleFile = join(contentDir, 'article.mdx');
+
+ try {
+ const articleContent = readFileSync(articleFile, 'utf-8');
+
+ // Extract all visual elements from article IN ORDER (with all props)
+ const articleVisuals = extractAllVisualsWithPosition(articleContent);
+ articleVisuals.forEach(item => {
+ item.sourceFile = 'content/article.mdx';
+ });
+ allVisuals.push(...articleVisuals);
+
+ // Parse chapter order and extract from chapters
+ const chapterOrder = parseArticleChapters(articleContent, contentDir);
+
+ for (const chapterPath of chapterOrder) {
+ try {
+ const chapterContent = readFileSync(chapterPath, 'utf-8');
+
+ // Extract all visuals IN ORDER from this chapter (with all props)
+ const chapterVisuals = extractAllVisualsWithPosition(chapterContent);
+ const relativePath = relative(contentDir, chapterPath);
+ chapterVisuals.forEach(item => {
+ item.sourceFile = `content/${relativePath}`;
+ });
+ allVisuals.push(...chapterVisuals);
+ } catch (error) {
+ console.error(`Error reading chapter ${chapterPath}:`, error);
+ }
+ }
+
+ // Process other MDX files not already handled
+ const allMdxFiles = findMdxFiles(contentDir, contentDir, [], false);
+ const processedFiles = new Set([articleFile, ...chapterOrder]);
+
+ for (const filePath of allMdxFiles) {
+ if (!processedFiles.has(filePath)) {
+ try {
+ const rawContent = readFileSync(filePath, 'utf-8');
+ const fileVisuals = extractAllVisualsWithPosition(rawContent);
+ const relativePath = relative(contentDir, filePath);
+ fileVisuals.forEach(item => {
+ item.sourceFile = `content/${relativePath}`;
+ });
+ allVisuals.push(...fileVisuals);
+ } catch (error) {
+ console.error(`Error reading ${filePath}:`, error);
+ }
+ }
+ }
+ } catch (error) {
+ console.error('Error processing article:', error);
+ }
+
+ // Keep all occurrences (no deduplication)
+ // Duplicates will be numbered in dataviz.astro (e.g., d3-line-chart, d3-line-chart-2)
+ return allVisuals;
+}
diff --git a/content/articles/multi-harness-rl/app/yarn.lock b/content/articles/multi-harness-rl/app/yarn.lock
new file mode 100644
index 00000000..a1d2c23a
--- /dev/null
+++ b/content/articles/multi-harness-rl/app/yarn.lock
@@ -0,0 +1,5857 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@ampproject/remapping@^2.2.0", "@ampproject/remapping@^2.2.1":
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz"
+ integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.5"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@antfu/install-pkg@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz"
+ integrity sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==
+ dependencies:
+ package-manager-detector "^1.3.0"
+ tinyexec "^1.0.1"
+
+"@antfu/utils@^9.2.0":
+ version "9.3.0"
+ resolved "https://registry.npmjs.org/@antfu/utils/-/utils-9.3.0.tgz"
+ integrity sha512-9hFT4RauhcUzqOE4f1+frMKLZrgNog5b06I7VmZQV1BkvwvqrbC8EBZf3L1eEL2AKb6rNKjER0sEvJiSP1FXEA==
+
+"@astrojs/compiler@^2.10.3":
+ version "2.12.2"
+ resolved "https://registry.npmjs.org/@astrojs/compiler/-/compiler-2.12.2.tgz"
+ integrity sha512-w2zfvhjNCkNMmMMOn5b0J8+OmUaBL1o40ipMvqcG6NRpdC+lKxmTi48DT8Xw0SzJ3AfmeFLB45zXZXtmbsjcgw==
+
+"@astrojs/internal-helpers@0.4.1":
+ version "0.4.1"
+ resolved "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.4.1.tgz"
+ integrity sha512-bMf9jFihO8YP940uD70SI/RDzIhUHJAolWVcO1v5PUivxGKvfLZTLTVVxEYzGYyPsA3ivdLNqMnL5VgmQySa+g==
+
+"@astrojs/markdown-remark@5.3.0":
+ version "5.3.0"
+ resolved "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-5.3.0.tgz"
+ integrity sha512-r0Ikqr0e6ozPb5bvhup1qdWnSPUvQu6tub4ZLYaKyG50BXZ0ej6FhGz3GpChKpH7kglRFPObJd/bDyf2VM9pkg==
+ dependencies:
+ "@astrojs/prism" "3.1.0"
+ github-slugger "^2.0.0"
+ hast-util-from-html "^2.0.3"
+ hast-util-to-text "^4.0.2"
+ import-meta-resolve "^4.1.0"
+ mdast-util-definitions "^6.0.0"
+ rehype-raw "^7.0.0"
+ rehype-stringify "^10.0.1"
+ remark-gfm "^4.0.0"
+ remark-parse "^11.0.0"
+ remark-rehype "^11.1.1"
+ remark-smartypants "^3.0.2"
+ shiki "^1.22.0"
+ unified "^11.0.5"
+ unist-util-remove-position "^5.0.0"
+ unist-util-visit "^5.0.0"
+ unist-util-visit-parents "^6.0.1"
+ vfile "^6.0.3"
+
+"@astrojs/mdx@^3.1.9":
+ version "3.1.9"
+ resolved "https://registry.npmjs.org/@astrojs/mdx/-/mdx-3.1.9.tgz"
+ integrity sha512-3jPD4Bff6lIA20RQoonnZkRtZ9T3i0HFm6fcDF7BMsKIZ+xBP2KXzQWiuGu62lrVCmU612N+SQVGl5e0fI+zWg==
+ dependencies:
+ "@astrojs/markdown-remark" "5.3.0"
+ "@mdx-js/mdx" "^3.1.0"
+ acorn "^8.14.0"
+ es-module-lexer "^1.5.4"
+ estree-util-visit "^2.0.0"
+ gray-matter "^4.0.3"
+ hast-util-to-html "^9.0.3"
+ kleur "^4.1.5"
+ rehype-raw "^7.0.0"
+ remark-gfm "^4.0.0"
+ remark-smartypants "^3.0.2"
+ source-map "^0.7.4"
+ unist-util-visit "^5.0.0"
+ vfile "^6.0.3"
+
+"@astrojs/prism@3.1.0":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@astrojs/prism/-/prism-3.1.0.tgz"
+ integrity sha512-Z9IYjuXSArkAUx3N6xj6+Bnvx8OdUSHA8YoOgyepp3+zJmtVYJIl/I18GozdJVW1p5u/CNpl3Km7/gwTJK85cw==
+ dependencies:
+ prismjs "^1.29.0"
+
+"@astrojs/sitemap@^3.7.0":
+ version "3.7.0"
+ resolved "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.0.tgz"
+ integrity sha512-+qxjUrz6Jcgh+D5VE1gKUJTA3pSthuPHe6Ao5JCxok794Lewx8hBFaWHtOnN0ntb2lfOf7gvOi9TefUswQ/ZVA==
+ dependencies:
+ sitemap "^8.0.2"
+ stream-replace-string "^2.0.0"
+ zod "^3.25.76"
+
+"@astrojs/svelte@^5.5.0":
+ version "5.7.3"
+ resolved "https://registry.npmjs.org/@astrojs/svelte/-/svelte-5.7.3.tgz"
+ integrity sha512-0PAwn2KLVpGsJppG8dWM1P9+/VrjAdhMWgEgwC1PjY2xFG565bTw7OuGaS5zh5Fu4AcjVoBkVQKjW/N3mLnrJA==
+ dependencies:
+ "@sveltejs/vite-plugin-svelte" "^3.1.2"
+ svelte2tsx "^0.7.22"
+
+"@astrojs/telemetry@3.1.0":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.1.0.tgz"
+ integrity sha512-/ca/+D8MIKEC8/A9cSaPUqQNZm+Es/ZinRv0ZAzvu2ios7POQSsVD+VOj7/hypWNsNM3T7RpfgNq7H2TU1KEHA==
+ dependencies:
+ ci-info "^4.0.0"
+ debug "^4.3.4"
+ dlv "^1.1.3"
+ dset "^3.1.3"
+ is-docker "^3.0.0"
+ is-wsl "^3.0.0"
+ which-pm-runs "^1.1.0"
+
+"@babel/code-frame@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz"
+ integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.27.1"
+ js-tokens "^4.0.0"
+ picocolors "^1.1.1"
+
+"@babel/compat-data@^7.27.2":
+ version "7.28.0"
+ resolved "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz"
+ integrity sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==
+
+"@babel/core@^7.0.0", "@babel/core@^7.0.0-0", "@babel/core@^7.26.0":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz"
+ integrity sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==
+ dependencies:
+ "@ampproject/remapping" "^2.2.0"
+ "@babel/code-frame" "^7.27.1"
+ "@babel/generator" "^7.28.3"
+ "@babel/helper-compilation-targets" "^7.27.2"
+ "@babel/helper-module-transforms" "^7.28.3"
+ "@babel/helpers" "^7.28.3"
+ "@babel/parser" "^7.28.3"
+ "@babel/template" "^7.27.2"
+ "@babel/traverse" "^7.28.3"
+ "@babel/types" "^7.28.2"
+ convert-source-map "^2.0.0"
+ debug "^4.1.0"
+ gensync "^1.0.0-beta.2"
+ json5 "^2.2.3"
+ semver "^6.3.1"
+
+"@babel/generator@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz"
+ integrity sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==
+ dependencies:
+ "@babel/parser" "^7.28.3"
+ "@babel/types" "^7.28.2"
+ "@jridgewell/gen-mapping" "^0.3.12"
+ "@jridgewell/trace-mapping" "^0.3.28"
+ jsesc "^3.0.2"
+
+"@babel/helper-annotate-as-pure@^7.27.1":
+ version "7.27.3"
+ resolved "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz"
+ integrity sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==
+ dependencies:
+ "@babel/types" "^7.27.3"
+
+"@babel/helper-compilation-targets@^7.27.2":
+ version "7.27.2"
+ resolved "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz"
+ integrity sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==
+ dependencies:
+ "@babel/compat-data" "^7.27.2"
+ "@babel/helper-validator-option" "^7.27.1"
+ browserslist "^4.24.0"
+ lru-cache "^5.1.1"
+ semver "^6.3.1"
+
+"@babel/helper-globals@^7.28.0":
+ version "7.28.0"
+ resolved "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz"
+ integrity sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==
+
+"@babel/helper-module-imports@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz"
+ integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==
+ dependencies:
+ "@babel/traverse" "^7.27.1"
+ "@babel/types" "^7.27.1"
+
+"@babel/helper-module-transforms@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz"
+ integrity sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==
+ dependencies:
+ "@babel/helper-module-imports" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.27.1"
+ "@babel/traverse" "^7.28.3"
+
+"@babel/helper-plugin-utils@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz"
+ integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==
+
+"@babel/helper-string-parser@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz"
+ integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==
+
+"@babel/helper-validator-identifier@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz"
+ integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==
+
+"@babel/helper-validator-option@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz"
+ integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==
+
+"@babel/helpers@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz"
+ integrity sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==
+ dependencies:
+ "@babel/template" "^7.27.2"
+ "@babel/types" "^7.28.2"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.20.7", "@babel/parser@^7.25.4", "@babel/parser@^7.27.2", "@babel/parser@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz"
+ integrity sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==
+ dependencies:
+ "@babel/types" "^7.28.2"
+
+"@babel/plugin-syntax-jsx@^7.27.1":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz"
+ integrity sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==
+ dependencies:
+ "@babel/helper-plugin-utils" "^7.27.1"
+
+"@babel/plugin-transform-react-jsx@^7.25.9":
+ version "7.27.1"
+ resolved "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz"
+ integrity sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==
+ dependencies:
+ "@babel/helper-annotate-as-pure" "^7.27.1"
+ "@babel/helper-module-imports" "^7.27.1"
+ "@babel/helper-plugin-utils" "^7.27.1"
+ "@babel/plugin-syntax-jsx" "^7.27.1"
+ "@babel/types" "^7.27.1"
+
+"@babel/polyfill@^7.10.1":
+ version "7.12.1"
+ resolved "https://registry.npmjs.org/@babel/polyfill/-/polyfill-7.12.1.tgz"
+ integrity sha512-X0pi0V6gxLi6lFZpGmeNa4zxtwEmCs42isWLNjZZDE0Y8yVfgu0T2OAHlzBbdYlqbW/YXVvoBHpATEM+goCj8g==
+ dependencies:
+ core-js "^2.6.5"
+ regenerator-runtime "^0.13.4"
+
+"@babel/runtime@^7.21.0":
+ version "7.28.6"
+ resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz"
+ integrity sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==
+
+"@babel/template@^7.27.2":
+ version "7.27.2"
+ resolved "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz"
+ integrity sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==
+ dependencies:
+ "@babel/code-frame" "^7.27.1"
+ "@babel/parser" "^7.27.2"
+ "@babel/types" "^7.27.1"
+
+"@babel/traverse@^7.27.1", "@babel/traverse@^7.28.3":
+ version "7.28.3"
+ resolved "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz"
+ integrity sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==
+ dependencies:
+ "@babel/code-frame" "^7.27.1"
+ "@babel/generator" "^7.28.3"
+ "@babel/helper-globals" "^7.28.0"
+ "@babel/parser" "^7.28.3"
+ "@babel/template" "^7.27.2"
+ "@babel/types" "^7.28.2"
+ debug "^4.3.1"
+
+"@babel/types@^7.0.0", "@babel/types@^7.20.7", "@babel/types@^7.25.4", "@babel/types@^7.26.0", "@babel/types@^7.27.1", "@babel/types@^7.27.3", "@babel/types@^7.28.2":
+ version "7.28.2"
+ resolved "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz"
+ integrity sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==
+ dependencies:
+ "@babel/helper-string-parser" "^7.27.1"
+ "@babel/helper-validator-identifier" "^7.27.1"
+
+"@braintree/sanitize-url@^7.1.1":
+ version "7.1.1"
+ resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.1.tgz"
+ integrity sha512-i1L7noDNxtFyL5DmZafWy1wRVhGehQmzZaz1HiN5e7iylJMSZR7ekOV7NsIqa5qBldlLrsKv4HbgFUVlQrz8Mw==
+
+"@chevrotain/cst-dts-gen@11.0.3":
+ version "11.0.3"
+ resolved "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-11.0.3.tgz"
+ integrity sha512-BvIKpRLeS/8UbfxXxgC33xOumsacaeCKAjAeLyOn7Pcp95HiRbrpl14S+9vaZLolnbssPIUuiUd8IvgkRyt6NQ==
+ dependencies:
+ "@chevrotain/gast" "11.0.3"
+ "@chevrotain/types" "11.0.3"
+ lodash-es "4.17.21"
+
+"@chevrotain/gast@11.0.3":
+ version "11.0.3"
+ resolved "https://registry.npmjs.org/@chevrotain/gast/-/gast-11.0.3.tgz"
+ integrity sha512-+qNfcoNk70PyS/uxmj3li5NiECO+2YKZZQMbmjTqRI3Qchu8Hig/Q9vgkHpI3alNjr7M+a2St5pw5w5F6NL5/Q==
+ dependencies:
+ "@chevrotain/types" "11.0.3"
+ lodash-es "4.17.21"
+
+"@chevrotain/regexp-to-ast@11.0.3":
+ version "11.0.3"
+ resolved "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-11.0.3.tgz"
+ integrity sha512-1fMHaBZxLFvWI067AVbGJav1eRY7N8DDvYCTwGBiE/ytKBgP8azTdgyrKyWZ9Mfh09eHWb5PgTSO8wi7U824RA==
+
+"@chevrotain/types@11.0.3":
+ version "11.0.3"
+ resolved "https://registry.npmjs.org/@chevrotain/types/-/types-11.0.3.tgz"
+ integrity sha512-gsiM3G8b58kZC2HaWR50gu6Y1440cHiJ+i3JUvcp/35JchYejb2+5MVeJK0iKThYpAa/P2PYFV4hoi44HD+aHQ==
+
+"@chevrotain/utils@11.0.3":
+ version "11.0.3"
+ resolved "https://registry.npmjs.org/@chevrotain/utils/-/utils-11.0.3.tgz"
+ integrity sha512-YslZMgtJUyuMbZ+aKvfF3x1f5liK4mWNxghFRv7jqRR9C3R3fAOGTTKvxXDa2Y1s9zSbcpuO0cAxDYsc9SrXoQ==
+
+"@citation-js/core@^0.7.0", "@citation-js/core@^0.7.14":
+ version "0.7.18"
+ resolved "https://registry.npmjs.org/@citation-js/core/-/core-0.7.18.tgz"
+ integrity sha512-EjLuZWA5156dIFGdF7OnyPyWFBW43B8Ckje6Sn/W2RFxHDu0oACvW4/6TNgWT80jhEA4bVFm7ahrZe9MJ2B2UQ==
+ dependencies:
+ "@citation-js/date" "^0.5.0"
+ "@citation-js/name" "^0.4.2"
+ fetch-ponyfill "^7.1.0"
+ sync-fetch "^0.4.1"
+
+"@citation-js/date@^0.5.0", "@citation-js/date@^0.5.1":
+ version "0.5.1"
+ resolved "https://registry.npmjs.org/@citation-js/date/-/date-0.5.1.tgz"
+ integrity sha512-1iDKAZ4ie48PVhovsOXQ+C6o55dWJloXqtznnnKy6CltJBQLIuLLuUqa8zlIvma0ZigjVjgDUhnVaNU1MErtZw==
+
+"@citation-js/name@^0.4.2":
+ version "0.4.2"
+ resolved "https://registry.npmjs.org/@citation-js/name/-/name-0.4.2.tgz"
+ integrity sha512-brSPsjs2fOVzSnARLKu0qncn6suWjHVQtrqSUrnqyaRH95r/Ad4wPF5EsoWr+Dx8HzkCGb/ogmoAzfCsqlTwTQ==
+
+"@citation-js/plugin-bibjson@^0.7.14":
+ version "0.7.18"
+ resolved "https://registry.npmjs.org/@citation-js/plugin-bibjson/-/plugin-bibjson-0.7.18.tgz"
+ integrity sha512-i0iFu26QjfNNTaXmW/QRCUH+8LLgn1ZZppWyAnu+cJWbSLDduTumKt4U4AbPARb40Rewy+Ibqj1LPDfUqEUrrQ==
+ dependencies:
+ "@citation-js/date" "^0.5.0"
+ "@citation-js/name" "^0.4.2"
+
+"@citation-js/plugin-bibtex@^0.7.14":
+ version "0.7.18"
+ resolved "https://registry.npmjs.org/@citation-js/plugin-bibtex/-/plugin-bibtex-0.7.18.tgz"
+ integrity sha512-TdsZSMpgpfcx2NMPu0KiulEoecllwT5EtRUzAJl2pDsdPD1tUqqbyj/NBi0l8fwNy1r7WwAqSFGiqGPjQWpFdg==
+ dependencies:
+ "@citation-js/date" "^0.5.0"
+ "@citation-js/name" "^0.4.2"
+ moo "^0.5.1"
+
+"@citation-js/plugin-csl@^0.7.14":
+ version "0.7.18"
+ resolved "https://registry.npmjs.org/@citation-js/plugin-csl/-/plugin-csl-0.7.18.tgz"
+ integrity sha512-cJcOdEZurmtIxNj0d4cOERHpVQJB/mN3YPSDNqfI/xTFRN3bWDpFAsaqubPtMO2ZPpoDS+ZGIP1kggbwCfMmlA==
+ dependencies:
+ "@citation-js/date" "^0.5.0"
+ citeproc "^2.4.6"
+
+"@csstools/cascade-layer-name-parser@^2.0.5":
+ version "2.0.5"
+ resolved "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz"
+ integrity sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==
+
+"@csstools/color-helpers@^5.1.0":
+ version "5.1.0"
+ resolved "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz"
+ integrity sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==
+
+"@csstools/css-calc@^2.1.4":
+ version "2.1.4"
+ resolved "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz"
+ integrity sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==
+
+"@csstools/css-color-parser@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz"
+ integrity sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==
+ dependencies:
+ "@csstools/color-helpers" "^5.1.0"
+ "@csstools/css-calc" "^2.1.4"
+
+"@csstools/css-parser-algorithms@^3.0.5":
+ version "3.0.5"
+ resolved "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz"
+ integrity sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==
+
+"@csstools/css-tokenizer@^3.0.4":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz"
+ integrity sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==
+
+"@csstools/media-query-list-parser@^4.0.3":
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz"
+ integrity sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==
+
+"@csstools/postcss-alpha-function@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.0.tgz"
+ integrity sha512-r2L8KNg5Wriq5n8IUQcjzy2Rh37J5YjzP9iOyHZL5fxdWYHB08vqykHQa4wAzN/tXwDuCHnhQDGCtxfS76xn7g==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-cascade-layers@^5.0.2":
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz"
+ integrity sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==
+ dependencies:
+ "@csstools/selector-specificity" "^5.0.0"
+ postcss-selector-parser "^7.0.0"
+
+"@csstools/postcss-color-function-display-p3-linear@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.0.tgz"
+ integrity sha512-7q+OuUqfowRrP84m/Jl0wv3pfCQyUTCW5MxDIux+/yty5IkUUHOTigCjrC0Fjy3OT0ncGLudHbfLWmP7E1arNA==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-color-function@^4.0.11":
+ version "4.0.11"
+ resolved "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.11.tgz"
+ integrity sha512-AtH22zLHTLm64HLdpv5EedT/zmYTm1MtdQbQhRZXxEB6iYtS6SrS1jLX3TcmUWMFzpumK/OVylCm3HcLms4slw==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-color-mix-function@^3.0.11":
+ version "3.0.11"
+ resolved "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.11.tgz"
+ integrity sha512-cQpXBelpTx0YhScZM5Ve0jDCA4RzwFc7oNafzZOGgCHt/GQVYiU8Vevz9QJcwy/W0Pyi/BneY+KMjz23lI9r+Q==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-color-mix-variadic-function-arguments@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.1.tgz"
+ integrity sha512-c7hyBtbF+jlHIcUGVdWY06bHICgguV9ypfcELU3eU3W/9fiz2dxM8PqxQk2ndXYTzLnwPvNNqu1yCmQ++N6Dcg==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-content-alt-text@^2.0.7":
+ version "2.0.7"
+ resolved "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.7.tgz"
+ integrity sha512-cq/zWaEkpcg3RttJ5+GdNwk26NwxY5KgqgtNL777Fdd28AVGHxuBvqmK4Jq4oKhW1NX4M2LbgYAVVN0NZ+/XYQ==
+ dependencies:
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-exponential-functions@^2.0.9":
+ version "2.0.9"
+ resolved "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz"
+ integrity sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==
+ dependencies:
+ "@csstools/css-calc" "^2.1.4"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+
+"@csstools/postcss-font-format-keywords@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz"
+ integrity sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==
+ dependencies:
+ "@csstools/utilities" "^2.0.0"
+ postcss-value-parser "^4.2.0"
+
+"@csstools/postcss-gamut-mapping@^2.0.11":
+ version "2.0.11"
+ resolved "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz"
+ integrity sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+
+"@csstools/postcss-gradients-interpolation-method@^5.0.11":
+ version "5.0.11"
+ resolved "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.11.tgz"
+ integrity sha512-8M3mcNTL3cGIJXDnvrJ2oWEcKi3zyw7NeYheFKePUlBmLYm1gkw9Rr/BA7lFONrOPeQA3yeMPldrrws6lqHrug==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-hwb-function@^4.0.11":
+ version "4.0.11"
+ resolved "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.11.tgz"
+ integrity sha512-9meZbsVWTZkWsSBazQips3cHUOT29a/UAwFz0AMEXukvpIGGDR9+GMl3nIckWO5sPImsadu4F5Zy+zjt8QgCdA==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-ic-unit@^4.0.3":
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.3.tgz"
+ integrity sha512-RtYYm2qUIu9vAaHB0cC8rQGlOCQAUgEc2tMr7ewlGXYipBQKjoWmyVArqsk7SEr8N3tErq6P6UOJT3amaVof5Q==
+ dependencies:
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+ postcss-value-parser "^4.2.0"
+
+"@csstools/postcss-initial@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz"
+ integrity sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==
+
+"@csstools/postcss-is-pseudo-class@^5.0.3":
+ version "5.0.3"
+ resolved "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz"
+ integrity sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==
+ dependencies:
+ "@csstools/selector-specificity" "^5.0.0"
+ postcss-selector-parser "^7.0.0"
+
+"@csstools/postcss-light-dark-function@^2.0.10":
+ version "2.0.10"
+ resolved "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.10.tgz"
+ integrity sha512-g7Lwb294lSoNnyrwcqoooh9fTAp47rRNo+ILg7SLRSMU3K9ePIwRt566sNx+pehiCelv4E1ICaU1EwLQuyF2qw==
+ dependencies:
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-logical-float-and-clear@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz"
+ integrity sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==
+
+"@csstools/postcss-logical-overflow@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz"
+ integrity sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==
+
+"@csstools/postcss-logical-overscroll-behavior@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz"
+ integrity sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==
+
+"@csstools/postcss-logical-resize@^3.0.0":
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz"
+ integrity sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==
+ dependencies:
+ postcss-value-parser "^4.2.0"
+
+"@csstools/postcss-logical-viewport-units@^3.0.4":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz"
+ integrity sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==
+ dependencies:
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-media-minmax@^2.0.9":
+ version "2.0.9"
+ resolved "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz"
+ integrity sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==
+ dependencies:
+ "@csstools/css-calc" "^2.1.4"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/media-query-list-parser" "^4.0.3"
+
+"@csstools/postcss-media-queries-aspect-ratio-number-values@^3.0.5":
+ version "3.0.5"
+ resolved "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz"
+ integrity sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==
+ dependencies:
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/media-query-list-parser" "^4.0.3"
+
+"@csstools/postcss-nested-calc@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz"
+ integrity sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==
+ dependencies:
+ "@csstools/utilities" "^2.0.0"
+ postcss-value-parser "^4.2.0"
+
+"@csstools/postcss-normalize-display-values@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz"
+ integrity sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==
+ dependencies:
+ postcss-value-parser "^4.2.0"
+
+"@csstools/postcss-oklab-function@^4.0.11":
+ version "4.0.11"
+ resolved "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.11.tgz"
+ integrity sha512-9f03ZGxZ2VmSCrM4SDXlAYP+Xpu4VFzemfQUQFL9OYxAbpvDy0FjDipZ0i8So1pgs8VIbQI0bNjFWgfdpGw8ig==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-progressive-custom-properties@^4.2.0":
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.0.tgz"
+ integrity sha512-fWCXRasX17N1NCPTCuwC3FJDV+Wc031f16cFuuMEfIsYJ1q5ABCa59W0C6VeMGqjNv6ldf37vvwXXAeaZjD9PA==
+ dependencies:
+ postcss-value-parser "^4.2.0"
+
+"@csstools/postcss-random-function@^2.0.1":
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz"
+ integrity sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==
+ dependencies:
+ "@csstools/css-calc" "^2.1.4"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+
+"@csstools/postcss-relative-color-syntax@^3.0.11":
+ version "3.0.11"
+ resolved "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.11.tgz"
+ integrity sha512-oQ5fZvkcBrWR+k6arHXk0F8FlkmD4IxM+rcGDLWrF2f31tWyEM3lSraeWAV0f7BGH6LIrqmyU3+Qo/1acfoJng==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+"@csstools/postcss-scope-pseudo-class@^4.0.1":
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz"
+ integrity sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==
+ dependencies:
+ postcss-selector-parser "^7.0.0"
+
+"@csstools/postcss-sign-functions@^1.1.4":
+ version "1.1.4"
+ resolved "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz"
+ integrity sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==
+ dependencies:
+ "@csstools/css-calc" "^2.1.4"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+
+"@csstools/postcss-stepped-value-functions@^4.0.9":
+ version "4.0.9"
+ resolved "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz"
+ integrity sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==
+ dependencies:
+ "@csstools/css-calc" "^2.1.4"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+
+"@csstools/postcss-text-decoration-shorthand@^4.0.3":
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz"
+ integrity sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==
+ dependencies:
+ "@csstools/color-helpers" "^5.1.0"
+ postcss-value-parser "^4.2.0"
+
+"@csstools/postcss-trigonometric-functions@^4.0.9":
+ version "4.0.9"
+ resolved "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz"
+ integrity sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==
+ dependencies:
+ "@csstools/css-calc" "^2.1.4"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+
+"@csstools/postcss-unset-value@^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz"
+ integrity sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==
+
+"@csstools/selector-resolve-nested@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz"
+ integrity sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==
+
+"@csstools/selector-specificity@^5.0.0":
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz"
+ integrity sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==
+
+"@csstools/utilities@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz"
+ integrity sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==
+
+"@esbuild/darwin-arm64@0.21.5":
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz"
+ integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==
+
+"@iconify/types@^2.0.0":
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz"
+ integrity sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==
+
+"@iconify/utils@^3.0.1":
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/@iconify/utils/-/utils-3.0.2.tgz"
+ integrity sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==
+ dependencies:
+ "@antfu/install-pkg" "^1.1.0"
+ "@antfu/utils" "^9.2.0"
+ "@iconify/types" "^2.0.0"
+ debug "^4.4.1"
+ globals "^15.15.0"
+ kolorist "^1.8.0"
+ local-pkg "^1.1.1"
+ mlly "^1.7.4"
+
+"@img/sharp-darwin-arm64@0.33.5":
+ version "0.33.5"
+ resolved "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz"
+ integrity sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==
+ optionalDependencies:
+ "@img/sharp-libvips-darwin-arm64" "1.0.4"
+
+"@img/sharp-libvips-darwin-arm64@1.0.4":
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz"
+ integrity sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==
+
+"@jridgewell/gen-mapping@^0.3.12", "@jridgewell/gen-mapping@^0.3.5":
+ version "0.3.13"
+ resolved "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz"
+ integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.4.15", "@jridgewell/sourcemap-codec@^1.5.0", "@jridgewell/sourcemap-codec@^1.5.5":
+ version "1.5.5"
+ resolved "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
+ integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
+
+"@jridgewell/trace-mapping@^0.3.18", "@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.28":
+ version "0.3.30"
+ resolved "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz"
+ integrity sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@jsquash/png@^3.1.1":
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/@jsquash/png/-/png-3.1.1.tgz"
+ integrity sha512-C10pc+0H6j0h8fENOfnGOvkXCmvpSQTDGlfGd0sHphZhPSGTyLjIrHba0FaZZdsKqA/wlmhYicUHb92vfZphaw==
+
+"@mdx-js/mdx@^3.1.0":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.0.tgz"
+ integrity sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ "@types/estree-jsx" "^1.0.0"
+ "@types/hast" "^3.0.0"
+ "@types/mdx" "^2.0.0"
+ collapse-white-space "^2.0.0"
+ devlop "^1.0.0"
+ estree-util-is-identifier-name "^3.0.0"
+ estree-util-scope "^1.0.0"
+ estree-walker "^3.0.0"
+ hast-util-to-jsx-runtime "^2.0.0"
+ markdown-extensions "^2.0.0"
+ recma-build-jsx "^1.0.0"
+ recma-jsx "^1.0.0"
+ recma-stringify "^1.0.0"
+ rehype-recma "^1.0.0"
+ remark-mdx "^3.0.0"
+ remark-parse "^11.0.0"
+ remark-rehype "^11.0.0"
+ source-map "^0.7.0"
+ unified "^11.0.0"
+ unist-util-position-from-estree "^2.0.0"
+ unist-util-stringify-position "^4.0.0"
+ unist-util-visit "^5.0.0"
+ vfile "^6.0.0"
+
+"@mermaid-js/parser@^0.6.2":
+ version "0.6.2"
+ resolved "https://registry.npmjs.org/@mermaid-js/parser/-/parser-0.6.2.tgz"
+ integrity sha512-+PO02uGF6L6Cs0Bw8RpGhikVvMWEysfAyl27qTlroUB8jSWr1lL0Sf6zi78ZxlSnmgSY2AMMKVgghnN9jTtwkQ==
+ dependencies:
+ langium "3.3.1"
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@^2.0.2", "@nodelib/fs.stat@2.0.5":
+ version "2.0.5"
+ resolved "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@oslojs/encoding@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz"
+ integrity sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==
+
+"@rollup/pluginutils@^5.1.3":
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.2.0.tgz"
+ integrity sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ estree-walker "^2.0.2"
+ picomatch "^4.0.2"
+
+"@rollup/rollup-darwin-arm64@4.48.1":
+ version "4.48.1"
+ resolved "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.1.tgz"
+ integrity sha512-+XjmyChHfc4TSs6WUQGmVf7Hkg8ferMAE2aNYYWjiLzAS/T62uOsdfnqv+GHRjq7rKRnYh4mwWb4Hz7h/alp8A==
+
+"@shikijs/core@1.29.2":
+ version "1.29.2"
+ resolved "https://registry.npmjs.org/@shikijs/core/-/core-1.29.2.tgz"
+ integrity sha512-vju0lY9r27jJfOY4Z7+Rt/nIOjzJpZ3y+nYpqtUZInVoXQ/TJZcfGnNOGnKjFdVZb8qexiCuSlZRKcGfhhTTZQ==
+ dependencies:
+ "@shikijs/engine-javascript" "1.29.2"
+ "@shikijs/engine-oniguruma" "1.29.2"
+ "@shikijs/types" "1.29.2"
+ "@shikijs/vscode-textmate" "^10.0.1"
+ "@types/hast" "^3.0.4"
+ hast-util-to-html "^9.0.4"
+
+"@shikijs/engine-javascript@1.29.2":
+ version "1.29.2"
+ resolved "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.29.2.tgz"
+ integrity sha512-iNEZv4IrLYPv64Q6k7EPpOCE/nuvGiKl7zxdq0WFuRPF5PAE9PRo2JGq/d8crLusM59BRemJ4eOqrFrC4wiQ+A==
+ dependencies:
+ "@shikijs/types" "1.29.2"
+ "@shikijs/vscode-textmate" "^10.0.1"
+ oniguruma-to-es "^2.2.0"
+
+"@shikijs/engine-oniguruma@1.29.2":
+ version "1.29.2"
+ resolved "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.29.2.tgz"
+ integrity sha512-7iiOx3SG8+g1MnlzZVDYiaeHe7Ez2Kf2HrJzdmGwkRisT7r4rak0e655AcM/tF9JG/kg5fMNYlLLKglbN7gBqA==
+ dependencies:
+ "@shikijs/types" "1.29.2"
+ "@shikijs/vscode-textmate" "^10.0.1"
+
+"@shikijs/langs@1.29.2":
+ version "1.29.2"
+ resolved "https://registry.npmjs.org/@shikijs/langs/-/langs-1.29.2.tgz"
+ integrity sha512-FIBA7N3LZ+223U7cJDUYd5shmciFQlYkFXlkKVaHsCPgfVLiO+e12FmQE6Tf9vuyEsFe3dIl8qGWKXgEHL9wmQ==
+ dependencies:
+ "@shikijs/types" "1.29.2"
+
+"@shikijs/themes@1.29.2":
+ version "1.29.2"
+ resolved "https://registry.npmjs.org/@shikijs/themes/-/themes-1.29.2.tgz"
+ integrity sha512-i9TNZlsq4uoyqSbluIcZkmPL9Bfi3djVxRnofUHwvx/h6SRW3cwgBC5SML7vsDcWyukY0eCzVN980rqP6qNl9g==
+ dependencies:
+ "@shikijs/types" "1.29.2"
+
+"@shikijs/types@1.29.2":
+ version "1.29.2"
+ resolved "https://registry.npmjs.org/@shikijs/types/-/types-1.29.2.tgz"
+ integrity sha512-VJjK0eIijTZf0QSTODEXCqinjBn0joAHQ+aPSBzrv4O2d/QSbsMw+ZeSRx03kV34Hy7NzUvV/7NqfYGRLrASmw==
+ dependencies:
+ "@shikijs/vscode-textmate" "^10.0.1"
+ "@types/hast" "^3.0.4"
+
+"@shikijs/vscode-textmate@^10.0.1":
+ version "10.0.2"
+ resolved "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz"
+ integrity sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==
+
+"@sveltejs/vite-plugin-svelte-inspector@^2.1.0":
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz"
+ integrity sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==
+ dependencies:
+ debug "^4.3.4"
+
+"@sveltejs/vite-plugin-svelte@^3.0.0", "@sveltejs/vite-plugin-svelte@^3.1.2":
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz"
+ integrity sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==
+ dependencies:
+ "@sveltejs/vite-plugin-svelte-inspector" "^2.1.0"
+ debug "^4.3.4"
+ deepmerge "^4.3.1"
+ kleur "^4.1.5"
+ magic-string "^0.30.10"
+ svelte-hmr "^0.16.0"
+ vitefu "^0.2.5"
+
+"@types/babel__core@^7.20.5":
+ version "7.20.5"
+ resolved "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz"
+ integrity sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==
+ dependencies:
+ "@babel/parser" "^7.20.7"
+ "@babel/types" "^7.20.7"
+ "@types/babel__generator" "*"
+ "@types/babel__template" "*"
+ "@types/babel__traverse" "*"
+
+"@types/babel__generator@*":
+ version "7.27.0"
+ resolved "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz"
+ integrity sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==
+ dependencies:
+ "@babel/types" "^7.0.0"
+
+"@types/babel__template@*":
+ version "7.4.4"
+ resolved "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz"
+ integrity sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==
+ dependencies:
+ "@babel/parser" "^7.1.0"
+ "@babel/types" "^7.0.0"
+
+"@types/babel__traverse@*":
+ version "7.28.0"
+ resolved "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz"
+ integrity sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==
+ dependencies:
+ "@babel/types" "^7.28.2"
+
+"@types/cookie@^0.6.0":
+ version "0.6.0"
+ resolved "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz"
+ integrity sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==
+
+"@types/d3-array@*":
+ version "3.2.1"
+ resolved "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz"
+ integrity sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==
+
+"@types/d3-axis@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz"
+ integrity sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-brush@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz"
+ integrity sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-chord@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz"
+ integrity sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==
+
+"@types/d3-color@*":
+ version "3.1.3"
+ resolved "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz"
+ integrity sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==
+
+"@types/d3-contour@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz"
+ integrity sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==
+ dependencies:
+ "@types/d3-array" "*"
+ "@types/geojson" "*"
+
+"@types/d3-delaunay@*":
+ version "6.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz"
+ integrity sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==
+
+"@types/d3-dispatch@*":
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz"
+ integrity sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==
+
+"@types/d3-drag@*":
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz"
+ integrity sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-dsv@*":
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz"
+ integrity sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==
+
+"@types/d3-ease@*":
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz"
+ integrity sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==
+
+"@types/d3-fetch@*":
+ version "3.0.7"
+ resolved "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz"
+ integrity sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==
+ dependencies:
+ "@types/d3-dsv" "*"
+
+"@types/d3-force@*":
+ version "3.0.10"
+ resolved "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz"
+ integrity sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==
+
+"@types/d3-format@*":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz"
+ integrity sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==
+
+"@types/d3-geo@*":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz"
+ integrity sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==
+ dependencies:
+ "@types/geojson" "*"
+
+"@types/d3-hierarchy@*":
+ version "3.1.7"
+ resolved "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz"
+ integrity sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==
+
+"@types/d3-interpolate@*":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz"
+ integrity sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==
+ dependencies:
+ "@types/d3-color" "*"
+
+"@types/d3-path@*":
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz"
+ integrity sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==
+
+"@types/d3-polygon@*":
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz"
+ integrity sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==
+
+"@types/d3-quadtree@*":
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz"
+ integrity sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==
+
+"@types/d3-random@*":
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz"
+ integrity sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==
+
+"@types/d3-scale-chromatic@*":
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz"
+ integrity sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==
+
+"@types/d3-scale@*":
+ version "4.0.9"
+ resolved "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz"
+ integrity sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==
+ dependencies:
+ "@types/d3-time" "*"
+
+"@types/d3-selection@*":
+ version "3.0.11"
+ resolved "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz"
+ integrity sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==
+
+"@types/d3-shape@*":
+ version "3.1.7"
+ resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz"
+ integrity sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==
+ dependencies:
+ "@types/d3-path" "*"
+
+"@types/d3-time-format@*":
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz"
+ integrity sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==
+
+"@types/d3-time@*":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz"
+ integrity sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==
+
+"@types/d3-timer@*":
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz"
+ integrity sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==
+
+"@types/d3-transition@*":
+ version "3.0.9"
+ resolved "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz"
+ integrity sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==
+ dependencies:
+ "@types/d3-selection" "*"
+
+"@types/d3-zoom@*":
+ version "3.0.8"
+ resolved "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz"
+ integrity sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==
+ dependencies:
+ "@types/d3-interpolate" "*"
+ "@types/d3-selection" "*"
+
+"@types/d3@^7.4.3":
+ version "7.4.3"
+ resolved "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz"
+ integrity sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==
+ dependencies:
+ "@types/d3-array" "*"
+ "@types/d3-axis" "*"
+ "@types/d3-brush" "*"
+ "@types/d3-chord" "*"
+ "@types/d3-color" "*"
+ "@types/d3-contour" "*"
+ "@types/d3-delaunay" "*"
+ "@types/d3-dispatch" "*"
+ "@types/d3-drag" "*"
+ "@types/d3-dsv" "*"
+ "@types/d3-ease" "*"
+ "@types/d3-fetch" "*"
+ "@types/d3-force" "*"
+ "@types/d3-format" "*"
+ "@types/d3-geo" "*"
+ "@types/d3-hierarchy" "*"
+ "@types/d3-interpolate" "*"
+ "@types/d3-path" "*"
+ "@types/d3-polygon" "*"
+ "@types/d3-quadtree" "*"
+ "@types/d3-random" "*"
+ "@types/d3-scale" "*"
+ "@types/d3-scale-chromatic" "*"
+ "@types/d3-selection" "*"
+ "@types/d3-shape" "*"
+ "@types/d3-time" "*"
+ "@types/d3-time-format" "*"
+ "@types/d3-timer" "*"
+ "@types/d3-transition" "*"
+ "@types/d3-zoom" "*"
+
+"@types/debug@^4.0.0":
+ version "4.1.12"
+ resolved "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz"
+ integrity sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==
+ dependencies:
+ "@types/ms" "*"
+
+"@types/estree-jsx@^1.0.0":
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz"
+ integrity sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==
+ dependencies:
+ "@types/estree" "*"
+
+"@types/estree@*", "@types/estree@^1.0.0", "@types/estree@^1.0.1", "@types/estree@^1.0.6", "@types/estree@1.0.8":
+ version "1.0.8"
+ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz"
+ integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
+
+"@types/geojson@*":
+ version "7946.0.16"
+ resolved "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz"
+ integrity sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==
+
+"@types/hast@^3.0.0", "@types/hast@^3.0.4":
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz"
+ integrity sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==
+ dependencies:
+ "@types/unist" "*"
+
+"@types/katex@^0.16.0":
+ version "0.16.7"
+ resolved "https://registry.npmjs.org/@types/katex/-/katex-0.16.7.tgz"
+ integrity sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==
+
+"@types/mdast@^3.0.0":
+ version "3.0.15"
+ resolved "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.15.tgz"
+ integrity sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==
+ dependencies:
+ "@types/unist" "^2"
+
+"@types/mdast@^4.0.0":
+ version "4.0.4"
+ resolved "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz"
+ integrity sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==
+ dependencies:
+ "@types/unist" "*"
+
+"@types/mdx@^2.0.0":
+ version "2.0.13"
+ resolved "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz"
+ integrity sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==
+
+"@types/ms@*":
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz"
+ integrity sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==
+
+"@types/nlcst@^2.0.0":
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz"
+ integrity sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==
+ dependencies:
+ "@types/unist" "*"
+
+"@types/node@*", "@types/node@^18.0.0 || >=20.0.0", "@types/node@^25.2.3":
+ version "25.2.3"
+ resolved "https://registry.npmjs.org/@types/node/-/node-25.2.3.tgz"
+ integrity sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ==
+ dependencies:
+ undici-types "~7.16.0"
+
+"@types/node@^17.0.5":
+ version "17.0.45"
+ resolved "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz"
+ integrity sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==
+
+"@types/sax@^1.2.1":
+ version "1.2.7"
+ resolved "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz"
+ integrity sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==
+ dependencies:
+ "@types/node" "*"
+
+"@types/trusted-types@^2.0.7":
+ version "2.0.7"
+ resolved "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz"
+ integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==
+
+"@types/unist@*", "@types/unist@^3.0.0":
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz"
+ integrity sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==
+
+"@types/unist@^2", "@types/unist@^2.0.0":
+ version "2.0.11"
+ resolved "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz"
+ integrity sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==
+
+"@ungap/structured-clone@^1.0.0":
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz"
+ integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==
+
+"@xmldom/xmldom@^0.8.3":
+ version "0.8.11"
+ resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz"
+ integrity sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==
+
+"@zumer/snapdom@^2.0.1":
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/@zumer/snapdom/-/snapdom-2.0.2.tgz"
+ integrity sha512-W6quT4lMcPu8Q9O/Q6witSfc6/+xuY8C8yDoHug/+o7zYKCNE/e0I3//XsWDkyq9C0mDE0TAWF/8bwCR7x3gHQ==
+
+acorn-jsx@^5.0.0:
+ version "5.3.2"
+ resolved "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
+ integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+"acorn@^6.0.0 || ^7.0.0 || ^8.0.0", acorn@^8.0.0, acorn@^8.10.0, acorn@^8.14.0, acorn@^8.15.0, acorn@^8.9.0:
+ version "8.15.0"
+ resolved "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz"
+ integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
+
+ansi-align@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz"
+ integrity sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==
+ dependencies:
+ string-width "^4.1.0"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-regex@^6.0.1:
+ version "6.2.0"
+ resolved "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz"
+ integrity sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==
+
+ansi-styles@^6.2.1:
+ version "6.2.1"
+ resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
+ integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
+
+arg@^5.0.0:
+ version "5.0.2"
+ resolved "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz"
+ integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
+
+argparse@^1.0.7:
+ version "1.0.10"
+ resolved "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz"
+ integrity sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==
+ dependencies:
+ sprintf-js "~1.0.2"
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+aria-query@^5.3.0, aria-query@^5.3.2:
+ version "5.3.2"
+ resolved "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz"
+ integrity sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==
+
+array-iterate@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/array-iterate/-/array-iterate-2.0.1.tgz"
+ integrity sha512-I1jXZMjAgCMmxT4qxXfPXa6SthSoE8h6gkSI9BGGNv8mP8G/v0blc+qFnZu6K42vTOiuME596QaLO0TP3Lk0xg==
+
+astring@^1.8.0:
+ version "1.9.0"
+ resolved "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz"
+ integrity sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==
+
+astro-compressor@^0.4.1:
+ version "0.4.1"
+ resolved "https://registry.npmjs.org/astro-compressor/-/astro-compressor-0.4.1.tgz"
+ integrity sha512-IAgYAlxYRMolaptsFB9quH7RxX4eUeC4UfULCPhT+ILcg7m1kbaxuyUKqGg9Hh+KZ+FNPPDAz7DqUfl+HDWhQg==
+
+astro-mermaid@^1.0.4:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/astro-mermaid/-/astro-mermaid-1.1.0.tgz"
+ integrity sha512-eW5aqOISq2Uf8goCedSl12d0qy8y77A+jRwKxwYc7LvrEN9HiCQuwTGu3zQvi0mB4+ydwaueaYbFUFSCIs2jrA==
+ dependencies:
+ import-meta-resolve "^4.2.0"
+ mdast-util-to-string "^4.0.0"
+ unist-util-visit "^5.0.0"
+
+astro@^4.0.0, "astro@^4.0.0 || ^5.0.0", astro@^4.10.0, astro@^4.8.0:
+ version "4.16.19"
+ resolved "https://registry.npmjs.org/astro/-/astro-4.16.19.tgz"
+ integrity sha512-baeSswPC5ZYvhGDoj25L2FuzKRWMgx105FetOPQVJFMCAp0o08OonYC7AhwsFdhvp7GapqjnC1Fe3lKb2lupYw==
+ dependencies:
+ "@astrojs/compiler" "^2.10.3"
+ "@astrojs/internal-helpers" "0.4.1"
+ "@astrojs/markdown-remark" "5.3.0"
+ "@astrojs/telemetry" "3.1.0"
+ "@babel/core" "^7.26.0"
+ "@babel/plugin-transform-react-jsx" "^7.25.9"
+ "@babel/types" "^7.26.0"
+ "@oslojs/encoding" "^1.1.0"
+ "@rollup/pluginutils" "^5.1.3"
+ "@types/babel__core" "^7.20.5"
+ "@types/cookie" "^0.6.0"
+ acorn "^8.14.0"
+ aria-query "^5.3.2"
+ axobject-query "^4.1.0"
+ boxen "8.0.1"
+ ci-info "^4.1.0"
+ clsx "^2.1.1"
+ common-ancestor-path "^1.0.1"
+ cookie "^0.7.2"
+ cssesc "^3.0.0"
+ debug "^4.3.7"
+ deterministic-object-hash "^2.0.2"
+ devalue "^5.1.1"
+ diff "^5.2.0"
+ dlv "^1.1.3"
+ dset "^3.1.4"
+ es-module-lexer "^1.5.4"
+ esbuild "^0.21.5"
+ estree-walker "^3.0.3"
+ fast-glob "^3.3.2"
+ flattie "^1.1.1"
+ github-slugger "^2.0.0"
+ gray-matter "^4.0.3"
+ html-escaper "^3.0.3"
+ http-cache-semantics "^4.1.1"
+ js-yaml "^4.1.0"
+ kleur "^4.1.5"
+ magic-string "^0.30.14"
+ magicast "^0.3.5"
+ micromatch "^4.0.8"
+ mrmime "^2.0.0"
+ neotraverse "^0.6.18"
+ ora "^8.1.1"
+ p-limit "^6.1.0"
+ p-queue "^8.0.1"
+ preferred-pm "^4.0.0"
+ prompts "^2.4.2"
+ rehype "^13.0.2"
+ semver "^7.6.3"
+ shiki "^1.23.1"
+ tinyexec "^0.3.1"
+ tsconfck "^3.1.4"
+ unist-util-visit "^5.0.0"
+ vfile "^6.0.3"
+ vite "^5.4.11"
+ vitefu "^1.0.4"
+ which-pm "^3.0.0"
+ xxhash-wasm "^1.1.0"
+ yargs-parser "^21.1.1"
+ zod "^3.23.8"
+ zod-to-json-schema "^3.23.5"
+ zod-to-ts "^1.2.0"
+ optionalDependencies:
+ sharp "^0.33.3"
+
+autoprefixer@^10.4.21:
+ version "10.4.21"
+ resolved "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz"
+ integrity sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==
+ dependencies:
+ browserslist "^4.24.4"
+ caniuse-lite "^1.0.30001702"
+ fraction.js "^4.3.7"
+ normalize-range "^0.1.2"
+ picocolors "^1.1.1"
+ postcss-value-parser "^4.2.0"
+
+axobject-query@^4.0.0, axobject-query@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz"
+ integrity sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==
+
+bail@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz"
+ integrity sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==
+
+base-64@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz"
+ integrity sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==
+
+base64-arraybuffer@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz"
+ integrity sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==
+
+base64-js@^1.3.1:
+ version "1.5.1"
+ resolved "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz"
+ integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==
+
+boxen@8.0.1:
+ version "8.0.1"
+ resolved "https://registry.npmjs.org/boxen/-/boxen-8.0.1.tgz"
+ integrity sha512-F3PH5k5juxom4xktynS7MoFY+NUWH5LC4CnH11YB8NPew+HLpmBLCybSAEyb2F+4pRXhuhWqFesoQd6DAyc2hw==
+ dependencies:
+ ansi-align "^3.0.1"
+ camelcase "^8.0.0"
+ chalk "^5.3.0"
+ cli-boxes "^3.0.0"
+ string-width "^7.2.0"
+ type-fest "^4.21.0"
+ widest-line "^5.0.0"
+ wrap-ansi "^9.0.0"
+
+braces@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+browserslist@^4.24.0, browserslist@^4.24.4, browserslist@^4.25.1, "browserslist@>= 4.21.0":
+ version "4.25.3"
+ resolved "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz"
+ integrity sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==
+ dependencies:
+ caniuse-lite "^1.0.30001735"
+ electron-to-chromium "^1.5.204"
+ node-releases "^2.0.19"
+ update-browserslist-db "^1.1.3"
+
+buffer-crc32@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-1.0.0.tgz"
+ integrity sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==
+
+buffer@^5.7.1:
+ version "5.7.1"
+ resolved "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz"
+ integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.1.13"
+
+buffer@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz"
+ integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==
+ dependencies:
+ base64-js "^1.3.1"
+ ieee754 "^1.2.1"
+
+camelcase@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.npmjs.org/camelcase/-/camelcase-8.0.0.tgz"
+ integrity sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==
+
+caniuse-lite@^1.0.30001702, caniuse-lite@^1.0.30001735:
+ version "1.0.30001737"
+ resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz"
+ integrity sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==
+
+ccount@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz"
+ integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==
+
+chalk@^5.3.0:
+ version "5.6.0"
+ resolved "https://registry.npmjs.org/chalk/-/chalk-5.6.0.tgz"
+ integrity sha512-46QrSQFyVSEyYAgQ22hQ+zDa60YHA4fBstHmtSApj1Y5vKtG27fWowW03jCk5KcbXEWPZUIR894aARCA/G1kfQ==
+
+character-entities-html4@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz"
+ integrity sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==
+
+character-entities-legacy@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz"
+ integrity sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==
+
+character-entities@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz"
+ integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
+
+character-reference-invalid@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz"
+ integrity sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==
+
+chevrotain-allstar@~0.3.0:
+ version "0.3.1"
+ resolved "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.3.1.tgz"
+ integrity sha512-b7g+y9A0v4mxCW1qUhf3BSVPg+/NvGErk/dOkrDaHA0nQIQGAtrOjlX//9OQtRlSCy+x9rfB5N8yC71lH1nvMw==
+ dependencies:
+ lodash-es "^4.17.21"
+
+chevrotain@^11.0.0, chevrotain@~11.0.3:
+ version "11.0.3"
+ resolved "https://registry.npmjs.org/chevrotain/-/chevrotain-11.0.3.tgz"
+ integrity sha512-ci2iJH6LeIkvP9eJW6gpueU8cnZhv85ELY8w8WiFtNjMHA5ad6pQLaJo9mEly/9qUyCpvqX8/POVUTf18/HFdw==
+ dependencies:
+ "@chevrotain/cst-dts-gen" "11.0.3"
+ "@chevrotain/gast" "11.0.3"
+ "@chevrotain/regexp-to-ast" "11.0.3"
+ "@chevrotain/types" "11.0.3"
+ "@chevrotain/utils" "11.0.3"
+ lodash-es "4.17.21"
+
+ci-info@^4.0.0, ci-info@^4.1.0:
+ version "4.3.0"
+ resolved "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz"
+ integrity sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==
+
+citeproc@^2.4.6, citeproc@^2.4.63:
+ version "2.4.63"
+ resolved "https://registry.npmjs.org/citeproc/-/citeproc-2.4.63.tgz"
+ integrity sha512-68F95Bp4UbgZU/DBUGQn0qV3HDZLCdI9+Bb2ByrTaNJDL5VEm9LqaiNaxljsvoaExSLEXe1/r6n2Z06SCzW3/Q==
+
+clear-cut@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/clear-cut/-/clear-cut-2.0.2.tgz"
+ integrity sha512-WVgn/gSejQ+0aoR8ucbKIdo6icduPZW6AbWwyUmAUgxy63rUYjwa5rj/HeoNPhf0/XPrl82X8bO/hwBkSmsFtg==
+
+cli-boxes@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz"
+ integrity sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==
+
+cli-cursor@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz"
+ integrity sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==
+ dependencies:
+ restore-cursor "^5.0.0"
+
+cli-spinners@^2.9.2:
+ version "2.9.2"
+ resolved "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz"
+ integrity sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==
+
+clsx@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz"
+ integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
+
+code-red@^1.0.3:
+ version "1.0.4"
+ resolved "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz"
+ integrity sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.4.15"
+ "@types/estree" "^1.0.1"
+ acorn "^8.10.0"
+ estree-walker "^3.0.3"
+ periscopic "^3.1.0"
+
+collapse-white-space@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz"
+ integrity sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-convert@~0.5.0:
+ version "0.5.3"
+ resolved "https://registry.npmjs.org/color-convert/-/color-convert-0.5.3.tgz"
+ integrity sha512-RwBeO/B/vZR3dfKL1ye/vx8MHZ40ugzpyfeVG5GsiuGnrlMWe2o8wxBbLCpw9CsxV+wHuzYlCiWnybrIA0ling==
+
+color-diff@^1.1.0:
+ version "1.4.0"
+ resolved "https://registry.npmjs.org/color-diff/-/color-diff-1.4.0.tgz"
+ integrity sha512-4oDB/o78lNdppbaqrg0HjOp7pHmUc+dfCxWKWFnQg6AB/1dkjtBDop3RZht5386cq9xBUDRvDvSCA7WUlM9Jqw==
+
+color-name@^1.0.0, color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+color-string@^1.9.0:
+ version "1.9.1"
+ resolved "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz"
+ integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
+ dependencies:
+ color-name "^1.0.0"
+ simple-swizzle "^0.2.2"
+
+color@^4.2.3:
+ version "4.2.3"
+ resolved "https://registry.npmjs.org/color/-/color-4.2.3.tgz"
+ integrity sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==
+ dependencies:
+ color-convert "^2.0.1"
+ color-string "^1.9.0"
+
+comma-separated-tokens@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz"
+ integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==
+
+commander@^8.3.0:
+ version "8.3.0"
+ resolved "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz"
+ integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
+
+commander@7:
+ version "7.2.0"
+ resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
+ integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
+
+common-ancestor-path@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-1.0.1.tgz"
+ integrity sha512-L3sHRo1pXXEqX8VU28kfgUY+YGsk09hPqZiZmLacNib6XNTCM8ubYeT7ryXQw8asB1sKgcU5lkB7ONug08aB8w==
+
+confbox@^0.1.8:
+ version "0.1.8"
+ resolved "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz"
+ integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==
+
+confbox@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz"
+ integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==
+
+convert-source-map@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz"
+ integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
+
+cookie@^0.7.2:
+ version "0.7.2"
+ resolved "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz"
+ integrity sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==
+
+core-js@^2.6.5:
+ version "2.6.12"
+ resolved "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz"
+ integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
+
+core-util-is@~1.0.0:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz"
+ integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
+cose-base@^1.0.0:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz"
+ integrity sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==
+ dependencies:
+ layout-base "^1.0.0"
+
+cose-base@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz"
+ integrity sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==
+ dependencies:
+ layout-base "^2.0.0"
+
+cross-fetch@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.1.0.tgz"
+ integrity sha512-uKm5PU+MHTootlWEY+mZ4vvXoCn4fLQxT9dSc1sXVMSFkINTJVN8cAQROpwcKm8bJ/c7rgZVIBWzH5T78sNZZw==
+ dependencies:
+ node-fetch "^2.7.0"
+
+css-blank-pseudo@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz"
+ integrity sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==
+ dependencies:
+ postcss-selector-parser "^7.0.0"
+
+css-has-pseudo@^7.0.3:
+ version "7.0.3"
+ resolved "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz"
+ integrity sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==
+ dependencies:
+ "@csstools/selector-specificity" "^5.0.0"
+ postcss-selector-parser "^7.0.0"
+ postcss-value-parser "^4.2.0"
+
+css-line-break@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz"
+ integrity sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==
+ dependencies:
+ utrie "^1.0.2"
+
+css-prefers-color-scheme@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz"
+ integrity sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==
+
+css-tree@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz"
+ integrity sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==
+ dependencies:
+ mdn-data "2.0.14"
+ source-map "^0.6.1"
+
+css-tree@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz"
+ integrity sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==
+ dependencies:
+ mdn-data "2.0.30"
+ source-map-js "^1.0.1"
+
+cssdb@^8.4.0:
+ version "8.4.0"
+ resolved "https://registry.npmjs.org/cssdb/-/cssdb-8.4.0.tgz"
+ integrity sha512-lyATYGyvXwQ8h55WeQeEHXhI+47rl52pXSYkFK/ZrCbAJSgVIaPFjYc3RM8TpRHKk7W3wsAZImmLps+P5VyN9g==
+
+cssesc@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz"
+ integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+cytoscape-cose-bilkent@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz"
+ integrity sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==
+ dependencies:
+ cose-base "^1.0.0"
+
+cytoscape-fcose@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz"
+ integrity sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==
+ dependencies:
+ cose-base "^2.2.0"
+
+cytoscape@^3.2.0, cytoscape@^3.29.3:
+ version "3.33.1"
+ resolved "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.1.tgz"
+ integrity sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==
+
+d@^1.0.1, d@^1.0.2, d@1:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/d/-/d-1.0.2.tgz"
+ integrity sha512-MOqHvMWF9/9MX6nza0KgvFH4HpMU0EF5uUDXqX/BtxtU8NfB0QzRtJ8Oe/6SuS4kbhyzVJwjd97EA4PKrzJ8bw==
+ dependencies:
+ es5-ext "^0.10.64"
+ type "^2.7.2"
+
+d3-array@^3.2.0, "d3-array@2 - 3", "d3-array@2.10.0 - 3", "d3-array@2.5.0 - 3", d3-array@3:
+ version "3.2.4"
+ resolved "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz"
+ integrity sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==
+ dependencies:
+ internmap "1 - 2"
+
+"d3-array@1 - 2":
+ version "2.12.1"
+ resolved "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz"
+ integrity sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==
+ dependencies:
+ internmap "^1.0.0"
+
+d3-axis@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz"
+ integrity sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==
+
+d3-brush@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz"
+ integrity sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "3"
+ d3-transition "3"
+
+d3-chord@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz"
+ integrity sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==
+ dependencies:
+ d3-path "1 - 3"
+
+"d3-color@1 - 3", d3-color@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz"
+ integrity sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==
+
+d3-contour@4:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz"
+ integrity sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==
+ dependencies:
+ d3-array "^3.2.0"
+
+d3-delaunay@6:
+ version "6.0.4"
+ resolved "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz"
+ integrity sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==
+ dependencies:
+ delaunator "5"
+
+"d3-dispatch@1 - 3", d3-dispatch@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz"
+ integrity sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==
+
+"d3-drag@2 - 3", d3-drag@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz"
+ integrity sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-selection "3"
+
+"d3-dsv@1 - 3", d3-dsv@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz"
+ integrity sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==
+ dependencies:
+ commander "7"
+ iconv-lite "0.6"
+ rw "1"
+
+"d3-ease@1 - 3", d3-ease@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz"
+ integrity sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==
+
+d3-fetch@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz"
+ integrity sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==
+ dependencies:
+ d3-dsv "1 - 3"
+
+d3-force@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz"
+ integrity sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-quadtree "1 - 3"
+ d3-timer "1 - 3"
+
+"d3-format@1 - 3", d3-format@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz"
+ integrity sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==
+
+d3-geo@3:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz"
+ integrity sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==
+ dependencies:
+ d3-array "2.5.0 - 3"
+
+d3-hierarchy@3:
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz"
+ integrity sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==
+
+"d3-interpolate@1 - 3", "d3-interpolate@1.2.0 - 3", d3-interpolate@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz"
+ integrity sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==
+ dependencies:
+ d3-color "1 - 3"
+
+d3-path@^3.1.0, "d3-path@1 - 3", d3-path@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz"
+ integrity sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==
+
+d3-path@1:
+ version "1.0.9"
+ resolved "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz"
+ integrity sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==
+
+d3-polygon@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz"
+ integrity sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==
+
+"d3-quadtree@1 - 3", d3-quadtree@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz"
+ integrity sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==
+
+d3-random@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz"
+ integrity sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==
+
+d3-sankey@^0.12.3:
+ version "0.12.3"
+ resolved "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz"
+ integrity sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==
+ dependencies:
+ d3-array "1 - 2"
+ d3-shape "^1.2.0"
+
+d3-scale-chromatic@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz"
+ integrity sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==
+ dependencies:
+ d3-color "1 - 3"
+ d3-interpolate "1 - 3"
+
+d3-scale@4:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz"
+ integrity sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==
+ dependencies:
+ d3-array "2.10.0 - 3"
+ d3-format "1 - 3"
+ d3-interpolate "1.2.0 - 3"
+ d3-time "2.1.1 - 3"
+ d3-time-format "2 - 4"
+
+"d3-selection@2 - 3", d3-selection@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz"
+ integrity sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==
+
+d3-shape@^1.2.0:
+ version "1.3.7"
+ resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz"
+ integrity sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==
+ dependencies:
+ d3-path "1"
+
+d3-shape@3:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz"
+ integrity sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==
+ dependencies:
+ d3-path "^3.1.0"
+
+"d3-time-format@2 - 4", d3-time-format@4:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz"
+ integrity sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==
+ dependencies:
+ d3-time "1 - 3"
+
+"d3-time@1 - 3", "d3-time@2.1.1 - 3", d3-time@3:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz"
+ integrity sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==
+ dependencies:
+ d3-array "2 - 3"
+
+"d3-timer@1 - 3", d3-timer@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz"
+ integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
+
+"d3-transition@2 - 3", d3-transition@3:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz"
+ integrity sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==
+ dependencies:
+ d3-color "1 - 3"
+ d3-dispatch "1 - 3"
+ d3-ease "1 - 3"
+ d3-interpolate "1 - 3"
+ d3-timer "1 - 3"
+
+d3-zoom@3:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz"
+ integrity sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==
+ dependencies:
+ d3-dispatch "1 - 3"
+ d3-drag "2 - 3"
+ d3-interpolate "1 - 3"
+ d3-selection "2 - 3"
+ d3-transition "2 - 3"
+
+d3@^7.9.0:
+ version "7.9.0"
+ resolved "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz"
+ integrity sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==
+ dependencies:
+ d3-array "3"
+ d3-axis "3"
+ d3-brush "3"
+ d3-chord "3"
+ d3-color "3"
+ d3-contour "4"
+ d3-delaunay "6"
+ d3-dispatch "3"
+ d3-drag "3"
+ d3-dsv "3"
+ d3-ease "3"
+ d3-fetch "3"
+ d3-force "3"
+ d3-format "3"
+ d3-geo "3"
+ d3-hierarchy "3"
+ d3-interpolate "3"
+ d3-path "3"
+ d3-polygon "3"
+ d3-quadtree "3"
+ d3-random "3"
+ d3-scale "4"
+ d3-scale-chromatic "3"
+ d3-selection "3"
+ d3-shape "3"
+ d3-time "3"
+ d3-time-format "4"
+ d3-timer "3"
+ d3-transition "3"
+ d3-zoom "3"
+
+dagre-d3-es@7.0.11:
+ version "7.0.11"
+ resolved "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.11.tgz"
+ integrity sha512-tvlJLyQf834SylNKax8Wkzco/1ias1OPw8DcUMDE7oUIoSEW25riQVuiu/0OWEFqT0cxHT3Pa9/D82Jr47IONw==
+ dependencies:
+ d3 "^7.9.0"
+ lodash-es "^4.17.21"
+
+dayjs@^1.11.18:
+ version "1.11.18"
+ resolved "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz"
+ integrity sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==
+
+debug@^4.0.0, debug@^4.1.0, debug@^4.3.1, debug@^4.3.4, debug@^4.3.7, debug@^4.4.1:
+ version "4.4.1"
+ resolved "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz"
+ integrity sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==
+ dependencies:
+ ms "^2.1.3"
+
+decode-named-character-reference@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz"
+ integrity sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==
+ dependencies:
+ character-entities "^2.0.0"
+
+dedent-js@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/dedent-js/-/dedent-js-1.0.1.tgz"
+ integrity sha512-OUepMozQULMLUmhxS95Vudo0jb0UchLimi3+pQ2plj61Fcy8axbP9hbiD4Sz6DPqn6XG3kfmziVfQ1rSys5AJQ==
+
+deepmerge@^4.3.1:
+ version "4.3.1"
+ resolved "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz"
+ integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
+
+delaunator@5:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/delaunator/-/delaunator-5.0.1.tgz"
+ integrity sha512-8nvh+XBe96aCESrGOqMp/84b13H9cdKbG5P2ejQCh4d4sK9RL4371qou9drQjMhvnPmhWl5hnmqbEE0fXr9Xnw==
+ dependencies:
+ robust-predicates "^3.0.2"
+
+dequal@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz"
+ integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
+
+detect-libc@^2.0.3:
+ version "2.0.4"
+ resolved "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz"
+ integrity sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==
+
+deterministic-object-hash@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/deterministic-object-hash/-/deterministic-object-hash-2.0.2.tgz"
+ integrity sha512-KxektNH63SrbfUyDiwXqRb1rLwKt33AmMv+5Nhsw1kqZ13SJBRTgZHtGbE+hH3a1mVW1cz+4pqSWVPAtLVXTzQ==
+ dependencies:
+ base-64 "^1.0.0"
+
+devalue@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/devalue/-/devalue-5.1.1.tgz"
+ integrity sha512-maua5KUiapvEwiEAe+XnlZ3Rh0GD+qI1J/nb9vrJc3muPXvcF/8gXYTWF76+5DAqHyDUtOIImEuo0YKE9mshVw==
+
+devlop@^1.0.0, devlop@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz"
+ integrity sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==
+ dependencies:
+ dequal "^2.0.0"
+
+diff@^5.0.0, diff@^5.2.0:
+ version "5.2.0"
+ resolved "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz"
+ integrity sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==
+
+dlv@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz"
+ integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
+
+docx@^9.5.1:
+ version "9.5.3"
+ resolved "https://registry.npmjs.org/docx/-/docx-9.5.3.tgz"
+ integrity sha512-uFVrYiN2WKx1an884SS6mRu4JuCO10fpnRGQLYmbYgMLVAqqjrKPc2qMXsGj9JuDVrzdntGRw+y84bSjBxXqew==
+ dependencies:
+ "@types/node" "^25.2.3"
+ hash.js "^1.1.7"
+ jszip "^3.10.1"
+ nanoid "^5.1.3"
+ xml "^1.0.1"
+ xml-js "^1.6.8"
+
+dompurify@^3.2.5:
+ version "3.2.6"
+ resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz"
+ integrity sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==
+ optionalDependencies:
+ "@types/trusted-types" "^2.0.7"
+
+dset@^3.1.3, dset@^3.1.4:
+ version "3.1.4"
+ resolved "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz"
+ integrity sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==
+
+electron-to-chromium@^1.5.204:
+ version "1.5.208"
+ resolved "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.208.tgz"
+ integrity sha512-ozZyibehoe7tOhNaf16lKmljVf+3npZcJIEbJRVftVsmAg5TeA1mGS9dVCZzOwr2xT7xK15V0p7+GZqSPgkuPg==
+
+emoji-regex-xs@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-1.0.0.tgz"
+ integrity sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==
+
+emoji-regex@^10.3.0:
+ version "10.4.0"
+ resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz"
+ integrity sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+entities@^6.0.0:
+ version "6.0.1"
+ resolved "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz"
+ integrity sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==
+
+es-module-lexer@^1.5.4:
+ version "1.7.0"
+ resolved "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz"
+ integrity sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==
+
+es5-ext@^0.10.35, es5-ext@^0.10.62, es5-ext@^0.10.64, es5-ext@~0.10.14:
+ version "0.10.64"
+ resolved "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.64.tgz"
+ integrity sha512-p2snDhiLaXe6dahss1LddxqEm+SkuDvV8dnIQG0MWjyHpcMNfXKPE+/Cc0y+PhxJX3A4xGNeFCj5oc0BUh6deg==
+ dependencies:
+ es6-iterator "^2.0.3"
+ es6-symbol "^3.1.3"
+ esniff "^2.0.1"
+ next-tick "^1.1.0"
+
+es6-iterator@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz"
+ integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==
+ dependencies:
+ d "1"
+ es5-ext "^0.10.35"
+ es6-symbol "^3.1.1"
+
+es6-symbol@^3.1.1, es6-symbol@^3.1.3:
+ version "3.1.4"
+ resolved "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.4.tgz"
+ integrity sha512-U9bFFjX8tFiATgtkJ1zg25+KviIXpgRvRHS8sau3GfhVzThRQrOeksPeT0BWW2MNZs1OEWJ1DPXOQMn0KKRkvg==
+ dependencies:
+ d "^1.0.2"
+ ext "^1.7.0"
+
+esast-util-from-estree@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz"
+ integrity sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==
+ dependencies:
+ "@types/estree-jsx" "^1.0.0"
+ devlop "^1.0.0"
+ estree-util-visit "^2.0.0"
+ unist-util-position-from-estree "^2.0.0"
+
+esast-util-from-js@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz"
+ integrity sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==
+ dependencies:
+ "@types/estree-jsx" "^1.0.0"
+ acorn "^8.0.0"
+ esast-util-from-estree "^2.0.0"
+ vfile-message "^4.0.0"
+
+esbuild@^0.21.3, esbuild@^0.21.5:
+ version "0.21.5"
+ resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz"
+ integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.21.5"
+ "@esbuild/android-arm" "0.21.5"
+ "@esbuild/android-arm64" "0.21.5"
+ "@esbuild/android-x64" "0.21.5"
+ "@esbuild/darwin-arm64" "0.21.5"
+ "@esbuild/darwin-x64" "0.21.5"
+ "@esbuild/freebsd-arm64" "0.21.5"
+ "@esbuild/freebsd-x64" "0.21.5"
+ "@esbuild/linux-arm" "0.21.5"
+ "@esbuild/linux-arm64" "0.21.5"
+ "@esbuild/linux-ia32" "0.21.5"
+ "@esbuild/linux-loong64" "0.21.5"
+ "@esbuild/linux-mips64el" "0.21.5"
+ "@esbuild/linux-ppc64" "0.21.5"
+ "@esbuild/linux-riscv64" "0.21.5"
+ "@esbuild/linux-s390x" "0.21.5"
+ "@esbuild/linux-x64" "0.21.5"
+ "@esbuild/netbsd-x64" "0.21.5"
+ "@esbuild/openbsd-x64" "0.21.5"
+ "@esbuild/sunos-x64" "0.21.5"
+ "@esbuild/win32-arm64" "0.21.5"
+ "@esbuild/win32-ia32" "0.21.5"
+ "@esbuild/win32-x64" "0.21.5"
+
+escalade@^3.2.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
+ integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==
+
+escape-string-regexp@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz"
+ integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==
+
+esniff@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/esniff/-/esniff-2.0.1.tgz"
+ integrity sha512-kTUIGKQ/mDPFoJ0oVfcmyJn4iBDRptjNVIzwIFR7tqWXdVI9xfA2RMwY/gbSpJG3lkdWNEjLap/NqVHZiJsdfg==
+ dependencies:
+ d "^1.0.1"
+ es5-ext "^0.10.62"
+ event-emitter "^0.3.5"
+ type "^2.7.2"
+
+esprima@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz"
+ integrity sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==
+
+estree-util-attach-comments@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz"
+ integrity sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==
+ dependencies:
+ "@types/estree" "^1.0.0"
+
+estree-util-build-jsx@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz"
+ integrity sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==
+ dependencies:
+ "@types/estree-jsx" "^1.0.0"
+ devlop "^1.0.0"
+ estree-util-is-identifier-name "^3.0.0"
+ estree-walker "^3.0.0"
+
+estree-util-is-identifier-name@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz"
+ integrity sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==
+
+estree-util-scope@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz"
+ integrity sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ devlop "^1.0.0"
+
+estree-util-to-js@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz"
+ integrity sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==
+ dependencies:
+ "@types/estree-jsx" "^1.0.0"
+ astring "^1.8.0"
+ source-map "^0.7.0"
+
+estree-util-visit@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz"
+ integrity sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==
+ dependencies:
+ "@types/estree-jsx" "^1.0.0"
+ "@types/unist" "^3.0.0"
+
+estree-walker@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz"
+ integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==
+
+estree-walker@^3.0.0, estree-walker@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz"
+ integrity sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==
+ dependencies:
+ "@types/estree" "^1.0.0"
+
+event-emitter@^0.3.5:
+ version "0.3.5"
+ resolved "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz"
+ integrity sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==
+ dependencies:
+ d "1"
+ es5-ext "~0.10.14"
+
+eventemitter3@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz"
+ integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==
+
+exsolve@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz"
+ integrity sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==
+
+ext@^1.7.0:
+ version "1.7.0"
+ resolved "https://registry.npmjs.org/ext/-/ext-1.7.0.tgz"
+ integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw==
+ dependencies:
+ type "^2.7.2"
+
+extend-shallow@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz"
+ integrity sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==
+ dependencies:
+ is-extendable "^0.1.0"
+
+extend@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz"
+ integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
+fast-glob@^3.3.2:
+ version "3.3.3"
+ resolved "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz"
+ integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.8"
+
+fastq@^1.6.0:
+ version "1.19.1"
+ resolved "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz"
+ integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
+ dependencies:
+ reusify "^1.0.4"
+
+fetch-ponyfill@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.npmjs.org/fetch-ponyfill/-/fetch-ponyfill-7.1.0.tgz"
+ integrity sha512-FhbbL55dj/qdVO3YNK7ZEkshvj3eQ7EuIGV2I6ic/2YiocvyWv+7jg2s4AyS0wdRU75s3tA8ZxI/xPigb0v5Aw==
+ dependencies:
+ node-fetch "~2.6.1"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up-simple@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz"
+ integrity sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==
+
+find-up@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz"
+ integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+ dependencies:
+ locate-path "^5.0.0"
+ path-exists "^4.0.0"
+
+find-yarn-workspace-root2@1.2.16:
+ version "1.2.16"
+ resolved "https://registry.npmjs.org/find-yarn-workspace-root2/-/find-yarn-workspace-root2-1.2.16.tgz"
+ integrity sha512-hr6hb1w8ePMpPVUK39S4RlwJzi+xPLuVuG8XlwXU3KD5Yn3qgBWVfy3AzNlDhWvE1EORCE65/Qm26rFQt3VLVA==
+ dependencies:
+ micromatch "^4.0.2"
+ pkg-dir "^4.2.0"
+
+flattie@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz"
+ integrity sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==
+
+fonteditor-core@^2.6.3:
+ version "2.6.3"
+ resolved "https://registry.npmjs.org/fonteditor-core/-/fonteditor-core-2.6.3.tgz"
+ integrity sha512-YUryIKjkenjZ41E7JvM3V+02Ak4mTHDDTwBWgs9KBzypzHqLZHuua1UDRevZNTKawmnq1dbBAa70Jddl2+F4FQ==
+ dependencies:
+ "@xmldom/xmldom" "^0.8.3"
+
+fraction.js@^4.3.7:
+ version "4.3.7"
+ resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
+ integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+fsevents@2.3.2:
+ version "2.3.2"
+ resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz"
+ integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==
+
+gensync@^1.0.0-beta.2:
+ version "1.0.0-beta.2"
+ resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz"
+ integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
+
+get-east-asian-width@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz"
+ integrity sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==
+
+github-slugger@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz"
+ integrity sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==
+
+glob-parent@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+globals@^15.15.0:
+ version "15.15.0"
+ resolved "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz"
+ integrity sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==
+
+graceful-fs@^4.1.5:
+ version "4.2.11"
+ resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz"
+ integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
+
+gray-matter@^4.0.3:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz"
+ integrity sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==
+ dependencies:
+ js-yaml "^3.13.1"
+ kind-of "^6.0.2"
+ section-matter "^1.0.0"
+ strip-bom-string "^1.0.0"
+
+hachure-fill@^0.5.2:
+ version "0.5.2"
+ resolved "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz"
+ integrity sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==
+
+hash.js@^1.1.7:
+ version "1.1.7"
+ resolved "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz"
+ integrity sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==
+ dependencies:
+ inherits "^2.0.3"
+ minimalistic-assert "^1.0.1"
+
+hast-util-from-dom@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz"
+ integrity sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hastscript "^9.0.0"
+ web-namespaces "^2.0.0"
+
+hast-util-from-html-isomorphic@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz"
+ integrity sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hast-util-from-dom "^5.0.0"
+ hast-util-from-html "^2.0.0"
+ unist-util-remove-position "^5.0.0"
+
+hast-util-from-html@^2.0.0, hast-util-from-html@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz"
+ integrity sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ devlop "^1.1.0"
+ hast-util-from-parse5 "^8.0.0"
+ parse5 "^7.0.0"
+ vfile "^6.0.0"
+ vfile-message "^4.0.0"
+
+hast-util-from-parse5@^8.0.0, hast-util-from-parse5@^8.0.1:
+ version "8.0.3"
+ resolved "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz"
+ integrity sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ devlop "^1.0.0"
+ hastscript "^9.0.0"
+ property-information "^7.0.0"
+ vfile "^6.0.0"
+ vfile-location "^5.0.0"
+ web-namespaces "^2.0.0"
+
+hast-util-heading-rank@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz"
+ integrity sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
+hast-util-is-element@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz"
+ integrity sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
+hast-util-parse-selector@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz"
+ integrity sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
+hast-util-raw@^9.0.0:
+ version "9.1.0"
+ resolved "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz"
+ integrity sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ "@ungap/structured-clone" "^1.0.0"
+ hast-util-from-parse5 "^8.0.0"
+ hast-util-to-parse5 "^8.0.0"
+ html-void-elements "^3.0.0"
+ mdast-util-to-hast "^13.0.0"
+ parse5 "^7.0.0"
+ unist-util-position "^5.0.0"
+ unist-util-visit "^5.0.0"
+ vfile "^6.0.0"
+ web-namespaces "^2.0.0"
+ zwitch "^2.0.0"
+
+hast-util-to-estree@^3.0.0:
+ version "3.1.3"
+ resolved "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz"
+ integrity sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ "@types/estree-jsx" "^1.0.0"
+ "@types/hast" "^3.0.0"
+ comma-separated-tokens "^2.0.0"
+ devlop "^1.0.0"
+ estree-util-attach-comments "^3.0.0"
+ estree-util-is-identifier-name "^3.0.0"
+ hast-util-whitespace "^3.0.0"
+ mdast-util-mdx-expression "^2.0.0"
+ mdast-util-mdx-jsx "^3.0.0"
+ mdast-util-mdxjs-esm "^2.0.0"
+ property-information "^7.0.0"
+ space-separated-tokens "^2.0.0"
+ style-to-js "^1.0.0"
+ unist-util-position "^5.0.0"
+ zwitch "^2.0.0"
+
+hast-util-to-html@^9.0.0, hast-util-to-html@^9.0.3, hast-util-to-html@^9.0.4:
+ version "9.0.5"
+ resolved "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz"
+ integrity sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ ccount "^2.0.0"
+ comma-separated-tokens "^2.0.0"
+ hast-util-whitespace "^3.0.0"
+ html-void-elements "^3.0.0"
+ mdast-util-to-hast "^13.0.0"
+ property-information "^7.0.0"
+ space-separated-tokens "^2.0.0"
+ stringify-entities "^4.0.0"
+ zwitch "^2.0.4"
+
+hast-util-to-jsx-runtime@^2.0.0:
+ version "2.3.6"
+ resolved "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz"
+ integrity sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ comma-separated-tokens "^2.0.0"
+ devlop "^1.0.0"
+ estree-util-is-identifier-name "^3.0.0"
+ hast-util-whitespace "^3.0.0"
+ mdast-util-mdx-expression "^2.0.0"
+ mdast-util-mdx-jsx "^3.0.0"
+ mdast-util-mdxjs-esm "^2.0.0"
+ property-information "^7.0.0"
+ space-separated-tokens "^2.0.0"
+ style-to-js "^1.0.0"
+ unist-util-position "^5.0.0"
+ vfile-message "^4.0.0"
+
+hast-util-to-parse5@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz"
+ integrity sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ comma-separated-tokens "^2.0.0"
+ devlop "^1.0.0"
+ property-information "^6.0.0"
+ space-separated-tokens "^2.0.0"
+ web-namespaces "^2.0.0"
+ zwitch "^2.0.0"
+
+hast-util-to-string@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz"
+ integrity sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
+hast-util-to-text@^4.0.0, hast-util-to-text@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz"
+ integrity sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/unist" "^3.0.0"
+ hast-util-is-element "^3.0.0"
+ unist-util-find-after "^5.0.0"
+
+hast-util-whitespace@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz"
+ integrity sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+
+hastscript@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz"
+ integrity sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ comma-separated-tokens "^2.0.0"
+ hast-util-parse-selector "^4.0.0"
+ property-information "^7.0.0"
+ space-separated-tokens "^2.0.0"
+
+html-escaper@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz"
+ integrity sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==
+
+html-void-elements@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz"
+ integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==
+
+html2canvas@^1.4.1:
+ version "1.4.1"
+ resolved "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz"
+ integrity sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==
+ dependencies:
+ css-line-break "^2.1.0"
+ text-segmentation "^1.0.3"
+
+http-cache-semantics@^4.1.1:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz"
+ integrity sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==
+
+iconv-lite@0.6:
+ version "0.6.3"
+ resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz"
+ integrity sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==
+ dependencies:
+ safer-buffer ">= 2.1.2 < 3.0.0"
+
+ieee754@^1.1.13, ieee754@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz"
+ integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==
+
+immediate@~3.0.5:
+ version "3.0.6"
+ resolved "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz"
+ integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
+import-meta-resolve@^4.1.0, import-meta-resolve@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz"
+ integrity sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==
+
+inherits@^2.0.3, inherits@~2.0.3, inherits@~2.0.4:
+ version "2.0.4"
+ resolved "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz"
+ integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
+inline-style-parser@0.2.4:
+ version "0.2.4"
+ resolved "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz"
+ integrity sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==
+
+internmap@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz"
+ integrity sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==
+
+"internmap@1 - 2":
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz"
+ integrity sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==
+
+is-alphabetical@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz"
+ integrity sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==
+
+is-alphanumerical@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz"
+ integrity sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==
+ dependencies:
+ is-alphabetical "^2.0.0"
+ is-decimal "^2.0.0"
+
+is-arrayish@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz"
+ integrity sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==
+
+is-buffer@^2.0.0:
+ version "2.0.5"
+ resolved "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz"
+ integrity sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==
+
+is-decimal@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz"
+ integrity sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==
+
+is-docker@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz"
+ integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==
+
+is-extendable@^0.1.0:
+ version "0.1.1"
+ resolved "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz"
+ integrity sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.1:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-hexadecimal@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz"
+ integrity sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==
+
+is-inside-container@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz"
+ integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==
+ dependencies:
+ is-docker "^3.0.0"
+
+is-interactive@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz"
+ integrity sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+is-plain-obj@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz"
+ integrity sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==
+
+is-reference@^3.0.0, is-reference@^3.0.1:
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz"
+ integrity sha512-ixkJoqQvAP88E6wLydLGGqCJsrFUnqoH6HnaczB8XmDH1oaWU+xxdptvikTgaEhtZ53Ky6YXiBuUI2WXLMCwjw==
+ dependencies:
+ "@types/estree" "^1.0.6"
+
+is-unicode-supported@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz"
+ integrity sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==
+
+is-unicode-supported@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz"
+ integrity sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==
+
+is-wsl@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz"
+ integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==
+ dependencies:
+ is-inside-container "^1.0.0"
+
+isarray@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz"
+ integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
+js-tokens@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^3.13.0:
+ version "3.14.1"
+ resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz"
+ integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+js-yaml@^3.13.1:
+ version "3.14.1"
+ resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz"
+ integrity sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==
+ dependencies:
+ argparse "^1.0.7"
+ esprima "^4.0.0"
+
+js-yaml@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ dependencies:
+ argparse "^2.0.1"
+
+jsesc@^3.0.2:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz"
+ integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==
+
+json5@^2.2.3:
+ version "2.2.3"
+ resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz"
+ integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==
+
+jszip@^3.10.1:
+ version "3.10.1"
+ resolved "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz"
+ integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
+ dependencies:
+ lie "~3.3.0"
+ pako "~1.0.2"
+ readable-stream "~2.3.6"
+ setimmediate "^1.0.5"
+
+katex@^0.16.0, katex@^0.16.22:
+ version "0.16.22"
+ resolved "https://registry.npmjs.org/katex/-/katex-0.16.22.tgz"
+ integrity sha512-XCHRdUw4lf3SKBaJe4EvgqIuWwkPSo9XoeO8GjQW94Bp7TWv9hNhzZjZ+OH9yf1UmLygb7DIT5GSFQiyt16zYg==
+ dependencies:
+ commander "^8.3.0"
+
+khroma@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz"
+ integrity sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==
+
+kind-of@^6.0.0, kind-of@^6.0.2:
+ version "6.0.3"
+ resolved "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz"
+ integrity sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==
+
+kleur@^3.0.3:
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz"
+ integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==
+
+kleur@^4.0.3, kleur@^4.1.5:
+ version "4.1.5"
+ resolved "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz"
+ integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==
+
+kolorist@^1.8.0:
+ version "1.8.0"
+ resolved "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz"
+ integrity sha512-Y+60/zizpJ3HRH8DCss+q95yr6145JXZo46OTpFvDZWLfRCE4qChOyk1b26nMaNpfHHgxagk9dXT5OP0Tfe+dQ==
+
+langium@3.3.1:
+ version "3.3.1"
+ resolved "https://registry.npmjs.org/langium/-/langium-3.3.1.tgz"
+ integrity sha512-QJv/h939gDpvT+9SiLVlY7tZC3xB2qK57v0J04Sh9wpMb6MP1q8gB21L3WIo8T5P1MSMg3Ep14L7KkDCFG3y4w==
+ dependencies:
+ chevrotain "~11.0.3"
+ chevrotain-allstar "~0.3.0"
+ vscode-languageserver "~9.0.1"
+ vscode-languageserver-textdocument "~1.0.11"
+ vscode-uri "~3.0.8"
+
+layout-base@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz"
+ integrity sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==
+
+layout-base@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz"
+ integrity sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==
+
+lie@~3.3.0:
+ version "3.3.0"
+ resolved "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz"
+ integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
+ dependencies:
+ immediate "~3.0.5"
+
+load-yaml-file@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmjs.org/load-yaml-file/-/load-yaml-file-0.2.0.tgz"
+ integrity sha512-OfCBkGEw4nN6JLtgRidPX6QxjBQGQf72q3si2uvqyFEMbycSFFHwAZeXx6cJgFM9wmLrf9zBwCP3Ivqa+LLZPw==
+ dependencies:
+ graceful-fs "^4.1.5"
+ js-yaml "^3.13.0"
+ pify "^4.0.1"
+ strip-bom "^3.0.0"
+
+local-pkg@^1.1.1:
+ version "1.1.2"
+ resolved "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.2.tgz"
+ integrity sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==
+ dependencies:
+ mlly "^1.7.4"
+ pkg-types "^2.3.0"
+ quansync "^0.2.11"
+
+locate-character@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz"
+ integrity sha512-SW13ws7BjaeJ6p7Q6CO2nchbYEc3X3J6WrmTTDto7yMPqVSZTUyY5Tjbid+Ab8gLnATtygYtiDIJGQRRn2ZOiA==
+
+locate-path@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz"
+ integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+ dependencies:
+ p-locate "^4.1.0"
+
+lodash-es@^4.17.21, lodash-es@4.17.21:
+ version "4.17.21"
+ resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
+ integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
+
+log-symbols@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz"
+ integrity sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==
+ dependencies:
+ chalk "^5.3.0"
+ is-unicode-supported "^1.3.0"
+
+longest-streak@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz"
+ integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==
+
+looks-same@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.npmjs.org/looks-same/-/looks-same-10.0.1.tgz"
+ integrity sha512-MHTGwSbifNjQIKkjAr9aOCDxg+E06wtdLBdSmjJSnFxl7u5rkQqTfx+FYK+Fv6sK2MKJFlBaTYN6WOLdm5JSGA==
+ dependencies:
+ "@jsquash/png" "^3.1.1"
+ buffer-crc32 "^1.0.0"
+ color-diff "^1.1.0"
+ nested-error-stacks "^2.1.0"
+ parse-color "^1.0.0"
+
+lower-case@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz"
+ integrity sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==
+ dependencies:
+ tslib "^2.0.3"
+
+lru-cache@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz"
+ integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==
+ dependencies:
+ yallist "^3.0.2"
+
+magic-string@^0.30.10, magic-string@^0.30.14, magic-string@^0.30.4:
+ version "0.30.18"
+ resolved "https://registry.npmjs.org/magic-string/-/magic-string-0.30.18.tgz"
+ integrity sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.5"
+
+magicast@^0.3.5:
+ version "0.3.5"
+ resolved "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz"
+ integrity sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==
+ dependencies:
+ "@babel/parser" "^7.25.4"
+ "@babel/types" "^7.25.4"
+ source-map-js "^1.2.0"
+
+markdown-extensions@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz"
+ integrity sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==
+
+markdown-table@^3.0.0:
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz"
+ integrity sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==
+
+marked@^16.2.1:
+ version "16.4.1"
+ resolved "https://registry.npmjs.org/marked/-/marked-16.4.1.tgz"
+ integrity sha512-ntROs7RaN3EvWfy3EZi14H4YxmT6A5YvywfhO+0pm+cH/dnSQRmdAmoFIc3B9aiwTehyk7pESH4ofyBY+V5hZg==
+
+mdast-util-definitions@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz"
+ integrity sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ "@types/unist" "^3.0.0"
+ unist-util-visit "^5.0.0"
+
+mdast-util-directive@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz"
+ integrity sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ "@types/unist" "^3.0.0"
+ ccount "^2.0.0"
+ devlop "^1.0.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+ parse-entities "^4.0.0"
+ stringify-entities "^4.0.0"
+ unist-util-visit-parents "^6.0.0"
+
+mdast-util-find-and-replace@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz"
+ integrity sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ escape-string-regexp "^5.0.0"
+ unist-util-is "^6.0.0"
+ unist-util-visit-parents "^6.0.0"
+
+mdast-util-footnote@^1.0.0:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/mdast-util-footnote/-/mdast-util-footnote-1.1.1.tgz"
+ integrity sha512-Y8JiA5fZm0kCqvugmSfaSvaVU2trhAmhCYXJOgPKVfmDa8isqzb5Tf7uWvCq4s8I+dll09GZjboItwZFSsD4JQ==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ mdast-util-to-markdown "^1.0.0"
+ micromark-util-normalize-identifier "^1.0.0"
+
+mdast-util-from-markdown@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz"
+ integrity sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ "@types/unist" "^3.0.0"
+ decode-named-character-reference "^1.0.0"
+ devlop "^1.0.0"
+ mdast-util-to-string "^4.0.0"
+ micromark "^4.0.0"
+ micromark-util-decode-numeric-character-reference "^2.0.0"
+ micromark-util-decode-string "^2.0.0"
+ micromark-util-normalize-identifier "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+ unist-util-stringify-position "^4.0.0"
+
+mdast-util-gfm-autolink-literal@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz"
+ integrity sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ ccount "^2.0.0"
+ devlop "^1.0.0"
+ mdast-util-find-and-replace "^3.0.0"
+ micromark-util-character "^2.0.0"
+
+mdast-util-gfm-footnote@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz"
+ integrity sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ devlop "^1.1.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+ micromark-util-normalize-identifier "^2.0.0"
+
+mdast-util-gfm-strikethrough@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz"
+ integrity sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+
+mdast-util-gfm-table@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz"
+ integrity sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ devlop "^1.0.0"
+ markdown-table "^3.0.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+
+mdast-util-gfm-task-list-item@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz"
+ integrity sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ devlop "^1.0.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+
+mdast-util-gfm@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz"
+ integrity sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==
+ dependencies:
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-gfm-autolink-literal "^2.0.0"
+ mdast-util-gfm-footnote "^2.0.0"
+ mdast-util-gfm-strikethrough "^2.0.0"
+ mdast-util-gfm-table "^2.0.0"
+ mdast-util-gfm-task-list-item "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+
+mdast-util-math@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/mdast-util-math/-/mdast-util-math-3.0.0.tgz"
+ integrity sha512-Tl9GBNeG/AhJnQM221bJR2HPvLOSnLE/T9cJI9tlc6zwQk2nPk/4f0cHkOdEixQPC/j8UtKDdITswvLAy1OZ1w==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/mdast" "^4.0.0"
+ devlop "^1.0.0"
+ longest-streak "^3.0.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.1.0"
+ unist-util-remove-position "^5.0.0"
+
+mdast-util-mdx-expression@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz"
+ integrity sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==
+ dependencies:
+ "@types/estree-jsx" "^1.0.0"
+ "@types/hast" "^3.0.0"
+ "@types/mdast" "^4.0.0"
+ devlop "^1.0.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+
+mdast-util-mdx-jsx@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz"
+ integrity sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==
+ dependencies:
+ "@types/estree-jsx" "^1.0.0"
+ "@types/hast" "^3.0.0"
+ "@types/mdast" "^4.0.0"
+ "@types/unist" "^3.0.0"
+ ccount "^2.0.0"
+ devlop "^1.1.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+ parse-entities "^4.0.0"
+ stringify-entities "^4.0.0"
+ unist-util-stringify-position "^4.0.0"
+ vfile-message "^4.0.0"
+
+mdast-util-mdx@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz"
+ integrity sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==
+ dependencies:
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-mdx-expression "^2.0.0"
+ mdast-util-mdx-jsx "^3.0.0"
+ mdast-util-mdxjs-esm "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+
+mdast-util-mdxjs-esm@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz"
+ integrity sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==
+ dependencies:
+ "@types/estree-jsx" "^1.0.0"
+ "@types/hast" "^3.0.0"
+ "@types/mdast" "^4.0.0"
+ devlop "^1.0.0"
+ mdast-util-from-markdown "^2.0.0"
+ mdast-util-to-markdown "^2.0.0"
+
+mdast-util-phrasing@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz"
+ integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ unist-util-is "^5.0.0"
+
+mdast-util-phrasing@^4.0.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz"
+ integrity sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ unist-util-is "^6.0.0"
+
+mdast-util-to-hast@^13.0.0:
+ version "13.2.0"
+ resolved "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz"
+ integrity sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/mdast" "^4.0.0"
+ "@ungap/structured-clone" "^1.0.0"
+ devlop "^1.0.0"
+ micromark-util-sanitize-uri "^2.0.0"
+ trim-lines "^3.0.0"
+ unist-util-position "^5.0.0"
+ unist-util-visit "^5.0.0"
+ vfile "^6.0.0"
+
+mdast-util-to-markdown@^1.0.0:
+ version "1.5.0"
+ resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz"
+ integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ "@types/unist" "^2.0.0"
+ longest-streak "^3.0.0"
+ mdast-util-phrasing "^3.0.0"
+ mdast-util-to-string "^3.0.0"
+ micromark-util-decode-string "^1.0.0"
+ unist-util-visit "^4.0.0"
+ zwitch "^2.0.0"
+
+mdast-util-to-markdown@^2.0.0, mdast-util-to-markdown@^2.1.0:
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz"
+ integrity sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ "@types/unist" "^3.0.0"
+ longest-streak "^3.0.0"
+ mdast-util-phrasing "^4.0.0"
+ mdast-util-to-string "^4.0.0"
+ micromark-util-classify-character "^2.0.0"
+ micromark-util-decode-string "^2.0.0"
+ unist-util-visit "^5.0.0"
+ zwitch "^2.0.0"
+
+mdast-util-to-string@^3.0.0:
+ version "3.2.0"
+ resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz"
+ integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+
+mdast-util-to-string@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz"
+ integrity sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+
+mdn-data@2.0.14:
+ version "2.0.14"
+ resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz"
+ integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
+
+mdn-data@2.0.30:
+ version "2.0.30"
+ resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz"
+ integrity sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==
+
+merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+"mermaid@^10.0.0 || ^11.0.0", mermaid@^11.10.1:
+ version "11.12.0"
+ resolved "https://registry.npmjs.org/mermaid/-/mermaid-11.12.0.tgz"
+ integrity sha512-ZudVx73BwrMJfCFmSSJT84y6u5brEoV8DOItdHomNLz32uBjNrelm7mg95X7g+C6UoQH/W6mBLGDEDv73JdxBg==
+ dependencies:
+ "@braintree/sanitize-url" "^7.1.1"
+ "@iconify/utils" "^3.0.1"
+ "@mermaid-js/parser" "^0.6.2"
+ "@types/d3" "^7.4.3"
+ cytoscape "^3.29.3"
+ cytoscape-cose-bilkent "^4.1.0"
+ cytoscape-fcose "^2.2.0"
+ d3 "^7.9.0"
+ d3-sankey "^0.12.3"
+ dagre-d3-es "7.0.11"
+ dayjs "^1.11.18"
+ dompurify "^3.2.5"
+ katex "^0.16.22"
+ khroma "^2.1.0"
+ lodash-es "^4.17.21"
+ marked "^16.2.1"
+ roughjs "^4.6.6"
+ stylis "^4.3.6"
+ ts-dedent "^2.2.0"
+ uuid "^11.1.0"
+
+micromark-core-commonmark@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz"
+ integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw==
+ dependencies:
+ decode-named-character-reference "^1.0.0"
+ micromark-factory-destination "^1.0.0"
+ micromark-factory-label "^1.0.0"
+ micromark-factory-space "^1.0.0"
+ micromark-factory-title "^1.0.0"
+ micromark-factory-whitespace "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-chunked "^1.0.0"
+ micromark-util-classify-character "^1.0.0"
+ micromark-util-html-tag-name "^1.0.0"
+ micromark-util-normalize-identifier "^1.0.0"
+ micromark-util-resolve-all "^1.0.0"
+ micromark-util-subtokenize "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.1"
+ uvu "^0.5.0"
+
+micromark-core-commonmark@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz"
+ integrity sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==
+ dependencies:
+ decode-named-character-reference "^1.0.0"
+ devlop "^1.0.0"
+ micromark-factory-destination "^2.0.0"
+ micromark-factory-label "^2.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-factory-title "^2.0.0"
+ micromark-factory-whitespace "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-chunked "^2.0.0"
+ micromark-util-classify-character "^2.0.0"
+ micromark-util-html-tag-name "^2.0.0"
+ micromark-util-normalize-identifier "^2.0.0"
+ micromark-util-resolve-all "^2.0.0"
+ micromark-util-subtokenize "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-directive@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz"
+ integrity sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==
+ dependencies:
+ devlop "^1.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-factory-whitespace "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+ parse-entities "^4.0.0"
+
+micromark-extension-footnote@^1.0.0:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/micromark-extension-footnote/-/micromark-extension-footnote-1.0.2.tgz"
+ integrity sha512-sD7t/hooONLnbPYgcQpBcTjh7mPEcD2R/jRS5DLYDNm0XbngbaJZ01F1aI3v6VXVMdAu3XtFKUecNRlXbgGk/Q==
+ dependencies:
+ micromark-core-commonmark "^1.0.0"
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-chunked "^1.0.0"
+ micromark-util-normalize-identifier "^1.0.0"
+ micromark-util-resolve-all "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ uvu "^0.5.0"
+
+micromark-extension-gfm-autolink-literal@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz"
+ integrity sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==
+ dependencies:
+ micromark-util-character "^2.0.0"
+ micromark-util-sanitize-uri "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-gfm-footnote@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz"
+ integrity sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==
+ dependencies:
+ devlop "^1.0.0"
+ micromark-core-commonmark "^2.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-normalize-identifier "^2.0.0"
+ micromark-util-sanitize-uri "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-gfm-strikethrough@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz"
+ integrity sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==
+ dependencies:
+ devlop "^1.0.0"
+ micromark-util-chunked "^2.0.0"
+ micromark-util-classify-character "^2.0.0"
+ micromark-util-resolve-all "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-gfm-table@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz"
+ integrity sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==
+ dependencies:
+ devlop "^1.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-gfm-tagfilter@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz"
+ integrity sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==
+ dependencies:
+ micromark-util-types "^2.0.0"
+
+micromark-extension-gfm-task-list-item@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz"
+ integrity sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==
+ dependencies:
+ devlop "^1.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-gfm@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz"
+ integrity sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==
+ dependencies:
+ micromark-extension-gfm-autolink-literal "^2.0.0"
+ micromark-extension-gfm-footnote "^2.0.0"
+ micromark-extension-gfm-strikethrough "^2.0.0"
+ micromark-extension-gfm-table "^2.0.0"
+ micromark-extension-gfm-tagfilter "^2.0.0"
+ micromark-extension-gfm-task-list-item "^2.0.0"
+ micromark-util-combine-extensions "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-math@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/micromark-extension-math/-/micromark-extension-math-3.1.0.tgz"
+ integrity sha512-lvEqd+fHjATVs+2v/8kg9i5Q0AP2k85H0WUOwpIVvUML8BapsMvh1XAogmQjOCsLpoKRCVQqEkQBB3NhVBcsOg==
+ dependencies:
+ "@types/katex" "^0.16.0"
+ devlop "^1.0.0"
+ katex "^0.16.0"
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-mdx-expression@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz"
+ integrity sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ devlop "^1.0.0"
+ micromark-factory-mdx-expression "^2.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-events-to-acorn "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-extension-mdx-jsx@^3.0.0:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz"
+ integrity sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ devlop "^1.0.0"
+ estree-util-is-identifier-name "^3.0.0"
+ micromark-factory-mdx-expression "^2.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-events-to-acorn "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+ vfile-message "^4.0.0"
+
+micromark-extension-mdx-md@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz"
+ integrity sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==
+ dependencies:
+ micromark-util-types "^2.0.0"
+
+micromark-extension-mdxjs-esm@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz"
+ integrity sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ devlop "^1.0.0"
+ micromark-core-commonmark "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-events-to-acorn "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+ unist-util-position-from-estree "^2.0.0"
+ vfile-message "^4.0.0"
+
+micromark-extension-mdxjs@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz"
+ integrity sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==
+ dependencies:
+ acorn "^8.0.0"
+ acorn-jsx "^5.0.0"
+ micromark-extension-mdx-expression "^3.0.0"
+ micromark-extension-mdx-jsx "^3.0.0"
+ micromark-extension-mdx-md "^2.0.0"
+ micromark-extension-mdxjs-esm "^3.0.0"
+ micromark-util-combine-extensions "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-factory-destination@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz"
+ integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
+micromark-factory-destination@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz"
+ integrity sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==
+ dependencies:
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-factory-label@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz"
+ integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ uvu "^0.5.0"
+
+micromark-factory-label@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz"
+ integrity sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==
+ dependencies:
+ devlop "^1.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-factory-mdx-expression@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz"
+ integrity sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ devlop "^1.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-events-to-acorn "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+ unist-util-position-from-estree "^2.0.0"
+ vfile-message "^4.0.0"
+
+micromark-factory-space@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz"
+ integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-types "^1.0.0"
+
+micromark-factory-space@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz"
+ integrity sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==
+ dependencies:
+ micromark-util-character "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-factory-title@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz"
+ integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ==
+ dependencies:
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
+micromark-factory-title@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz"
+ integrity sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==
+ dependencies:
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-factory-whitespace@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz"
+ integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ==
+ dependencies:
+ micromark-factory-space "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
+micromark-factory-whitespace@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz"
+ integrity sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==
+ dependencies:
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-util-character@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz"
+ integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==
+ dependencies:
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
+micromark-util-character@^2.0.0:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz"
+ integrity sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==
+ dependencies:
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-util-chunked@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz"
+ integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ==
+ dependencies:
+ micromark-util-symbol "^1.0.0"
+
+micromark-util-chunked@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz"
+ integrity sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==
+ dependencies:
+ micromark-util-symbol "^2.0.0"
+
+micromark-util-classify-character@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz"
+ integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw==
+ dependencies:
+ micromark-util-character "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+
+micromark-util-classify-character@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz"
+ integrity sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==
+ dependencies:
+ micromark-util-character "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-util-combine-extensions@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz"
+ integrity sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==
+ dependencies:
+ micromark-util-chunked "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-util-decode-numeric-character-reference@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz"
+ integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw==
+ dependencies:
+ micromark-util-symbol "^1.0.0"
+
+micromark-util-decode-numeric-character-reference@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz"
+ integrity sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==
+ dependencies:
+ micromark-util-symbol "^2.0.0"
+
+micromark-util-decode-string@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz"
+ integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ==
+ dependencies:
+ decode-named-character-reference "^1.0.0"
+ micromark-util-character "^1.0.0"
+ micromark-util-decode-numeric-character-reference "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+
+micromark-util-decode-string@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz"
+ integrity sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==
+ dependencies:
+ decode-named-character-reference "^1.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-decode-numeric-character-reference "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+
+micromark-util-encode@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz"
+ integrity sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==
+
+micromark-util-events-to-acorn@^2.0.0:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz"
+ integrity sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ "@types/unist" "^3.0.0"
+ devlop "^1.0.0"
+ estree-util-visit "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+ vfile-message "^4.0.0"
+
+micromark-util-html-tag-name@^1.0.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz"
+ integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q==
+
+micromark-util-html-tag-name@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz"
+ integrity sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==
+
+micromark-util-normalize-identifier@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz"
+ integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q==
+ dependencies:
+ micromark-util-symbol "^1.0.0"
+
+micromark-util-normalize-identifier@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz"
+ integrity sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==
+ dependencies:
+ micromark-util-symbol "^2.0.0"
+
+micromark-util-resolve-all@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz"
+ integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA==
+ dependencies:
+ micromark-util-types "^1.0.0"
+
+micromark-util-resolve-all@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz"
+ integrity sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==
+ dependencies:
+ micromark-util-types "^2.0.0"
+
+micromark-util-sanitize-uri@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz"
+ integrity sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==
+ dependencies:
+ micromark-util-character "^2.0.0"
+ micromark-util-encode "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+
+micromark-util-subtokenize@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz"
+ integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A==
+ dependencies:
+ micromark-util-chunked "^1.0.0"
+ micromark-util-symbol "^1.0.0"
+ micromark-util-types "^1.0.0"
+ uvu "^0.5.0"
+
+micromark-util-subtokenize@^2.0.0:
+ version "2.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz"
+ integrity sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==
+ dependencies:
+ devlop "^1.0.0"
+ micromark-util-chunked "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromark-util-symbol@^1.0.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz"
+ integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==
+
+micromark-util-symbol@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz"
+ integrity sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==
+
+micromark-util-types@^1.0.0, micromark-util-types@^1.0.1:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz"
+ integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==
+
+micromark-util-types@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz"
+ integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==
+
+micromark@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz"
+ integrity sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==
+ dependencies:
+ "@types/debug" "^4.0.0"
+ debug "^4.0.0"
+ decode-named-character-reference "^1.0.0"
+ devlop "^1.0.0"
+ micromark-core-commonmark "^2.0.0"
+ micromark-factory-space "^2.0.0"
+ micromark-util-character "^2.0.0"
+ micromark-util-chunked "^2.0.0"
+ micromark-util-combine-extensions "^2.0.0"
+ micromark-util-decode-numeric-character-reference "^2.0.0"
+ micromark-util-encode "^2.0.0"
+ micromark-util-normalize-identifier "^2.0.0"
+ micromark-util-resolve-all "^2.0.0"
+ micromark-util-sanitize-uri "^2.0.0"
+ micromark-util-subtokenize "^2.0.0"
+ micromark-util-symbol "^2.0.0"
+ micromark-util-types "^2.0.0"
+
+micromatch@^4.0.2, micromatch@^4.0.8:
+ version "4.0.8"
+ resolved "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+ dependencies:
+ braces "^3.0.3"
+ picomatch "^2.3.1"
+
+mimic-function@^5.0.0:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz"
+ integrity sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==
+
+minimalistic-assert@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz"
+ integrity sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==
+
+mlly@^1.7.4:
+ version "1.8.0"
+ resolved "https://registry.npmjs.org/mlly/-/mlly-1.8.0.tgz"
+ integrity sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==
+ dependencies:
+ acorn "^8.15.0"
+ pathe "^2.0.3"
+ pkg-types "^1.3.1"
+ ufo "^1.6.1"
+
+moo@^0.5.1:
+ version "0.5.2"
+ resolved "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz"
+ integrity sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==
+
+mri@^1.1.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz"
+ integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==
+
+mrmime@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz"
+ integrity sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==
+
+ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+nanoid@^3.3.11:
+ version "3.3.11"
+ resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz"
+ integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
+
+nanoid@^5.1.3:
+ version "5.1.6"
+ resolved "https://registry.npmjs.org/nanoid/-/nanoid-5.1.6.tgz"
+ integrity sha512-c7+7RQ+dMB5dPwwCp4ee1/iV/q2P6aK1mTZcfr1BTuVlyW9hJYiMPybJCcnBlQtuSmTIWNeazm/zqNoZSSElBg==
+
+neotraverse@^0.6.18:
+ version "0.6.18"
+ resolved "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz"
+ integrity sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==
+
+nested-error-stacks@^2.1.0:
+ version "2.1.1"
+ resolved "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz"
+ integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw==
+
+next-tick@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/next-tick/-/next-tick-1.1.0.tgz"
+ integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==
+
+nlcst-to-string@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz"
+ integrity sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==
+ dependencies:
+ "@types/nlcst" "^2.0.0"
+
+no-case@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz"
+ integrity sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==
+ dependencies:
+ lower-case "^2.0.2"
+ tslib "^2.0.3"
+
+node-fetch@^2.6.1, node-fetch@^2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz"
+ integrity sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==
+ dependencies:
+ whatwg-url "^5.0.0"
+
+node-fetch@~2.6.1:
+ version "2.6.13"
+ resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz"
+ integrity sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==
+ dependencies:
+ whatwg-url "^5.0.0"
+
+node-releases@^2.0.19:
+ version "2.0.19"
+ resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz"
+ integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==
+
+normalize-range@^0.1.2:
+ version "0.1.2"
+ resolved "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz"
+ integrity sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==
+
+onetime@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz"
+ integrity sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==
+ dependencies:
+ mimic-function "^5.0.0"
+
+oniguruma-to-es@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-2.3.0.tgz"
+ integrity sha512-bwALDxriqfKGfUufKGGepCzu9x7nJQuoRoAFp4AnwehhC2crqrDIAP/uN2qdlsAvSMpeRC3+Yzhqc7hLmle5+g==
+ dependencies:
+ emoji-regex-xs "^1.0.0"
+ regex "^5.1.1"
+ regex-recursion "^5.1.1"
+
+opentype.js@^1.3.4:
+ version "1.3.4"
+ resolved "https://registry.npmjs.org/opentype.js/-/opentype.js-1.3.4.tgz"
+ integrity sha512-d2JE9RP/6uagpQAVtJoF0pJJA/fgai89Cc50Yp0EJHk+eLp6QQ7gBoblsnubRULNY132I0J1QKMJ+JTbMqz4sw==
+ dependencies:
+ string.prototype.codepointat "^0.2.1"
+ tiny-inflate "^1.0.3"
+
+ora@^8.1.1:
+ version "8.2.0"
+ resolved "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz"
+ integrity sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==
+ dependencies:
+ chalk "^5.3.0"
+ cli-cursor "^5.0.0"
+ cli-spinners "^2.9.2"
+ is-interactive "^2.0.0"
+ is-unicode-supported "^2.0.0"
+ log-symbols "^6.0.0"
+ stdin-discarder "^0.2.2"
+ string-width "^7.2.0"
+ strip-ansi "^7.1.0"
+
+p-limit@^2.2.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz"
+ integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+ dependencies:
+ p-try "^2.0.0"
+
+p-limit@^6.1.0:
+ version "6.2.0"
+ resolved "https://registry.npmjs.org/p-limit/-/p-limit-6.2.0.tgz"
+ integrity sha512-kuUqqHNUqoIWp/c467RI4X6mmyuojY5jGutNU0wVTmEOOfcuwLqyMVoAi9MKi2Ak+5i9+nhmrK4ufZE8069kHA==
+ dependencies:
+ yocto-queue "^1.1.1"
+
+p-locate@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz"
+ integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+ dependencies:
+ p-limit "^2.2.0"
+
+p-queue@^8.0.1:
+ version "8.1.0"
+ resolved "https://registry.npmjs.org/p-queue/-/p-queue-8.1.0.tgz"
+ integrity sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw==
+ dependencies:
+ eventemitter3 "^5.0.1"
+ p-timeout "^6.1.2"
+
+p-timeout@^6.1.2:
+ version "6.1.4"
+ resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz"
+ integrity sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==
+
+p-try@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz"
+ integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+package-manager-detector@^1.3.0:
+ version "1.5.0"
+ resolved "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.5.0.tgz"
+ integrity sha512-uBj69dVlYe/+wxj8JOpr97XfsxH/eumMt6HqjNTmJDf/6NO9s+0uxeOneIz3AsPt2m6y9PqzDzd3ATcU17MNfw==
+
+pagedjs@^0.4.3:
+ version "0.4.3"
+ resolved "https://registry.npmjs.org/pagedjs/-/pagedjs-0.4.3.tgz"
+ integrity sha512-YtAN9JAjsQw1142gxEjEAwXvOF5nYQuDwnQ67RW2HZDkMLI+b4RsBE37lULZa9gAr6kDAOGBOhXI4wGMoY3raw==
+ dependencies:
+ "@babel/polyfill" "^7.10.1"
+ "@babel/runtime" "^7.21.0"
+ clear-cut "^2.0.2"
+ css-tree "^1.1.3"
+ event-emitter "^0.3.5"
+
+pako@~1.0.2:
+ version "1.0.11"
+ resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz"
+ integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
+parse-color@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/parse-color/-/parse-color-1.0.0.tgz"
+ integrity sha512-fuDHYgFHJGbpGMgw9skY/bj3HL/Jrn4l/5rSspy00DoT4RyLnDcRvPxdZ+r6OFwIsgAuhDh4I09tAId4mI12bw==
+ dependencies:
+ color-convert "~0.5.0"
+
+parse-entities@^4.0.0:
+ version "4.0.2"
+ resolved "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz"
+ integrity sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ character-entities-legacy "^3.0.0"
+ character-reference-invalid "^2.0.0"
+ decode-named-character-reference "^1.0.0"
+ is-alphanumerical "^2.0.0"
+ is-decimal "^2.0.0"
+ is-hexadecimal "^2.0.0"
+
+parse-latin@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz"
+ integrity sha512-mhHgobPPua5kZ98EF4HWiH167JWBfl4pvAIXXdbaVohtK7a6YBOy56kvhCqduqyo/f3yrHFWmqmiMg/BkBkYYQ==
+ dependencies:
+ "@types/nlcst" "^2.0.0"
+ "@types/unist" "^3.0.0"
+ nlcst-to-string "^4.0.0"
+ unist-util-modify-children "^4.0.0"
+ unist-util-visit-children "^3.0.0"
+ vfile "^6.0.0"
+
+parse5@^7.0.0, parse5@^7.1.2:
+ version "7.3.0"
+ resolved "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz"
+ integrity sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==
+ dependencies:
+ entities "^6.0.0"
+
+pascal-case@^3.1.1:
+ version "3.1.2"
+ resolved "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz"
+ integrity sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==
+ dependencies:
+ no-case "^3.0.4"
+ tslib "^2.0.3"
+
+path-data-parser@^0.1.0, path-data-parser@0.1.0:
+ version "0.1.0"
+ resolved "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz"
+ integrity sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+pathe@^2.0.1, pathe@^2.0.3:
+ version "2.0.3"
+ resolved "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz"
+ integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==
+
+periscopic@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz"
+ integrity sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ estree-walker "^3.0.0"
+ is-reference "^3.0.0"
+
+picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+picomatch@^4.0.2:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz"
+ integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+
+pify@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz"
+ integrity sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==
+
+pkg-dir@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz"
+ integrity sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==
+ dependencies:
+ find-up "^4.0.0"
+
+pkg-types@^1.3.1:
+ version "1.3.1"
+ resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz"
+ integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==
+ dependencies:
+ confbox "^0.1.8"
+ mlly "^1.7.4"
+ pathe "^2.0.1"
+
+pkg-types@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.0.tgz"
+ integrity sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==
+ dependencies:
+ confbox "^0.2.2"
+ exsolve "^1.0.7"
+ pathe "^2.0.3"
+
+playwright-core@1.55.0:
+ version "1.55.0"
+ resolved "https://registry.npmjs.org/playwright-core/-/playwright-core-1.55.0.tgz"
+ integrity sha512-GvZs4vU3U5ro2nZpeiwyb0zuFaqb9sUiAJuyrWpcGouD8y9/HLgGbNRjIph7zU9D3hnPaisMl9zG9CgFi/biIg==
+
+playwright@^1.55.0:
+ version "1.55.0"
+ resolved "https://registry.npmjs.org/playwright/-/playwright-1.55.0.tgz"
+ integrity sha512-sdCWStblvV1YU909Xqx0DhOjPZE4/5lJsIS84IfN9dAZfcl/CIZ5O8l3o0j7hPMjDvqoTF8ZUcc+i/GL5erstA==
+ dependencies:
+ playwright-core "1.55.0"
+ optionalDependencies:
+ fsevents "2.3.2"
+
+points-on-curve@^0.2.0, points-on-curve@0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz"
+ integrity sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==
+
+points-on-path@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz"
+ integrity sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==
+ dependencies:
+ path-data-parser "0.1.0"
+ points-on-curve "0.2.0"
+
+postcss-attribute-case-insensitive@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz"
+ integrity sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==
+ dependencies:
+ postcss-selector-parser "^7.0.0"
+
+postcss-clamp@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz"
+ integrity sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==
+ dependencies:
+ postcss-value-parser "^4.2.0"
+
+postcss-color-functional-notation@^7.0.11:
+ version "7.0.11"
+ resolved "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.11.tgz"
+ integrity sha512-zfqoUSaHMko/k2PA9xnaydVTHqYv5vphq5Q2AHcG/dCdv/OkHYWcVWfVTBKZ526uzT8L7NghuvSw3C9PxlKnLg==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+postcss-color-hex-alpha@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz"
+ integrity sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==
+ dependencies:
+ "@csstools/utilities" "^2.0.0"
+ postcss-value-parser "^4.2.0"
+
+postcss-color-rebeccapurple@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz"
+ integrity sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==
+ dependencies:
+ "@csstools/utilities" "^2.0.0"
+ postcss-value-parser "^4.2.0"
+
+postcss-custom-media@^11.0.6:
+ version "11.0.6"
+ resolved "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz"
+ integrity sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==
+ dependencies:
+ "@csstools/cascade-layer-name-parser" "^2.0.5"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/media-query-list-parser" "^4.0.3"
+
+postcss-custom-properties@^14.0.6:
+ version "14.0.6"
+ resolved "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz"
+ integrity sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==
+ dependencies:
+ "@csstools/cascade-layer-name-parser" "^2.0.5"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/utilities" "^2.0.0"
+ postcss-value-parser "^4.2.0"
+
+postcss-custom-selectors@^8.0.5:
+ version "8.0.5"
+ resolved "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz"
+ integrity sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==
+ dependencies:
+ "@csstools/cascade-layer-name-parser" "^2.0.5"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ postcss-selector-parser "^7.0.0"
+
+postcss-dir-pseudo-class@^9.0.1:
+ version "9.0.1"
+ resolved "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz"
+ integrity sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==
+ dependencies:
+ postcss-selector-parser "^7.0.0"
+
+postcss-double-position-gradients@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.3.tgz"
+ integrity sha512-Dl0Z9sdbMwrPslgOaGBZRGo3TASmmgTcqcUODr82MTYyJk6devXZM6MlQjpQKMJqlLJ6oL1w78U7IXFdPA5+ug==
+ dependencies:
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+ postcss-value-parser "^4.2.0"
+
+postcss-focus-visible@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz"
+ integrity sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==
+ dependencies:
+ postcss-selector-parser "^7.0.0"
+
+postcss-focus-within@^9.0.1:
+ version "9.0.1"
+ resolved "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz"
+ integrity sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==
+ dependencies:
+ postcss-selector-parser "^7.0.0"
+
+postcss-font-variant@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz"
+ integrity sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==
+
+postcss-gap-properties@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz"
+ integrity sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==
+
+postcss-image-set-function@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz"
+ integrity sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==
+ dependencies:
+ "@csstools/utilities" "^2.0.0"
+ postcss-value-parser "^4.2.0"
+
+postcss-lab-function@^7.0.11:
+ version "7.0.11"
+ resolved "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.11.tgz"
+ integrity sha512-BEA4jId8uQe1gyjZZ6Bunb6ZsH2izks+v25AxQJDBtigXCjTLmCPWECwQpLTtcxH589MVxhs/9TAmRC6lUEmXQ==
+ dependencies:
+ "@csstools/css-color-parser" "^3.1.0"
+ "@csstools/css-parser-algorithms" "^3.0.5"
+ "@csstools/css-tokenizer" "^3.0.4"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/utilities" "^2.0.0"
+
+postcss-logical@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz"
+ integrity sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==
+ dependencies:
+ postcss-value-parser "^4.2.0"
+
+postcss-nesting@^13.0.2:
+ version "13.0.2"
+ resolved "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz"
+ integrity sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==
+ dependencies:
+ "@csstools/selector-resolve-nested" "^3.1.0"
+ "@csstools/selector-specificity" "^5.0.0"
+ postcss-selector-parser "^7.0.0"
+
+postcss-opacity-percentage@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz"
+ integrity sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==
+
+postcss-overflow-shorthand@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz"
+ integrity sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==
+ dependencies:
+ postcss-value-parser "^4.2.0"
+
+postcss-page-break@^3.0.4:
+ version "3.0.4"
+ resolved "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz"
+ integrity sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==
+
+postcss-place@^10.0.0:
+ version "10.0.0"
+ resolved "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz"
+ integrity sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==
+ dependencies:
+ postcss-value-parser "^4.2.0"
+
+postcss-preset-env@^10.3.1:
+ version "10.3.1"
+ resolved "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.3.1.tgz"
+ integrity sha512-8ZOOWVwQ0iMpfEYkYo+U6W7fE2dJ/tP6dtEFwPJ66eB5JjnFupfYh+y6zo+vWDO72nGhKOVdxwhTjfzcSNRg4Q==
+ dependencies:
+ "@csstools/postcss-alpha-function" "^1.0.0"
+ "@csstools/postcss-cascade-layers" "^5.0.2"
+ "@csstools/postcss-color-function" "^4.0.11"
+ "@csstools/postcss-color-function-display-p3-linear" "^1.0.0"
+ "@csstools/postcss-color-mix-function" "^3.0.11"
+ "@csstools/postcss-color-mix-variadic-function-arguments" "^1.0.1"
+ "@csstools/postcss-content-alt-text" "^2.0.7"
+ "@csstools/postcss-exponential-functions" "^2.0.9"
+ "@csstools/postcss-font-format-keywords" "^4.0.0"
+ "@csstools/postcss-gamut-mapping" "^2.0.11"
+ "@csstools/postcss-gradients-interpolation-method" "^5.0.11"
+ "@csstools/postcss-hwb-function" "^4.0.11"
+ "@csstools/postcss-ic-unit" "^4.0.3"
+ "@csstools/postcss-initial" "^2.0.1"
+ "@csstools/postcss-is-pseudo-class" "^5.0.3"
+ "@csstools/postcss-light-dark-function" "^2.0.10"
+ "@csstools/postcss-logical-float-and-clear" "^3.0.0"
+ "@csstools/postcss-logical-overflow" "^2.0.0"
+ "@csstools/postcss-logical-overscroll-behavior" "^2.0.0"
+ "@csstools/postcss-logical-resize" "^3.0.0"
+ "@csstools/postcss-logical-viewport-units" "^3.0.4"
+ "@csstools/postcss-media-minmax" "^2.0.9"
+ "@csstools/postcss-media-queries-aspect-ratio-number-values" "^3.0.5"
+ "@csstools/postcss-nested-calc" "^4.0.0"
+ "@csstools/postcss-normalize-display-values" "^4.0.0"
+ "@csstools/postcss-oklab-function" "^4.0.11"
+ "@csstools/postcss-progressive-custom-properties" "^4.2.0"
+ "@csstools/postcss-random-function" "^2.0.1"
+ "@csstools/postcss-relative-color-syntax" "^3.0.11"
+ "@csstools/postcss-scope-pseudo-class" "^4.0.1"
+ "@csstools/postcss-sign-functions" "^1.1.4"
+ "@csstools/postcss-stepped-value-functions" "^4.0.9"
+ "@csstools/postcss-text-decoration-shorthand" "^4.0.3"
+ "@csstools/postcss-trigonometric-functions" "^4.0.9"
+ "@csstools/postcss-unset-value" "^4.0.0"
+ autoprefixer "^10.4.21"
+ browserslist "^4.25.1"
+ css-blank-pseudo "^7.0.1"
+ css-has-pseudo "^7.0.3"
+ css-prefers-color-scheme "^10.0.0"
+ cssdb "^8.4.0"
+ postcss-attribute-case-insensitive "^7.0.1"
+ postcss-clamp "^4.1.0"
+ postcss-color-functional-notation "^7.0.11"
+ postcss-color-hex-alpha "^10.0.0"
+ postcss-color-rebeccapurple "^10.0.0"
+ postcss-custom-media "^11.0.6"
+ postcss-custom-properties "^14.0.6"
+ postcss-custom-selectors "^8.0.5"
+ postcss-dir-pseudo-class "^9.0.1"
+ postcss-double-position-gradients "^6.0.3"
+ postcss-focus-visible "^10.0.1"
+ postcss-focus-within "^9.0.1"
+ postcss-font-variant "^5.0.0"
+ postcss-gap-properties "^6.0.0"
+ postcss-image-set-function "^7.0.0"
+ postcss-lab-function "^7.0.11"
+ postcss-logical "^8.1.0"
+ postcss-nesting "^13.0.2"
+ postcss-opacity-percentage "^3.0.0"
+ postcss-overflow-shorthand "^6.0.0"
+ postcss-page-break "^3.0.4"
+ postcss-place "^10.0.0"
+ postcss-pseudo-class-any-link "^10.0.1"
+ postcss-replace-overflow-wrap "^4.0.0"
+ postcss-selector-not "^8.0.1"
+
+postcss-pseudo-class-any-link@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz"
+ integrity sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==
+ dependencies:
+ postcss-selector-parser "^7.0.0"
+
+postcss-replace-overflow-wrap@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz"
+ integrity sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==
+
+postcss-selector-not@^8.0.1:
+ version "8.0.1"
+ resolved "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz"
+ integrity sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==
+ dependencies:
+ postcss-selector-parser "^7.0.0"
+
+postcss-selector-parser@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz"
+ integrity sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==
+ dependencies:
+ cssesc "^3.0.0"
+ util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
+ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8, postcss@^8.0.3, postcss@^8.1.0, postcss@^8.4, postcss@^8.4.43, postcss@^8.4.6, postcss@^8.5.6:
+ version "8.5.6"
+ resolved "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz"
+ integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
+ dependencies:
+ nanoid "^3.3.11"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
+preferred-pm@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.npmjs.org/preferred-pm/-/preferred-pm-4.1.1.tgz"
+ integrity sha512-rU+ZAv1Ur9jAUZtGPebQVQPzdGhNzaEiQ7VL9+cjsAWPHFYOccNXPNiev1CCDSOg/2j7UujM7ojNhpkuILEVNQ==
+ dependencies:
+ find-up-simple "^1.0.0"
+ find-yarn-workspace-root2 "1.2.16"
+ which-pm "^3.0.1"
+
+prism-themes@^1.9.0:
+ version "1.9.0"
+ resolved "https://registry.npmjs.org/prism-themes/-/prism-themes-1.9.0.tgz"
+ integrity sha512-tX2AYsehKDw1EORwBps+WhBFKc2kxfoFpQAjxBndbZKr4fRmMkv47XN0BghC/K1qwodB1otbe4oF23vUTFDokw==
+
+prismjs@^1.29.0:
+ version "1.30.0"
+ resolved "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz"
+ integrity sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==
+
+process-nextick-args@~2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz"
+ integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
+prompts@^2.4.2:
+ version "2.4.2"
+ resolved "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz"
+ integrity sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==
+ dependencies:
+ kleur "^3.0.3"
+ sisteransi "^1.0.5"
+
+property-information@^6.0.0:
+ version "6.5.0"
+ resolved "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz"
+ integrity sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==
+
+property-information@^7.0.0:
+ version "7.1.0"
+ resolved "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz"
+ integrity sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==
+
+quansync@^0.2.11:
+ version "0.2.11"
+ resolved "https://registry.npmjs.org/quansync/-/quansync-0.2.11.tgz"
+ integrity sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+readable-stream@^3.5.0:
+ version "3.6.2"
+ resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz"
+ integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==
+ dependencies:
+ inherits "^2.0.3"
+ string_decoder "^1.1.1"
+ util-deprecate "^1.0.1"
+
+readable-stream@~2.3.6:
+ version "2.3.8"
+ resolved "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz"
+ integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
+ dependencies:
+ core-util-is "~1.0.0"
+ inherits "~2.0.3"
+ isarray "~1.0.0"
+ process-nextick-args "~2.0.0"
+ safe-buffer "~5.1.1"
+ string_decoder "~1.1.1"
+ util-deprecate "~1.0.1"
+
+recma-build-jsx@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz"
+ integrity sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ estree-util-build-jsx "^3.0.0"
+ vfile "^6.0.0"
+
+recma-jsx@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz"
+ integrity sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==
+ dependencies:
+ acorn-jsx "^5.0.0"
+ estree-util-to-js "^2.0.0"
+ recma-parse "^1.0.0"
+ recma-stringify "^1.0.0"
+ unified "^11.0.0"
+
+recma-parse@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz"
+ integrity sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ esast-util-from-js "^2.0.0"
+ unified "^11.0.0"
+ vfile "^6.0.0"
+
+recma-stringify@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz"
+ integrity sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ estree-util-to-js "^2.0.0"
+ unified "^11.0.0"
+ vfile "^6.0.0"
+
+regenerator-runtime@^0.13.4:
+ version "0.13.11"
+ resolved "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz"
+ integrity sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==
+
+regex-recursion@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.1.1.tgz"
+ integrity sha512-ae7SBCbzVNrIjgSbh7wMznPcQel1DNlDtzensnFxpiNpXt1U2ju/bHugH422r+4LAVS1FpW1YCwilmnNsjum9w==
+ dependencies:
+ regex "^5.1.1"
+ regex-utilities "^2.3.0"
+
+regex-utilities@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz"
+ integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==
+
+regex@^5.1.1:
+ version "5.1.1"
+ resolved "https://registry.npmjs.org/regex/-/regex-5.1.1.tgz"
+ integrity sha512-dN5I359AVGPnwzJm2jN1k0W9LPZ+ePvoOeVMMfqIMFz53sSwXkxaJoxr50ptnsC771lK95BnTrVSZxq0b9yCGw==
+ dependencies:
+ regex-utilities "^2.3.0"
+
+rehype-autolink-headings@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.npmjs.org/rehype-autolink-headings/-/rehype-autolink-headings-7.1.0.tgz"
+ integrity sha512-rItO/pSdvnvsP4QRB1pmPiNHUskikqtPojZKJPPPAVx9Hj8i8TwMBhofrrAYRhYOOBZH9tgmG5lPqDLuIWPWmw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@ungap/structured-clone" "^1.0.0"
+ hast-util-heading-rank "^3.0.0"
+ hast-util-is-element "^3.0.0"
+ unified "^11.0.0"
+ unist-util-visit "^5.0.0"
+
+rehype-citation@^2.3.1:
+ version "2.3.1"
+ resolved "https://registry.npmjs.org/rehype-citation/-/rehype-citation-2.3.1.tgz"
+ integrity sha512-bwSuB5SMilyS/vT7K7ByTxjeKda4GWJin6dHKKZyp5O2z+uLk2ySG7a5/IOmuGovoajN9AcYxTRE4kUiVTk51g==
+ dependencies:
+ "@citation-js/core" "^0.7.14"
+ "@citation-js/date" "^0.5.1"
+ "@citation-js/name" "^0.4.2"
+ "@citation-js/plugin-bibjson" "^0.7.14"
+ "@citation-js/plugin-bibtex" "^0.7.14"
+ "@citation-js/plugin-csl" "^0.7.14"
+ citeproc "^2.4.63"
+ cross-fetch "^4.0.0"
+ hast-util-from-dom "^5.0.0"
+ hast-util-from-parse5 "^8.0.1"
+ js-yaml "^4.1.0"
+ parse5 "^7.1.2"
+ unified "^11.0.0"
+ unist-util-visit "^5.0.0"
+
+rehype-katex@^7.0.1:
+ version "7.0.1"
+ resolved "https://registry.npmjs.org/rehype-katex/-/rehype-katex-7.0.1.tgz"
+ integrity sha512-OiM2wrZ/wuhKkigASodFoo8wimG3H12LWQaH8qSPVJn9apWKFSH3YOCtbKpBorTVw/eI7cuT21XBbvwEswbIOA==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/katex" "^0.16.0"
+ hast-util-from-html-isomorphic "^2.0.0"
+ hast-util-to-text "^4.0.0"
+ katex "^0.16.0"
+ unist-util-visit-parents "^6.0.0"
+ vfile "^6.0.0"
+
+rehype-parse@^9.0.0:
+ version "9.0.1"
+ resolved "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz"
+ integrity sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hast-util-from-html "^2.0.0"
+ unified "^11.0.0"
+
+rehype-raw@^7.0.0:
+ version "7.0.0"
+ resolved "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz"
+ integrity sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hast-util-raw "^9.0.0"
+ vfile "^6.0.0"
+
+rehype-recma@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz"
+ integrity sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==
+ dependencies:
+ "@types/estree" "^1.0.0"
+ "@types/hast" "^3.0.0"
+ hast-util-to-estree "^3.0.0"
+
+rehype-slug@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz"
+ integrity sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ github-slugger "^2.0.0"
+ hast-util-heading-rank "^3.0.0"
+ hast-util-to-string "^3.0.0"
+ unist-util-visit "^5.0.0"
+
+rehype-stringify@^10.0.0, rehype-stringify@^10.0.1:
+ version "10.0.1"
+ resolved "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz"
+ integrity sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ hast-util-to-html "^9.0.0"
+ unified "^11.0.0"
+
+rehype@^13.0.2:
+ version "13.0.2"
+ resolved "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz"
+ integrity sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ rehype-parse "^9.0.0"
+ rehype-stringify "^10.0.0"
+ unified "^11.0.0"
+
+remark-directive@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz"
+ integrity sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ mdast-util-directive "^3.0.0"
+ micromark-extension-directive "^3.0.0"
+ unified "^11.0.0"
+
+remark-footnotes@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/remark-footnotes/-/remark-footnotes-4.0.1.tgz"
+ integrity sha512-He6YzQFk/Wu2KgfjI80EyPXjt/G+WFaYfUH+xapqPQBdm3aTdEyzosXXv9a2FbTxGqgOfJ4q/TCB46v+wofRpQ==
+ dependencies:
+ "@types/mdast" "^3.0.0"
+ mdast-util-footnote "^1.0.0"
+ micromark-extension-footnote "^1.0.0"
+ unified "^10.0.0"
+
+remark-gfm@^4.0.0:
+ version "4.0.1"
+ resolved "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz"
+ integrity sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ mdast-util-gfm "^3.0.0"
+ micromark-extension-gfm "^3.0.0"
+ remark-parse "^11.0.0"
+ remark-stringify "^11.0.0"
+ unified "^11.0.0"
+
+remark-math@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/remark-math/-/remark-math-6.0.0.tgz"
+ integrity sha512-MMqgnP74Igy+S3WwnhQ7kqGlEerTETXMvJhrUzDikVZ2/uogJCb+WHUg97hK9/jcfc0dkD73s3LN8zU49cTEtA==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ mdast-util-math "^3.0.0"
+ micromark-extension-math "^3.0.0"
+ unified "^11.0.0"
+
+remark-mdx@^3.0.0:
+ version "3.1.0"
+ resolved "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.0.tgz"
+ integrity sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==
+ dependencies:
+ mdast-util-mdx "^3.0.0"
+ micromark-extension-mdxjs "^3.0.0"
+
+remark-parse@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz"
+ integrity sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ mdast-util-from-markdown "^2.0.0"
+ micromark-util-types "^2.0.0"
+ unified "^11.0.0"
+
+remark-rehype@^11.0.0, remark-rehype@^11.1.1:
+ version "11.1.2"
+ resolved "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz"
+ integrity sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==
+ dependencies:
+ "@types/hast" "^3.0.0"
+ "@types/mdast" "^4.0.0"
+ mdast-util-to-hast "^13.0.0"
+ unified "^11.0.0"
+ vfile "^6.0.0"
+
+remark-smartypants@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz"
+ integrity sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==
+ dependencies:
+ retext "^9.0.0"
+ retext-smartypants "^6.0.0"
+ unified "^11.0.4"
+ unist-util-visit "^5.0.0"
+
+remark-stringify@^11.0.0:
+ version "11.0.0"
+ resolved "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz"
+ integrity sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==
+ dependencies:
+ "@types/mdast" "^4.0.0"
+ mdast-util-to-markdown "^2.0.0"
+ unified "^11.0.0"
+
+restore-cursor@^5.0.0:
+ version "5.1.0"
+ resolved "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz"
+ integrity sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==
+ dependencies:
+ onetime "^7.0.0"
+ signal-exit "^4.1.0"
+
+retext-latin@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/retext-latin/-/retext-latin-4.0.0.tgz"
+ integrity sha512-hv9woG7Fy0M9IlRQloq/N6atV82NxLGveq+3H2WOi79dtIYWN8OaxogDm77f8YnVXJL2VD3bbqowu5E3EMhBYA==
+ dependencies:
+ "@types/nlcst" "^2.0.0"
+ parse-latin "^7.0.0"
+ unified "^11.0.0"
+
+retext-smartypants@^6.0.0:
+ version "6.2.0"
+ resolved "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz"
+ integrity sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==
+ dependencies:
+ "@types/nlcst" "^2.0.0"
+ nlcst-to-string "^4.0.0"
+ unist-util-visit "^5.0.0"
+
+retext-stringify@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/retext-stringify/-/retext-stringify-4.0.0.tgz"
+ integrity sha512-rtfN/0o8kL1e+78+uxPTqu1Klt0yPzKuQ2BfWwwfgIUSayyzxpM1PJzkKt4V8803uB9qSy32MvI7Xep9khTpiA==
+ dependencies:
+ "@types/nlcst" "^2.0.0"
+ nlcst-to-string "^4.0.0"
+ unified "^11.0.0"
+
+retext@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz"
+ integrity sha512-sbMDcpHCNjvlheSgMfEcVrZko3cDzdbe1x/e7G66dFp0Ff7Mldvi2uv6JkJQzdRcvLYE8CA8Oe8siQx8ZOgTcA==
+ dependencies:
+ "@types/nlcst" "^2.0.0"
+ retext-latin "^4.0.0"
+ retext-stringify "^4.0.0"
+ unified "^11.0.0"
+
+reusify@^1.0.4:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz"
+ integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
+
+robust-predicates@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz"
+ integrity sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==
+
+rollup@^1.20.0||^2.0.0||^3.0.0||^4.0.0, rollup@^4.20.0:
+ version "4.48.1"
+ resolved "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz"
+ integrity sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg==
+ dependencies:
+ "@types/estree" "1.0.8"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.48.1"
+ "@rollup/rollup-android-arm64" "4.48.1"
+ "@rollup/rollup-darwin-arm64" "4.48.1"
+ "@rollup/rollup-darwin-x64" "4.48.1"
+ "@rollup/rollup-freebsd-arm64" "4.48.1"
+ "@rollup/rollup-freebsd-x64" "4.48.1"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.48.1"
+ "@rollup/rollup-linux-arm-musleabihf" "4.48.1"
+ "@rollup/rollup-linux-arm64-gnu" "4.48.1"
+ "@rollup/rollup-linux-arm64-musl" "4.48.1"
+ "@rollup/rollup-linux-loongarch64-gnu" "4.48.1"
+ "@rollup/rollup-linux-ppc64-gnu" "4.48.1"
+ "@rollup/rollup-linux-riscv64-gnu" "4.48.1"
+ "@rollup/rollup-linux-riscv64-musl" "4.48.1"
+ "@rollup/rollup-linux-s390x-gnu" "4.48.1"
+ "@rollup/rollup-linux-x64-gnu" "4.48.1"
+ "@rollup/rollup-linux-x64-musl" "4.48.1"
+ "@rollup/rollup-win32-arm64-msvc" "4.48.1"
+ "@rollup/rollup-win32-ia32-msvc" "4.48.1"
+ "@rollup/rollup-win32-x64-msvc" "4.48.1"
+ fsevents "~2.3.2"
+
+roughjs@^4.6.6:
+ version "4.6.6"
+ resolved "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz"
+ integrity sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==
+ dependencies:
+ hachure-fill "^0.5.2"
+ path-data-parser "^0.1.0"
+ points-on-curve "^0.2.0"
+ points-on-path "^0.2.1"
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+rw@1:
+ version "1.3.3"
+ resolved "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz"
+ integrity sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==
+
+sade@^1.7.3:
+ version "1.8.1"
+ resolved "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz"
+ integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==
+ dependencies:
+ mri "^1.1.0"
+
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+ version "5.1.2"
+ resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz"
+ integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
+safe-buffer@~5.2.0:
+ version "5.2.1"
+ resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz"
+ integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+
+"safer-buffer@>= 2.1.2 < 3.0.0":
+ version "2.1.2"
+ resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
+ integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+
+sax@^1.2.4, sax@^1.4.1:
+ version "1.4.4"
+ resolved "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz"
+ integrity sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==
+
+section-matter@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz"
+ integrity sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==
+ dependencies:
+ extend-shallow "^2.0.1"
+ kind-of "^6.0.0"
+
+semver@^6.3.1:
+ version "6.3.1"
+ resolved "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz"
+ integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
+
+semver@^7.6.3:
+ version "7.7.2"
+ resolved "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz"
+ integrity sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==
+
+setimmediate@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz"
+ integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
+
+sharp@^0.33.3, sharp@^0.33.5:
+ version "0.33.5"
+ resolved "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz"
+ integrity sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==
+ dependencies:
+ color "^4.2.3"
+ detect-libc "^2.0.3"
+ semver "^7.6.3"
+ optionalDependencies:
+ "@img/sharp-darwin-arm64" "0.33.5"
+ "@img/sharp-darwin-x64" "0.33.5"
+ "@img/sharp-libvips-darwin-arm64" "1.0.4"
+ "@img/sharp-libvips-darwin-x64" "1.0.4"
+ "@img/sharp-libvips-linux-arm" "1.0.5"
+ "@img/sharp-libvips-linux-arm64" "1.0.4"
+ "@img/sharp-libvips-linux-s390x" "1.0.4"
+ "@img/sharp-libvips-linux-x64" "1.0.4"
+ "@img/sharp-libvips-linuxmusl-arm64" "1.0.4"
+ "@img/sharp-libvips-linuxmusl-x64" "1.0.4"
+ "@img/sharp-linux-arm" "0.33.5"
+ "@img/sharp-linux-arm64" "0.33.5"
+ "@img/sharp-linux-s390x" "0.33.5"
+ "@img/sharp-linux-x64" "0.33.5"
+ "@img/sharp-linuxmusl-arm64" "0.33.5"
+ "@img/sharp-linuxmusl-x64" "0.33.5"
+ "@img/sharp-wasm32" "0.33.5"
+ "@img/sharp-win32-ia32" "0.33.5"
+ "@img/sharp-win32-x64" "0.33.5"
+
+shiki@^1.22.0, shiki@^1.23.1:
+ version "1.29.2"
+ resolved "https://registry.npmjs.org/shiki/-/shiki-1.29.2.tgz"
+ integrity sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==
+ dependencies:
+ "@shikijs/core" "1.29.2"
+ "@shikijs/engine-javascript" "1.29.2"
+ "@shikijs/engine-oniguruma" "1.29.2"
+ "@shikijs/langs" "1.29.2"
+ "@shikijs/themes" "1.29.2"
+ "@shikijs/types" "1.29.2"
+ "@shikijs/vscode-textmate" "^10.0.1"
+ "@types/hast" "^3.0.4"
+
+signal-exit@^4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
+simple-swizzle@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz"
+ integrity sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==
+ dependencies:
+ is-arrayish "^0.3.1"
+
+sisteransi@^1.0.5:
+ version "1.0.5"
+ resolved "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz"
+ integrity sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==
+
+sitemap@^8.0.2:
+ version "8.0.2"
+ resolved "https://registry.npmjs.org/sitemap/-/sitemap-8.0.2.tgz"
+ integrity sha512-LwktpJcyZDoa0IL6KT++lQ53pbSrx2c9ge41/SeLTyqy2XUNA6uR4+P9u5IVo5lPeL2arAcOKn1aZAxoYbCKlQ==
+ dependencies:
+ "@types/node" "^17.0.5"
+ "@types/sax" "^1.2.1"
+ arg "^5.0.0"
+ sax "^1.4.1"
+
+source-map-js@^1.0.1, source-map-js@^1.2.0, source-map-js@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+source-map@^0.6.1:
+ version "0.6.1"
+ resolved "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz"
+ integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==
+
+source-map@^0.7.0, source-map@^0.7.4:
+ version "0.7.6"
+ resolved "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz"
+ integrity sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==
+
+space-separated-tokens@^2.0.0:
+ version "2.0.2"
+ resolved "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz"
+ integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==
+
+sprintf-js@~1.0.2:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
+ integrity sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==
+
+stdin-discarder@^0.2.2:
+ version "0.2.2"
+ resolved "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz"
+ integrity sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==
+
+stream-browserify@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz"
+ integrity sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==
+ dependencies:
+ inherits "~2.0.4"
+ readable-stream "^3.5.0"
+
+stream-replace-string@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz"
+ integrity sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==
+
+string_decoder@^1.1.1:
+ version "1.3.0"
+ resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz"
+ integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+ dependencies:
+ safe-buffer "~5.2.0"
+
+string_decoder@~1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz"
+ integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+ dependencies:
+ safe-buffer "~5.1.0"
+
+string-width@^4.1.0:
+ version "4.2.3"
+ resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^7.0.0, string-width@^7.2.0:
+ version "7.2.0"
+ resolved "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz"
+ integrity sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==
+ dependencies:
+ emoji-regex "^10.3.0"
+ get-east-asian-width "^1.0.0"
+ strip-ansi "^7.1.0"
+
+string.prototype.codepointat@^0.2.1:
+ version "0.2.1"
+ resolved "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz"
+ integrity sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==
+
+stringify-entities@^4.0.0:
+ version "4.0.4"
+ resolved "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz"
+ integrity sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==
+ dependencies:
+ character-entities-html4 "^2.0.0"
+ character-entities-legacy "^3.0.0"
+
+strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@^7.1.0:
+ version "7.1.0"
+ resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz"
+ integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==
+ dependencies:
+ ansi-regex "^6.0.1"
+
+strip-bom-string@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz"
+ integrity sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==
+
+strip-bom@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz"
+ integrity sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==
+
+style-to-js@^1.0.0:
+ version "1.1.17"
+ resolved "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz"
+ integrity sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==
+ dependencies:
+ style-to-object "1.0.9"
+
+style-to-object@1.0.9:
+ version "1.0.9"
+ resolved "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz"
+ integrity sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==
+ dependencies:
+ inline-style-parser "0.2.4"
+
+stylis@^4.3.6:
+ version "4.3.6"
+ resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz"
+ integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
+
+svelte-hmr@^0.16.0:
+ version "0.16.0"
+ resolved "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz"
+ integrity sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==
+
+"svelte@^3.19.0 || ^4.0.0", "svelte@^3.55 || ^4.0.0-next.0 || ^4.0 || ^5.0.0-next.0", "svelte@^4.0.0 || ^5.0.0-next.0", "svelte@^4.0.0 || ^5.0.0-next.190", svelte@^4.2.19:
+ version "4.2.20"
+ resolved "https://registry.npmjs.org/svelte/-/svelte-4.2.20.tgz"
+ integrity sha512-eeEgGc2DtiUil5ANdtd8vPwt9AgaMdnuUFnPft9F5oMvU/FHu5IHFic+p1dR/UOB7XU2mX2yHW+NcTch4DCh5Q==
+ dependencies:
+ "@ampproject/remapping" "^2.2.1"
+ "@jridgewell/sourcemap-codec" "^1.4.15"
+ "@jridgewell/trace-mapping" "^0.3.18"
+ "@types/estree" "^1.0.1"
+ acorn "^8.9.0"
+ aria-query "^5.3.0"
+ axobject-query "^4.0.0"
+ code-red "^1.0.3"
+ css-tree "^2.3.1"
+ estree-walker "^3.0.3"
+ is-reference "^3.0.1"
+ locate-character "^3.0.0"
+ magic-string "^0.30.4"
+ periscopic "^3.1.0"
+
+svelte2tsx@^0.7.22:
+ version "0.7.42"
+ resolved "https://registry.npmjs.org/svelte2tsx/-/svelte2tsx-0.7.42.tgz"
+ integrity sha512-PSNrKS16aVdAajoFjpF5M0t6TA7ha7GcKbBajD9RG3M+vooAuvLnWAGUSC6eJL4zEOVbOWKtcS2BuY4rxPljoA==
+ dependencies:
+ dedent-js "^1.0.1"
+ pascal-case "^3.1.1"
+
+sync-fetch@^0.4.1:
+ version "0.4.5"
+ resolved "https://registry.npmjs.org/sync-fetch/-/sync-fetch-0.4.5.tgz"
+ integrity sha512-esiWJ7ixSKGpd9DJPBTC4ckChqdOjIwJfYhVHkcQ2Gnm41323p1TRmEI+esTQ9ppD+b5opps2OTEGTCGX5kF+g==
+ dependencies:
+ buffer "^5.7.1"
+ node-fetch "^2.6.1"
+
+text-segmentation@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz"
+ integrity sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==
+ dependencies:
+ utrie "^1.0.2"
+
+tiny-inflate@^1.0.3:
+ version "1.0.3"
+ resolved "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz"
+ integrity sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==
+
+tinyexec@^0.3.1:
+ version "0.3.2"
+ resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz"
+ integrity sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==
+
+tinyexec@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz"
+ integrity sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+tr46@~0.0.3:
+ version "0.0.3"
+ resolved "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
+ integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==
+
+trim-lines@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz"
+ integrity sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==
+
+trough@^2.0.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz"
+ integrity sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==
+
+ts-dedent@^2.2.0:
+ version "2.2.0"
+ resolved "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz"
+ integrity sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==
+
+tsconfck@^3.1.4:
+ version "3.1.6"
+ resolved "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz"
+ integrity sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==
+
+tslib@^2.0.3:
+ version "2.8.1"
+ resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
+ integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
+
+type-fest@^4.21.0:
+ version "4.41.0"
+ resolved "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz"
+ integrity sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==
+
+type@^2.7.2:
+ version "2.7.3"
+ resolved "https://registry.npmjs.org/type/-/type-2.7.3.tgz"
+ integrity sha512-8j+1QmAbPvLZow5Qpi6NCaN8FB60p/6x8/vfNqOk/hC+HuvFZhL4+WfekuhQLiqFZXOgQdrs3B+XxEmCc6b3FQ==
+
+"typescript@^4.9.4 || ^5.0.0", "typescript@^4.9.4 || ^5.0.2", typescript@^5.0.0, typescript@^5.3.3:
+ version "5.9.2"
+ resolved "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz"
+ integrity sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==
+
+ufo@^1.6.1:
+ version "1.6.1"
+ resolved "https://registry.npmjs.org/ufo/-/ufo-1.6.1.tgz"
+ integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==
+
+undici-types@~7.16.0:
+ version "7.16.0"
+ resolved "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz"
+ integrity sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==
+
+unified@^10.0.0:
+ version "10.1.2"
+ resolved "https://registry.npmjs.org/unified/-/unified-10.1.2.tgz"
+ integrity sha512-pUSWAi/RAnVy1Pif2kAoeWNBa3JVrx0MId2LASj8G+7AiHWoKZNTomq6LG326T68U7/e263X6fTdcXIy7XnF7Q==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ bail "^2.0.0"
+ extend "^3.0.0"
+ is-buffer "^2.0.0"
+ is-plain-obj "^4.0.0"
+ trough "^2.0.0"
+ vfile "^5.0.0"
+
+unified@^11.0.0, unified@^11.0.4, unified@^11.0.5:
+ version "11.0.5"
+ resolved "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz"
+ integrity sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ bail "^2.0.0"
+ devlop "^1.0.0"
+ extend "^3.0.0"
+ is-plain-obj "^4.0.0"
+ trough "^2.0.0"
+ vfile "^6.0.0"
+
+unist-util-find-after@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz"
+ integrity sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ unist-util-is "^6.0.0"
+
+unist-util-is@^5.0.0:
+ version "5.2.1"
+ resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-5.2.1.tgz"
+ integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw==
+ dependencies:
+ "@types/unist" "^2.0.0"
+
+unist-util-is@^6.0.0:
+ version "6.0.0"
+ resolved "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz"
+ integrity sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==
+ dependencies:
+ "@types/unist" "^3.0.0"
+
+unist-util-modify-children@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/unist-util-modify-children/-/unist-util-modify-children-4.0.0.tgz"
+ integrity sha512-+tdN5fGNddvsQdIzUF3Xx82CU9sMM+fA0dLgR9vOmT0oPT2jH+P1nd5lSqfCfXAw+93NhcXNY2qqvTUtE4cQkw==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ array-iterate "^2.0.0"
+
+unist-util-position-from-estree@^2.0.0:
+ version "2.0.0"
+ resolved "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz"
+ integrity sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==
+ dependencies:
+ "@types/unist" "^3.0.0"
+
+unist-util-position@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz"
+ integrity sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==
+ dependencies:
+ "@types/unist" "^3.0.0"
+
+unist-util-remove-position@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-5.0.0.tgz"
+ integrity sha512-Hp5Kh3wLxv0PHj9m2yZhhLt58KzPtEYKQQ4yxfYFEO7EvHwzyDYnduhHnY1mDxoqr7VUwVuHXk9RXKIiYS1N8Q==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ unist-util-visit "^5.0.0"
+
+unist-util-stringify-position@^3.0.0:
+ version "3.0.3"
+ resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz"
+ integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg==
+ dependencies:
+ "@types/unist" "^2.0.0"
+
+unist-util-stringify-position@^4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz"
+ integrity sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==
+ dependencies:
+ "@types/unist" "^3.0.0"
+
+unist-util-visit-children@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmjs.org/unist-util-visit-children/-/unist-util-visit-children-3.0.0.tgz"
+ integrity sha512-RgmdTfSBOg04sdPcpTSD1jzoNBjt9a80/ZCzp5cI9n1qPzLZWF9YdvWGN2zmTumP1HWhXKdUWexjy/Wy/lJ7tA==
+ dependencies:
+ "@types/unist" "^3.0.0"
+
+unist-util-visit-parents@^5.1.1:
+ version "5.1.3"
+ resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz"
+ integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-is "^5.0.0"
+
+unist-util-visit-parents@^6.0.0, unist-util-visit-parents@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz"
+ integrity sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ unist-util-is "^6.0.0"
+
+unist-util-visit@^4.0.0:
+ version "4.1.2"
+ resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-4.1.2.tgz"
+ integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-is "^5.0.0"
+ unist-util-visit-parents "^5.1.1"
+
+unist-util-visit@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz"
+ integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ unist-util-is "^6.0.0"
+ unist-util-visit-parents "^6.0.0"
+
+update-browserslist-db@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz"
+ integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==
+ dependencies:
+ escalade "^3.2.0"
+ picocolors "^1.1.1"
+
+util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+utrie@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz"
+ integrity sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==
+ dependencies:
+ base64-arraybuffer "^1.0.2"
+
+uuid@^11.1.0:
+ version "11.1.0"
+ resolved "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz"
+ integrity sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==
+
+uvu@^0.5.0:
+ version "0.5.6"
+ resolved "https://registry.npmjs.org/uvu/-/uvu-0.5.6.tgz"
+ integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA==
+ dependencies:
+ dequal "^2.0.0"
+ diff "^5.0.0"
+ kleur "^4.0.3"
+ sade "^1.7.3"
+
+vfile-location@^5.0.0:
+ version "5.0.3"
+ resolved "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz"
+ integrity sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ vfile "^6.0.0"
+
+vfile-message@^3.0.0:
+ version "3.1.4"
+ resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-3.1.4.tgz"
+ integrity sha512-fa0Z6P8HUrQN4BZaX05SIVXic+7kE3b05PWAtPuYP9QLHsLKYR7/AlLW3NtOrpXRLeawpDLMsVkmk5DG0NXgWw==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ unist-util-stringify-position "^3.0.0"
+
+vfile-message@^4.0.0:
+ version "4.0.3"
+ resolved "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz"
+ integrity sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ unist-util-stringify-position "^4.0.0"
+
+vfile@^5.0.0:
+ version "5.3.7"
+ resolved "https://registry.npmjs.org/vfile/-/vfile-5.3.7.tgz"
+ integrity sha512-r7qlzkgErKjobAmyNIkkSpizsFPYiUPuJb5pNW1RB4JcYVZhs4lIbVqk8XPk033CV/1z8ss5pkax8SuhGpcG8g==
+ dependencies:
+ "@types/unist" "^2.0.0"
+ is-buffer "^2.0.0"
+ unist-util-stringify-position "^3.0.0"
+ vfile-message "^3.0.0"
+
+vfile@^6.0.0, vfile@^6.0.3:
+ version "6.0.3"
+ resolved "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz"
+ integrity sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==
+ dependencies:
+ "@types/unist" "^3.0.0"
+ vfile-message "^4.0.0"
+
+"vite@^3.0.0 || ^4.0.0 || ^5.0.0", "vite@^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0", vite@^5.0.0, vite@^5.4.11:
+ version "5.4.19"
+ resolved "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz"
+ integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==
+ dependencies:
+ esbuild "^0.21.3"
+ postcss "^8.4.43"
+ rollup "^4.20.0"
+ optionalDependencies:
+ fsevents "~2.3.3"
+
+vitefu@^0.2.5:
+ version "0.2.5"
+ resolved "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz"
+ integrity sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==
+
+vitefu@^1.0.4:
+ version "1.1.1"
+ resolved "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz"
+ integrity sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==
+
+vscode-jsonrpc@8.2.0:
+ version "8.2.0"
+ resolved "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz"
+ integrity sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==
+
+vscode-languageserver-protocol@3.17.5:
+ version "3.17.5"
+ resolved "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz"
+ integrity sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==
+ dependencies:
+ vscode-jsonrpc "8.2.0"
+ vscode-languageserver-types "3.17.5"
+
+vscode-languageserver-textdocument@~1.0.11:
+ version "1.0.12"
+ resolved "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz"
+ integrity sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==
+
+vscode-languageserver-types@3.17.5:
+ version "3.17.5"
+ resolved "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz"
+ integrity sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==
+
+vscode-languageserver@~9.0.1:
+ version "9.0.1"
+ resolved "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz"
+ integrity sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==
+ dependencies:
+ vscode-languageserver-protocol "3.17.5"
+
+vscode-uri@~3.0.8:
+ version "3.0.8"
+ resolved "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.0.8.tgz"
+ integrity sha512-AyFQ0EVmsOZOlAnxoFOGOq1SQDWAB7C6aqMGS23svWAllfOaxbuFvcT8D1i8z3Gyn8fraVeZNNmN6e9bxxXkKw==
+
+web-namespaces@^2.0.0:
+ version "2.0.1"
+ resolved "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz"
+ integrity sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==
+
+webidl-conversions@^3.0.0:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
+ integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==
+
+whatwg-url@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
+ integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==
+ dependencies:
+ tr46 "~0.0.3"
+ webidl-conversions "^3.0.0"
+
+which-pm-runs@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz"
+ integrity sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==
+
+which-pm@^3.0.0, which-pm@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.npmjs.org/which-pm/-/which-pm-3.0.1.tgz"
+ integrity sha512-v2JrMq0waAI4ju1xU5x3blsxBBMgdgZve580iYMN5frDaLGjbA24fok7wKCsya8KLVO19Ju4XDc5+zTZCJkQfg==
+ dependencies:
+ load-yaml-file "^0.2.0"
+
+widest-line@^5.0.0:
+ version "5.0.0"
+ resolved "https://registry.npmjs.org/widest-line/-/widest-line-5.0.0.tgz"
+ integrity sha512-c9bZp7b5YtRj2wOe6dlj32MK+Bx/M/d+9VB2SHM1OtsUHR0aV0tdP6DWh/iMt0kWi1t5g1Iudu6hQRNd1A4PVA==
+ dependencies:
+ string-width "^7.0.0"
+
+wrap-ansi@^9.0.0:
+ version "9.0.0"
+ resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz"
+ integrity sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==
+ dependencies:
+ ansi-styles "^6.2.1"
+ string-width "^7.0.0"
+ strip-ansi "^7.1.0"
+
+xml-js@^1.6.8:
+ version "1.6.11"
+ resolved "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz"
+ integrity sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==
+ dependencies:
+ sax "^1.2.4"
+
+xml@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz"
+ integrity sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==
+
+xxhash-wasm@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz"
+ integrity sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==
+
+yallist@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz"
+ integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
+
+yargs-parser@^21.1.1:
+ version "21.1.1"
+ resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz"
+ integrity sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==
+
+yocto-queue@^1.1.1:
+ version "1.2.1"
+ resolved "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz"
+ integrity sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==
+
+zod-to-json-schema@^3.23.5:
+ version "3.24.6"
+ resolved "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.24.6.tgz"
+ integrity sha512-h/z3PKvcTcTetyjl1fkj79MHNEjm+HpD6NXheWjzOekY7kV+lwDYnHw+ivHkijnCSMz1yJaWBD9vu/Fcmk+vEg==
+
+zod-to-ts@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmjs.org/zod-to-ts/-/zod-to-ts-1.2.0.tgz"
+ integrity sha512-x30XE43V+InwGpvTySRNz9kB7qFU8DlyEy7BsSTCHPH1R0QasMmHWZDCzYm6bVXtj/9NNJAZF3jW8rzFvH5OFA==
+
+zod@^3, zod@^3.23.8, zod@^3.24.1, zod@^3.25.76:
+ version "3.25.76"
+ resolved "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz"
+ integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==
+
+zwitch@^2.0.0, zwitch@^2.0.4:
+ version "2.0.4"
+ resolved "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz"
+ integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==
diff --git a/content/articles/multi-harness-rl/create-research-article/cli.js b/content/articles/multi-harness-rl/create-research-article/cli.js
new file mode 100755
index 00000000..77aafa4d
--- /dev/null
+++ b/content/articles/multi-harness-rl/create-research-article/cli.js
@@ -0,0 +1,635 @@
+#!/usr/bin/env node
+
+import { execSync } from "child_process";
+import fs from "fs";
+import path from "path";
+import { createInterface } from "readline";
+
+// =============================================================================
+// Terminal formatting (zero deps)
+// =============================================================================
+
+const fmt = {
+ bold: (s) => `\x1b[1m${s}\x1b[22m`,
+ green: (s) => `\x1b[32m${s}\x1b[39m`,
+ cyan: (s) => `\x1b[36m${s}\x1b[39m`,
+ red: (s) => `\x1b[31m${s}\x1b[39m`,
+ dim: (s) => `\x1b[2m${s}\x1b[22m`,
+ yellow: (s) => `\x1b[33m${s}\x1b[39m`,
+};
+
+const TEMPLATE_REPO =
+ "https://huggingface.co/spaces/tfrere/research-article-template";
+
+// =============================================================================
+// Prompt helpers
+// =============================================================================
+
+function createPrompt() {
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
+
+ const ask = (question, defaultValue) =>
+ new Promise((resolve) => {
+ const hint = defaultValue ? ` ${fmt.dim(`[${defaultValue}]`)}` : "";
+ rl.question(` ${question}${hint}: `, (answer) => {
+ resolve(answer.trim() || defaultValue || "");
+ });
+ });
+
+ const select = (question, options) =>
+ new Promise((resolve) => {
+ console.log(`\n ${question}\n`);
+ options.forEach((opt, i) => {
+ const marker = i === 0 ? fmt.cyan(">") : " ";
+ const hint = opt.hint ? fmt.dim(` - ${opt.hint}`) : "";
+ console.log(` ${marker} ${fmt.bold(String(i + 1))}. ${opt.label}${hint}`);
+ });
+ const tryRead = () => {
+ rl.question(`\n Choice ${fmt.dim(`[1-${options.length}]`)}: `, (answer) => {
+ const idx = parseInt(answer, 10) - 1;
+ if (idx >= 0 && idx < options.length) {
+ resolve(options[idx].value);
+ } else if (answer.trim() === "") {
+ resolve(options[0].value);
+ } else {
+ tryRead();
+ }
+ });
+ };
+ tryRead();
+ });
+
+ const confirm = (question, defaultYes = true) =>
+ new Promise((resolve) => {
+ const hint = defaultYes ? fmt.dim(" [Y/n]") : fmt.dim(" [y/N]");
+ rl.question(` ${question}${hint}: `, (answer) => {
+ const a = answer.trim().toLowerCase();
+ if (a === "") resolve(defaultYes);
+ else resolve(a === "y" || a === "yes");
+ });
+ });
+
+ return { rl, ask, select, confirm, close: () => rl.close() };
+}
+
+// =============================================================================
+// Content generators
+// =============================================================================
+
+function todayFormatted() {
+ const d = new Date();
+ const months = [
+ "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
+ ];
+ return `${months[d.getMonth()]}. ${String(d.getDate()).padStart(2, "0")}, ${d.getFullYear()}`;
+}
+
+function generateArticleMdx(cfg) {
+ const authorLine = cfg.authorUrl
+ ? ` - name: "${cfg.authorName}"\n url: "${cfg.authorUrl}"\n affiliations: [1]`
+ : ` - name: "${cfg.authorName}"\n affiliations: [1]`;
+
+ const affiliationLine = cfg.affiliationUrl
+ ? ` - name: "${cfg.affiliationName}"\n url: "${cfg.affiliationUrl}"`
+ : ` - name: "${cfg.affiliationName}"`;
+
+ const isArticle = cfg.layout === "article";
+
+ return `---
+title: "${cfg.title}"
+description: "${cfg.description}"
+authors:
+${authorLine}
+affiliations:
+${affiliationLine}
+published: "${todayFormatted()}"
+template: "${cfg.layout}"
+tableOfContentsAutoCollapse: ${isArticle}
+showPdf: ${isArticle}
+---
+
+import Introduction from "./chapters/introduction.mdx";
+
+
+`;
+}
+
+function generateIntroChapter(layout) {
+ if (layout === "article") {
+ return `## Abstract
+
+Write your abstract here. This section should briefly summarize the key points of your research.
+
+## Introduction
+
+Start writing your article here. You can use all the features of the template:
+
+- **Markdown** for text formatting
+- **KaTeX** for math: $E = mc^2$
+- **Citations** using BibTeX references [@example2025]
+- **D3.js embeds** for interactive visualizations
+- **Images** with automatic figure numbering
+
+See the [template documentation](https://huggingface.co/spaces/tfrere/research-article-template) for more details.
+`;
+ }
+
+ return `## Introduction
+
+Start writing your paper here. You can use all the features of the template:
+
+- **Markdown** for text formatting
+- **KaTeX** for math: $E = mc^2$
+- **Citations** using BibTeX references [@example2025]
+- **D3.js embeds** for interactive visualizations
+- **Images** with automatic figure numbering
+
+See the [template documentation](https://huggingface.co/spaces/tfrere/research-article-template) for more details.
+`;
+}
+
+function generateBibliography() {
+ return `@article{example2025,
+ title={Example Reference},
+ author={Author, Example},
+ journal={Journal of Examples},
+ year={2025}
+}
+`;
+}
+
+function spaceIdToUrl(spaceId) {
+ return `https://${spaceId.replace("/", "-").toLowerCase()}.hf.space`;
+}
+
+function generateReadme(name, layout, title, spaceId) {
+ const layoutLabel = layout === "article" ? "Research Article" : "Research Paper";
+ const safeTitle = title || name;
+ const thumbLine = spaceId
+ ? `\nthumbnail: ${spaceIdToUrl(spaceId)}/thumb.auto.jpg`
+ : "";
+ return `---
+title: "${safeTitle}"
+emoji: 📝
+colorFrom: blue
+colorTo: indigo
+sdk: docker
+app_port: 8080
+header: mini
+pinned: false
+tags:
+ - research-article-template${thumbLine}
+---
+
+# ${name}
+
+A ${layoutLabel.toLowerCase()} built with [research-article-template](https://huggingface.co/spaces/tfrere/research-article-template).
+
+## Quick start
+
+\`\`\`bash
+cd app
+npm run dev # dev server at http://localhost:4321
+\`\`\`
+
+## Deploy to Hugging Face
+
+\`\`\`bash
+# 1. Create a Space at huggingface.co/new-space (select Docker SDK)
+# 2. Push to it:
+git remote add space git@hf.co:spaces//
+git push space main
+\`\`\`
+
+That's it. The Dockerfile and nginx config are included.
+
+## Edit your content
+
+| File | What |
+|------|------|
+| \`app/src/content/article.mdx\` | Main article (metadata + chapter imports) |
+| \`app/src/content/chapters/\` | Your chapters (one .mdx per section) |
+| \`app/src/content/bibliography.bib\` | BibTeX references |
+| \`app/src/content/embeds/\` | D3.js HTML visualizations |
+| \`app/src/content/assets/data/\` | CSV/JSON data files |
+| \`app/src/content/assets/image/\` | Images |
+
+## Commands
+
+| Command | Description |
+|---------|-------------|
+| \`npm run dev\` | Dev server |
+| \`npm run build\` | Production build |
+| \`npm run export:pdf\` | Export as PDF |
+| \`npm run export:latex\` | Export as LaTeX |
+| \`npm run sync:template\` | Pull latest template updates |
+
+## License
+
+CC-BY-4.0
+`;
+}
+
+// =============================================================================
+// Cleanup - remove demo content from the cloned template
+// =============================================================================
+
+const CLEANUP_DIRS = [
+ ".git",
+ ".cursor/rules",
+ ".vscode",
+ ".playwright-mcp",
+ "app/.claude",
+ "app/venv",
+ "app/screenshots",
+ "app/src/content/chapters/demo",
+ "app/src/content/embeds/demo",
+ "app/src/content/embeds/arxiv",
+ "app/src/content/embeds/smol-playbook",
+ "app/src/content/embeds/typography",
+ "app/src/content/assets/audio",
+ "app/src/content/assets/sprites",
+ "app/src/components/demo",
+];
+
+const CLEANUP_FILES = [
+ "AUDIT-2026.md",
+ "ROADMAP-2026.md",
+ "CHANGELOG.md",
+ "CONTRIBUTING.md",
+ "prototype-project-page.html",
+ "app/scripts/fetch-hf-citations.py",
+];
+
+const CLEANUP_GLOBS_IN = {
+ "app/src/content/embeds": (name) =>
+ name !== "banner.html" && name.endsWith(".html"),
+ "app/public": (name) => name.endsWith(".pdf"),
+};
+
+function cleanupProject(dir) {
+ // Remove known directories
+ for (const p of CLEANUP_DIRS) {
+ const full = path.join(dir, p);
+ if (fs.existsSync(full)) fs.rmSync(full, { recursive: true, force: true });
+ }
+
+ // Remove known files
+ for (const p of CLEANUP_FILES) {
+ const full = path.join(dir, p);
+ if (fs.existsSync(full)) fs.rmSync(full, { force: true });
+ }
+
+ // Remove matching files inside specific directories
+ for (const [relDir, matcher] of Object.entries(CLEANUP_GLOBS_IN)) {
+ const full = path.join(dir, relDir);
+ if (!fs.existsSync(full)) continue;
+ for (const entry of fs.readdirSync(full)) {
+ if (matcher(entry)) {
+ fs.rmSync(path.join(full, entry), { recursive: true, force: true });
+ }
+ }
+ }
+
+ // Clear data dir but keep it
+ const dataDir = path.join(dir, "app/src/content/assets/data");
+ if (fs.existsSync(dataDir)) {
+ for (const f of fs.readdirSync(dataDir)) {
+ if (f === ".gitkeep") continue;
+ fs.rmSync(path.join(dataDir, f), { recursive: true, force: true });
+ }
+ fs.writeFileSync(path.join(dataDir, ".gitkeep"), "");
+ }
+
+ // Clear ALL images (LFS pointers are not valid image files)
+ const imageDir = path.join(dir, "app/src/content/assets/image");
+ if (fs.existsSync(imageDir)) {
+ for (const f of fs.readdirSync(imageDir)) {
+ fs.rmSync(path.join(imageDir, f), { recursive: true, force: true });
+ }
+ // Generate a minimal real placeholder PNG (1x1 white pixel)
+ // so the project has a working example image
+ const pngData = Buffer.from(
+ "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAAC0lEQVQI12NgAAIABQAB" +
+ "Nl7BcQAAAABJRU5ErkJggg==",
+ "base64",
+ );
+ fs.writeFileSync(path.join(imageDir, "placeholder.png"), pngData);
+ }
+
+ // Remove all auto-generated and LFS thumbnails from public
+ const publicDir = path.join(dir, "app/public");
+ if (fs.existsSync(publicDir)) {
+ for (const f of fs.readdirSync(publicDir)) {
+ if (
+ f.endsWith(".pdf") ||
+ f.startsWith("thumb.auto") ||
+ f === "thumb.png"
+ ) {
+ fs.rmSync(path.join(publicDir, f), { force: true });
+ }
+ }
+ }
+
+ // Remove LFS-tracked .gitattributes entries that won't apply to new project
+ const gitattrs = path.join(dir, ".gitattributes");
+ if (fs.existsSync(gitattrs)) {
+ fs.rmSync(gitattrs, { force: true });
+ }
+}
+
+// =============================================================================
+// Main
+// =============================================================================
+
+async function main() {
+ const args = process.argv.slice(2);
+
+ if (args.includes("--help") || args.includes("-h")) {
+ console.log(`
+ ${fmt.bold("create-research-article")}
+
+ Scaffold a new research paper or article.
+
+ ${fmt.bold("Usage:")}
+ npx create-research-article [project-name] [options]
+
+ ${fmt.bold("Options:")}
+ --template=article|paper Skip template selection prompt
+ --help, -h Show this help message
+`);
+ process.exit(0);
+ }
+
+ const positionalName = args.find((a) => !a.startsWith("-"));
+ const templateFlag = args
+ .find((a) => a.startsWith("--template="))
+ ?.split("=")[1]
+ ?.toLowerCase();
+
+ console.log("");
+ console.log(
+ ` ${fmt.bold("create-research-article")} ${fmt.dim("v0.2.0")}`,
+ );
+ console.log(fmt.dim(" Scaffold a new research paper or article\n"));
+
+ // Check git is available
+ try {
+ execSync("git --version", { stdio: "ignore" });
+ } catch {
+ console.error(fmt.red(" Error: git is required but not installed.\n"));
+ process.exit(1);
+ }
+
+ const prompt = createPrompt();
+
+ try {
+ // 1. Project name
+ const name =
+ positionalName ||
+ (await prompt.ask("Project name", "my-research-article"));
+
+ const targetDir = path.resolve(process.cwd(), name);
+ if (fs.existsSync(targetDir)) {
+ console.error(
+ fmt.red(`\n Error: directory "${name}" already exists.\n`),
+ );
+ prompt.close();
+ process.exit(1);
+ }
+
+ // 2. Layout
+ let layout;
+ if (templateFlag && ["article", "paper"].includes(templateFlag)) {
+ layout = templateFlag;
+ console.log(fmt.cyan(` Using template: ${layout}`));
+ } else {
+ if (templateFlag) {
+ console.log(fmt.yellow(` Warning: unknown template "${templateFlag}", showing picker.\n`));
+ }
+ layout = await prompt.select("Choose a layout:", [
+ {
+ value: "article",
+ label: "Research Article",
+ hint: "Full layout with banner, TOC, DOI, citations, PDF export",
+ },
+ {
+ value: "paper",
+ label: "Research Paper",
+ hint: "Lighter single-column layout, blog-friendly",
+ },
+ ]);
+ }
+
+ // 3. Metadata
+ console.log("");
+ const title = await prompt.ask("Title", "My Research Article");
+ const description = await prompt.ask("Short description", "");
+ const authorName = await prompt.ask("Author name", "");
+ const authorUrl = await prompt.ask("Author URL", "");
+ const affiliationName = await prompt.ask("Affiliation", "");
+ const affiliationUrl = await prompt.ask("Affiliation URL", "");
+
+ prompt.close();
+
+ // 4. Clone
+ console.log("");
+ console.log(fmt.cyan(" Cloning template..."));
+ execSync(
+ `GIT_LFS_SKIP_SMUDGE=1 git clone --depth 1 "${TEMPLATE_REPO}" "${targetDir}"`,
+ { stdio: "pipe" },
+ );
+
+ // 5. Cleanup
+ console.log(fmt.cyan(" Cleaning up demo content..."));
+ cleanupProject(targetDir);
+
+ // 6. Generate content
+ console.log(fmt.cyan(" Generating project files..."));
+
+ fs.writeFileSync(
+ path.join(targetDir, "app/src/content/article.mdx"),
+ generateArticleMdx({
+ layout,
+ title,
+ description,
+ authorName,
+ authorUrl,
+ affiliationName,
+ affiliationUrl,
+ }),
+ );
+
+ // Rename chapter: remove old, create new
+ const chaptersDir = path.join(targetDir, "app/src/content/chapters");
+ if (fs.existsSync(chaptersDir)) {
+ for (const f of fs.readdirSync(chaptersDir)) {
+ fs.rmSync(path.join(chaptersDir, f), { recursive: true, force: true });
+ }
+ } else {
+ fs.mkdirSync(chaptersDir, { recursive: true });
+ }
+ fs.writeFileSync(
+ path.join(chaptersDir, "introduction.mdx"),
+ generateIntroChapter(layout),
+ );
+
+ fs.writeFileSync(
+ path.join(targetDir, "app/src/content/bibliography.bib"),
+ generateBibliography(),
+ );
+
+ fs.writeFileSync(path.join(targetDir, "README.md"), generateReadme(name, layout, title));
+
+ // 7. Init git
+ execSync("git init", { cwd: targetDir, stdio: "pipe" });
+ execSync("git add -A", { cwd: targetDir, stdio: "pipe" });
+ execSync('git commit -m "Initial scaffold from create-research-article"', {
+ cwd: targetDir,
+ stdio: "pipe",
+ });
+
+ // 8. Install dependencies
+ console.log(fmt.cyan(" Installing dependencies (this may take a moment)..."));
+ try {
+ execSync("npm install", {
+ cwd: path.join(targetDir, "app"),
+ stdio: "pipe",
+ });
+ console.log(fmt.green(" Dependencies installed."));
+ } catch {
+ console.log(
+ fmt.yellow(
+ " Warning: npm install failed. Run it manually: cd app && npm install",
+ ),
+ );
+ }
+
+ // 9. Optional: deploy to Hugging Face
+ let deployed = false;
+ const hasHfCli = (() => {
+ try {
+ execSync("huggingface-cli whoami", { stdio: "pipe" });
+ return true;
+ } catch {
+ return false;
+ }
+ })();
+
+ if (hasHfCli) {
+ const prompt2 = createPrompt();
+ console.log("");
+ const wantDeploy = await prompt2.confirm(
+ "Deploy to Hugging Face Spaces?",
+ true,
+ );
+
+ if (wantDeploy) {
+ const hfUser = execSync("huggingface-cli whoami", { encoding: "utf8" })
+ .split("\n")[0]
+ .trim();
+ const defaultSpace = `${hfUser}/${name}`;
+ const spaceId = await prompt2.ask("Space ID", defaultSpace);
+ prompt2.close();
+
+ console.log(fmt.cyan(` Creating Space ${spaceId}...`));
+ try {
+ execSync(
+ `huggingface-cli repo create "${spaceId}" --repo-type space --space_sdk docker --exist-ok`,
+ { stdio: "pipe" },
+ );
+
+ execSync(
+ `git remote add space "git@hf.co:spaces/${spaceId}"`,
+ { cwd: targetDir, stdio: "pipe" },
+ );
+
+ // Update README thumbnail URL to point to the Space's own auto-generated thumb
+ const readmePath = path.join(targetDir, "README.md");
+ const readmeContent = fs.readFileSync(readmePath, "utf8");
+ const thumbUrl = `${spaceIdToUrl(spaceId)}/thumb.auto.jpg`;
+ if (!readmeContent.includes("thumbnail:")) {
+ const updated = readmeContent.replace(
+ /^(tags:\n(?:\s+-[^\n]+\n?)+)/m,
+ `$1thumbnail: ${thumbUrl}\n`,
+ );
+ fs.writeFileSync(readmePath, updated);
+ } else {
+ const updated = readmeContent.replace(
+ /^thumbnail:.*$/m,
+ `thumbnail: ${thumbUrl}`,
+ );
+ fs.writeFileSync(readmePath, updated);
+ }
+ execSync("git add README.md && git commit --amend --no-edit", {
+ cwd: targetDir,
+ stdio: "pipe",
+ });
+
+ console.log(fmt.cyan(" Pushing to Hugging Face..."));
+ execSync("git push space main", {
+ cwd: targetDir,
+ stdio: "pipe",
+ });
+
+ deployed = true;
+ const spaceUrl = `https://huggingface.co/spaces/${spaceId}`;
+ console.log(fmt.green(` Deployed! ${spaceUrl}`));
+ } catch (e) {
+ console.log(
+ fmt.yellow(` Warning: deploy failed. ${e.message}`),
+ );
+ console.log(
+ fmt.dim(" You can deploy manually later with: git push space main"),
+ );
+ }
+ } else {
+ prompt2.close();
+ }
+ }
+
+ // 10. Success
+ console.log("");
+ console.log(fmt.green(fmt.bold(" Done! Project created successfully.")));
+ console.log("");
+ console.log(` ${fmt.bold("Next steps:")}`);
+ console.log("");
+ console.log(fmt.cyan(` cd ${name}/app`));
+ console.log(fmt.cyan(" npm run dev"));
+ if (!deployed && !hasHfCli) {
+ console.log("");
+ console.log(` ${fmt.dim("To deploy to Hugging Face:")}`);
+ console.log(
+ fmt.dim(
+ " pip install huggingface_hub && huggingface-cli login",
+ ),
+ );
+ console.log(
+ fmt.dim(
+ ` huggingface-cli repo create ${name} --repo-type space --space_sdk docker`,
+ ),
+ );
+ console.log(
+ fmt.dim(
+ ` cd ${name} && git remote add space git@hf.co:spaces//${name} && git push space main`,
+ ),
+ );
+ } else if (!deployed) {
+ console.log("");
+ console.log(` ${fmt.dim("To deploy later:")}`);
+ console.log(
+ fmt.dim(
+ ` cd ${name} && git remote add space git@hf.co:spaces//${name} && git push space main`,
+ ),
+ );
+ }
+ console.log("");
+ } catch (err) {
+ prompt.close();
+ console.error(fmt.red(`\n Error: ${err.message}\n`));
+ process.exit(1);
+ }
+}
+
+main();
diff --git a/content/articles/multi-harness-rl/create-research-article/package.json b/content/articles/multi-harness-rl/create-research-article/package.json
new file mode 100644
index 00000000..1ed76616
--- /dev/null
+++ b/content/articles/multi-harness-rl/create-research-article/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "create-research-article",
+ "version": "0.4.0",
+ "description": "Scaffold a new research paper or article from the research-article-template",
+ "license": "CC-BY-4.0",
+ "type": "module",
+ "bin": {
+ "create-research-article": "cli.js"
+ },
+ "files": [
+ "cli.js"
+ ],
+ "engines": {
+ "node": ">=18.0.0"
+ },
+ "keywords": [
+ "create",
+ "scaffold",
+ "research",
+ "paper",
+ "article",
+ "template",
+ "astro",
+ "mdx",
+ "scientific-writing"
+ ],
+ "author": {
+ "name": "Thibaud Frere",
+ "url": "https://huggingface.co/tfrere"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://huggingface.co/spaces/tfrere/research-article-template"
+ }
+}
\ No newline at end of file
diff --git a/content/articles/multi-harness-rl/entrypoint.sh b/content/articles/multi-harness-rl/entrypoint.sh
new file mode 100644
index 00000000..11569d7c
--- /dev/null
+++ b/content/articles/multi-harness-rl/entrypoint.sh
@@ -0,0 +1,52 @@
+#!/bin/bash
+set -e
+
+echo "🚀 Starting Hugging Face Space..."
+
+# Ensure proper permissions for the app directory at runtime
+chmod -R 755 /app/src/content/assets 2>/dev/null || true
+
+# Check if Notion import is enabled and token is available
+if [ "${ENABLE_NOTION_IMPORT:-false}" = "true" ] && [ -n "$NOTION_TOKEN" ] && [ -n "$NOTION_PAGE_ID" ]; then
+ echo "🔄 Notion import enabled - fetching content from Notion..."
+ echo " Page ID: $NOTION_PAGE_ID"
+
+ cd /app
+
+ if npm run notion:import 2>&1; then
+ echo "✅ Notion import completed successfully!"
+
+ echo "🔨 Rebuilding site with Notion content..."
+ npm run build 2>&1
+
+ # PDF export only if Playwright was installed at build time
+ if [ "${ENABLE_PDF_EXPORT:-false}" = "true" ] && command -v chromium > /dev/null 2>&1; then
+ echo "📄 Generating PDF..."
+ npm run export:pdf -- --theme=light --wait=full 2>&1 || echo "⚠️ PDF generation failed (non-critical)"
+ else
+ echo "⏭️ Skipping PDF generation (Playwright not available)"
+ fi
+
+ echo "✅ Site rebuilt and ready!"
+ else
+ echo "⚠️ Notion import failed - using pre-built content"
+ fi
+else
+ if [ "${ENABLE_NOTION_IMPORT:-false}" = "true" ]; then
+ echo "⚠️ Notion import enabled but NOTION_TOKEN or NOTION_PAGE_ID not found"
+ echo " NOTION_TOKEN: ${NOTION_TOKEN:+SET}${NOTION_TOKEN:-NOT SET}"
+ echo " NOTION_PAGE_ID: ${NOTION_PAGE_ID:+SET}${NOTION_PAGE_ID:-NOT SET}"
+ echo " → Using pre-built content"
+ else
+ echo "⏭️ Notion import disabled - serving pre-built content"
+ fi
+fi
+
+if [ -x /opt/review/bin/uvicorn ]; then
+ echo "💬 Starting review API (REVIEW_MODE=${REVIEW_MODE:-off})..."
+ ( cd /app/review && /opt/review/bin/uvicorn server:app --host 127.0.0.1 --port 7861 \
+ --proxy-headers --forwarded-allow-ips 127.0.0.1 ) &
+fi
+
+echo "🌐 Starting nginx on port 8080..."
+exec nginx -g 'daemon off;'
diff --git a/content/articles/multi-harness-rl/nginx.conf b/content/articles/multi-harness-rl/nginx.conf
new file mode 100644
index 00000000..bc8ec1dc
--- /dev/null
+++ b/content/articles/multi-harness-rl/nginx.conf
@@ -0,0 +1,78 @@
+worker_processes auto;
+pid /tmp/nginx.pid;
+
+events {
+ worker_connections 1024;
+ use epoll;
+ multi_accept on;
+}
+
+http {
+ include /etc/nginx/mime.types;
+ default_type application/octet-stream;
+
+ access_log /tmp/access.log;
+ error_log /tmp/error.log;
+
+ sendfile on;
+ keepalive_timeout 65;
+
+ # Gzip compression
+ gzip on;
+ gzip_vary on;
+ gzip_proxied any;
+ gzip_comp_level 5;
+ gzip_min_length 256;
+ gzip_types
+ text/plain
+ text/css
+ application/javascript
+ application/json
+ application/xml
+ application/rss+xml
+ image/svg+xml;
+
+ server {
+ listen 8080;
+ server_name localhost;
+
+ root /app/dist;
+ index index.html;
+
+ # Health check endpoint
+ location = /health {
+ return 200 "ok";
+ add_header Content-Type text/plain;
+ }
+
+ # Review comments and Hugging Face sign-in, served by review/server.py
+ location /api/review/ {
+ proxy_pass http://127.0.0.1:7861;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ client_max_body_size 64k;
+ add_header Cache-Control "no-store" always;
+ }
+ location /oauth/ {
+ proxy_pass http://127.0.0.1:7861;
+ proxy_set_header Host $host;
+ proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ add_header Cache-Control "no-store" always;
+ }
+
+ # Cache immutable build assets from Astro
+ location /_astro/ {
+ expires 30d;
+ add_header Cache-Control "public, max-age=2592000, immutable";
+ }
+
+ # Handle all routes with fallback to index.html first
+ location / {
+ # Try to serve the file directly, then with /index.html appended,
+ # then fall back to root index.html
+ try_files $uri $uri/index.html /index.html;
+ }
+ }
+}
diff --git a/content/articles/multi-harness-rl/review/pull_comments.py b/content/articles/multi-harness-rl/review/pull_comments.py
new file mode 100644
index 00000000..125c094a
--- /dev/null
+++ b/content/articles/multi-harness-rl/review/pull_comments.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+"""Pull every review thread from the Space into one readable file.
+
+ uv run --with huggingface_hub python review/pull_comments.py # from https://fineenvs-multi-harness-rl.hf.space
+ uv run --with huggingface_hub python review/pull_comments.py --all # include resolved threads in comments.md
+ uv run --with huggingface_hub python review/pull_comments.py --url http://localhost:4331
+
+Uses the owner-only /api/review/export endpoint with your local Hugging Face login (the token only
+proves to the Space who is asking). Writes review/.pulled/threads/*.json, one per thread, and
+review/.pulled/comments.md, grouped by chapter and section with the quoted text each comment points
+at. review/.pulled/ is git-ignored, so the comments never reach the repository or the Space.
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import urllib.request
+from collections import defaultdict
+from pathlib import Path
+
+from huggingface_hub import get_token
+
+HERE = Path(__file__).resolve().parent
+
+
+def main() -> None:
+ ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
+ ap.add_argument("--url", default="https://fineenvs-multi-harness-rl.hf.space")
+ ap.add_argument("--out", default=str(HERE / ".pulled"))
+ ap.add_argument("--all", action="store_true", help="include resolved threads in comments.md")
+ args = ap.parse_args()
+
+ token = get_token()
+ if not token:
+ raise SystemExit("Log in first: hf auth login")
+ req = urllib.request.Request(f"{args.url.rstrip('/')}/api/review/export", headers={"Authorization": f"Bearer {token}"})
+ threads = json.load(urllib.request.urlopen(req))
+
+ out = Path(args.out)
+ (out / "threads").mkdir(parents=True, exist_ok=True)
+ for t in threads:
+ (out / "threads" / f"{t['id']}.json").write_text(json.dumps(t, ensure_ascii=False, indent=1))
+
+ groups: dict[tuple[str, str], list[dict]] = defaultdict(list)
+ for t in sorted(threads, key=lambda t: t["created_at"]):
+ if t.get("status") == "resolved" and not args.all:
+ continue
+ a = t["anchor"]
+ groups[(a.get("chapter") or "(no chapter)", a.get("section") or "")].append(t)
+
+ lines = [f"# Review comments ({sum(len(v) for v in groups.values())} shown, {len(threads)} in total)", ""]
+ for (chapter, section), ts in groups.items():
+ lines += [f"## {chapter}" + (f" › {section}" if section else ""), ""]
+ for t in ts:
+ a = t["anchor"]
+ target = f"Figure: {a.get('title') or a.get('figure')}" if a["type"] == "figure" else f"“{a['quote']}”"
+ state = t.get("decision") or t.get("status", "open")
+ lines += [f"### {t['id']} [{state}]", f"> {target}", ""]
+ sug = t.get("suggestion")
+ if sug:
+ lines += [f"**Suggestion:** delete “{a['quote']}”" if sug["action"] == "delete"
+ else f"**Suggestion:** replace “{a['quote']}” with “{sug['text']}”", ""]
+ for m in t["messages"]:
+ lines.append(f"- **{m['author']['name']}** ({m['author']['username']}, {m['created_at']}): {m['body']}")
+ lines.append("")
+ (out / "comments.md").write_text("\n".join(lines))
+ print(f"{len(threads)} threads from {args.url} -> {out}/comments.md")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/content/articles/multi-harness-rl/review/requirements.txt b/content/articles/multi-harness-rl/review/requirements.txt
new file mode 100644
index 00000000..fa1ac617
--- /dev/null
+++ b/content/articles/multi-harness-rl/review/requirements.txt
@@ -0,0 +1,3 @@
+fastapi>=0.110
+uvicorn>=0.30
+huggingface_hub[oauth]>=2.0.0
diff --git a/content/articles/multi-harness-rl/review/server.py b/content/articles/multi-harness-rl/review/server.py
new file mode 100644
index 00000000..cfd97ae5
--- /dev/null
+++ b/content/articles/multi-harness-rl/review/server.py
@@ -0,0 +1,428 @@
+"""Review comments for the article, stored on a storage bucket mounted in the Space.
+
+The article is a static site served by nginx. This small API runs next to it, behind nginx at
+/api/review/, and adds one thing: reviewers can sign in with Hugging Face, select text or a figure,
+and leave a comment. Each thread is one JSON file under COMMENTS_DIR, which defaults to a folder on the
+storage bucket mounted at /data, so comments survive restarts and redeploys.
+
+Two modes, set with the REVIEW_MODE Space variable:
+- off (default): the published article. Every review endpoint answers 404 and the page never loads
+ the review layer.
+- on: people who open the article with ?review can sign in and comment, if they are on the reviewer
+ list (REVIEWERS) or own the Space.
+
+Configuration (Space variables):
+ REVIEW_MODE "on" to accept comments, anything else to stay published
+ REVIEWERS comma-separated Hugging Face usernames allowed to read and write comments
+ COMMENTS_DIR folder for the threads, default /data/review-comments (the mounted bucket)
+
+The Space owner can also export and import every thread with their Hugging Face token in an
+Authorization header (see review/pull_comments.py). The token is only used to check who is calling.
+"""
+
+from __future__ import annotations
+
+import hashlib
+import json
+import os
+import secrets
+import threading
+import time
+from pathlib import Path
+from typing import Any
+
+from fastapi import FastAPI, HTTPException, Request
+from fastapi.responses import JSONResponse
+from starlette.concurrency import run_in_threadpool
+from huggingface_hub import HfApi, attach_huggingface_oauth, parse_huggingface_oauth
+
+REVIEW_MODE = os.environ.get("REVIEW_MODE", "off").strip().lower() == "on"
+OWNER = (os.environ.get("REVIEW_OWNER") or os.environ.get("SPACE_AUTHOR_NAME") or "").strip()
+REVIEWERS = {u.strip().lower() for u in os.environ.get("REVIEWERS", "").split(",") if u.strip()}
+if OWNER:
+ REVIEWERS.add(OWNER.lower())
+COMMENTS_DIR = Path(os.environ.get("COMMENTS_DIR", "").strip() or "/data/review-comments")
+
+MAX_BODY = 5000
+MAX_QUOTE = 1200
+WRITES_PER_MINUTE = 30
+
+app = FastAPI(docs_url=None, redoc_url=None, openapi_url=None)
+try:
+ attach_huggingface_oauth(app)
+except ValueError as e:
+ # In a Space, sign-in needs `hf_oauth: true` in the README metadata. Without it, stay published.
+ if REVIEW_MODE:
+ print(f"[review] review mode is off: {e} Add `hf_oauth: true` to the README metadata.", flush=True)
+ REVIEW_MODE = False
+
+
+# ---------------------------------------------------------------- storage
+
+
+class Store:
+ """All threads in memory, written through to one JSON file each under COMMENTS_DIR."""
+
+ def __init__(self, root: Path) -> None:
+ self.root = root / "threads"
+ self.lock = threading.Lock()
+ self.threads: dict[str, dict[str, Any]] = {}
+ self.error = ""
+
+ def ready(self) -> bool:
+ """Create the folder if we can, and refuse a /data that is not a mounted volume."""
+ # Without a mounted bucket a root container can still create /data, but on the temporary
+ # disk, where every comment would vanish at the next restart.
+ if str(self.root).startswith("/data/") and not os.path.ismount("/data") and os.environ.get("ALLOW_EPHEMERAL") != "1":
+ self.error = "No storage bucket is mounted at /data on this Space, so comments cannot be saved yet."
+ return False
+ try:
+ self.root.mkdir(parents=True, exist_ok=True)
+ probe = self.root / ".write-test"
+ probe.write_text("ok")
+ probe.unlink()
+ self.error = ""
+ return True
+ except OSError as e:
+ self.error = (f"Cannot write comments to {self.root} ({e.strerror}). "
+ "Mount a storage bucket at /data (read & write) in the Space settings.")
+ return False
+
+ def load(self) -> None:
+ threads: dict[str, dict[str, Any]] = {}
+ if self.ready():
+ for f in self.root.glob("*.json"):
+ try:
+ t = json.loads(f.read_text())
+ threads[t["id"]] = t
+ except (OSError, ValueError, KeyError) as e:
+ print(f"[review] skipping {f.name}: {e}", flush=True)
+ with self.lock:
+ self.threads = threads
+
+ def save(self, thread: dict[str, Any]) -> None:
+ if not self.ready():
+ raise HTTPException(503, self.error)
+ # Write to a temporary file and rename, so a crash never leaves half a thread on disk.
+ path = self.root / f"{thread['id']}.json"
+ body = json.dumps(thread, ensure_ascii=False, indent=1)
+ tmp = path.with_suffix(".json.tmp")
+ try:
+ tmp.write_text(body)
+ os.replace(tmp, path)
+ except OSError:
+ # A mounted bucket may not support rename; write the file in place instead.
+ tmp.unlink(missing_ok=True)
+ path.write_text(body)
+ with self.lock:
+ self.threads[thread["id"]] = thread
+
+ def delete(self, tid: str) -> None:
+ (self.root / f"{tid}.json").unlink(missing_ok=True)
+ with self.lock:
+ self.threads.pop(tid, None)
+
+ def all(self) -> list[dict[str, Any]]:
+ with self.lock:
+ return sorted(self.threads.values(), key=lambda t: t["created_at"])
+
+ def get(self, tid: str) -> dict[str, Any]:
+ with self.lock:
+ t = self.threads.get(tid)
+ if t is None:
+ raise HTTPException(404, "Thread not found.")
+ return json.loads(json.dumps(t))
+
+
+store = Store(COMMENTS_DIR)
+
+
+@app.on_event("startup")
+def _startup() -> None:
+ if REVIEW_MODE:
+ try:
+ store.load()
+ except Exception as e: # the page still works, the panel shows the error
+ print(f"[review] could not load comments: {e}", flush=True)
+
+
+# ---------------------------------------------------------------- auth
+
+
+def _user(request: Request) -> dict[str, str] | None:
+ info = parse_huggingface_oauth(request)
+ if info is None:
+ return None
+ u = info.user_info
+ return {"username": u.preferred_username, "name": u.name or u.preferred_username, "avatar": u.picture or ""}
+
+
+def _require_reviewer(request: Request) -> dict[str, str]:
+ if not REVIEW_MODE:
+ raise HTTPException(404)
+ user = _user(request)
+ if user is None:
+ raise HTTPException(401, "Sign in with Hugging Face to review.")
+ if user["username"].lower() not in REVIEWERS:
+ raise HTTPException(403, "This account is not on the reviewer list.")
+ return user
+
+
+_writes: dict[str, list[float]] = {}
+
+
+def _guard_write(request: Request, user: dict[str, str]) -> None:
+ # The session cookie is SameSite=None so sign-in works inside the Space iframe. A custom header
+ # can't be sent cross-site without a CORS preflight, which this API never grants, so requiring
+ # it blocks forged posts from other sites.
+ if request.headers.get("x-review") != "1":
+ raise HTTPException(403, "Missing review header.")
+ now = time.time()
+ recent = [t for t in _writes.get(user["username"], []) if now - t < 60]
+ if len(recent) >= WRITES_PER_MINUTE:
+ raise HTTPException(429, "Too many comments in a minute. Try again shortly.")
+ _writes[user["username"]] = recent + [now]
+
+
+def _clean(text: Any, limit: int) -> str:
+ s = str(text or "").replace("\r\n", "\n").strip()
+ if not s:
+ raise HTTPException(400, "Empty text.")
+ return s[:limit]
+
+
+def _now() -> str:
+ return time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())
+
+
+# ---------------------------------------------------------------- routes
+
+
+@app.get("/api/review/status")
+def status(request: Request) -> JSONResponse:
+ if not REVIEW_MODE:
+ return JSONResponse({"enabled": False})
+ user = _user(request)
+ return JSONResponse({
+ "enabled": True,
+ "user": user,
+ "reviewer": bool(user and user["username"].lower() in REVIEWERS),
+ "owner": OWNER,
+ "storage": store.error or "ok",
+ })
+
+
+@app.get("/api/review/threads")
+def list_threads(request: Request) -> list[dict[str, Any]]:
+ _require_reviewer(request)
+ if store.error and not store.ready():
+ raise HTTPException(503, store.error)
+ return store.all()
+
+
+@app.post("/api/review/threads")
+async def create_thread(request: Request) -> dict[str, Any]:
+ user = _require_reviewer(request)
+ _guard_write(request, user)
+ payload = await request.json()
+ a = payload.get("anchor") or {}
+ kind = a.get("type")
+ if kind == "text":
+ anchor = {"type": "text", "quote": _clean(a.get("quote"), MAX_QUOTE),
+ "prefix": str(a.get("prefix") or "")[-80:], "suffix": str(a.get("suffix") or "")[:80]}
+ elif kind == "figure":
+ anchor = {"type": "figure", "figure": _clean(a.get("figure"), 200), "title": str(a.get("title") or "")[:300]}
+ else:
+ raise HTTPException(400, "Unknown anchor type.")
+ anchor["section"] = str(a.get("section") or "")[:200]
+ anchor["chapter"] = str(a.get("chapter") or "")[:200]
+ # A suggestion proposes an edit to the selected text, as in Google Docs' suggesting mode: delete
+ # it, or replace it with new text. The owner accepts or rejects it; accepted ones are applied to
+ # the article source afterwards.
+ suggestion = None
+ sug = payload.get("suggestion")
+ if sug:
+ if kind != "text":
+ raise HTTPException(400, "Suggestions need selected text.")
+ action = sug.get("action")
+ if action == "delete":
+ suggestion = {"action": "delete"}
+ elif action == "replace":
+ suggestion = {"action": "replace", "text": _clean(sug.get("text"), MAX_QUOTE)}
+ else:
+ raise HTTPException(400, "Unknown suggestion.")
+ body = str(payload.get("body") or "").replace("\r\n", "\n").strip()[:MAX_BODY]
+ if not body and not suggestion:
+ raise HTTPException(400, "Empty text.")
+ thread = {
+ "id": time.strftime("%Y%m%d-%H%M%S", time.gmtime()) + "-" + secrets.token_hex(3),
+ "anchor": anchor,
+ "status": "open",
+ "created_at": _now(),
+ "messages": [{"id": secrets.token_hex(4), "author": user, "body": body, "created_at": _now()}],
+ }
+ if suggestion:
+ thread["kind"] = "suggestion"
+ thread["suggestion"] = suggestion
+ await run_in_threadpool(store.save, thread)
+ return thread
+
+
+@app.post("/api/review/threads/{tid}/replies")
+async def reply(tid: str, request: Request) -> dict[str, Any]:
+ user = _require_reviewer(request)
+ _guard_write(request, user)
+ payload = await request.json()
+ thread = store.get(tid)
+ thread["messages"].append({"id": secrets.token_hex(4), "author": user, "body": _clean(payload.get("body"), MAX_BODY), "created_at": _now()})
+ await run_in_threadpool(store.save, thread)
+ return thread
+
+
+@app.patch("/api/review/threads/{tid}")
+async def update_thread(tid: str, request: Request) -> dict[str, Any]:
+ user = _require_reviewer(request)
+ _guard_write(request, user)
+ payload = await request.json()
+ thread = store.get(tid)
+ if payload.get("status") in ("open", "resolved"):
+ thread["status"] = payload["status"]
+ thread["resolved_by"] = user["username"] if payload["status"] == "resolved" else None
+ if payload["status"] == "open":
+ thread.pop("decision", None)
+ thread["updated_at"] = _now()
+ if payload.get("decision") in ("accepted", "rejected"):
+ if thread.get("kind") != "suggestion":
+ raise HTTPException(400, "Only suggestions are accepted or rejected.")
+ if user["username"].lower() != OWNER.lower():
+ raise HTTPException(403, "Only the article's owner can accept or reject suggestions.")
+ thread["decision"] = payload["decision"]
+ thread["status"] = "resolved"
+ thread["resolved_by"] = user["username"]
+ thread["updated_at"] = _now()
+ await run_in_threadpool(store.save, thread)
+ return thread
+
+
+REACTIONS = {"👍", "❤️", "😄", "🎉", "👀", "➕", "🔥"}
+
+
+def _own_or_owner(msg: dict[str, Any], user: dict[str, str]) -> bool:
+ return msg["author"]["username"] == user["username"] or user["username"].lower() == OWNER.lower()
+
+
+@app.patch("/api/review/threads/{tid}/messages/{mid}")
+async def edit_message(tid: str, mid: str, request: Request) -> dict[str, Any]:
+ user = _require_reviewer(request)
+ _guard_write(request, user)
+ payload = await request.json()
+ thread = store.get(tid)
+ msg = next((m for m in thread["messages"] if m["id"] == mid), None)
+ if msg is None:
+ raise HTTPException(404, "Message not found.")
+ if msg["author"]["username"] != user["username"]:
+ raise HTTPException(403, "You can only edit your own comments.")
+ msg["body"] = _clean(payload.get("body"), MAX_BODY)
+ msg["edited_at"] = _now()
+ await run_in_threadpool(store.save, thread)
+ return thread
+
+
+@app.post("/api/review/threads/{tid}/messages/{mid}/reactions")
+async def react(tid: str, mid: str, request: Request) -> dict[str, Any]:
+ """Toggle one emoji reaction by the signed-in reviewer."""
+ user = _require_reviewer(request)
+ _guard_write(request, user)
+ emoji = str((await request.json()).get("emoji") or "")
+ if emoji not in REACTIONS:
+ raise HTTPException(400, "Unknown reaction.")
+ thread = store.get(tid)
+ msg = next((m for m in thread["messages"] if m["id"] == mid), None)
+ if msg is None:
+ raise HTTPException(404, "Message not found.")
+ who = msg.setdefault("reactions", {}).setdefault(emoji, [])
+ if user["username"] in who:
+ who.remove(user["username"])
+ else:
+ who.append(user["username"])
+ if not who:
+ msg["reactions"].pop(emoji)
+ await run_in_threadpool(store.save, thread)
+ return thread
+
+
+@app.delete("/api/review/threads/{tid}/messages/{mid}")
+async def delete_message(tid: str, mid: str, request: Request) -> dict[str, Any]:
+ user = _require_reviewer(request)
+ _guard_write(request, user)
+ thread = store.get(tid)
+ msg = next((m for m in thread["messages"] if m["id"] == mid), None)
+ if msg is None:
+ raise HTTPException(404, "Message not found.")
+ if not _own_or_owner(msg, user):
+ raise HTTPException(403, "You can only delete your own comments.")
+ thread["messages"] = [m for m in thread["messages"] if m["id"] != mid]
+ if not thread["messages"]:
+ await run_in_threadpool(store.delete, tid)
+ return {"deleted": tid}
+ await run_in_threadpool(store.save, thread)
+ return thread
+
+
+# ---------------------------------------------------------------- owner: export and import
+
+_token_owner: dict[str, tuple[bool, float]] = {}
+
+
+def _require_owner(request: Request) -> None:
+ """The Space owner, signed in on the page or presenting their Hugging Face token."""
+ if not REVIEW_MODE:
+ raise HTTPException(404)
+ user = _user(request)
+ if user and OWNER and user["username"].lower() == OWNER.lower():
+ return
+ auth = request.headers.get("authorization", "")
+ if not auth.lower().startswith("bearer ") or not OWNER:
+ raise HTTPException(401, "Owner only.")
+ token = auth[7:].strip()
+ key = hashlib.sha256(token.encode()).hexdigest()
+ ok, at = _token_owner.get(key, (False, 0.0))
+ if time.time() - at > 300: # ask the Hub who this is, at most every five minutes per token
+ try:
+ ok = HfApi().whoami(token=token).get("name", "").lower() == OWNER.lower()
+ except Exception:
+ ok = False
+ _token_owner[key] = (ok, time.time())
+ if not ok:
+ raise HTTPException(403, "Owner only.")
+
+
+@app.get("/api/review/export")
+def export_threads(request: Request) -> list[dict[str, Any]]:
+ _require_owner(request)
+ return store.all()
+
+
+@app.post("/api/review/import")
+async def import_threads(request: Request) -> dict[str, Any]:
+ """Add threads from an export or another source. Existing ids are kept unless replace is true."""
+ _require_owner(request)
+ payload = await request.json()
+ items = payload.get("threads") or []
+ replace = bool(payload.get("replace"))
+ added, skipped = 0, 0
+ for t in items:
+ if not isinstance(t, dict) or not {"id", "anchor", "messages", "created_at"} <= t.keys():
+ raise HTTPException(400, "Each thread needs id, anchor, messages and created_at.")
+ tid = str(t["id"])
+ if not tid.replace("-", "").replace("_", "").isalnum() or len(tid) > 80:
+ raise HTTPException(400, f"Bad thread id: {tid!r}")
+ with store.lock:
+ exists = tid in store.threads
+ if exists and not replace:
+ skipped += 1
+ continue
+ t.setdefault("status", "open")
+ await run_in_threadpool(store.save, t)
+ added += 1
+ return {"added": added, "skipped": skipped, "total": len(store.all())}
diff --git a/content/articles/multi-harness-rl/scripts/release.mjs b/content/articles/multi-harness-rl/scripts/release.mjs
new file mode 100644
index 00000000..8ea222fe
--- /dev/null
+++ b/content/articles/multi-harness-rl/scripts/release.mjs
@@ -0,0 +1,95 @@
+#!/usr/bin/env node
+
+/**
+ * Release script for Research Article Template
+ * Handles semantic versioning and changelog updates
+ */
+
+import { execSync } from 'child_process';
+import { readFileSync, writeFileSync } from 'fs';
+import { join } from 'path';
+
+const PACKAGE_JSON_PATH = join(process.cwd(), 'app', 'package.json');
+const CHANGELOG_PATH = join(process.cwd(), 'CHANGELOG.md');
+
+function getCurrentVersion() {
+ const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8'));
+ return packageJson.version;
+}
+
+function updateVersion(newVersion) {
+ const packageJson = JSON.parse(readFileSync(PACKAGE_JSON_PATH, 'utf8'));
+ packageJson.version = newVersion;
+ writeFileSync(PACKAGE_JSON_PATH, JSON.stringify(packageJson, null, 2) + '\n');
+ console.log(`✅ Updated package.json to version ${newVersion}`);
+}
+
+function updateChangelog(newVersion) {
+ const changelog = readFileSync(CHANGELOG_PATH, 'utf8');
+ const today = new Date().toISOString().split('T')[0];
+
+ const updatedChangelog = changelog.replace(
+ '## [Unreleased]',
+ `## [Unreleased]\n\n## [${newVersion}] - ${today}`
+ );
+
+ writeFileSync(CHANGELOG_PATH, updatedChangelog);
+ console.log(`✅ Updated CHANGELOG.md with version ${newVersion}`);
+}
+
+function createGitTag(version) {
+ try {
+ execSync(`git tag -a v${version} -m "Release version ${version}"`, { stdio: 'inherit' });
+ console.log(`✅ Created git tag v${version}`);
+ } catch (error) {
+ console.error(`❌ Failed to create git tag: ${error.message}`);
+ }
+}
+
+function main() {
+ const args = process.argv.slice(2);
+ const versionType = args[0]; // 'major', 'minor', 'patch'
+
+ if (!['major', 'minor', 'patch'].includes(versionType)) {
+ console.error('❌ Please specify version type: major, minor, or patch');
+ console.log('Usage: node scripts/release.mjs [major|minor|patch]');
+ process.exit(1);
+ }
+
+ const currentVersion = getCurrentVersion();
+ const [major, minor, patch] = currentVersion.split('.').map(Number);
+
+ let newVersion;
+ switch (versionType) {
+ case 'major':
+ newVersion = `${major + 1}.0.0`;
+ break;
+ case 'minor':
+ newVersion = `${major}.${minor + 1}.0`;
+ break;
+ case 'patch':
+ newVersion = `${major}.${minor}.${patch + 1}`;
+ break;
+ }
+
+ console.log(`🚀 Releasing version ${newVersion} (from ${currentVersion})`);
+
+ // Update files
+ updateVersion(newVersion);
+ updateChangelog(newVersion);
+
+ // Create git tag
+ createGitTag(newVersion);
+
+ console.log(`\n🎉 Release ${newVersion} prepared!`);
+ console.log('\nNext steps:');
+ console.log('1. Review the changes:');
+ console.log(' git diff');
+ console.log('2. Commit the changes:');
+ console.log(` git add . && git commit -m "chore: release version ${newVersion}"`);
+ console.log('3. Push the changes and tags:');
+ console.log(` git push && git push --tags`);
+ console.log('4. Create a release on Hugging Face Spaces');
+}
+
+main();