Query, view, export, and diff stored configs and josh sources
Introduction
Every run registered in a joshpy registry stores its rendered config (.jshc) and josh source (.josh) content. The joshpy.inspect module lets you view, export, and diff these stored files – even after the original files have been modified or deleted.
This is useful for:
Discovering what’s in a registry: List labels, sessions, and run details from the terminal
Comparing runs: See exactly what changed between two parameter configurations
Debugging failures: Inspect the config and model source for a failed run
Auditing: Verify what was actually run, not what’s currently on disk
Setup
from joshpy.registry import RunRegistryfrom joshpy.jobs import JobConfigregistry = RunRegistry(":memory:")session_id = registry.create_session( config=JobConfig(simulation="Main"), experiment_name="inspect_demo",)# Register two runs with different configs and the same josh sourcejosh_source ="""\start simulation Main grid.size = 30 m grid.low = 33.9 degrees latitude, -116.05 degrees longitude grid.high = 33.91 degrees latitude, -116.04 degrees longitude steps.low = 0 count steps.high = 86 countend simulationstart patch Default Tree.init = create 10 count of Tree export.treeCount.step = count(Tree) export.averageHeight.step = mean(Tree.height)end patchstart organism Tree age.init = 0 years age.step = prior.age + 1 year height.init = 0 meters height.step = prior.height + sample uniform from 0 meters to config sweep_config.maxGrowthend organism"""registry.register_run( session_id=session_id, run_hash="abc123def456", josh_path="model.josh", josh_content=josh_source, config_content="maxGrowth = 5 meters\nfireYear = 75 count", file_mappings={"soil_quality": {"path": "data/soil_quality.jshd", "hash": "a1b2c3"}, }, parameters={"maxGrowth": 5, "fireYear": 75},)registry.label_run("abc123def456", "low_growth")registry.register_run( session_id=session_id, run_hash="fed987cba654", josh_path="model.josh", josh_content=josh_source, config_content="maxGrowth = 15 meters\nfireYear = 75 count", file_mappings={"soil_quality": {"path": "data/soil_quality.jshd", "hash": "a1b2c3"}, }, parameters={"maxGrowth": 15, "fireYear": 75},)registry.label_run("fed987cba654", "high_growth")# Record completed runs (3 replicates each)for run_hash in ["abc123def456", "fed987cba654"]:for rep inrange(3): run_id = registry.start_run( run_hash=run_hash, session_id=session_id, replicate=rep, output_path=f"output/{run_hash}/rep{rep}.csv", ) registry.complete_run(run_id, exit_code=0)registry.update_session_status(session_id, "completed")print(f"Registered 2 runs: low_growth, high_growth")
Registered 2 runs: low_growth, high_growth
Querying the Registry
Before viewing or diffing runs, you often need to know what’s in the registry. The registry exposes four describe_*() methods that return human-readable summaries – the same output the CLI produces, available directly on the registry object so you don’t have to import anything.
Note
Each describe_*() method is a thin wrapper around the matching joshpy.inspect.format_*() function (describe_labels → format_labels, and so on). Use whichever reads better in your code; the free functions remain available if you prefer to import them.
Listing Labels
Labels are the human-readable names assigned to run configurations. Use describe_labels() to see what’s available:
A session groups the configs from a single sweep. Use describe_sessions() to see all sessions with their experiment name, status, and run counts:
print(registry.describe_sessions())
SESSION EXPERIMENT STATUS JOBS RUNS (ok/fail/pend) CREATED
078a9501... inspect_demo completed 2 6/0/0 2026-06-17 04:57:08
Run Details
Use describe_run() to see full details for a specific run – parameters, data files, and replicate results:
print(registry.describe_run("low_growth"))
Run: low_growth (abc123def456)
==============================
Session: 078a9501-9a68-4864-bdd8-b8215b59a5e7
Josh: model.josh
Created: 2026-06-17 04:57:08
Parameters:
fireYear = 75.0
maxGrowth = 5.0
Data files:
soil_quality: data/soil_quality.jshd
Consistency:
[warning] ran_not_ingested: run_hash abc123def456 has a succeeded run but no ingested cell_data (run ahead of analysis — ingest results).
Runs: 3 succeeded, 0 failed, 0 pending
REP EXIT STARTED OUTPUT
0 0 2026-06-17 04:57:08 output/abc123def456/rep0.csv
1 0 2026-06-17 04:57:08 output/abc123def456/rep1.csv
2 0 2026-06-17 04:57:08 output/abc123def456/rep2.csv
When you need the underlying data rather than a formatted string – for example to drive your own logic or reporting – use get_run_info(), which returns a structured RunDetail (the data-layer counterpart to describe_run()):
All four queries are available as CLI flags, so you can inspect a registry without writing any Python:
# List all labeled runspython-m joshpy.inspect experiment.duckdb --labels# List sessions with status and run countspython-m joshpy.inspect experiment.duckdb --sessions# Show full details for a run (by label or hash)python-m joshpy.inspect experiment.duckdb --info low_growth# Print a data summary for the whole registrypython-m joshpy.inspect experiment.duckdb --summary
Tip
Use --labels first to discover what labels are available, then --info or --diff to dig deeper.
Note
For debug logs tied to a registered run, use the debug CLI in registry mode:
# READ-ONLY snapshot exported from registry
# Run: abc123def456
# Editing this file has no effect. To change parameters,
# edit your source .jshc file and re-run.
maxGrowth = 5 meters
fireYear = 75 count
Or use view_config() to get the content as a string without writing a file:
from joshpy.inspect import view_configcontent = view_config(registry, "high_growth")print(content)
maxGrowth = 15 meters
fireYear = 75 count
Diffing Two Configs
Export both configs and compare them side by side:
# READ-ONLY snapshot exported from registry
# Run: abc123def456
# Editing this file has no effect. To change parameters,
# edit your source .jshc file and re-run.
maxGrowth = 5 meters
fireYear = 75 count
print(f"--- {path2.name} ---")
--- high_growth.jshc ---
print(path2.read_text())
# READ-ONLY snapshot exported from registry
# Run: fed987cba654
# Editing this file has no effect. To change parameters,
# edit your source .jshc file and re-run.
maxGrowth = 15 meters
fireYear = 75 count
For a quick textual comparison that works anywhere – terminal, notebook, CI, or over SSH – use text_diff, which returns a unified diff of the two stored configs:
from joshpy.inspect import text_diffprint(text_diff(registry, "low_growth", "high_growth"), end="")
To open an interactive side-by-side diff in VS Code or Cursor instead:
# Opens side-by-side diff in VS Coderegistry.compare_configs("low_growth", "high_growth")# Or with Cursorregistry.compare_configs("low_growth", "high_growth", ide="cursor")
Viewing Josh Sources
The same workflow applies to the stored josh simulation source. This is especially useful when the original .josh file was rendered from a .josh.j2 template – the temp file is deleted after the sweep, but the content lives on in the registry.
from joshpy.inspect import view_joshsource = view_josh(registry, "low_growth")print(source[:200], "...")
# Opens side-by-side diff of josh sources in VS Coderegistry.compare_josh("low_growth", "high_growth")
For a headless textual comparison, use text_josh_diff (the josh-source counterpart to text_diff):
from joshpy.inspect import text_josh_diffprint(text_josh_diff(registry, "low_growth", "high_growth"), end="")
Note
If both runs used the same josh source (common when only config parameters vary), the diff will show identical files. This confirms that only the config changed between runs.
Command-Line Interface
The python -m joshpy.inspect CLI provides the same functionality from the terminal, without writing Python.
# Open config diff in VS Codepython-m joshpy.inspect experiment.duckdb --diff low_growth high_growth# Open josh source diff in Cursorpython-m joshpy.inspect experiment.duckdb --diff low_growth high_growth --type josh --ide cursor
To print a unified text diff to stdout instead of launching an IDE – useful on a headless machine, in CI, or over SSH – add --print:
# Print a config diff to the terminal (no IDE required)python-m joshpy.inspect experiment.duckdb --diff low_growth high_growth --print# Print a josh source diffpython-m joshpy.inspect experiment.duckdb --diff low_growth high_growth --type josh --print
Export Only
Use --export-only to write files without opening an IDE:
The registry treats a run’s replicate index as a collision-avoidance tag, not a meaningful coordinate — what matters is how many replicates you have. This makes re-running safe and predictable:
Ingestion is idempotent. Re-ingesting outputs already loaded is a no-op; a replicate index already in the registry is skipped rather than duplicated.
get_replicate_count() counts distinct replicate indices — the source of truth for “how many do I have”. loaded_replicates() returns the actual set.
# Normally ingest() loads these; insert a few directly for illustration.run_id = registry.get_runs_for_hash("abc123def456")[0].run_idfor rep in (0, 1, 2): registry.conn.execute("INSERT INTO cell_data (run_id, run_hash, step, replicate) VALUES (?, ?, ?, ?)", [run_id, "abc123def456", 0, rep], )
<_duckdb.DuckDBPyConnection object at 0x7f005850edf0>
<_duckdb.DuckDBPyConnection object at 0x7f005850edf0>
<_duckdb.DuckDBPyConnection object at 0x7f005850edf0>
Pooling backfills to a dense replicate set. With collision_policy="pool", re-running dispatches exactly the missing indices in 0..target-1 — a lost or never-run replicate is refilled at its own index, so the set stays dense (target replicates means indices 0..target-1). Re-running at the same target is a no-op; a larger target fills in the new indices. This works on local, Josh Cloud, and batch-remote alike.
Parity guard.check_consistency() surfaces run↔︎analysis drift — duplicate replicates, data without a config, or runs that succeeded but were never ingested. It runs automatically after ingest(); you can also call it directly:
# high_growth has completed runs but no ingested data yet.for issue in registry.check_consistency():print(f"[{issue.severity}] {issue.kind}: {issue.detail}")
[warning] ran_not_ingested: run_hash fed987cba654 has a succeeded run but no ingested cell_data (run ahead of analysis — ingest results).
drop_run() is the one sanctioned way to mutate run data. Every other registry write is append-only or metadata. Use it to clear a run before redoing a config from scratch (rather than pooling more replicates onto bad outputs):