Value objects#

Everything that crosses a boundary in MatMelt is a frozen dataclass built from standard-library types. Not because immutability is fashionable, but because these values are written to a database, read back by a different process, and compared across restarts — and a mutable object that half the system has a reference to cannot be reasoned about across a restart at all.

core/jobspec.py imports nothing but the standard library. Full signatures are in Value objects.

The job vocabulary#

JobSpec — what to run. An argv command, input_artifacts (name → local path), output_artifacts (names the job promises), env, cwd, resources, and scratch_inputs marking which inputs are implementation rather than data. It names no machine (What vs. where).

scratch_inputs exists for one concrete reason: a stage’s runner script is an input, but it is not the scientist’s data. Marking it scratch keeps screen_runner.py out of the material directory, so a workspace contains results rather than a copy of the tool.

Handle — the durable identity of a submitted job: backend_name, job_id, and an extra mapping of JSON-safe values. The invariant is stated in its docstring and enforced by test: job_id is a process id or scheduler job id, never a socket, a Popen, or an ORM object. Everything poll, fetch and release need later must survive a round trip through JSON, because that is exactly what happens to it in the ledger.

JobResult — what happened: exit_code, stdout, stderr, and the resolved paths of collected outputs. exit_code is int | None, and None means “ran, exit code unrecoverable” — never defaulted to 0. Conflating “unknown” with “success” is how a fabricated result gets into a database.

JobStatus — a string enum with exactly four members and an is_terminal property. String-valued so it round-trips through JSON and reads plainly in a log.

Resources — typed cores, gpu, walltime_s. Typed rather than a free-form dict so a backend can map them onto --cpus-per-task and --time without guessing key names.

Capabilities / HealthStatus — what a backend advertises, and whether it is usable now. HealthStatus.detail is a place where secrets could leak, so the contract is explicit: it never contains one, and health_check never raises.

BackendError — infrastructure only. A non-zero exit is not one of these.

The stage vocabulary#

WorkItem — one material: an id and a directory.

StageInputSpec / StageOutputSpec — a declared file plus a human description. The description is not decoration; it is what makes a refusal readable:

missing or empty required input file(s) ['structure.cif'] in
materials-work/Cl1Na1_3D-1 (one candidate crystal from the structure stage)

StageVerdict — ok, reason, terminal. Three fields carrying the verdict-vs-fault doctrine: terminal=True means a deterministic science rejection that must not be retried (The stage contract).

Requirement — module, pip_name, purpose. Two separate name fields because they differ often enough that guessing would hand users a broken command: mace is installed by mace-torch.

The state vocabulary#

MaterialState — nine string-valued states; see Material states.

MaterialRecord — a material as stored: state, lineage, attempts, error fields, timestamps, and its completion timeline. The timeline is what lets retry derive a resume state per material without consulting config.

InFlightJob — a ledger row: the stage, the backend, the handle, when it was submitted.

MonitorSnapshot and friends — one value describing everything the dashboard shows, produced by a pure function and consumed by renderers (State and storage).

Conventions worth keeping#

  • Frozen, with independent default containers. A shared mutable default is a bug that appears only on the second instance.

  • Standard-library types only. These values are persisted; a third-party type in one of them would make the on-disk format depend on that library.

  • Optional means unknown. None in these objects means “not known”, never “zero” and never “false” — in exit_code, in a CPU sample, in a missing metric.

  • String enums. They survive JSON and read correctly in a message.