Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,15 +29,16 @@ Read these before writing or modifying any code:

1. `contracts/schema-and-versioning.md` — every data contract
2. `contracts/consumer-registry.md` — closed `ConsumerType` enum
3. `methodology/calibration-methodology.md` — confidence pipeline
4. `methodology/labeling-protocol.md` — ground-truth definition
5. `methodology/manipulation-taxonomy.md` — manipulation signatures
6. `architecture/system-design.md` — layer-by-layer architecture (includes Deployment Modes)
7. `architecture/adaptive-polling-spec.md` — polling state machine
8. `architecture/deduplication-and-storms.md` — signal merge algorithm
9. `architecture/storage-and-scaling.md` — storage architecture and migration triggers
10. `operations/distributed-runbook.md` — cutover, rollback, failover procedures
11. `operations/manual-testing.md` — runnable surfaces and local smoke stack
3. `contracts/cross-package-compatibility.md` — cross-package version and schema gates
4. `methodology/calibration-methodology.md` — confidence pipeline
5. `methodology/labeling-protocol.md` — ground-truth definition
6. `methodology/manipulation-taxonomy.md` — manipulation signatures
7. `architecture/system-design.md` — layer-by-layer architecture (includes Deployment Modes)
8. `architecture/adaptive-polling-spec.md` — polling state machine
9. `architecture/deduplication-and-storms.md` — signal merge algorithm
10. `architecture/storage-and-scaling.md` — storage architecture and migration triggers
11. `operations/distributed-runbook.md` — cutover, rollback, failover procedures
12. `operations/manual-testing.md` — runnable surfaces and local smoke stack

## Group Index

Expand Down
98 changes: 98 additions & 0 deletions docs/contracts/cross-package-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
# Cross-Package Compatibility Policy

Augur ships as three independent PyPI packages (`augur-signals`, `augur-labels`, `augur-format`). The packages share Pydantic contracts in `augur_signals.models` and must agree on the schema at install time. This document pins the versioning rules and the runtime gates that enforce them.

## Version alignment rule

All three workspace packages are released **in lock step**. A release cuts `vX.Y.Z` for all three simultaneously; there is no release where `augur-labels` and `augur-format` point at different `augur-signals` versions in their pinned range.

### Dependency specifier

Each dependent package pins `augur-signals` with a compatible-release specifier on the minor version:

```toml
# augur-labels/pyproject.toml, augur-format/pyproject.toml
"augur-signals ~= 0.1.0" # >=0.1.0, <0.2.0
```

Patch upgrades (`0.1.0 → 0.1.1`) are permitted transparently. A minor bump (`0.1.x → 0.2.0`) **must** re-cut `augur-labels` and `augur-format` with an updated specifier before publishing.

### Major vs minor vs patch

Pre-1.0 semantics (0.x phase):

| Bump | When | Downstream action |
| --- | --- | --- |
| `0.1.Z → 0.1.Z+1` | Internal refactor, additive API, bug fix that preserves schema | None. All three packages co-publish a patch. |
| `0.1.Z → 0.2.0` | Breaking change to any exported contract (`MarketSignal`, `SignalContext`, `FeatureVector`, `EventBus`, etc.), even if field-additive | Downstream pin tightens to `~= 0.2.0`; MODELS_SCHEMA_VERSION major may bump. |
| `0.Y.Z → 1.0.0` | API stability commitment | Full semver applies from here on. |

After 1.0, a major bump in `augur-signals` forces a major bump in both dependents (their import-time gates refuse the major-mismatch).

## Runtime compatibility gate

Both `augur-labels` and `augur-format` run a two-part check at package import time. The gate is in each package's `_compat.py` module and is invoked from `__init__.py` *before any public symbol is re-exported*.

### Gate 1 — installed version range

```python
# Parses importlib.metadata.version("augur-signals") and rejects anything
# outside [_SIGNALS_MIN, _SIGNALS_LT).
_SIGNALS_MIN: Final[str] = "0.1.0"
_SIGNALS_LT: Final[str] = "0.2.0"
```

