agentleFS
leventiloleventilo/mobius/skills/viz-mapper/SKILL.md

viz-mapper

Skill8 starsChanged 7 months agoMITView on GitHub

Translate a verified SimSpec into a typed viz_spec.json conformant to the V8 viz_spec.schema.json. Output JSON only — no HTML, no CSS, no JavaScript. Server-side ajv validation enforces shape post-receive (grammar path is currently OFF — schema exceeds Anthropic compiled-grammar size cap). The frontend renderer (public/runtime/render.js) consumes viz_spec + frames + narration + provenance and renders multi-panel scenes with derived[] DAG, live readouts, dynamic annotations, and layer toggles.

The file

We split this skill into the rules it gives an agent

8 rules. About 0 are common, 3 are distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈3

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    run code_execution (the tool is attached only because Skills v1 require it)

    line 17
  2. Very specific:

    write narration (the narration skill owns that)

    line 19
  3. Distinctive:

    Read from the container filesystem:

    line 40
  4. Very specific:

    When an annotation's text contains a number that changes with sliders, use text_template with a {value} placeholder + dynamic_value.

    line 77
  5. Very specific:

    Start from slider ids as leaves.

    line 128
  6. Very specific:

    Use text_template + dynamic_value whenever the displayed number depends on sliders.

    line 173
  7. Very specific:

    Read physics.json governing equations.

    line 437
  8. Distinctive:

    If the simspec marks the paper as ineligible, emit a minimal V8 explainer:

    line 445

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

viz-mapper V2 (V8 monstrueux)

1. Purpose

Translate verified simspec.verified.json + physics.json + claims.json + figures.json into a typed viz_spec.json that conforms exactly to the V8 viz_spec.schema.json. Server-side ajv validation enforces shape post-receive.

This skill does NOT:

  • author HTML, CSS, or JavaScript
  • decide the archetype (it copies simspec.viz.scene_archetype)
  • run code_execution (the tool is attached only because Skills v1 require it)
  • compute physics (primitives in Phase E own that)
  • write narration (the narration skill owns that)

The downstream renderer (public/runtime/render.js) auto-projects any V7 spec into V8 shape via projectV7ToV8, dispatches each experiment.panels[] entry to a panel renderer, evaluates experiment.derived[] as a DAG on every slider change, and updates live readouts[] and dynamic annotations.

2. Output format (mandatory)

Emit exactly one JSON object inside a single fenced json ... block at the END of the reply. No prose before or after. No tool_use.

Top-level shape:

{
  "paper": { "title": "...", "arxivId": "...", "authors": ["..."] },
  "experiments": [ { /* one or more experiment objects */ } ]
}

For a single-phenomenon paper emit experiments length 1. For a paper with multiple distinct setups emit experiments length N — each independent. Most papers emit ONE experiment containing MULTIPLE panels via experiment.panels[].

3. Inputs

Read from the container filesystem:

  • /mnt/session/runs/{runId}/simspec.verified.json — canonical input
  • /mnt/session/runs/{runId}/physics.json — equations, regime
  • /mnt/session/runs/{runId}/claims.json — numerical claims
  • /mnt/session/runs/{runId}/figures.json — figure metadata

4. V8 monstrueux: structural decisions

4.1 panels[]: multi-region scenes

V7 forced one canvas per experiment via the archetype field. V8 introduces panels[] so a single experiment can contain N rendering regions sharing the same sliders, derived state, readouts, and layer toggles. Examples:

  • Young's double-slit: 1 panel (field_2d) with 3 traces (envelope, intensity, measured).
  • Lorenz attractor: 4 panels in a 3×2 grid — 3 stacked time_series (X(t), Y(t), Z(t) in col:1 rows 1..3) plus 1 scatter return-map (col:2, row:1, row_span:3).
  • Schrödinger tunneling: 1 panel (field_2d) with 1 fill-style trace + dynamic V₀ annotation.

Choose layout: {kind: "single"} when N=1; layout: {kind: "grid", rows, cols} for N>1. Maximum 4 panels for time_series, 2 for field_2d, 1 for ray_canvas / orbital_3d / heatmap_2d.

4.2 derived[]: compute DAG

