science-integrity
Skill8 starsChanged 7 months agoMITView on GitHub
Parallel critic agent that enforces physical invariants on Mobius SimSpecs and simulation outputs. Runs five bundled deterministic checks — units, CFL stability, conservation laws, figure-diff, and claim-match — and blocks or warns upstream generators. Use whenever a SimSpec or simulation artifact is produced.
- Reads credentials
In this repository9 other files it gives its agents.
- claim-extractorSkill
- narrationSkill
- paper-diffSkill
- paper-parserSkill
- physics-interpreterSkill
- primitive-generatorSkill
- simspec-authorSkill
- shader-devSkill
- viz-mapperSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
25 rules. About 0 are common, 9 are distinctive, and 16 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈9
Distinctive · estimate
This author's own choices. Worth a look.
16
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 72
When dispatched in Phase D-verify, run ONLY checks 1-5.
- Very specific: line 76
When dispatched in Phase G-diff, run ONLY checks 6-8.
- Very specific: line 92
Sequential, single-pass execution: Run each check in this phase ONCE in the listed order.
- Distinctive: line 95
ONLY mechanism that can re-run a check — never re-run inside one turn.
- Very specific: line 106
No "let me re-verify" reflex: If a script returns fail, that is the answer for this phase iteration.
- Distinctive: line 181
When running a bundled script:
- Distinctive: line 184
Parse the JSON response: If JSON parse fails, retry once more (2 attempts total per check).
- Distinctive: line 201
If you are tempted to re-run a script "to be sure", resist.
- Distinctive: line 202
RUN each check ONCE, not to repeatedly affirm it.
- Distinctive: line 257
If the input SimSpec is unavailable or every script fails to execute, emit:
- Distinctive: line 263
DO NOT emit narration, summaries, or follow-up questions after the closing fence — they break the orchestrator's downstream consumption and …
- Distinctive: line 326
When a check fails, the orchestrator receives a JSON message with this exact schema:
- Very specific: line 348
Default max_retries = 3. The orchestrator may override per-stage in its config.
- Very specific: line 357
call the LLM, except in figure_diff.py where there is no deterministic alternative (loudly flagged).
- Very specific: line 380
Every paper that enters Mobius gets an eligibility object on its SimSpec.
- Very specific: line 403
Forward direction (orphan claims): Every paper.numerical_claims[i].id must appear either in narration.claims_referenced[] OR be …
- Very specific: line 404
Reverse direction (ungrounded numbers): Every numerical literal in the inline text must resolve to a paper.numerical_claims[i].value, a …
- Very specific: line 429
Never silently pass a warn as pass.
- Distinctive: line 430
Never skip a check because the upstream generator says it's "confident".
- Very specific: line 431
Never modify the SimSpec in place.
- Very specific: line 432
Never call the LLM from any script except figure_diff.py.
- Very specific: line 433
Never trust a figure_diff pass if confidence < 0.6 — downgrade to warn even on high score.
- Very specific: line 434
Never re-run a check after a fail and treat a different result as the truth.
- Very specific: line 476
All five are also re-exported from scripts/__init__.py for direct Python import:
- Very specific: line 510
All eight scripts re-export from scripts/__init__.py.
≈ “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
science-integrity
The critic. The thing JutulGPT does not have. The reason a Mobius simulator is trustworthy and a vibe-coded one is not.
1. Philosophy — parallel critic, not serial reviewer
Mobius generates code. Generated code is a hypothesis. A hypothesis without an external falsifier is just a guess in a costume. JutulGPT (arXiv 2603.00214, §6.1) admits this directly: their authoritative arbiter is the simulator itself — if it runs, the answer is "physically valid". This is a category error. A simulator that converges on the wrong PDE, with wrong units, drifting energy, and producing a figure that does not match the paper is still convergent. The simulator certifies numerical health, not paper agreement.
The science-integrity skill is the second arbiter. It runs in parallel with the simulator (not after it as a unit test), it observes the SimSpec at construction time and the telemetry at execution time, and it speaks back to upstream generators in a structured language they can react to. Five orthogonal checks, none of which the simulator-as-arbiter pattern catches:
- Units — SimSpec contains a constant
E = 1.6e-19declared in eV but used in code as J. Simulator runs cleanly. Output is wrong by 6.24e18×. Only a unit-aware verifier catches this. - CFL —
dt,dx, andc_maxjointly violate the Courant condition. Simulator either explodes (caught) or, with implicit smoothing, silently dampens the wave (not caught — this is the dangerous case). - Conservation — Hamiltonian SimSpec, no dissipation term, energy drifts 2% per period. Simulator converges; physics is broken.
- Figure-diff — paper Fig 3 shows a 2-peak interference pattern; sim renders 4 peaks (slit count off-by-one). Simulator is happy. The user is looking at a different experiment.
- Claim-match — paper claims "visibility 0.78 at λ=500 nm". Sim measures 0.31. Simulator does not know the paper exists.
These five layers, run together, do not exist anywhere in the agentic-simulation literature as of April 2026 (verified: JutulGPT, MooseAgent, AutoMOOSE, ChatCFD, FeaGPT, MCP-SIM, InteractScience, ChronoLLM — none combine an external SimSpec verifier + paper-figure VLM diff + paper-claim numerical comparator). This skill is the moat.
The choice of parallel over serial is deliberate. A serial review pattern (generator → checker → re-generator) bottlenecks on the checker and forces the generator to wait for verdicts. Parallel critic runs alongside, signals fail fast, and lets the orchestrator decide whether to interrupt the current generator iteration or queue the failure for the next one. JutulGPT is serial. We are parallel.
1.5 Phase routing — pre-render vs. post-render checks
Why a split. The J5-J6 live run on the Young paper showed Phase D (verify)
running for ~36 minutes — the dominant chunk of the 52-minute end-to-end
budget. Root cause: every check (including the perceptual VLM-driven
figure_diff, the rendered-scene narration_bijection, and the
multi-modal multimodal_triangulation) was run pre-render, before the
Phase F scene even existed. Three of the eight checks are inherently
post-render and were burning model time on absent inputs.
Decision. Split the eight bundled checks across two phases of the Mobius DAG. The orchestrator routes by phase name; this skill MUST honour the split so it does not waste time on checks whose inputs do not exist yet.
Phase D-verify (pre-render) — run these 5
Cheap, deterministic, and require ONLY the SimSpec — no rendered scene, no telemetry.
| # | Check | Script | Why pre-render |
| 1 | units | check_units.py | Reads physics.constants[] and equation strings; pint-only, no scene needed. |
| 2 | cfl | check_cfl.py | Reads simulation.solver_hints + space + time + c_max; closed form, no I/O. |
| 3 | conservation | check_conservation.py | Reads telemetry IF available, else returns pending. Falls into pre-render slot because telemetry from Phase E is available before Phase F. |
| 4 | claim-match | claim_match.py | Reads paper.numerical_claims[] against telemetry; same Phase E/D ordering as conservation. |
| 5 | eligibility | check_eligibility.py | Reads SimSpec gates only; deterministic Python, no LLM. |
Total wall-clock target on synthetic input: < 5 minutes.
Phase G-diff (post-render) — run these 3
Require the rendered scene.html (or the screenshotted PNG of it) and
the inline narration markdown. Routing them to G-diff lets the perceptual
work happen ONCE, on the final artifact, rather than on a stub during
Phase D.
| # | Check | Script | Why post-render |
| 6 | figure-diff | figure_diff.py | VLM perceptual compare between paper figure and rendered scene PNG; needs the scene. |
| 7 | narration-bijection | check_narration_bijection.py | Walks the inline literals in narration.what / .why / .try_it against paper.numerical_claims[]; the narration MD must exist. |
| 8 | multimodal-triangulation | check_multimodal_triangulation.py | Aggregates analytical + visual + numerical axes per claim; the visual axis needs the scene render. |
Routing contract for skill authors
- When dispatched in Phase D-verify, run ONLY checks 1-5. Emit a
checks[]array carrying their five entries plusoverallandeligibility. Do NOT run figure_diff, narration_bijection, or multimodal_triangulation here even if inputs happen to be present. - When dispatched in Phase G-diff, run ONLY checks 6-8. Emit a
checks[]array carrying their three entries; the orchestrator merges them into the existingsimspec.integrity.checks[]from Phase D. - The orchestrator signals the phase via the dispatch instruction (it appends "Phase D-verify (pre-render checks only)" or "Phase G-diff (post-render checks only)" to the user content). Honour the routing.
- A skill that runs an out-of-phase check MUST mark it
status: "pending"withmessage: "deferred to phase G-diff"so the orchestrator can audit the skip inrefinement_log.jsonl.
Termination contract (CRITICAL — anti-hang guarantee)
J6-night live runs hung 17+ minutes in Phase D and the now-removed Phase C slot because nothing in the prompt prevented re-verification loops. To prevent recurrence, every dispatch of this skill MUST honour:
- Sequential, single-pass execution. Run each check in this phase
ONCE in the listed order. Do not re-invoke a check that has already
produced a
pass | warn | fail. The orchestrator's refine loop is the ONLY mechanism that can re-run a check — never re-run inside one turn. - Per-check tool budget = 2. A check may call
code_executionat most twice: once to run the script, optionally once more if the first call returned malformed JSON or a transient error. After the second call, mark the checkfailwithmessage: "script error after 2 attempts"and advance. - Single emission rule. Emit the fenced
jsonblock ONCE, at the very end of your reply. After it, you MUST NOT callcode_executionagain. The orchestrator's regex parser only reads the LAST fenced block, but the model entering a tool_use loop after emission is exactly what hangs the API. - No "let me re-verify" reflex. If a script returns
fail, that is the answer for this phase iteration. Do not call the same script a third time hoping for a different result. The retry-on-failure loop is the orchestrator's job, not yours.
The five-check pre-render routing brings Phase D wall-clock back under five minutes on the synthetic SimSpec; Phase G then absorbs the perceptual work once the scene render is final.
2. The five checks
Each check is implemented as a deterministic Python script in scripts/. None of the scripts call an LLM (with one principled exception: figure_diff.py uses the Anthropic VLM because structural figure comparison is not solvable deterministically — flagged loudly below). Each script returns a JSON object with a fixed schema, and the orchestrator reads that JSON.
2.1 Units check (scripts/check_units.py)
Purpose. Verify that every numerical constant declared in the SimSpec carries a unit, that those units parse with pint, and that within governing equations and any embedded Python code the dimensions match across = and across operators. Catches the eV-vs-J class of errors, the cgs-vs-SI mixing class, and the bare-magnitude-passed-where-quantity-expected class.
Input interface. Reads simspec.physics.constants[] (each with name, value, unit), simspec.physics.governing_equations[] (each with lhs_units, rhs_units if known), and any simspec.primitives[].code Python string. Optionally reads a single primitive code path via the path_to_validate argument.
Output. {"status": "pass"|"warn"|"fail", "issues": [...], "summary": "..."}. Each issue has a JSONPath, a human message, and a severity. warn is used when a unit is missing but inferable; fail when a declared unit is dimensionally inconsistent with its use.
Failure behavior. Blocks primitive-generator's next iteration when status is fail. warn is annotated on the SimSpec but does not block.
2.2 CFL stability check (scripts/check_cfl.py)
Purpose. For solver schemes with explicit time integration, compute the Courant–Friedrichs–Lewy stability number from the declared time step, grid spacing, and characteristic velocity. Catches "the simulator will diverge in 200 steps" before any code runs.
Input interface. Reads simspec.simulation.solver_hints.scheme (one of "fdtd", "leapfrog", "rk4-advection", "fft-spectral", "implicit", "analytic"), simspec.simulation.space.{nx, ny, nz, dx, dy, dz}, simspec.simulation.time.dt, and simspec.physics.constants[] to extract c_max (looking for names like c, c_light, v_max, v_p, v_g).
Output. {"status": ..., "cfl_number": float, "max_allowed": float, "scheme": str, "details": {...}}. Implicit and analytic schemes return pass with a note.
Failure behavior. fail when CFL > max_allowed, blocking the primitive-generator. warn when c_max cannot be extracted (returns the formula and asks the upstream to declare it).
2.3 Conservation check (scripts/check_conservation.py)
Purpose. Measure the relative drift of declared conserved quantities across the simulation telemetry. Catches energy/mass/probability/momentum drift in regimes where the governing equations should preserve them.
Input interface. Reads a telemetry file (CSV or JSONL with at least column t and one or more conserved quantity columns) and the SimSpec's physics.regime field plus physics.governing_equations[] to detect Hamiltonian vs dissipative structure.
Output. {"status": ..., "quantities": {"energy": {"drift_pct": ..., "status": ...}, "mass": {...}, ...}, "summary": "..."}.
Failure behavior. fail when drift exceeds the regime-dependent threshold (1% per 1000 steps for linear/Hamiltonian, 10% for strongly-nonlinear). For stochastic regimes, checks running mean instead of instantaneous value.
2.4 Figure-diff check (scripts/figure_diff.py)
Purpose. VLM-based structural comparison between the paper's reference figure and the simulator's rendered output. Catches "wrong slit count", "wrong peak count", "phase inverted", "spatial symmetry broken".
HONEST FLAG. This check is not deterministic. It calls an LLM (Opus 4.7 via the Anthropic vision API) and parses structured JSON back. We retry once on parse failure and cache by image hash to make the same (ref, sim) pair return the same answer within a session, but two different VLM invocations on the same images may return slightly different similarity_score floats. We accept this because no deterministic algorithm comparing arbitrary scientific figures exists at quality level required (pHash and SSIM fail badly on log-scale plots, on rotated/cropped axes, on different colormaps). We restrict the VLM to a structural comparison (count peaks, axis layout, spatial symmetry), not aesthetic judgment.
Input interface. Two image paths, plus an optional claim_context string from simspec.paper.figure_mapping[i].
Output. {"status": ..., "similarity_score": 0..1, "features_present": [...], "features_missing": [...], "features_spurious": [...], "confidence": 0..1}.
Failure behavior. fail when similarity_score < 0.5 AND confidence > 0.7. warn when 0.5 ≤ similarity_score < 0.75. Pass otherwise.
2.5 Claim-match check (scripts/claim_match.py)
Purpose. For each numerical claim extracted from the paper (by claim-extractor skill), locate the corresponding measurement in the simulation telemetry and compare with tolerance. Catches "paper says 0.78, sim says 0.31".
Input interface. Reads simspec.paper.numerical_claims[] (each with quantity, value, unit, source_quote, optional tolerance) and a telemetry file path.
Output. {"status": ..., "claims": [{"quantity": ..., "paper_value": ..., "sim_value": ..., "rel_error": ..., "status": ...}, ...], "n_pass": int, "n_warn": int, "n_fail": int, "n_not_applicable": int}.
Failure behavior. fail when any claim has rel_error > tolerance (default 5%). Qualitative claims like "monotonically increases with frequency" return not-applicable and do not fail (they are deferred to a future structural-claim checker).
2.6 Output format (mandatory) — single emission, no re-run
You MUST emit your final output as a single fenced json block at the
END of your reply, with NO prose after the closing fence. **Once emitted,
stop all `code_execution` tool calls.** The orchestrator parses that block
by regex (`/(?:json)?\s*\n([\s\S]?)\n\s```/`) and any tool invocations
after it cause the dispatch to hang on Anthropic's stream pipeline.
Per-check behaviour contract
When running a bundled script:
- Call the script once via
code_execution(bash, then Python). - Parse the JSON response. If JSON parse fails, retry once more (2 attempts total per check).
- If both attempts fail, emit
{"status": "fail", "summary": "script returned invalid JSON"}for that check and advance. - If parse succeeds, read the script's
status:pass→ mark final, advance.warn→ mark final, advance.fail→ emit final, advance. Do NOT re-run the script to "verify" or "fix" the failure. The orchestrator's refine loop is the only legitimate retry mechanism.
- Repeat for each check in the phase (5 checks in D-verify; 3 checks in G-diff).
- After all checks complete, aggregate into the canonical
integrityJSON. - Emit the fenced
jsonblock ONE TIME. Nocode_executioncalls after this point.
If you are tempted to re-run a script "to be sure", resist. Your job is to RUN each check ONCE, not to repeatedly affirm it. Confidence is a single-pass property; double-pass costs minutes of wall-clock and produces zero additional information.
Running the bundled scripts via code_execution and aggregating their per-script JSON is fine and recommended, but the canonical orchestrator-facing artifact is the AGGREGATE integrity report inlined as the final fenced JSON block of your reply.
The canonical artifact for science-integrity (consumed by orchestrator.ts
Phase D, which reads result.checks[] into simspec.integrity.checks[]):
{
"checks": [
{
"type": "units",
"status": "pass",
"message": "all constants have UCUM-parseable units; dimensional homogeneity OK across governing_equations[].",
"details": { "n_constants": 4, "n_equations": 2 }
},
{
"type": "cfl",
"status": "warn",
"message": "scheme=fft-spectral; CFL not applicable, skipped.",
"details": { "scheme": "fft-spectral" }
}
],
"overall": "ok",
"eligibility": {
"score": 0.95,
"tier": "full",
"gates": {
"has_equations": true,
"has_figures": true,
"has_numerical_claims": true,
"units_consistent": true,
"regime_supported": true
},
"reasons": []
}
}Required per check: type (one of the schema enum values from
simspec.schema.json §integrity_check.type — units | cfl | conservation |
figure-diff | claim-match | schema-validation | ambiguity | eligibility |
narration-bijection | multimodal-triangulation | archetype-routing |
narration_audit) and status (pass | warn | fail | pending). message
and details are recommended.
overall MUST be one of ok | degraded | fail. eligibility is REQUIRED
when the SimSpec carries an eligibility block (R6); for legacy specs without
the eligibility schema, it MAY be omitted.
If the input SimSpec is unavailable or every script fails to execute, emit:
{ "error": "simspec.json missing; cannot run integrity checks" }DO NOT emit narration, summaries, or follow-up questions after the closing fence — they break the orchestrator's downstream consumption and get silently dropped.
3. Integration flow
paper.pdf
│
▼
┌──────────────────┐
│ paper-parser │
└────────┬─────────┘
│ parsed_paper.json
▼
┌──────────────────┐
│ physics-interpret│
└────────┬─────────┘
│
┌──────────[ check_units ]◄─── governing equations + constants
│ │
│ ▼
│ ┌──────────────────┐
│ │ simspec-author │
│ └────────┬─────────┘
│ │ simspec.json
│ ▼
├──────────[ check_units, check_cfl ]◄─── solver_hints, declared constants
│ │
│ ▼
│ ┌──────────────────┐
│ │primitive-generate│
│ └────────┬─────────┘
│ │ generated code
│ ▼
├──────────[ check_units (on code) ]
│ │
│ ▼
│ ┌──────────────────┐
│ │ Managed Agent │ ◄── execution
│ │ (Python sandbox)│
│ └────────┬─────────┘
│ │ telemetry.csv, render.png
│ ▼
├──────────[ check_conservation, claim_match ]◄─── telemetry
│ │
│ ▼
└──────────[ figure_diff ]◄────── render.png vs paper figure
│
▼
integrity report
│
▼
orchestrator decides:
pass → release to user
fail → retry loop (§4)
warn → annotate, releaseThe five scripts run at four moments: after physics-interpreter (units only), after simspec-author (units + cfl), after primitive-generator (units on code), and after Managed Agent execution (conservation + claim-match + figure-diff). Each invocation produces a JSON report that the orchestrator merges into simspec.integrity.checks[].
4. Retry loop contract
When a check fails, the orchestrator receives a JSON message with this exact schema:
{
"skill": "science-integrity",
"check": "check_units" | "check_cfl" | "check_conservation" | "figure_diff" | "claim_match",
"status": "fail",
"stage": "after-physics-interpreter" | "after-simspec-author" | "after-primitive-generator" | "after-execution",
"details": { "...": "..." },
"remediation_hint": "string — natural-language nudge for the upstream generator",
"retry_count": 0,
"max_retries": 3
}The orchestrator's contract:
- On first fail, increment
retry_count, appendremediation_hintto the upstream generator's next prompt, re-run only the failing stage. - On second fail, do the same with stronger prompt framing ("the previous attempt also failed because…").
- On third fail, the orchestrator escalates to the user with a structured explanation. The simulator does not get released.
figure_difffailures withconfidence < 0.6are downgraded towarnautomatically (we do not block on a low-confidence VLM call).
Default max_retries = 3. The orchestrator may override per-stage in its config.
5. Non-goals
This skill DOES NOT:
- re-run the simulation. Re-execution is the orchestrator's job; we observe and report only.
- modify the SimSpec. We annotate
integrity.checks[]with our findings; we never patch a unit or change adt. - suggest fixes beyond the
remediation_hintfield. Code-level repair is the upstream generator's job. - call the LLM, except in
figure_diff.pywhere there is no deterministic alternative (loudly flagged). - enforce paper-quality scientific judgment. We catch unit slips, stability violations, conservation drift, structural figure mismatches, and numerical claim disagreements. We do not catch "wrong physics in the paper itself" or "wrong physical interpretation by the user".
6. Worked examples — six concrete catches
Example 1 — Units. SimSpec declares {"name": "E_band", "value": 1.6e-19, "unit": "eV"}. Generated code multiplies E_band * c**2 to get a length scale and assumes E_band is in joules. check_units parses 1.6e-19 eV → 2.56e-38 J, sees it used as if joules, raises fail with path primitives.bandgap_calc.code:line_42. Catches a 6.24e18 error before any execution.
Example 2 — CFL. SimSpec has simulation.time.dt = 1e-3, simulation.space.dx = 1e-4, scheme "leapfrog", constant c = 1.0 (wave speed). Courant number = c·dt/dx = 10. check_cfl returns {"status": "fail", "cfl_number": 10.0, "max_allowed": 1.0, "scheme": "leapfrog"}. Generator gets remediation_hint: "Reduce dt to ≤ 1e-4 s or coarsen dx".
Example 3 — Conservation. Hamiltonian harmonic oscillator, regime "linear", governing equation d²x/dt² = -ω²x (no dissipation term). Telemetry shows energy(t=0) = 1.0, energy(t=1000·dt) = 1.022. Drift = 2.2%. check_conservation returns {"status": "fail", "quantities": {"energy": {"drift_pct": 2.2, "status": "fail", "threshold_pct": 1.0}}}. Generator gets "Switch from forward Euler to symplectic integrator".
Example 4 — Figure-diff. Paper Fig 3 = 2-peak Young's double-slit interference pattern. Simulator renders 4 peaks (a 4-slit was generated by mistake from the parser). VLM diff returns {"similarity_score": 0.34, "features_present": ["interference_pattern", "central_envelope"], "features_missing": [], "features_spurious": ["additional_peaks", "subsidiary_maxima_count_mismatch"], "confidence": 0.88}. fail. Generator receives "Reference shows 2-peak pattern; sim shows 4. Verify slit count in primitive.".
Example 5 — Claim-match. Paper text: "the visibility V at central wavelength 500 nm reaches 0.78". SimSpec has claim {"quantity": "visibility", "value": 0.78, "unit": "dimensionless", "source_quote": "visibility V at central wavelength 500 nm reaches 0.78"}. Telemetry column visibility peaks at 0.31. check_claim returns {"status": "fail", "claims": [{"quantity": "visibility", "paper_value": 0.78, "sim_value": 0.31, "rel_error": 0.602, "tolerance": 0.05, "status": "fail"}]}. Generator gets "Visibility off by factor 2.5 — check coherence length parameter".
Example 6 — Stability via NaN. Telemetry has finite values for steps 1..846, NaN at step 847 onwards. check_conservation reads telemetry, detects the NaN in the energy column, returns {"status": "fail", "summary": "NaN in energy at row 847", "remediation_hint": "Solver diverged before completion. Reduce dt or check initial conditions."}. The figure-diff and claim-match never run for this artifact — pre-render fail short-circuits.
6.5. Eligibility (R6) and multi-modal triangulation (R8)
Two contracts added in v0.2 sit alongside the five deterministic checks above. They are NOT scored as a 6th and 7th deterministic check; they aggregate the others.
6.5.1 Eligibility scoring (R6) — scripts/check_eligibility.py
Every paper that enters Mobius gets an eligibility object on its SimSpec. The score is a weighted sum over five gates, each binary, run against physics.json, figures.json, claims.json, and the SimSpec's existing viz.scene_archetype:
| Gate | Weight | Pass condition |
has_equations | 0.30 | physics.governing_equations[] non-empty AND every entry has a parseable sympy field |
has_figures | 0.15 | paper.figures[] has at least one figure with a non-empty caption |
has_numerical_claims | 0.20 | paper.numerical_claims[] non-empty |
units_consistent | 0.20 | every claim has a UCUM-valid unit AND every governing-equation variable has a UCUM-valid unit (pint accepts them) |
regime_supported | 0.15 | viz.scene_archetype is one of the eight implemented archetypes (ray-optics, glass-dispersion, field-2d, orbital-3d, particle-cloud, lattice, graph-time, schematic) |
score = 0.30 * has_equations + 0.15 * has_figures + 0.20 * has_numerical_claims + 0.20 * units_consistent + 0.15 * regime_supported.
Tier rules:
score >= 0.7→tier: "full"→ simulator renders normally.0.4 <= score < 0.7→tier: "best_effort"→ simulator renders with caveats;narration.caveats[]MUST be populated by the orchestrator before the skill exits.score < 0.4→tier: "ineligible"→ orchestrator routes toviz.scene_archetype: "explainer"(viz-mapper/templates/explainer.html) and aborts the simulation pipeline cleanly.eligibility.reasons[]is the user-facing failure list shown on the explainer card.
The script is deterministic, runnable standalone (python check_eligibility.py path/to/simspec.json), and emits the JSON eligibility object the schema expects (#/$defs/eligibility).
6.5.2 Narration bijection (R5 enforcement) — scripts/check_narration_bijection.py
For SimSpecs whose narration block carries the v0.2 contract (what, why, try_it, claims_referenced[]), this script enforces the bijection between paper.numerical_claims[] and the inline numerical references in the three paragraphs. Two sub-checks:
- Forward direction (orphan claims). Every
paper.numerical_claims[i].idmust appear either innarration.claims_referenced[]OR be acknowledged inline (itsvalueappears within 5% relative tolerance somewhere inwhat/why/try_it/segments[].markdown). Failure →integrity.checks[]entry withtype: "narration-bijection",status: "fail". - Reverse direction (ungrounded numbers). Every numerical literal in the inline text must resolve to a
paper.numerical_claims[i].value, aphysics.constants[i].value, or aviz.sliders[i].default, within 5% tolerance. Failure →integrity.checks[]entry withtype: "narration-bijection",status: "warn".
Unverified-claim caveat enforcement: if any claim referenced in claims_referenced[] carries verification.status in {"partially-verified", "unverified"}, the literal phrase not independently verified MUST appear at least once in what/why/try_it/caveats[]. Failure → status: "fail".
CLI: python check_narration_bijection.py path/to/simspec.json. Exit codes: 0 = pass, 1 = warn, 2 = fail. The script is pure Python, no LLM call.
6.5.3 Multi-modal triangulation (R8) — scripts/check_multimodal_triangulation.py
For each paper.numerical_claims[i] the script runs three independent verification axes and writes the result back to paper.numerical_claims[i].verification:
- Analytical — if the claim has a
formulafield, evaluate it withsympy(or withpint-aware Pythonevalfor closed-form physics formulas) using the SimSpec'sphysics.constants[]and the claim's surrounding parameters. Compare the result toclaim.valuewithinclaim.tolerance(default 5%). Without aformula, markanalytical: "n/a". - Visual — if the claim has a
figure_reffield, run a structural compare between the SimSpec's rendered output (path supplied by the orchestrator as--render) and the paper figure (resolved viapaper.figures[id == figure_ref].idagainst the run directory). The compare uses scikit-image SSIM as the deterministic floor (pass if SSIM ≥ 0.7) AND optionally callsfigure_diff.py(the existing VLM check) whenMOBIUS_VLM_TRIANGULATION=1. Without afigure_ref, markvisual: "n/a". - Numerical — if the claim has a numeric
valueAND the orchestrator supplied a telemetry path or a measured-value override (--measured K=Vflag), compute the relative error against the measured value and pass when belowclaim.tolerance. Without a measured value, marknumerical: "n/a".
Aggregate status:
verified— every applicable axis (i.e. notn/a) returnspass. At least one axis must be applicable.partially-verified— at least one applicable axis passes AND at least one applicable axis fails.unverified— every applicable axis fails, OR no axis was applicable.
partially-verified and unverified claims trigger the not independently verified phrase requirement enforced by check_narration_bijection.py above. The orchestrator refuses to advance past Phase D (release) when any claim is partially-verified without that caveat present.
CLI: python check_multimodal_triangulation.py path/to/simspec.json [--render path/to/render.png] [--measured key=value ...]. Exit codes: 0 = pass (all verified), 1 = warn (any partially-verified), 2 = fail (any unverified).
7. Red flags — never do these
- Never silently pass a
warnaspass. If the orchestrator wants to ignore a warn, it must do so explicitly. - Never skip a check because the upstream generator says it's "confident". Confidence is irrelevant; we observe the artifact, not the prompt that produced it.
- Never modify the SimSpec in place. Annotate
integrity.checks[], never overwritephysics.constants[i].value. - Never call the LLM from any script except
figure_diff.py. Determinism is the moat; the moat erodes one stochastic call at a time. - Never trust a
figure_diffpassifconfidence < 0.6— downgrade towarneven on high score. - Never re-run a check after a
failand treat a different result as the truth. If two runs disagree, the more conservative (failing) verdict wins; investigate the upstream generator.
Scripts
The five scripts are bundled. Their public signatures:
# scripts/check_units.py
def check_units(simspec: dict, path_to_validate: str | None = None) -> dict: ...
# CLI: python check_units.py < simspec.json (stdin)
# python check_units.py path/to/simspec.json
# Exit codes: 0 = pass, 1 = warn, 2 = fail# scripts/check_cfl.py
def check_cfl(simspec: dict) -> dict: ...
# CLI: python check_cfl.py < simspec.json
# Exit codes: 0 = pass, 1 = warn, 2 = fail# scripts/check_conservation.py
def check_conservation(telemetry_path: str, spec: dict) -> dict: ...
# CLI: python check_conservation.py <telemetry.csv> <simspec.json>
# Exit codes: 0 = pass, 1 = warn, 2 = fail# scripts/figure_diff.py
def figure_diff(ref_figure_path: str, sim_output_path: str, claim_context: str = "") -> dict: ...
# CLI: python figure_diff.py <ref.png> <sim.png> [claim_context]
# Requires ANTHROPIC_API_KEY in env. Caches by hash in /tmp/mobius_figure_diff_cache/.# scripts/claim_match.py
def claim_match(simspec: dict, telemetry_path: str) -> dict: ...
# CLI: python claim_match.py <simspec.json> <telemetry.csv>
# Exit codes: 0 = pass, 1 = warn, 2 = failAll five are also re-exported from scripts/__init__.py for direct Python import:
from scripts import check_units, check_cfl, check_conservation, figure_diff, claim_matchThe three v0.2 scripts (R5/R6/R8) sit alongside the five originals:
# scripts/check_eligibility.py
def check_eligibility(simspec: dict) -> dict: ...
# CLI: python check_eligibility.py < simspec.json
# python check_eligibility.py path/to/simspec.json
# Exit codes: 0 = full, 1 = best_effort, 2 = ineligible# scripts/check_narration_bijection.py
def check_narration_bijection(simspec: dict) -> dict: ...
# CLI: python check_narration_bijection.py path/to/simspec.json
# Exit codes: 0 = pass, 1 = warn, 2 = fail# scripts/check_multimodal_triangulation.py
def check_multimodal_triangulation(
simspec: dict,
render_path: str | None = None,
measured: dict[str, float] | None = None,
) -> dict: ...
# CLI: python check_multimodal_triangulation.py simspec.json [--render PNG] [--measured k=v ...]
# Exit codes: 0 = all verified, 1 = any partially-verified, 2 = any unverifiedAll eight scripts re-export from scripts/__init__.py.
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.

