Metadata-Version: 2.5
Name: pin-derive
Version: 0.10.1
Summary: Python bindings for the pin-derive bidirectional constraint engine
Project-URL: Repository, https://github.com/nightwork-dev/pin-derive
Project-URL: Issues, https://github.com/nightwork-dev/pin-derive/issues
Author: Nightwork
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: constraints,procedural-generation,solver,wasm,wasmtime
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: wasmtime>=27
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: twine>=5; extra == 'dev'
Description-Content-Type: text/markdown

# pin-derive

Python bindings for **pin-derive**, a small bidirectional interval constraint
engine. Declare cells and multidirectional relations, pin whichever facts you
know, and the remaining cells narrow to derived points or still-open ranges.
The same network solves in every direction: pin price and margin to derive
profit, or pin profit and price to derive margin from the same relation.

Four runtimes share one JSON contract and one conformance corpus: the TypeScript
reference engine (`pin-derive` on npm), the native Rust crate
(`pin-derive-core`), the JavaScript wasm bindings (`pin-derive-wasm`), and this
package. It runs the Rust core as a bundled wasm binary through
[`wasmtime`](https://pypi.org/project/wasmtime/), so it is a pure-Python wheel
that works on every platform wasmtime supports, with no Rust toolchain and no
compilation at install time.

## Install

```bash
uv add pin-derive        # or: pip install pin-derive
```

Requires Python 3.11+. The only dependency is `wasmtime`.

## Quickstart

```python
from pin_derive import PinDerive

engine = PinDerive()  # loads the bundled wasm binary

# A network is a spec: cells with an initial interval, relations, pins.
structure = {
    "cells": [
        {"id": "price",  "init": {"lo": 0, "hi": None}, "label": "price ($)"},
        {"id": "margin", "init": {"lo": 0, "hi": None}, "label": "margin"},
        {"id": "profit", "init": {"lo": 0, "hi": None}, "label": "profit ($)"},
    ],
    "relations": [
        {"type": "product", "z": "profit", "x": "price", "y": "margin"},  # profit = price × margin
    ],
}

# Forward: pin price and margin, profit derives.
forward = engine.solve({
    **structure,
    "pins": [
        {"cell": "price",  "value": {"lo": 50,  "hi": 50}},
        {"cell": "margin", "value": {"lo": 0.2, "hi": 0.2}},
    ],
})
profit = next(c for c in forward["cells"] if c["id"] == "profit")
print(profit["provenance"], profit["value"]["lo"])  # derived 10.0

# Backward: pin profit and price instead, margin derives from the same relation.
backward = engine.solve({
    **structure,
    "pins": [
        {"cell": "profit", "value": {"lo": 10, "hi": 10}},
        {"cell": "price",  "value": {"lo": 50, "hi": 50}},
    ],
})
margin = next(c for c in backward["cells"] if c["id"] == "margin")
print(margin["provenance"], margin["value"]["lo"])  # derived 0.2
```

Pin fewer cells than the relation needs and the under-determined cells come
back with `boundsStatus: "bounded"` and a `{lo, hi}` range to choose from
instead of an error. Pin values that cannot all hold and the one cell that
broke reports `boundsStatus: "conflicting"`, the snapshot's `conflicts` list
names where two facts collided, and every other cell keeps its derived value.

## The spec

| key | what it holds |
|---|---|
| `cells` | `{id, init?: {lo, hi}, label?, meta?}`; `None` on either side means unbounded |
| `relations` | a tagged union: `equal`, `sum`, `product`, `affine`, `quantize`, `lte`, `within`, `min`, `max`, `clamp`, `lookup`, `curve` |
| `pins` | `{cell, value: {lo, hi}, id?, meta?}` held assumptions, replayed in order |
| `commits` | same shape as pins; choices made inside a decision range |
| `options` | `trace: True` to record the narrowing DAG on a solve; the search knobs `repairs` reads |

Every spec, result, and relation shape is a `TypedDict` exported from
`pin_derive`, and the package ships `py.typed`, so mypy and an editor show the
full contract. The relation library is documented in the repository's
`docs/REFERENCE.md`; the spec format is identical across all four runtimes.

## API

Every method on `PinDerive` takes a spec as a `dict` or a JSON string and
returns parsed JSON. Engine-reported errors raise `PinDeriveError` (a
`RuntimeError`) carrying the contractual, cross-runtime message.

| method | what it does |
|---|---|
| `solve(spec)` | solve a spec; truth only: values, decisions, conflicts |
| `explain(spec, options=None)` | why each conflict is there and what would relax it; a clean spec returns the empty envelope |
| `repairs(spec)` | which pin relaxations (drop, widen, shift) clear which conflicts, under `spec["options"]` search knobs |
| `preview(spec, moves, options=None)` | the snapshot as if `moves` were applied; mutates nothing |
| `commit(spec, moves, accept=None, options=None, expect_base=None)` | atomic move batch under an acceptance predicate, with compare-and-swap on the base fingerprint |
| `fill(request)` | proportional interior allocation across the solved network's ranges |
| `lp(program)` | two-phase simplex over a domain-neutral linear program |
| `apply_patches(spec, patches)` / `preview_patches(spec, patches, options=None)` | executable decisions: add, remove, or replace pins and commits, all-or-nothing |
| `solve_fingerprint(spec)` / `snapshot_fingerprint(snapshot)` | SHA-256 identity of an input state / a solved state |
| `canonicalize_solve_state(spec)` | the canonical bytes `solve_fingerprint` digests |
| `decision_delta(spec, next_spec)` | structured diff of two states' decisions |
| `prepare(spec)` | compile a structure once; returns a `PreparedRef` for the batch calls |
| `solve_many` / `explain_many` / `preview_many` / `commit_many` | one result per move set or batch on a prepared handle, in input order |
| `free_prepared(handle)` | drop a prepared handle (`PreparedRef.free()` does the same) |

Dependency traces (`options["trace"]`) come back as the raw `trace` list on the
snapshot. The `traceForCell` and `conflictingFacts` readers over that list
exist in the TypeScript package and the Rust crate, not in the wasm bindings.

## Bundled wasm

`pin_derive/_pin_derive_wasm.wasm` ships inside the wheel and is built from the
repository's Rust core with `wasm-opt -Oz`. Pass an explicit path only when
testing a freshly built development artifact:

```python
engine = PinDerive("../target/wasm32-unknown-unknown/release/pin_derive_wasm.wasm")
```

To rebuild it from a repository checkout:

```bash
node scripts/wasm-build.mjs   # from the repo root; cargo + pinned wasm-opt, needs `pnpm install` once
cp rust/target/wasm32-unknown-unknown/release/pin_derive_wasm.wasm \
  rust/python/src/pin_derive/_pin_derive_wasm.wasm
```

Use the build script rather than `cargo build` directly: the bundled binary is
expected to be the optimized one.

## Stability

Pre-1.0. The JSON spec, snapshot, and error strings are the stable contract:
they are locked by conformance fixtures shared with the other three runtimes,
and a change to them is a minor-version release with notes. The method names
above follow that contract one-to-one. The C ABI of the wasm binary itself is
internal to these bindings.

## License

Apache-2.0. Source, design notes, and the full reference live in the
[pin-derive repository](https://github.com/nightwork-dev/pin-derive).
