agentleFS
leventiloleventilo/mobius/skills/science-integrity/SKILL.md

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

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
  1. Very specific:

    When dispatched in Phase D-verify, run ONLY checks 1-5.

    line 72
  2. Very specific:

    When dispatched in Phase G-diff, run ONLY checks 6-8.

    line 76
  3. Very specific:

    Sequential, single-pass execution: Run each check in this phase ONCE in the listed order.

    line 92
  4. Distinctive:

    ONLY mechanism that can re-run a check — never re-run inside one turn.

    line 95
  5. Very specific:

    No "let me re-verify" reflex: If a script returns fail, that is the answer for this phase iteration.

    line 106
  6. Distinctive:

    When running a bundled script:

    line 181
  7. Distinctive:

    Parse the JSON response: If JSON parse fails, retry once more (2 attempts total per check).

    line 184
  8. Distinctive:

    If you are tempted to re-run a script "to be sure", resist.

    line 201
  9. Distinctive:

    RUN each check ONCE, not to repeatedly affirm it.

    line 202
  10. Distinctive:

    If the input SimSpec is unavailable or every script fails to execute, emit:

    line 257
  11. Distinctive:

    DO NOT emit narration, summaries, or follow-up questions after the closing fence — they break the orchestrator's downstream consumption and …

    line 263
  12. Distinctive:

    When a check fails, the orchestrator receives a JSON message with this exact schema:

    line 326
  13. Very specific:

    Default max_retries = 3. The orchestrator may override per-stage in its config.

    line 348
  14. Very specific:

    call the LLM, except in figure_diff.py where there is no deterministic alternative (loudly flagged).

    line 357
  15. Very specific:

    Every paper that enters Mobius gets an eligibility object on its SimSpec.

    line 380
  16. Very specific:

    Forward direction (orphan claims): Every paper.numerical_claims[i].id must appear either in narration.claims_referenced[] OR be …

    line 403
  17. Very specific:

    Reverse direction (ungrounded numbers): Every numerical literal in the inline text must resolve to a paper.numerical_claims[i].value, a …

    line 404
  18. Very specific:

    Never silently pass a warn as pass.

    line 429
  19. Distinctive:

    Never skip a check because the upstream generator says it's "confident".

    line 430
  20. Very specific:

    Never modify the SimSpec in place.

    line 431
  21. Very specific:

    Never call the LLM from any script except figure_diff.py.

    line 432
  22. Very specific:

    Never trust a figure_diff pass if confidence < 0.6 — downgrade to warn even on high score.

    line 433
  23. Very specific:

    Never re-run a check after a fail and treat a different result as the truth.

    line 434
  24. Very specific:

    All five are also re-exported from scripts/__init__.py for direct Python import:

    line 476
  25. Very specific:

    All eight scripts re-export from scripts/__init__.py.

    line 510

≈ “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

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:

  1. Units — SimSpec contains a constant E = 1.6e-19 declared in eV but used in code as J. Simulator runs cleanly. Output is wrong by 6.24e18×. Only a unit-aware verifier catches this.
  2. CFL — dt, dx, and c_max jointly violate the Courant condition. Simulator either explodes (caught) or, with implicit smoothing, silently dampens the wave (not caught — this is the dangerous case).
  3. Conservation — Hamiltonian SimSpec, no dissipation term, energy drifts 2% per period. Simulator converges; physics is broken.
  4. 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.
  5. 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.

#CheckScriptWhy pre-render
1unitscheck_units.pyReads physics.constants[] and equation strings; pint-only, no scene needed.
2cflcheck_cfl.pyReads simulation.solver_hints + space + time + c_max; closed form, no I/O.
3conservationcheck_conservation.pyReads telemetry IF available, else returns pending. Falls into pre-render slot because telemetry from Phase E is available before Phase F.
4claim-matchclaim_match.pyReads paper.numerical_claims[] against telemetry; same Phase E/D ordering as conservation.
5eligibilitycheck_eligibility.pyReads 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.