Catches two failure modes:

1. A user ran `pip install --no-deps` or a private index served a broken set.
2. A transitive dependency upgraded `augur-signals` past the compatible range without bumping this package.

### Gate 2 — schema-contract major version

```python
from augur_signals.models import MODELS_SCHEMA_VERSION
# Rejects when the major digit does not match _EXPECTED_MODELS_SCHEMA_MAJOR.
```

`MODELS_SCHEMA_VERSION` is the canonical constant in `augur_signals.models`. Every Pydantic model in that package pins `schema_version: Literal["X.Y.Z"]` to the same value. A major bump in the constant signals a breaking schema change that requires downstream rebuild; the gate refuses to proceed with a mismatched dependent build.

### Failure mode

Both gates raise `IncompatibleAugurSignals`, a subclass of `ImportError`. The error message names the observed version, the expected range, and points at this document. The package fails to import — no partial import, no soft warning, no graceful degradation.

```
augur-labels requires augur-signals >=0.1.0,<0.2.0; found 0.2.3.
Align the package versions (same minor) before running.
See docs/contracts/cross-package-compatibility.md.
```

## Release procedure

1. Change any exported contract in `augur-signals`.
2. If the change is breaking, bump `MODELS_SCHEMA_VERSION` major.
3. Bump all three package versions in lock step:
- `pyproject.toml` (root)
- `src/augur_signals/pyproject.toml`
- `src/augur_labels/pyproject.toml`
- `src/augur_format/pyproject.toml`
4. Update the dependent pins:
- `augur-labels` and `augur-format` change `"augur-signals ~= X.Y.0"`.
- `augur_labels/_compat.py` and `augur_format/_compat.py` update `_SIGNALS_MIN`, `_SIGNALS_LT`, and `_EXPECTED_MODELS_SCHEMA_MAJOR` in the same commit.
5. Update `CHANGELOG.md` under `## [Unreleased]`.
6. Run gates, tag `vX.Y.Z`, build and publish all three wheels in one `uv publish` invocation.

Release automation enforcing the lock-step bump is a follow-up (tracked against v0.2.0).

## Why not collapse into one package

A single `augur` package with optional-dependency subsets would eliminate cross-package version drift at the cost of:

