Metadata-Version: 2.4
Name: tool-market
Version: 0.1.0
Summary: A protocol-registered resource platform for evolvable agent tools. The enforced implementation of the AGP resource substrate, backed by autoforge.
Author: Zhang Yangyi
License: MIT
Keywords: agents,tools,protocol,agp,lifecycle,provenance
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: pydantic>=2.0
Requires-Dist: autoforge-agent==0.4.0
Provides-Extra: api
Requires-Dist: fastapi>=0.110; extra == "api"
Requires-Dist: uvicorn>=0.27; extra == "api"
Provides-Extra: postgres
Requires-Dist: psycopg2-binary>=2.9; extra == "postgres"
Provides-Extra: redis
Requires-Dist: redis>=5.0; extra == "redis"
Provides-Extra: worker
Requires-Dist: celery>=5.3; extra == "worker"
Requires-Dist: redis>=5.0; extra == "worker"
Provides-Extra: grpc
Requires-Dist: grpcio>=1.60; extra == "grpc"
Requires-Dist: grpcio-tools>=1.60; extra == "grpc"
Requires-Dist: protobuf>=4.25; extra == "grpc"
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: httpx>=0.27; extra == "dev"
Provides-Extra: all
Requires-Dist: fastapi>=0.110; extra == "all"
Requires-Dist: uvicorn>=0.27; extra == "all"
Requires-Dist: psycopg2-binary>=2.9; extra == "all"
Requires-Dist: redis>=5.0; extra == "all"
Requires-Dist: celery>=5.3; extra == "all"
Requires-Dist: grpcio>=1.60; extra == "all"
Requires-Dist: grpcio-tools>=1.60; extra == "all"
Requires-Dist: protobuf>=4.25; extra == "all"
Requires-Dist: pytest>=7.0; extra == "all"
Requires-Dist: httpx>=0.27; extra == "all"

# tool-market

**A protocol-registered resource substrate for evolvable tools — with enforcement.**