#CheckScriptWhy post-render
6figure-difffigure_diff.pyVLM perceptual compare between paper figure and rendered scene PNG; needs the scene.
7narration-bijectioncheck_narration_bijection.pyWalks the inline literals in narration.what / .why / .try_it against paper.numerical_claims[]; the narration MD must exist.
8multimodal-triangulationcheck_multimodal_triangulation.pyAggregates 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 plus overall and eligibility. 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 existing simspec.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" with message: "deferred to phase G-diff" so the orchestrator can audit the skip in refinement_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_execution at 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 check fail with message: "script error after 2 attempts" and advance.
  • Single emission rule. Emit the fenced json block ONCE, at the very end of your reply. After it, you MUST NOT call code_execution again. 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:

  1. Call the script once via code_execution (bash, then Python).
  2. Parse the JSON response. If JSON parse fails, retry once more (2 attempts total per check).
  3. If both attempts fail, emit {"status": "fail", "summary": "script returned invalid JSON"} for that check and advance.
  4. 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.
  5. Repeat for each check in the phase (5 checks in D-verify; 3 checks in G-diff).
  6. After all checks complete, aggregate into the canonical integrity JSON.
  7. Emit the fenced json block ONE TIME. No code_execution calls 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, release

The 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, append remediation_hint to 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_diff failures with confidence < 0.6 are downgraded to warn automatically (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 a dt.
  • suggest fixes beyond the remediation_hint field. Code-level repair is the upstream generator's job.
  • call the LLM, except in figure_diff.py where 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:

GateWeightPass condition
has_equations0.30physics.governing_equations[] non-empty AND every entry has a parseable sympy field
has_figures0.15paper.figures[] has at least one figure with a non-empty caption
has_numerical_claims0.20paper.numerical_claims[] non-empty
units_consistent0.20every claim has a UCUM-valid unit AND every governing-equation variable has a UCUM-valid unit (pint accepts them)
regime_supported0.15viz.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 to viz.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].id must appear either in narration.claims_referenced[] OR be acknowledged inline (its value appears within 5% relative tolerance somewhere in what/why/try_it/segments[].markdown). Failure → integrity.checks[] entry with type: "narration-bijection", status: "fail".
  • Reverse direction (ungrounded numbers). Every numerical literal in the inline text must resolve to a paper.numerical_claims[i].value, a physics.constants[i].value, or a viz.sliders[i].default, within 5% tolerance. Failure → integrity.checks[] entry with type: "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 formula field, evaluate it with sympy (or with pint-aware Python eval for closed-form physics formulas) using the SimSpec's physics.constants[] and the claim's surrounding parameters. Compare the result to claim.value within claim.tolerance (default 5%). Without a formula, mark analytical: "n/a".
  • Visual — if the claim has a figure_ref field, run a structural compare between the SimSpec's rendered output (path supplied by the orchestrator as --render) and the paper figure (resolved via paper.figures[id == figure_ref].id against the run directory). The compare uses scikit-image SSIM as the deterministic floor (pass if SSIM ≥ 0.7) AND optionally calls figure_diff.py (the existing VLM check) when MOBIUS_VLM_TRIANGULATION=1. Without a figure_ref, mark visual: "n/a".
  • Numerical — if the claim has a numeric value AND the orchestrator supplied a telemetry path or a measured-value override (--measured K=V flag), compute the relative error against the measured value and pass when below claim.tolerance. Without a measured value, mark numerical: "n/a".

Aggregate status:

  • verified — every applicable axis (i.e. not n/a) returns pass. 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 warn as pass. 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 overwrite physics.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_diff pass if confidence < 0.6 — downgrade to warn even on high score.
  • Never re-run a check after a fail and 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 = fail

All 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_match

The 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 unverified

All eight scripts re-export from scripts/__init__.py.

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.