- Losing the physical LLM-isolation boundary (no import of `anthropic` / `ollama` possible anywhere in `augur_signals`). The CI grep guard would become the only defence.
- A larger install closure for deployments that need only one slice (a signal-extraction worker would still resolve the labeling and formatting submodules' metadata).
- Releases coupling bug fixes across domains (a labeling-only patch forces a re-release of the formatter).

The three-package split accepts cross-version drift risk in exchange for enforceable isolation and independently-installable subsets. The gates in this document bound the risk to same-minor drift only.

## Scope

This policy applies only to the three `augur-*` workspace packages. Optional-dependency extras on each package (`llm-local`, `llm-cloud`, `bus-nats`, etc.) follow each wrapped SDK's own versioning conventions and are not gated here.
6 changes: 5 additions & 1 deletion src/augur_format/augur_format/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@

from __future__ import annotations

__version__ = "0.0.0"
from augur_format._compat import check_compatibility

__version__ = "0.1.0"

check_compatibility()
82 changes: 82 additions & 0 deletions src/augur_format/augur_format/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
"""Cross-package compatibility gate for `augur-format`.

Runs at package import time and fails loud when the resolved
`augur-signals` version or its schema contract sits outside the
range this `augur-format` build was compiled against.

See `docs/contracts/cross-package-compatibility.md` for the policy.
"""

from __future__ import annotations

from importlib.metadata import PackageNotFoundError, version
from typing import Final

# Compatibility window on augur-signals. Change these in lock step
# with the `"augur-signals ~=X.Y.0"` specifier in pyproject.toml and
# with the MODELS_SCHEMA_VERSION expected below.
_SIGNALS_MIN: Final[str] = "0.1.0"
_SIGNALS_LT: Final[str] = "0.2.0"

_EXPECTED_MODELS_SCHEMA_MAJOR: Final[str] = "1"


class IncompatibleAugurSignalsError(ImportError):
"""Raised when the resolved `augur-signals` violates the compat range."""


def _parse_version(v: str) -> tuple[int, int, int]:
core = v.split("+", maxsplit=1)[0].split("-", maxsplit=1)[0]
parts = core.split(".")
if len(parts) < 3:
parts = [*parts, *(["0"] * (3 - len(parts)))]
try:
return (
int(parts[0]),
int(parts[1]),
int(
parts[2].split("rc")[0].split("a")[0].split("b")[0].split("dev")[0].split("post")[0]
or "0"
),
)
except ValueError as exc:
raise IncompatibleAugurSignalsError(f"Unparseable augur-signals version: {v!r}") from exc


def _require_range(installed: str) -> None:
got = _parse_version(installed)
lo = _parse_version(_SIGNALS_MIN)
hi = _parse_version(_SIGNALS_LT)
if not (lo <= got < hi):
raise IncompatibleAugurSignalsError(
f"augur-format requires augur-signals >={_SIGNALS_MIN},<{_SIGNALS_LT}; "
f"found {installed}. Align the package versions (same minor) before "
"running. See docs/contracts/cross-package-compatibility.md."
)


def _require_schema_major() -> None:
from augur_signals.models import MODELS_SCHEMA_VERSION

observed_major = MODELS_SCHEMA_VERSION.split(".", maxsplit=1)[0]
if observed_major != _EXPECTED_MODELS_SCHEMA_MAJOR:
raise IncompatibleAugurSignalsError(
f"augur-format expects augur-signals MODELS_SCHEMA_VERSION major "
f"{_EXPECTED_MODELS_SCHEMA_MAJOR}, got "
f"{MODELS_SCHEMA_VERSION}. This is a cross-package schema "
"mismatch — upgrade augur-format or downgrade augur-signals."
)


def check_compatibility() -> None:
"""Run both gates; raises `IncompatibleAugurSignalsError` on mismatch."""
try:
installed = version("augur-signals")
except PackageNotFoundError as exc:
raise IncompatibleAugurSignalsError(
"augur-signals is not installed but is a required dependency "
"of augur-format. Install it via `uv sync` or "
"`pip install augur-signals~=0.1.0`."
) from exc
_require_range(installed)
_require_schema_major()
2 changes: 1 addition & 1 deletion src/augur_format/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ dependencies = [
"jinja2>=3.1",
"httpx>=0.27",
"websockets>=13.0",
"augur-signals",
"augur-signals ~= 0.1.0",
]

[project.optional-dependencies]
Expand Down
6 changes: 5 additions & 1 deletion src/augur_labels/augur_labels/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,8 @@

from __future__ import annotations

__version__ = "0.0.0"
from augur_labels._compat import check_compatibility

__version__ = "0.1.0"

check_compatibility()
86 changes: 86 additions & 0 deletions src/augur_labels/augur_labels/_compat.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Cross-package compatibility gate for `augur-labels`.

Runs at package import time and fails loud when the resolved
`augur-signals` version or its schema contract sits outside the
range this `augur-labels` build was compiled against.

See `docs/contracts/cross-package-compatibility.md` for the policy.
"""

from __future__ import annotations

from importlib.metadata import PackageNotFoundError, version
from typing import Final

# Compatibility window on augur-signals. Change these in lock step
# with the `"augur-signals ~=X.Y.0"` specifier in pyproject.toml and
# with the MODELS_SCHEMA_VERSION expected below.
_SIGNALS_MIN: Final[str] = "0.1.0"
_SIGNALS_LT: Final[str] = "0.2.0"

# Schema-contract major version. Bumping the augur-signals models' major
# schema_version bumps this; augur-labels must be re-released with the
# matching expected value.
_EXPECTED_MODELS_SCHEMA_MAJOR: Final[str] = "1"


class IncompatibleAugurSignalsError(ImportError):
"""Raised when the resolved `augur-signals` violates the compat range."""


def _parse_version(v: str) -> tuple[int, int, int]:
# Strip PEP 440 suffixes (post/rc/dev); accept "X.Y.Z" prefix.
core = v.split("+", maxsplit=1)[0].split("-", maxsplit=1)[0]
parts = core.split(".")
if len(parts) < 3:
parts = [*parts, *(["0"] * (3 - len(parts)))]
try:
return (
int(parts[0]),
int(parts[1]),
int(
parts[2].split("rc")[0].split("a")[0].split("b")[0].split("dev")[0].split("post")[0]
or "0"
),
)
except ValueError as exc:
raise IncompatibleAugurSignalsError(f"Unparseable augur-signals version: {v!r}") from exc


def _require_range(installed: str) -> None:
got = _parse_version(installed)
lo = _parse_version(_SIGNALS_MIN)
hi = _parse_version(_SIGNALS_LT)
if not (lo <= got < hi):
raise IncompatibleAugurSignalsError(
f"augur-labels requires augur-signals >={_SIGNALS_MIN},<{_SIGNALS_LT}; "
f"found {installed}. Align the package versions (same minor) before "
"running. See docs/contracts/cross-package-compatibility.md."
)


def _require_schema_major() -> None:
from augur_signals.models import MODELS_SCHEMA_VERSION

observed_major = MODELS_SCHEMA_VERSION.split(".", maxsplit=1)[0]
if observed_major != _EXPECTED_MODELS_SCHEMA_MAJOR:
raise IncompatibleAugurSignalsError(
f"augur-labels expects augur-signals MODELS_SCHEMA_VERSION major "
f"{_EXPECTED_MODELS_SCHEMA_MAJOR}, got "
f"{MODELS_SCHEMA_VERSION}. This is a cross-package schema "
"mismatch — upgrade augur-labels or downgrade augur-signals."
)


def check_compatibility() -> None:
"""Run both gates; raises `IncompatibleAugurSignalsError` on mismatch."""
try:
installed = version("augur-signals")
except PackageNotFoundError as exc:
raise IncompatibleAugurSignalsError(
"augur-signals is not installed but is a required dependency "
"of augur-labels. Install it via `uv sync` or "
"`pip install augur-signals~=0.1.0`."
) from exc
_require_range(installed)
_require_schema_major()
2 changes: 1 addition & 1 deletion src/augur_labels/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ dependencies = [
"httpx>=0.27",
"click>=8.1",
"filelock>=3.15",
"augur-signals",
"augur-signals ~= 0.1.0",
]

[build-system]
Expand Down
10 changes: 10 additions & 0 deletions src/augur_signals/augur_signals/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,17 @@
Schemas are authoritative in docs/contracts/schema-and-versioning.md.
Every exported model sets schema_version to "1.0.0"; major-version
bumps follow the versioning policy in that document.

MODELS_SCHEMA_VERSION is the single source of truth for the major
schema contract. Dependent packages (augur-labels, augur-format)
assert compatibility against it at import time — see
docs/contracts/cross-package-compatibility.md.
"""

from __future__ import annotations

from typing import Final

from augur_signals.models._identifiers import new_signal_id
from augur_signals.models.context import RelatedMarketState, SignalContext
from augur_signals.models.enums import (
Expand All @@ -19,7 +26,10 @@
from augur_signals.models.signal import MarketSignal
from augur_signals.models.snapshot import MarketSnapshot

MODELS_SCHEMA_VERSION: Final[str] = "1.0.0"

__all__ = [
"MODELS_SCHEMA_VERSION",
"ConsumerType",
"FeatureVector",
"InterpretationMode",
Expand Down
Loading
Loading