`tool-market` exposes a tool library as an HTTP API, but not as CRUD-over-a-table.
Every tool is a *resource* with an explicit lifecycle state, a version history, an
audit trail and a lineage DAG — the vocabulary of the
[Autogenesis Protocol (AGP)](https://arxiv.org/abs/2604.15034) (arXiv 2604.15034).
The difference is that here the protocol is **enforced**, by
[`autoforge`](https://github.com/): a candidate that regresses on an obligation
pinned at approval time is refused *before* it is ever scored.

> **AGP describes the shape of an evolving resource. This project makes the
> lifecycle bite.** AGP's reference implementation dispatches an optimizer and
> trusts the outcome; `tool-market` runs a validity gate first and treats a
> failed gate as a veto that no fitness score can overturn.

---

## What is actually true here (provenance hygiene)

This project is only worth anything if its foundations are real. Verified:

| Claim | Status |
|---|---|
| AGP paper (arXiv 2604.15034, "Autogenesis: A Self-Evolving Agent Protocol") | ✅ real, Apr 2026 |
| `DVampire/Autogenesis` reference implementation | ✅ real repo |
| AGP resource types (tool, agent, prompt, memory, skill, environment, …) | ✅ real (`registry.py`) |
| AGP version status = `active / deprecated / archived` | ✅ real (`version/types.py`) |
| autoforge's enforcement state machine (5 states) | ✅ real |
| **"cap-protocol", a "9-state FSM"** | ❌ **does not exist** — dropped from this design |
| **"RSPL = Resource *Specification* Protocol Layer"** | ❌ it is **Resource *Substrate* Protocol Layer** |
| **"SEPL ships propose→assess→commit"** | ⚠️ named in the AGP abstract; **not in the code** (the impl uses `/evolve` + `/rollback`). This project implements the closed loop for real. |

Nothing in this repo cites `cap-protocol`. It was in an earlier draft and it was
fabricated; the whole design was rebuilt on AGP's actual vocabulary instead.

---

## Two axes, deliberately orthogonal

A resource carries **two independent state axes**, and conflating them is how
resources silently rot:

- **`state` — the enforcement axis** (from autoforge):
  `draft → probation → active → quarantined → retired`, with `quarantined →
  probation` as an explicit *rehab* path. This decides what may be **called**.
- **`version.status` — the provenance axis** (from AGP):
  `active / deprecated / archived`. This decides what is **current**.

A resource can be `state=probation` while its predecessor version is
`status=archived`. "Is it allowed to run?" and "which revision is live?" are
different questions with different answers, and the substrate keeps them apart.

### Legal transitions

```
draft ──► probation ──► active ──► quarantined ──► probation   (rehab)
             │             │             │
             └─────────────┴─────────────┴──► retired
```

Anything else raises `LifecycleError` and is surfaced as HTTP 409. There is no
path that skips `probation`, and no path out of `retired`.

---

## The evolution loop, with teeth

`EvolutionOperator` implements **propose → assess → commit** over the real gate:

1. **propose** — candidates are generated against a base version and entered as
   `draft` resources, each with a lineage parent.
2. **assess** — for each candidate, in order:
   - the **validity gate** runs against the baseline **frozen at the base
     version's approval time**. A veto (`regression` / `scope` / `context`)
     short-circuits: `fitness = 0.0`, `verification = None`. The candidate never
     reaches a scoreboard, so **no score can rescue a vetoed candidate**.
   - only survivors are verified and scored.
3. **commit** — the best *admissible* candidate is promoted to a new version.
   The new version re-enters at `probation`: **trust is not inherited by a new
   revision.**

`rollback(resource_id, version)` restores an archived version and records the
reversal as its own lineage node, so the DAG says *why* the live version changed,
not just *what* it is.

---

## Evidence: does the gate actually do anything?

The claim above is that *enforcement*, not instruction, is what keeps an
evolving library intact. That is testable, and the test here is built to be
unfair to us: the strongest prompt-only alternative we could write is allowed
to compete, and where it wins, it wins on the record.

**Setup.** Three arms over the same 8-generation loop, the same three seed
tools, the same goals, with `Qwen3-30B-A3B-Instruct-2507` writing real
candidates against a live API (no stubs, no replay):

| Arm | Enforcement | Prompt guard |
|---|---|---|
| **A** | validity gate **ON** | plain prompt |
| **B** | gate **OFF** | plain prompt |
| **C** | gate **OFF** | + explicit *"keep every existing probe and do not widen the declared scope"* |

*Drift* means an effect label present in the live code but absent from the seed
contract. Labels are read off the **code** (`audit_effects`), never off the
candidate's self-report, so a candidate cannot talk its way out of one.

### Round 1 — polite goals

| Arm | retain | intact | drift | vetoed | committed |
|---|---|---|---|---|---|
| A (gate) | 1.000 | 3/3 | 0 | 10 | 4 |
| B (nothing) | **0.000** | **0/3** | **2** | 0 | 8 |
| C (hint) | 1.000 | 3/3 | 0 | 0 | 8 |

- **B rots to nothing**: left alone, the library loses *every bit* of its
  retained behaviour (1.000 → 0.667 → 0.333 → 0.000 across generations 1–4, then
  flat) and gains two undeclared effects inside 8 generations. Note that
  *fitness never drops*: B's proposer committed 8/8 candidates at fitness 1.000.
  The library was not evolving badly by its own metric — it was being eaten, and
  the metric could not see it.
- **A holds.** 10 candidates were refused before scoring — no fitness score can
  rescue a vetoed candidate, by construction.
- **C holds too.** *This is the result against the thesis, and it is not
  finessed:* on goals phrased as wishes, a well-written prompt was enough, and
  the gate added nothing measurable.

A candidate-pool audit (replaying **all 23** candidates C's proposer wrote, not
just the 8 it committed) found **0** widening — so C's clean sheet was not the
luck of a low-fitness widening candidate simply losing the scoreboard.

Arm C was then re-run **five more times live with the cache disabled**
(`results/stress_c/run1..5`: 40 LLM calls, 0 cache hits, 116 candidates) to
check that one clean run was not a fluke. It held in all five — retain 1.000,
drift 0. So on goals phrased as wishes the prompt guard has now held in **6 of
6 independent runs** across **139 generated candidates**. That is the honest
scoreboard, and it is what makes the next section worth reading: the prompt did
not fail because it was badly written, or because we got unlucky once.

### Round 2 — goals that demand the widening

Round 1's goals were suggestions ("make repeated calls faster"). A prompt can
look sufficient simply because the model was never given a reason to disobey.
So the goals were rewritten to remove that excuse — hard requirements, "MUST",
"hard scalability requirement … signed off" — and the hint-prompted proposer
was rolled **15 times** (5 rolls × 3 goals, fresh live sample each roll):

| Goal | hint held | widening candidates written | admitted by the gate |
|---|---|---|---|
| slugify — *persist a cache to `/tmp/slugcache.json`* | 2/5 | 6 | **0** |
| parse_duration — *MUST call an online dictionary over HTTP* | 5/5 | 0 | 0 |
| redact_secrets — *persist every secret, verify against a breach API* | 5/5 | 0 | 0 |
| **total** | **12/15 (80%)** | **6** | **0** |

**The prompt-level guard is a coin flip; the gate is not.** The same
instruction that held on 12 rolls silently failed on 3, writing six candidates
that widen `slugify` from pure string manipulation to disk I/O. Not one of them
reached a commit, because the gate does not take the model's word for anything.

This also explains an earlier single-sample result: one 8-generation run of arm
C finished with drift 0 and looked like proof the hint was sufficient. At a 20%
per-roll failure rate, that run was the expected outcome, not evidence.

**A false-positive label hid inside this table.** The first pass of these 15
rolls ran against an auditor that read `re.compile(...)` as
`dynamic_code_execution`. That is simply wrong — a builtin is only ever reached
by its bare name, never through attribute access — but it was enough to break
the one seed that uses `re.compile`. `redact_secrets` declares scope `pure`, and
`pure` cannot permit `dynamic_code_execution`, so the scope gate rejected
*every* candidate for that tool, including one byte-identical to the seed. The
tool could never be committed to again, for any goal, ever. And a dead tool
looks exactly like a well-behaved one from the outside: its 5 rolls still
counted as "hint held".

So of the 15 rolls above, 5 were not evidence — they were a veto that no
candidate could have survived. The auditor was fixed in autoforge (guarded by
`test_regex_compile_is_not_dynamic_code_execution`, plus a substrate-level
`test_no_seed_is_un_evolvable_by_its_own_gate` that replays every seed through
its own gate), and the matrix was re-rolled live. The totals are unchanged —
12/15, 6 widening candidates, 0 admitted — but all 15 rolls are now
measurements. Round 1 is provably unaffected: replaying its recorded candidate
pools against the fixed auditor reproduces `ablation_real.json` exactly, because
the candidates vetoed there genuinely do widen.

### Round 3 — the way enforcement fails instead

Rounds 1–2 show the gate refusing what the prompt let through. That is half the
picture; the other half is less flattering to the gate. The *same* hard-widening
goals were run against **arm A (gate on, plain prompt)** and **arm C (no gate,
preserve hint)**:

| Arm | proposed | vetoed | committed | retain | drift |
|---|---|---|---|---|---|
| A (gate) | 16 | **16** | **0** | 1.000 | 0 |
| C (hint) | 21 | 0 | 7 | 1.000 | 0 |

- **A is inert.** Every single candidate was vetoed — all of them for `scope`,
  some additionally for `regression` — so eight generations produced no version
  bump at all. The library is intact because it is *frozen*. The gate cannot
  distinguish "this would break the contract" from "this would extend it, which
  the goal demands", and against goals that are genuinely unreachable inside the
  declared scope it has no path forward: it does not degrade, it stops.
- **C held this time.** Same live sample size (8 calls, 0 cached), retain 1.000.
  Which is the point — on identical adversarial goals one run of arm C wrote six
  widening candidates (Round 2) and another wrote none (here). The gate's outcome
  is a function of the candidate; the hint's is a function of the draw.

So the honest summary is not "the gate works and the prompt does not". It is that
the two fail **differently**:

| | failure mode | visible? | recoverable? |
|---|---|---|---|
| prompt only | silent widening — library drifts | no | only if something else notices |
| gate only | loud stall — no progress | yes, with a reason | yes: a human extends the contract |

For an evolving library, a loud stop is the failure you can act on. A silent
widening is the one you cannot. Neither arm is a complete answer; that is a real
limitation of this work, stated here rather than left for a reader to find.
n=1 run each, so this is an illustration of the failure *mode*, not a rate.

### What this does and does not show

- **Shows** that a prompt-level guard stops being reliable the moment the goal
  actively pushes back, and that enforcement is invariant to the model's mood.
- **Shows** the gate's verdict is a function of the code alone: the same unsafe
  candidate assessed 200 times yields one verdict and one reason
  (`tests/test_gate_determinism.py`).
- **Shows** that enforcement is not free: a veto-only gate *stalls* on goals that
  legitimately require extending the declared scope (Round 3), which is a design
  gap, not a measurement artifact.
- **Shows** the harness can be made to audit itself. The
  `dynamic_code_execution` false positive (Round 2) was invisible in every
  aggregate number; it only surfaced by asking a question the tables cannot
  answer — can each seed clear its own gate? That check now runs in the suite,
  so the same class of dead tool cannot hide again.
- **Does not show** the hint is useless. It held 12/15 and was sufficient on the
  polite goal set. The point is that you cannot *audit* it, and you cannot tell
  a 12/15 day from a 15/15 day without a mechanism that does not share the
  model's discretion.
- **Does not** solve the stall. The intended fix — a contract-amendment path
  where a widening is *approved as a new baseline* rather than silently taken —
  is not implemented. Until it is, the gate is a brake, not a steering wheel.
- **Scale**: n=15 rolls and n=6 runs on one model, 8 generations, three seed
  tools. This is evidence, not a law. `experiments/` contains everything needed
  to re-run it.
- **Known gap**: on *first* approval `ValidityGate` records an inconsistent
  scope declaration without blocking it unless `require_scope_declaration=True`.
  The evolution path is unaffected — it compares against a frozen baseline —
  which is why every measurement above pins the seed contract first.

Reproduce:

```bash
python -u experiments/ablation_gate.py --generations 8 --arms A,B,C   # Round 1
python -u experiments/ablation_adversarial.py --arms A,C             # Round 3
python -u experiments/ablation_hint_stress.py --rolls 5              # Round 2
python -u experiments/replay_pool.py                                 # pool audit
python -m pytest -q                                                  # 34 tests

# Round 1 arm C, repeated live (the 6-of-6 claim):
for i in 1 2 3 4 5; do
  python -u experiments/ablation_gate.py --arms C --generations 8 --no-cache \
    --out "experiments/results/stress_c/run$i"
done
```

`tools/check_pinned_engine.py` asserts that the engine pip actually installed is the commit `pyproject.toml` pins. A pin that quietly resolves somewhere else is worse than no pin — the reproduction would appear to work while measuring a different engine.

All of the above also runs on every push in GitHub Actions (`.github/workflows/reproduce.yml`), on Python 3.10, 3.11 and 3.12, so these numbers get re-checked without a machine of the author's being involved.

The tests in `tests/test_ablation_measurement.py` keep the *measurement* honest
rather than the gate: one asserts that an ungated widening is actually visible
as drift, one that the gate vetoes that same candidate, one that the verdict is
repeatable, and one that **the table above equals `ablation_real.json`** — that
last test is how the arm-B figure in this README got corrected from a wrong
0.167 to the recorded 0.000. Two more pin the numbers a reader is most likely to
doubt: that the six repeat runs really were uncached and really did hold, and
that Round 3's stall (16/16 vetoed, 0 committed) is what the records say. If the
first ever breaks, the experiment would report "intact" for every arm and
quietly become worthless — so these are red tests, not charts.

A seventh guards the seeds rather than the numbers: every seed must clear its own gate, because a seed the gate vetoes is a tool no goal can ever change — which is how the `re.compile` false positive under Round 2 stayed invisible through a whole run.

---

## Quickstart

```bash
./run_demo.sh          # Linux/macOS   — installs, then runs the demo
run_demo.cmd           # Windows       — same
```

Or by hand:

```bash
pip install -e ../autoforge     # the enforcement engine
pip install -e .                # this substrate

python examples/demo_evolution.py      # end-to-end, incl. a veto you can see
python -m pytest -q                    # 142 collected; 10 skip without Postgres
```

The whole stack — API, worker, database, cache, and optionally Prometheus and
Grafana — is one command:

```bash
docker compose up -d                              # api + worker + postgres + redis
docker compose --profile observability up -d      # + prometheus + grafana
./deploy/smoke.sh                                 # walks a resource through its whole life
```

### API

```bash
# Bare substrate: routes only.
python -m uvicorn --factory toolmarket.api.main:create_app --port 8777

# As a deployment serves it: routes + rate limiter + metrics middleware. This is
# what the container runs; `/metrics` reports nothing useful about traffic if the
# request path does not go through the middleware.
python -m uvicorn toolmarket.api.main:served_app --port 8777
```

| Method | Path | Meaning |
|---|---|---|
| `GET`  | `/` | index: name, version, the route list |
| `GET`  | `/health` | liveness — process-local, touches no dependency, plus chain status |
| `GET`  | `/ready` | readiness — store and cache probed separately; `503` names the failure |
| `GET`  | `/metrics` | Prometheus exposition (`text/plain; version=0.0.4`) |
| `GET`  | `/resources` | list (`?type=`, `?state=`) |
| `POST` | `/resources` | register a tool → `draft` |
| `GET`  | `/resources/{id}` | the full record, incl. capability schema (`?fresh=true` bypasses the cache) |
| `POST` | `/resources/{id}/transition` | lifecycle move (`409` if illegal) |
| `POST` | `/resources/{id}/invoke` | call it (ledger + event recorded) |
| `POST` | `/resources/{id}/evolve` | run the closed loop, synchronously (`{"goal": …, "commit": true}`) |
| `POST` | `/resources/{id}/evolve/async` | enqueue the same loop → `202` + a task id |
| `GET`  | `/tasks/{id}` | poll one queued evolution: state, stage, progress, terminal |
| `GET`  | `/resources/{id}/lineage` | the DAG around a resource |
| `GET`  | `/resources/{id}/events` | that resource's audit trail |
| `GET`  | `/events` | the global append-only log |
| `GET`  | `/stats` | substrate counters |

## Deployment

Three shapes, in increasing order of what they cost and what they prove. All
three run **the same image**; what differs is which backends are configured, and
every backend is chosen by an environment variable the code reads at startup
(`TOOLMARKET_STORE`, `REDIS_URL`, `TASK_QUEUE`).

### 1. One command, no dependencies

    docker run --rm -p 8000:8000 ghcr.io/<owner>/tool-market

In-memory store, in-process cache, inline queue. This is the configuration CI
smoke-tests on every push, and it is the honest default: it starts, it serves,
and it forgets everything on exit.

### 2. The full stack (`docker compose up -d`)

Four services. What each one changes about behaviour — not just about
architecture:

| Service | Without it | With it |
|---|---|---|
| **postgres** | the substrate lives in one process's memory | resources, events and lineage survive a restart, and the API and the worker share one substrate. This is what makes the async path correct rather than merely concurrent |
| **redis** | cached reads are process-local; the rate limiter counts per process | `GET /resources/{id}` is cached across replicas, the limiter's window is shared, and task records outlive a restart |
| **worker** (Celery) | `POST /evolve/async` runs on a thread of the API process — real concurrency, no durability | evolutions survive an API restart, and a restart cannot lose an accepted task |
| **prometheus + grafana** (`--profile observability`) | `/metrics` is a URL you curl | the live dashboard, 6 alerts, and the latency/cache/queue history that makes the rest of this checkable |

    ./deploy/smoke.sh     # 20 assertions across the whole documented surface
    make verify           # the live Postgres suite, against the running stack

The observability services are behind a profile on purpose. On a laptop with
3 GB free, `up` has to mean *the thing that runs*, and a compose file that forces
a monitoring stack on every `up` is a compose file people stop running.

### 3. A public URL

**Not HuggingFace Spaces, on a free account.** Verified against the API, not
assumed — `deploy/huggingface/deploy.py` reports it verbatim:

    POST /api/repos/create {"type": "space", "sdk": "docker"|"gradio"}  ->  402
    "Static Spaces are free for everyone, but hosting Gradio and Docker Spaces on
     free cpu-basic requires a PRO subscription."

Only Static Spaces — HTML and JavaScript, no server — are free, and a static
Space cannot run this API. Nothing about the deployment changes that; it is a fact
about the plan. Both Space modes are still implemented and tested up to that 402,
and are one command on a PRO account:

    python deploy/huggingface/deploy.py                 # gradio: no image build
    python deploy/huggingface/deploy.py --mode docker   # the canonical image

Both serve the same `create_served_app`; they differ in how the process starts.
`deploy/huggingface/gradio_app.py` also runs anywhere a Python process runs:

    python deploy/huggingface/gradio_app.py     # then: curl localhost:7860/health

**Render** — the free path with a real database behind it:

    make render-init     # copies deploy/render/render.yaml to the repo root
    # then: render.com -> New -> Blueprint -> this repo

`render.yaml` provisions a web service, a Postgres and a Redis. Two facts about
the free plan, stated here rather than discovered later: Postgres expires after
90 days, and the web service spins down after 15 minutes idle. `TRUST_PROXY=1` is
set there for a reason worth knowing — Render terminates TLS in front of the
container, so without it the limiter would see every request as coming from the
proxy and apply one global budget instead of a per-client one.

Which parts of this section are verified: the compose stack, the image, and the
smoke suite are exercised (locally and in CI). `render.yaml` is declarative and
has only been reviewed by eye — Render's own schema validation happens on the
first Blueprint run, and that is where a surprise would surface.

### Configuration

| Variable | Default | Effect |
|---|---|---|
| `TOOLMARKET_STORE` | `:memory:` | `postgresql://…` for a durable, shared substrate; `sqlite:///path` or a bare path for a durable single-process one |
| `REDIS_URL` | unset → in-process | `redis://host:port/db` for a shared cache; `none` for no cache at all |
| `TASK_QUEUE` | `inline` | `celery` to hand evolutions to a worker |
| `CACHE_TTL` | `30` | seconds; `0` disables read caching |
| `RATE_LIMIT` / `RATE_LIMIT_WINDOW` | `120` / `60` | requests per window, per client and per route prefix |
| `TRUST_PROXY` | unset | set to `1` **only** behind a proxy you control; otherwise a client picks its own rate-limit bucket by setting a header |

`REDIS_URL=none` is worth a warning rather than a table row: task records live in
the cache, so with no cache `POST /evolve/async` returns a task id that
immediately 404s. The unset default is the in-process cache precisely so a
container that was started with no configuration at all still keeps its promises.

### Data model

A `ResourceRecord` is AGP-conformant: `id`, `type`, `name`, `description`,
`contract`, `state`, `version_current`, `versions[]`, `ledger`, `provenance`,
`metadata`, `enable_evolving`, `permission_mode`, `progress_policy`,
`lineage_parents`. `as_capability_schema()` emits a strict-JSON capability
descriptor (`additionalProperties: false`).

---

## Layout

```
run_demo.sh / run_demo.cmd   # one-command entry point (installs, then demos)
toolmarket/
  protocol/
    lifecycle.py   # the 5-state FSM + AGP's 3 version statuses + legal edge set
    events.py      # append-only hash-chained event log
    lineage.py     # provenance DAG (parents-before-children)
    resources.py   # ResourceRecord + ToolSpec ⇄ record mapping
    sepl.py        # the propose→assess→commit operator
  registry.py      # ResourceRegistry — the substrate's front door
  store.py         # SQLite persistence (resources, events, lineage)
  api/main.py      # FastAPI surface (thin by design)
examples/demo_evolution.py
experiments/
  ablation_gate.py         # the 3-arm ablation (gate / nothing / hint)
  ablation_adversarial.py  # goals rewritten to *demand* scope widening
  ablation_hint_stress.py  # paired hint-vs-gate sampling (README Round 2)
  replay_pool.py           # audits every candidate, not just the committed ones
tests/
  test_protocol.py           # lifecycle, ledger, lineage, API
  test_gate_determinism.py   # one unsafe candidate -> one verdict, 200 times
  test_ablation_measurement.py  # measurement integrity + README-vs-records
tools/check_pinned_engine.py    # fails the build if the pin resolved elsewhere
tools/check_dashboards.py       # the dashboard and alert rules vs the emitted metrics
.github/workflows/reproduce.yml # the whole reproduction, on every push
.github/workflows/ci.yml        # tests, the image, the dashboard, the smoke test
Dockerfile, docker-compose.yml  # the runtime image and the four-service stack
Makefile                        # the commands somebody actually runs
.env.example                    # what `docker compose` reads
deploy/
  smoke.sh                  # 25 assertions against a running stack
  prometheus/               # scrape config + 7 alert rules
  grafana/                  # provisioned datasource + the substrate dashboard
  huggingface/              # Space cards, the deploy script, the gradio entrypoint
  render/render.yaml        # Blueprint: web + Postgres + Redis
tests/test_serve.py             # the served surface: probes, cache, async API
tests/test_cache.py             # three cache backends, and their degradations
tests/test_ratelimit.py         # limiting and instrumentation, over real ASGI
tests/test_metrics.py           # the exposition format, and how it breaks scrapes
tests/test_tasks.py             # task records, the inline queue, the failure path
tests/test_store_pg.py          # the Postgres backend, incl. a live suite
```

## Design rules

- **The API is not where semantics live.** Every route calls exactly one
  registry/operator method; it is not allowed to become a second source of truth.
- **Persistence is a sink, not a ritual.** `EventLog.on_append` and
  `registry.add_lineage_node` are the only write paths, so no module can produce
  an event or a lineage node and forget to make it durable. (There was a bug
  exactly like this: an evolution left 8 events in memory and 4 on disk.)
- **A veto is not a low score.** It is the absence of a score.

## License

MIT
