Metadata-Version: 2.4
Name: voussoir
Version: 0.5.1
Summary: Hierarchical test artifact framework — 10 layers, pluggable naming charts, full trace lineage
Project-URL: Repository, https://github.com/acidblock/voussoir
Project-URL: Homepage, https://github.com/acidblock/voussoir
Author: William Baldwin
License-Expression: Apache-2.0
License-File: LICENSE
Keywords: observability,quality-engineering,testing,traceability
Classifier: Development Status :: 3 - Alpha
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Testing
Classifier: Topic :: System :: Monitoring
Requires-Python: >=3.11
Provides-Extra: dev
Requires-Dist: allure-pytest>=2.13; extra == 'dev'
Requires-Dist: cryptography>=42; extra == 'dev'
Requires-Dist: factory-boy>=3.3; extra == 'dev'
Requires-Dist: faker>=22.0; extra == 'dev'
Requires-Dist: hypothesis>=6.0; extra == 'dev'
Requires-Dist: jsonschema>=4.0; extra == 'dev'
Requires-Dist: locust>=2.20; extra == 'dev'
Requires-Dist: mcp>=1.0; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: opentelemetry-api>=1.20; extra == 'dev'
Requires-Dist: opentelemetry-exporter-otlp>=1.20; extra == 'dev'
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
Requires-Dist: pytest>=8.0; extra == 'dev'
Requires-Dist: ruff>=0.4; extra == 'dev'
Requires-Dist: textual>=0.60; extra == 'dev'
Provides-Extra: harness
Requires-Dist: factory-boy>=3.3; extra == 'harness'
Requires-Dist: faker>=22.0; extra == 'harness'
Requires-Dist: locust>=2.20; extra == 'harness'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Provides-Extra: observe
Requires-Dist: opentelemetry-api>=1.20; extra == 'observe'
Requires-Dist: opentelemetry-exporter-otlp>=1.20; extra == 'observe'
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'observe'
Provides-Extra: sign
Requires-Dist: cryptography>=42; extra == 'sign'
Provides-Extra: test
Requires-Dist: allure-pytest>=2.13; extra == 'test'
Requires-Dist: hypothesis>=6.0; extra == 'test'
Requires-Dist: jsonschema>=4.0; extra == 'test'
Requires-Dist: pytest-xdist>=3.5; extra == 'test'
Requires-Dist: pytest>=8.0; extra == 'test'
Provides-Extra: tier1
Requires-Dist: allure-pytest>=2.13; extra == 'tier1'
Requires-Dist: cryptography>=42; extra == 'tier1'
Requires-Dist: factory-boy>=3.3; extra == 'tier1'
Requires-Dist: faker>=22.0; extra == 'tier1'
Requires-Dist: hypothesis>=6.0; extra == 'tier1'
Requires-Dist: jsonschema>=4.0; extra == 'tier1'
Requires-Dist: locust>=2.20; extra == 'tier1'
Requires-Dist: mcp>=1.0; extra == 'tier1'
Requires-Dist: opentelemetry-api>=1.20; extra == 'tier1'
Requires-Dist: opentelemetry-exporter-otlp>=1.20; extra == 'tier1'
Requires-Dist: opentelemetry-sdk>=1.20; extra == 'tier1'
Requires-Dist: pytest-xdist>=3.5; extra == 'tier1'
Requires-Dist: pytest>=8.0; extra == 'tier1'
Requires-Dist: textual>=0.60; extra == 'tier1'
Provides-Extra: tui
Requires-Dist: textual>=0.60; extra == 'tui'
Description-Content-Type: text/markdown

# Voussoir

Hierarchical test-artifact framework — **10 layers, pluggable naming charts, full trace lineage**.

Named for the wedge-shaped stones forming a self-supporting arch. Each
voussoir is shaped by its neighbors; the arch holds only when all stones
are fully placed — each a keystone.

Voussoir models test artifacts as a hierarchy whose structure carries the weight. The layer
topology is the invariant; everything else — the vocabulary you read it in,
the depth you nest it to — is surface. **Tier 0 means `import voussoir` pulls
in zero third-party packages**; the optional extras add pytest, load tooling,
OpenTelemetry, and the interactive TUI.