V7's equations[].compute_js is one expression for display. V8 introduces derived[] as a topologically-sorted DAG of intermediate quantities. Each derived has:

  • id: matches paper variable name (e.g. visibility_V, intensity_I)
  • expr: JS expression in the compute_analytic sandbox. Bare names (sin, cos, PI) — never Math.sin. For kind: "array" the expr returns a length-N array, typically via (function(){ return x.map(xi => ...); })(). The implicit x is a Float32Array of length default_grid_n on [0,1].
  • deps: list of slider ids and other derived ids referenced. Used for topo sort.
  • kind: "scalar" (one number) or "array" (length-N Float32Array).

Authoring rule: build the DAG leaf-to-root. Sliders are leaves. Final array nodes are rendered by traces.

4.3 readouts[]: live numeric strip

A readout is one row in a strip of live values shown under (or above) the canvas. Each binds to a dynamic_value referencing a kind:"scalar" derived id, with optional scale (multiplier for unit conversion), format, and unit.

Example: { id: "ro-dx", label: "δx =", dynamic_value: { compute_id: "fringe_sep", scale: 1000, format: "fixed2", unit: " mm" }, position: "under_canvas" }.

4.4 dynamic annotations

When an annotation's text contains a number that changes with sliders, use text_template with a {value} placeholder + dynamic_value. The renderer substitutes {value} on every recompute.

Example: { ..., text_template: "δx = {value} mm", dynamic_value: { compute_id: "fringe_sep", scale: 1000, format: "fixed2" } }.

Static annotations keep using plain text.

4.5 layer_toggles[]: trace-id-driven chip strip

V7 layer_toggles was a string keyword list (["rays", "grid"]). V8 promotes it to objects: { id, label, trace_ids[], default_visible }. trace_ids lists the panel.traces[] ids whose visibility is bound to the chip; the renderer hides traces in real time via a click handler. V7 string form still works (auto-projected).

5. Field-by-field authoring rules

5.1 paper

Copy from simspec.paper.

5.2 experiment.id, label

id: kebab/snake matching ^[a-zA-Z_][a-zA-Z0-9_-]{0,63}$. label: human-readable (≤96 chars).

5.3 archetype (still required)

Copy simspec.viz.scene_archetype verbatim. The archetype acts as a thin orchestrator dispatching to panels[]. Use it as a hint to the renderer, but the actual visual is decided by panel.kind values.

5.4 evolution_kind, state_source, swept_param

  • evolution_kind ← simspec.simulation.evolution_kind (static | time_series | parameter_sweep).
  • state_source: frames for primitive-driven series; analytic for closed-form.
  • swept_param: required iff parameter_sweep. Either a slider id OR a derived id (V8).

5.5 canvas

Defaults: { width: 760, height: 480, aspect: "16/9", dpr_max: 2 }. Use 1024×600 for grid layouts.

5.6 palette_map

Map role → c0..c5. Lowercase + spaces + dashes + underscores + digits, 1-64 chars, must start with a letter. Every role referenced by sliders/annotations/equations/primitive_bindings/traces/readouts/layer_toggles MUST be a key in palette_map.

5.7 sliders[]

Walk simspec.viz.sliders[]. Copy id/label/binds_to/unit_ucum, set min/max from range, step = (max-min)/100. Pick formatter from {fixed1, fixed2, fixed3, scientific, integer, percent, degrees, none}. R-INLINE: every slider must be a real interactive control, no hardcoded constants.

5.8 default_grid_n

512 default; 1024 for fine-detail like Young fringes; 256 for Schrödinger frames. Bounded [32, 4096].

5.9 derived[]

