Metadata-Version: 2.4
Name: purelms-shared
Version: 0.4.0
Summary: Pydantic schemas bridging PureLMS (AGPL) and its simulation backends (MIT)
Author-email: Daniel McQuillen <daniel@mcquilleninteractive.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/danielmcquillen/purelms-shared
Project-URL: Repository, https://github.com/danielmcquillen/purelms-shared
Project-URL: Bug Tracker, https://github.com/danielmcquillen/purelms-shared/issues
Keywords: purelms,simulation,pydantic,schemas
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Education
Classifier: Topic :: Scientific/Engineering
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: pydantic<3,>=2.6
Provides-Extra: dev
Requires-Dist: pytest<10,>=8.0; extra == "dev"
Requires-Dist: ruff<1,>=0.15; extra == "dev"
Requires-Dist: pre-commit<5,>=4.0; extra == "dev"
Dynamic: license-file

# purelms-shared

Pydantic schemas that bridge [PureLMS](https://github.com/danielmcquillen/purelms) (AGPL-3.0-or-later) and the simulation backend containers (MIT, in [`purelms-interactive-tasks`](https://github.com/danielmcquillen/purelms-interactive-tasks) — formerly `purelms-backends`).

## What's in here

- `purelms_shared.envelopes` — `SimulationInputEnvelope` and `SimulationOutputEnvelope`, the JSON contract for run input + output. Plus the helper types: `InputFile`, `ResourceFile`, `OutputArtifact`, `Message`, `ExecutionContext`.
- `purelms_shared.callbacks` — `ProgressCallback` and `CompleteCallback`, the request bodies a backend POSTs to the worker during a run.
- `purelms_shared.evidence` — `EvidenceManifest` and `EvidenceReference`, the immutable record of a completed run that credential issuance reads later.
- `purelms_shared.constants` — `RunStatus`, `OutputStatus`, `MessageLevel`, `BackendCallbackEvent`, `InputFileRole` enums, plus the `purelms.{input,output,evidence}.v1` schema-version literals.

## What's not in here

- No Django.
- No business logic.
- No dependencies beyond `pydantic`.

If you find yourself reaching for any of those while writing code here, you've probably wandered into the LMS's `purelms.simulations` Django app or a specific backend's runner — not this package.

## Why it's MIT

[PureLMS](https://github.com/danielmcquillen/purelms) is AGPL-3.0-or-later (matching Validibot's Community Edition). Simulation backends are MIT so educator-contributors aren't burdened with copyleft for the per-backend image they ship.

The only artifact that crosses the AGPL/MIT boundary is this schema package — so it's MIT too. Neither side imports the other's code; they share a data contract.

## Versioning

Schema versions are dotted-package strings, not integers:

- `purelms.input.v1` — input envelope shape v1
- `purelms.output.v1` — output envelope shape v1
- `purelms.evidence.v1` — evidence manifest shape v1

Additive changes (new optional field with a default) ship in `v1`. Incompatible changes bump to `v2` with a new class living alongside the old one — running backends pin specific versions of this package, and we never break old envelopes.

**"Additive within v1" is NOT backward compatible at the wire level.** Pydantic's `ConfigDict(extra="forbid")` on every envelope class means a consumer pinning an older `purelms-shared` will REJECT messages from a newer producer that include the new field. Bump the package version and update every consumer's floor pin in lockstep — this was the discipline `unit_block_id` followed when it was added in 0.4.0.

## Updating the wire format — lockstep consumer updates

When you add a field to an envelope or a value to a shared enum, update these consumer files in the SAME PR:

**Python consumers** (each pins a `purelms-shared>=X.Y.Z` floor):

- `purelms/pyproject.toml` (and re-resolve the lockfile)
- `purelms-interactive-tasks/pyproject.toml`
- `purelms-interactive-tasks/echo/backend/pyproject.toml`
- `purelms-interactive-tasks/energyplus_single_zone/backend/pyproject.toml`
- `purelms-interactive-tasks/_template/backend/pyproject.toml`
- Any other per-InteractiveTask `backend/pyproject.toml`

**TypeScript consumers** (vendor the wire types — `extra="forbid"` doesn't apply here, but unions out of sync silently break bundles branching on the missing case):

- `purelms/purelms/static/src/ts/sims/api/types.ts` — the LMS-side dispatcher's authoritative TS types
- `purelms-interactive-tasks/echo/frontend/src/echo.ts` — echo's vendored inline types
- `purelms-interactive-tasks/energyplus_single_zone/frontend/src/types.ts` — EnergyPlus's vendored types
- Any other per-InteractiveTask `frontend/src/types.ts`

**The wire-format-sync tests** in `purelms/purelms/simulations/tests/test_wire_format_sync.py` parametrize over each enum value and grep the LMS-side TS file for it — they fail loudly if a Python enum gains a value that doesn't appear in the matching TS union. Run them in CI on every push.

Why this matters: Slice 3d's post-shipping review round 2 caught a real drift — the TS `RunStatus` union was missing `failed_simulation`, `failed_runtime`, and `timed_out` entirely. Bundles branching on status would have silently failed to render any FAILED_* UI. The lockstep procedure above + the sync tests close that failure mode.

## On `extra="forbid"` — why strictness over flexibility

Every envelope class in this package sets `ConfigDict(extra="forbid", frozen=True)`. That's the choice driving the lockstep-update tax above: a consumer pinning `purelms-shared<X` will *reject* a message from a producer at `>=X` that includes a new field, even when the new field is purely additive and the consumer wouldn't have read it anyway.

We considered the looser alternative — `extra="ignore"` — and rejected it. The reasoning, recorded here so future contributors don't re-litigate:

- **Wire schemas are a credential surface, not just a transport.** The output envelope is what evidence manifests and credentials are computed from. A field the producer thinks is meaningful but the consumer silently ignores is the classic shape of evidence drift. `extra="forbid"` turns "the consumer is one version behind" from a silent mis-credential into a loud rejection at deserialization time.
- **The lockstep cost is bounded and visible.** Six `pyproject.toml` floor-pins plus a handful of TS type files — all enumerated in the section above, all enforced by the `test_wire_format_sync.py` parametrized tests. A grep-and-bump operation, not an open-ended migration. The cost is real but pays for itself the first time it catches a drift like Slice 3d round 2's `RunStatus` mismatch.
- **The alternative failure mode is worse.** With `extra="ignore"`, a backend author adding `provenance_hash` to the output envelope and an LMS one version behind would silently strip it on read. The credential issued from that run would lack the field. The bug surfaces months later in audit — if at all. Strict mode surfaces it on the first run.

**The escape hatch.** If the lockstep tax becomes painful in practice, the relaxation is mechanical: change `extra="forbid"` to `extra="ignore"` on the relevant envelope class (or all of them) and bump to `v2`. Old consumers keep working; new producers can add fields without coordinating updates. The decision is reversible — we'd lose the drift-catching property in exchange for looser coupling. We didn't bake the strictness into the wire format itself; it's a per-class Pydantic setting.

**Three triggers that would prompt revisiting:**

1. **A third external InteractiveTask author appears** (i.e. someone outside the PureLMS core team writing a backend). At that point the shared schema + lockstep-floor-pin workflow becomes a contributor-onboarding tax worth paying for — likely by extracting the TS types into an npm-published `@purelms/shared-types` package so frontend bundles can `npm install` instead of vendoring.
2. **Backend authors complain** that the lockstep update is blocking them — e.g. "I want to add `simulation_seed` to my outputs but I can't ship until the LMS bumps its `purelms-shared` floor." That's the signal the strictness cost has crossed the line from disciplined to obstructive.
3. **A second implementation language wants to write backends** (Go, Rust, Node). At that point the right answer probably isn't "port Pydantic" but "generate JSON Schema from these classes and let each language consume it." The strictness invariant survives the language change; the implementation does not.

None of those triggers are tripped today. If they trip in the future, this section is the breadcrumb back to the decision.

## Install

```bash
uv add purelms-shared
# or
pip install purelms-shared
```

Quick usage:

```python
from purelms_shared import (
    SimulationInputEnvelope,
    SimulationOutputEnvelope,
    OutputStatus,
    Message,
    MessageLevel,
)

# Backends construct the output envelope at completion:
out = SimulationOutputEnvelope(
    run_id=run_id,
    status=OutputStatus.SUCCESS,
    outputs={"annual_eui_kbtu_ft2": 47.3},
    messages=[
        Message(level=MessageLevel.INFO, code="EPLUS.OK", text="Annual run complete"),
    ],
    runtime_seconds=23.4,
)
```

## Architecture context

See the PureLMS [simulation backend contract](https://github.com/danielmcquillen/purelms-project/blob/main/docs/architecture/simulation-backend-contract.md) and [simulation runtime protocol](https://github.com/danielmcquillen/purelms-project/blob/main/docs/architecture/simulation-runtime-protocol.md) for the full surrounding design.

## Legacy

`purelms_shared.energyplus` carries an older EnergyPlus-specific result schema used by the (soon-to-be-retired) Modal runtime in `purelms-modal`. It will be removed when that runtime is phased out per [ADR-0002](https://github.com/danielmcquillen/purelms-project/blob/main/docs/adr/0002-pluggable-simulation-execution.md). New code should use the generic envelope schemas above.

## License

MIT — see [LICENSE](LICENSE).