## Getting Started

New here? You don't need to install anything to explore Voussoir — clone the
repo and run the guided tour. It's Tier 0, so the manual runs from a bare
checkout with zero third-party packages. (The `demo/` tutorial is
clone-and-run; `pip install voussoir` ships only the importable package.)

```bash
git clone https://github.com/acidblock/voussoir.git
cd voussoir
python demo/voussoir_man.py --tour   # 1. the guided tour (non-interactive)
python demo/voussoir_man.py          # 2. the interactive  voussoir-man>  REPL
```

The **tour** walks the 10 layers, re-renders them under every shipped chart
(`testing`, `time`, `space`, `observability`, `optics`, `voussoir`) so you watch one
arch wear different vocabularies, then shows the plugin map, a failing
trace-lineage arch, and the keystone principles — top to bottom, nothing to
type. The **REPL** is the same content on demand: type `help`, then try
`layers`, `conv space`, `conv voussoir`, `trace`, `all`, `q`.

Then **use it in your own code** — `pip install voussoir` (Tier 0, zero deps)
and head to [Quick start](#quick-start), or
[Migrating an existing pytest suite](docs/migration.md) to adopt it
incrementally.

## Install

```bash
pip install voussoir            # Tier 0 — pure data model, zero deps
pip install "voussoir[tier1]"   # + pytest, locust, OpenTelemetry
pip install "voussoir[tui]"     # + the Textual explorer (voussoir-tui)
```

| Extra | Pulls in | For |
|-------|----------|-----|
| _(none)_ | nothing | the pure data model, trace lineage, report generators |
| `test` | pytest, pytest-xdist, allure-pytest, hypothesis | running tests + the pytest plugin |
| `harness` | locust, faker, factory-boy | load / data generation |
| `observe` | opentelemetry-{api,sdk,exporter-otlp} | span export |
| `tui` | textual | the `voussoir-tui` explorer |
| `sign` | cryptography | Ed25519 signed heads (HMAC signing is dependency-free) |
| `dev` | all of the above + ruff, mypy, pytest-cov | development |

Latest development tree:
`pip install "voussoir @ git+https://github.com/acidblock/voussoir.git"`
(or `pip install -e /path/to/voussoir` from a local checkout).

## Quick start

```python
from voussoir import (
    Environment, Scenario, Suite, Experiment, Harness, HarnessMode,
    Test, Condition, Validation, Fixture, Monitor, Status,
    TraceLineage, CONVENTIONS,
)

# Compose a hierarchy (leaf up)
val = Validation(name="status_200", expected=200, actual=200, status=Status.PASSED)
test = Test(name="health_check", validations=[val])
harness = Harness(name="smoke", mode=HarnessMode.SMOKE, tests=[test])
# ... Experiment -> Suite -> Scenario -> Environment

# Trace everything — the trace_id propagates down the chain
root = TraceLineage(layer="L0:Environment", name="staging")
child = root.child("L1:Scenario", "login_flow")
assert child.trace_id == root.trace_id
```

## The 10 layers

A spine `Environment → Scenario → Suite → Experiment → Harness → Test`
(L0–L4) with four branch layers attached to each `Test`: `Condition` (L4a,
ambient), `Validation` (L4b, judgment), `Fixture` (L4c, material), `Monitor`
(L4d, observer).

A **naming chart** relabels those 10 roles without changing the graph or the
trace ids — that invariance *is* the point. The shipped charts are examples,
not a fixed set (`testing` is the default):

| Layer | testing | time | space | observability | optics |
|-------|---------|------|-------|---------------|--------|
| L0 | Environment | Epoch | Universe | Pipeline | Scene |
| L1 | Scenario | Era | Galaxy | Stream | Aspect |
| L2 | Suite | Period | System | Probe | Exposure |
| L3 | Experiment | Phase | Orbit | Signal | Reflection |
| L3.5 | Harness | Cadence | Trajectory | Injector | Mirror |
| L4 | Test | Moment | Point | Sample | Ray |
| L4a | Condition | Window | Field | Pressure | Medium |
| L4b | Validation | Deadline | Fix | Assertion | Resolution |
| L4c | Fixture | Timestamp | Landmark | Payload | Object |
| L4d | Monitor | Clock | Sensor | Collector | Detector |

Plus one deliberately *meta* chart — `voussoir` (self-referential: the
framework testing its own layers) — and **[bring your
own](docs/naming-conventions.md#bring-your-own-convention)**. None is
privileged; the integrity lives in the topology, not the names.

## Recursion — every keystone has layers

The spine is fixed-depth, but the leaf is not the end: a `Test` may carry
`sub_environments`, a full nested campaign. Examined closely, the atomic
stone is itself an arch — the hierarchy is **scale-invariant**.

```python
nested = Environment(name="payments_subsystem", scenarios=[...])
leaf = Test(name="charge_card", validations=[ok], sub_environments=[nested])
```

Nesting is **additive** (defaults empty → existing trees are byte-identical)
and bears load at every scale — nothing hides across the boundary: a failure
deep in a sub-arch makes the outer `replay()` FAILED, `describe()` renders the
nested arch, and `verify_lineage(spans, single_trace=True)` confirms the
sub-arch is wired into one connected campaign. See
[`docs/layers.md`](docs/layers.md#recursion--every-keystone-has-layers) and
[ADR-001](docs/adr/001-recursion.md).

## Trace lineage & the Layer-3 guardrail

Every artifact carries a `TraceLineage`: a `trace_id` (one per campaign,
propagated unchanged to all descendants — OpenTelemetry `traceId` semantics),
a per-node `span_id`, and a `parent_span_id` linking to its parent. A failure
at any layer can be reported with the full chain that produced it.

The **guardrail** (`voussoir.verify_lineage`, Tier 0) checks a recorded span
set and returns violations:

| Kind | Meaning |
|------|---------|
| `orphan-span` | a span whose `parent_span_id` was never recorded |
| `trace-divergence` | a child whose `trace_id` differs from its parent's |
| `duplicate-span` | the same `span_id` recorded more than once |
| `disconnected-subtrace` | _(under `single_trace=True`)_ a span not part of the campaign's one connected trace |

`single_trace=True` is a **per-campaign** assertion — use it when a span set
is meant to be one connected lineage (e.g. a nested arch). It is *not* a
session-wide switch: a pytest session legitimately holds many traces (one per
test), so applying it globally would mis-fire. See
[`docs/trace-lineage.md`](docs/trace-lineage.md) for the full explanation.

### In a pytest suite

Add the fixtures to your `conftest.py`:

```python
from voussoir.conftest_base import *  # noqa: F401, F403
```

Every test then gets `voussoir_trace`, `test_trace`, and `make_condition` /
`make_validation` / `make_fixture` / `make_monitor` / `make_test`. The
package also registers a `pytest11` plugin, so the guardrail's
`lineage_registry` fixture is available with no wiring:

```python
def test_login(lineage_registry, test_trace):
    lineage_registry.record(test_trace)
    ...
```

Run with `--voussoir-strict-lineage` (or `voussoir_strict_lineage = true` in
pytest config) and a broken chain **fails the session**; without it, the same
violations are an advisory summary.

## Interactive manual — beyond the tour

[Getting Started](#getting-started) covers the guided tour and the REPL.
Beyond those:

```bash
python demo/voussoir_man.py --markdown # emit the manual as markdown
python demo/voussoir_man.py --no-color # plain output (no ANSI)
voussoir-tui                           # the Textual explorer  (needs the [tui] extra)
bash demo/voussoir_canvas.sh           # open the manual in a claude-canvas document
python -m voussoir.readiness           # deployment-readiness matrix + coverage map
```

The manual's content is structured data in
[`voussoir.manual`](VOUSSOIR_INTERFACE.md#315-manual-content--voussoirmanual-module-import-only)
(Tier 0) — both the CLI man-page and the TUI render the same source, and its
sample-hierarchy builders (`build_sample_lineage`, `build_self_test_lineage`)
are importable for your own demos and tests.
[`voussoir.readiness`](VOUSSOIR_INTERFACE.md#316-readiness-catalog--voussoirreadiness-module-import-only)
is the companion catalog: every practical executor / source / sink
integration point (`CATALOG`), what it costs to stand up (`Readiness` tiers),
and what trace artifact it emits.

`voussoir-tui` is the one *installed* entry point (via the `[tui]` extra); the
`demo/` scripts run from a clone. The TUI re-renders the layer table and
trace arch as you switch charts (sidebar or number keys), and maps Voussoir
onto the 5-layer Claude Code extensibility stack (Memory → Skills → Hooks →
Subagents → Plugins).

```text
┌ Voussoir ─────────────────── convention: voussoir ─┐
│ Conventions │ ▏Layers▕  Trace   Plugins   Princ.   │
│  Testing    │  Layer  Role     Name                 │
│  Time       │  L0     root     Framework            │
│  Space      │  L1     scope    Capability           │
│  Observ.    │  L2     group    Module               │
│ ▶ Voussoir  │  L3     intent   TestFile             │
│             │  L4     leaf     Case                 │
└─────────────┴──────────────────────────────────────┘
 q Quit  1 Testing  2 Time  3 Space  4 Observ.  5 Voussoir
```

(Rendered screenshot: [`demo/voussoir_tui.svg`](demo/voussoir_tui.svg).)

## Examples

Runnable, Tier-0 scripts in [`examples/`](examples/) — each exits `0` and is
kept green by `tests/test_examples.py`:

| Script | Shows |
|--------|-------|
| `build_hierarchy.py` | the full L0→L4 tree, `describe()` + `replay()` |
| `charts.py` | one topology rendered under every shipped chart |
| `lineage_guardrail.py` | `verify_lineage()` clean vs. a broken chain |
| `recursion.py` | a nested sub-arch; failure surfaces at the outer root |
| `cross_validation.py` | Voussoir as the neutral instrument validating external subjects under bring-your-own charts |

## Docs

- [`VOUSSOIR_INTERFACE.md`](VOUSSOIR_INTERFACE.md) — the full interface reference: Python API, CLI, pytest plugins, MCP tools, wire formats
- [`docs/layers.md`](docs/layers.md) — each layer's role + recursion
- [`docs/naming-conventions.md`](docs/naming-conventions.md) — charts (the examples) + bring-your-own
- [`docs/trace-lineage.md`](docs/trace-lineage.md) — propagation, the guardrail, and `single_trace`
- [`docs/migration.md`](docs/migration.md) — adopting Voussoir in an existing pytest suite
- [`docs/cross-validation.md`](docs/cross-validation.md) — Voussoir as a neutral instrument validating external subjects
- [`docs/constraints.md`](docs/constraints.md) — the standing-constraint registry (enforced invariants)
- [`docs/schema-versioning.md`](docs/schema-versioning.md) — what bumps the interchange schemas' version (and what doesn't)
- [`docs/adr/`](docs/adr/) — architecture decisions (ADR-001: recursion)

## Project layout & quality

```
voussoir/      the package (model, trace/, lineage, reporting, pytest_plugin, …)
tests/         the suite (~1,130 tests)
examples/      runnable Tier-0 showcases
docs/          layers / naming / trace-lineage / migration / adr
demo/          the interactive manual (CLI + Textual TUI + canvas wrapper)
plugin/        the Claude Code plugin (marketplace, /voussoir command, validator agent, voussoir-naming skill, MCP server)
scripts/       dev tooling — git hooks (pre-push main-guard) + installer
```

CI (GitHub Actions) gates `main` on three checks: the test suite (Python
3.11 / 3.12 / 3.13), `ruff` lint + format, and a **Tier-0 import** job that
installs the package with *no* extras and proves `import voussoir` works.

## License

Apache-2.0. See [`LICENSE`](LICENSE).
