Roadmap to 0.1
0.1 is a walking skeleton: one app boots alone, two apps call each other through a contract over the in-process and the HTTP transport, and one test suite passes over both. Every step is a pull request that leaves main working. Each step names the specification documents it implements, so the spec is written alongside the code (ADR 0015).
| Step | Delivers | Spec |
|---|---|---|
| 1 | Package layout, pyproject.toml, lint, type checks, test harness. CI matrix per ADR 0012: Django 5.2 on Python 3.12 and the newest supported pair, on x86-64 and arm64, with deprecation warnings as errors |
— |
| 2 | Contracts: @contract, @action, portable types, typed errors, @implements, validation at import time |
01 |
| 3 | In-process transport with serialization round-trip; connector with single routing |
02 |
| 4 | App declaration and host: NeolithAppConfig, settings built from declarations, neolith run <app> boots one app on SQLite |
05 |
| 5 | HTTP transport: provider endpoints via django-ninja, OpenAPI at the well-known path, connector over httpx, shared-secret tokens for the calling app, and the principal header | 03 |
| 6 | Conformance suite running every contract test over both transports; local-first routing and a failover test with two providers |
06, 02 |
| 7 | Boundary check: neolith check wrapping import-linter, report-only then enforce per app |
— |
| 8 | Two small example apps under examples/ showing attached, detached and both-at-once; the 0.1 proof |
all |
Libraries named in the steps (django-ninja, httpx, import-linter) are working assumptions. Each needs an Accepted decision record before the step that adds it lands (K5). For django-ninja that record is ADR 0011, still Proposed.
Gate for releasing 0.1: verified on Python 3.15 with Django 6.1 (ADR 0012).
Deferred past 0.1: events and the outbox (spec 04), neolith detach data moves, breaking-change detection, routing policies beyond single and local-first, gRPC transport.
The public surface of 0.1
Section titled “The public surface of 0.1”Everything an app author touches. Designed before the internals; the internals serve this.
from typing import Protocol
from neolith import NeolithAppConfig, contract, action, implements, connect, ContractError
@contract("inventory.v1")class Inventory(Protocol): @action(idempotent=True) def reserve(self, request: ReserveRequest) -> ReserveReply: ...
@action def release(self, request: ReleaseRequest) -> None: ...
@implements(Inventory)class InventoryImpl: def reserve(self, request: ReserveRequest, ctx: CallContext) -> ReserveReply: ... def release(self, request: ReleaseRequest, ctx: CallContext) -> None: ...
class InventoryConfig(NeolithAppConfig): name = "inventory" provides = ["inventory.v1"] requires = [] resources = {"database": "inventory"}
# in a callerinventory = connect("inventory.v1")try: reply = inventory.reserve(ReserveRequest(sku="A1", qty=2))except ContractError as e: e.kind # "unavailable", "conflict", ...Rules that shape it:
- Request and reply types are Pydantic models restricted to the portable type system. Anything else fails at import time with a message naming the field.
- A contract is declared as a
Protocolclass; it is never instantiated.connect()returns a connector whose methods mirror the actions. - Why
Protocoland@implements(Inventory)rather than a plain class and@Inventory.implement: mypy accepts the...bodies of the actions only inside a Protocol, and it does not apply a class decorator’s return type (python/mypy#3135), so animplementattribute added by@contractwould not type-check. - An implementation class is checked against its contract when the module is imported: every action present, signatures compatible. Missing or extra methods are errors, not warnings.
- An implementation receives the call context as an optional second parameter annotated
CallContext, as the sketch shows. Outgoing calls find it on their own (ADR 0017). - Configuration of providers, transports and routing lives in Django settings under one key,
NEOLITH, and is validated by the host at startup.
Shell:
neolith run <app> [<app> ...] boot the named apps in one hostneolith check [--report] boundary checkneolith openapi <contract> print the projection