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.
In this repository9 other files it gives its agents.
- claim-extractorSkill
- narrationSkill
- paper-diffSkill
- paper-parserSkill
- physics-interpreterSkill
- primitive-generatorSkill
- science-integritySkill
- simspec-authorSkill
- shader-devSkill
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
- Distinctive: line 17
run code_execution (the tool is attached only because Skills v1 require it)
- Very specific: line 19
write narration (the narration skill owns that)
- Distinctive: line 40
Read from the container filesystem:
- Very specific: line 77
When an annotation's text contains a number that changes with sliders, use text_template with a {value} placeholder + dynamic_value.
- Very specific: line 128
Start from slider ids as leaves.
- Very specific: line 173
Use text_template + dynamic_value whenever the displayed number depends on sliders.
- Very specific: line 437
Read physics.json governing equations.
- Distinctive: line 445
If the simspec marks the paper as ineligible, emit a minimal V8 explainer:
≈ “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.
Files
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
narrationskill 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) — neverMath.sin. Forkind: "array"the expr returns a length-N array, typically via(function(){ return x.map(xi => ...); })(). The implicitxis a Float32Array of lengthdefault_grid_non[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:framesfor primitive-driven series;analyticfor closed-form.swept_param: required iffparameter_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:
- 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(...) ...). - Build array derived for plotted quantities. The implicit
xis 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; }); })(). - List every slider id and every other derived id referenced in
deps[]. - Final node consumed by a trace must be
kind: "array". - 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. Thefunctionkeyword +=>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_idreferencing akind: "array"derived.source: "frames"+primitive_id+state_keyreferencing simspec.primitives[].state.palette_role→ palette_map.style:line(default),fill(Schrödinger ψ²),dots,bars,area.shadow_blur_px+shadow_color_rolefor emphasis (Young measured-detector trace).stroke_dash_pattern: [4, 4]for envelope/reference curves.z_orderfor 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)
- Read physics.json governing equations. Identify observable quantities.
- Identify panels[] from figures.json. Assign layout.rows/cols.
- Build derived[] DAG leaf-to-root. Scalar vs array kind.
- For each panel, assign traces[] referencing derived ids or primitive state_keys.
- 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
- Schema: every required field present; no
additionalPropertiesviolations. - archetype is one of the 9 enum values.
- palette_map covers every role referenced anywhere.
- parameter_sweep → swept_param ∈ sliders[].id ∪ derived[].id.
- frames + time_series → primitive_bindings non-empty OR panel.traces[] with source=frames.
- derived ids unique; deps[] reference real slider or derived ids.
- trace.compute_id references kind=array; readout.compute_id references kind=scalar.
- Annotation has either text OR text_template (not both). text_template requires dynamic_value.
- layer_toggle.trace_ids reference real panel traces.
- Output is exactly one fenced
jsonblock — no prose outside.
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
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.

