---
description: The frozen dataclasses that travel between layers — JobSpec, Handle, JobResult and the rest — and the invariants each one carries.
---

# 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 {doc}`../api/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 ({doc}`../explanation/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:

```text
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
({doc}`../explanation/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 {doc}`../reference/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
({doc}`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.

## Related

- {doc}`contracts` — who produces and consumes each of these.
- {doc}`../api/value-objects` — the generated reference.