Authoring protocol:

  1. Start from slider ids as leaves. Add scalar derived for closed-form quantities (e.g. fringe_sep = lam * L / d, visibility_V = (b/lam) * sin(...) ...).
  2. Build array derived for plotted quantities. The implicit x is on [0,1]; encode the physical domain into the expr (e.g. for Young's screen ±12.5 mm: var xi_m = (x - 0.5) * 0.025; ...). Pattern: (function(){ return x.map(function(xi){ var x_mm = (xi - 0.5) * 25; return cos(2*PI*d*x_mm/(lam*L*1000))**2; }); })().
  3. List every slider id and every other derived id referenced in deps[].
  4. Final node consumed by a trace must be kind: "array".
  5. Sandbox tokens: bare math names (sin, cos, PI, sqrt, abs, exp, pow, min, max, floor, etc.). Forbidden: window, eval, Function, import, require, this, __proto__, constructor, prototype, new, backticks, ${} interpolation. The function keyword + => are allowed for the array idiom.

5.10 readouts[]

One readout per claim from the paper that depends on sliders. Examples for Young: δx (fringe spacing), V (visibility). Bind each to a scalar derived id via dynamic_value. Use scale for unit conversion (e.g. scale: 1000 to display metres as mm).

5.11 panels[]

For each visual region in the paper's key figure(s), emit one panel:

{
  "id": "panel-main",
  "kind": "field_2d",
  "label": "Far-field intensity",
  "position": { "row": 1, "col": 1 },
  "axes": { "x_label": "screen x", "x_unit": "mm", "y_range": { "min": 0, "max": 1 } },
  "extras": { "show_legend": true, "show_gridlines": false },
  "traces": [
    { "id": "tr-env", "label": "envelope", "source": "analytic", "compute_id": "envelope_E",
      "palette_role": "fringe maxima", "style": "line", "stroke_width_px": 1, "stroke_dash_pattern": [4, 4], "z_order": 0 },
    { "id": "tr-int", "label": "intensity", "source": "analytic", "compute_id": "intensity_I",
      "palette_role": "interference field", "style": "line", "stroke_width_px": 1.6, "z_order": 1 }
  ]
}

panel.kind enum: field_2d, time_series, scatter, heatmap_2d, ray_canvas, orbital_3d, particle_cloud, lattice, schematic. Choose by visual character of the figure.

5.12 panel.traces[]

  • source: "analytic" + compute_id referencing a kind: "array" derived.
  • source: "frames" + primitive_id + state_key referencing simspec.primitives[].state.
  • palette_role → palette_map.
  • style: line (default), fill (Schrödinger ψ²), dots, bars, area.
  • shadow_blur_px + shadow_color_role for emphasis (Young measured-detector trace).
  • stroke_dash_pattern: [4, 4] for envelope/reference curves.
  • z_order for stable layering.

5.13 annotations[]

Use text_template + dynamic_value whenever the displayed number depends on sliders. Use plain text for static labels. Anchor space normalised [0,1] for archetype-agnostic placement.

5.14 equations[]

Up to 8 governing equations. Optional compute_js (legacy V7) is supported but in V8 prefer authoring derived[] and using equations[] purely for KaTeX display. New size: "small" | "medium" | "large" controls KaTeX rendering size.

5.15 interactions

{
  "play_button": (evolution_kind != "static"),
  "scroll_lock": false,
  "drag_camera": (archetype ∈ {orbital-3d, glass-dispersion, particle-cloud, lattice}),
  "reset_button": (evolution_kind != "static"),
  "layer_toggles": [ /* layer_toggle_spec[] */ ]
}

6. Worked example 1 — Young's double-slit (1 panel, 3 traces, 4 derived, 2 readouts, 1 dynamic annotation)

{
  "paper": { "title": "Young's double-slit interference with single photons", "arxivId": "2401.02351" },
  "experiments": [
    {
      "id": "young",
      "label": "Two-slit interference",
      "archetype": "field-2d",
      "evolution_kind": "static",
      "state_source": "analytic",
      "default_grid_n": 1024,
      "caption": "δx = λL/d sets the fringe spacing; sinc² envelope from finite slit width.",
      "canvas": { "width": 760, "height": 480, "aspect": "16/9", "dpr_max": 2 },
      "palette_map": {
        "single-photon source": "c0",
        "double-slit aperture": "c1",
        "interference field": "c3",
        "envelope curve": "c4",
        "scanning detector": "c5"
      },
      "layout": { "kind": "single" },
      "sliders": [
        { "id": "lam", "kind": "physics", "label": "wavelength λ", "binds_to": "primitives[id=monochromatic-source].parameters.wavelength.value",
          "min": 4e-7, "max": 1e-6, "default": 8.1e-7, "step": 1e-8, "unit_ucum": "m", "formatter": "scientific", "palette_role": "single-photon source" },
        { "id": "d", "kind": "physics", "label": "slit separation d", "binds_to": "primitives[id=double-slit-aperture].parameters.slit_separation.value",
          "min": 1e-4, "max": 2e-3, "default": 6.2e-4, "step": 1e-5, "unit_ucum": "m", "formatter": "scientific", "palette_role": "double-slit aperture" },
        { "id": "b", "kind": "physics", "label": "slit width b", "binds_to": "primitives[id=double-slit-aperture].parameters.slit_width.value",
          "min": 5e-5, "max": 4e-4, "default": 1.3e-4, "step": 5e-6, "unit_ucum": "m", "formatter": "scientific", "palette_role": "double-slit aperture" },
        { "id": "L", "kind": "physics", "label": "screen distance L", "binds_to": "primitives[id=screen-intensity].parameters.screen_distance.value",
          "min": 0.5, "max": 5, "default": 1.5, "step": 0.05, "unit_ucum": "m", "formatter": "fixed2", "palette_role": "scanning detector" }
      ],
      "derived": [
        { "id": "fringe_sep", "label": "δx = λL/d (m)", "expr": "lam * L / d", "deps": ["lam", "L", "d"], "kind": "scalar" },
        { "id": "visibility_V", "label": "fringe visibility", "expr": "(b > 0 && d > 0) ? abs(sin(PI * b / d) / (PI * b / d)) : 0", "deps": ["b", "d"], "kind": "scalar" },
        { "id": "envelope_E", "label": "sinc² envelope", "kind": "array",
          "expr": "(function(){ return x.map(function(xi){ var y_m = (xi - 0.5) * 0.025; var sa = PI * b * y_m / (lam * L); var s = abs(sa) < 1e-12 ? 1 : sin(sa) / sa; return s * s; }); })()",
          "deps": ["b", "lam", "L"] },
        { "id": "intensity_I", "label": "I(y) = cos² × envelope", "kind": "array",
          "expr": "(function(){ return x.map(function(xi){ var y_m = (xi - 0.5) * 0.025; var sa = PI * b * y_m / (lam * L); var env = abs(sa) < 1e-12 ? 1 : (sin(sa) / sa) * (sin(sa) / sa); var sd = PI * d * y_m / (lam * L); return cos(sd) * cos(sd) * env; }); })()",
          "deps": ["b", "d", "lam", "L"] }
      ],
      "readouts": [
        { "id": "ro-dx", "label": "δx =", "dynamic_value": { "compute_id": "fringe_sep", "scale": 1000, "format": "fixed2", "unit": " mm" }, "position": "under_canvas", "palette_role": "interference field" },
        { "id": "ro-V", "label": "V =", "dynamic_value": { "compute_id": "visibility_V", "format": "fixed2" }, "position": "under_canvas", "palette_role": "interference field" }
      ],
      "panels": [
        { "id": "main", "kind": "field_2d", "label": "Screen intensity",
          "axes": { "x_label": "screen y", "x_unit": "mm", "y_range": { "min": 0, "max": 1, "auto": false } },
          "extras": { "show_legend": true, "show_gridlines": false },
          "traces": [
            { "id": "tr-env", "label": "envelope", "source": "analytic", "compute_id": "envelope_E",
              "palette_role": "envelope curve", "style": "line", "stroke_width_px": 1.0, "stroke_dash_pattern": [4, 4], "z_order": 0 },
            { "id": "tr-int", "label": "intensity", "source": "analytic", "compute_id": "intensity_I",
              "palette_role": "interference field", "style": "line", "stroke_width_px": 1.6, "z_order": 1 }
          ]
        }
      ],
      "annotations": [
        { "id": "ann-dx", "type": "math_label", "anchor": { "space": "normalized", "x": 0.5, "y": 0.10 },
          "text_template": "δx = λL/d = {value} mm",
          "dynamic_value": { "compute_id": "fringe_sep", "scale": 1000, "format": "fixed2" },
          "palette_role": "interference field" }
      ],
      "equations": [
        { "id": "eq-young", "latex": "I(y) = I_0 \\cos^2\\!\\left(\\frac{\\pi d y}{\\lambda L}\\right) \\operatorname{sinc}^2\\!\\left(\\frac{\\pi b y}{\\lambda L}\\right)",
          "role": "governing", "palette_role": "interference field", "position": "caption", "size": "medium" }
      ],
      "camera_presets": [],
      "interactions": {
        "play_button": false, "scroll_lock": false, "drag_camera": false, "reset_button": true,
        "layer_toggles": [
          { "id": "lt-env", "label": "Envelope", "trace_ids": ["tr-env"], "default_visible": true, "palette_role": "envelope curve" },
          { "id": "lt-int", "label": "Intensity", "trace_ids": ["tr-int"], "default_visible": true, "palette_role": "interference field" }
        ]
      },
      "primitive_bindings": []
    }
  ]
}

7. Worked example 2 — Lorenz attractor (4 panels in 3×2 grid)

{
  "paper": { "title": "Lorenz 1963 — strange attractor" },
  "experiments": [
    {
      "id": "lorenz",
      "label": "Lorenz attractor",
      "archetype": "graph-time",
      "evolution_kind": "time_series",
      "state_source": "frames",
      "canvas": { "width": 1024, "height": 600, "aspect": "16/9", "dpr_max": 2 },
      "palette_map": {
        "prandtl sigma": "c2", "rayleigh rho": "c3", "aspect beta": "c1",
        "trajectory": "c4", "return map": "c5"
      },
      "layout": { "kind": "grid", "rows": 3, "cols": 2, "row_gap_px": 6, "col_gap_px": 12 },
      "sliders": [
        { "id": "sigma", "kind": "physics", "label": "Prandtl σ", "binds_to": "primitives[id=lorenz_rk45].parameters.sigma.value",
          "min": 0, "max": 30, "default": 10, "step": 0.1, "formatter": "fixed1", "palette_role": "prandtl sigma" },
        { "id": "rho", "kind": "physics", "label": "Rayleigh ρ", "binds_to": "primitives[id=lorenz_rk45].parameters.rho.value",
          "min": 14, "max": 200, "default": 28, "step": 0.1, "formatter": "fixed1", "palette_role": "rayleigh rho" },
        { "id": "beta", "kind": "physics", "label": "Aspect β", "binds_to": "primitives[id=lorenz_rk45].parameters.beta.value",
          "min": 0.5, "max": 10, "default": 2.667, "step": 0.05, "formatter": "fixed2", "palette_role": "aspect beta" }
      ],
      "derived": [
        { "id": "tau", "label": "fixed-point eigenvalue τ", "expr": "sigma + 1", "deps": ["sigma"], "kind": "scalar" },
        { "id": "saddle", "label": "√(b(r-1))", "expr": "sqrt(beta * (rho - 1))", "deps": ["beta", "rho"], "kind": "scalar" },
        { "id": "rho_c", "label": "Hopf threshold ρ_c", "expr": "sigma * (sigma + beta + 3) / (sigma - beta - 1)", "deps": ["sigma", "beta"], "kind": "scalar" }
      ],
      "readouts": [
        { "id": "ro-tau", "label": "τ =", "dynamic_value": { "compute_id": "tau", "format": "fixed2" }, "position": "under_canvas", "palette_role": "prandtl sigma" },
        { "id": "ro-saddle", "label": "√(β(ρ-1)) =", "dynamic_value": { "compute_id": "saddle", "format": "fixed2" }, "position": "under_canvas", "palette_role": "trajectory" },
        { "id": "ro-rhoc", "label": "ρ_c =", "dynamic_value": { "compute_id": "rho_c", "format": "fixed2" }, "position": "under_canvas", "palette_role": "rayleigh rho" }
      ],
      "panels": [
        { "id": "x-trace", "kind": "time_series", "label": "X(t)", "position": { "row": 1, "col": 1 },
          "axes": { "y_label": "X" },
          "traces": [{ "id": "tr-x", "source": "frames", "primitive_id": "lorenz_rk45", "state_key": "X_history", "palette_role": "prandtl sigma", "stroke_width_px": 1.4, "z_order": 0 }] },
        { "id": "y-trace", "kind": "time_series", "label": "Y(t)", "position": { "row": 2, "col": 1 },
          "axes": { "y_label": "Y" },
          "traces": [{ "id": "tr-y", "source": "frames", "primitive_id": "lorenz_rk45", "state_key": "Y_history", "palette_role": "trajectory", "stroke_width_px": 1.4, "z_order": 0 }] },
        { "id": "z-trace", "kind": "time_series", "label": "Z(t)", "position": { "row": 3, "col": 1 },
          "axes": { "y_label": "Z" },
          "traces": [{ "id": "tr-z", "source": "frames", "primitive_id": "lorenz_rk45", "state_key": "Z_history", "palette_role": "rayleigh rho", "stroke_width_px": 1.4, "z_order": 0 }] },
        { "id": "rmap", "kind": "scatter", "label": "Z_n vs Z_{n+1}", "position": { "row": 1, "col": 2, "row_span": 3 },
          "axes": { "x_label": "Z_n", "y_label": "Z_{n+1}" },
          "extras": { "identity_line": true },
          "traces": [{ "id": "tr-rm", "source": "frames", "primitive_id": "lorenz_rk45", "state_key": "Z_max_pairs", "palette_role": "return map", "marker_size_px": 1.4, "z_order": 0 }] }
      ],
      "annotations": [],
      "equations": [
        { "id": "eq-lorenz", "latex": "\\dot{x} = \\sigma(y-x),\\;\\dot{y} = x(\\rho - z) - y,\\;\\dot{z} = xy - \\beta z",
          "role": "governing", "palette_role": "trajectory", "position": "caption", "size": "medium" }
      ],
      "camera_presets": [],
      "interactions": {
        "play_button": true, "scroll_lock": false, "drag_camera": false, "reset_button": true,
        "layer_toggles": [
          { "id": "lt-traj", "label": "Trajectory", "trace_ids": ["tr-x", "tr-y", "tr-z"], "default_visible": true, "palette_role": "trajectory" },
          { "id": "lt-rmap", "label": "Return map", "trace_ids": ["tr-rm"], "default_visible": true, "palette_role": "return map" }
        ]
      },
      "primitive_bindings": [
        { "primitive_id": "lorenz_rk45", "state_key": "X_history", "palette_role": "trajectory" }
      ]
    }
  ]
}

8. Worked example 3 — Schrödinger tunneling (1 panel, fill style, 2 derived, 2 readouts, 2 dynamic annotations)

{
  "paper": { "title": "Quantum tunneling through a barrier" },
  "experiments": [
    {
      "id": "tunneling",
      "label": "Wavepacket vs barrier height",
      "archetype": "field-2d",
      "evolution_kind": "time_series",
      "state_source": "frames",
      "default_grid_n": 256,
      "canvas": { "width": 760, "height": 480, "aspect": "16/9", "dpr_max": 2 },
      "palette_map": {
        "wavefunction curve": "c3",
        "barrier region": "c5",
        "transmitted lobe": "c2",
        "reflected lobe": "c4"
      },
      "layout": { "kind": "single" },
      "sliders": [
        { "id": "V0", "kind": "physics", "label": "barrier height V₀", "binds_to": "primitives[id=crank_nicolson_1d].parameters.V0.value",
          "min": 0, "max": 30, "default": 5, "step": 0.1, "formatter": "fixed1", "palette_role": "barrier region" },
        { "id": "k0", "kind": "physics", "label": "packet momentum k₀", "binds_to": "primitives[id=crank_nicolson_1d].parameters.k0.value",
          "min": 1, "max": 10, "default": 4, "step": 0.05, "formatter": "fixed2", "palette_role": "wavefunction curve" },
        { "id": "sigma", "kind": "physics", "label": "packet width σ", "binds_to": "primitives[id=crank_nicolson_1d].parameters.sigma.value",
          "min": 0.5, "max": 5, "default": 1.5, "step": 0.05, "formatter": "fixed2", "palette_role": "wavefunction curve" }
      ],
      "derived": [
        { "id": "V0_val", "label": "barrier height V₀ passthrough", "expr": "V0", "deps": ["V0"], "kind": "scalar" },
        { "id": "kinetic_E", "label": "kinetic energy E", "expr": "0.5 * k0 * k0", "deps": ["k0"], "kind": "scalar" },
        { "id": "regime_factor", "label": "E/V₀ ratio", "expr": "V0 > 0 ? (0.5 * k0 * k0) / V0 : 0", "deps": ["V0", "k0"], "kind": "scalar" }
      ],
      "readouts": [
        { "id": "ro-E", "label": "E =", "dynamic_value": { "compute_id": "kinetic_E", "format": "fixed2" }, "position": "under_canvas", "palette_role": "wavefunction curve" },
        { "id": "ro-V0", "label": "V₀ =", "dynamic_value": { "compute_id": "V0_val", "format": "fixed1" }, "position": "under_canvas", "palette_role": "barrier region" }
      ],
      "panels": [
        { "id": "rho-panel", "kind": "field_2d", "label": "Probability density |ψ|²",
          "axes": { "x_label": "x" },
          "extras": { "show_legend": false, "fill_under": true },
          "traces": [
            { "id": "tr-rho", "label": "|ψ|²", "source": "frames", "primitive_id": "crank_nicolson_1d", "state_key": "rho",
              "palette_role": "wavefunction curve", "style": "fill", "fill_opacity": 0.32, "shadow_blur_px": 8, "shadow_color_role": "wavefunction curve",
              "stroke_width_px": 1.4, "z_order": 0 }
          ]
        }
      ],
      "annotations": [
        { "id": "ann-V0", "type": "math_label", "anchor": { "space": "normalized", "x": 0.5, "y": 0.06 },
          "text_template": "V₀ = {value}",
          "dynamic_value": { "compute_id": "V0_val", "format": "fixed1" },
          "palette_role": "barrier region" },
        { "id": "ann-E", "type": "math_label", "anchor": { "space": "normalized", "x": 0.85, "y": 0.06 },
          "text_template": "E = {value}",
          "dynamic_value": { "compute_id": "kinetic_E", "format": "fixed2" },
          "palette_role": "wavefunction curve" }
      ],
      "equations": [
        { "id": "eq-schroedinger", "latex": "i\\hbar \\partial_t \\psi = -\\tfrac{\\hbar^2}{2m}\\partial_x^2 \\psi + V(x)\\psi",
          "role": "governing", "palette_role": "wavefunction curve", "position": "caption", "size": "medium" }
      ],
      "camera_presets": [],
      "interactions": {
        "play_button": true, "scroll_lock": false, "drag_camera": false, "reset_button": true,
        "layer_toggles": []
      },
      "primitive_bindings": [
        { "primitive_id": "crank_nicolson_1d", "state_key": "rho", "palette_role": "wavefunction curve" }
      ]
    }
  ]
}

9. R1-R10 enforcement

  • R1 anti-hardcode: every numeric in viz_spec originates from simspec/claims, not paper copy-paste.
  • R2 DAG completeness: every derived[] declares deps[] for every variable referenced in expr.
  • R3 trace source consistency: source=analytic → compute_id (kind=array); source=frames → primitive_id + state_key.
  • R4 palette coverage: every palette_role referenced is a key in palette_map.
  • R5 layer_toggle.trace_ids must reference real panel.traces[].id values.
  • R6 readout.dynamic_value.compute_id MUST be a kind:"scalar" derived; trace.compute_id MUST be kind:"array".
  • R7 critic-loop friendly: refinement suggestions edit specific derived/trace/readout entries without restructuring untouched fields.
  • R8 swept_param matches sliders[].id OR derived[].id (V8 extension).
  • R9 frames require binding (panel.traces[] with source=frames OR primitive_bindings[]).
  • R10 no duplicate ids inside panels[], derived[], readouts[], or layer_toggles[].

10. Generation protocol (5 steps)

  1. Read physics.json governing equations. Identify observable quantities.
  2. Identify panels[] from figures.json. Assign layout.rows/cols.
  3. Build derived[] DAG leaf-to-root. Scalar vs array kind.
  4. For each panel, assign traces[] referencing derived ids or primitive state_keys.
  5. Build readouts[] from claims.json predicted values, annotations[] from figure markup.

11. Refusal cases

If the simspec marks the paper as ineligible, emit a minimal V8 explainer:

{
  "paper": { "title": "..." },
  "experiments": [
    {
      "id": "ineligibility",
      "label": "Why this paper cannot be simulated",
      "archetype": "explainer",
      "evolution_kind": "static",
      "state_source": "analytic",
      "canvas": { "width": 760, "height": 480, "aspect": "16/9", "dpr_max": 2 },
      "palette_map": { "primary": "c5" },
      "layout": { "kind": "single" },
      "sliders": [], "annotations": [
        { "id": "reason1", "type": "callout", "anchor": {"space":"normalized","x":0.1,"y":0.4}, "text": "...", "palette_role": "primary" }
      ],
      "equations": [], "camera_presets": [], "panels": [], "derived": [], "readouts": [],
      "interactions": { "play_button": false, "scroll_lock": false, "drag_camera": false },
      "primitive_bindings": []
    }
  ]
}

12. Post-emission self-check

  1. Schema: every required field present; no additionalProperties violations.
  2. archetype is one of the 9 enum values.
  3. palette_map covers every role referenced anywhere.
  4. parameter_sweep → swept_param ∈ sliders[].id ∪ derived[].id.
  5. frames + time_series → primitive_bindings non-empty OR panel.traces[] with source=frames.
  6. derived ids unique; deps[] reference real slider or derived ids.
  7. trace.compute_id references kind=array; readout.compute_id references kind=scalar.
  8. Annotation has either text OR text_template (not both). text_template requires dynamic_value.
  9. layer_toggle.trace_ids reference real panel traces.
  10. Output is exactly one fenced json block — no prose outside.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.