Metadata-Version: 2.4
Name: snapgate
Version: 0.1.0
Summary: Correct-by-construction generation: one projection bottleneck, content-addressed recipes, and an exhaustive sweep that proves the invariant holds everywhere.
Project-URL: Homepage, https://github.com/guitargnarr/snapgate
Project-URL: Source, https://github.com/guitargnarr/snapgate
Project-URL: Issues, https://github.com/guitargnarr/snapgate/issues
Author: Matthew Scott
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: constraints,correct-by-construction,deterministic,generation,property-testing,provenance,reproducibility
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: Programming Language :: Python :: 3.14
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: dev
Requires-Dist: hypothesis>=6.100; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Description-Content-Type: text/markdown

# snapgate

**Correct-by-construction generation.** Write sloppy generators; get provably-correct
output — one projection function makes every artifact valid by construction, one recipe
hash makes it reproducible forever, one exhaustive sweep proves the invariant holds across
the whole parameter space.

Zero dependencies. Python ≥ 3.10. Apache-2.0.

```
pip install snapgate
```

## The pattern

Most constrained-generation code tries to make every generator perfect, then samples the
output space with tests and hopes. snapgate inverts both moves:

1. **Project, don't perfect.** Generators are allowed to be approximate. Every artifact
   passes through one total projection — `project(x, constraint)` — at a single emission
   chokepoint. The domain supplies `contains(x)` and `nearest(x)`; the framework
   guarantees totality, idempotence, and that an already-valid artifact is never
   disturbed. Holding a `Projected[T]` **is** the proof the value crossed the chokepoint;
   nothing else can construct one.

2. **Hash the recipe, not the artifact.** An artifact's identity is the content hash of
   the recipe that produces it. The `Ledger` stores recipes — never artifacts — and
   `recall(id)` re-generates the exact artifact and verifies its content hash. A good
   result can never be lost, because the description of how it was made *is* the result.

3. **Sweep, don't sample.** `sweep(axes, generate, gates)` enumerates the **full**
   Cartesian product of your parameter space and classifies every case per gate:
   `PASS` / `LEAK` (invariant violated) / `SKIP` (gate not applicable) / `ERROR`.
   Fail-closed exit code, JSON report, per-axis pass-rate breakdown. Not a sample —
   the whole surface.

This pattern was extracted from a private production generation system where it has held a
hard domain invariant over an exhaustive combinatorial sweep of several hundred thousand
cases, in CI, at zero leaks. snapgate is the pattern with the domain removed.

## Sixty seconds

```python
from snapgate import Gate, project, sweep

# 1. Your domain constraint: membership + your own notion of "nearest valid".
class MultipleOf:
    def __init__(self, k): self.k = k
    def contains(self, x): return x % self.k == 0
    def nearest(self, x):  return round(x / self.k) * self.k

# 2. A sloppy generator, made correct at the chokepoint.
def generate(case):
    raw = case["a"] * 7 + case["b"]           # no attempt to stay valid
    return project(raw, MultipleOf(12)).value  # valid by construction

# 3. Prove it over the WHOLE space, not a sample.
report = sweep(
    axes={"a": range(100), "b": range(100)},   # all 10,000 cases
    generate=generate,
    gates=[Gate("multiple-of-12", lambda case, x: x % 12 == 0)],
)
assert not report.failed
print(report.summary())
```

And reproducibility:

```python
from snapgate import Ledger, Recipe, rng_for

def generate(recipe):
    rng = rng_for(recipe)                      # ONLY source of randomness
    return [rng.randint(0, 127) for _ in range(recipe.config["n"])]

ledger = Ledger("ledger.jsonl", generate=generate)
entry = ledger.record(Recipe("root", {"n": 8, "mood": "bright"}))
# ...months later, artifact long deleted:
artifact = ledger.recall(entry["id"])          # exact reconstruction, hash-verified
```

## The laws

All normative, all executable (`tests/test_laws.py`, property-based via Hypothesis):

| # | Law | Statement |
|---|-----|-----------|
| 1 | Totality | Projection against a satisfiable constraint always yields a valid value |
| 2 | Identity | An already-valid candidate is returned unchanged |
| 3 | Idempotence | Projecting twice equals projecting once |
| 4 | Determinism | Equal recipes ⇒ equal ids ⇒ equal artifact content hashes |
| 5 | Recall integrity | A ledger recall reproduces the exact artifact or raises |
| 6 | Sensitivity | A constraint-violating artifact reaching its gate is LEAK, never PASS |

Law 6 matters most: a proof harness you never saw catch anything proves nothing. Both
shipped examples include a deliberately-broken generator variant that the sweep **must**
catch, run in CI (`tests/test_sensitivity.py`).

## Examples (two domains, same three primitives)

- [`examples/palette`](examples/palette) — **WCAG-contrast-safe color palettes.**
  Constraint: every text/background pair meets its WCAG 2.x contrast ratio (4.5:1 normal,
  3:1 large), computed, never eyeballed. `nearest` walks lightness to the closest
  compliant color. The sweep enumerates hue × lightness × role combinations and proves 0
  violations.

- [`examples/budget`](examples/budget) — **conservation-constrained budget allocation.**
  Constraint: channel allocations sum exactly to the total and respect per-channel
  min/max. `nearest` is a bounded redistribution. The sweep enumerates budget levels ×
  channel mixes × bound profiles and proves conservation everywhere.

Run either:

```
snapgate sweep examples/palette/spec.py --json palette-report.json
snapgate sweep examples/budget/spec.py
```

Exit code is non-zero on any LEAK or ERROR — a sweep drops into CI as-is.

## CLI

```
snapgate sweep <module-or-file> [--json PATH] [--progress N] [--breakdown GATE:AXIS]
```

A spec module declares `AXES` (mapping of axis name → values), `GATES` (list of
`snapgate.Gate`), and `generate(case) -> artifact`.

## When to reach for this

- Generated output must satisfy a hard validity rule (schema, range, physical
  constraint, style rule, legal window) with **zero** tolerance for leaks.
- You need generated artifacts to be exactly reproducible months later without storing
  them.
- Your parameter space is finite and enumerable, and "we tested some of it" isn't good
  enough.

## What this is not

- Not a solver: `nearest` comes from your domain knowledge, snapgate just makes its
  application total, witnessed, and proven.
- Not property-based testing: Hypothesis samples cleverly; a sweep enumerates
  exhaustively. Use both (snapgate's own laws are Hypothesis-tested).

## Spec

The full normative specification, RFC-2119 style: [SPEC.md](SPEC.md).
